
这几年做车载和工业控制项目我从STM32切到GD32的次数越来越多。原因无非两个缺货和成本。工程上换平台最头疼的就是CAN总线因为它涉及时钟、位时序、滤波器、中断一堆细节不像点个LED灯那么简单。很多人上来就用STM32CubeMX生成一份代码烧到GD32里结果CAN死活不通然后开始怀疑芯片有问题。其实问题多半出在“CubeMX生成的工程只是长得像底层思路还是STM32那套”而GD32的时钟树、外设库、寄存器细节都有差异。这篇文章我打算用保姆级的粒度把“用STM32CubeMX给GD32配CAN总线”这件事完整摊开。包括工程怎么建、波特率怎么算、HAL代码怎么翻译成GD32固件库、上板之后怎么调以及常见异常怎么排查。适合刚接触GD32、想在项目里快速把CAN跑起来的人也适合已经被CAN折腾了一两天、准备搜“异常处理”关键词的人。我所有代码示例以GD32F103系列固件库为准其他系列API有差异但思路通用。1. 思路先行CubeMX生成的代码凭什么能用在GD32上1.1 兼容性真相引脚像外设不像GD32在设计上大量参考了STM32的引脚定义很多型号的PA11、PA12都是CAN的默认收发引脚PB8、PB9是重映射引脚这部分确实可以“抄作业”。但要注意“引脚兼容”不等于“库兼容”。STM32CubeMX没法直接选择GD32芯片型号因为ST的软件包里根本没有GD32的器件。所以常见做法是选一颗引脚和外设布局最接近的STM32型号来生成工程骨架比如用GD32F103C8T6替代STM32F103C8T6。CubeMX负责把引脚、时钟树、外设中断这些框架搭好而真正控制CAN外设的初始化函数最后要用GD32官方固件库重写。另一个容易误解的点是GD32的CAN外设名字。STM32F103上叫CAN1GD32F103上一般叫CAN0。如果你写代码时还在用CAN1编译当然过不了。这是很多从STM32迁移过来的人第一道坎。1.2 工作流怎么搭什么用CubeMX什么用GD32库我的习惯组合是CubeMX负责生成工程框架、配置引脚复用、生成串口调试代码GD32固件库负责实际控制CAN外设。为什么中间隔了一层还要这么麻烦首先CubeMX对工程文件的管理、启动文件、链接脚本这些细节做得比较完整比手动从零搭一个Keil工程要省事得多。其次CubeMX生成的GPIO初始化代码和串口初始化代码经过简单改动就能在GD32上跑这部分工作量不小价值很高。最后CAN外设本身用GD32库重写因为GD32官方提供的gd32f10x_can.c才是针对这颗芯片验证过的驱动HAL库的HAL_CAN_Init函数在GD32上未必能正确处理所有寄存器。你要理解这个流程不是在“硬凑”而是把工具的优势分开放到正确的地方。CubeMX当画板工具用GD32库当执行引擎用各干各擅长的活。如果你不想用CubeMXGD32官方也有自己的图形化配置工具叫GD32 Embedded Builder可以生成工程。但社区资料、报错案例都少新手遇到问题往往搜不到答案。所以我更推荐先用CubeMX搭骨架等熟悉了再考虑换官方工具。2. 工程搭建从CubeMX到能跑的CAN工程2.1 新建工程时的芯片选择打开STM32CubeMX新建工程选芯片时直接搜STM32F103C8T6。如果你的GD32是CBT6之类的128KB Flash版本就选STM32F103CBT6原理一样。需要注意的是CubeMX生成工程时默认的Flash大小、RAM大小会和GD32实际容量有出入这在初始化阶段不影响CAN但后面做Bootloader或者地址分配时要单独核对。选完芯片后重点做三件事RCC配置、SYS配置、时钟树调整。RCC页面选择Crystal/Ceramic Resonator也就是让HSE使用外部晶振。SYS页面把Debug设为Serial Wire否则你烧录一次程序之后SWD接口可能会被禁用下次下载直接失败这是新手最容易踩的坑。时钟树页面先不管后面单独讲。2.2 GPIO、CAN外设和串口调试通道在Pinout页面把PA11设为CAN_RXPA12设为CAN_TX。如果你的板子把CAN引脚引到了PB8、PB9那CubeMX会自动帮你做重映射但要注意GD32这边还需要使能AFIO时钟后面代码翻译时再处理。同时建议把USART1打开PA9为TX、PA10为RX用于打印调试信息。CAN调试如果没有串口输出就像开车不看仪表盘出了问题只能靠猜。外设列表里找到CAN1勾选。CubeMX会显示CAN参数配置页面里面有Prescaler、Time Quanta、BS1、BS2这些参数。这里填多少取决于你的实际APB1时钟千万不要直接照抄网上某个工程因为别人板子的时钟源和主频可能和你不一样。2.3 生成代码后先做最小编译配置完成后在Project Manager里选择工具链MDK-ARM也好IAR也好看你平时习惯。生成代码后不要急着改CAN先编译一次确保工程骨架没问题然后通过串口打印一个SystemClock_Config里的时钟数值确认当前芯片实际跑在多少MHz。这一步看似多余但能帮你把“时钟问题”和“CAN问题”分开。我见过太多人CAN调不通最后发现是主频配置早就错了串口波特率都对不上压根没进入CAN调试阶段。3. 参数计算与引脚配置波特率算不对后面全是白干3.1 位时间、采样点与波特率计算CAN波特率不是随便填一个数就能用的。经典CAN的每一位时间由4段组成同步段SYNC_SEG、传播时间段、相位缓冲段1也就是BS1、相位缓冲段2BS2。其中传播时间段和BS1合在一起看就是CubeMX里的BS1。位时间的总长度用时间量子TQ表示位时间 1(SYNC_SEG) BS1 BS2波特率公式波特率 APB1时钟 / (Prescaler × 位时间)以位时间12TQ为例1 BS1 BS2 12。如果BS19、BS22采样点等于 (1 9) / 12 83.3%。这个采样点对大多数CAN总线系统来说是比较稳的配置整车厂和工业设备里也常按80%到85%来设计。3.2 两个真实算例从36MHz和54MHz的APB1出发STM32F103跑72MHz时APB1最大是36MHz。假设目标波特率500kbps36MHz / 500kbps 72个时钟周期。如果Prescaler取6位时间就是12TQBS19、BS22采样点83.3%。验证一下36M / (6 × 12) 500k。没问题。GD32F103如果按官方例程跑108MHz系统时钟APB1经常是54MHz。同样目标500kbps54MHz / 500kbps 108个时钟周期。如果Prescaler取9位时间还是12TQBS19、BS22。验证54M / (9 × 12) 500k。也没问题。看出问题了吗同样的500kbps在STM32上是Prescaler6在GD32上却是Prescaler9。如果你拿着STM32的参数直接往GD32里填波特率就会变成 54M / (6 × 12) 750kbps这两个速率根本对上不总线上当然全是错误帧。所以每次配CAN我都会先做一件事在代码里把APB1时钟值读到串口打印出来然后按公式算一遍Prescaler。这是最笨也最可靠的方法。3.3 引脚和复用功能检查清单PA11作为CAN_RXPA12作为CAN_TX在GD32F10x固件库里的配置大概是rcu_periph_clock_enable(RCU_GPIOA); 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);注意新版GD32库的GPIO接口变成了gpio_mode_set和gpio_output_options_set具体名字以你当前库的例程为准但寄存器语义是一样的。如果用的是重映射的PB8、PB9还需要使能AFIO时钟并调用gpio_pin_remap_config把CAN引脚重映射过去。引脚这块大部分人都不会错真正容易忽略的是CAN收发器。MCU的CAN_TX和CAN_RX是逻辑电平必须经过TJA1050、SN65HVD230这类收发器才能上总线。收发器供电、共地、终端电阻这三个环节出问题波形就会很难看通信自然起不来。4. 代码翻译HAL初始化到GD32固件库4.1 时钟树核对这是最容易翻车的一步CubeMX生成的SystemClock_Config在STM32上是对的在GD32上未必。GD32F103的最高主频支持到108MHz而STM32F103是72MHz两者的时钟树、PLL配置、Flash等待周期都有差异。如果你直接用CubeMX生成的时钟配置顶多跑在72MHz这不算致命。真正致命的是你把APB1当作36MHz去算CAN波特率但GD32库的官方系统初始化函数已经把APB1设置成了54MHz。我的做法是把CubeMX生成的SystemClock_Config直接删掉换成GD32固件库里官方提供的时钟初始化函数。例如system_init(); system_clock_108m_hxtal();这样APB1就是54MHz。然后去CubeMX生成的CAN参数里把Prescaler按54MHz重新算一遍。千万不要在时钟还没确认的情况下开始调CAN这是这个项目里最值得强调的经验。4.2 CAN初始化结构体逐字段对照CubeMX生成的HAL_CAN_Init配置了AutoBusOff、AutoWakeUp、AutoRetransmit、ReceiveFifoLock、TransmitFifoOrder、Mode、SyncJumpWidth、TimeSeg1、TimeSeg2、Prescaler这些字段。GD32固件库里的can_parameter_struct结构体字段几乎一一对应。以500kbps、APB154MHz为例can_parameter_struct can_parameter; can_deinit(CAN0); can_parameter.time_triggered_mode DISABLE; can_parameter.auto_bus_off_recovery DISABLE; can_parameter.auto_wake_up DISABLE; can_parameter.no_auto_retransmit DISABLE; can_parameter.recv_fifo_lock DISABLE; can_parameter.trans_fifo_order DISABLE; can_parameter.working_mode CAN_NORMAL_MODE; can_parameter.resync_jump_width CAN_BT_SJW_1TQ; can_parameter.time_segment_1 CAN_BT_BS1_9TQ; can_parameter.time_segment_2 CAN_BT_BS2_2TQ; can_parameter.prescaler 9; can_init(CAN0, can_parameter);注意time_segment_1对应CubeMX里的BS1time_segment_2对应BS2。中间的传播时间段在经典CAN里一般不建议单独设得很大把它并进BS1计算就行。还有一点容易被忽略no_auto_retransmit这个字段字面意思有点绕。DISABLE表示允许硬件自动重发ENABLE表示禁止自动重发。做总线测试时可以临时设为ENABLE方便观察单帧错误但正常业务里建议保持自动重发不然一帧发送失败就直接丢掉了。4.3 发送、接收中断和过滤器的最小实现发送一帧标准帧核心代码can_message_struct tx_msg; tx_msg.tx_sfid 0x123; tx_msg.tx_efid 0; tx_msg.tx_ff CAN_FF_STANDARD; tx_msg.tx_dlen 8; for (uint8_t i 0; i 8; i) { tx_msg.tx_data[i] i; } can_message_transmit(CAN0, tx_msg);接收侧使能FIFO0中断nvic_irq_enable(CAN0_RX0_IRQn, 0, 0); can_interrupt_enable(CAN0, CAN_INT_RF0);中断处理函数void CAN0_RX0_IRQHandler(void) { can_receive_message_struct rx_msg; can_message_receive(CAN0, CAN_FIFO0, rx_msg); // 把 rx_msg.rx_sfid 和 rx_msg.rx_data 搬到全局缓冲区 }不同GD32型号和库版本里这个中断号可能叫CAN0_RX0_IRQn也可能在头文件里显示为USBD_LP_CAN0_RX0_IRQn。编译报错时去gd32f10x.h里搜一下IRQn就能找到。过滤器配置建议放在CAN初始化之后引脚配置之后can_filter_parameter_struct can_filter; can_filter.filter_number 0; can_filter.filter_mode CAN_FILTERMODE_MASK; can_filter.filter_bits CAN_FILTERBITS_32BIT; can_filter.filter_fifo_number CAN_FIFO0; can_filter.filter_enable ENABLE; 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_init(can_filter);mask全为0表示不屏蔽任何位也就是接收所有报文。这一步做完MCU的CAN控制器才算是真正具备收发能力了。5. 上板调试环回、正常模式与抓包验证5.1 环回模式不通外部也能验证控制器第一次上电强烈建议先把CAN工作模式设成CAN_LOOPBACK_MODE。环回模式下CAN控制器发送的报文不走外部引脚直接在内部绕回接收通路。你发一帧自己就能收到一帧。这个模式的作用是什么它能帮你把问题范围缩小到MCU内部。如果环回模式都收不到说明初始化、中断、过滤器、时钟配置至少有一个地方错了跟外面的收发器、终端电阻、总线电缆一点关系都没有。如果环回模式正常那就大胆往前走问题大概率出在外部硬件连接或总线参数上。环回模式测试代码很简单定时发送一帧数据在接收中断里把数据打印出来对比发送和接收是否一致。如果数据字节对不上先检查结构体里的字节顺序和长度字段再检查DMA或中断里是否发生了覆盖。5.2 正常模式接收发器、终端电阻与CAN分析仪环回通过后把can_parameter.working_mode改成CAN_NORMAL_MODE重新初始化。接下来就是外部硬件环节。首先确认CAN收发器的TXD、RXD分别连着MCU的CAN_TX、CAN_RX收发器供电正常。然后在总线的两端各接一个120欧姆终端电阻。如果只有两个节点应该分别在两个节点附近接如果节点很多只在总线物理末端接不能在中间节点乱接。建议准备一个USB转CAN分析仪把设备和电脑连起来。用分析仪发送一帧看MCU能不能收到再用MCU发送一帧看分析仪能不能抓到。一来一回问题在哪一段基本就清楚了。如果分析仪能收到MCU的帧但数据不对优先查CAN_ID的格式标准帧还是扩展帧CubeMX和GD32库里都要保持一致。如果完全收不到用示波器或者逻辑分析仪看CAN_H和CAN_L之间的差分电平。5.3 波形确认与负载率估算CAN总线正常工作时CAN_H和CAN_L之间有2V左右的差分电压显性位时差分电压约2V隐性位时约0V。发送波形如果看起来像一条直线先看是不是没有节点在发送如果波形有方波但毛刺很大大概率是终端电阻缺失、共地不良或线缆太长。很多人还会关心负载率。负载率的估算公式很简单负载率 每秒发送的帧数 × 每帧位数 / 波特率 × 100%经典CAN标准帧一帧大约111位扩展帧大约131位。以500kbps为例如果每10ms发一帧标准帧8字节数据每秒100帧负载率就是100 × 111 / 500000 × 100% ≈ 2.2%这个值非常低。如果每1ms发一帧负载率就到22%左右依然可以接受但中断频率已经变高MCU主循环会被频繁打断。这时候就需要考虑批量发送和FIFO管理的优化。6. 异常处理错误帧、滤波器和中断的那些坑6.1 错误状态机与错误帧类型CAN异常排查如果只记一件事那就是错误计数。CAN控制器内部有发送错误计数和接收错误计数当错误过多时节点会从主动错误状态进入被动错误状态严重时进入bus-off。在bus-off状态下节点相当于从总线上摘除收发都不工作。我调试的时候会在1ms定时中断里周期读取CAN外设的错误计数寄存器并用串口打印出来。如果看到TEC或者REC数值一直往上跳说明总线上确实在报错。这时再结合错误帧类型判断方向错误帧类型现象常见原因位错误发送节点自己发出的电平与自己监视到的电平不一致收发器故障、总线短路、多节点同时发送填充错误连续5个相同位后没找到填充位波特率不匹配、信号质量差CRC错误接收端CRC校验失败波特率不匹配、线缆过长、电磁干扰应答错误发送节点没收到显性应答位总线上只有一个节点、终端电阻缺失、接收节点未初始化形式错误固定位电平错误协议配置错误、总线干扰遇到“接上总线就bus-off”这种问题我一般先断开所有节点只留MCU和分析仪用最低速率比如50kbps测试。把速率降下来之后如果正常再逐步恢复500kbps这样能快速判断是不是位时序参数的问题。6.2 中断接收还是DMA接收别再纠结了很多入门者会问CAN能不能像串口那样用DMA接收。这里要说明一下STM32和GD32的传统bxCAN外设本身没有像串口那样的DMA请求映射它自带FIFO和邮箱结构报文到达后置状态标志触发中断。只要你中断函数里及时执行can_message_receive把报文从FIFO搬到内存就不存在丢失风险。所以结论是经典bxCAN用中断接收就行不用刻意找DMA。真正该优化的不是“中断还是DMA”而是中断处理函数里的代码量。中断里只做搬运解析和处理放到主循环这是能保证高负载下稳定运行的关键。如果负载率真的很高一帧接一帧地到达而中断里处理太慢FIFO0会溢出新报文会覆盖或丢弃。这个问题在调试时可以通过CAN外设的状态寄存器看到溢出标志。避免办法是缩短中断程序执行时间必要时配置FIFO1做双缓冲。6.3 常见问题速查表把平时被问到最多的问题整理出来现象排查方向环回模式收不到时钟没使能、过滤器配置错误、中断没使能、NVIC优先级分组问题环回正常正常模式不通收发器供电、CAN_H/CAN_L接反、终端电阻缺失、共地不良波特率明明一样却通信失败APB1时钟算错、采样点差距过大、收发器速率不支持能发能收但数据偶发错误总线干扰、线缆过长、线缆无屏蔽、终端电阻位置不对J-Link烧录失败SWD引脚被占用、Debug模式没选Serial Wire、目标芯片选择错误程序跑起来后CAN一直bus-off波特率严重不匹配、节点独占总线、总线短路烧录这块多说一句J-Link烧GD32F103时目标芯片选择Cortex-M3核心即可SWD速度可以适当降低有些GD32芯片在高速SWD下复位时序比较敏感。如果遇到烧不进去先把连接速度降到100kHz绝大多数情况能解。7. 实战换坑记录三个几乎必踩的问题第一个坑是APB1时钟错。我最早用CubeMX给GD32F103生成工程默认还是STM32的72MHzAPB136MHz。我在CAN配置里填了Prescaler6环回模式一切正常。结果一接外部设备对方设备也是500kbps但两边怎么都对不上。后来才意识到GD32官方库把系统时钟已经初始化到了108MHzAPB1是54MHz而CubeMX那套配置里还是36MHz。从那以后我每次移植第一件事就是打印APB1时钟。第二个坑是滤波器把报文全滤掉了。Debug时看接收中断一直不触发发送又正常一度以为是中断配置问题。最后发现是过滤器初始化里filter_mask_high和filter_mask_low写成了非零值ID匹配要求变得极其严格导致所有报文被过滤掉。改成全0之后立刻恢复接收。第三个坑是回环模式正常一上总线就bus-off。排查了很久最后发现是收发器的CAN_H和CAN_L接反了。这种低级错误在飞线上特别容易发生所以我现在调试CAN都会先拿万用表确认引脚定义再上电绝不在接线混乱的情况下直接开测。8. 最后再分享一个小技巧做CAN调试强烈建议在板子上留出CAN_H和CAN_L的测试点最好再加一个地线测试点。示波器探头、分析仪线缆能直接夹上去不用临时飞线。CAN是最怕接触不良的外设之一飞线松动导致的信号毛刺非常难查。我个人还有一个习惯任何一次CAN调试都在1ms定时中断里周期打印错误计数和状态寄存器。这个习惯帮我省了大量排查时间因为大部分CAN问题不会直接告诉你原因但它一定会在错误计数上留下痕迹。把TEC、REC的数值变化规律看明白了异常原因基本就浮出水面了。