
简介面向嵌入式开发初学者与工业控制、汽车电子方向工程师的STM32F103 CAN通信实例代码包针对CAN总线开发入门与工程实践需求提供从控制器初始化、波特率与滤波器配置到收发邮箱操作、中断回调及错误处理的完整参考实现。压缩包共81个文件、约280KB以28个c源文件与30个h头文件构成核心驱动与应用逻辑辅以12个s启动文件及工程配置类文件目录按Source、App、CMSIS与标准外设库分层组织便于对照阅读与移植。已有2876人学习下载说明其在CAN入门场景中具备一定参考价值。代码覆盖CAN1/CAN2控制器配置、帧格式处理与HAL库调用示例读者可据此理解差分总线通信流程、调试收发时序并排查常见错误状态快速搭建可运行的CAN通信验证环境。1. 从一块“哑火”的板子说起STM32F103 CAN 实例代码能解决什么去年帮一个做工业控制的朋友调板子两块 STM32F103 板子通过 CAN 收发器对接代码烧进去之后串口打印一切正常但总线上就是收不到任何一帧数据。示波器一挂波形干净得很问题出在初始化顺序和过滤器配置上——这种“代码看着没错、总线就是不通”的场景几乎是每个第一次碰 CAN 的开发者都会踩的坑。这份 STM32F103 CAN 实例代码就是围绕这类真实痛点整理出来的一套可跑通、可对照、可二次开发的工程参考它把 CAN 外设的时钟使能、GPIO 复用、位时序计算、过滤器组配置、发送邮箱与接收 FIFO 的中断处理串成一条完整链路适合正在做车载节点、工控从站、多机通信的嵌入式开发者直接拿来对照自己的工程。你不需要从零啃参考手册的寄存器位定义先把这套代码跑起来再回头理解每一行配置为什么这么写效率会高很多。2. 工程结构与技术选型这套 CAN 实例代码是怎么搭起来的2.1 目录组织与文件职责划分拿到一份 STM32F103 的 CAN 实例代码第一件事不是急着编译而是先看清目录里每个文件管什么。这类工程通常沿用标准外设库Standard Peripheral Library的组织方式核心目录大致如下CAN_Demo/ ├── CMSIS/ # 内核启动文件与系统初始化 ├── STM32F10x_FWLib/ # 标准外设库源码can.c、gpio.c、rcc.c 等 ├── User/ │ ├── main.c # 主流程初始化 收发循环 │ ├── can_app.c # CAN 应用层封装发送、接收、过滤器配置 │ ├── can_app.h │ └── stm32f10x_it.c # 中断服务函数CAN RX0 / TX 中断 ├── Hardware/ │ ├── can_port.c # CAN 引脚与收发器控制 │ └── led.c # 状态指示 └── Project/ # Keil 或 IAR 工程文件这个划分的关键在于把“外设驱动”和“应用逻辑”分开can_app.c里只关心“发一帧、收一帧、配过滤器”不掺和 GPIO 和时钟can_port.c负责 PA11/PA12 的复用推挽配置和收发器使能引脚。二次开发时你改应用层不会碰到底层引脚换板子时只动can_port.c这是这套结构最实用的地方。2.2 为什么选标准库而不是 HALSTM32F103 这一代芯片社区里流传的 CAN 实例代码绝大多数基于标准外设库原因很直接标准库对 CAN 的寄存器封装更贴近手册CAN_InitTypeDef里CAN_SJW、CAN_BS1、CAN_BS2、CAN_Prescaler四个字段和参考手册的位时序图一一对应调波特率时你能清楚知道自己在改哪一段。HAL 库把位时序包在HAL_CAN_Init里抽象层更厚新手反而容易搞不清 500kbps 到底是怎么算出来的。常见做法是如果项目已经用了标准库就直接沿用如果是新项目且芯片资源紧张标准库编译出来的体积也更小。这套实例代码的定位是“看得懂、改得动”所以标准库是更合适的选择。需要提醒的是标准库的stm32f10x_can.c里CAN_Transmit返回的是邮箱号不是成功/失败标志这个细节后面会专门讲。2.3 CAN 外设初始化的完整链路CAN 初始化的顺序有讲究错一步总线就不通。下面这段是can_app.c里最核心的初始化函数我按实际工程习惯补全了注释void CAN_App_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; CAN_InitTypeDef CAN_InitStructure; CAN_FilterInitTypeDef CAN_FilterInitStructure; /* 1. 使能 CAN 和 GPIO 时钟CAN 挂在 APB1 上 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); /* 2. 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_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); /* 3. 位时序APB136MHz分频4BS19tqBS24tqSJW1tq 波特率 36MHz / (4 * (1 9 4)) 500kbps */ 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; CAN_InitStructure.CAN_TXFP DISABLE; CAN_InitStructure.CAN_Mode CAN_Mode_Normal; CAN_InitStructure.CAN_SJW CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 CAN_BS1_9tq; CAN_InitStructure.CAN_BS2 CAN_BS2_4tq; CAN_InitStructure.CAN_Prescaler 4; CAN_Init(CAN1, CAN_InitStructure); /* 4. 过滤器组032位掩码模式接收所有帧进 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 消息挂起中断 */ CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); }逐段说明几个容易翻车的参数。CAN_Prescaler是分频系数取值 1~1024它和 BS1、BS2 一起决定波特率公式是APB1时钟 / (Prescaler * (1 BS1 BS2))其中 1 是同步段。上面 36MHz 除以 4 再除以 14正好 500kbps这是工业现场最常用的速率之一。CAN_ABOM打开后节点进入离线状态会自动恢复调试阶段强烈建议开否则一旦因为错误计数超限掉线你得手动复位才能继续。过滤器那段把 ID 和掩码全设 0意思是“全部接收”先跑通再收紧这是我一贯的顺序——先让数据流起来再谈过滤精度。3. 收发链路拆解一帧 CAN 报文从发送到接收的完整路径3.1 发送三个邮箱怎么用STM32F103 的 CAN1 有 3 个发送邮箱标准库的CAN_Transmit会返回一个邮箱号0/1/2如果三个邮箱都满则返回CAN_TxStatus_NoMailBox。很多实例代码直接忽略返回值结果在高负载时丢帧还找不到原因。正确的写法是先判邮箱是否可用uint8_t CAN_App_Send(uint32_t id, uint8_t *data, uint8_t len) { CanTxMsg TxMessage; uint8_t mailbox; TxMessage.StdId id; /* 标准帧 ID11 位 */ TxMessage.ExtId 0x00; 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) { CAN_CancelTransmit(CAN1, mailbox); return 2; /* 超时取消发送 */ } } return 0; }这里有两个参数值得说清楚。DLC最大是 8超过 8 字节标准库不会帮你截断写 9 会得到未定义行为这是血泪经验。CAN_TransmitStatus的返回值有三种CAN_TxStatus_Failed、CAN_TxStatus_Ok、CAN_TxStatus_Pending轮询时必须等Ok只判“不等于 Failed”是不够的。另外CAN_CancelTransmit在超时后调用能避免邮箱被长期占用导致后续帧发不出去。3.2 接收中断里只做搬运不做业务接收侧最常见的错误是在中断服务函数里直接跑业务逻辑比如解析报文、驱动电机、打印日志。CAN 接收中断触发频率可能很高业务一慢就丢帧。稳妥的做法是中断里只把报文搬进环形缓冲区主循环再消费/* 环形缓冲区容量按实际总线负载调整 */ #define CAN_RX_BUF_SIZE 32 static CanRxMsg rx_buf[CAN_RX_BUF_SIZE]; static volatile uint16_t rx_head 0; static volatile uint16_t rx_tail 0; void USB_LP_CAN1_RX0_IRQHandler(void) { if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) ! RESET) { uint16_t next (rx_head 1) % CAN_RX_BUF_SIZE; if (next ! rx_tail) { /* 缓冲区未满才写入 */ CAN_Receive(CAN1, CAN_FIFO0, rx_buf[rx_head]); rx_head next; } else { /* 缓冲区满丢弃并计数便于排查负载问题 */ CAN_Receive(CAN1, CAN_FIFO0, rx_buf[rx_head]); } CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); } }CAN_Receive的第二个参数是 FIFO 编号过滤器分配到 FIFO0 就必须读 FIFO0读错 FIFO 会一直读不到数据。中断函数名USB_LP_CAN1_RX0_IRQHandler是 STM32F103 的固定向量名写在startup_stm32f10x_md.s里改名会导致中断进不去——这个坑我在早期项目里踩过编译不报错就是收不到数据查了半天。3.3 主循环消费与状态指示主循环从环形缓冲区取数据做业务解析同时用 LED 指示总线活动方便现场判断节点是否活着int main(void) { uint8_t tx_data[8] {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}; SystemInit(); LED_Init(); CAN_App_Init(); NVIC_Config(); /* 配置 CAN RX0 中断优先级 */ while (1) { /* 发送一帧心跳ID 0x123 */ CAN_App_Send(0x123, tx_data, 8); /* 消费接收缓冲区 */ while (rx_tail ! rx_head) { CanRxMsg *msg rx_buf[rx_tail]; if (msg-StdId 0x456) { LED_Toggle(); /* 收到对端帧翻转指示灯 */ } rx_tail (rx_tail 1) % CAN_RX_BUF_SIZE; } Delay_ms(100); } }NVIC_Config里要把 CAN RX0 中断的抢占优先级设得比 SysTick 高否则在Delay_ms里可能被延迟响应。这段主循环刻意写得简单是为了让读者先确认“发得出、收得到”再往里加协议解析。实际项目里心跳周期、ID 分配、数据字段都要按协议文档来但骨架就是这三步初始化、发、收。4. 避坑与排查CAN 总线不通时先看这几处4.1 现象串口打印正常总线上测不到波形原因通常是 GPIO 复用没配对。PA11 必须是上拉输入GPIO_Mode_IPUPA12 必须是复用推挽GPIO_Mode_AF_PP如果 PA12 配成普通推挽CAN 外设根本接管不了引脚。解决方法是回头检查can_port.c里的 GPIO 配置并用万用表确认收发器供电和使能引脚电平。4.2 现象能发不能收或者收发都失败先查过滤器。过滤器组没使能CAN_FilterActivation为 DISABLE、FIFO 分配和中断读取的 FIFO 不一致、掩码模式配错都会导致“帧到了但进不了 FIFO”。排查手段是把过滤器临时设成全接收ID 和掩码全 0确认链路通了再逐步收紧。另一个高频原因是终端电阻CAN 总线两端各需要 120Ω只接一端或者一个都没接短距离可能勉强通信稍长一点就大量错误帧。4.3 现象波特率对不上错误计数器飙升位时序参数和实际 APB1 时钟不匹配是主因。STM32F103 在 72MHz 主频下 APB1 是 36MHz但如果你改过时钟树APB1 可能不是 36MHz此时按 36MHz 算出来的分频值就全错了。解决方法是先确认SystemCoreClock和 APB1 分频再套公式重算CAN_Prescaler。采样点位置也有讲究BS1 和 BS2 的比例影响采样点工业现场常用 87.5% 左右即(1BS1)/(1BS1BS2)接近 0.875。4.4 现象运行一段时间后节点掉线复位才能恢复这是没开CAN_ABOM的典型表现。总线干扰导致错误计数超过 255节点进入离线状态标准库默认不会自动恢复。把CAN_ABOM设为 ENABLE 后硬件会在检测到 128 次连续 11 位隐性后自动回到正常状态。同时建议在应用层加一个错误计数监控通过CAN_GetLastErrorCode读取最近错误类型便于定位是位错误、格式错误还是应答错误。4.5 现象中断进不去或者进去一次就卡死中断进不去先查向量表里的函数名是否和启动文件一致再查NVIC_Init是否使能了对应通道。进去一次就卡死多半是中断里没清挂起标志CAN_ClearITPendingBit漏掉就会反复触发。还有一种情况是中断优先级配置冲突CAN RX0 和 USB 共用中断向量如果工程里同时用了 USB要确认两者没有互相覆盖。5. 二次开发改造从跑通到落地的几个实用手法把这套实例代码跑通只是起点真正落地时要按项目需求改造。我一般从三个方向动手协议层封装、错误处理强化、可测试性设计。协议层封装上建议在can_app.c之上再加一层can_protocol.c把“ID 含义、数据字段、周期”从收发逻辑里剥离出来。比如定义一个结构体描述每条报文typedef struct { uint32_t id; uint8_t dlc; uint16_t period_ms; void (*on_recv)(uint8_t *data, uint8_t len); } CanMsgDef; static const CanMsgDef msg_table[] { {0x123, 8, 100, NULL}, /* 本节点发送的心跳 */ {0x456, 8, 0, HandleCtrlFrame}, /* 接收的控制帧 */ };这样新增一条报文只改表不动收发主流程后期维护成本低很多。错误处理上把CAN_App_Send的返回值利用起来邮箱满时进重试队列而不是直接丢同时统计发送失败次数通过调试串口输出现场排查时这就是黑匣子。可测试性上建议留一个“回环模式”开关把CAN_Mode临时改成CAN_Mode_LoopBack不接收发器也能自测收发链路验证过滤器 and 中断配置是否正确这个习惯帮我省过很多次来回插拔线的时间。还有一个容易被忽略的点CAN 帧的字节序。多字节数据在总线上是大端传输如果你的 MCU 是小端组帧和解析时要做转换否则对端读出来的数值是反的。我一般会在协议层统一用移位拼装不直接 memcpy 结构体避免对齐和字节序双重坑。从那以后我每次拿到一份新的 CAN 实例代码都强制先跑回环模式确认收发链路再上真实总线最后才调过滤器——这个顺序帮我避开了太多“以为是硬件问题、其实是配置问题”的弯路。希望这套拆解能帮到你把板子上的 CAN 真正跑起来。本文还有配套的精品资源点击获取