STM32F103 CAN通信实战:从回环自测到双机通信的工程代码骨架
发布时间:2026/10/14 18:31:18 锦皓数字建站

简介这份STM32F103 CAN实例代码面向嵌入式开发初学者与需要快速上手CAN通信的工程师围绕STM32F103系列微控制器的CAN控制器展开帮助读者理解从硬件接口到软件编程的完整实现路径。资源包共81个文件以30个h头文件、28个c源文件为核心配合12个s启动文件及uv2、opt、dep等工程配置与备份文件整体约280KB目录涵盖Source、App、CMSIS与STM32F10x_StdPeriph_Driver等模块结构清晰便于按层查阅。代码涉及CAN初始化、波特率与滤波器配置、发送接收邮箱操作、中断回调及错误处理等关键环节可作为学习CAN帧格式、工作模式切换与HAL/LL库调用的实践参考。目前已有2872人学习下载适合希望借助现成工程快速验证CAN通信、排查配置问题的开发者对照调试与二次开发。1. STM32F103 CAN实例代码从回环到双机通信一个能直接抄的工程骨架很多人第一次在 STM32F103 上写 CAN 实例代码卡住的地方不是寄存器而是“我到底该抄哪一段”。网上搜到的例程要么只跑回环模式要么把过滤器配置写得像黑匣子改一个 ID 就收不到数据。我自己的血泪经验是CAN 的坑几乎全在过滤器、波特率和终端电阻这三件事上代码本身反而简单。这篇笔记围绕 STM32F103 的 CAN 外设给出一套能直接落地的实例代码骨架先讲清楚 bxCAN 的收发模型和选型理由再给出回环自测、双机通信两套可复现的代码最后把过滤器、波特率、中断这几个最容易翻车的点单独拆开。适合已经能点灯、能跑串口准备把两块 F103 板子连起来通信的开发者。全程基于标准外设库寄存器版本会点出差异。2. 先把 bxCAN 的收发模型和硬件连接理清楚在动手写代码之前得先知道 STM32F103 的 CAN 控制器到底怎么工作。它不是“发一个字节就出去”的串口而是一个带邮箱、过滤器、位时序的完整控制器。理解这三块后面调代码才不会靠猜。2.1 三个发送邮箱和两个接收 FIFO 到底怎么用STM32F103 的 bxCAN 有 3 个发送邮箱Mailbox0~2和 2 个接收 FIFOFIFO0、FIFO1每个 FIFO 有 3 级深度。发送时你把报文写进一个空邮箱置位请求发送硬件自动仲裁、发送、等 ACK。接收时硬件按过滤器把报文投递到某个 FIFO你从 FIFO 读出来。这里有个容易忽略的点发送邮箱不是队列是三个并列的槽位。你连续发三帧硬件会按 ID 优先级仲裁不是按你写入顺序发。如果三帧 ID 优先级相同才按请求顺序。所以做周期报文时别指望“先写先发”要靠 ID 优先级设计。接收 FIFO 的溢出也是常见翻车点。FIFO 只有 3 级如果中断里处理太慢或者没及时读第 4 帧来了会置溢出标志旧报文可能被覆盖。我一般会在中断里只做“读出来塞进环形缓冲区”解析放到主循环。2.2 回环、静默、正常三种模式的选型理由bxCAN 有三种测试模式相关配置回环Loopback、静默Silent、静默回环Silent Loopback加上正常模式。选错模式是新手最常见的“发了没反应”原因。回环模式下发送的报文不经过 CAN_TX 引脚直接内部回送到接收路径用来验证过滤器和收发流程不需要外部收发器也不需要另一块板子。静默模式下控制器只接收不发送但能听总线适合做总线监听调试。正常模式才是真正上总线。我的习惯是新板子第一次上电先跑回环确认过滤器和中断都对再接收发器跑正常模式。这样能把“代码问题”和“硬件问题”分开省很多时间。2.3 收发器、终端电阻和共地硬件上最容易忽略的三件事F103 的 CAN_TX/CAN_RX 是 TTL 电平必须外接 CAN 收发器常见的是 3.3V 供电的型号才能上差分总线。收发器到总线之间两端各需要一个 120 欧姆终端电阻总线短、节点少时可以只在两端各一个中间节点不加。共地这件事很多人栽过。CAN 是差分理论上不依赖地但收发器有共模电压范围两块板子如果各自用不同电源且不共地通信会时好时坏甚至烧收发器。我一般会把两块板的 GND 连起来再上总线。提示回环模式不需要收发器和终端电阻但正常模式缺一不可。调试时先确认硬件再怀疑代码。3. 用标准外设库跑通回环自测的最小代码这一章给出一套完整可编译的回环自测代码。目标很明确不接任何外部硬件让 CAN 自己发一帧、自己收到验证过滤器、中断、收发流程全部打通。跑通这段再改正常模式就只是改一个参数。3.1 GPIO、时钟和 CAN 初始化的完整配置先看初始化和回环配置。下面这段是核心省略了系统时钟配置假设已经配到 72MHz。#include stm32f10x.h // CAN 回环自测初始化 发送一帧 中断接收 void CAN_Config_Loopback(void) { GPIO_InitTypeDef GPIO_InitStructure; CAN_InitTypeDef CAN_InitStructure; CAN_FilterInitTypeDef CAN_FilterInitStructure; // 1. 时钟CAN 在 APB1GPIO 在 APB2 RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 2. GPIOCAN_RX 上拉输入CAN_TX 复用推挽 GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; // CAN_RX GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; // CAN_TX GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // 3. CAN 初始化回环模式500kbps 36MHz APB1 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; // 发送优先级由 ID 决定 CAN_InitStructure.CAN_Mode CAN_Mode_LoopBack; // 回环模式 CAN_InitStructure.CAN_SJW CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 CAN_BS1_9tq; CAN_InitStructure.CAN_BS2 CAN_BS2_8tq; CAN_InitStructure.CAN_Prescaler 4; // 36M/(198)/4 500k CAN_Init(CAN1, CAN_InitStructure); // 4. 过滤器接收所有 ID投递到 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); // 5. 中断FIFO0 非空中断 NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel USB_LP_CAN1_RX0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); }逻辑说明时钟部分 CAN1 挂在 APB1GPIO 挂在 APB2漏掉任何一个都会初始化失败。GPIO 的 RX 必须上拉输入因为 CAN 总线空闲是隐性电平内部上拉能避免悬空误触发。波特率计算是36MHz / (1 BS1 BS2) / Prescaler这里36M / 18 / 4 500k。参数说明CAN_ABOM建议开总线异常时能自动恢复CAN_NART关掉表示允许自动重传调试时更容易看到现象CAN_RFLM关掉表示 FIFO 满了新报文覆盖旧的做实时性要求高的场景可以打开锁定。过滤器这里全 0 表示接收所有 ID是最省事的调试配置。3.2 发送一帧标准帧的代码与参数含义初始化完成后发送一帧标准数据帧// 发送一帧标准帧ID0x123数据 8 字节 uint8_t CAN_Send_StdFrame(uint16_t std_id, uint8_t *data, uint8_t len) { CanTxMsg TxMessage; uint8_t mailbox; TxMessage.StdId std_id; // 标准帧 ID11 位 TxMessage.ExtId 0; TxMessage.IDE CAN_Id_Standard; // 标准帧 TxMessage.RTR CAN_RTR_Data; // 数据帧 TxMessage.DLC len; // 数据长度 0~8 for (uint8_t i 0; i len; i) { TxMessage.Data[i] data[i]; } mailbox CAN_Transmit(CAN1, TxMessage); if (mailbox CAN_TxStatus_NoMailBox) { return 1; // 三个邮箱都满发送失败 } // 等待发送完成实际项目建议用中断或超时 uint32_t timeout 0xFFFFF; while (CAN_TransmitStatus(CAN1, mailbox) ! CAN_TxStatus_Ok) { if (--timeout 0) return 2; // 超时 } return 0; }逻辑说明CAN_Transmit返回被占用的邮箱号如果返回NoMailBox说明三个邮箱都满此时要么等要么丢帧。CAN_TransmitStatus轮询发送结果返回Ok表示收到 ACKFailed表示仲裁丢失或错误。参数说明StdId是 11 位范围 0~0x7FFDLC最大 8超过会被截断IDE选标准帧还是扩展帧标准帧 ID 短、优先级仲裁快能用标准帧就别用扩展帧。轮询等待在实际项目里要换成超时或中断否则会阻塞主循环。3.3 中断接收与过滤器命中验证接收中断里把报文读出来验证过滤器是否命中// 接收缓冲区 volatile CanRxMsg g_rx_msg; volatile uint8_t g_rx_flag 0; void USB_LP_CAN1_RX0_IRQHandler(void) { if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) ! RESET) { CAN_Receive(CAN1, CAN_FIFO0, (CanRxMsg *)g_rx_msg); g_rx_flag 1; CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); } } // 主循环里检查 void main_loop_check(void) { if (g_rx_flag) { g_rx_flag 0; // g_rx_msg.StdId 应该是 0x123g_rx_msg.DLC 应该是 8 // 在这里比对数据验证回环是否成功 } }逻辑说明中断里只做读取和置标志解析放主循环避免中断里耗时过长导致 FIFO 溢出。CAN_Receive会把 FIFO 里的报文搬到CanRxMsg结构体同时释放 FIFO 空间。参数说明CAN_IT_FMP0是 FIFO0 非空中断对应USB_LP_CAN1_RX0_IRQn如果过滤器投递到 FIFO1中断向量是CAN1_RX1_IRQn标志是CAN_IT_FMP1。这两个别搞混搞混了就是“发了收不到”的经典原因。4. 改成双机正常通信波特率、过滤器、中断三处改动回环跑通后改成两块板子真实通信只需要改三处模式改正常、过滤器按需收、中断向量确认。但每一处都有细节改错一个就通不了。4.1 波特率计算采样点为什么建议落在 75% 附近CAN 波特率由 APB1 时钟、Prescaler、BS1、BS2 决定。公式是波特率 APB1 / (Prescaler × (1 BS1 BS2)) 采样点 (1 BS1) / (1 BS1 BS2)以 36MHz APB1、500kbps 为例Prescaler41BS1BS218。采样点建议落在 75%~87.5%所以 BS1 取 9、BS2 取 8采样点 10/18 ≈ 55.6%偏低。更好的组合是 BS113、BS24采样点 14/18 ≈ 77.8%Prescaler 仍为 4。参数组合A组合B说明Prescaler44分频系数BS1913相位段1BS284相位段2采样点55.6%77.8%建议 75% 以上波特率500k500k两者相同采样点偏低时长总线或线缆质量差的情况下容易出错。我一般按 75%~80% 配SJW 取 1 就够总线抖动大可以取 2。4.2 过滤器配置掩码模式收指定 ID 的两种写法双机通信通常只关心特定 ID。过滤器有两种常用写法掩码模式IdMask和列表模式IdList。掩码模式适合“收一批 ID”列表模式适合“只收几个精确 ID”。// 掩码模式只接收 ID 高 8 位为 0x12 的标准帧 CAN_FilterInitStructure.CAN_FilterMode CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale CAN_FilterScale_32bit; // 标准帧 ID 左移 5 位放到高 16 位 CAN_FilterInitStructure.CAN_FilterIdHigh (0x123 5); CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; // 掩码高 8 位必须匹配低 3 位不关心 CAN_FilterInitStructure.CAN_FilterMaskIdHigh (0x7F8 5); CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000;逻辑说明32 位掩码模式下FilterIdHigh存的是标准帧 ID 左移 5 位后的值因为标准帧 ID 只有 11 位要放到 16 位寄存器的高位。掩码位为 1 表示该位必须匹配为 0 表示不关心。参数说明0x7F8 5表示高 8 位全匹配、低 3 位不关心这样 0x120~0x127 都能收到。如果要精确收 0x123掩码用0x7FF 5。列表模式则把FilterMode改成IdList每个寄存器存一个 ID最多存 4 个标准帧。4.3 双机联调时先确认的三件事两块板子接好收发器和终端电阻后先别急着跑业务代码按顺序确认三件事第一两端波特率完全一致包括 Prescaler、BS1、BS2、SJW。差一个 tq 就通不了而且现象是“偶尔收到、大量错误”。第二两端都处于正常模式不是回环。回环模式下报文不出引脚另一块板子永远收不到。第三用示波器或逻辑分析仪看 CAN_H/CAN_L 差分波形空闲时应该是隐性电平差分接近 0V发送时能看到明显差分跳变。如果一直隐性说明发送端没真正发出如果一直显性可能是终端电阻或收发器接错。注意双机联调时如果一端用回环、一端用正常正常端会一直等不到 ACK发送邮箱会重传直到错误计数升高。先确认两端模式一致。5. 避坑与排查CAN 调不通时先看这五个地方CAN 调不通的现象就那么几种发不出去、收不到、收到但数据错、时好时坏。下面五条是我踩过的坑按“现象 → 原因 → 解决”写照着排查能省大半天。5.1 发送邮箱一直不空CAN_TransmitStatus永远返回 Failed现象发送代码卡在轮询里或者返回 Failed总线上没有波形。原因最常见的是没有其他节点给 ACK。CAN 发送需要至少一个其他节点正确接收并拉低 ACK 位单节点正常模式发送永远等不到 ACK。其次是波特率不匹配或者收发器没供电。解决调试阶段先用回环模式验证代码再上双机。双机时确认两端波特率一致、收发器供电正常、终端电阻到位。如果只想单机测试用回环或静默回环。5.2 过滤器配了却收不到FIFO 一直是空现象中断不进或者进了但CAN_MessagePending返回 0。原因过滤器没激活或者FilterFIFOAssignment和中断向量不匹配。比如过滤器投递到 FIFO1但中断使能的是 FIFO0。另一个常见原因是 32 位掩码模式下 ID 没有左移 5 位导致匹配不上。解决先确认CAN_FilterActivation ENABLE再确认FilterFIFOAssignment和CAN_ITConfig的 FIFO 一致最后检查 ID 移位。调试时先把过滤器全 0 接收所有 ID确认通路后再收紧。5.3 总线错误计数飙升进入被动错误甚至离线现象CAN_GetLastErrorCode返回错误错误计数寄存器数值很大通信中断。原因终端电阻缺失或阻值不对、总线短路、两端波特率不一致、共地没做。终端电阻缺失时信号反射会导致位错误错误计数快速上升。解决确认总线两端各一个 120 欧姆电阻用万用表量 CAN_H 和 CAN_L 之间电阻断电时应该约 60 欧姆两个 120 并联确认两端共地确认波特率一致。CAN_ABOM打开能让控制器自动恢复但根因不解决还会再离线。5.4 中断里读报文导致 FIFO 溢出丢帧现象高速率通信时偶尔丢帧CAN_GetITStatus能看到溢出标志。原因中断里做了耗时操作比如解析、打印、写 Flash导致 FIFO 3 级深度被填满新报文覆盖旧报文。解决中断里只做CAN_Receive和置标志把报文塞进环形缓冲区解析放主循环。如果速率很高考虑开 FIFO 锁定模式CAN_RFLM ENABLE满了就丢新帧而不是覆盖旧帧至少保证已收到的完整。5.5 标准帧和扩展帧混用ID 对不上现象发送端用标准帧接收端按扩展帧解析ID 完全对不上。原因IDE字段没对齐或者过滤器配置时按扩展帧的移位方式处理标准帧。标准帧 ID 11 位扩展帧 29 位在 32 位过滤器里的存放位置不同。解决统一帧类型。能用标准帧就用标准帧ID 短、仲裁快。过滤器配置时标准帧 ID 左移 5 位放高 16 位扩展帧要拆成高 16 位和低 16 位分别放。混用时给标准帧和扩展帧分配不同的过滤器组。6. 进阶用环形缓冲区 时间戳把 CAN 接收做成可追溯的回环和双机跑通只是起点。真正做项目时CAN 接收不能只靠一个全局变量得有缓冲区、有时间戳、能追溯。这一章给一个我常用的接收框架把中断和主循环彻底解耦。6.1 环形缓冲区的结构设计与写入逻辑核心思路中断里只做“读 FIFO 写环形缓冲区”主循环从缓冲区取。缓冲区用头尾指针不用内存拷贝写入是 O(1)。#define CAN_RB_SIZE 64 // 必须是 2 的幂方便取模 typedef struct { CanRxMsg msg; uint32_t timestamp; // 毫秒时间戳 } CanRbItem; typedef struct { CanRbItem buf[CAN_RB_SIZE]; volatile uint16_t head; // 写入位置 volatile uint16_t tail; // 读取位置 } CanRingBuffer; CanRingBuffer g_can_rb; // 中断里调用写入一条报文 void CAN_RB_Push(CanRxMsg *msg) { uint16_t next (g_can_rb.head 1) (CAN_RB_SIZE - 1); if (next g_can_rb.tail) { return; // 缓冲区满丢弃 } g_can_rb.buf[g_can_rb.head].msg *msg; g_can_rb.buf[g_can_rb.head].timestamp GetTickMs(); g_can_rb.head next; } // 主循环里调用取一条报文 uint8_t CAN_RB_Pop(CanRbItem *item) { if (g_can_rb.tail g_can_rb.head) { return 0; // 空 } *item g_can_rb.buf[g_can_rb.tail]; g_can_rb.tail (g_can_rb.tail 1) (CAN_RB_SIZE - 1); return 1; }逻辑说明head和tail都是volatile因为一个在中断改、一个在主循环改。缓冲区大小取 2 的幂取模用位与代替除法速度快。满的时候直接丢弃不阻塞中断。参数说明CAN_RB_SIZE根据总线负载定500kbps 下如果主循环 10ms 跑一次64 条通常够。时间戳用GetTickMs()基于 SysTick精度 1ms做时序分析够用。6.2 时间戳与丢帧统计怎么判断总线是否健康有了时间戳就能算两帧之间的间隔判断周期报文是否准时。再加一个丢帧计数就能评估总线健康度。volatile uint32_t g_can_drop_count 0; // 缓冲区满丢弃计数 volatile uint32_t g_can_total_count 0; // 总接收计数 // 中断里 void USB_LP_CAN1_RX0_IRQHandler(void) { if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) ! RESET) { CanRxMsg msg; CAN_Receive(CAN1, CAN_FIFO0, msg); g_can_total_count; uint16_t next (g_can_rb.head 1) (CAN_RB_SIZE - 1); if (next g_can_rb.tail) { g_can_drop_count; // 满了记一笔 } else { CAN_RB_Push(msg); } CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); } }逻辑说明g_can_drop_count和g_can_total_count的比值就是丢帧率。如果丢帧率超过 1%要么加大缓冲区要么提高主循环处理速度要么降低总线负载。参数说明这两个计数建议通过串口或调试器定期输出。丢帧率持续为 0 说明缓冲区够偶尔丢一帧可能是主循环被其他任务阻塞要查任务调度。6.3 一个我常用的调试习惯我调 CAN 有个固定习惯第一次上电先跑回环用调试器看g_rx_msg的 ID 和数据对不对再接第二块板子用逻辑分析仪抓 CAN_H/CAN_L确认差分波形和波特率最后才跑业务逻辑。这三步走完基本不会出现“代码看着对但就是不通”的情况。还有一点过滤器别一上来就收所有 ID。调试阶段收所有 ID 方便但业务阶段一定要收紧到实际需要的 ID否则总线上的无关报文会占满 FIFO真正要的报文反而被覆盖。这个坑我在一个多节点项目里踩过后来把过滤器按功能分组每个节点只收自己关心的 ID丢帧率直接降到 0。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。