GD32 CAN总线开发实战:从位时序到过滤器配置与错误排查
发布时间:2026/10/3 15:35:22 锦皓数字建站

GD32这颗国产Cortex-M单片机这几年在项目里的出镜率是真的高尤其是车载、工控和机器人领域几乎绕不开它。而CAN总线作为最经典的工业现场通信方式之一两根差分线就能把几十个节点连在一起抗干扰和实时性又比普通串口强太多凡是跟车辆、设备通信沾边的项目基本都有它的身影。这篇我就拿自己的实际使用经验从GD32的CAN外设结构说起把位时序计算、引脚配置、过滤器、收发代码、错误排查完整过一遍手把手带你把GD32的CAN跑起来。1. 拆解GD32的CAN外设先看清手里有什么家伙1.1 时钟与引脚初学者翻车的第一站GD32F103系列和常见的F103类MCU在CAN外设上结构很接近都是挂载在APB1总线上的也就是说CAN外设的输入时钟来自APB1而不是主频。很多新手在这里踩坑主频跑了108MHz甚至更高但APB1如果分了频CAN的时钟可能只有36MHz甚至27MHz波特率算出来自然不对。引脚方面GD32F103的CAN0默认映射在PA11RX和PA12TX也可以通过重映射功能挪到PB8和PB9。如果你用的是CAN1引脚又是另外一组。我自己的习惯是规划原理图之前先翻一遍数据手册的引脚复用表把默认映射和重映射都查出来避免画完板子才发现引脚冲突那就真的欲哭无泪了。另外要注意时钟树里APB1分频系数和CAN波特率是强相关的。比如APB136MHz时CAN外设时钟就是36MHz如果APB1不小心配成了54MHz甚至超过36MHz的极限CAN外设可能直接工作异常。初始化之前先确认SystemCoreClock和RCU_CFG0里APB1的分频这一步花两分钟后面能省两小时。1.2 收发器与硬件电路设计不只是接两根线CAN控制器只是协议层物理层要接一个CAN收发器才能跑差分信号。常见的收发器有TJA1050、SN65HVD230、ISO1042等区别主要在于耐压、速率、是否隔离。做车载或工业场合我推荐直接选带隔离的模块或芯片毕竟共地干扰和浪涌可能烧片子。电路设计的几个要点终端电阻总线最远两端各接一个120欧姆电阻不是每个节点都接。低速、短距离的测试环境只接一个120欧姆也能跑但为了信号完整性最好按规范来。共模电感在CAN_H和CAN_L上串共模电感能有效抑制共模干扰这种设计在车规产品里几乎是标配。保护二极管TVS管并接在收发器总线侧防浪涌和静电。我见过不少批量产品因为省了TVS现场总出莫名其妙的通信故障。你的开发板如果是自己画的建议至少留出终端电阻焊盘和TVS管位置调试起来会非常方便。后面遇到通信不稳定先看一眼终端电阻有没有接对比翻代码快多了。1.3 过滤器与邮箱结构CAN外设的硬件加速器GD32的CAN模块提供了多个过滤器组每个过滤器组可以工作在列表模式或掩码模式作用是把总线上一堆报文精准筛掉只把你想收的报文送进接收FIFO。发送侧有3个发送邮箱接收侧有FIFO0和FIFO1两个接收FIFO每个FIFO深度为3。也就是说即使CPU暂时没来得及处理最多还能缓存几帧数据。理解这个结构很重要尤其后面聊到“中断接收还是DMA接收”时你会明白为什么FIFO本身已经减轻了实时性压力。2. 搞懂CAN协议代码之前先理解帧和电平2.1 显性电平与隐性电平物理层的二进制CAN总线用的是差分信号两条线上电平之差决定逻辑状态。显性电平对应逻辑0隐性电平对应逻辑1。收发器把控制器输出的TX信号转换成差分电压再把自己的差分接收结果送回RX引脚。关键点是显性电平会覆盖隐性电平。这意味着如果两个节点同时发送一个发显性、一个发隐性总线上最终呈现的是显性。这个特性是CAN总线仲裁机制的基础也解释了为什么CAN的错误处理能那么快——全是靠电平检测实现的。实际调试时用示波器看CAN_H和CAN_L之间的差分波形正常通信时能看到清晰的显性和隐性电平跳变。如果波形幅度很小或者只有一边在跳基本可以认定收发器或终端电阻有问题。2.2 数据帧结构一帧报文里到底有什么CAN 2.0A标准帧的结构从SOF帧起始开始后面跟仲裁段、控制段、数据段、CRC段、ACK段、EOF帧结束以及IFS帧间隔。很多初学者只看ID和Data但真正排查通信问题时CRC段和ACK段反而是重点。举个例子发送方在ACK段会释放总线接收方如果收到有效报文会在ACK槽发送显性位发送方才能确认报文被收到。如果没有接收节点或者接收节点校验失败发送方看到的ACK就是隐性电平这会直接触发发送错误计数器加8。数据段最多8个字节这也是CAN协议在设计时就定好的它不是用来传大文件的而是用来传实时性要求高的控制指令和状态信息。如果你需要传大块数据通常是在应用层做分包处理这也是很多工业协议比如J1939、CANopen做的事情。2.3 仲裁机制为什么多个节点同时发不受影响CAN的仲裁很有意思它是边发送边监听的。每个节点发一位就同时读回总线上的电平如果自己发隐性却读到显性说明有其他节点在发优先级更高的帧ID更小自己就立即停止发送变成接收者。这就解释了为什么ID越小优先级越高。你不需要像RS-485那样搞主从轮询协议CAN总线天然支持多主并发实时性远超传统串口总线。当然这也要求波特率一致、数据帧格式一致否则传输过程中就会出现帧错误或仲裁紊乱。2.4 错误帧与状态机总线如何自我修复错误帧是整个CAN协议里最容易被忽略又最关键的部分。错误帧由错误标志和错误界定符组成节点检测到错误就会立刻拉低总线宣告这帧数据作废其他节点也会同步丢掉这帧数据。每个节点内部有发送错误计数器和接收错误计数器数值不同节点状态也不同状态条件行为错误主动错误计数低于128可以主动发送错误标志错误被动错误计数在128到255之间只能发送被动错误标志总线关闭发送错误计数超过255自动断开总线不再参与通信这个状态机在实际项目里非常重要。如果你的设备在总线上频繁发错误帧就需要看错误计数器的变化趋势判断是物理层干扰、波特率不匹配还是发送逻辑错误一步一步排查。3. 位时序与波特率参数不是拍脑袋填的3.1 采样点一帧数据在哪个时刻被读取CAN协议把每一位时间分成若干最小时间量子TQ一个位时间由同步段、传播段、相位缓冲段1、相位缓冲段2组成。真正采样的时刻通常在相位缓冲段1和相位缓冲段2的分界点这个位置就是采样点。采样点的选择直接影响抗干扰能力。采样点太靠近位的开头信号还没稳定太靠近位的结尾又容易受到下一位跳变的影响。业内常用75%左右的采样点既能容忍一定的线缆延迟又能避开边缘毛刺。3.2 GD32位时序寄存器拆解GD32固件库中can_parameter_struct结构体里有prescaler、bs1、bs2这些字段它们对应硬件上的波特率预分频器和时间段寄存器。波特率的计算公式是CAN波特率 CAN时钟 / 预分频系数 / (1 BS1 BS2)这个公式里的1是同步段占用的TQ数是固定的。BS1和BS2各自的范围是1到16不同型号略有差异但它们的总和决定了每个位时间由多少个TQ组成。3.3 波特率计算实例500kbps怎么配出来假设APB136MHz目标波特率500kbps每位时间是2微秒。如果希望采样点是75%一个自然的选择是每个位时间用8个TQ同步段1个TQBS15个TQBS22个TQ采样点 (15)/8 75%此时每个TQ是250纳秒预分频系数等于36MHz乘以0.25微秒结果9。所以prescaler 9bs1 5bs2 2如果APB1换成54MHz同样的波特率需要重新算而不是照抄。很多移植代码跑不动的根源就在这里时钟树变了预分频全部变了。4. 从零初始化GD32 CAN完整配置流程4.1 开发环境准备Keil也好GCC也罢先把工程搞干净我习惯用Keil MDK配合J-Link烧录器来做GD32开发固件库从官网下载F10x标准外设库工程模板按功能模块拆分。如果你手上的芯片是GD32E230系列那就用对应的库整个流程大同小异。还有一个叫GD32 Embedded Builder的工具官方的IDE适合不喜欢折腾Keil破解的人。但不管用什么IDE核心都是三件事芯片头文件、启动文件、SystemInit函数。时钟初始化如果不对后面所有外设都是白搭。4.2 第一步使能时钟和GPIO复用首先使能GPIOA和CAN0的时钟然后把PA11配置成浮动输入RXPA12配置成复用推挽输出TX。这里我踩过一个坑GPIO速度必须配到50MHz以上否则高速波特率下波形会变形。void can_gpio_init(void) { rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_CAN0); gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_12); gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_11); }注意先使能时钟再初始化GPIO顺序反了可能在调试时出现寄存器读回不对的怪问题。4.3 第二步初始化CAN参数结构体GD32固件库的can_parameter_struct设计得很直观设置工作模式、波特率、采样点位置、自动唤醒、自动重传等参数。这里重点说两个time_triggered_mode一般关掉auto_bus_off_recovery建议打开否则总线关闭后设备不会自动恢复掉线了你还得人工复位。can_parameter_struct can_para; can_deinit(CAN0); can_para.time_triggered_mode DISABLE; can_para.auto_bus_off_recovery ENABLE; can_para.auto_wake_up ENABLE; can_para.auto_retransmit ENABLE; can_para.recv_fifo_overwrite DISABLE; can_para.prescaler 9; can_para.bs1 CAN_BT_BS1_5TQ; can_para.bs2 CAN_BT_BS2_2TQ; can_para.working_mode CAN_NORMAL_MODE; can_init(CAN0, can_para);调试阶段可以先把working_mode配成CAN_LOOPBACK_MODE自己发自己收不接外部总线也能验证代码流程。等基本跑通再切回正常模式。4.4 第三步配置过滤器过滤器是新手最容易懵的地方。先搞清楚你要用列表模式还是掩码模式列表模式只接收ID完全匹配的报文掩码模式ID的某些位固定匹配某些位忽略实际项目中我常用掩码模式把标准ID的高11位整体参与匹配这样就可以只收某个特定ID范围的报文其他全丢掉。这样做的好处是CPU不用处理无关中断实时性提升非常明显。can_filter_parameter_struct can_filter; can_filter.filter_list_high 0x0000; can_filter.filter_list_low 0x0000; can_filter.filter_mask_high 0x0000; can_filter.filter_mask_low 0x0000; can_filter.filter_fifo_number CAN_FIFO0; can_filter.filter_mode CAN_FILTERMODE_MASK; can_filter.filter_bits CAN_FILTERBITS_32BIT; can_filter.filter_enable ENABLE; can_filter_init(can_filter);这里全零掩码表示所有报文都进FIFO0适合前期调试。正式项目里再根据协议填具体的ID和掩码。有一点要记住过滤器配置不生效接收中断就一个都来不了但发送是正常的所以排查时先查过滤器。4.5 第四步中断配置还是DMA配置这是很多人纠结的问题。我的结论很简单波特率不超过1Mbps、报文频率不极端的情况下优先用中断接收代码清晰、调试方便。如果某个CAN通道的数据量极大比如每秒上千帧再考虑DMA搬运。CAN模块本身有FIFO缓存深度为3中断接收只要服务函数足够快基本不会丢帧。DMA的好处是减少CPU介入但配置复杂而且如果DMA描述符管理不当出现覆盖时反而更难排查。先中断跑通功能再优化性能这才是正确路径。5. 源码解析一收一发里的门道5.1 发送接口实现检查邮箱、填充数据、请求发送GD32有3个发送邮箱是分时复用的资源。每次发送前要找一个空闲邮箱把ID、数据长度、数据内容填进去然后请求发送。发送完成后硬件会清除对应的发送完成标志。uint8_t can_send_frame(uint32_t id, uint8_t *data, uint8_t len) { can_trasnmit_message_struct tx_msg; tx_msg.tx_sfid id; tx_msg.tx_ff CAN_FRAME_STD; tx_msg.tx_ft CAN_FT_DATA; tx_msg.tx_dlen len; tx_msg.tx_data[0] data[0]; tx_msg.tx_data[1] data[1]; tx_msg.tx_data[2] data[2]; tx_msg.tx_data[3] data[3]; tx_msg.tx_data[4] data[4]; tx_msg.tx_data[5] data[5]; tx_msg.tx_data[6] data[6]; tx_msg.tx_data[7] data[7]; return can_message_transmit(CAN0, tx_msg); }can_message_transmit会返回发送状态我个人建议在关键控制帧上检查发送结果。比如节点启动时发一个上线报文如果发送失败或发送超时说明CAN总线可能被其他错误节点拉死了这时候要给出明确的报警状态。5.2 接收中断服务函数拿到报文后赶紧干正事接收中断的特点是中断标志清零越早越不会反复进中断。GD32的接收FIFO有非空中断标志在中断服务函数里把数据读出来清零标志再处理业务逻辑。void CAN0_RX0_IRQHandler(void) { can_receive_message_struct rx_msg; uint8_t data_buf[8]; uint8_t i; can_message_receive(CAN0, CAN_FIFO0, rx_msg); for (i 0; i rx_msg.rx_dlen; i) { data_buf[i] rx_msg.rx_data[i]; } process_can_message(rx_msg.rx_sfid, data_buf, rx_msg.rx_dlen); can_interrupt_flag_clear(CAN0, CAN_INT_FLAG_RFL0); }注意不要在中断里做耗时太长的操作比如打印日志、写Flash、位操作一大堆。我见过有人直接在CAN中断里写401个字节的日志到EEPROM结果中断被拖死后续帧全部丢失。正确做法是中断里开辟一个环形缓冲区把数据存下来设置一个标志主循环再去解析和执行。5.3 错误状态监测与恢复别等总线彻底死掉才知道GD32的CAN外设提供了错误中断包括错误主动、错误被动、总线关闭等。工程建议把总线关闭和错误被动中断都开了一旦发现节点进入异常状态立即上报上位机并尝试重新初始化CAN模块。can_interrupt_enable(CAN0, CAN_INT_ERR); can_interrupt_enable(CAN0, CAN_INT_BO); can_interrupt_enable(CAN0, CAN_INT_EPV);如果开了自动总线恢复总线关闭后硬件会自动回归正常状态但建议还是记录一下错误日志便于现场分析。很多时候总线频繁收错误帧不是软件逻辑的问题而是某个节点的收发器坏了一直在拉低总线。这种问题必须先定位到是哪个节点再谈修复。5.4 一个可以跑通的最小测试程序测试思路很简单发送一个固定报文然后通过中断接收自己发的报文先配成环回模式如果串口打印出接收成功说明CAN模块、GPIO、过滤器、中断链路都是通的。这个最小测试程序我一般在硬件调试第一天就会跑一遍省得后面连了外部总线再找问题。int main(void) { uint8_t car_count 0; systick_config(); uart_init(115200); can_gpio_init(); can_config_filter_interrupt(); while (1) { can_send_frame(0x123, car_count, 1); car_count; delay_1ms(1000); } }6. 常见问题与排查技巧实录6.1 上电之后一直发错误帧怎么查最典型的现象是CAN分析仪上一片错误帧导致正常报文完全发不出去。第一步先查波特率很多情况下是总线上两个节点波特率不一致导致每个节点都认为对方发的是错误帧。把USB-CAN分析仪的波特率调到和设备一致看错误帧是否消失。第二步查物理层示波器挂CAN_H和CAN_L看差分幅度是否正常。正常工作时差分幅度应该在2V左右如果只有0.5V可能是收发器供电不足或终端电阻接错。第三步查接地多个节点如果地电位差太大共模电压超标也会持续产生错误帧。6.2 能发不能收九成是过滤器的问题接收中断不触发但发送正常这时优先检查过滤器配置。过滤器全零掩码模式下理论上所有报文都能进FIFO如果你用的掩码不对收不到很正常。还有一个容易忽略的点FIFO0和FIFO1是独立的中断标志也不同。你开了FIFO0的非空中断但报文被过滤到FIFO1那就永远进不了中断。给过滤器指定filter_fifo_number CAN_FIFO0同时开CAN_INT_RFL0两边对齐。6.3 负载率要怎么估算和实测负载率反映总线忙不忙计算公式是总线实际占用时间除以总时间。比如500kbps总线上1秒内数据传输占用了400毫秒那负载率就是40%。你可以用CAN分析仪直接看负载率也可以在应用层估算每帧标准帧大约110个位时间帧间隔还要额外算。如果每10毫秒发一帧负载率大约是110位除以500kbps乘10毫秒约2.2%。这个估算很实用设计通信周期时先用它粗算一下别把总线塞得太满超过80%以后延迟和错误率会急剧上升。6.4 中断接收还是DMA接收我的最终建议我用了几年GD32的CAN最终结论是超过90%的场合用中断接收就够了。CAN的FIFO本身有缓存中断优先级可以配成最高处理函数足够精简的话CPU开销其实很小。DMA接收适合以下场景一帧数据量很大、报文频率稳定且极高、CPU负载已经非常紧张。但DMA的配置复杂度高而且要格外小心描述符地址对齐和缓存一致性调试成本很高。先跑中断实测丢帧率可接受就不要上DMA简单可靠才是王道。6.5 调试工具和调试验证建议调试CAN设备一个USB-CAN分析仪几乎是必需品便宜的双通道百元左右能看报文、错误帧、负载率足够用。配上CAN_H和CAN_L的双绞线两端的120欧电阻一个小型CAN网络就搭起来了。我个人还习惯在代码里加一个自检机制上电时发一个心跳报文然后判断是否能在规定时间内收到对端确认。如果收不到确认说明链路不通或对端没起来直接把错误码上报到调试串口。这个方法在生产现场非常有用能快速定位是单点故障还是全网故障。最后再分享一个小技巧调试CAN驱动时一定要准备一个可以随时插拔的终端电阻开关。我用过一个带拨码开关的CAN分析仪总线不稳定时先把所有节点断开只留下分析仪和设备把终端电阻全部接上验证最小系统通信是否正常。逐个加入节点就能一步步锁定故障源。这种排查思路比翻代码高效得多尤其是领导站你身后催进度的时候你能沉住气把问题缩小到最小范围就已经成功一半了。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。