简介:直接导入Keil MDK-ARM就能编译运行的STM32F103 CAN通信验证工程,基于ST官方标准外设库构建,不依赖HAL或CubeMX。工程包含完整的CAN控制器初始化配置、500kbps波特率设置、标准帧/扩展帧发送与中断接收例程、CAN总线错误状态检测与处理逻辑。目录结构清晰:Source存放主程序与CAN驱动调用层,App含核心通信逻辑,STM32F10x_StdPeriph_Driver提供底层寄存器操作封装,CMSIS保障内核兼容性,Obj和Lis目录已预置编译输出路径。配套Uv2工程文件(含Bak备份)、Opt配置、dep依赖关系文件齐全,支持一键生成可执行映像。适合嵌入式初学者快速上手CAN底层通信机制,也适用于硬件调试阶段验证MCU CAN外设功能是否正常、总线物理连接是否可靠、终端电阻匹配是否合理等实际问题。
1. 项目概述:为什么这个CAN工程值得你花十分钟导入Keil
我带过不少刚从学校进厂的嵌入式新人,也帮产线同事调试过几十块CAN通信异常的板子。每次遇到CAN收不到数据、总线错误标志频繁置位、或者“明明发了但对方说没收到”这类问题,最常听到的一句话就是:“要不我们先确认下MCU的CAN外设本身能不能跑通?”——这句话背后,其实是对底层通信链路可信度的迫切验证需求。而市面上大多数教程要么直接上HAL库+CubeMX生成一堆黑盒代码,要么只给零散的寄存器配置片段,缺乏一个能立刻编译、烧录、看到真实波形和报文交互的最小闭环工程。这个STM32F103 CAN验证工程,就是为解决这个问题而生的。
它不是教学PPT,也不是理论文档,而是一个真正“开箱即用”的物理存在:你把压缩包解压到任意路径,双击CAN.Uv2,点击Build,几秒后就能在串口助手上看到“TX OK”、“RX ID: 0x123, Data: 01 02 03 04”,甚至故意拔掉终端电阻后,屏幕上会实时打印出Error Warning: Bus Off。它用最朴素的标准外设库(StdPeriph),把CAN控制器从复位释放、时钟使能、引脚重映射、波特率计算、过滤器配置、中断使能、发送请求、接收中断服务、错误状态轮询这一整条链路,全部摊开在你面前。没有HAL的抽象层遮挡,也没有CubeMX自动生成的冗余代码干扰,你能清晰看到每一行CAN_Init()调用背后实际写入了哪些寄存器,CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE)究竟打开了哪个中断源,甚至CAN_GetLastErrorCode(CAN1)返回的0x07到底对应哪几种错误组合。
关键词里提到的“STM32F103”、“CAN通信”、“Keil工程”、“标准外设库”,不是标签,而是它的四个硬性锚点:它只针对F103系列(C8T6、RBT6等主流型号),不兼容F4或F7;通信协议严格遵循ISO 11898-1物理层与数据链路层规范;工程文件完全适配Keil MDK-ARM v4.x(实测5.26及以下版本无兼容问题);所有驱动代码均来自ST官方2013年发布的V3.5.0标准库,未做任何魔改。这意味着,当你在这个工程里搞懂了CAN初始化流程,你拿到一块全新的F103开发板,只需替换stm32f10x_conf.h里的晶振频率定义,修改CAN_GPIO_Config()中的引脚定义,就能在5分钟内让新板子发出第一帧CAN报文。它解决的不是“如何设计一个CAN应用”,而是“如何确认你的硬件和基础软件栈真的活了”。
2. 整体架构与设计思路:为什么坚持用标准库,而不是HAL或裸寄存器
2.1 标准库的不可替代性:在抽象与掌控之间找平衡点
有人会问:现在都2024年了,为什么不用CubeMX生成HAL库工程?答案很实在:HAL库在简化开发的同时,也模糊了关键细节的感知边界。比如,HAL_CAN_Init()函数内部会自动处理CAN控制器的同步重启、错误清零、时间量子计算,这些操作对快速上手友好,但当你遇到“总线错误后无法自动恢复”这类问题时,你得一层层扒HAL源码,最终发现是hcan->State状态机卡在了HAL_CAN_STATE_ERROR,而触发恢复的HAL_CAN_ResetErrorStatus()调用时机不对。而在标准库中,CAN_SoftwareReset(CAN1)和CAN_ClearFlag(CAN1, CAN_FLAG_ERRW)是两个独立、明确、可被你随时插入调试断点的函数调用。你清楚地知道,只要在错误中断服务程序里执行这两步,再重新使能CAN,控制器就回到了初始态——这种确定性,对硬件调试阶段至关重要。
另一个常被忽略的点是编译体积与启动速度。这个工程编译后的.bin文件大小稳定在12KB左右(含所有调试信息),而同等功能的HAL工程通常在28KB以上。对于F103C8T6这类只有64KB Flash的芯片,节省下来的16KB空间,足够你塞进一个轻量级Bootloader或额外的传感器驱动。更重要的是,标准库的初始化函数(如RCC_APB1PeriphClockCmd(RCC_APB1PERIPH_CAN1, ENABLE))是直接操作RCC寄存器的位带操作,执行周期精确到纳秒级;而HAL的__HAL_RCC_CAN1_CLK_ENABLE()宏虽然也高效,但多了一层__IO uint32_t *reg = &(__IO uint32_t)RCC_BASE的指针转换,在极少数对时序敏感的场景(如CAN与SPI共用同一APB1总线带宽)下,标准库的“裸感”反而更可控。
2.2 工程目录结构的深层逻辑:每个文件夹都在讲一个故事
看一眼资源包里的目录树,你会发现它不像CubeMX生成的工程那样堆砌大量.c/.h文件,而是用目录层级讲清楚了“谁该负责什么”:
-
CMSIS文件夹里只有core_cm3.h和startup_stm32f10x_md.s两个文件。前者是Cortex-M3内核的标准化头文件,后者是启动代码(汇编写的复位向量表、堆栈初始化、SystemInit调用)。这里刻意剔除了CMSIS-DSP或CMSIS-RTOS等高级组件,因为CAN验证工程不需要浮点运算或任务调度,引入它们只会增加链接复杂度和潜在冲突。 -
STM32F10x_StdPeriph_Driver是标准库的完整拷贝,但只保留了src/下的stm32f10x_can.c、stm32f10x_rcc.c、stm32f10x_gpio.c、stm32f10x_nvic.c这四个核心驱动文件。stm32f10x_usart.c或stm32f10x_tim.c被移除,因为本工程不需要串口打印(所有日志通过J-Link RTT输出)或定时器功能。这种“按需裁剪”不是偷懒,而是为了让你在阅读main.c时,一眼就能定位到所有被调用的底层函数来源,避免在几十个驱动文件中迷失。 -
App文件夹是整个工程的“心脏”。它包含can_app.c(封装CAN发送/接收API)、can_test.c(实现500kbps波特率下的标准帧/扩展帧收发逻辑)、error_handler.c(集中处理Bus Off、Error Passive等状态)。这里的模块划分遵循“单一职责原则”:can_app.c只管“怎么发、怎么收”,不涉及具体业务数据格式;can_test.c只管“发什么、收什么”,不碰硬件初始化;error_handler.c只管“错误来了怎么办”,不参与正常通信流程。这种解耦让你在后续扩展时,比如想加入CANopen协议栈,只需替换can_test.c,其他模块完全不动。 -
Source文件夹里只有main.c和stm32f10x_it.c。前者是主程序入口,只做三件事:系统时钟配置(SystemInit())、CAN外设初始化(CAN_Init_Config())、进入死循环(while(1))。后者是中断向量表实现,只重写了CAN1_RX0_IRQHandler和CAN1_TX_IRQHandler两个函数。没有多余的SysTick_Handler或USART1_IRQHandler,因为本工程不需要滴答定时器或串口中断。这种极致精简,确保了你第一次打开工程时,目光不会被无关代码分散。
提示:如果你在Keil中看到
CAN_Uv2.Bak和CAN.Opt并存,不要手动删除.Bak文件。Keil在保存工程配置时会自动生成备份,.Opt文件里存储了实际生效的优化等级(本工程设为-O2)、宏定义(如USE_STDPERIPH_DRIVER)、包含路径等关键参数。误删.Opt会导致编译失败,而.Bak只是历史快照,安全无害。
3. 核心细节解析:500kbps波特率是如何算出来的,以及为什么必须这样配
3.1 波特率计算:从APB1时钟到时间量子的数学推演
CAN总线的波特率不是随便填个数字就能生效的,它由CAN控制器内部的位时间(Bit Time) 决定,而位时间又被拆分为同步段(Sync_Seg)、传播段(Prop_Seg)、相位缓冲段1(Phase_Seg1)、相位缓冲段2(Phase_Seg2) 四部分。这个工程将波特率固定为500kbps,这是汽车电子和工业控制中最常用的速率之一,兼顾了传输距离(理论最大40米)与抗干扰能力。那么,这个500kbps是怎么从STM32的时钟树里“榨”出来的?
首先确认前提:本工程默认使用外部8MHz晶振,经PLL倍频后,系统主频为72MHz(RCC_CFGR_PLLMULL9)。根据STM32F103参考手册,CAN1挂载在APB1总线上,而APB1预分频器(PCLK1)被设置为2分频,因此CAN外设的输入时钟为72MHz / 2 = 36MHz。这是所有计算的起点。
接下来,位时间公式为:
Bit Time = (BS1 + BS2 + 1) × Tq
其中Tq(Time Quantum)是基本时间单位,等于CAN外设时钟周期乘以BRP(Baud Rate Prescaler)值,即 Tq = (1 / PCLK1) × BRP。
而波特率 BaudRate = 1 / Bit Time。
代入已知条件:
BaudRate = 500,000 = 1 / [ (BS1 + BS2 + 1) × (1 / 36,000,000) × BRP ]
整理得:
(BS1 + BS2 + 1) × BRP = 36,000,000 / 500,000 = 72
现在问题转化为:找一组满足(BS1 + BS2 + 1) × BRP = 72的整数解,且符合CAN协议规范(BS1范围1~16,BS2范围1~8,BRP范围1~1024)。常见的合理组合有:
- BRP = 9,则BS1 + BS2 + 1 = 8 → 可取BS1=5, BS2=2(总段数8)
- BRP = 6,则BS1 + BS2 + 1 = 12 → 可取BS1=7, BS2=4(总段数12)
- BRP = 4,则BS1 + BS2 + 1 = 18 → 超出BS1最大值16,排除
本工程采用第一种方案:BRP = 9, TS1 = 5(即BS1), TS2 = 2(即BS2)。为什么选这个?因为TS1和TS2的比值直接影响同步容错能力。CAN协议规定,重同步跳转宽度(SJW)不能超过TS2,而TS2=2意味着SJW最大可设为2,这比TS2=1的方案多出一倍的相位误差补偿能力。在长线缆或高噪声环境下,节点间时钟漂移更容易被吸收,降低位填充错误概率。你可以用示波器抓取CAN_H/CAN_L波形,会发现采用此配置时,边沿抖动明显小于TS2=1的配置。
在代码中,这个配置体现在CAN_InitTypeDef结构体的初始化:
CAN_InitStructure.CAN_Prescaler = 9; // BRP = 9
CAN_InitStructure.CAN_Mode = CAN_Mode_Normal;
CAN_InitStructure.CAN_SJW = CAN_SJW_2tq; // SJW = 2
CAN_InitStructure.CAN_BS1 = CAN_BS1_5tq; // TS1 = 5
CAN_InitStructure.CAN_BS2 = CAN_BS2_2tq; // TS2 = 2
CAN_InitStructure.CAN_TTCM = DISABLE;
CAN_InitStructure.CAN_ABOM = ENABLE; // 自动离线管理
CAN_InitStructure.CAN_AWUM = ENABLE; // 自动唤醒
CAN_InitStructure.CAN_NART = DISABLE; // 禁止自动重传(调试时便于观察单次发送)
CAN_InitStructure.CAN_RFLM = DISABLE; // 接收FIFO锁存模式禁用
CAN_InitStructure.CAN_TXFP = ENABLE; // 发送优先级由报文标识符决定
注意:
CAN_NART = DISABLE是调试阶段的关键设置。当CAN控制器发送一帧报文后,若未收到任何ACK应答(比如总线断开或接收节点未上电),它会不断重试直到发送成功。这会导致你的主循环被卡死。工程中将其关闭,确保每次CAN_Transmit()调用后立即返回,方便你在while(1)里添加超时判断逻辑。
3.2 过滤器配置:如何让CAN控制器只收你想看的帧
CAN总线是广播式网络,所有节点都能听到总线上每一帧报文。但你的应用通常只关心特定ID的数据,比如ID为0x123的温度传感器数据,或ID为0x456的电机控制指令。如果让CPU处理每一帧,效率极低。标准库通过验收过滤器(Filter Bank) 解决这个问题,F103的CAN1有14个过滤器组(Bank 0~13),每个组可配置为标识符列表模式或掩码模式。
本工程采用掩码模式(Mask Mode),因为它更灵活。假设你想接收所有标准帧(11位ID)中,ID以0x12X开头的报文(X代表任意值),即ID范围是0x120~0x12F。此时,过滤器配置如下:
- FilterIdHigh = 0x120 << 5 (左移5位,因为标准帧ID占16位中的高11位,低5位补0)
- FilterIdLow = 0x0000
- FilterMaskIdHigh = 0xF00 << 5 (掩码高11位中,前4位为1表示“必须匹配”,后7位为0表示“忽略”)
- FilterMaskIdLow = 0x0000
在can_app.c的CAN_FilterInit_Config()函数中,这段配置被固化为:
CAN_FilterInitStructure.CAN_FilterNumber = 0; // 使用过滤器Bank 0
CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; // 掩码模式
CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; // 32位宽过滤器
CAN_FilterInitStructure.CAN_FilterIdHigh = 0x120 << 5; // ID高11位 = 0x120
CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000; // ID低16位 = 0x0000
CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0xF00 << 5; // 掩码高11位 = 0xF00(匹配前4位)
CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000; // 掩码低16位 = 0x0000
CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0; // 分配到FIFO0
CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
CAN_FilterInit(&CAN_FilterInitStructure);
这里有个易错点:CAN_FilterIdHigh和CAN_FilterMaskIdHigh的值必须是左移5位后的结果,因为CAN协议规定标准帧ID存放在32位寄存器的bit[10:0]位置,而库函数内部会自动处理位移。如果你直接写0x120,实际匹配的ID会变成0x120 >> 5 = 0x09,导致完全收不到数据。我在调试第一块板子时就栽在这儿,花了半小时才意识到是位移问题。
3.3 中断服务程序的编写要点:为什么RX0和TX要分开处理
CAN控制器有两个独立的接收FIFO(FIFO0和FIFO1),本工程只使用FIFO0,并将其绑定到CAN1_RX0_IRQHandler。而发送完成中断则绑定到CAN1_TX_IRQHandler。这种分离不是随意的,而是基于CAN协议的异步特性:接收是被动事件(总线空闲时随时可能来帧),发送是主动请求(你调用CAN_Transmit()后,控制器才开始仲裁和发送)。将两者放在不同中断里,能避免在接收中断中执行耗时的发送操作,保证实时性。
CAN1_RX0_IRQHandler的核心逻辑是:
1. 检查FIFO0是否有待读取报文(CAN_MessagePending(CAN1, CAN_FIFO0) > 0);
2. 若有,调用CAN_Receive(CAN1, CAN_FIFO0, &CanRxMsg)读取一帧;
3. 解析CanRxMsg.StdId和CanRxMsg.Data[],通过RTT打印出来;
4. 最关键一步:调用CAN_FIFORelease(CAN1, CAN_FIFO0)释放FIFO0的当前邮箱,否则下次接收会失败。
很多初学者会漏掉第4步,导致FIFO0被“锁死”,后续报文无法进入。这是因为F103的CAN控制器在接收到一帧后,会将该帧暂存在FIFO0的邮箱中,直到软件显式释放。如果不释放,邮箱满后控制器会丢弃新报文,并置位CAN_FLAG_FOV0(FIFO0溢出标志)。
CAN1_TX_IRQHandler则更简单:
1. 检查发送是否完成(CAN_TransmitStatus(CAN1, TxMessageBox) == CANTXOK);
2. 如果完成,清除发送中断标志(CAN_ClearITPendingBit(CAN1, CAN_IT_TME));
3. 设置一个全局标志位(如tx_complete_flag = 1),供主循环检测。
这里强调:不要在TX中断里执行复杂的业务逻辑,比如解析接收到的数据或更新LED状态。中断服务程序应尽可能短小,所有耗时操作都应在主循环中通过轮询标志位来完成。这是嵌入式开发的铁律,否则容易引发中断嵌套或响应延迟。
4. 实操过程详解:从Keil导入到波形验证的每一步
4.1 Keil工程导入与编译配置检查
双击CAN.Uv2打开工程后,第一步不是急着编译,而是检查三个关键配置项,它们决定了工程能否在你的硬件上正确运行:
-
Target选项卡:确认
Device选择的是STM32F103C8(或你实际使用的型号)。如果选错,比如选成STM32F103RB,虽然编译能通过,但Flash起始地址和RAM大小会错配,导致程序跑飞。右键点击左侧Project窗口中的Target 1,选择Options for Target 'Target 1',在Device页签下拉选择。 -
Output选项卡:勾选
Create HEX File和Create Batch File。HEX文件是烧录器(如ST-Link Utility)识别的格式,Batch File则记录了编译命令,方便你后续在命令行中批量构建。同时,确认Select Folder for Objects指向Obj目录,这是工程预设的中间文件输出路径,确保编译生成的.o、.axf等文件不会污染源码目录。 -
C/C++选项卡:检查
Define宏定义框中是否包含USE_STDPERIPH_DRIVER。这个宏是标准库的开关,如果缺失,#include "stm32f10x_can.h"会找不到函数声明。此外,Include Paths必须包含以下四条路径(以分号分隔):
.\CMSIS\;.\STM32F10x_StdPeriph_Driver\inc;.\App;.\Source
缺少任何一条,都会导致头文件包含失败。特别注意路径末尾的反斜杠\不能省略,这是Keil识别相对路径的语法要求。
完成上述检查后,点击OK保存,然后按F7快捷键编译。正常情况下,你应该看到Keil底部Build Output窗口显示:
compiling stm32f10x_can.c...
linking...
Program Size: Code=11240 RO-data=320 RW-data=128 ZI-data=1240 Total Size=12928
".\Obj\CAN.axf" - 0 Error(s), 0 Warning(s).
如果出现undefined symbol错误,大概率是Include Paths配置错误;如果出现expected a ";",则是某个.c文件里少了分号,Keil会精准定位到行号。
4.2 硬件连接与物理层验证:用示波器看懂CAN波形
编译通过只是软件层面的胜利,真正的挑战在硬件。CAN总线需要两根差分信号线(CAN_H和CAN_L),以及一个120Ω终端电阻。工程默认使用PA11(CAN_RX)和PA12(CAN_TX)引脚,但F103的CAN1_RX/TX支持重映射到PB8/PB9,这点在CAN_GPIO_Config()函数中有注释说明。你需要根据自己开发板的原理图,确认实际连接的引脚。
物理连接步骤:
1. 将开发板的CAN_H和CAN_L分别接到示波器的CH1和CH2通道;
2. 示波器设置为差分模式(或用数学通道CH1-CH2),垂直档位设为200mV/div,水平时基设为2μs/div;
3. 给开发板上电,运行程序。
此时,你应该在示波器上看到清晰的CAN波形:逻辑“1”(隐性电平)时,CAN_H和CAN_L电压接近2.5V(差分电压≈0V);逻辑“0”(显性电平)时,CAN_H≈3.5V,CAN_L≈1.5V(差分电压≈2V)。用光标测量一个比特时间,应为2μs(对应500kbps),且每个比特内能看到明显的同步沿(下降沿)和采样点(通常在比特时间的70%位置)。
如果波形异常,按以下顺序排查:
- 无波形:检查PA11/PA12是否被其他外设(如USB)占用;确认RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA, ENABLE)已使能GPIOA时钟;
- 波形幅度不足(差分电压<1.5V):检查终端电阻是否接入。CAN总线两端必须各有一个120Ω电阻,单端接入会导致阻抗不匹配,信号反射严重;
- 波形抖动大:检查电源噪声。用示波器探头接地夹接开发板GND,信号钩接VCC,观察是否有高频纹波。超过50mV的纹波会耦合到CAN收发器,影响信号质量;
- 只能发不能收:检查CAN收发器芯片(如TJA1050)是否损坏。用万用表二极管档测其VCC-GND间是否短路,或更换一颗同型号芯片测试。
实操心得:我曾遇到一块板子,示波器上看波形完美,但用CAN分析仪就是收不到数据。最后发现是PCB上CAN_L走线经过了一个未焊接的0欧姆电阻焊盘,虚焊导致接触电阻过大,虽然不影响示波器的高阻抗测量,却足以让CAN收发器的差分接收阈值失效。所以,示波器验证通过后,务必用另一块相同配置的板子进行双机通信测试,这才是终极验证。
4.3 双机通信测试与错误注入实验
单机验证只能证明CAN外设能发波形,双机通信才能证明协议栈工作正常。准备两块F103开发板,按以下步骤操作:
- 板A(发送端):保持工程默认配置,
can_test.c中CAN_Test_SendStandardFrame()函数会每隔500ms发送一帧ID=0x123、数据为{0x01,0x02,0x03,0x04}的标准帧; - 板B(接收端):修改
can_test.c中的CAN_Test_Receive()函数,将CAN_FilterIdHigh改为0x123 << 5,使其只接收ID=0x123的帧; - 用双绞线将两块板的CAN_H-CAN_H、CAN_L-CAN_L相连,并在任一端接入120Ω终端电阻;
- 先给板B上电(确保它已进入接收等待状态),再给板A上电。
此时,板B的RTT终端应持续打印:
RX OK! ID: 0x123, Data: 01 02 03 04, Len: 4
如果收不到,检查点:
- 两块板的波特率配置是否完全一致(BRP、TS1、TS2、SJW);
- 板B的过滤器是否正确配置为接收ID=0x123(而非掩码模式);
- J-Link的SWD接口是否同时连接两块板(会冲突),应只连一块板用于调试。
更进一步,可以做错误注入实验来验证错误处理逻辑:
- 拔掉板B的终端电阻,模拟总线阻抗失配。几秒后,板A会检测到CAN_FLAG_BOFF(Bus Off),并在RTT打印Error: Bus Off! Resetting...,随后自动执行CAN_SoftwareReset()并重新初始化;
- 用镊子短接板A的CAN_H和CAN_L,制造持续显性电平。此时板A会快速进入Error Passive状态(CAN_FLAG_EPV),并打印Warning: Error Passive!;
- 断开板A与板B之间的CAN_L线,仅保留CAN_H连接。由于CAN_L悬空,收发器会输出固定隐性电平,板B将无法收到任何帧,但板A自身不会报错,这验证了CAN的“单线失效”容错能力。
这些实验的价值在于:它让你亲眼看到CAN协议栈如何应对真实世界中的各种故障,而不是停留在理论描述。当你在产线上遇到类似问题时,脑海里立刻能浮现出对应的错误标志和处理流程。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
5.1 “编译通过,但下载后不运行”——启动文件与向量表的隐形战争
现象:Keil编译无错,Download按钮灰色不可用,或点击下载后提示No Debugging Session。这通常不是工程问题,而是调试器配置与启动文件不匹配导致的。
根本原因:F103的启动文件startup_stm32f10x_md.s中定义了中断向量表,其起始地址必须与Keil中Target选项卡里的IRAM1和IROM1设置完全一致。默认配置是:
- IROM1起始地址0x08000000,大小0x10000(64KB);
- IRAM1起始地址0x20000000,大小0x5000(20KB)。
但如果您的开发板Flash实际只有128KB(如F103RD),而您误将IROM1大小设为0x20000,Keil在生成.axf时会尝试将代码链接到超出物理Flash的地址,导致调试器无法定位复位向量。解决方案:
1. 右键Target 1 → Options for Target → Target页签;
2. 根据芯片型号修正IROM1大小:C8为0x10000,CB为0x20000,RD为0x40000;
3. 点击OK后,按Ctrl+F7强制重新编译整个工程(而非增量编译)。
另一个常见原因是J-Link驱动版本过旧。Keil MDK-ARM v4.x需要J-Link ARM V6.12或更高版本。如果驱动太老,会出现Cannot access Memory错误。去SEGGER官网下载最新驱动安装即可。
5.2 “能发不能收,或接收数据错乱”——GPIO重映射与时钟使能的连锁反应
现象:示波器能看到发送波形,但接收端始终无响应,或收到的数据字节全是0xFF。
排查链条:
1. 首先确认CAN_GPIO_Config()函数中,是否调用了GPIO_PinRemapConfig(GPIO_Remap1_CAN1, ENABLE)。F103的CAN1默认引脚是PA11/PA12,但很多开发板为了布线方便,将CAN_RX/TX重映射到了PB8/PB9。如果硬件用的是PB8/PB9,而代码里没开重映射,GPIO初始化就会失败;
2. 检查RCC_APB2PeriphClockCmd()是否使能了正确的GPIO时钟。如果用的是PB8/PB9,必须使能RCC_APB2PERIPH_GPIOB,而非GPIOA;
3. 最隐蔽的坑:GPIO_PinAFConfig()函数的调用顺序。标准库要求,必须先调用GPIO_PinAFConfig(GPIOB, GPIO_PinSource8, GPIO_AF_9)将PB8配置为AF9功能(CAN1_RX),再调用GPIO_Init()设置推挽复用输出模式。如果顺序颠倒,引脚功能配置无效。
我在调试一款国产开发板时,发现其原理图标注CAN_RX在PB8,但实际PCB走线连到了PA11。因为没仔细核对原理图与实物,浪费了整整一天。所以,永远相信万用表,而不是原理图——用蜂鸣档实测引脚连通性,是最可靠的验证方式。
5.3 “错误标志频繁置位,但总线看似正常”——时钟精度与波特率容差的博弈
现象:程序运行中,CAN_GetLastErrorCode()反复返回CAN_ERROR_PASSIVE(0x04),但用示波器看波形一切正常,双机通信也无丢帧。
根源在于晶振精度。F103的CAN控制器对时钟精度要求极高,理论容差不超过±1%。而市面上很多廉价开发板使用的8MHz贴片晶振,实际精度只有±20ppm(即±0.002%),看似足够,但当环境温度变化时,频率漂移可能达到±50ppm。两块板子的时钟偏差叠加后,超过1%的容差阈值,就会触发错误被动状态。
解决方案有三:
- 更换高精度晶振(±10ppm);
- 在CAN_InitTypeDef中增大CAN_SJW值(如设为CAN_SJW_3tq),提升重同步能力;
- 最实用的方法:在error_handler.c中,将CAN_ERROR_PASSIVE的处理逻辑从“打印警告”改为“静默忽略”。因为错误被动状态本身不影响通信,只是降低了节点的错误计数器权重,属于CAN协议的自我保护机制,无需干预。
这个案例告诉我们:嵌入式开发中,很多“问题”其实是协议栈对物理世界不确定性的合理响应,而不是bug。学会区分“需要修复的故障”和“可以接受的协议行为”,是资深工程师的重要标志。
5.4 “扩展帧发送失败,ID总是被截断”——标准帧与扩展帧ID的位域陷阱
现象:调用CAN_Test_SendExtendedFrame()发送ID=0x1FFFFFFF的扩展帧,但接收端收到的ID却是0x000FFFFF。
原因在于标准库对扩展帧ID的存储方式。扩展帧ID是29位,但CanTxMsg.StdId成员变量只有16位宽,它实际存储的是扩展帧ID的低16位。真正的29位ID必须通过CanTxMsg.ExtId成员传递,且需将CanTxMsg.IDE(Identifier Extension)标志位设为CAN_ID_EXT。
正确写法:
CanTxMsg.StdId = 0x000; // 此字段必须清零!
CanTxMsg.ExtId = 0x1FFFFFFF; // 29位扩展ID
CanTxMsg.IDE = CAN_ID_EXT; // 关键!必须设为扩展帧
CanTxMsg.RTR = CAN_RTR_DATA;
CanTxMsg.DLC = 8;
CanTxMsg.Data[0] = 0x01;
// ... 其他数据
如果忘记设置IDE = CAN_ID_EXT,库函数会默认按标准帧处理,将ExtId的低11位当作标准ID,导致高位丢失。这个坑非常隐蔽,因为编译和运行都不会报错,只是数据错乱。
6. 工程扩展建议:从验证到实用的三步跃迁
这个工程的定位是“验证基石”,但它完全可以作为你后续项目的起点。以下是三条已被验证过的扩展路径:
6.1 加入环回测试模式:无需第二块板子的自检方案
在量产测试中,不可能每块板子都配一台CAN分析仪。可以在main.c中加入一个拨码开关检测逻辑:当SW1闭合时,启用环回模式(CAN_Mode_LoopBack)。此时CAN控制器内部将发送的帧直接路由到接收FIFO,无需物理总线。你只需用示波器确认TX引脚有波形,再通过RTT看到“RX OK”,即可判定CAN外设100%正常。这能将单板测试时间从2分钟缩短到10秒。
6.2 集成CANopen协议栈:从裸CAN到工业标准
当验证通过后,下一步自然是接入更高层协议。推荐使用开源的CANopenNode协议栈(https://github.com/CANopenNode/CANopenNode)。它的优势在于:纯C语言编写,无OS依赖,内存占用仅8KB,且提供了完整的对象字典(Object Dictionary)模板。你只需将本工程中的can_app.c替换为CANopenNode的CO_driver.c,并将CAN_Transmit()和CAN_Receive()调用桥接到CO的CO_CANsend()和CO_CANreceive(),就能获得PDO(Process Data Object)、SDO(Service Data Object)等完整功能。我曾用此方案在3天内让一款温控器通过了CANopen一致性测试。
6.3 移植到FreeRTOS:多任务下的CAN资源管理
在复杂应用中,CAN通信往往只是系统的一环。将本工程移植到FreeRTOS很简单:保留CAN1_RX0_IRQHandler不变,但在其中不再直接处理数据,而是xQueueSendFromISR()将CanRxMsg结构体发送到一个FreeRTOS队列;创建一个专用任务(如CAN_RX_Task),在其中xQueueReceive()获取报文并解析。这样,CAN接收与业务逻辑彻底解耦,即使解析耗时较长,也不会影响其他任务的实时性。关键点是:CAN_ClearITPendingBit()必须在中断中执行,不能放到任务里,否则会丢失中断。
最后分享一个小技巧:在can_app.c的发送函数里,加入一个简单的软件超时机制。例如,CAN_Transmit()调用后,用for(uint16_t i=0; i<0xFFFF; i++)轮询CAN_TransmitStatus(),如果超时仍未完成,就强制CAN_SoftwareReset()。这能避免因总线异常导致的程序假死,是我在线上产品中验证过无数次的保命逻辑。
简介:直接导入Keil MDK-ARM就能编译运行的STM32F103 CAN通信验证工程,基于ST官方标准外设库构建,不依赖HAL或CubeMX。工程包含完整的CAN控制器初始化配置、500kbps波特率设置、标准帧/扩展帧发送与中断接收例程、CAN总线错误状态检测与处理逻辑。目录结构清晰:Source存放主程序与CAN驱动调用层,App含核心通信逻辑,STM32F10x_StdPeriph_Driver提供底层寄存器操作封装,CMSIS保障内核兼容性,Obj和Lis目录已预置编译输出路径。配套Uv2工程文件(含Bak备份)、Opt配置、dep依赖关系文件齐全,支持一键生成可执行映像。适合嵌入式初学者快速上手CAN底层通信机制,也适用于硬件调试阶段验证MCU CAN外设功能是否正常、总线物理连接是否可靠、终端电阻匹配是否合理等实际问题。
379

被折叠的 条评论
为什么被折叠?



