
聊到GD32很多从51或者STM32转过来的朋友第一反应都是“这不就是个国产替代嘛”。确实GD32在很多引脚和底层寄存器上跟STM32有千丝万缕的关系但只要真上手做过项目你会发现区别远不止“换个Logo”这么简单尤其是CAN总线这种对时序和稳定性要求很高的外设。我最早接触GD32的CAN总线是在一个车载仪表盘配套项目里。当时手里的样片是GD32F103系列主控负责跟车身控制器通信跑的是500kbps的CAN总线。说实话第一次调的时候还是踩了不少坑——不是移植了STM32的库就万事大吉时钟树不一样、复用映射不一样、库函数细节也有差异。这篇文章我把从环境搭建到源码配置的完整思路写出来结合我自己实测过的工程配置希望能帮你少走点弯路。这篇文章适合谁手里有GD32开发板、想跑通CAN通信但还没头绪的人准备评估GD32的性价比、想看看CAN外设是否好上手的工程师以及项目进度紧、需要快速从零把CAN调通的单片机开发者。我会按从硬件到软件、从原理到代码的顺序讲尽量让不同基础的读者都能拿着直接用。1. 内容整体设计与思路拆解先定一个总体规划。玩转CAN总线不正确理解为你只是调试一个“串口高级版”。CAN总线的难点在于它是一套完整的、多层协议栈支撑的通信方案涉及硬件收发器、控制器寄存器配置、位时序计算、报文过滤、错误处理等等。如果你的思路只是“发数据、收数据”那后面遇到问题会非常难查。1.1 项目功能定位与系统组成一个最小的GD32 CAN通信系统包括这样几部分一个带CAN控制器的GD32芯片比如GD32F103、GD32F303、GD32F450一个CAN收发器芯片典型的是TJA1050、MCP2551或者SN65HVD230一根双绞线两端匹配120欧终端电阻一个上位机或另一个CAN节点用来验证收发单片机的CAN控制器负责协议处理、滤波、错误检测但它的电气信号是TTL级别的CAN_TX和CAN_RX不是差分信号所以必须外接CAN收发器把TTL电平转成CAN_H和CAN_L的差分信号。很多新手在这里栽跟头——拿示波器去点CAN_TX引脚看到了方波就以为信号是对的其实总线上的波形完全不一样。1.2 技术选型解析为什么选GD32而不直接用STM32既然很多代码都是兼容的为什么要专门用GD32做项目我个人的看法是成本、供货和本地化支持这三个因素排在最前面。GD32F103系列在功能上与同封装的STM32F103高度相似但主频更高比如GD32F103的最高主频能到108MHzSTM32F103是72MHzFlash和SRAM的配比也有差异。更重要的是GD32的生态已经比较完善了官方提供了GD32F10x固件库、GD32 Embedded Builder集成环境还有适配Keil、IAR的Pack包基本能无缝上手。当然这里要强调一句不能把GD32和STM32当成孪生兄弟看在所有开发中都直接套用。我先提三个自己踩过的坑后面再详细讲时钟树不同。GD32的PLL倍频配置跟ST的库函数不通用系统主频不同会导致CAN位时序的预分频设置完全不同。GPIO复用映射有差异。CAN的某些引脚在这个型号上映射到了别的复用功能上你如果不查数据手册盲目按STM32的引脚配置可能发不出去数据。库函数API细节有变化。比如GD32的can_parameter_struct、can_trasmit_message_struct这些结构体字段和ST的HAL库有明显差别直接替换不了。所以这篇文章里的所有源码我都会用GD32官方的标准外设库来写不是拿ST改改名就贴上来这点请你放心。2. 硬件准备与协议原理先行CAN总线的基础认知软件写得再好硬件接错了信号也出不去。反过来如果对CAN协议没有基本认知你也看不懂发送和接收状态寄存器到底在报什么错。这两个部分必须结合起来学。2.1 硬件连接的常见做法给一个非常标准的节点连接方式以GD32F103 TJA1050为例GD32_PB8 (CAN0_RX) - TJA1050 RXD GD32_PB9 (CAN0_TX) - TJA1050 TXD TJA1050 CANH - 总线CANH线 TJA1050 CANL - 总线CANL线 TJA1050 VCC - 5V注意有的板子是3.3V版本 TJA1050 GND - 公共地两个终端电阻每个节点不一定都要放但在总线两端必须各有一个120欧电阻。很多实验板会把终端电阻直接做在板子上如果你用两个带120欧电阻的板子相互通信就等于并了60欧会导致信号反射通信质量反而变差。这时候你就得飞线把其中一个电阻断开。收发器选型上我做了一个简单对照型号供电电压速率特点TJA10505V最高1Mbps最经典应用广泛但要注意5V与MCU电平匹配MCP25515V最高1MbpsMicrochip家的老牌产品耐压高SN65HVD2303.3V最高1Mbps适合3.3V单片机省去电平转换TJA10423.3V/5V最高1Mbps低功耗待机改进版GD32的IO耐压情况跟具体型号有关如果你用5V的TJA1050建议确认GPIO配置为开漏输出并接上拉或者干脆选3.3V的收发器更省心。我自己做实验最常用SN65HVD230一根杜邦线连GD32开发板不用操心电平问题。2.2 CAN协议中的关键概念有人说CAN协议复杂我倒觉得它的核心逻辑特别像公司开会发言规则所有人都能说话但发言前先“听”总线上有没有人在说如果同时有人抢麦优先级最高的ID获胜其他人自动闭嘴等下一轮消息发出去之后说的人要同时听如果听到的内容和自己说的对不上就知道出错了技术上对应几个概念帧类型。日常用得最多的是数据帧用来发真实数据。远程帧用来请求某个节点发送数据有点像“点对点催更”。错误帧和过载帧是节点在异常时主动发出的你可以不会主动构造它们但要能通过错误状态寄存器判断是什么情况导致了错误帧。仲裁机制。CAN总线上每个帧都有IDID数值越小优先级越高。当两个节点同时发数据CAN控制器逐位比较电平显性电平逻辑0会覆盖隐性电平逻辑1所以ID小的节点最终抢占总线。这也是为什么工程上通常把控制类消息的ID设得很小比如0x001这种。报文过滤。接收方不需要处理总线上所有报文可以通过验收屏蔽寄存器只接收关心ID的报文。这个功能放到GD32里就是CAN_FIFO的filter配置可以配置成列表模式或掩码模式。错误处理。CAN节点有三个状态主动错误、被动错误、离线。如果一个节点错误累计值过高它会自己退出总线不影响其他节点通信。很多朋友调试时发现“只有我这块板子收不到数据其他节点正常”最可能就是这个节点进入了Bus-Off状态。以上概念不必一次全懂先知道这些名词对应的大概场景后面结合寄存器配置再回头看会感觉通透得多。3. 工程环境搭建从零跑通第一个CAN实例这部分我不讲太多理论直接把从开箱到第一个CAN Demo跑通的流程过一遍。环境不同细节会有点差别但通用逻辑一致。3.1 开发环境与固件库准备我常用的方案是Keil MDK配合GD32官方固件库。很多人问为什么不用GD32 Embedded Builder其实那个工具官方做了很久界面类似STM32CubeMX生成代码的速度很快但初学阶段我还是推荐Keil原因很简单网上资料多、调试器兼容性好、出问题好查。需要准备的软件包Keil MDK 5.3x以上版本装好对应器件支持包GD32F10x固件库从GD官网下载或者直接在Gitee/GitHub上找官方镜像J-Link或者DAP-Link驱动建议先用DAP-Link便宜且供电方便安装GD32器件Pack这一步很多人会卡住。我建议不要直接双击Pack文件而是打开Keil的Pack Installer在“File - Import”里导入GD32官方提供的Pack这样可以自动处理依赖。有些旧版本Pack在导入时提示缺CMSIS版本你就顺手把CMSIS的Pack也装一下。3.2 新建工程时的关键配置我在Keil里新建GD32工程的固定套路在Project窗口里新建两个组Library存放固件库源文件和核心文件、User存放main.c等应用代码。把固件库里的gd32f10x_can.c、gd32f10x_gpio.c、gd32f10x_rcu.c、gd32f10x_usart.c调试用添加进去。在C/C选项卡里添加宏定义这个很关键比如我用的是GD32F103系列那么需要定义GD32F10X_MD或者根据型号选择具体看固件库手册不定义这个宏会导致system_gd32f10x.c编译出错。选择调试器的Flash下载算法在“Utilities - Settings”里加载对应型号的Programming Algorithm如果算法不对下载时就会提示No Algorithm found不解决这个问题后面烧程序都是空谈。3.3 J-Link烧录GD32的注意事项用J-Link给GD32烧录有一个小坑J-Link默认可能将芯片识别成STM32F103此时能连上内核但写到Flash时会出错。解决办法是给J-Link添加GD32的Device描述或者在Keil的Flash Download里配置GD32的算法文件。如果你只是想快速测试也可以用串口烧录。GD32支持串口ISP模式BOOT0拉高通过串口把HEX文件送进去。但这种方式适合固化程序调试阶段还是得用调试器否则printf是个大麻烦。4. 核心源码解析GD32的CAN外设配置与数据收发下面进入正题。这段代码我直接给了完整的初始化、发送和接收逻辑。其实代码本身不复杂复杂的是理解这个外设的工作逻辑。我会把每一步涉及到的寄存器构造讲清楚而不是只让你抄代码。4.1 初始化GPIO与CAN外设时钟在GD32里外设使用前必须先开时钟。很多问题就是漏掉了这一步导致寄存器写不进值。void can_gpio_config(void) { rcu_periph_clock_enable(RCU_GPIOB); rcu_periph_clock_enable(RCU_CAN0); gpio_af_set(GPIOB, GPIO_AF_9, GPIO_PIN_8); gpio_af_set(GPIOB, GPIO_AF_9, GPIO_PIN_9); gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_8); gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_9); }注意GPIO_AF_9这个复用号不是所有GD32系列都一样。GD32F10x系列里PB8/PB9复用CAN0AF号是9但如果你用的是GD32F30x或者GD32F4xx系列可能要用PD0/PD1等引脚且AF号不同。最稳妥的办法是查芯片数据手册里的“Alternate Function Mapping”表格。另外GPIO模式要设置成复用推挽不要配成普通推挽输出。很多人在这里偷懒直接配成GPIO_MODE_OUTPUT结果CAN总是进不了正常通信状态因为CAN控制器没法通过正确的电气特性操作总线。4.2 CAN控制器初始化与位时序配置这是整个配置中最重要的部分直接决定波特率对不对、通信稳不稳。void can_config(void) { can_parameter_struct can_parameter; can_deinit(CAN0); can_struct_para_init(CAN_INITIAL_STRUCT, can_parameter); can_parameter.time_triggered DISABLE; can_parameter.auto_bus_off_recovery ENABLE; can_parameter.auto_wake_up DISABLE; can_parameter.auto_retransmit ENABLE; can_parameter.rec_fifo_overwrite 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_13TQ; can_parameter.time_segment_2 CAN_BT_BS2_2TQ; can_parameter.prescaler 9; can_init(CAN0, can_parameter); }这里如果你用的是500kbps的波特率、APB1经过分频后是72MHz那总线位时间计算方式为CAN时钟 APB1时钟 / prescaler 72MHz / 9 8MHz。一个位时间由同步段(1TQ) BS1(13TQ) BS2(2TQ) 16TQ所以实际波特率 8MHz / 16TQ 500kHz采样点 (同步段 BS1) / 整个位时间 (1 13) / 16 87.5%。有的朋友图省事喜欢直接用库函数里的CAN_BAUDRATE参数比如can_parameter.resync_jump_width CAN_BT_SJW_1TQ; can_parameter.time_segment_1 CAN_BT_BS1_13TQ; can_parameter.time_segment_2 CAN_BT_BS2_2TQ; can_parameter.prescaler 4;如果你APB1不是72MHz这个prescaler就不能照抄。最典型的错误就是主频倍频配置不同导致APB1是54MHz或者36MHz但你仍然用prescaler为9的配置实际波特率就完全不对。所以一定要先搞清楚自己主频是多少再按公式反推prescaler别做只会抄代码的“CV工程师”。4.3 数据发送检查邮箱、填入内容、请求发送GD32的CAN外设和STM32一样有3个发送邮箱。发送前需要找一个空闲邮箱然后往里塞数据。uint8_t can_send_data(uint32_t id, uint8_t *data, uint8_t len) { can_trasmit_message_struct transmit_message; transmit_message.tx_sfid id; transmit_message.tx_efid 0; transmit_message.tx_ff CAN_FT_DATA; transmit_message.tx_ft CAN_FT_STANDARD; transmit_message.tx_dlen len; for (uint8_t i 0; i len; i) { transmit_message.tx_data[i] data[i]; } /* 返回邮箱号0-2表示发送成功255表示没有空闲邮箱 */ uint8_t mailbox can_message_transmit(CAN0, transmit_message); if (mailbox CAN_MAILBOX0 || mailbox CAN_MAILBOX1 || mailbox CAN_MAILBOX2) { return 1; } return 0; }这里有一个需要特别注意的点can_message_transmit函数在标准库里返回的是邮箱编号有的老版本返回状态如果你用旧库最好是实际打印一下这个返回值确保它命中了邮箱号区间。发送完成不代表总线上的其他节点已经正确接收了。GD32的CAN外设会自己处理仲裁和错误重发除非发送失败否则你不太需要关心总线细节。但如果你把auto_retransmit设置为DISABLE发送失败后就不会自动重发需要手动处理这在高实时性冗余设计中是有意义的。4.4 数据接收轮询方式与中断方式接收部分我建议直接使用中断方式。虽然轮询代码看起来简单但实际项目里你无法预测报文什么时候来轮询会占用CPU大量时间。先看轮询方式的接收核心代码void can_poll_receive(void) { can_receive_message_struct receive_message; if (can_receive_message_length_get(CAN0, CAN_FIFO0) ! 0) { can_message_receive(CAN0, CAN_FIFO0, receive_message); printf(Get ID: 0x%x, Len: %d\n, receive_message.rx_sfid, receive_message.rx_dlen); for (uint8_t i 0; i receive_message.rx_dlen; i) { printf(0x%02x , receive_message.rx_data[i]); } printf(\n); } }5. 中断接收还是DMA接收我的实测结论与建议很多初学者在CAN接收上会纠结“用中断还是DMA”。我直接用实测经验说结论如果你不是在做高带宽持续接收用中断就够了如果要把CPU解放出来再考虑DMA模式。5.1 什么情况下选中断接收CAN报文一个帧最多8字节数据即使1ms一帧每秒也就1000帧对MCU来说中断压力其实不大。中断服务函数只需要把数据拷贝到应用层缓冲区然后置一个标志位让主循环去处理。这种模式下功耗和实时性都很好。void CAN0_RX0_IRQHandler(void) { can_receive_message_struct receive_message; can_message_receive(CAN0, CAN_FIFO0, receive_message); /* 简单处理直接把收到的数据写到一个全局结构体里 */ last_rx_id receive_message.rx_sfid; last_rx_len receive_message.rx_dlen; for (uint8_t i 0; i receive_message.rx_dlen; i) { last_rx_data[i] receive_message.rx_data[i]; } rx_flag 1; }这里我要提一个很多教程都没讲清楚的细节CAN0_RX0_IRQHandler里的RX0指的是FIFO0的接收中断不是第0通道。如果你配置了多个FIFO那中断函数有CAN0_RX0_IRQHandler和CAN0_RX1_IRQHandler两个别只开FIFO1却只在RX0中断里收数据那就永远收不到。5.2 什么情况下选DMA接收DMA的核心价值是“搬运数据不占CPU”。但CAN控制器的接收流程是总线数据进入CAN控制器内部FIFO再由你通过寄存器读出到内存。DMA做的只是后一半搬运工作。问题在于CAN的报文是不定长的DMA搬运完成后你可能无法预知应该读多少字节。所以在DMA模式下常见做法是先把固定长度的头部信息读出来根据数据长度再次触发DMA搬运剩余数据。这大大增加了复杂度。我的个人建议低于1Mbps的CAN总线应用根本不需要DMA。只有当你有多个CAN通道、同时接收大量报文CPU忙不过来时才需要认真设计DMA接收流程。否则引入DMA反而会加大调试难度。6. 常见问题与排查技巧实录GD32 CAN调试避坑指南这部分我整理了自己和身边朋友实际踩过的坑比任何理论都值钱。你按照这个顺序查基本能解决90%的CAN不通信问题。6.1 连不上总线示波器看不到波形先确认GPIO复用配置正确再确认CAN收发器供电正常。我遇到过一次非常诡异的情况代码在STM32上跑得好好的移植到GD32上之后CAN_H和CAN_L之间就是没有差分信号。后来查数据手册发现该型号的PB9如果完全按STM32F103的配置配置成了普通推挽输出而不是复用功能导致CAN_TX信号根本没连接到收发器。所以你要检查两件事有没有调用gpio_af_set配置复用功能有没有把GPIO模式设置成GPIO_MODE_AF这两个配置缺一个收发器都收不到来自MCU的CAN信号。6.2 用USB-CAN分析仪能收到自己发的数据但另一个MCU节点收不到这种现象多半是波特率不一致或者采样点设置差别太大。用逻辑分析仪抓一下总线上实际波形测量一个位的实际时间如果500kbps下实测位时间是2.5us说明波特率确实不对。重新按APB1时钟计算prescaler即可。另一个隐蔽的坑是两个节点用了相同的波特率但一个采样点设在50%一个设在87.5%。在总线较长、信号上升沿不够陡时就可能出现一个节点能正确采样、另一个节点频繁报错。调试时尽量统一采样点在75%到87.5%之间。6.3 两个节点通信一个节点报错提示Bus-OffBus-Off的本质是发送错误计数超过255次节点自动退出总线。常规排查顺序先看这两块板子的地线是否共地CAN总线虽然是差分传输但收发器之间必须有公共参考地否则共模电压会飘导致总线干扰。再查终端电阻是否匹配两边总线端各一个120欧不要多加也不要漏加。然后查波特率配置。把两个节点的CAN_BT寄存器值对比一下看看是不是一个用8MHz CAN时钟一个用9MHz CAN时钟。数值一样但寄存器配置不同算出来波特率就可能不一样。最后检查节点是不是同时用两个ID发送如果一个节点发送的ID恰好和另一个节点的应答帧冲突也可能导致仲裁失败。6.4 错误帧数量很多通信间歇性失败这通常和硬件走线、电磁环境有关。CAN总线要用双绞线而且最好屏蔽层接地。实验阶段用杜邦线虽然能通但距离长了以后输出波形质量急剧下降。如果你只是做开发板调试建议把CAN_H和CAN_L线绕在一起做成简易双绞线同时缩短总线长度。个人经验是只要线尾的120欧电阻焊接牢靠双绞线不合理也能通但不稳定就是它带来的。6.5 烧录配置的杂项问题很多人会在工程里遇到Cannot access target或者No Cortex-M SW Device Found这个和CAN本身关系不大但几乎每个人都会遇到一次。先看调试器是否被正确识别再看目标板供电是不是正常。如果之前有程序把SWD引脚复用了确实会导致连不上调试器这时候按住复位键再点下载靠运气抢时间。7. 实战扩展多节点组网与数据解析CAN的魅力在于多节点组网。我直接把经验再延伸一步帮你把系统串起来。7.1 多节点组网的地址分配不要把ID理解为主机地址。CAN是广播式的每个节点都能收到总线上所有报文关键在于ID的优先级和内容属性。一般的工程习惯是0x000~0x07F控制类高优先级报文周期发送0x080~0x0FF状态类报文周期发送0x100~0x1FF诊断类报文按需发送例如发动机转速报文固定ID为0x0A1每10ms发送一次里面包含转速、水温、油量等参数。另一个节点想用这个数据就配置CAN过滤器只接收0x0A1。7.2 数据解析与PDU设计技巧CAN帧里最多8字节的数据如果数据量超过8字节你需要自己定义拆包组包的规则。常见做法是采用类似UDS的协议每一帧的前两个字节是服务ID和数据长度后面最多6字节是有效数据。多帧传输时加序列号。比如要传一个字符串“HelloCAN”可以定义Byte0: 0x01 表示握手开始Byte1: 0x07 表示长度Byte2~Byte7: “HelloC”前6个字节第二帧Byte0: 0x02 表示续帧Byte1: 0x01 表示还剩余1个字节Byte2: “AN”接收端按这个规则重新拼接就还原出原始字符串。8. 调试心得收尾玩GD32的CAN总线说到底就是把三件事做好时钟算准、GPIO复用对、位时序不出错。只要这三件事没问题后面的数据收发就是水到渠成。如果你踩到问题先用逻辑分析仪看物理波形再查寄存器配置不要一开始就怀疑芯片本身GD32的CAN外设经过这么多项目验证稳定性是没什么大问题的。我个人在实际项目里的习惯是每个板卡的CAN初始化代码里预留一个自检模式。上电后如果检测到BOOT引脚为高就进入CAN回环测试自发自收把接收到的数据通过串口打印出来。这样即使没有外部节点也能快速验证硬件通路是否正常。建议你也把这个小功能做成模板排查问题会方便很多。最后再分享一个小技巧调试CAN时别忘了多看错误状态寄存器。GD32库里有can_error_state_get之类的函数能直接返回当前错误状态。通过它判断节点是在主动错误、被动错误还是离线状态比你在那瞎猜快得多。后面如果你想往深处玩可以继续研究多CAN通道网关、利用过滤器的掩码模式做复杂路由这些都是从今天这个Demo能自然延伸出去的方向。