☰
STM32F103RBT6 CAN总线开发调通:HAL库配置、过滤器与中断实战
2026/10/20 12:13:59 网站建设 项目流程

简介:一套已调通的 STM32F103RBT6 CAN 总线开发代码,基于 HAL 库与 STM32CubeMX 配置,面向嵌入式初学者及需要快速落地 CAN 通信的开发者,解决从 CubeMX 初始化、Keil 工程移植到消息收发调试的全流程问题。压缩包共 586 个文件,约 6.01MB,其中包含 337 个 C 源文件与 104 个头文件,覆盖 HAL 库驱动与用户应用代码;另有汇编启动文件、链接脚本、Keil 工程文件(uvproj/uvopt)、CubeMX 配置(ioc)以及编译生成的 hex/axf 文件,便于直接下载验证或二次开发。目前已有 4420 人学习下载。代码内提供 CAN 初始化、滤波器配置、发送/接收回调等关键实现,并留有调试与验证思路,可用于工业控制、汽车电子等场景,能帮助快速理解 F103RBT6 的 CAN 外设使用要点,减少踩坑时间。 最近在把 stm32f103rbt6 的 CAN 开发代码完整调通,HAL 库环境下从 CubeMX 配置、过滤器、中断接收到稳定性验证都跑了一遍。CAN 这个外设说难不难,但坑点非常集中:引脚重映射、波特率、过滤器、FIFO 中断,任何一处设置不对,现象都极具迷惑性。这篇文章不打算按手册复述 API,而是把这次“已调通”的工程配置、代码结构和检查顺序完整写下来,给准备用 F103RBT6 接 CAN 总线的朋友做一个可以直接参考的记录。

1. F103RBT6的CAN值得认真调一次的原因

1.1 这块芯片在CAN通信中的定位

STM32F103RBT6 在工控板、机器人控制器、车载诊断设备里出现频率极高,64 引脚、128KB Flash、20KB RAM,主频 72MHz,资源不算豪华,但够用。CAN 外设是 bxCAN,支持 CAN 2.0A/B 协议,标准帧、扩展帧都能收发,3 个发送邮箱、2 个接收 FIFO、14 个过滤组,硬件能力对大多数工业现场总线应用来说是足够的。

现在不少项目选择 F103 + HAL 库,是因为 CubeMX 能快速生成初始化代码,少写很多底层寄存器操作。但生成并不等于调通,HAL 库把寄存器细节包装得很整齐,也把 CAN 的门槛藏了起来:过滤器怎么配、FIFO 中断从哪里进、总线错误怎么恢复,文档里都有,但真正调的时候全是“发送超时”“回调没触发”这种折磨人的现象。我这次把工程完整跑通,最大的体会是:CAN 能不能通,取决于你对位时间、滤波器、中断通知这三件事的理解,而不是会不会调用 HAL 函数。

1.2 调通CAN需要先建立三个认知

第一条,CAN 是双线差分通信,不是 UART 那种点对点收发。总线上所有节点共享同一对 CAN_H/CAN_L,靠 ID 仲裁和 ACK 机制决定谁发送、谁接收。加上错误帧、填充位这些机制,你看到的波形和时间参数,跟串口、I2C 完全不一样,不能用过去的直觉去猜。

第二条,HAL 库的 API 只是外设的“驱动外壳”,真正决定能不能通信的是 CAN_BTR 寄存器里的位时间参数,以及两端是否约定同样的波特率和采样点。很多人把配置生成后就直接用,结果两边的 500kbps 根本不是同一个 500kbps。

第三条,过滤器和 FIFO 是接收链路上的两层“闸门”,任何一个设错,即使总线上跑着大量报文,软件里也读不到。这三条认知建立起来,后面遇到的现象基本都能对号入座。

2. CubeMX初始化:位时间参数藏着通信的真相

2.1 引脚、时钟与使能顺序

CubeMX 里把 CAN1 选上之后,默认会把 PA11(RX)、PA12(TX)点亮。如果你的板子实际走的是重映射后的 PB8/PB9 引脚,需要手动改配置,同时确保 AFIO 时钟已使能。很多朋友第一次在这里翻车:CubeMX 明明生成了初始化代码,硬件上却收不到数据,折腾一整天后发现板子接的是 PB8/PB9,代码里还在用 PA11/PA12。

时钟方面,CAN1 挂在 APB1 总线上。F103 在 72MHz 主频下,APB1 通常是 36MHz,HAL 库初始化波特率时就是拿 APB1 频率来算的。所以建议先看一眼 CubeMX 的 RCC 时钟树,确定 APB1 实际分频后的频率,再动 CAN 参数。我以前偷懒跳过这步,后面算出的波特率差得离谱,排查非常痛苦。

2.2 500kbps到底是怎么算出来的

CAN 一个位周期由同步段、传播段、相位缓冲段 1、相位缓冲段 2 组成。STM32 把传播段和相位缓冲段 1 合并成 TimeSeg1(BS1),相位缓冲段 2 是 TimeSeg2(BS2),同步段固定为 1TQ。波特率公式就是:

波特率 = CAN时钟频率 / (Prescaler * (1 + BS1 + BS2))

我这次目标 500kbps,CAN 时钟 36MHz,需要总分频倍数为 72。最终参数取 Prescaler=6、BS1=8、BS2=3、SJW=1,那么每个位就是 12TQ,采样点约等于 (1+8)/12 = 75%,这个采样点位置对多数收发器都比较友好。

注意:SJW 不是随便给的补偿参数,它表示重同步时一个位最多能吸收多少个 TQ 的相位误差。总线越长、波特率越低,SJW 可以适当调大到 2TQ,短距离开发板上 1TQ 通常够用。

2.3 Normal、LoopBack、Silent三种模式选哪个

CubeMX 的 Mode 下拉里有正常、回环、静默等选项。我的习惯是:首版先在 LoopBack 模式下调,让 CAN 外设内部自发自收,确认软件链路通,再切到 Normal 模式接外部收发器和终端电阻。LoopBack 模式下报文不会真正发到总线上,很适合没有任何第二节点时排查程序问题。Silent 模式只接收不发送,适合做总线监听。真正和外部设备联调时,记得切回 Normal,否则你这边发得再欢,别人也收不到。

3. 过滤器设置:从“来者不拒”到“精确定位”

3.1 过滤器到底过滤掉了什么

CAN 总线上所有节点共享同一对差分线,理论上每个节点都会收到总线上所有报文。如果没有过滤器,CPU 会被无关帧频繁打断,实时性高的任务很容易被淹没。RBT6 的 CAN1 有 14 个过滤组,每个过滤组可配置成 32 位或 16 位两种位宽,也能配置成掩码模式或列表模式。

标准帧 ID 只有 11 位,扩展帧 ID 是 29 位。掩码模式里,Mask 对应位是 1 时必须匹配,是 0 时忽略该位,适合接收一段连续的 ID 区间;列表模式则是精确匹配,只在接收几个固定 ID 时非常高效。项目早期别急着把筛选收太窄,先用“来者不拒”验证链路,后面再根据协议收窄。

3.2 HAL库的滤波器配置代码

HAL_CAN_ConfigFilter 的初始化结构体字段很多,我最推荐的打底配置是“全通过”,即不关心任何 ID 位,所有报文都进 FIFO0:

CAN_FilterTypeDef filter; filter.FilterBank = 0; filter.FilterMode = CAN_FILTERMODE_IDMASK; filter.FilterScale = CAN_FILTERSCALE_32BIT; filter.FilterIdHigh = 0x0000; filter.FilterIdLow = 0x0000; filter.FilterMaskIdHigh = 0x0000; filter.FilterMaskIdLow = 0x0000; filter.FilterFIFOAssignment = CAN_RX_FIFO0; filter.FilterActivation = ENABLE; HAL_CAN_ConfigFilter(&hcan, &filter);

这段配置能让所有报文都进入 FIFO0。如果做双节点点对点测试,这会大大降低排查难度,先确保能收到东西,再去研究怎么收得精准。

3.3 从“全通过”到“只收0x123”

如果你想只接收标准帧 ID 为 0x123 的报文,HAL 代码里通常这样写:

filter.FilterIdHigh = (uint16_t)((0x123U << 5) & 0xFFFF); filter.FilterIdLow = 0x0000; filter.FilterMaskIdHigh = (uint16_t)((0x7FFU << 5) & 0xFFFF); filter.FilterMaskIdLow = 0x0000;

这里把标准 ID 左移了 5 位,是因为标准 ID 在 32 位过滤器寄存器中位于高位字的特定位置,HAL 库封装后这么写是常见用法。如果你不想深究寄存器位段,建议直接用 CubeMX 的 CAN Filter 图形页面生成,或者继续用全通过配置,确认业务确实需要过滤再有针对性地写。

3.4 FIFO0和FIFO1怎么分配

过滤组可以指定匹配帧进 FIFO0 还是 FIFO1。接收中断里,FIFO0 对应 HAL_CAN_RxFifo0MsgPendingCallback,FIFO1 对应 HAL_CAN_RxFifo1MsgPendingCallback。简单应用全部映射到 FIFO0 就够了;如果协议里同时存在实时控制帧和周期状态帧,可以把控制帧安排进 FIFO0,状态帧进 FIFO1,利用不同中断优先级做分流。FIFO0 的优先级默认比 FIFO1 高,这个特性在满载测试时很好用。

4. 收发代码组织:让发送不占CPU、接收不丢帧

4.1 发送Mailbox的用法

bxCAN 有 3 个发送邮箱,调用 HAL_CAN_AddTxMessage 会按优先级分配一个邮箱。发送前最好看一眼邮箱空闲数,邮箱全满时再塞帧会直接失败。我封装的一个发送函数长这样:

uint8_t can_send_std(uint16_t id, uint8_t *data, uint8_t len) { CAN_TxHeaderTypeDef tx; uint32_t mailbox; if (len > 8) len = 8; tx.StdId = id; tx.ExtId = 0; tx.IDE = CAN_ID_STD; tx.RTR = CAN_RTR_DATA; tx.DLC = len; if (HAL_CAN_GetTxMailboxesFreeLevel(&hcan) == 0) return 1; if (HAL_CAN_AddTxMessage(&hcan, &tx, data, &mailbox) != HAL_OK) return 2; return 0; }

这个函数里没有等数据发送完成的死循环,因为 CAN 发送是否成功还取决于总线上有没有其他节点、有没有 ACK 应答。如果对方不在线或者波特率不对,死等发送完成会把整个主循环卡住。调用端可以根据返回值做超时重处理,而不是盲目重试。

4.2 接收走中断回调

接收有两种方式:主循环轮询和中断回调。我的工程选中断 + 环形缓冲区,初始化为:

HAL_CAN_Start(&hcan); HAL_CAN_ActivateNotification(&hcan, HAL_CAN_RX_FIFO0_MSG_PENDING | HAL_CAN_RX_FIFO1_MSG_PENDING);

然后在回调里尽快取走数据,不要做复杂处理:

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rx; uint8_t d[8]; if (hcan->Instance == CAN1) { HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx, d); /* 把 rx.StdId、d 拷贝到应用层环形缓冲区 */ } }

回调里不要放 printf、不要放 HAL_Delay,这些操作耗时太长,CAN 波特率一高就容易溢出 FIFO。中断回调只负责把数据搬进应用层缓冲区,解析、处理、分类全部放主循环。

4.3 错误中断与总线恢复

CAN 的错误管理不只是“发不出来就报错”,还涉及主动错误、被动错误、Bus Off 几个状态。初始化时我通常把 AutoBusOff 设为 DISABLE,把 AutoRetransmission 设为 DISABLE,然后注册错误回调,在 HALE_ErrorCallback 里读取 hcan->ErrorCode 和错误计数器,方便定位问题。

如果总线故障严重到进入 Bus Off,外设会停止参与总线通信。恢复方式可以是等总线安静后重新调用 HAL_CAN_Start,也可以先 HAL_CAN_Stop 再 Start。自动重发这个功能看起来方便,但总线故障时它会让发送邮箱一直占着不放,反而掩盖了真实问题。我建议应用层自己控制超时重传,保留对发送失败场景的完整判断。

5. 调通过程中踩过的坑(按排查顺序)

5.1 引脚重映射与AFIO时钟

这次一开始就栽在引脚映射上。板子上的 CAN 收发器接的是 PB8/PB9,我却在 CubeMX 里默认选了 PA11/PA12。现象特别迷惑:LoopBack 模式正常,切到 Normal 模式后外接设备完全收不到。查了两天才意识到,板子根本没把 PA11/PA12 引到收发器。改成 PB8/PB9 并打开 AFIO 时钟后,通信立刻恢复。以后拿到板子,第一件事就是看原理图确认 CAN 收发器的 TX/RX 接到了哪个引脚。

5.2 波特率没对上:所有发送都“假成功”

对方节点用的是 250kbps,我这边配置成 500kbps,现象是 HAL_CAN_AddTxMessage 返回成功,但总线分析仪上全是错误帧,我的发送邮箱也经常满。后来用示波器看 CAN_H/CAN_L 的位宽度才发现,500kbps 的单 bit 理论宽度是 2us,250kbps 是 4us,一量就知道谁对谁错。调 CAN 时手头没有分析仪,至少要有示波器,否则这种问题全靠猜。

5.3 忘记激活中断通知

HAL_CAN_Start 只是启动外设,并不会自动开启报文接收中断。你在 CubeMX 里配好了 NVIC,代码里没有调用 HAL_CAN_ActivateNotification,FIFO 里照样有数据,但回调就是进不去。很多人第一次用 HAL 库都会漏这一步。启动外设、开启通知、实现回调、注册中断向量函数,四步缺一不可。中断向量函数也要检查一下,CAN1_RX0_IRQHandler 里必须调用 HAL_CAN_IRQHandler(&hcan)。

5.4 在回调里用了延时导致丢帧

刚开始我在接收回调里加了一个 HAL_Delay,想等数据稳定再处理,结果高波特率下帧率一高,FIFO 溢出丢帧。后来把回调里的所有耗时操作清空,只做数据搬移,问题消失。接收回调的黄金法则是:越快返回越好。示波器观察到的数据错乱,很多时候不是协议问题,是回调里代码写得太重。

5.5 终端电阻

CAN 总线两端必须各接一个 120 欧终端电阻。很多开发板默认没有接,需要外接两个电阻。缺少终端电阻时,短距离调试可能勉强能通,但波形会有振铃,距离稍微拉长就丢帧或完全不通。我自己踩过这个坑后,已经把终端电阻做进了自制的 CAN 测试小板上,反而比每次都翻箱倒柜找电阻省事。

6. 调通后的验证:波形与长期稳定性如何测

6.1 示波器看CAN波形

可靠的 CAN 差分波形应该满足:显性时 CAN_H 约 3.5V、CAN_L 约 1.5V,差分约 2V;隐性时两条线都约 2.5V,差分接近 0V。如果差分电压明显偏低,多半是收发器供电不足或者终端电阻接错。波形上升沿和下降沿有明显锯齿,则要考虑线缆过长、终端电阻缺失、总线分支过多这几个因素。

通过位宽度可以快速验证波特率:500kbps 的位宽是 2us,250kbps 是 4us。量三次取平均,基本能判断两端配置是否一致。采样点设置是否合理,在长时间跑数据时会有更明显的体现:采样点太靠后,总线稍微有点干扰就出错误帧。

6.2 双节点跑压力测试

我只信“连续跑一晚上不丢帧”这种结论。测试方法是两个节点各发一个 ID:主节点 100Hz 发送心跳帧给从节点,从节点收到后回发一帧,主节点统计回帧数量。通过之后把频率加到 500Hz、1000Hz,跑 12 小时,记录丢帧数和 CAN 错误计数器变化。人为拔掉一根 CAN 线再插回去,观察程序能否自动恢复,这个测试很能反映工程代码健壮性。只有这些测试都过了,我才会把工程标记为“已调通”。

6.3 给后续复用的小建议

我把 can_init、can_send_frame、can_get_received_frame、can_error_handler 这些功能封装到了独立的 can_app.c 里,主循环只丢数据进来、取数据出去。后面如果换到 F407 或者其他 F103 型号,改一下时钟配置和引脚映射就能快速复用。另一个值得做的事:在调试阶段加一个诊断模式,上位机通过指定 ID 的报文去修改波特率或过滤器参数,这样不用反复烧 Flash 就能测试不同场景,上线后排查问题也方便不少。

如果按“引脚 → 波特率 → 模式 → 过滤器 → 中断”这个顺序去排查,绝大多数 CAN 通信问题都能在半小时内定位。我个人在这块踩得最多的还是“波特率明明算对了但实际不对”,根源就是对 APB1 时钟树结构不够重视。现在每到一个新板子,第一件事就是确认 RCC 时钟树和 CubeMX 里看到的时钟一致。希望这篇记录能帮你少走这段弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询