简介:这份资源是面向嵌入式初学者与STM32开发者的CAN总线通信程序例程,基于STM32F103ZET6单片机与官方固件库编写,已在开发板上调试通过,可用于学习CAN外设初始化、报文收发与中断处理等核心环节,帮助读者快速搭建总线通信实验环境。压缩包共70个文件,以30个h头文件与26个c源文件为主体,涵盖外设驱动、中断服务与主程序逻辑,另含uvproj、uvopt等Keil工程配置及若干备份文件,整体约188KB,目录结构清晰,便于按模块查阅与移植。目前已有2542人学习下载,适合作为CAN通信入门与工程模板参考。读者可从中获取完整的固件库工程框架、CAN配置与收发流程示例,以及中断与平台配置的参考写法,为后续寄存器版本学习与项目移植打下基础。
1. 从一块工控板说起:STM32F103ZET6 的 CAN 例程到底能跑多稳
前阵子帮一个做工业控制的朋友调板子,现象很典型:两块 STM32F103ZET6 板子,一块做主控发指令,一块做执行单元收指令,中间走 CAN 总线,结果上电后收发都不稳定,偶尔通、偶尔丢帧,示波器看波形又“看着挺正常”。这种玄学问题最后查出来是终端电阻和波特率配置对不上。也正因为这类场景太常见,我才把手里这份 STM32F103ZET6 单片机 CAN 总线通信程序例程重新拆了一遍。它本质上是一套基于 STM32F103ZET6 的 bxCAN 外设驱动例程,覆盖初始化、过滤器配置、收发中断、回环自测这几块,适合刚接触单片机 CAN 通信的工程师,也适合已经会点灯但没真正跑过总线通信的人拿来当起点。你拿到手能直接编译、烧录、观察收发,不用从零啃参考手册的寄存器位定义。
2. 拆开 bxCAN:STM32F103ZET6 的 CAN 外设到底怎么初始化
2.1 为什么选 bxCAN 而不是软件模拟时序
STM32F103ZET6 属于增强型产品线,片上集成的 CAN 控制器是 bxCAN,支持 CAN 2.0A 和 2.0B 协议,也就是标准帧和扩展帧都能处理。很多新手会问:能不能用普通 GPIO 加定时器软件模拟 CAN 时序?理论上可以,但实际项目里基本没人这么干。原因有三点:第一,CAN 的位填充、CRC 校验、错误帧处理逻辑非常繁琐,软件模拟很难保证时序精度;第二,bxCAN 自带 3 个发送邮箱和 2 个接收 FIFO,硬件层面就帮你做了优先级仲裁和缓冲;第三,总线错误管理(错误计数器、主动错误/被动错误状态切换)是硬件自动完成的,软件模拟根本追不上。所以选型结论很明确:只要芯片带 bxCAN,就用硬件外设,例程里也是这么做的。
bxCAN 的初始化核心是三个寄存器组:CAN_MCR(主控制)、CAN_BTR(位时序)、CAN_FMR(过滤器主控)。例程里通常先退出睡眠模式、进入初始化模式,再配置位时序,最后设置过滤器并回到正常模式。这个顺序不能乱,尤其是初始化模式必须在配置 BTR 之前进入,否则写入无效。
2.2 位时序参数怎么算:波特率不是拍脑袋填的
CAN 波特率的计算公式是:波特率 = APB1 时钟 / (分频系数 × (1 + BS1 + BS2))。STM32F103ZET6 的 CAN 挂在 APB1 上,常见系统时钟 72MHz 时 APB1 为 36MHz。假设你要 500kbps,分频系数取 4,那么 36MHz / 4 = 9MHz,再除以 (1 + BS1 + BS2) 要等于 500k,即 (1 + BS1 + BS2) = 18,所以 BS1 + BS2 = 17。采样点一般设在 75% 到 87.5% 之间,取 BS1 = 14、BS2 = 3,采样点 = (1 + 14) / 18 ≈ 83.3%,比较稳妥。
// CAN 位时序配置,目标波特率 500kbps,APB1 = 36MHz CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; // 同步跳转宽度 1 个时间单元 CAN_InitStructure.CAN_BS1 = CAN_BS1_14tq; // 时间段 1 占 14 个时间单元 CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq; // 时间段 2 占 3 个时间单元 CAN_InitStructure.CAN_Prescaler = 4; // 分频系数 4 // 实际波特率 = 36MHz / (4 * (1 + 14 + 3)) = 500kbps这里 SJW 取 1 是因为同总线设备时钟偏差不大,取大了反而容易在噪声环境下误同步。BS1 和 BS2 的分配直接影响采样点位置,BS1 越大采样点越靠后,对长线缆的延迟容忍度更高,但太小又容易采到边沿。我一般建议先用例程默认值跑通,再用示波器量实际位宽反推验证。
2.3 过滤器配置:别让无关帧把 FIFO 撑爆
bxCAN 有 14 个过滤器组(互联型是 28 个,ZET6 是 14 个),每个组可以配置为屏蔽位模式或标识符列表模式。例程里通常用屏蔽位模式,只接收特定 ID 范围的帧。如果你不配置过滤器,默认是接收所有帧,短时间测试没问题,但总线负载一高,FIFO 溢出就会丢帧,而且这种丢帧在调试时很难发现,因为中断里读到的永远是最新的帧。
// 过滤器 0 配置为屏蔽位模式,只接收标准帧 ID 0x123 CAN_FilterInitStructure.CAN_FilterNumber = 0; CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh = 0x123 << 5; // 标准帧 ID 左移 5 位 CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0xFFFF; // 高 16 位全部必须匹配 CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; CAN_FilterInit(&CAN_FilterInitStructure);注意标准帧 ID 要左移 5 位再写入 FilterIdHigh,因为寄存器里 ID 是从 bit5 开始对齐的,这个坑我见过不止一个人踩。屏蔽位为 1 表示该位必须匹配,为 0 表示不关心。如果你要接收一组 ID,比如 0x120 到 0x12F,可以把掩码低 4 位设成 0,其余设成 1。
3. 收发中断与回环自测:把通信链路先跑通再上总线
3.1 发送流程:三个邮箱不是随便用的
bxCAN 有 3 个发送邮箱,例程里通常用轮询方式找一个空邮箱,填入标识符、数据长度和数据,然后请求发送。这里有个细节:邮箱优先级由标识符数值决定,数值越小优先级越高,而不是按请求顺序。如果你有多个不同优先级的报文,要提前规划 ID 分配,别指望先请求的先发。
// 发送一帧标准数据帧,ID = 0x123,数据长度 8 CanTxMsg TxMessage; TxMessage.StdId = 0x123; TxMessage.ExtId = 0; TxMessage.IDE = CAN_Id_Standard; // 标准帧 TxMessage.RTR = CAN_RTR_Data; // 数据帧 TxMessage.DLC = 8; for (uint8_t i = 0; i < 8; i++) { TxMessage.Data[i] = i; // 填充测试数据 } uint8_t mailbox = CAN_Transmit(CAN1, &TxMessage); if (mailbox == CAN_TxStatus_NoMailBox) { // 三个邮箱全满,需要等待或丢弃 }CAN_Transmit 返回的是邮箱编号,如果返回 NoMailBox 说明总线太忙或者对方没有应答导致邮箱一直不释放。这时候不要死等,常见做法是记录一次发送失败计数,超过阈值就报错。另外,发送失败中断里可以读到具体的错误码,比如仲裁丢失、位填充错误等,这些信息对排查物理层问题很有用。
3.2 接收中断:FIFO 溢出标志必须清
接收中断里要做两件事:读报文、清标志。很多人只读报文忘了清 FIFO 溢出标志,结果跑一段时间后中断再也不进来了。例程里一般会在中断服务函数里判断 CAN_IT_FMP0 和 CAN_IT_FF0 两个标志。
void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) != RESET) { CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); // 处理接收到的数据 process_can_data(RxMessage.StdId, RxMessage.Data, RxMessage.DLC); } if (CAN_GetITStatus(CAN1, CAN_IT_FF0) != RESET) { // FIFO 溢出,说明处理速度跟不上总线速率 CAN_ClearITPendingBit(CAN1, CAN_IT_FF0); overflow_count++; } }FIFO 溢出标志不清会一直触发中断,但更隐蔽的问题是:溢出后 FIFO 里的旧数据会被新数据覆盖,你读到的可能不是完整的一帧序列。所以一旦发现溢出计数在涨,要么提高处理速度,要么加过滤器减少无关帧。
3.3 回环自测:不接总线也能验证驱动
例程里通常带一个回环模式(Loopback)测试,把 CAN_MCR 的 SILM 和 LBKM 位置起来,发送的帧不经过物理总线直接回送到接收 FIFO。这个模式特别适合板子刚焊好、还没接总线的时候验证驱动是否正常。
// 进入回环静默模式,用于自测 CAN1->MCR |= CAN_MCR_SILM | CAN_MCR_LBKM; // 发送一帧,然后在接收中断里应该能收到相同 ID 和数据 // 测试完成后记得清除这两位,回到正常模式 CAN1->MCR &= ~(CAN_MCR_SILM | CAN_MCR_LBKM);回环测试能过,说明初始化、过滤器、收发中断逻辑都没问题,剩下的就是物理层的事了。如果回环都过不了,别急着查线,先看时钟使能和 GPIO 复用配置对不对。
4. 避坑排查:CAN 通信调不通时先看这五条
4.1 现象:回环模式正常,一接总线就丢帧
原因:终端电阻缺失或阻值不对。CAN 总线两端各需要 120 欧姆终端电阻,很多开发板只焊了一端,或者根本没焊。没有终端电阻时,信号反射会导致位采样错误,表现就是偶尔通、偶尔丢。
解决:用万用表量 CANH 和 CANL 之间的电阻,断电状态下应该是 60 欧姆左右(两个 120 欧姆并联)。如果量出来是 120 欧姆,说明只焊了一端;如果是无穷大,两端都没焊。
4.2 现象:发送一直失败,邮箱不释放
原因:总线上没有其他节点应答。CAN 协议要求数据帧必须被至少一个节点应答(ACK 位),如果总线上只有你一个节点在发,发送会一直重试直到错误计数溢出。
解决:至少接两个节点,或者用回环模式自测。单节点调试时不要用正常模式发帧,这是新手最容易翻车的地方。
4.3 现象:接收中断进不来,但示波器看总线有波形
原因:过滤器配置错误,把需要的帧过滤掉了。常见的是标准帧 ID 没有左移 5 位,或者掩码设成了全 0 导致所有位都不关心但模式选错。
解决:先把过滤器配置成接收所有帧(掩码全 0,模式选屏蔽位),确认能收到后再逐步收紧。另外检查 CAN_FilterActivation 是否使能,这个漏了过滤器不生效。
4.4 现象:通信一段时间后死机或数据错乱
原因:FIFO 溢出后没有正确处理,或者中断优先级配置不当导致接收中断被其他中断打断太久。
解决:在接收中断里只做数据拷贝,复杂处理放到主循环。同时把 CAN 接收中断优先级设高一点,但不要高过系统滴答定时器,否则会影响系统时基。
4.5 现象:两块板子单独测试都正常,连在一起就不通
原因:波特率配置不一致。看起来都是 500k,但一块板子的晶振是 8MHz,另一块是 12MHz,分频系数没跟着改,实际波特率差了几个百分点,短帧可能碰巧能通,长帧必丢。
解决:统一晶振频率和分频系数,或者用示波器量一位的时间宽度反推实际波特率。CAN 对波特率偏差的容忍度大概在 0.5% 以内,超过就不可靠。
5. 进阶技巧:用 ID 规划做优先级调度与错误计数监控
例程跑通之后,真正上项目还要解决两个问题:报文优先级怎么排、总线健康度怎么监控。CAN 的仲裁机制决定了 ID 数值越小优先级越高,所以紧急报文(比如急停指令)要分配最小的 ID,状态上报类报文可以用大 ID。我一般会把 ID 按功能分段:0x000 到 0x0FF 给控制指令,0x100 到 0x1FF 给状态反馈,0x200 以上给调试信息。这样过滤器配置也简单,一个过滤器组就能覆盖一个段。
错误计数监控是另一个容易被忽略的点。bxCAN 的 CAN_ESR 寄存器里有 TEC(发送错误计数)和 REC(接收错误计数),这两个值超过 127 会进入被动错误状态,超过 255 会进入总线关闭状态。你可以在主循环里定期读这两个值,一旦发现持续增长就说明物理层有问题。
// 定期检查错误计数,判断总线健康度 uint8_t tec = (CAN1->ESR >> 16) & 0xFF; uint8_t rec = (CAN1->ESR >> 24) & 0xFF; if (tec > 96 || rec > 96) { // 错误计数偏高,检查终端电阻和线缆 bus_health_warning = 1; } if ((CAN1->ESR & CAN_ESR_BOFF) != 0) { // 总线关闭,需要重新初始化 CAN CAN_DeInit(CAN1); CAN_Init(CAN1, &CAN_InitStructure); CAN_FilterInit(&CAN_FilterInitStructure); }总线关闭恢复这个逻辑,例程里不一定有,但实际项目里必须加。我见过一个现场设备,总线被干扰导致 BOFF 后没有自动恢复,整条线瘫了半小时才有人发现。从那以后我每次写 CAN 驱动,都会在 1ms 定时器里轮询一次 ESR,把 TEC、REC 和 BOFF 状态一起处理掉。另外,回环自测建议保留在工程里,每次改完过滤器或位时序,先跑一遍回环再上总线,能省掉很多插拔线缆的时间。希望这些拆解能帮到你,少走一点我当年走过的弯路。
本文还有配套的精品资源,点击获取