☰
STM32F103 CAN通信实战:从实例代码到多节点网络
2026/10/13 19:47:59 网站建设 项目流程

简介:这份STM32F103 CAN实例代码面向嵌入式开发初学者与需要快速上手CAN通信的工程师,围绕STM32F103系列微控制器的CAN控制器展开,帮助读者理解从硬件接口到软件编程的完整实现路径。资源包共81个文件,以30个h头文件、28个c源文件为核心,配合12个s启动文件及uv2、opt、dep等工程配置与备份文件,整体约280KB,目录按Source、App、CMSIS、STM32F10x_StdPeriph_Driver等模块组织,结构清晰便于检索。内容覆盖CAN控制器初始化、波特率与滤波器配置、发送接收邮箱操作、中断回调及错误处理等关键环节,并附带说明文档与工程文件,可直接编译调试。目前已有2872人学习下载,适合作为学习CAN总线协议与STM32外设驱动的实践起点,帮助开发者快速搭建可靠的多主通信网络。

1. 从一块“哑巴”板子说起:STM32F103 CAN 实例代码到底能解决什么

手头有一块 STM32F103 的开发板,想让它跟另一块板子或者上位机通过 CAN 总线聊起来,结果翻遍官方库发现只有一堆寄存器定义和结构体,连个能直接编译下载的完整例子都找不到——这大概是很多嵌入式从业者第一次碰 CAN 时的真实处境。CAN 总线在汽车电子、工业控制、储能设备里用得极广,但它的初始化流程比串口复杂得多,光一个滤波器配置就能卡住半天。这份 STM32F103 CAN 实例代码要解决的,就是把“能跑通”这件事从玄学变成可复现的流程:它提供了一套基于标准外设库的完整工程,包含 CAN 初始化、滤波器配置、收发中断处理以及一个可验证的回环测试逻辑。适合谁?适合已经会点灯、会串口收发,但第一次接触 CAN 或者被 STM32 的 CAN 滤波器搞晕的嵌入式开发者。你不需要先啃完 300 页的参考手册,照着实例把参数改对,就能先看到数据在总线上跑起来。

2. 拆开实例看门道:CAN 初始化的三个关键参数与代码落地

拿到一份 CAN 实例代码,最忌讳的就是直接编译下载然后祈祷。CAN 能不能通,八成取决于初始化那几行参数对不对。这一章把实例里的初始化部分拆开,讲清楚每个参数为什么这么设,以及改错了会出什么现象。

2.1 位时序与波特率:不是随便填个数就能通

CAN 是异步总线,没有时钟线,所以通信双方必须提前约定好位时序。STM32F103 的 CAN 外设挂在 APB1 总线上,通常 APB1 时钟是 36MHz。实例代码里一般会定义一个结构体CAN_InitStructure,其中CAN_Prescaler、CAN_BS1、CAN_BS2这三个参数共同决定波特率。

计算公式是:波特率 = APB1时钟 / (Prescaler × (1 + BS1 + BS2))。

假设要配 500kbps,APB1 为 36MHz,常见做法是 Prescaler 设为 4,BS1 设为 15,BS2 设为 2,这样 36M / (4 × (1+15+2)) = 500k。实例代码里通常直接给出这组值,但你要根据自己板子的晶振和 APB1 分频去核对。

// CAN 初始化核心代码片段(基于标准外设库) CAN_InitTypeDef CAN_InitStructure; CAN_FilterInitTypeDef CAN_FilterInitStructure; // 使能 CAN 和 GPIO 时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 配置 CAN 引脚:PA11 为 CAN_RX,PA12 为 CAN_TX GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入 GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure); // CAN 位时序配置:500kbps @ APB1=36MHz CAN_InitStructure.CAN_TTCM = DISABLE; // 关闭时间触发模式 CAN_InitStructure.CAN_ABOM = ENABLE; // 自动离线恢复,调试时建议开 CAN_InitStructure.CAN_AWUM = DISABLE; // 自动唤醒关闭 CAN_InitStructure.CAN_NART = DISABLE; // 允许自动重传 CAN_InitStructure.CAN_RFLM = DISABLE; // 接收 FIFO 不锁定 CAN_InitStructure.CAN_TXFP = DISABLE; // 发送优先级由标识符决定 CAN_InitStructure.CAN_Mode = CAN_Mode_Normal; // 正常模式,回环测试时改 Loopback CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 = CAN_BS1_15tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_2tq; CAN_InitStructure.CAN_Prescaler = 4; CAN_Init(CAN1, &CAN_InitStructure);

这段代码里CAN_ABOM建议在调试阶段使能,这样总线一恢复芯片能自动回来,不用手动复位。CAN_Mode是第一个要确认的地方:如果你只有一块板子,必须改成CAN_Mode_Loopback,否则没有应答信号,发送会一直失败。CAN_SJW是重新同步跳跃宽度,一般设 1tq 就够,总线质量差可以适当加大。

2.2 滤波器配置:不过滤就等着收一堆垃圾

STM32F103 的 CAN 滤波器是出了名的绕。它有两个 FIFO,14 个滤波器组(互联型有 28 个),每个组可以配成 32 位掩码模式或 16 位列表模式。实例代码里通常会配一个“接收所有”的滤波器方便测试,但实际项目里必须精确过滤。

// 滤波器配置:接收所有标准帧,存入 FIFO0 CAN_FilterInitStructure.CAN_FilterNumber = 0; CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000; CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; CAN_FilterInit(&CAN_FilterInitStructure);

CAN_FilterIdHigh和CAN_FilterIdLow拼起来是 32 位,标准帧的 ID 要左移 5 位再填进去,这一点实例代码里一般会封装成宏或者注释说明。CAN_FilterMaskIdHigh/Low全 0 表示“不关心任何位”,也就是全通过。如果你只想收 ID 为 0x123 的标准帧,掩码要设成 0x7FF 左移后的值,ID 也要对应移位。很多新手在这里翻车:明明发了数据但收不到,就是因为 ID 没移位,滤波器把它当成了扩展帧或者直接过滤掉了。

2.3 发送与接收:中断方式比轮询靠谱在哪

实例代码通常提供两种收发方式:轮询和中断。轮询适合快速验证,中断适合实际项目。发送部分核心是选一个空的发送邮箱,填好标识符、数据长度和数据,然后请求发送。

// 中断方式发送一帧标准数据帧 CanTxMsg TxMessage; TxMessage.StdId = 0x123; // 标准标识符 TxMessage.ExtId = 0x00; TxMessage.IDE = CAN_Id_Standard; // 标准帧 TxMessage.RTR = CAN_RTR_Data; // 数据帧 TxMessage.DLC = 8; // 数据长度 8 字节 TxMessage.Data[0] = 0x11; TxMessage.Data[1] = 0x22; // ... 填充其余数据 CAN_Transmit(CAN1, &TxMessage);

接收中断里要判断是 FIFO0 还是 FIFO1 有挂起,然后调用CAN_Receive把数据读出来,最后必须调用CAN_FIFORelease释放邮箱,否则下一次中断不会再来。实例代码里一般会用一个全局结构体存接收到的数据,然后在主循环里处理。这里有个血泪经验:中断里不要做耗时操作,把数据拷走就赶紧退出,否则高负载时容易丢帧。

3. 从编译到收发:把实例跑起来的最小验证流程

代码看懂是一回事,真正下载到板子上跑通是另一回事。这一章给出一条最短验证路径,用两块 STM32F103 板子加一个 CAN 收发器模块,把实例代码跑成能互相收发的小系统。

3.1 硬件接线与终端电阻:最容易忽略的物理层

CAN 总线是差分信号,需要 CAN_H 和 CAN_L 两根线。两块板子之间除了这两根线,还必须接 120 欧姆终端电阻,通常接在总线两端。很多开发板自带了收发器(比如 TJA1050 或 SN65HVD230),但终端电阻不一定焊了。如果你只连了两根线发现通信不稳定或者干脆不通,先拿万用表量一下 CAN_H 和 CAN_L 之间的电阻,正常应该是 60 欧姆左右(两个 120 欧并联)。只有一块板子做回环测试时不需要外部收发器,但要把模式改成 Loopback。

接线步骤:

  1. 板 A 的 CAN_H 接板 B 的 CAN_H,CAN_L 接 CAN_L。
  2. 在板 A 和板 B 的 CAN_H 与 CAN_L 之间各并一个 120 欧电阻(如果板子上已有跳线选择终端电阻,直接短接)。
  3. 共地。两块板子的 GND 必须连在一起,否则差分信号没有参考,通信会随机出错。

3.2 编译环境与下载配置

实例代码一般提供的是 Keil MDK 工程或者 Makefile + GCC 的版本。以 Keil 为例,打开工程后先确认芯片型号选的是 STM32F103C8 还是其他尾缀,然后检查外部晶振频率是否和你的板子一致。如果用的是 8MHz 晶振,系统时钟一般倍频到 72MHz,APB1 分频后是 36MHz,这跟上一章的波特率计算是对应的。下载器用 ST-Link 的话,在 Debug 选项卡里选 ST-Link Debugger,Flash Download 里勾上 Reset and Run。

# 如果用 GCC + Makefile,编译和下载命令大致如下 make clean make st-flash write build/firmware.bin 0x8000000

编译时如果报错找不到stm32f10x_can.h,检查标准外设库的路径有没有加到 Include Paths 里。标准库的 CAN 驱动文件是stm32f10x_can.c,启动文件选startup_stm32f10x_md.s(中容量)还是hd.s(大容量)取决于你的芯片型号,选错了会在中断向量表那里跑飞。

3.3 用回环模式先验证软件逻辑

只有一块板子的时候,把CAN_InitStructure.CAN_Mode改成CAN_Mode_Loopback,发送的数据会直接回到自己的接收 FIFO,不经过外部总线。这样可以先确认滤波器、中断、数据打包这些软件逻辑没问题。回环模式下你发什么就应该收到什么,如果收不到,问题一定在软件配置上,跟硬件无关。

// 回环测试主循环片段 while (1) { if (CAN_TransmitStatus(CAN1, 0) == CAN_TxStatus_Ok) { // 上一帧发送完成,可以发下一帧 } CanTxMsg tx; tx.StdId = 0x123; tx.IDE = CAN_Id_Standard; tx.RTR = CAN_RTR_Data; tx.DLC = 2; tx.Data[0] = 0xAA; tx.Data[1] = 0x55; CAN_Transmit(CAN1, &tx); Delay_ms(500); // 简单延时,实际项目用定时器 }

回环通了之后,再把模式改回 Normal,接上另一块板子或者 CAN 分析仪,就能看到真实总线上的波形和数据。如果回环通但正常模式不通,九成是硬件接线、终端电阻或者波特率不一致的问题。

4. 避坑与排查:CAN 调试中最容易翻车的五个地方

CAN 调试的坑往往不是代码写错了,而是一些默认配置和硬件细节在作怪。这一章列五个最常见的翻车现场,每条按现象、原因、解决来说。

4.1 发送一直失败,CAN_TransmitStatus永远返回 Pending

现象:代码下载后,发送函数调用了,但状态一直不是 Ok,也没有任何数据发出去。

原因:最常见的是没有其他节点应答。CAN 总线要求至少有两个节点,发送方发出数据后,接收方要拉低应答位。如果你只有一块板子且模式是 Normal,没有节点应答,发送会一直重试直到错误计数溢出。另一个原因是 CAN 收发器没有供电或者 standby 引脚没拉低。

解决:单板调试改 Loopback 模式;双板检查收发器供电和使能引脚;用示波器看 CAN_TX 引脚有没有波形输出,如果没有,说明 MCU 的 CAN 外设根本没工作,回头查时钟使能和引脚复用配置。

4.2 能发不能收,中断进不去

现象:发送正常,另一块板子也显示收到了,但自己的接收中断死活不触发。

原因:滤波器配置错误是最常见的。标准帧的 ID 需要左移 5 位再填入CAN_FilterIdHigh,如果直接填了原始 ID,滤波器匹配的就不是你期望的帧。另一个原因是中断优先级没配置或者 NVIC 没使能CAN1_RX0_IRQn。

解决:先把滤波器设成全通过(掩码全 0)排除过滤问题;检查NVIC_Init里有没有使能对应的中断通道;在中断函数入口翻转一个 GPIO,用示波器看有没有进中断。

4.3 波特率算错,总线错误计数飙升

现象:两块板子都配置了 500k,但通信时好时坏,错误计数器很快涨到 128 进入错误被动状态。

原因:APB1 时钟不是你以为的 36MHz。如果你的系统时钟配置成了 48MHz 或者外部晶振不是 8MHz,APB1 的分频系数会变,波特率自然就偏了。另外CAN_BS1和CAN_BS2的宏定义值容易看错,CAN_BS1_15tq代表的是 15 个时间单元,不是 15+1。

解决:用RCC_GetClocksFreq打印实际 APB1 时钟,重新按公式算 Prescaler。或者直接用 CAN 分析仪测实际波特率,偏差超过 1% 就会不稳定。

4.4 中断里读数据后没释放 FIFO,第二次中断再也不来

现象:第一帧数据能收到,之后接收中断就沉默了。

原因:CAN_Receive只是把数据从邮箱读到结构体,邮箱本身还处于挂起状态。必须调用CAN_FIFORelease(CAN1, CAN_FIFO0)告诉外设这个邮箱已经处理完了,否则中断标志不会清除。

解决:在接收中断处理完数据后立刻释放 FIFO。如果用了 FIFO 锁定模式(CAN_RFLM = ENABLE),还要注意溢出处理。

4.5 终端电阻漏接,短距离能通长距离必挂

现象:两块板子放在桌上用杜邦线连着能通信,线一延长到一米以上就开始丢帧。

原因:CAN 总线两端必须有 120 欧终端电阻来吸收信号反射。很多开发板为了省成本没有焊,短距离时反射影响小,线一长反射就严重了。

解决:在总线两端的 CAN_H 和 CAN_L 之间各并一个 120 欧电阻。如果不确定板子上有没有,断电用万用表量 CAN_H 和 CAN_L 之间的电阻,接近 60 欧说明两端都有,接近 120 欧说明只有一端有,无穷大就是都没有。

5. 进阶用法:用过滤器分组和标识符规划搭一个多节点小网络

把单节点收发跑通之后,下一步通常是多个节点挂在同一根总线上,各取所需。STM32F103 的 14 个滤波器组如果只用一组全通过,等于所有节点都在处理所有报文,CPU 白白浪费。这一章讲怎么用过滤器分组和标识符规划,让每个节点只收自己关心的帧。

5.1 标识符分配与滤波器分组策略

假设有三个节点:节点 A 负责采集传感器,节点 B 负责控制执行器,节点 C 是网关。可以这样分配标准帧 ID:

节点功能标识符范围方向
A传感器数据上报0x100 ~ 0x10FA 发,B/C 收
B执行器控制指令0x200 ~ 0x20FC 发,B 收
C网关转发0x300 ~ 0x30FC 发,A/B 收

节点 B 只需要收 0x200 段和 0x300 段,那就可以配两个滤波器组:一组匹配 0x200 段(掩码屏蔽低 4 位),一组匹配 0x300 段。STM32F103 的滤波器组可以分别激活,每个组可以指向 FIFO0 或 FIFO1,这样不同优先级的报文可以分到不同 FIFO,中断里先处理高优先级 FIFO。

// 滤波器组 0:接收 0x200 ~ 0x20F,存入 FIFO0 CAN_FilterInitStructure.CAN_FilterNumber = 0; CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; // 标准帧 ID 左移 5 位:0x200 << 5 = 0x4000 CAN_FilterInitStructure.CAN_FilterIdHigh = 0x4000; CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000; // 掩码:低 4 位不关心,其余必须匹配 CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x7FF0; CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; CAN_FilterInit(&CAN_FilterInitStructure); // 滤波器组 1:接收 0x300 ~ 0x30F,存入 FIFO1 CAN_FilterInitStructure.CAN_FilterNumber = 1; CAN_FilterInitStructure.CAN_FilterIdHigh = 0x6000; // 0x300 << 5 CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x7FF0; CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO1; CAN_FilterInit(&CAN_FilterInitStructure);

这里掩码0x7FF0的意思是:高 11 位(标准帧 ID 部分)必须完全匹配,低 4 位(也就是 ID 的最低 4 位)不关心。这样 0x200 到 0x20F 都能通过。注意CAN_FilterIdHigh和掩码都是 16 位值,但标准帧 ID 只有 11 位,左移 5 位后占据高 11 位,低 5 位是 RTR、IDE 等标志位,掩码里对应位要设 1 才能保证只匹配数据帧。

5.2 用双 FIFO 做优先级分流

STM32F103 的 CAN 有两个接收 FIFO,每个 FIFO 有 3 个邮箱深度。你可以把紧急报文(比如急停指令)分到 FIFO1,普通传感器数据分到 FIFO0,中断里先读 FIFO1 再读 FIFO0。这样即使 FIFO0 被大量数据塞满,紧急指令也不会被阻塞。

配置的时候注意:每个滤波器组只能指向一个 FIFO,但多个组可以指向同一个 FIFO。FIFO 溢出时如果CAN_RFLM设成 ENABLE,新报文会被丢弃但旧报文保留;设成 DISABLE 则新报文覆盖旧报文。实际项目里紧急报文建议用锁定模式,普通数据用覆盖模式。

5.3 验证多节点通信的实用技巧

多节点调试最怕的是“不知道谁在发、谁没收”。我一般会拿一个 USB-CAN 分析仪挂在总线上,开监听模式看所有报文。如果没有分析仪,可以在每个节点的接收中断里翻转一个 GPIO,用逻辑分析仪或者示波器看哪个节点在什么时候收到了什么。另外,发送的时候尽量用不同的数据内容做标记,比如节点 A 发 0xA1,节点 B 发 0xB1,这样在分析仪上一眼就能看出是谁发的。

从那以后我每次配 CAN 滤波器,都强制先拿全通过模式跑一遍,确认收发链路没问题,再逐步收紧掩码。这样一旦收不到数据,就能立刻判断是滤波器的问题还是链路的问题,省掉大量来回改代码的时间。希望帮到你。

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

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

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

立即咨询