ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

GD32替换STM32:CAN模块三大踩坑与解决指南

2026/9/28 1:11:19 拓冰建站 浏览量
GD32替换STM32:CAN模块三大踩坑与解决指南 如果你最近在折腾GD32替换STM32大概率绕不开这个问题引脚、内核、外设都号称兼容程序也确实能编译能下载但跑到CAN模块时就各种诡异。我这次把一个量产项目的主控从STM32F103换到GD32F103软件沿用HAL库不动底层结果别的外设都安安稳稳唯独CAN模块连续踩了三个坑。这篇文章就把这三个坑完整记录下来每个都给出现象、原因、代码对比和最终解法给正在做同样替换的朋友一个可以少走弯路的参考。先说结论GD32和STM32在寄存器层面高度兼容HAL库代码大概率能直接编译进GD32但这种“能编译”不等于“能正常工作”尤其CAN这种带协议状态机的外设初始化、中断、波特率三大环节都很容易翻车。我会尽量把排查思路也写清楚方便你在自己的工程里举一反三。1. 项目背景与适用场景1.1 替换需求从哪来最近这两年做硬件的人应该都能感受到一个趋势成本压力和供应链备选需求同时增加GD32F103系列成了很多人手里的首选替代方案。原因很简单GD32F103和STM32F103在引脚定义、片上外设、内存映射上做了高度兼容很多板子甚至不用改原理图就能把芯片换上去软件层面也可以保留原来的HAL库工程只需要在Keil里换芯片型号、改几个头文件路径就能编过。搜索这些关键词的人也不少“gd32在keil”“建立gd32的标准工程模板”“gd32 dfu驱动”“gd32 embedded builder”这些话题下的讨论量都很大说明大家正在经历类似的上手过程。我刚开始也以为替换就是把STM32工程直接烧进GD32结果真正跑起来才发现外设兼容性这件事不能想当然。1.2 本文适用哪些场景和工程条件这篇笔记的适用场景比较明确你的原工程基于STM32F103系列使用的是STM32 HAL库目前打算把主控整体替换成GD32F103系列并且想尽量保留原来的HAL库代码不切换到GD32官方外设库。文中的代码示例主要针对CAN1模块对应引脚是PB8( CAN_RX )和PB9( CAN_TX )这是F103系列最常用的CAN引脚组合。中断部分涉及CAN1_RX0也就是FIFO0接收中断。如果你用的是GD32F303、GD32E103等兄弟型号核心思路一样但极少数寄存器和中断向量名称会有差异需要对照具体芯片的用户手册微调。1.3 动手前先准备好工具和环境排CAN的坑光靠眼睛看代码是不够的最好提前准备好几样东西。第一一个支持GD32下载调试的工具J-Link和ST-Link实测都能用注意ST-Link烧录时如果提示无法连接多半要检查驱动和芯片的调试端口状态。第二一个CAN分析仪或者至少两个CAN节点用来做回环测试和总线互通测试。第三调试器里的寄存器窗口一定要会看后面排查HAL_CAN_Init超时、中断不触发、波特率不对时都要靠直接读寄存器来定位。我个人的建议是先拿一块GD32最小系统板和一个CAN收发器搭个简易测试环境不要在正式量产板上边改边测这样能把变量控制到最小排查效率会高很多。2. 替换前必须搞清楚的底层差异2.1 寄存器兼容与“看起来能跑”的边界STM32F103的CAN控制器是ST经典的bxCANGD32F103的CAN控制器则是在IP层面做了兼容设计的版本两者在寄存器偏移地址和大部分位定义上基本一致。这就是为什么STM32 HAL库里的CAN_TypeDef结构体定义在GD32上访问寄存器时不会崩。但“基本一致”不意味着“完全一致”。GD32的CAN模块在部分位定义、上电复位状态、中断向量组织上都有自己的实现细节。很多替换项目里GPIO、串口、I2C这些外设几乎无感兼容HAL库代码直接跑一点问题没有于是大家很容易默认CAN也没问题。实际上一旦涉及初始化时序、中断入口、位时序计算差异就会从角落里冒出来。2.2 HAL库和GD32外设库之间的灰色地带HAL库这套东西ST官方是围绕自家芯片验证过的抽象层再漂亮也只是对ST硬件负责。GD32官方提供的是标准外设库以及后来推的类似HAL的库但它们和ST的HAL库并不是同一套代码。你在GD32上使用ST的HAL库本质上是“借ST的外设驱动去驱动GD32的硬件”这种跨家使用在日常项目里很常见也能跑通但你要清楚自己走在了官方支持范围的灰色地带。这个灰色地带里CAN是最容易踩雷的模块之一。原因在于CAN外设的状态机比较复杂初始化要进入初始化模式、退出初始化模式要等待硬件确认中断又有多个中断源共享一个向量波特率又依赖APB1时钟频率。这三件事每一件都受芯片底层时序和向量表影响而HAL库恰恰在这些地方做了比较强的硬件假设。2.3 为什么CAN最容易踩坑我们不妨把问题拆解成三层看。第一层是初始化层HAL_CAN_Init内部会等待CAN硬件进入初始化模式如果硬件状态没准备好函数就会超时。第二层是中断层CAN接收中断要触发前提是中断向量名字对得上、NVIC配置正确、中断标志使能任何一个环节不对中断就静默失效。第三层是参数层波特率由APB1时钟、预分频、位时间共同决定APB1时钟一变整个波特率全变。这三大层面正好对应我实际遇到的三个坑。如果你能把这三点全部打通CAN模块在GD32上就算基本稳住。下面我按坑逐个讲。3. 坑一HAL_CAN_Init直接卡超时CAN模块没起来3.1 先复现现场程序卡在HAL_CAN_Init超时替换后的第一版程序下载到GD32板子上现象非常明确上电后代码在调用HAL_CAN_Init的位置出不来函数返回值一直是HAL_TIMEOUT。原工程在STM32上跑得好好的同样的代码换芯片编译后居然卡死在初始化。我在调试器里打断点单步跟进了HAL_CAN_Init的内部流程发现它卡在等待INAK位的位置。HAL库的思路是先把CAN_MCR的INRQ位置1请求进入初始化模式然后循环读取CAN_MSR的INAK位等硬件确认已经进入初始化模式。超时时间到了INAK位还没置起来HAL库就返回HAL_TIMEOUT。顺手确认了一下GPIO配置和时钟使能这部分都没问题HAL_CAN_MspInit里的引脚复用和时钟打开都正常执行了。这就很奇怪了时钟有了引脚配了为什么CAN硬件就是不给初始化模式的确认信号3.2 为什么会超时INAK位一直等不到我对比了GD32的数据手册和STM32的参考手册最后把问题锁定在CAN外设的上电状态上。GD32的CAN模块在上电后内部状态机并不总是处于一个干净的复位态尤其在某些批次或掉电不彻底的情况下CAN控制器的状态位会处在异常状态导致INRQ请求发出后INAK位迟迟不给出响应。另外一个容易被忽略的因素是SysTick。HAL库的超时判断依赖HAL_GetTick()而HAL_GetTick()默认由SysTick驱动。如果替换到GD32后你改了系统主频却没有同步修改SysTick的时基配置HAL_GetTick()的时间基准就不准超时判断也就形同虚设。我这次虽然不是这个原因但在群里见过不少朋友栽在这上面特地说一下。注意排CAN初始化问题之前先确认一个最基本的事情——你的系统时钟和HAL_GetTick()是准的。系统时基不对后面所有超时类问题都会变成玄学。3.3 代码对比软件复位是第一步最直接的解决方案是在HAL_CAN_MspInit里对CAN外设做一次完整的软件复位把CAN模块的上电不确定状态清掉。STM32原工程里的MspInit通常只做时钟使能和GPIO配置没有外部复位这一步这在ST芯片上够用在GD32上则需要补上。STM32原代码void HAL_CAN_MspInit(CAN_HandleTypeDef* hcan) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_CAN1_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_8 | GPIO_PIN_9; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); }GD32修改后的代码void HAL_CAN_MspInit(CAN_HandleTypeDef* hcan) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_CAN1_CLK_ENABLE(); // 关键先强制复位CAN外设清掉上电不确定状态 __HAL_RCC_CAN1_FORCE_RESET(); __HAL_RCC_CAN1_RELEASE_RESET(); __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_8 | GPIO_PIN_9; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); }不要小看这两行强制复位和释放复位它会把CAN控制器的所有寄存器和内部状态机拉回到一个确定性的初始状态。加了这一步之后HAL_CAN_Init超时的问题在我这边直接消失。如果个别板子加了复位还是很慢可以在初始化前做一次手动等待给硬件更多时间CAN1-MCR | CAN_MCR_INRQ; uint32_t tick HAL_GetTick(); while ((CAN1-MSR CAN_MSR_INAK) 0) { if ((HAL_GetTick() - tick) 50) { __HAL_RCC_CAN1_FORCE_RESET(); __HAL_RCC_CAN1_RELEASE_RESET(); break; } }这个手动等待更适合做兜底逻辑不建议直接替换HAL库自身的等待流程而是在HAL_CAN_Init返回超时之后做一个重试分支整体会更稳妥。4. 坑二CAN中断进不去接收只能靠轮询4.1 发送正常、接收中断不触发先确认是不是中断链路初始化问题解决后CAN模块能启动了发送报文也能从TXD引脚看到波形但接收中断始终不触发。我用逻辑分析仪抓CAN_RX引脚发现总线上的数据帧确实进来了然而程序里的中断服务函数一次都没进。把接收方式临时改成轮询查询RF0R寄存器又能正常收到报文。这个现象很有意思说明CAN控制器的硬件接收链路是通的数据已经进到FIFO0里了问题出在“FIFO0有消息”这个事件没能通过中断通知CPU。问题范围一下缩小到了NVIC配置、中断服务函数、中断标志使能三层链路里。我当时第一反应是检查HAL_CAN_ActivateNotification确认接收FIFO0消息挂号中断是否已经使能。这一步没问题使能了 CAN_IT_RX_FIFO0_MSG_PENDING。接着查NVIC配置寄存器看下来也已使能优先级也没问题。最后才想到去对启动文件的向量表一查就发现了问题。4.2 共享中断向量与ISR名称不匹配在STM32F103的HAL库工程里经常直接写一个这样的中断服务函数void CAN1_RX0_IRQHandler(void) { HAL_CAN_IRQHandler(hcan1); }问题来了换到GD32F103之后如果你用GD32的启动文件CAN1_RX0这个中断在启动文件里对应的入口名称可能是USB_LP_CAN1_RX0_IRQHandler因为CAN0接收中断和USB低优先级中断共享同一个向量。启动文件里的向量表地址和中断服务函数名是对应关系函数名对不上中断来了以后CPU跳到向量表指定的地址但这个地址上的函数符号没定义中断自然就进不去。这个坑最隐蔽的地方在于工程的启动文件可能来自不同渠道。你如果还是用STM32的启动文件那CAN1_RX0_IRQHandler这个名字就能对上工程能跑但换成GD32官方启动文件之后这个名字就失配了。GD32和STM32在中断向量布局上虽然有大量重叠但中断服务函数的命名习惯不一定一致尤其在共享向量上特别容易出问题。注意替换芯片后先用GD32官方启动文件再根据启动文件里的中断向量表逐一核对ISR函数名。不要想当然沿用STM32工程里的中断函数名。4.3 代码对比统一中断入口并区分中断源如果你的工程里没有使用USB功能最简单的做法是把中断服务函数名改成GD32启动文件里的名字然后直接调用HAL_CAN_IRQHandlervoid USB_LP_CAN1_RX0_IRQHandler(void) { HAL_CAN_IRQHandler(hcan1); }如果工程里同时使用了USB和CAN就需要在一个共享中断入口里区分中断源避免互相抢占导致回调失效。HAL库的CAN中断处理函数会自己读取CAN的寄存器状态来判断是否属于CAN事件但ISR入口处最好还是做一层源判断尤其是在USB也产生中断的情况下。一个可以落地的写法是void USB_LP_CAN1_RX0_IRQHandler(void) { // 先处理USB中断 if (USB-ISTR USB_ISTR_CTR) { HAL_PCD_IRQHandler(hpcd); } // 再处理CAN中断 if ((CAN1-MSR CAN_MSR_RX) || (CAN1-IER CAN_IER_FMPIE0)) { HAL_CAN_IRQHandler(hcan1); } }不管哪种写法都不要漏掉HAL_CAN_ActivateNotification这一步HAL_CAN_ActivateNotification(hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);没有这行代码即使中断服务函数进了HAL_CAN_IRQHandler也不会去检查FIFO0的消息挂号标志回调函数永远不会执行。这个点比较容易和前面的中断向量问题混在一起排查时按顺序看先确认ISR有没有进再确认HAL_CAN_IRQHandler有没有走接收分支最后确认FIFO0挂起标志是否被HAL库的正常中断流程清除。这部分的验证方法是在中断服务函数入口放一个断点或者翻转一个IO口。如果ISR没进问题在向量名或NVIC如果ISR进了但回调没反应问题在HAL库的中断源判断和ActivateNotification配置。5. 坑三波特率算错了总线上帧发不出去5.1 自测能通、上总线就废先怀疑波特率中断问题解决后我已经能在单板上用回环模式正常收发CAN报文了。于是信心满满地接上CAN分析仪准备看真实总线的数据交互结果一接上总线整个通信就断。再回头看连两个GD32板子互相通信都不稳定报文时通时不通错误计数器持续上涨。这种“回环正常、上总线就废”的现象非常典型几乎可以锁定是波特率偏差或者位时序采样点配置有问题。GPIO和中继链路在回环测试里已经验证过了物理层只要终端电阻正常剩下的就是通信参数不匹配。5.2 被忽略的APB1时钟差异为什么原STM32工程里的CAN波特率参数到了GD32上会失效核心原因在于APB1时钟变了。原工程是STM32CubeMX生成的主频按72MHz配置APB1经过二分频得到36MHz。而我替换到GD32后参考了一些GD32的模板工程把主频配到了108MHzAPB1设为主频的二分频也就是54MHz。GD32F103运行在108MHz本身没有问题这属于合理配置但CAN模块的时钟源正好挂在APB1上。HAL库在计算CAN波特率时用的是HAL_RCC_GetPCLK1Freq()它返回的是当前APB1实际频率。APB1从36MHz变成54MHz后如果预分频和位时间参数保持原值实际波特率就变成了原来的1.5倍。你本来想跑500kbps结果跑出来750kbps总线上的其他节点当然不认。还有一个隐性问题就是采样点位置。CAN总线的位时间由同步段、传播时间段、相位缓冲段1、相位缓冲段2构成采样点位置对总线通信质量影响很大。很多工程习惯直接沿用参数只关心“波特率算出来对”不关心采样点是否落在合理区间这在STM32上可能侥幸能用在GD32这种换时钟源之后就容易暴露。5.3 代码对比用寄存器反算实际波特率先看SPI上常见的原工程配置目标500kbpsAPB1为36MHz时的参数hcan1.Init.Prescaler 4; hcan1.Init.SyncJumpWidth CAN_SJW_1TQ; hcan1.Init.TimeSeg1 CAN_BS1_13TQ; hcan1.Init.TimeSeg2 CAN_BS2_4TQ;这个配置下位时间等于同步段1TQ加上13TQ再加上4TQ总共18TQ波特率等于36MHz除以预分频4再除以18TQ正好500kbps采样点约77.8%属于比较理想的参数组合。如果GD32的APB1保持在36MHz这些参数完全可以沿用。但如果你决定让GD32跑108MHz主频APB1用54MHz那预分频必须从4改成6位时间保持18TQ不变才能继续维持500kbpshcan1.Init.Prescaler 6; hcan1.Init.SyncJumpWidth CAN_SJW_1TQ; hcan1.Init.TimeSeg1 CAN_BS1_13TQ; hcan1.Init.TimeSeg2 CAN_BS2_4TQ;这里实际计算是54MHz除以预分频6得到9MHz再除以18TQ正好500kbps。采样点仍然是77.8%跟原工程完全一致。为了保险起见可以在初始化完成后加一段代码直接读CAN_BTR寄存器反算实际波特率。这部分要注意HAL库里的TimeSeg1和TimeSeg2是寄存器位段值不是直接的TQ数值直接拿结构体成员做加减会算错。更可靠的做法是读硬件寄存器解码uint32_t btr CAN1-BTR; uint32_t brp (btr 0x3FF) 1; uint32_t ts1 ((btr 16) 0xF) 1; uint32_t ts2 ((btr 20) 0xF) 1; uint32_t can_clock HAL_RCC_GetPCLK1Freq(); uint32_t real_baud can_clock / (brp * (1 ts1 ts2)); printf(CAN clock%lu, brp%lu, ts1%lu, ts2%lu, baud%lu\r\n, (unsigned long)can_clock, (unsigned long)brp, (unsigned long)ts1, (unsigned long)ts2, (unsigned long)real_baud);这段代码直接以硬件寄存器里的实际配置为准最适合排查“我以为配的是500k实际上跑的是750k”这类问题。实操心得在调试器里弄一个Watch窗口把HAL_RCC_GetPCLK1Freq()的返回值盯住先确认APB1再去算CAN分频。我后来凡是碰到CAN不上总线的第一件事就是打印PCLK1和BTR寄存器两秒钟就能定位是不是波特率问题。6. 替换GD32后CAN模块的通用排查顺序与补充建议6.1 CAN替换排查顺序速查表把这次的踩坑经验整理成一张速查表下次再遇到GD32替换STM32后CAN异常可以按这个顺序从上往下查。步骤检查项排查方法1系统时钟与APB1频率查看RCC配置确认PCLK1是36MHz还是54MHz2CAN外设时钟和复位MspInit里是否加了CAN外设强制复位3GPIO复用与速度PB8/PB9是否配置成复用推挽速度是否足够4中断向量名和NVIC对照启动文件ISR名确认NVIC已使能且优先级合理5波特率分频参数用BTR寄存器反算实际波特率计算误差6滤波器和FIFO映射确认滤波器组关联FIFO0且未屏蔽接收7总线物理层两端120欧终端电阻CANH/CANL不能接反这个顺序基本遵循“从MCU内部到外部总线”的排查逻辑前面步骤问题不解决后面测什么都没有意义。6.2 几条实战补充最后补几个替换过程中的实用经验。第一启动文件尽量用GD32官方的别图省事沿用STM32的启动文件。别小看这个文件中断向量名、堆栈初始化、启动代码细节都跟芯片型号有关。第二GD32替换后第一次下载如果调试器提示连接失败先排查芯片调试端口是不是被锁或者进入了低功耗模式网上搜“gd32单片机锁住了解锁方法”能搜到很多解决方案。第三在Keil里装好GD32芯片包再建工程否则器件列表里找不到对应型号还会引发一堆编译选项问题。另外建议在正式量产板之前先花半天时间做一块“最小验证板”把CAN、串口、GPIO这几个关键外设单独跑一遍跑通了再整体移植。我这次就是靠这个办法把三个坑全部隔离在验证阶段没有污染到业务代码。最后再分享一个小技巧替换阶段不要一上来就连真实总线先把CAN模式改成回环模式自测。回环模式下发送报文不经过总线直接在CAN控制器内部被接收能把MCU侧问题与总线侧问题分开。如果回环都收不到说明外设初始化、滤波器、中断链路肯定有问题回环能通再接真实总线就能把波特率、终端电阻、线缆这些问题隔离开来。这个思路帮我在现场少踩了很多坑希望你也能用上。