ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

GD32替换STM32的CAN模块移植:三大典型坑及排查实例

GD32替换STM32的CAN模块移植:三大典型坑及排查实例 GD32替换STM32这事,前两年还只是小范围讨论,今年已经成了很多团队的真实现状。我们手头两个量产的STM32F103项目,也整体平移到了GD32F103,引脚、外设、开发环境看上去都兼容,但真正调起来才发现,最容易翻车的恰恰是那些“看起来一样”的地方。尤其是原工程里直接用STM32 HAL库跑CAN,移植到GD32上后,三个坑几乎挨个踩了一遍:CAN节点离线、过滤器规则“形同虚设”、接收回调死活不触发。这篇文章不打算泛泛讲GD32和STM32的区别,专门盯着HAL库的CAN模块,把这三个坑的现象、根因、解决代码和排查方法完整写出来,给准备做替换的同行动手时做个参考。1. 替换前,先把 CAN 的“兼容地基”查一遍1.1 引脚和寄存器的兼容程度,比你想象中微妙GD32F103和STM32F103的CAN,在硬件引脚上是明确兼容的,CAN_TX、CAN_RX的引脚映射完全一致,软件上寄存器布局也很接近。但如果你默认“ST的HAL库代码可以直接烧到GD32上跑”,那就容易踩坑了。先说一个容易忽略的资源差异:STM32F103的bxCAN过滤器组有14个,GD32F103不少型号做到了28个。多出来的过滤器组本身是好事,但有些老代码里如果写死了FilterBank 14,在STM32上编译运行是非法配置,在GD32上却是合法配置。问题就来了——两种芯片对“合法”的认定不一样,代码一旦跨芯片复用,行为就会变得不可预测。更要命的是软件层的“库”并不等价。STM32生态主流是HAL库,GD32官方提供的是标准外设库,两者名字都带“库”,但API、结构体、底层寄存器操作差异很大。我们刚替换时,也想过“直接把ST HAL库的CAN驱动文件拷过来,再配一下寄存器地址不就行了”,实际操作后才发现,过滤器的结构体字段、中断服务函数命名、时钟频率获取方式,每一个环节都有“翻译”的坑。1.2 替换前必做的三件事这里强烈建议不要直接在旧工程上改芯片型号。哪怕Keil里把Device换成GD32F103C8,也只改了芯片支持包层面的东西,工程里的启动文件、系统时钟文件、外设库文件还是ST那一套,后面问题会像滚雪球一样。替换前建议先把三件套换掉:启动文件:换成GD32官方固件库里的startup_gd32f1xx.s,别用STM32的startup_stm32f10x_hd.s。系统时钟初始化文件:换成system_gd32f1xx.c,对应的头文件、系统时钟配置函数都以GD32官方库为准。CAN外设层:要么全部使用GD32标准外设库,要么做好HAL库到GD32寄存器层的适配层,不要两套库混着用。我们第一次偷懒,只改了Device,结果烧进去以后UART波特率直接差了2倍。原因很简单:STM32的系统文件把SystemCoreClock设成了72MHz,但GD32的RCU复位默认值和PLL配置逻辑跟STM32并不是完全一致的,外设时钟一旦偏离,后面所有基于PCLK1计算的外设——CAN就是其中之一——全都会跟着错。这个坑跟CAN关系最大,下面单独展开。2. 坑一: PCLK1 变了,波特率自然全乱2.1 现象: 节点离线 错误帧刷屏这个坑的故障现象非常典型:把写好的GD32程序烧进去,板上电后,总线上的其他STM32节点全部报错,用CAN分析仪一抓,错误帧疯狂增加;GD32节点本身也进入不了正常通信状态,发出去的报文没人ACK。有时候HAL_CAN_Init返回的又是HAL_OK,让你一度怀疑是不是收发器坏了。这种情况十有八九不是收发器的问题,而是波特率和总线上的其他节点对不上。CAN总线的通信前提是每个节点都用同一个波特率,只要有一个节点的位时序算错,整条总线的错误帧就会飙升。更麻烦的是,这类故障不会在HAL_CAN_Init阶段暴露,因为初始化函数只负责把分频系数和位时序写进寄存器,它不管最终波特率是不是你期望的那个。2.2 根因: HAL 只写寄存器,不帮你核对真实时钟CAN的波特率公式很简单:波特率 PCLK1 / (Prescaler * (1 BS1 BS2))这个PCLK1就是CAN外设的输入时钟,它挂在APB1总线上。在STM32F103的HAL库里,hcan.Init.Prescaler、TimeSeg1、TimeSeg2这三个参数会直接写进CAN_BTR寄存器,但HAL不会去确认APB1的实际频率是多少。问题就出在:你换到GD32之后,PCLK1很可能已经不是原来的数值了。比如原工程在STM32上的SystemClock_Config是72MHz系统时钟、APB1二分频得到36MHz,但同样的配置烧到GD32上,由于RCU寄存器默认值或PLL配置的细微差异,APB1实际可能变成72MHz。这时候,原来按36MHz算好的Prescaler9就失效了,实际波特率变成72MHz / (9 * 8) 1Mbps,而不是你想要的500Kbps。我们查出这个问题的过程也很折腾。一开始怀疑收发器电路,换了其他节点上的收发器还是一样;后来在初始化CAN之前用调试器读了一次HAL_RCC_GetPCLK1Freq(),才发现PCLK1根本就不是36MHz,而是72MHz。所有波特率全部翻倍,总线自然无法通信。2.3 正确做法: 先测 PCLK1,再算分频正确的替换流程应该是:移植完成后,第一步不是跑CAN,而是确认时钟。建议在CAN初始化之前,用调试器或者串口把HAL_RCC_GetPCLK1Freq()的实际值读出来,然后根据这个实际值反推分频系数。很多GD32官方例程提供的系统时钟配置函数(比如system_clock_108m()、system_clock_72m())写得很清楚,能保证PCLK1按照预期输出。但如果你的工程还是沿用STM32的SystemClock_Config,PCLK1很可能就和预期不一致。不要凭经验假设,直接读、直接打印,这是最稳的。2.4 代码对比: ST HAL 原版 vs GD32 适配先看STM32原始工程里常见的HAL库配置(以500Kbps、PCLK136MHz为例):/* STM32 HAL 原版: 假设 PCLK1 36MHz */ void MX_CAN_Init(void) { hcan.Instance CAN1; hcan.Init.Prescaler 9; hcan.Init.Mode CAN_MODE_NORMAL; hcan.Init.SyncJumpWidth CAN_SJW_1TQ; hcan.Init.TimeSeg1 CAN_BS1_5TQ; hcan.Init.TimeSeg2 CAN_BS2_2TQ; hcan.Init.TimeTriggeredMode DISABLE; hcan.Init.AutoBusOff ENABLE; hcan.Init.AutoWakeUp DISABLE; hcan.Init.AutoRetransmission ENABLE; hcan.Init.ReceiveFifoLocked DISABLE; hcan.Init.TransmitFifoPriority ENABLE; if (HAL_CAN_Init(hcan) ! HAL_OK) { Error_Handler(); } }这个配置里,位时序总共是8TQ(1个同步段 5个TQ的BS1 2个TQ的BS2),Prescaler9,所以波特率是36MHz / (9 * 8) 500Kbps。替换到GD32之后,建议改成“先取实际PCLK1,再算分频”的写法:/* GD32 适配: 先读 PCLK1, 再动态计算 */ #define CAN_BAUDRATE 500000U #define TOTAL_TQ 8U /* 1 BS1(5) BS2(2) */ void MX_CAN_Init_GD32(void) { uint32_t pclk1; uint32_t prescaler; pclk1 HAL_RCC_GetPCLK1Freq(); /* 替换后务必确认实际值 */ prescaler pclk1 / CAN_BAUDRATE / TOTAL_TQ; /* 如果除不尽,直接断言,防止带病运行 */ if ((pclk1 % (CAN_BAUDRATE * TOTAL_TQ)) ! 0) { Error_Handler(); } hcan.Instance CAN1; hcan.Init.Prescaler prescaler; hcan.Init.Mode CAN_MODE_NORMAL; hcan.Init.SyncJumpWidth CAN_SJW_1TQ; hcan.Init.TimeSeg1 CAN_BS1_5TQ; hcan.Init.TimeSeg2 CAN_BS2_2TQ; /* 其他字段保持不变 */ if (HAL_CAN_Init(hcan) ! HAL_OK) { Error_Handler(); } }如果你用的不是HAL移植层,而是GD32标准外设库,换汤不换药,核心还是先拿到PCLK1的真实频率,再配置波特率。下面这张表是常见PCLK1和波特率的配置参考,8TQ是我个人比较喜欢用的位时序,采样点在75%左右,CAN总线稳定性不错:PCLK1目标波特率Prescaler(8TQ)实际波特率36MHz500Kbps9500Kbps36MHz250Kbps18250Kbps36MHz125Kbps36125Kbps72MHz500Kbps18500Kbps72MHz250Kbps36250Kbps72MHz125Kbps72125Kbps3. 坑二: 过滤器“接口过了,寄存器没对齐”3.1 现象: 该来的不来,不该来的全来了这个坑的故障现象更隐蔽。CAN的收发已经正常了,总线上的错误帧也没了,节点能发能收,但你会发现接收过滤几乎不起作用。比如原工程配置的是只接收ID为0x123的报文,换成GD32之后,发0x456过来也能通过过滤进队列;或者反过来,配置了FIFO0接收,结果报文全部进了FIFO1,接收中断回调一直不触发。最让人抓狂的是,用CAN分析仪发标准帧,全都能进;发扩展帧,也能进。过滤器处于一种“完全放行”的状态。这类问题单靠看波形是看不出来的,必须对着寄存器捋。3.2 根因: 两套库的过滤器结构体并不等价在STM32 HAL库里,过滤器配置用的是CAN_FilterTypeDef,字段包括FilterIdHigh、FilterIdLow、FilterMaskIdHigh、FilterMaskIdLow、FilterFIFOAssignment、FilterBank、FilterMode、FilterScale。在GD32标准外设库里,对应的结构体是can_filter_parameter_struct,字段名完全不同,比如filter_list_high、filter_list_low、filter_mask_high、filter_mask_low、filter_fifo。名字不同只是表面问题,真正的风险在底层位域。很多开发者在移植时会“凭感觉”把ST HAL的字段映射到GD32的字段,结果该左移的没左移,该放的位放错了位置,过滤规则自然失效。以标准帧11位ID为例,不管是STM32还是GD32,过滤器寄存器的位定义其实是一致的:标准ID要放到寄存器的高位区域。在HAL库的FilterIdHigh字段里,11位ID要左移5位再填入;在GD32标准库的filter_list_high里,同样是左移5位。如果有一个方向写错了,过滤规则就会变成过滤了一个完全不同的ID,甚至过滤掩码全变成0,等于不过滤。3.3 解决方案: 要么统一 API,要么直接寄存器面对这种“两套库结构体不对齐”的问题,我建议两个方案选一个,不要扭扭捏捏混着用。方案一:统一使用GD32标准外设库。虽然代码风格跟HAL不一样,但至少不会踩“字段翻译”的坑,官方库内部已经把寄存器全部对齐好了。这也是我们后来在正式量产代码里采用的方式。方案二:不依赖任何库,直接在应用层写寄存器操作。这样做最可控,但你需要仔细对着参考手册抠寄存器位,适合CAN配置比较固定、不经常改的项目。下面给出一段代码对比,方便你直观看到两边字段的对应关系。逻辑完全一样,但API接口不同。先看STM32 HAL写法:/* STM32 HAL: 标准帧 0x123, 掩码 0x7FF, 关联 FIFO0 */ CAN_FilterTypeDef canFilter; canFilter.FilterBank 0; canFilter.FilterMode CAN_FILTERMODE_IDMASK; canFilter.FilterScale CAN_FILTERSCALE_32BIT; canFilter.FilterIdHigh (uint16_t)(((0x123U 0x7FFU) 5) 0xFFFFU); canFilter.FilterIdLow 0U; canFilter.FilterMaskIdHigh (uint16_t)(((0x7FFU 0x7FFU) 5) 0xFFFFU); canFilter.FilterMaskIdLow 0U; canFilter.FilterFIFOAssignment CAN_FILTER_FIFO0; canFilter.SlaveStartFilterBank 0; HAL_CAN_ConfigFilter(hcan, canFilter);再看GD32标准外设库的写法:/* GD32 标准外设库: 标准帧 0x123, 掩码 0x7FF, 关联 FIFO0 */ can_filter_parameter_struct canFilter; canFilter.filter_number 0; canFilter.filter_mode CAN_FILTERMODE_MASK; canFilter.filter_bits CAN_FILTERBITS_32BIT; canFilter.filter_fifo CAN_FIFO0; canFilter.filter_list_high (uint16_t)(((0x123U 0x7FFU) 5) 0xFFFFU); canFilter.filter_list_low 0U; canFilter.filter_mask_high (uint16_t)(((0x7FFU 0x7FFU) 5) 0xFFFFU); canFilter.filter_mask_low 0U; can_filter_init(CAN0, canFilter);注意,GD32标准库里CAN外设的实例名通常叫CAN0,不是STM32 HAL里的CAN1,这个细节也容易造成混乱。如果固件库版本较新,结构体字段名可能略有差异,建议以你实际用的库头文件为准,但核心的(ID 5)这个移位逻辑是躲不开的。3.4 实操心得: 过滤器配置的几条经验我自己的习惯是:不管原来ST工程里过滤器怎么配,移植到GD32后,先把过滤功能作为独立模块单独测试,不要等整个系统联调时再查。具体来说,可以用一个最简单的过滤器先验证:掩码全部清零,ID设为0,这样理论上可以接收所有报文;如果这个逻辑在GD32上不成立,说明过滤器配置层的底层有问题,先解决底层再谈业务过滤。如果“收所有报文”没问题,再逐步加上ID和掩码,这样能快速定位是位域问题还是API映射问题。另外,GD32的过滤器组如果比STM32多,尽量用低编号的过滤器组(0~13)。不是不能用高编号,而是低编号的寄存器映射在两边更接近,排查时可以少考虑一些差异。等低编号过滤器全部验证没问题,再根据实际需要选用高编号组。4. 坑三: 中断向量名不一致,回调函数成了摆设4.1 现象: 回调一次都不执行,还没错误提示这个坑最阴。代码编译没问题,烧录没报错,应用层也调用了HAL_CAN_Start_IT和HAL_CAN_Receive_IT,但发送完成回调、接收回调一次都不执行。你可以在回调函数里打断点,发现触发不了;然后在中断服务函数里打断点,发现压根没进中断;最后回头看,中断向量可能直接进了HardFault_Handler或者什么都没干。这种情况最打击人,因为一切看起来都是正常的,程序也没有崩溃,但功能就是不工作。我们当时排查了整整一个下午,把所有GPIO和过滤器都查了一遍,最后才发现问题出在启动文件的中断向量表。4.2 根因: GD32 和 STM32 对 CAN 外设的命名体系不同讲到根因,先说一个容易被忽视的命名差异:STM32把F103系列唯一的CAN外设叫做CAN1,中断服务函数是CAN1_RX0_IRQHandler;而GD32官方固件库习惯把它叫做CAN0,对应的中断服务函数是CAN0_RX0_IRQHandler。如果你替换后用的是GD32的启动文件startup_gd32f1xx.s,向量表里已经写好了CAN0_RX0_IRQHandler。但你的中断处理代码还是从ST工程里拷过来的,函数名是CAN1_RX0_IRQHandler,那么链接器找不到向量表里对应的强符号定义,只能把中断指向默认的Default_Handler,你的函数即使写了一大堆逻辑,也永远不会被调用。还有一种情况是反过来:如果你继续用STM32的启动文件,但中断函数名改成了CAN0_RX0_IRQHandler,同样会因为向量表里只有CAN1_RX0_IRQHandler而导致静默失效。这两个名字之间没有宏定义,编译器不会给你任何警告,只能在调试时摸黑排查。4.3 解决方案: 统一“启动文件 中断函数名”最稳的解决办法就是:用GD32启动文件,就用GD32命名风格的中断服务函数;用STM32启动文件,就用STM32命名风格。不要在一个工程里混用两套命名体系。如果你选择GD32启动文件 GD32标准外设库,中断服务函数的框架大概是这样的:/* GD32 标准外设库: 中断服务函数命名 */ void CAN0_RX0_IRQHandler(void) { can_message_struct rx_message; can_interrupt_flag_clear(CAN0, CAN_INT_FLAG_RFO0); can_receive(CAN0, CAN_FIFO0, rx_message); App_CAN_OnReceive(rx_message); }如果你确实需要在GD32上继续跑STM32 HAL库代码,那就不碰GD32的启动文件,继续用ST启动文件,中断处理函数沿用ST的命名:/* STM32 HAL 风格: 中断服务函数命名 */ void CAN1_RX0_IRQHandler(void) { HAL_CAN_IRQHandler(hcan); } void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rxHeader, rxData) HAL_OK) { App_CAN_OnReceive(rxHeader, rxData); } }从长期可维护性来说,我更建议替换时一步到位,直接切换到GD32官方标准外设库。STM32 HAL代码确实更现代,API也很舒服,但GD32不是ST,底层寄存器映射、位段定义、中断向量表都有自己的体系,硬要把HAL“搬”过来,虽然能跑通,但后期升级库版本或者换GD32新型号时,维护成本会很高。4.4 实操心得: 如何用 map 文件快速定位中断问题如果你已经踩了“回调不触发”的坑,别急着改代码,先看一眼编译生成的map文件。以Keil为例,点击Output标签页勾选“Browse Information”之后,编译生成的.map文件里会列出所有符号的地址。搜索你的中断处理函数名,比如CAN0_RX0_IRQHandler或CAN1_RX0_IRQHandler,然后看它的地址是否落在启动文件向量表所在区域。更直接的方法是:在.map文件里搜Default_Handler的引用。如果很多你“明明写了”的中断函数最后都指向Default_Handler,那就说明函数名和向量表里的符号没对上。这个方法比打断点快得多,尤其适合排查多个外设同时移植的场景。5. 替换 CAN 模块的现场排查速查表5.1 新板级替换流程从零开始把CAN模块从STM32整体搬到GD32,我建议按照以下顺序走,能省掉大量交叉排查时间:用GD32官方模板新建一个最小工程,先让GPIO和UART跑通,验证系统时钟、启动文件、烧录链路都没问题。打印或仿真读取PCLK1,确认APB1频率和预期一致。配好CAN引脚复用,用回环模式测试收发。这一步能排除一半的硬件问题,回环模式下CAN不需要接外部总线,如果回环都失败,赶紧查时钟和引脚;如果回环成功,说明CAN外设本身工作正常。回环通过后,再接外部总线或CAN分析仪,先不配过滤器,收所有报文,确认总线物理层和波特率正常。逐步加上ID过滤,验证过滤规则符合预期。测试中断回调,建议先单独测试发送完成中断,再测接收FIFO中断。这样能快速区分是CAN外设问题还是中断链路问题。最后再挂真实业务报文,做长时间老化。5.2 故障速查表故障现象可能原因优先排查节点离线,错误帧刷屏PCLK1或波特率配置错误,收发器引脚问题读PCLK1实际频率,核对分频CAN初始化失败CAN引脚复用没配好,时钟未使能查GPIO复用和RCU外设时钟使能能发不能收过滤器配置错误,接收中断服务函数名不对先做回环测试,再核对过滤器接收回调不触发中断向量名和启动文件不匹配查map文件,确认中断函数地址进HardFault中断函数名重复定义,弱符号被意外覆盖查map和启动文件,检查全局符号5.3 顺带说说 DFU 和烧录很多人在GD32替换的第一步就卡在烧录上。GD32第一次烧录时,如果进入DFU模式,PC端需要安装GD32的DFU驱动,和普通的USB CDC驱动不是一回事。我们的经验是:调试阶段直接用ST-Link或者J-Link通过SWD口烧录,完全没必要走DFU。等产品需要批量产线烧录时,再根据产线设备选型决定是否研究DFU。这个虽然和CAN模块没有直接关系,但它是替换项目最常见的“第一坑”,放在这里提醒一句。6. 写在最后: 我踩过坑后留下的几条原则如果只留一句话,我会说:GD32替换STM32,真正要改的不是芯片型号,而是“库的体系”。STM32 HAL库那一套以hcan.Instance CAN1开头的代码,放到GD32上不是不能跑,而是你必须把它的底层依赖全部对齐:时钟频率、过滤器结构体、中断向量名,这三个地方只要有一个没对齐,表面看起来编译通过、运行正常,实际功能就是不对。我个人在量产项目里最终选择了全面切到GD32官方标准外设库,虽然重写CAN驱动层花了两天时间,但后续调试、升级、换新型号的成本反而低。如果你实在想继续用STM32 HAL库,那就必须做好移植适配层,把GD32的寄存器操作封装成HAL风格接口,而不是直接拿ST的库硬编。最后再留一个小技巧:替换后的第一块板子,不要急着跑复杂业务逻辑,先用“回环模式CAN分析仪”做一次最小通信测试。这个测试能同时验证时钟、波特率、过滤器、中断回调四个环节,大概率能帮你在半小时内把坑全部暴露出来,比闷头查代码高效得多。
返回列表