GD32VF103 RISC-V CAN回环自测:从时钟到过滤器全链路验证

发布时间:2026/10/20 20:50:41
GD32VF103 RISC-V CAN回环自测:从时钟到过滤器全链路验证
简介这份资源是面向嵌入式开发者的GD32VF103 CAN通信实践工程包基于RISC-V架构帮助读者在NucleiStudio中完成CAN发送功能的开发与验证。内容围绕CAN总线基础、GD32VF103的CAN模块配置、波特率与工作模式设置、发送函数编写、接收中断处理及错误检测等环节展开适合已具备一定单片机基础、希望切入RISC-V生态或汽车电子、工业自动化通信场景的开发者参考。压缩包共100个文件以C源码与头文件为主体辅以Makefile、链接脚本、启动汇编、工程配置与映射文件整体约248KB结构完整可直接导入NucleiStudio。目前已有1902人学习下载。通过阅读与调试该工程读者可掌握CAN标准帧与扩展帧的收发流程、中断与错误处理思路并借助串口或LED观察通信状态为后续在RISC-V平台上开发CAN应用打下实践基础。1. 拿到 gd32vf_can_test0.zip 先别急着烧录RISC-V GD32VF103 的 CAN 回环到底能验证什么如果你手头有一块 GD32VF103 开发板想验证 CAN 外设能不能跑通又不想先焊收发器、拉总线、接终端电阻那gd32vf_can_test0.zip这类工程就是最省事的起点。它跑在 RISC-V 内核的 GD32VF103 上核心做的是 CAN 回环Loopback自测MCU 自己发、自己收不依赖外部 CAN 节点用来确认时钟、引脚复用、波特率、过滤器、中断这一整条链路配置是否正确。适合刚接触 GD32VF103 CAN 的嵌入式工程师也适合从 STM32 转过来、想确认 RISC-V 工具链和固件库差异的人。它解决的不是“总线通信”问题而是“我的 CAN 初始化到底有没有写对”这个更前置的问题。2. 回环模式下的 CAN 初始化链路从时钟到过滤器2.1 为什么先用回环模式而不是直接上总线CAN 总线调试有个很现实的麻烦你发一帧出去如果对面没有节点应答控制器会一直重发最后进错误被动甚至总线关闭。新手很容易把“没接对端”误判成“代码写错了”。回环模式把 TX 和 RX 在芯片内部短接发送的帧自己收回来ACK 由自己提供这样就能把“配置问题”和“总线问题”彻底分开。GD32VF103 的 CAN 控制器支持几种测试模式回环模式Loopback是其中最常用的一种。它的行为是发送端把帧送到内部接收路径同时 TX 引脚仍然输出但接收不依赖外部 RX 电平。常见做法是先在回环下把波特率、过滤器、中断全部调通再切到正常模式接收发器。这里有个容易忽略的点回环模式下如果过滤器没配对你发了帧但收不到现象和“没发出去”一模一样。所以过滤器配置必须和发送的标识符严格对应这是后面避坑章节要重点说的。2.2 时钟与引脚GD32VF103 的 CAN 挂在 APB1 上GD32VF103 的 CAN0 挂在 APB1 总线上配置前必须先开时钟。RISC-V 版本的固件库和 STM32 标准库风格接近但函数名和宏定义有差异直接照搬 STM32 代码会编译不过。/* 使能 CAN0 和 GPIO 时钟CAN0 在 APB1GPIO 在 APB2 */ rcu_periph_clock_enable(RCU_CAN0); rcu_periph_clock_enable(RCU_GPIOB); /* GD32VF103 的 CAN 默认引脚是 PB8(RX) / PB9(TX)需重映射确认 */ gpio_init(GPIOB, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_8); gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);逻辑说明rcu_periph_clock_enable是 GD32 固件库的时钟使能函数和 STM32 的RCC_APB1PeriphClockCmd对应但名字不同。PB8 配成上拉输入CAN_RXPB9 配成复用推挽CAN_TX这是 GD32VF103 数据手册里 CAN0 的默认复用位置。参数上GPIO_OSPEED_50MHZ对 CAN 这种最高 1Mbps 的场景足够不需要更高。注意有些板子把 CAN 引脚引到了 PA11/PA12 并做了重映射这时候要额外调用引脚重映射配置否则波形出不来。先查原理图别默认 PB8/PB9。2.3 波特率计算一个参数错就全盘收不到CAN 波特率由 APB1 时钟、预分频、BS1、BS2、同步跳转宽度共同决定。GD32VF103 常见主频 108MHzAPB1 通常 54MHz。假设要 500kbps/* CAN 初始化结构体500kbps APB154MHz */ can_parameter_struct can_init_struct; can_struct_para_init(can_init_struct); can_init_struct.baud_rate_prescaler 6; /* 54MHz / 6 9MHz */ can_init_struct.time_segment_1 CAN_BT_BS1_8TP; can_init_struct.time_segment_2 CAN_BT_BS2_3TP; can_init_struct.resync_jump_width CAN_BT_SJW_1TP; can_init_struct.working_mode CAN_LOOPBACK_MODE; can_init_struct.auto_bus_off_recovery ENABLE; can_init_struct.auto_retrans ENABLE; can_init_struct.auto_wake_up DISABLE; can_init(CAN0, can_init_struct);逻辑说明波特率 时钟 / (预分频 × (1 BS1 BS2))。这里 54MHz / (6 × (183)) 54M / 72 750k不对。要 500k 得让 (1BS1BS2) × 预分频 108。常见组合是预分频 6、BS18、BS23 得到 72再调预分频到 9 得 54M/(9×12)500k。参数必须自己算一遍别抄。采样点位置由 BS1 和 BS2 比例决定一般建议采样点在 75% 到 87.5% 之间。BS18、BS23 时采样点在 (18)/(183)75%属于可用范围。如果通信不稳优先调 BS2 把采样点往 80% 挪。2.4 过滤器配置回环收不到帧的头号原因CAN 过滤器决定哪些帧能进接收 FIFO。回环模式下如果过滤器全关或掩码写错发送成功但接收中断永远不触发。/* 配置过滤器 0接收所有帧进 FIFO0 */ can_filter_parameter_struct filter_struct; can_struct_para_init(filter_struct); filter_struct.filter_number 0; filter_struct.filter_mode CAN_FILTERMODE_MASK; filter_struct.filter_bits CAN_FILTERBITS_32BIT; filter_struct.filter_list_high 0x0000; filter_struct.filter_list_low 0x0000; filter_struct.filter_mask_high 0x0000; filter_struct.filter_mask_low 0x0000; filter_struct.filter_fifo_number CAN_FIFO0; filter_struct.filter_enable ENABLE; can_filter_init(filter_struct);逻辑说明掩码模式下filter_list是期望值filter_mask是哪些位参与比较。全 0 掩码意味着所有位都不比较即接收全部帧。这是回环自测最省事的配法。参数CAN_FILTERBITS_32BIT表示用 32 位宽过滤器对应扩展帧格式如果只玩标准帧也可以用 16 位模式但 32 位全 0 掩码同样能收标准帧。注意过滤器编号不能重复多个过滤器要分配不同filter_number。FIFO0 和 FIFO1 是独立的中断里要读对应的 FIFO。3. 发送与接收中断把回环数据真正读出来3.1 发送一帧标准数据帧初始化完成后发送就是填结构体、选邮箱、等发送完成。/* 发送一帧标准数据帧ID0x123数据 8 字节 */ can_trasnmit_message_struct tx_msg; uint8_t data[8] {1,2,3,4,5,6,7,8}; tx_msg.tx_sfid 0x123; /* 标准帧标识符 */ tx_msg.tx_efid 0; tx_msg.tx_ft CAN_FT_DATA; /* 数据帧 */ tx_msg.tx_ff CAN_FF_STANDARD; /* 标准帧 */ tx_msg.tx_dlen 8; for (int i 0; i 8; i) { tx_msg.tx_data[i] data[i]; } uint8_t mailbox can_message_transmit(CAN0, tx_msg); while (can_transmit_states(CAN0, mailbox) ! CAN_TRANSMIT_OK) { /* 等待发送完成实际项目里建议加超时 */ }逻辑说明can_message_transmit返回使用的邮箱号0/1/2can_transmit_states查询该邮箱状态。回环模式下发送完成很快但正常模式如果没节点应答这里会卡住所以实际代码要加超时计数。参数tx_sfid是标准帧 ID11 位有效tx_ff选CAN_FF_STANDARD或CAN_FF_EXTENDED选错帧类型过滤器可能匹配不上。3.2 接收中断与 FIFO 读取回环的帧会进 FIFO0触发接收中断。中断里要判断 FIFO 号并读走数据否则 FIFO 满了后续帧会丢。/* CAN0 接收中断服务函数 */ void CAN0_RX0_IRQHandler(void) { if (can_interrupt_flag_get(CAN0, CAN_INT_FLAG_RFL0) SET) { can_receive_message_struct rx_msg; can_message_receive(CAN0, CAN_FIFO0, rx_msg); /* rx_msg.rx_sfid 应为 0x123rx_msg.rx_dlen 应为 8 */ can_interrupt_flag_clear(CAN0, CAN_INT_FLAG_RFL0); } }逻辑说明CAN_INT_FLAG_RFL0表示 FIFO0 非空。can_message_receive会把帧从 FIFO 弹出并填充rx_msg。读完后必须清标志否则中断会反复进。参数上CAN_FIFO0要和过滤器里配的filter_fifo_number一致配错就永远进不了这个分支。注意中断标志清除要在读取之后顺序反了可能丢帧。另外 NVIC 里要开CAN0_RX0_IRQn只配外设不开中断向量中断不会进。3.3 用调试器验证回环是否真的通了代码跑起来后别只看“没报错”就认为通了。最直接的验证是在接收中断里打个断点看rx_msg.rx_sfid和rx_msg.rx_data是否和发送一致。如果断点不进先查过滤器如果进了但数据不对查发送结构体填充顺序。另一个办法是让接收中断翻转一个 GPIO用示波器看波形。每收一帧翻转一次能直观看到帧率和是否丢帧。这个技巧在调波特率和采样点时特别有用比串口打印更实时。4. 避坑与排查回环自测最常见的五个翻车点4.1 发送成功但接收中断不触发现象can_message_transmit返回了邮箱号发送状态也变 OK但接收中断死活不进。原因九成是过滤器没配对。回环模式下帧确实发出去了但过滤器掩码或 FIFO 号配错帧被丢弃FIFO 始终为空。解决先把过滤器配成 32 位掩码全 0、FIFO0、使能确认能收到所有帧再逐步收紧掩码。同时确认filter_fifo_number和中断里读的 FIFO 一致。4.2 波特率算错导致回环都收不到现象回环模式下发送完成但接收数据错乱或干脆收不到。原因回环模式虽然不依赖外部总线但内部位时序仍然按波特率参数运行。预分频或 BS1/BS2 算错采样点偏移过大自己收自己的帧也会出错。解决按 APB1 实际时钟重新算一遍。用示波器量 TX 引脚一位时间应该等于 1/波特率。500kbps 时一位 2 微秒量出来不对就回去改预分频。4.3 中断进了但读出来全是 0现象接收中断能进但rx_msg.rx_data全是 0或者rx_sfid不对。原因发送结构体没初始化干净或者can_struct_para_init没调用结构体里有随机值。另一个可能是发送和接收用了不同的帧格式标准/扩展导致 ID 解析错位。解决发送前对can_trasnmit_message_struct做完整赋值别只填部分字段。确认tx_ff和过滤器位宽匹配32 位过滤器配标准帧时ID 在高位解析方式不同。4.4 忘记开 NVIC 或中断优先级配错现象外设寄存器里接收中断标志置位了但 CPU 不进中断函数。原因只调了can_interrupt_enable开外设中断没在 NVIC 里使能对应的 IRQ 通道。或者优先级分组没配中断被屏蔽。解决确认nvic_irq_enable(CAN0_RX0_IRQn, 优先级, 子优先级)被调用。GD32VF103 的 RISC-V 中断控制器和 Cortex-M 的 NVIC 操作方式不同要确认用的是 RISC-V 版本的 API。4.5 从回环切正常模式后通信失败现象回环一切正常接上收发器和总线后发不出去或收不到。原因回环模式不检查 ACK正常模式需要至少一个节点应答。如果总线上只有自己发送会一直重试直到总线关闭。另外收发器供电、终端电阻、CANH/CANL 接反都会导致失败。解决切正常模式前先确认总线上有第二个节点或者用分析仪模拟 ACK。量收发器 CANH/CANL 差分电压隐性约 0V显性约 2V。终端电阻两端各 120 欧只在一端接也行但反射会大。5. 进阶把回环工程改成可复用的 CAN 自测框架回环跑通只是第一步。真正省事的做法是把它改成一个自测框架上电后自动发若干帧、校验接收、统计丢帧用串口或 GPIO 输出结果。这样每次改时钟或换板子烧进去就能知道 CAN 链路是否正常。具体做法是定义一个发送-接收配对表循环发送并比对。下面是一个简化实现/* 自测发送 N 帧统计成功接收数 */ #define TEST_FRAMES 100 uint32_t rx_count 0; void can_selftest(void) { for (uint32_t i 0; i TEST_FRAMES; i) { can_trasnmit_message_struct tx; can_struct_para_init(tx); tx.tx_sfid 0x100 (i 0x7F); tx.tx_ff CAN_FF_STANDARD; tx.tx_ft CAN_FT_DATA; tx.tx_dlen 2; tx.tx_data[0] i 0xFF; tx.tx_data[1] (i 8) 0xFF; uint8_t mb can_message_transmit(CAN0, tx); uint32_t timeout 100000; while (can_transmit_states(CAN0, mb) ! CAN_TRANSMIT_OK timeout--) {} /* 等待接收中断累加 rx_count实际项目加延时或信号量 */ } /* rx_count 应接近 TEST_FRAMES差值即丢帧数 */ }逻辑说明每帧 ID 和数据都带序号接收端可以校验是否连续。timeout防止发送卡死。参数TEST_FRAMES按需调100 帧足够暴露过滤器或 FIFO 溢出问题。如果rx_count明显小于发送数优先查 FIFO 是否溢出、中断是否被更高优先级打断太久。验证方法上我习惯先跑回环自测再切正常模式用分析仪抓包对比。回环通过但正常模式失败问题一定在物理层或对端配置不用再回头怀疑 MCU 初始化。这个分界能省掉大量来回折腾。从那以后我每次调 CAN都强制先跑一遍回环自测确认过滤器、波特率、中断三件套没问题再动收发器和总线。希望帮到你。本文还有配套的精品资源点击获取