
简介本资源是一套面向嵌入式初学者与STM32进阶开发者的CAN通信实战代码包聚焦STM32F103系列芯片的CAN总线底层驱动与应用层实现解决CAN回环自测、双节点对接等典型工业通信场景中的配置难点与时序调试问题。压缩包共95个文件含57个头文件封装寄存器定义与接口函数、31个C源文件涵盖HAL库驱动、应用逻辑及CubeMX生成的底层初始化代码、1个启动汇编文件、1个链接脚本.ld、1个Makefile构建配置及PDF原理图与README说明文档整体体积仅1.86MB结构清晰、模块解耦驱动层代码具备跨平台移植能力。已有2691人学习下载配套《STM32专栏》知识点深度联动特别提供CAN时序参数计算验证方法与双模式回环/对接完整工程助读者透彻理解CAN波特率配置、过滤器设置、中断收发机制及错误帧分析等核心内容。 做嵌入式这几年凡是涉及到工业现场、车载电子的项目基本都绕不开 CAN 总线。STM32F103 这颗芯片之所以经久不衰除了量大管饱、价格实在另一个重要原因就是它内部集成了符合 CAN 2.0B 规范的 bxCAN 控制器——这意味着你在做多机通信时不需要额外挂一片 MCP2515 或者 SJA1000直接接一个 CAN 收发器芯片就能上总线。这篇文章是 STM32 基础系列的第五篇重点把 F103 的 CAN 通信代码讲透从外设结构到初始化从发送接收到调试排错争取让第一次接触 CAN 的同学也能照着把流程跑通。1. 不要急着敲代码先理解 CAN 总线在项目里的定位很多人学 CAN 通信上来就是找例程、下工程、改引脚结果板子一跑发现数据收不到然后就卡住了。其实 CAN 和串口、I2C 这类外设有一个本质区别CAN 是一个完整的协议栈而不只是一根数据线。它的物理层、数据链路层、应用层都是被明确定义的你写的代码只是把 MCU 里的数据交给控制器控制器再按协议去发。所以理解 CAN 的工作模型远比背代码重要。F103 的 CAN 控制器有两个值得注意的特点。第一它支持 CAN 2.0A 和 CAN 2.0B也就是说标准帧11 位标识符和扩展帧29 位标识符都能收发这也是目前工业现场最主流的两种帧格式。第二它内置了 3 个发送邮箱和 2 个接收 FIFO硬件上做了缓冲这就意味着你不需要像写串口那样自己维护一个大的软件发送队列——只要邮箱空的你把数据丢给控制器它会自己排队发送。1.1 CAN 和 UART点对点与多机共线的区别我在带新人时经常会问一个问题既然串口也能通信为什么还要用 CAN答案是串口是点对点的CAN 是真正意义上的多机总线。串口也可以组网用 RS485 加地址帧但总线仲裁很弱一旦两个节点同时抢线数据就冲突了只能靠主机轮询来规避。CAN 不一样。CAN 总线上的每个节点都可以主动发送数据而且它有一套硬件上的非破坏性仲裁机制当多个节点同时发送时标识符小的帧优先发送其他节点自动转为接收。这个过程完全由 CAN 控制器的硬件完成不占用 CPU。这意味着在电机控制器、传感器、上位机这些节点之间你可以大胆地让任何一个节点主动上报数据不需要主机去轮询整个系统的实时性会好很多。1.2 内置 bxCAN vs 外挂 MCP2515什么时候需要外扩F103 内置 CAN 控制器但注意它只是控制器不是收发器。CAN 控制器的输出是 TX 引脚输出的数字信号必须经过 CAN 收发器如 TJA1050、SN65HVD230转换成 CAN_H 和 CAN_L 这对差分信号才能接到总线上去。这是新手最容易忽略的一点——以为 STM32 的 PA12 引脚直接就能接 CAN 总线了实际上中间还需要一颗收发器芯片再加上 120Ω 终端电阻。那什么时候需要外挂 CAN 控制器芯片呢最常见的场景是你手上只有一个不集成 CAN 的 MCU比如某些低端型号但又需要接入 CAN 总线。MCP2515 就是一颗经典的 SPI 转 CAN 芯片通过 SPI 接口和 MCU 通信。但 F103 已经内置了 bxCAN完全没有必要多此一举。另外如果你刚开始调试不想焊收发器可以用 F103 的环回模式LoopBack数据会从内部返回不需要外部硬件也能验证代码流程这个后面会细说。2. F103 的 bxCAN 外设三个关键点先弄明白开始写代码之前建议先把 bxCAN 的几个硬件特性搞清楚。尤其是引脚、波特率和过滤器这三个点几乎覆盖了 CAN 通信 80% 的配置错误来源。2.1 引脚分配PA11/PA12 和 PA9/PA10 是两码事网络热词里我看到有人搜stm32f103 pa9 pa10 哪个是 tx rx这个问题在 CAN 通信里特别典型。很多同学照着串口的思路找 CAN 的引脚结果在 PA9、PA10 上浪费时间。这两个引脚是USART1的 TX 和 RX不是 CAN 的。F103 的 CAN1 默认复用引脚是引脚功能PA11CAN1_RX注意是接收PA12CAN1_TX注意是发送这里还有一个坑CAN 的 RX 引脚是 PA11TX 是 PA12和串口的命名习惯PA9TXPA10RX正好相反。我第一次写 CAN 代码的时候因为想当然地认为编号小的是 RX结果把 PA11 配成了 TX、PA12 配成了 RX回环测试当然怎么都不通。所以一定要看清数据手册或者参考手册的引脚复用表。另外如果你用的是互联型 F103比如 STM32F105CAN1 和 CAN2 都有引脚会更复杂一些。但主流项目中 F103 系列用的 CAN 基本就是 PA11/PA12 这一组记牢就行。2.2 波特率到底怎么算CAN 的波特率计算不复杂但公式有坑。F103 的 CAN 外设挂在APB1总线上APB1 的时钟是 36MHz系统时钟 72MHz 二分频所得。CAN 控制器内部有一个波特率预分频器和两个时间段寄存器最终一个 bit 的时间由同步段 时间段1BS1 时间段2BS2组成。公式如下CAN波特率 APB1时钟 / 预分频值 × 1 BS1 BS2注意这里每个数字的单位是 Tq时间量子。同步段固定为 1 TqBS1 在标准库里通过CAN_BS1_xTq宏配置BS2 对应CAN_BS2_xTq。举例配置 500kbps 波特率很多人常用的参数是CAN_InitStruct.CAN_Prescaler 4; // 预分频 4 CAN_InitStruct.CAN_BS1 CAN_BS1_11tq; // BS1 11 Tq CAN_InitStruct.CAN_BS2 CAN_BS2_6tq; // BS2 6 Tq那么36MHz / 4 × 1 11 6 36MHz / 72 500kHz。刚好是 500kbps。这个组态的采样点 1 11/1 11 6 66.7%对于大多数工业现场来说是够用的。如果你要的是 250kbps那可以把预分频改成 8其他不变36MHz / 8 × 18 250kHz。提示不要照抄网上的波特率配置就去和别人的设备通信。总线上所有节点的波特率必须完全一致而且采样点最好也接近。特别是和 PLC、电机控制器这类现成设备对接时先查对方的手册确认它用的波特率和采样点再用公式反推自己的预分频和 BS1/BS2。2.3 过滤器就是 CAN 的门卫F103 的 bxCAN 有 14 个过滤器组F103 非互联型每个过滤器组可以配置为 32 位掩码模式、32 位列表模式、16 位掩码模式或者 16 位列表模式。刚接触时会觉得抽象但它的作用其实很简单根据报文标识符决定哪些消息进入接收 FIFO哪些消息直接丢弃。过滤器的工作原理可以这样理解你给门卫规定了一个名单ID再规定了一个模糊匹配的范围掩码。只有通过匹配的报文才允许进入 FIFO0 或 FIFO1你的 CPU 才会在中断里收到它。其余报文在硬件层面就被丢弃了不占用 CPU。比如我只想接收 ID 为 0x123 的标准数据帧可以这样配置过滤器CAN_FilterInitStruct.CAN_FilterNumber 0; // 使用过滤器组0 CAN_FilterInitStruct.CAN_FilterMode CAN_FilterMode_IdMask; // 掩码模式 CAN_FilterInitStruct.CAN_FilterScale CAN_FilterScale_32bit; // 32位位宽 CAN_FilterInitStruct.CAN_FilterIdHigh (0x123 5); // 标准ID放在高16位的bit[15:5] CAN_FilterInitStruct.CAN_FilterIdLow 0; CAN_FilterInitStruct.CAN_FilterMaskIdHigh 0xFFE0; // 高11位必须匹配 CAN_FilterInitStruct.CAN_FilterMaskIdLow 0; CAN_FilterInitStruct.CAN_FilterFIFOAssignment CAN_FIFO0; // 消息进FIFO0 CAN_FilterInitStruct.CAN_FilterActivation ENABLE; CAN_FilterInit(CAN_FilterInitStruct);这段代码里(0x123 5)是把 11 位标准 ID 左移到 16 位寄存器的高 11 位位置。掩码0xFFE0表示高 11 位必须匹配低 5 位是 RTR、IDE 这些控制位不关心也符合只接收标准数据帧的需求。如果你希望完全匹配数据帧的控制位可以把掩码设成0xFFFF但一般场景下0xFFE0就够了。提示调试阶段最常用的过滤器配置是——让所有报文都进来也就是把掩码全部设为 0。这样你至少能确认硬件链路和中断流程是否正常然后再把过滤条件加上。千万别一上来就配置一个很严格的过滤结果数据没进中断你根本分不清是硬件问题还是过滤条件问题。3. CAN 初始化代码从时钟到过滤器一次配好接下来就是实际操作环节。下面的代码基于 STM32 标准外设库StdPeriph_LibF103 的开发里标准库仍然是主流选择代码清晰、寄存器操作透明。HAL 库的 CAN 驱动思路类似但 API 封装层次更高后面会简单提一下差异。3.1 时钟与 GPIO 配置CAN 外设的时钟使能要开两个一个是 GPIOA 的时钟引脚在 PA11/PA12另一个是 CAN1 所在的 APB1 总线时钟。注意 CAN1 的时钟是挂在 APB1 上的不是 APB2很多新手会把RCC_APB1Periph_CAN1错写成RCC_APB2Periph_CAN1编译虽然不报错但 CAN 根本没有时钟后面怎么调都不通。引脚配置上有两个细节PA11CAN_RX配置为上拉输入GPIO_Mode_IPU因为 CAN 总线空闲时是隐性电平内部上拉能让 RX 引脚在总线空闲时保持高电平避免误触发。PA12CAN_TX配置为复用推挽输出GPIO_Mode_AF_PP输出速度建议 50MHz。void CAN1_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); /* PA11 - CAN_RX上拉输入 */ GPIO_InitStruct.GPIO_Pin GPIO_Pin_11; GPIO_InitStruct.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStruct); /* PA12 - CAN_TX复用推挽输出 */ GPIO_InitStruct.GPIO_Pin GPIO_Pin_12; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStruct); }3.2 控制器参数配置F103 标准库提供了CAN_Init函数参数是一个CAN_InitTypeDef结构体。这个结构体里有几个参数需要动脑子理解而不是照抄。void CAN1_Controller_Config(void) { CAN_InitTypeDef CAN_InitStruct; CAN_InitStruct.CAN_TTCM DISABLE; // 关闭时间触发模式 CAN_InitStruct.CAN_ABOM ENABLE; // 使能自动离线恢复 CAN_InitStruct.CAN_AWUM ENABLE; // 自动唤醒模式 CAN_InitStruct.CAN_NART DISABLE; // 禁止非自动重传即自动重传 CAN_InitStruct.CAN_RFLM DISABLE; // FIFO溢出时不锁定 CAN_InitStruct.CAN_TXFP DISABLE; // 发送优先级由报文标识符决定 CAN_InitStruct.CAN_Mode CAN_Mode_Normal; // 正常模式 CAN_InitStruct.CAN_SJW CAN_SJW_1tq; // 同步跳转宽度1Tq CAN_InitStruct.CAN_BS1 CAN_BS1_11tq; // 时间段1 CAN_InitStruct.CAN_BS2 CAN_BS2_6tq; // 时间段2 CAN_InitStruct.CAN_Prescaler 4; // 波特率预分频 CAN_Init(CAN1, CAN_InitStruct); }重点说两个参数。CAN_ABOM自动离线管理。如果总线上的错误太多CAN 控制器会进入 Bus-Off 状态不再参与通信。如果这个位是 DISABLE控制器就会一直停在那里直到你手动重新初始化如果设为 ENABLE控制器会自动等待 128 个总线空闲周期后恢复。在工业现场总线受干扰导致离线很常见所以这个位我强烈建议使能。CAN_TXFP发送优先级。默认 DISABLE 时发送优先级由报文标识符决定标识符小的先发如果设成 ENABLE就按发送请求的顺序排队。具体用哪个要看你的应用如果所有节点的报文优先级重要保持 DISABLE如果只是顺序上报、没有优先级区分设 ENABLE 更公平。我一般保持 DISABLE因为 CAN 的低 ID 优先机制本来就是协议卖点。3.3 过滤器配置的两种常用场景过滤器配置的完整代码我放在 2.3 节已经展示过这里补充说两种实际常用的场景。场景一只接收特定 ID。比如主机只关心 ID 为 0x100、0x101 的两条报文这时可以用掩码模式把掩码的高 11 位设成只允许这两个 ID。实际上掩码模式能匹配的是一个连续范围比如掩码0xFFF0高 12 位匹配低 4 位不关心就能把 0x100~0x10F 全部收进来。如果只需精确匹配两个 ID更直接的办法是列表模式把 ID 0x100 和 0x101 分别填进过滤器的高 16 位和低 16 位硬件会精确匹配这两个 ID。场景二全部接收。调试时最省事把CAN_FilterMaskIdHigh和CAN_FilterMaskIdLow都设为 0这样所有报文都能通过。等确认总线数据无误后再逐步加过滤条件。/* 场景二调试时全部接收 */ CAN_FilterInitStruct.CAN_FilterIdHigh 0; CAN_FilterInitStruct.CAN_FilterIdLow 0; CAN_FilterInitStruct.CAN_FilterMaskIdHigh 0; /* 全部不关心全部接收 */ CAN_FilterInitStruct.CAN_FilterMaskIdLow 0;4. 收发数据的标准库代码发送、接收和中断初始化配置好之后收发函数就是整个 CAN 通信的核心。我给的代码不追求炫技重点是稳定、超时处理到位保证你在实际项目中能直接用。4.1 发送函数带上超时判断标准库的发送 API 是CAN_Transmit它接受一个CanTxMsg结构体指针里面填写标准 ID、IDE、RTR、DLC 和数据内容。CAN_Transmit返回一个 mailbox 编号如果三个发送邮箱都满了返回CAN_TxStatus_NoMailBox。注意CAN_Transmit只是把数据交给硬件邮箱并不代表数据已经发到总线上了。真正发完需要查询CAN_TransmitStatus返回CAN_TxStatus_Ok。所以一个健壮的发送函数要轮询等待发送完成同时要防止死等uint8_t CAN_SendMsg(uint32_t id, uint8_t *data, uint8_t len) { CanTxMsg TxMsg; uint8_t mailbox; uint32_t timeout 0; uint8_t i; if (len 8) len 8; /* CAN数据场最大8字节 */ TxMsg.StdId id; TxMsg.ExtId 0; TxMsg.IDE CAN_Id_Standard; /* 标准帧 */ TxMsg.RTR CAN_RTR_Data; /* 数据帧 */ TxMsg.DLC len; for (i 0; i len; i) { TxMsg.Data[i] data[i]; } mailbox CAN_Transmit(CAN1, TxMsg); if (mailbox CAN_TxStatus_NoMailBox) { return 0; } while (CAN_TransmitStatus(CAN1, mailbox) CAN_TxStatus_Pending) { timeout; if (timeout 0xFFFF) { return 0; /* 超时返回失败 */ } } return (CAN_TransmitStatus(CAN1, mailbox) CAN_TxStatus_Ok) ? 1 : 0; }为什么这里要轮询等待因为如果只调用CAN_Transmit就立刻返回下一次CAN_Transmit时上一个报文可能还在邮箱里排队帧的顺序就乱了。轮询等待确保前一个报文已发送完成或失败再发下一个。4.2 接收 FIFO0 中断不丢数据的写法F103 的 CAN 接收可以轮询也可以中断。轮询简单但如果 CPU 忙于其他任务FIFO 满了之后新的报文会被丢弃。在工业现场丢一帧数据可能就意味着位置误差或者状态丢失所以推荐用中断接收。首先初始化时使能 FIFO0 的接收中断CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);然后配置 NVIC。注意 F103 中 CAN1 的接收中断向量和 USB 共用名称是USB_LP_CAN1_RX0_IRQn这个细节特别隐晦很多新手在中断服务函数名上卡了很久。本文还有配套的精品资源点击获取