
简介针对意法半导体STM32F072微控制器的CAN通信实验例程适合嵌入式初学者与工程师快速上手CAN总线开发。资源围绕CAN外设的初始化、滤波器配置、报文收发与错误处理展开提供正常模式和回环模式两套工程并附带USART、PWM、TIM、LED等外设演示代码便于对照学习代码注释详细可灵活移植到其他Cortex-M0项目中。压缩包共585个文件、1.85MB以129个h头文件、93个c源文件、81个汇编文件为主另有uvproj/ewp/icf等工程配置、bat调试脚本、hex/srec固件及PDF说明文档目录结构完整可直接导入Keil或IAR进行编译烧录。已有939人下载学习对理解CAN底层机制和STM32外设联动设计有较高参考价值亦适合课程设计、竞赛备赛等场景使用。1. 为什么拿 STM32F072 练 CAN而不是 F1 或 F4有人练 CAN 急着上 F103 或 F407我反而建议先摸 STM32F072。F072 的 bxCAN 把邮箱、FIFO、掩码过滤和位时序都直接暴露成寄存器出问题时你能看到真正的现场20CAN 实验例程讲的就是这类小实验——波特率怎么配、仲裁谁赢、报文怎么过滤、错误怎么恢复每个实验只改少量代码就能验证一个点。它适合刚把串口调通、准备接触总线通信的嵌入式新手也适合长期写业务代码、想补齐 CAN 底层的熟手。下面不按某份源码包去复述只顺着 F072 外设把常见做法写清楚照这个思路搭一套例程can通信中的多数问题你都能现场定位。2. STM32F072 的 CAN 定位与位时序计算2.1 先分清 bxCAN 和 FDCANF072 能做什么、不能做什么STM32F072 这颗 48MHz 的 Cortex-M0 芯片用的是 bxCAN支持 CAN 2.0A/B也就是标准帧 11 位 ID 加扩展帧 29 位 ID它不支持 CAN FD。FD 那套 64 字节数据段、可变位率需要换带 FDCAN 外设的芯片比如 STM32G0 或 F3 系列。这一点在拆 20 个实验时先定边界帧格式、仲裁、过滤器、错误恢复都在 F072 上做涉及 CAN FD 的对比单独列一个“换芯片”实验。接收路径上F072 有 3 个发送邮箱和 2 个接收 FIFO每个 FIFO 可以装 3 帧读慢了数据会被覆盖所以例程里要么开接收中断要么让主循环频繁轮询。注意邮箱不是环形缓冲硬件按优先级调度发送没有“队列”语义过滤器是配在接收路径上的硬件不是每帧先到软件再由软件过滤。2.1.1 bxCAN 三个容易记错的结构邮箱不是环形缓冲发送邮箱 3 个硬件按优先级调度不具备队列语义过滤器是配在接收路径上不是每帧都先到软件再由软件过滤波特率误差由采样点决定不是把预分频凑个整就行2.2 波特率不是配出来的是算出来的CAN 的位时间由同步段、传播段、相位缓冲段 1BS1、相位缓冲段 2BS2组成。F072 的 CAN_BTR 寄存器只暴露 SJW、TS1、TS2、BRP 四个字段TS1 包含传播段加 BS1所以例程里看到的 BS1、BS2 是合并后的概念。波特率公式Baud PCLK / (BRP × (1 TS1 TS2))采样点 (1 TS1) / (1 TS1 TS2)采样点比波特率更能影响能否正确收到对端的帧一般建议落在 75%80%。最常翻车的地方是把 BS1BS2 兑成 16 个 TqBRP 拉很大然后两个节点互发收不到问题往往出在采样点低于 65%。下面这组是 PCLK48MHz 下的常用配置目标波特率BRPTS1(Tq)TS2(Tq)SJW(Tq)采样点125 kbit/s24123181.25%250 kbit/s12123181.25%500 kbit/s6123181.25%1 Mbit/s3123181.25%注意寄存器里 TS1、TS2 存的是“Tq 数减 1”即 TS112 时寄存器写 11TS23 时寄存器写 2。总线上节点一多最容易出现的现象是两两通信都正常、多挂几个就丢帧原因往往是采样点不一致A 节点在 83% 采样B 节点在 70% 采样经过多级收发器后边沿位置差异被放大。CAN 靠硬同步和重同步重新对齐本地位时序BS1/BS2 分配过窄时重同步的 SJW 跟不上正负误差长连续位之后整帧错位。配置位时序时我一般先把 SJW 设为 1用示波器测真实波特率与设置值对比误差偏大再调 SJW 为 2。不要一上来就把 SJW 拉满SJW 过大会影响同步边缘判定某些收发器组合下反而加重连续重同步导致的填充错误。2.2.1 时钟源的选择边界F072 用内部 HSI 也能跑 CAN但 HSI 校准后的误差有百分之几超出 CAN 协议对位时序的容差要求。正式实验请用外部晶振8MHz 进 PLL 到 48MHz。实验阶段为了验证错误恢复可以故意用 HSI 做节点观察总线错误帧和错误计数变化。2.3 滤波器是硬件侧“消费”不是软件侧“捡”F072 有 14 个滤波器组每个 32 位可配为标识符列表模式或掩码模式。标准场景我用掩码模式CAN_FiR1 写要匹配的 IDCAN_FiR2 写掩码。掩码为 0 的位表示“不关心”为 1 的位表示“必须相等”。很多人写成“1 表示不在乎”结果对端帧全收这是过滤实验最容易翻车的地方。打开滤波器组的寄存器代码如下注意改滤波器前必须把 FMR 的 FINIT 置 1void can_filter_mask(uint32_t filter_id, uint32_t id, uint32_t mask) { CAN1-FMR | CAN_FMR_FINIT; /* 进入初始化模式才能改滤波器 */ CAN1-FM2R ~(1UL filter_id); /* 0掩码模式1标识符列表 */ CAN1-FS2R ~(1UL filter_id); /* 032 位宽度 */ CAN1-FFA2R ~(1UL filter_id); /* 关联到 FIFO0 */ CAN1-sFilterRegister[filter_id].FR1 (id 21); /* 标准帧 ID 左移 21 位 */ CAN1-sFilterRegister[filter_id].FR2 (mask 21); /* 掩码同样左移 */ CAN1-FMR ~CAN_FMR_FINIT; /* 退出初始化模式 */ }关键参数说明标准帧的 11 位 ID 在寄存器的 [31:21] 位所以 id 和 mask 都要左移 21。调用can_filter_mask(0, 0x123, 0x7FF)表示只放行 ID 为 0x123 的帧把掩码改成 0x7F8则低 3 位不关心0x123 和 0x12B 都能通过。改完不要忘记清 FINIT否则后续发送接收都异常。整套流程失败的典型表现是所有帧都能进 FIFO或者一帧都进不了。3. 最小可运行例程寄存器版 C 代码从零收发3.1 GPIO 复用PA11/PA12 和 PB8/PB9 怎么选F072 的 CAN 收发引脚有 PA11/PA12 和 PB8/PB9 两组。走线短、旁边无强干扰我一般用 PA11/PA12PB8/PB9 如果和调试用的 UART 冲突要重新规划引脚。GPIO 要点是复用功能选 AF4、开复用推挽不要开成普通输出否则总线电平被拉死。引脚功能复用PA11CAN_RXAF4PA12CAN_TXAF4PB8CAN_RXAF4PB9CAN_TXAF4注意 AF4 是 F072 专用配置换到 F103 后 CAN 引脚复用是 AF9不能照抄。很多人把例程从 F072 移到其他型号后第一个“不工作”就出在这里。3.2 初始化序列80 行不到跑通初始化顺序是开时钟 → 配 GPIO → 配 CAN 模式与位时序 → 配过滤器 → 退出初始化。顺序不能颠倒退出初始化必须在滤波器配置之后执行。/* 前提系统时钟 48MHzAPB1 PCLK 48MHz */ void can_init(void) { RCC-IOPENR | RCC_IOPENR_GPIOAEN; /* 使能 GPIOA 时钟 */ GPIOA-MODER ~(GPIO_MODER_MODER11 | GPIO_MODER_MODER12); GPIOA-MODER | (GPIO_MODER_MODER11_1 | GPIO_MODER_MODER12_1); /* 复用模式 */ GPIOA-AFR[1] | (0x4U (3 * 4)) | (0x4U (4 * 4)); /* AF4 */ RCC-APB1ENR | RCC_APB1ENR_CAN1EN; /* 使能 CAN1 时钟 */ CAN1-MCR | CAN_MCR_INRQ; /* 请求进入初始化模式 */ CAN1-MCR ~CAN_MCR_SLEEP; /* 退出 sleep否则寄存器写不进 */ CAN1-MCR | CAN_MCR_NART; /* 关闭自动重传便于实验观察 */ CAN1-BTR (CAN_BTR_SJW_0 | /* SJW1Tq寄存器值0 */ (11U CAN_BTR_TS1_Pos) | /* 寄存器值11实际 12Tq */ (2U CAN_BTR_TS2_Pos) | /* 寄存器值2实际 3Tq */ 3U); /* BRP3得到 1Mbit/s */ can_filter_mask(0, 0x123, 0x7FF); /* 只放行 0x123 */ CAN1-MCR ~CAN_MCR_INRQ; /* 退出初始化 */ while (CAN1-MSR CAN_MSR_INAK); /* 等待退出完成 */ }代码逻辑说明BTR 里 TS1、TS2 是“Tq 数减 1”所以 12 和 3 要写成 11 和 2。最后 while 等待 INAK 清零是必须的退出初始化模式是异步过程不等它完成就发请求会被硬件丢弃。NART 位关闭自动重传对端没回 ACK 时帧只发一次实验时便于观察失败现场正式双向通信一般保持自动重传开启。3.3 发送一个标准帧邮箱操作与 TME 位发送流程是找一个空闲邮箱 → 写 ID/DLC/数据 → 分两次写 TIR 请求发送。邮箱空闲状态看 TSR 的 TME0/TME1/TME2。uint8_t can_send_std(uint32_t id, uint8_t *data, uint8_t len) { uint32_t mailbox 0; if (len 8) len 8; /* DLC 只有 4 位最多 8 字节 */ if (CAN1-TSR CAN_TSR_TME0) mailbox 0; else if (CAN1-TSR CAN_TSR_TME1) mailbox 1; else if (CAN1-TSR CAN_TSR_TME2) mailbox 2; else return 0xFF; /* 三个邮箱都在忙 */ CAN1-sTxMailBox[mailbox].TIR (id 21); /* 标准帧 ID 放 [31:21] */ CAN1-sTxMailBox[mailbox].TDTR len; /* DLC 低 4 位有效 */ CAN1-sTxMailBox[mailbox].TDLR data[0] | (data[1] 8) | (data[2] 16) | (data[3] 24); CAN1-sTxMailBox[mailbox].TDHR data[4] | (data[5] 8) | (data[6] 16) | (data[7] 24); CAN1-sTxMailBox[mailbox].TIR | CAN_TIxR_TXRQ; /* 请求发送 */ return 0; }几个容易误用的点TIR 的高 11 位是标准 ID不是低 11 位ID 和 TXRQ 要分两次写一次写完整 TIR 在部分编译器优化下会丢请求。数据字节序是小端data[0] 在 TDLR 最低 8 位data[3] 在最高 8 位解析报文时别按大端读。三个邮箱同时挂起时发送优先级由 ID 决定ID 小者优先不要误以为邮箱号越小越先发。3.4 接收一个标准帧FIFO 挂起与中断接收侧我一般开 FIFO0 消息挂起中断在 CAN1_IRQHandler 里读帧并释放邮箱void CAN1_IRQHandler(void) { if (CAN1-RF0R CAN_RF0R_FMP0) { /* FIFO0 有消息挂起 */ uint32_t id CAN1-sFIFOMailBox[0].RIR 21; uint8_t dlc CAN1-sFIFOMailBox[0].RDTR 0xF; uint32_t low CAN1-sFIFOMailBox[0].RDLR; uint32_t high CAN1-sFIFOMailBox[0].RDHR; /* 这里把 id、dlc、low、high 交给应用层解析 */ CAN1-RF0R CAN_RF0R_RFOM0; /* 释放邮箱必须写 1 清 */ } }释放邮箱这一步最容易漏漏了 FIFO 会满后续帧进不来表现是“收得到前面几帧然后永远沉默”。接收中断里不要做重活把数据拷贝出去就立即返回。如果不想用中断主循环轮询 FMP0 也可以但要保证轮询周期小于总线帧间隔。4. 从 1 到 20 个实验怎么拆过滤、仲裁与异常恢复4.1 第 1 个实验先做环回这是所有人的第一课没有对端节点时把 BTR 的 LBKM 位置 1发送帧会绕回接收路径单独调试发送链路。环回模式下 ACK 由内部产生不需要总线上的其他节点应答。例程里这个实验的验证指标是三个发送后 TME 释放、FIFO0 挂起、收到的 DLC 与发送一致。环回实验顺便把 can报文解析练一遍把 RDLR、RDHR 里 8 字节按小端拼出来和发送数据对比。CAN 报文内部字节序是小端第一个字节在低 8 位解析时高低 32 位读反是最常见的报文解析错误。第 1 个实验跑通后后面可以按这个套路拆实验编号实验点关键代码改动验证方式1环回收发BTR 置 LBKM1内部自检无需对端2正常模式双节点关 LBKM两板对接对端显示 IDDLC3扩展帧 29 位 IDTIR 的 IDE 位置 1收到 29 位 ID4掩码过滤FR1/FR2 填 ID 与掩码只收目标 ID5列表过滤FM2R 置 1精确匹配一组 ID6仲裁实验双板同时发不同 ID小 ID 先发7远程帧置 RTR 请求数据对端回数据帧8总线错误帧故意改错波特率查 LEC 错误码9自动离线恢复MCR 置 ABOM1错误后自动恢复10采样点调整改 BTR 的 TS1/TS2长线不丢帧这不是 20 个实验的官方清单只是常见套路你手上那套例程不管怎么编号都可以对照这张表找到它在验证哪个点。4.2 双节点通信ID 掩码过滤的常见误区掩码过滤是写错率最高的配置。掩码 0x7FF 表示 11 位全部要匹配等于只收一个 ID允许低 3 位变化时掩码是 0x7F8。这个倒置逻辑在 2.3 提过但双节点实验里还要补一句过滤是接收方的配置发送方完全不知道自己在被过滤。所以调试时先断开过滤收全量帧确认物理链路没问题再开过滤不要一上来就怀疑过滤。4.2.1 掩码模式下的 ID 过滤实验A 节点发送 0x123、0x12B 两帧B 节点过滤器设掩码 0x7F8预期只收到 0x123。代码如下can_filter_mask(0, 0x123, 0x7F8); /* 高 8 位必须匹配低 3 位不关心 */代码逻辑说明FR1 写入 0x123 21FR2 写入 0x7F8 21。硬件把每个收到的标准帧 ID 与 FR1 逐位比较FR2 对应位为 1 则必须相等为 0 则跳过。0x12B 的低 3 位是 011与掩码低 3 位 000 相与后不参与比较于是两条都放行但 0x123 和 0x12B 的高 8 位相同实际上会收到两帧。想只收 0x123 就把掩码改回 0x7FF。这里也是 can总线仲裁的入口实验两个节点同时发送不同 ID 时显性位覆盖隐性位ID 小的帧在仲裁段获胜。0x123 和 0x12B 同发0x123 二进制前缀更小持续赢得总线0x12B 等总线空闲再自动重发。CAN 报文中 id 号不代表地址它第一作用是仲裁优先级第二作用才是过滤匹配。4.3 CAN FD 只做概念上的对比不改硬件F072 没有 FDCAN真 FD 实验做不了。例程到这里一般只做估算对比标准帧开销约 44 位加 8 字节数据 64 位填充后一帧约 110 位1Mbit/s 下单帧约 110us。FD 帧数据段用 2Mbit/s 或更高速率传输64 字节数据约 256 位数据段耗时反而比普通帧更短。这个实验的价值是让读者明白 FD 优势在数据段可变速率F072 硬件跑不了要上 FD 得换带 FDCAN 外设的型号。4.4 错误状态与总线关闭恢复怎么实验不配 ABOM 时总线关闭后不会自动恢复例程大多在错误中断里做状态机判断。实验做法两个节点波特率配成不同值通信必然出错循环读 CAN_ESR 的 TEC/REC 与 LEC观察错误计数从 0 涨到 255 再进入 bus-off。static void can_print_error(void) { uint32_t esr CAN1-ESR; uint8_t rec esr 0xFF; /* REC 在低 8 位 */ uint8_t tec (esr 8) 0xFF; /* TEC 在 8~15 位 */ uint8_t lec (esr 4) 0x7; /* LEC 在 6:4 位 */ printf(TEC%u REC%u LEC%u\n, tec, rec, lec); }TEC 超过 255 进入 bus-off恢复条件是监测到 128 次 11 个连续隐性位软件先把 INRQ 置位重新初始化再清复位。例程里最常见的错误是在 bus-off 后只清标志不复位 CAN 外设导致总线上一直看不到该节点的帧。5. 用回环与 ESR 快速验证 STM32F072 例程5.1 用逻辑分析仪看这四种情况把 CAN_TX 和 CAN_RX 同时接逻辑分析仪采样率设 20MHz 以上。重点看四种情况空闲时两条线都是隐性高电平发送节点 TX 出现下降沿表示 SOF对端回 ACK 时 RX 上出现显性位回显总线忙碌时两个引脚持续跳变。正常帧以 7 个隐性位 EOF 结束波形上有明显的一段高电平收尾。量一帧的位时间1Mbit/s 配置下最短位宽应约 1us。位宽对但帧结构乱优先怀疑采样点位宽整体偏大或偏小优先查 BRP。这两个判断能把 CAN 调试工作量砍掉一半。5.2 错误码 LEC 的快速定位把 can_print_error 放进错误中断而不是主循环避免刷屏。触发方式两种对端波特率改为 500k 后通信失败LEC 显示 3ACK 错误这个错误只出现在发送节点侧总线干扰或位时序不对时 LEC 显示 1位填充错误接收侧报得更多。LEC 值含义典型场景1位填充错误干扰或采样点偏移3ACK 错误总线上没有其他节点应答6显性位错误总线被拉死或配置成普通 GPIO最后一个边界点F072 的发送缓冲区较高 32 位数据在 TDHR 的高 24 位和很多人预期的“data[0] 在高位”正好相反。如果解析报文时高低 32 位读反报文解析实验永远对不上把这个点记在例程排错页里看到“高低位反了”就知道去哪改。本文还有配套的精品资源点击获取