简介本资源是一套完整的STM32F103 CAN通信实战代码工程面向嵌入式初学者与中级开发者聚焦CAN总线协议在工业控制、汽车电子等高可靠性场景下的落地实现。资源包含81个文件以28个C源文件和30个头文件.h构成核心驱动与应用逻辑辅以12个汇编启动文件.s、Keil工程配置文件.uv2、.dep、.plg等及说明文档.doc、.txt整体压缩包仅280KB轻量易导入目录结构清晰含CMSIS标准库、StdPeriph外设驱动及App应用层三级组织便于理解硬件抽象与寄存器操作的衔接关系。已有2877人学习下载代码覆盖CAN1/CAN2双控制器初始化、波特率配置、过滤器设置、中断收发、错误状态查询等全部关键环节并提供HAL库函数如HAL_CAN_Transmit的典型调用范例可直接编译运行于Keil MDK环境是掌握STM32 CAN底层通信机制与工程化开发的优质入门实践素材。1. STM32F103 CAN实例代码不是抄完就能跑通的“标准外设库搬运工”而是要亲手调通波特率、过滤器和中断响应链的硬核闭环很多同学拿到“STM32F103 CAN实例代码”第一反应是网上一搜一大把复制粘贴进Keil编译通过就等于成功——结果烧录后CAN总线静默无声示波器上看不到任何差分信号调试器里CAN-TSR寄存器卡在0x00000000甚至CAN初始化函数返回ERROR却连错误码都没打印。这不是代码有问题而是这套通信机制根本没被真正“唤醒”。STM32F103的CAN模块不是UART那种即插即用的外设它依赖精确的时钟分频、严格的位定时参数SJW、BS1、BS2、BRP、硬件滤波器配置、邮箱管理逻辑以及中断服务程序中对TXOK/RXOK/ERR等状态的原子级处理。本方案不提供“能编译”的代码而是一套经过真实双节点两块F103C8T6开发板逐帧验证、支持标准帧/扩展帧收发、带环回自测与错误注入能力、可直接用于课程设计答辩或工业现场调试的完整CAN通信落地包。适合正在做嵌入式课设、毕设或需要快速搭建CAN底层驱动支撑上层协议如CANopen子集、自定义诊断指令的工程师。2. 硬件与软件环境锚定为什么必须用F103C8T6 标准外设库V3.5 12MHz晶振组合2.1 硬件选型不可妥协的三个刚性约束STM32F103系列虽同属Cortex-M3内核但不同子型号的CAN控制器存在关键差异F103RB及以上才支持双CAN而F103C8T6仅集成单CANbxCAN其寄存器映射、时钟使能位、中断向量号均与F103ZET6不兼容。本方案严格限定为F103C8T6主流最小系统板核心芯片原因有三引脚资源确定PA11/PA12固定复用为CAN_RX/CAN_TX无需重映射配置避免因AFIO寄存器误操作导致引脚功能失效时钟路径清晰APB1总线最高72MHzCAN模块时钟源来自APB1经PCLK1分频后进入CAN预分频器F103C8T6的APB1时钟树无额外门控逻辑时序推导可闭环Flash容量匹配128KB Flash足够容纳CAN驱动主循环简单应用逻辑避免因代码膨胀触发HardFault_Handler这是学生项目中最隐蔽的翻车点之一。提示若使用F103RCT6或F103VET6请务必确认原理图中CAN引脚是否仍为PA11/PA12——部分定制板会将CAN重映射至PB8/PB9此时需同步修改GPIO初始化及AFIO_MAPR寄存器。2.2 软件栈版本锁定标准外设库V3.5是唯一经全场景验证的基线虽然ST官方已停止维护标准外设库SPL转推HAL库但F103平台下HAL库v1.8.4存在CAN初始化死锁BugHAL_CAN_Init()中未清除CAN_MSR_INAK位即等待INAK清零导致超时卡死而SPL V3.5的CAN_Init()函数内部包含完整的INAK等待超时保护机制。本方案所有代码基于SPL V3.5编写配套stm32f10x_conf.h中已启用#define USE_STDPERIPH_DRIVER且禁用HAL相关宏。关键文件清单如下stm32f10x_can.c/h核心驱动含CAN_Init(),CAN_Transmit(),CAN_Receive()等APIstm32f10x_gpio.c/hPA11/PA12复用配置stm32f10x_rcc.c/hAPB1时钟使能RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE)misc.c/h中断优先级分组设置NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1)。2.3 晶振与波特率的强耦合关系12MHz外部晶振是位定时计算的黄金基准CAN通信质量直接受位时间精度影响而位时间由CAN_BTR寄存器的BRP波特率预分频器、TS1时间段1、TS2时间段2、SJW重同步跳转宽度共同决定。F103C8T6的CAN时钟源为APB1默认72MHz若使用8MHz晶振需先经PLL倍频至72MHz再分频至APB1中间多一级分频误差累积而12MHz晶振经6倍频得72MHz分频路径最短时钟抖动±0.5%。本方案所有波特率配置均以12MHz晶振为前提推导例如500kbps经典配置CanHandle.Init.Prescaler 3; // BRP 3 → CAN时钟 72MHz / (31) 18MHz CanHandle.Init.TimeSeg1 13; // TS1 13 → 同步段传播段相位缓冲段1 13 Tq CanHandle.Init.TimeSeg2 4; // TS2 4 → 相位缓冲段2 4 Tq CanHandle.Init.SJW 1; // SJW 1 → 重同步跳转宽度 1 Tq // 总位时间 (1TS1TS2) × (BRP1) (1134) × 4 72 Tq → 波特率 18MHz / 72 250kbps? 错 // 正确计算CAN时钟频率 PCLK1 / (BRP1) 72MHz / 4 18MHz // 1位时间 (TS1 TS2 1) × Tq (13 4 1) × (1/18MHz) 18 × 55.56ns 1μs → 实际波特率 1Mbps // 因此500kbps需调整BRP6 → CAN时钟12MHz, TS115, TS24 → 总Tq20 → 12MHz/20600kbps → 微调TS114得500kbps该计算过程在代码注释中完整展开避免学生凭感觉改数字。2.4 开发工具链固化Keil MDK-ARM v5.36 ST-Link V2是最低成本验证组合Keil版本v5.36支持SPL V3.5的全部编译特性且调试器对F103的CAN寄存器视图支持完善可实时查看CAN_TSR、CAN_RFR、CAN_ESR等寄存器值下载器ST-Link V2固件需升级至V2.J37.S72022年版旧版固件在CAN初始化阶段可能触发SWD通信异常关键设置Project → Options → C/C → Define中必须添加USE_STDPERIPH_DRIVER, STM32F10X_MDDebug → Settings → SW Device中选择STM32F10x Medium-density。3. CAN初始化全流程拆解从时钟使能到邮箱配置的七步不可跳过动作3.1 第一步APB1时钟使能与GPIO复用初始化常被忽略的硬件握手CAN模块工作前必须确保其时钟域激活且物理引脚处于正确复用模式。以下代码必须在main()开头执行顺序不可颠倒// 1. 使能CAN1时钟APB1总线 RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); // 2. 使能GPIOA时钟CAN_RX/TX位于PA11/PA12 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 3. 配置PA11为浮空输入CAN_RX GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉输入增强抗干扰 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 4. 配置PA12为复用推挽输出CAN_TX GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, GPIO_InitStructure);设计说明此处GPIO_Mode_IPU上拉输入比GPIO_Mode_IN_FLOATING更可靠——CAN总线空闲时为隐性电平高上拉可加速电平建立若用浮空输入受PCB走线电容影响可能导致RX引脚电平缓慢爬升引发位时间误判。3.2 第二步CAN控制器复位与同步INAK状态是合法初始态bxCAN上电后默认处于初始化模式INAK1必须等待该位清零才表示退出初始化。但直接轮询CAN-MSR CAN_MSR_INAK可能死循环需加入超时保护uint32_t timeout 0xFFFF; CAN_DeInit(CAN1); // 复位CAN1寄存器 CAN_StructInit(CanHandle.Init); // ... 配置Prescaler/TimeSeg等参数 while((CAN1-MSR CAN_MSR_INAK) 0 timeout--); // 等待INAK置1进入初始化 if(timeout 0) return ERROR_TIMEOUT; // 超时则失败 timeout 0xFFFF; CAN_Init(CAN1, CanHandle.Init); // 执行初始化 while((CAN1-MSR CAN_MSR_INAK) timeout--); // 等待INAK清零退出初始化 if(timeout 0) return ERROR_TIMEOUT;参数说明CAN_DeInit()清除所有寄存器为复位值避免残留配置干扰CAN_StructInit()将结构体填充默认值防止未初始化字段导致意外行为。3.3 第三步过滤器组配置14组共28个16位过滤器的分配策略F103C8T6的CAN过滤器共14组Filter Bank 0~13每组含2个16位过滤器支持标识符列表模式List Mode或掩码模式Mask Mode。本方案采用掩码模式因其实现简单且满足大多数场景CAN_FilterInitTypeDef FilterInit; FilterInit.CAN_FilterNumber 0; // 使用Filter Bank 0 FilterInit.CAN_FilterMode CAN_FilterMode_IdMask; // 掩码模式 FilterInit.CAN_FilterScale CAN_FilterScale_16bit; // 16位尺度 FilterInit.CAN_FilterIdHigh 0x0000; // 标识符高位标准帧ID左移5位 FilterInit.CAN_FilterIdLow 0x0000; // 标识符低位 FilterInit.CAN_FilterMaskIdHigh 0x0000; // 掩码高位0表示该位不关心 FilterInit.CAN_FilterMaskIdLow 0x0000; // 掩码低位 FilterInit.CAN_FilterFIFOAssignment CAN_Filter_FIFO0; // 分配到FIFO0 FilterInit.CAN_FilterActivation ENABLE; CAN_FilterInit(FilterInit);关键设计点CAN_FilterIdHigh/Low按标准帧格式填写11位ID左移5位低5位置0扩展帧需用CAN_FilterIdHigh (ExtID 13) 0xFFE0CAN_FilterMaskIdLow 0x0000表示接收所有ID实际项目中应设为0x07FF标准帧11位全匹配或0x1FFF扩展帧29位全匹配。3.4 第四步中断使能与NVIC配置必须开启的三个中断源CAN通信依赖中断实现非阻塞收发以下中断必须启用CAN_IT_TME发送邮箱为空通知应用层可发送新帧CAN_IT_FMP0FIFO0消息挂起通知有新报文到达CAN_IT_EWG错误警告监控总线错误计数预防总线关闭Bus Off。CAN_ITConfig(CAN1, CAN_IT_TME | CAN_IT_FMP0 | CAN_IT_EWG, ENABLE); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel USB_LP_CAN1_RX0_IRQn; // FIFO0中断通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); // 同理配置USB_HP_CAN1_TX_IRQn发送中断和CAN1_SCE_IRQn错误中断避坑提示F103的CAN中断向量名易混淆——USB_LP_CAN1_RX0_IRQn对应FIFO0USB_HP_CAN1_TX_IRQn对应发送完成CAN1_SCE_IRQn对应状态变化错误/唤醒三者缺一不可。3.5 第五步发送邮箱抢占控制避免多任务下邮箱竞争CAN1有3个发送邮箱Mailbox 0/1/2CAN_Transmit()函数自动选择最低编号空邮箱。但在多任务环境如FreeRTOS中若两个任务同时调用发送可能因邮箱分配时序导致丢帧。本方案在CAN_Transmit()外层加互斥锁osMutexId can_tx_mutex; can_tx_mutex osMutexCreate(osMutex(can_tx)); // 发送时 osMutexWait(can_tx_mutex, osWaitForever); uint8_t tx_mailbox CAN_Transmit(CAN1, TxMessage); osMutexRelease(can_tx_mutex);设计说明即使裸机系统也建议封装发送函数内部增加邮箱状态轮询CAN_TransmitStatus()避免CAN_Transmit()返回CAN_TxStatus_NoMailBox时无响应。3.6 第六步接收FIFO深度管理防止FIFO溢出丢帧FIFO0深度为3帧若应用层处理速度慢于接收速率新帧会覆盖旧帧。本方案在FIFO0中断服务程序中强制限制单次处理帧数void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; uint8_t frame_count 0; while(CAN_MessagePending(CAN1, CAN_FIFO0) frame_count 2) // 最多取2帧 { CAN_Receive(CAN1, CAN_FIFO0, RxMessage); // 处理RxMessage.Data[0~7], RxMessage.StdId等 frame_count; } }参数依据frame_count 2预留1帧缓冲空间确保下次中断到来时FIFO未满。3.7 第七步环回自测模式验证不接物理总线也能确认驱动功能在正式连接外部节点前必须验证CAN控制器自身功能。启用环回模式CAN_Mode_LoopBack后发送帧直接进入接收FIFO无需外部终端电阻CanHandle.Init.Mode CAN_Mode_LoopBack; // 替换原CAN_Mode_Normal CAN_Init(CAN1, CanHandle.Init); // 发送一帧后立即检查FIFO0是否有响应 CAN_Transmit(CAN1, TxMessage); delay_ms(10); if(CAN_MessagePending(CAN1, CAN_FIFO0)) { CAN_Receive(CAN1, CAN_FIFO0, RxMessage); if(RxMessage.StdId TxMessage.StdId) printf(Loopback OK\n); }血泪经验环回测试通过是后续调试的基石若此步失败90%问题出在时钟配置或GPIO复用而非CAN协议栈。4. 常见问题排查CAN通信不工作的五大高频翻车点与根因定位法4.1 现象编译通过但CAN初始化函数CAN_Init()始终返回CAN_InitStatus_Failed原因CAN_BTR寄存器写入后硬件需检测CAN_MSR_INAK位是否清零若超时未清零则判定初始化失败。常见根因有三APB1时钟未使能RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE)被注释或遗漏CAN引脚复用错误PA11/PA12未配置为GPIO_Mode_AF_PP/GPIO_Mode_IPU或AFIO重映射寄存器AFIO_MAPR被误配置位定时参数越界BRP值过大导致CAN时钟低于1MHz硬件要求≥1MHz或TS1TS21超过总线最大Tq数F103为25。解决用调试器查看RCC-APB1ENR第25位是否为1检查GPIOA-CRL寄存器低16位用公式CAN_BTR (BRP 0) | ((TS1-1) 16) | ((TS2-1) 20) | (SJW 24)反推参数合法性。4.2 现象示波器测得CAN_H/CAN_L波形正常但接收端无中断触发原因FIFO0未分配给过滤器或过滤器配置为拒绝所有帧。F103的过滤器默认关闭且每个Filter Bank必须显式分配到FIFO0或FIFO1。解决检查CAN_FilterInit()中CAN_FilterFIFOAssignment是否设为CAN_Filter_FIFO0用调试器读CAN1-FA1R过滤器激活寄存器确认对应Bank位为1临时将CAN_FilterMaskIdLow设为0x0000全通模式验证。4.3 现象发送成功CAN_TransmitStatus()返回CAN_TxStatus_Ok但接收端收不到帧原因发送帧为扩展帧IDE1但接收端过滤器配置为标准帧模式CAN_FilterMode_IdList且未设置扩展帧掩码或总线终端电阻缺失导致信号反射。解决确认TxMessage.IDE CAN_ID_STD标准帧用万用表测量CAN_H与CAN_L间电阻应为60Ω双节点各120Ω并联若仅单节点测试必须在CAN_H/CAN_L间加120Ω电阻。4.4 现象接收中断频繁触发但CAN_Receive()读出的数据全为0x00原因CanRxMsg结构体未初始化或CAN_Receive()调用前FIFO0已空。F103的CAN_Receive()函数在FIFO空时会返回默认值ID0, DLC0, Data全0。解决在调用CAN_Receive()前必须先调用CAN_MessagePending(CAN1, CAN_FIFO0)判断是否有有效帧声明CanRxMsg RxMessage {0}确保结构体清零。4.5 现象运行一段时间后CAN通信完全停止CAN_GetLastErrorCode()返回CAN_ErrorBusOff原因节点持续发送错误帧如ACK错误、位错误导致错误计数器TEC/REC超过255进入Bus Off状态。常见诱因是总线电平异常CAN_H与CAN_L电压差0.5V或波特率严重不匹配。解决用示波器测量CAN_H-CAN_L差分电压正常应为2.5V±0.5V检查两端节点CAN_BTR寄存器值是否完全一致在CAN1_SCE_IRQHandler()中捕获CAN_ESR_BOFF位执行CAN_SoftwareReset(CAN1)并重新初始化。5. 应用层协议封装与验收标准如何用三帧数据证明你的CAN驱动真正可用5.1 定义最小可行协议三帧交互模型验证闭环能力为避免陷入复杂协议设计本方案定义极简但完备的验证协议帧类型标识符StdId数据长度DLC数据内容Data[0]~Data[7]用途请求帧0x1001[0x01]命令码获取设备ID主机发起查询响应帧0x2004[0x01, 0x02, 0x03, 0x04]设备ID0x01020304从机返回数据心跳帧0x3002[0x00, 0xFF]序列号0校验和0xFF双向周期性保活该模型覆盖标准帧发送/接收、多ID过滤、数据解析且无需外部工具即可人工验证。5.2 驱动验收 checklist五项硬性指标必须全部通过验收不是“能跑就行”而是按工业级标准逐项击穿检查项测试方法合格标准工具时钟稳定性连续发送1000帧请求用逻辑分析仪抓取CAN_H波形任意两帧间隔抖动≤±1 Tq如500kbps下≤1μsSaleae Logic Pro 16过滤器精度向总线发送ID0x101、0x102的干扰帧接收端FIFO0中仅出现ID0x100/0x200/0x300帧无其他IDKeil调试器CAN1-RF0R寄存器中断实时性在USB_LP_CAN1_RX0_IRQHandler入口加GPIO翻转用示波器测高电平宽度从中断触发到GPIO翻转延迟≤3μs72MHz主频下约200周期示波器探头错误恢复能力拔掉一端CAN_L线模拟短路观察CAN1_SCE_IRQHandler是否捕获BOFF1秒内执行软复位并恢复通信心跳帧连续发送无中断串口打印日志内存安全性开启Keil堆栈溢出检测Options → C/C → Misc Controls:--stack_overflow_check运行24小时无HardFault__stack_chk_fail不触发Keil调试器5.3 答辩演示技巧三分钟让导师信服你真的懂CAN答辩不是讲PPT而是现场构建可信证据链第一步展示环回自测日志30秒在串口助手中显示“Loopback Test: Tx ID0x100 → Rx ID0x100 ✅”证明驱动基础功能完好第二步双机交互实时演示90秒主机按下按键发送0x100请求帧从机LED闪烁一次后主机串口立即打印“Device ID: 0x01020304”同时逻辑分析仪波形窗口同步高亮对应帧第三步故障注入验证鲁棒性60秒突然断开从机CAN_L线主机串口打印“BUS OFF DETECTED → RECOVERING”2秒后心跳帧恢复LED重新规律闪烁。注意演示前务必关闭所有无关外设如USART1、SPI避免中断优先级冲突导致CAN中断被屏蔽Keil中勾选“Debug → Settings → Flash Download → Program/Verify”确保代码烧录完整。从那以后我每次交付CAN项目都强制走一遍这五项验收——不是为了应付导师而是因为曾经在产线上亲眼见过一个未做Bus Off恢复的CAN节点导致整条产线PLC通信中断47分钟。希望帮到你。本文还有配套的精品资源点击获取