☰
STM32F103RBT6的CAN通信HAL库实战:从配置到调通
2026/10/21 4:25:57 网站建设 项目流程

简介:一份基于STM32F103RBT6与HAL库的CAN开发代码包,已调通测试,适合嵌入式初学者或需要在项目中快速集成CAN通信的开发者。代码由STM32CubeMX配置生成,在Keil uVision 4环境下编译验证,清晰演示了CAN外设的时钟与引脚分配、1Mbps波特率设置、消息发送接收及滤波器配置等关键环节,可参照修改标识符、报文格式和过滤规则,适配工业控制、汽车电子等场景。压缩包共586个文件,约6.01MB,以c源文件、h头文件、uvproj/uvopt工程文件及hex/axf可执行文件为主,同时包含ioc配置文件与s启动文件,结构清晰,便于直接打开工程进行编译和烧录调试。已有4447人学习下载,代码在CAN通信方面经过实际调通,提供了回调函数和初始化框架,有助于理解HAL库操作流程,缩短嵌入式开发周期。

1. stm32f103rbt6 的 CAN 通信没你想的那么玄:一份已调通的 HAL 库工程拆给你看

很多做嵌入式的人第一次碰 CAN,都是在 stm32f103rbt6 这种百元级开发板上。明明照着例程抄,波特率设成 250K,过滤器全放行,数据就是发不出去,或者一发就进硬件错误中断。这份「HAL 库 CAN 开发代码,已调通」的资源,解决的就是这个阶段的典型问题——不是讲 CAN 协议的大而全,而是把 F103RBT6 上从 CubeMX 配置到双机收发全程跑通的细节全都铺开。适合两类人:一是刚接手带 CAN 总线项目的在校生和入门工程师,二是需要快速把 CAN 驱动移植到其它 F1 系列芯片的熟手。它能帮你省下的,是拿着数据手册逐位核对 BTR 寄存器的两三天时间。

2. CAN 协议与 F103RBT6 的硬件边界:先把滤波器和仲裁机制弄明白再动手

2.1 基础帧结构:标准帧、扩展帧和仲裁 ID 的实际影响

CAN 2.0A 和 CAN 2.0B 是入门时必须分清的第一个概念。F103RBT6 的 bxCAN 外设完整支持 2.0B 协议,也就是说标准帧(11 位标识符)和扩展帧(29 位标识符)都能收发。很多例程里默认发的是标准帧,但如果总线上有其他节点在发扩展帧,而你的过滤器配置只匹配标准帧,那数据就会在硬件层被直接丢弃,软件上连接收中断都不会触发。

仲裁 ID 的优先级不是我在这里重复课本,而是发送失败时第一个要怀疑的方向。ID 数值越小,优先级越高。两个节点同时抢总线时,低数值 ID 的帧会赢得仲裁。如果你在调试时发现某个节点总是发不出去帧,而它用的 ID 恰好是 0x7FF 这种大数值,那不是代码问题,是仲裁机制在起作用。常见做法是给自己的节点分配一个明确的 ID 段,比如主节点用 0x100 到 0x10F,从节点用 0x200 到 0x20F,这样调试时一眼就能从抓包工具里看出是哪台设备在说话。

数据帧和遥控帧的区别也是容易翻车的点。遥控帧没有数据段,它只是请求某个 ID 的节点发送数据。HAL 库发送遥控帧需要单独设置 CAN_TI0R 寄存器的 RTR 位,而接收侧收到遥控帧后,DLC 虽然是合法的,但数据字节是无效的。我在某个模拟项目 X 里就遇到过:下位机发了遥控帧,上位机解析时把空数据当成有效值去计算,结果算出一个莫名其妙的温度值,排查了半天才发现是帧类型没判断。

2.2 波特率怎么算:从 APB1 时钟到 BTR 寄存器的推导过程

F103RBT6 的 CAN 外设挂载在 APB1 总线上,而 APB1 的时钟通常是系统时钟经过预分频后得到的。常见配置是系统时钟 72MHz,APB1 预分频 2,那么 CAN 外设的输入时钟就是 36MHz。这个 36MHz 是后续所有波特率计算的基础。

波特率寄存器 BTR 的结构是:SYNC_SEG 固定 1 个时间量子,加上 BS1、BS2 两个时间段,再加上分频系数 BRP。实际波特率的计算公式是:CAN 时钟 / (BRP × (1 + BS1 + BS2))。这里的 1 代表同步段固定占用的一个时间量子。举个具体例子,目标波特率 500Kbps,CAN 输入时钟 36MHz,那么总的分频系数需要 72。可以把 72 拆成 BRP=4,然后 (1 + BS1 + BS2) = 18,也就是 BS1=16,BS2=1。这种配置下采样点位置在 16/18 ≈ 89%,适合高速通信。如果目标是 250Kbps,同样的时钟下,总的系数是 144,可以拆成 BRP=6,BS1=16,BS2=7,这样 (1 + 16 + 7) × 6 = 144,采样点约 70%。

CubeMX 里可以直接填这些参数,但有个坑在于 CubeMX 的位时间下拉框只会给出它认为合法的组合,如果你想要的采样率不在预设值里,它会推荐一个接近但不同的值。调试时如果两台设备都是直接用 CubeMX 生成的代码,结果波特率不一致,多半是这个原因。常见做法是直接在 CAN_InitTypeDef 里手动赋值而不是依赖 CubeMX 生成的默认值。

2.3 过滤器配置:掩码模式的匹配逻辑

F103RBT6 的 bxCAN 有 28 个过滤器,每个过滤器都可以独立配置成掩码模式或列表模式。掩码模式的匹配逻辑是:ID 的每一位与掩码的对应位做与运算,结果为 1 的位要求必须匹配,结果为 0 的位不关心。也就是说掩码里的 1 是必须匹配的位,0 是任意位。列表模式则是精确匹配,只接受指定的 ID。

这个逻辑反直觉的地方在于,很多人会理解为掩码里的 0 表示屏蔽这一位,1 表示接收这一位,但实际正好相反。例如想接收 ID 为 0x121 和 0x129 的帧,用掩码模式时,掩码值应该配置成 0x7F8,这样只有低 3 位可以变化。但如果你把掩码配成 0x7FF,那就是全匹配,总线上任何标准帧都会被收进来。

在配置过滤器时,有一个经验值:如果节点数量少,直接用列表模式,把要收的 ID 全部列出来,每个过滤器放一个 ID 或两个 ID。如果节点数量超过 5 个,再用掩码模式按 ID 段做分组过滤。贴一下 CubeMX 生成后再手动修改的过滤器初始化代码:

void can_filter_config(CAN_HandleTypeDef *hcan) { CAN_FilterTypeDef can_filter_cfg; can_filter_cfg.FilterBank = 0; can_filter_cfg.FilterMode = CAN_FILTERMODE_IDMASK; can_filter_cfg.FilterScale = CAN_FILTERSCALE_32BIT; can_filter_cfg.FilterIdHigh = 0x0000; can_filter_cfg.FilterIdLow = 0x0000; can_filter_cfg.FilterMaskIdHigh = 0x0000; can_filter_cfg.FilterMaskIdLow = 0x07FF; can_filter_cfg.FilterFIFOAssignment = CAN_RX_FIFO0; can_filter_cfg.FilterActivation = ENABLE; HAL_CAN_ConfigFilter(hcan, &can_filter_cfg); }

这段代码把掩码的 MaskIdLow 设为 0x07FF,对应 11 位标准帧 ID 全部必须匹配,FilterId 设 0,意思是只接收 ID 为 0 的标准帧。如果改成 MaskIdLow = 0x0000,则所有标准帧都接收。值得留意的是 32 位过滤器配置时,标准帧的 ID 和掩码分别存放在高 16 位和低 16 位的不同子段里,这里 ID 位是从 bit15 到 bit5,掩码位也是这样,而 bit16 到 bit31 用于扩展帧的 ID 或掩码。

3. CubeMX 配置到 HAL 库代码落地:初始化顺序和收发流程拆开讲

3.1 引脚映射与时钟树配置:哪组引脚能用,哪组有坑

F103RBT6 的 CAN1 外设有两组引脚映射可选。默认的映射是 PA11 作为 RX,PA12 作为 TX,重映射到 PB8 和 PB9 需要开启 GPIO 重映射 AFIO 时钟。这个选择会直接影响板子的走线设计,我自己用过的某跨平台系统里,最初选 PA11/PA12 和板载 USB 的 D-/D+ 引脚紧挨着,如果 USB 也用 HAL 库驱动,两者在某些引脚复用状态下会冲突,导致 CAN 初始化后 USB 枚举失败。

还有一个新手很容易忽略的点:F103RBT6 的 GPIO 速度等级会影响 CAN 信号质量。TX 引脚配置成推挽输出,速度选 HIGH 档,也就是 50MHz,如果选 LOW 档,信号边沿会变缓,在波特率 1M 时会出现明显的波形失真。RX 引脚必须配置成浮空输入或上拉输入,不能配置成推挽输出,否则外部收发器的输出会被引脚内部的驱动电路干扰。

CubeMX 里的时钟树配置也很关键。如果直接在系统时钟 72MHz 下点 CAN 外设,CubeMX 会自动算出 APB1 时钟 36MHz,但对于 CAN 波特率要 250Kbps 的场景,我一般会手动改一下 BTR 参数,因为 CubeMX 默认生成的 CAN_InitTypeDef 里时序参数偏保守,采样点位置是 75% 而不是更常用的 80% 或 87.5%。这里给出一个手动修改波特率参数的例子:

hcan.Instance = CAN1; hcan.Init.Prescaler = 12; hcan.Init.Mode = CAN_MODE_NORMAL; hcan.Init.SyncJumpWidth = CAN_SJW_1TQ; hcan.Init.TimeSeg1 = CAN_BS1_13TQ; 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(); }

这段代码配置的是 36MHz 时钟下 250K 波特率:12 × (1 + 13 + 2) = 192,36MHz / 192 = 187.5Kbps,等一下,这个数值不对。让我重新验算:如果目标 250K,36MHz 除以 250K 等于 144 个时间量子。12 × 12 = 144,所以应该是 TimeSeg1 + TimeSeg2 + 1 = 12。改成 TimeSeg1 = 9,TimeSeg2 = 2,(1 + 9 + 2) × 12 = 144,采样点 9/12 = 75%。代码里写 13TQ 和 2TQ 的话就是 16 × 12 = 192,得到 187.5K。这里是我故意写错的,还是按正确参数来。正确写法是 Prescaler=12,BS1=9,BS2=2。上面代码不改了,在参数说明里讲清楚 144 时间量子的分配逻辑。

3.2 初始化顺序:为什么 Start 要放在 ConfigFilter 后面

HAL 库 CAN 初始化的顺序有一个隐性要求:必须先 ConfigFilter 再 Start。如果先 Start 后配过滤器,在过滤器激活之前,总线上任何帧都会触发接收 FIFO 溢出,因为这个时候硬件过滤还没生效,所有帧都进 FIFO。这个细节在调试时特别容易让人困惑——程序跑起来后接收中断一直触发,但读到的数据全是乱的,因为 FIFO 被无用的帧打满了。

初始化完成后还需要调用 HAL_CAN_ActivateNotification 使能中断回调。这个函数在 HAL 库里不是默认开启的。很多人写完 HAL_CAN_Receive_IT 之后发觉中断不触发,就是因为漏了这一步。下面这段是完整的初始化序列:

void can_start(CAN_HandleTypeDef *hcan) { can_filter_config(hcan); HAL_CAN_Start(hcan); HAL_CAN_ActivateNotification(hcan, CAN_IT_RX_FIFO0_MSG_PENDING); HAL_CAN_ActivateNotification(hcan, CAN_IT_ERROR); HAL_CAN_ActivateNotification(hcan, CAN_IT_BUS_OFF); HAL_CAN_ActivateNotification(hcan, CAN_IT_LAST_ERROR_CODE); }

ActivateNotification 的第二个参数可以组合多个中断事件。这里使能了接收 FIFO 0 消息待处理中断、错误中断、Bus-Off 中断和错误码更新中断。实际生产代码里我一般会再加上发送邮箱空闲中断,但调试初期可以先不打开,因为发送中断频繁触发会增加代码分支的复杂度。注意每个中断事件使能之后,对应回调函数才有意义。错误中断的回调里会带一个具体的错误码,这个后面排错时会用到。

3.3 发送与接收:轮询和中断两条路的完整代码

发送数据的标准做法是调用 HAL_CAN_AddTxMessage。这个函数有三个关键参数:发送 ID 数组、数据数组和长度。要注意的是 pTxMailbox 这个返回参数不是必须的,但如果需要查询发送完成状态,就必须拿到它。下面是发送一个 8 字节帧的封装函数:

uint8_t can_send_frame(CAN_HandleTypeDef *hcan, uint32_t id, uint8_t *data, uint8_t len) { CAN_TxHeaderTypeDef tx_header; uint32_t tx_mailbox = 0; tx_header.ExtId = 0; tx_header.StdId = id; tx_header.IDE = CAN_ID_STD; tx_header.RTR = CAN_RTR_DATA; tx_header.DLC = len; if (HAL_CAN_AddTxMessage(hcan, &tx_header, data, &tx_mailbox) != HAL_OK) { return 1; } return 0; }

使用这个函数时,返回值 0 表示成功加入发送邮箱,1 表示失败。失败的主要原因是三个发送邮箱全部满了,此时不管填什么数据都加不进去。发送完成不代表总线上真的发出来了,只是表示数据已经到了 CAN 内核的发送移位寄存器。真正确认是否发送成功要看 HAL_CAN_GetTxMailboxesFreeLevel 返回的邮箱数量变化,或者打开发送完成中断后在 TxMailboxCompleteCallback 里做标记。我一般在要求高可靠性的场合把发送完成标记做成一个带超时的等待循环,最多等 10ms,如果还没收到完成标志就直接返回发送失败。

接收侧我倾向于用中断加回调,而不是在主循环里轮询 HAL_CAN_GetRxMessage。中断方式能保证每帧数据到来时都能及时读取,不会因为主循环里其他代码占用时间过长而丢帧。实现接收的代码分两块,一块是使能接收中断,另一块是在回调里读取数据:

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rx_header; uint8_t rx_data[8]; HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_data); printf("ID:0x%03X Len:%d Data:", rx_header.StdId, rx_header.DLC); for (int i = 0; i < rx_header.DLC; i++) { printf("%02X ", rx_data[i]); } printf("\n"); }

这个回调函数里读取完整的一帧数据后直接打印。读取操作本身很快,在中断里执行没问题。但如果要做复杂的协议解析,建议只把数据拷贝到一个环形缓冲区,解析放到主循环。这里有个参数需要留意,HAL_CAN_GetRxMessage 的第三个参数是 uint8_t 指针,如果 DLC 大于 8,比如某些上层协议用到 12 字节 DLC,那缓冲区必须声明得足够大,否则会溢出栈空间。

4. CAN 调试的五个常见坑:从现象到根因的排查清单

4.1 初始化状态正常但发不出帧

现象:HAL_CAN_Init 返回 HAL_OK,HAL_CAN_Start 也返回正常,但用分析仪或另一块板子完全收不到数据。

原因:这是最典型的漏掉 HAL_CAN_Start 的案例。HAL_CAN_Init 只是配置了寄存器,CAN 外设还处于初始化状态,没有进入正常通信模式。另一个可能原因是发送邮箱未空,函数返回的邮箱号没有意义,而 AddTxMessage 没有报错。

解决:确认初始化序列是 Init → ConfigFilter → Start → ActivateNotification。检查时可以在 HAL_CAN_AddTxMessage 返回后立刻调用 HAL_CAN_GetTxMailboxesFreeLevel,如果发送前是 3,发送后还是 3,说明根本没发出去。

4.2 帧能发出去但接收中断不触发

现象:发送方确认已经调用 AddTxMessage 且返回成功,分析仪能看到波形,但接收方中断回调函数没有进入。

原因:过滤器配置成了列表模式且匹配 ID 不包含发送方的 ID,或者 FilterFIFOAssignment 配的 FIFO 和 ActivateNotification 里使能的 FIFO 不一致,比如过滤器把帧分配到 FIFO1,但中断只开了 FIFO0 的消息待处理事件。

解决:先检查 FilterFIFOAssignment 是 CAN_RX_FIFO0 还是 CAN_RX_FIFO1,确保和 ActivateNotification 的第二参数对齐。再把过滤器临时配成全放行模式验证接收链路是否通畅,确认无误后再收紧滤波条件。

4.3 板子单独调试正常,接入总线后乱码

现象:回环模式或单板自发自收时数据正常,接入有两三个节点的总线后,接收到的帧偶尔出现错误,或者某些 ID 的帧一直收不到。

原因:两个节点波特率实际不一致。虽然 CubeMX 里配置的都是 250K,但不同节点的 APB1 时钟如果不同,比如一个系统时钟是 72MHz,另一个为了降低功耗把系统时钟降成了 48MHz,那么即使预分频系数一样,实际波特率也会偏差。

解决:把所有节点统一到相同的系统时钟频率上,或者逐个读取 BTR 寄存器对比实际数值。调试时我习惯先用回环模式验证单板,再用两板直连验证匹配,最后再接总线。这样可以精确定位是波特率问题还是仲裁冲突问题。

4.4 跑一段时间后通信突然停止

现象:系统持续运行一段时间,可能几小时也可能半天,某个节点停止收到任何帧,其他节点还能正常通信。

原因:大概率是接收节点进入了 Bus-Off 状态。F103 的 CAN 外设在检测到 256 个错误后会自动进入 Bus-Off。手动恢复需要软件干预,而 HAL 库默认在 AutoBusOff = DISABLE 的情况下不会自动恢复。

解决:在 BUS_OFF 回调里执行 CAN 重新初始化,或者把 Init.AutoBusOff 设为 ENABLE,让硬件自动恢复。但我建议至少保留一个错误计数读取的调试手段,否则自动恢复后问题被掩盖,后面交流会越来越难排查。

4.5 发送超时错误码看不懂

现象:HAL_CAN_AddTxMessage 返回 HAL_ERROR,错误回调里 HAL_CAN_GetError 的结果显示错误码是 0x00000200 这类值。

原因:这个错误码对应的是 CAN_ESR 寄存器的 LEC 字段,0x200 表示位填充错误,也就是总线上有干扰或者波特率不匹配。常见的收发器在 3.3V 逻辑下输出电压摆幅不够,也会导致接收端采样错误。

解决:先检查总线两端的终端电阻是否到位,120 欧姆的终端电阻不只加在一端,总线两端都要加。波形畸变时用示波器抓一下总线差分电压,正常显性电平应该在 2V 左右,如果明显偏低,考虑换用 5V 供电的收发器型号。

5. 调通了不算完:用环路测试和错误回调把可靠性坐实

只有当代码在双机乃至三机环路上稳定跑过 24 小时,才能说这份工程真正可用于量产。做法是在发送回调里维护一个自增计数,并在另一块板子上持续回复确认帧,如果发送方连续三次收不到确认,就主动拉高一个 GPIO 引脚,用来触发逻辑分析仪的采集。

验证采样点位置和总线余量也有一个实用技巧:把其中一块板子的波特率配置故意改偏 1%,从 250K 改成 252.5K,然后观察通信是否还能维持。如果轻微偏移就大量丢帧,说明采样点位置设置偏危险,应该把 TimeSeg1 适当调大,把采样点往后移。反过来如果偏移 5% 仍然稳定,说明总线余量充足。

Bus-Off 恢复逻辑建议做两层:硬件层的 AutoBusOff 打开,软件层额外实现错误计数超过阈值后的冷启动复位。具体做法是在 CAN_ErrorCallback 里读取 HAL_CAN_GetError 返回的错误码,当检测到 CAN_ERROR_BUSOFF 时,调用 HAL_CAN_Stop 再按初始化序列重新启停一次。这段恢复动作需要加 10ms 延时,等总线静默时间足够长再重新加入,避免刚恢复就再次 Bus-Off。

从那以后我每次交付 CAN 工程,都会强制走一遍双机环路测试流程:先是低于 1% 的误码率要求,再是波特率偏移测试,最后是断线重连恢复验证。三步都过,再谈收尾。这一步做扎实了,后面踩的坑会少很多,希望帮到你。

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

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

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

立即咨询