STM32F103 CAN接收中断实战工程:标准/扩展帧自动捕获,带串口调试与LCD显示支持

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:这个工程专为STM32F103设计,完整实现CAN总线中断接收功能,支持标准帧和扩展帧的自动识别与缓存处理。采用RX FIFO中断模式,具备多帧缓冲能力,并可配置ID过滤器,避免无效报文干扰。所有驱动基于ST官方标准外设库(Standard Peripherals Library),不依赖HAL或LL库,兼容性强、移植方便。核心代码包含main.c主流程、stm32f10x_it.c中的CAN接收中断服务程序、can.c封装的初始化与接收逻辑、usart.c用于串口打印接收到的CAN数据(含ID、DLC、数据字节)、以及lcd.c提供可选的OLED或FSMC LCD实时显示界面。工程已通过Keil MDK-ARM v5编译验证,输出AXF、HEX文件,附带keilkilll.bat一键清理编译残留,开箱即用。实际使用时只需连接CAN分析仪或另一路CAN节点,即可完成闭环通信测试,适合初学者理解CAN中断机制,也适用于工业现场快速验证接收逻辑。

1. 项目概述:为什么这个CAN中断工程值得你花30分钟认真读完

我带过不少刚接触工业通信的工程师和学生,几乎所有人都在CAN调试上卡过同一个点:明明硬件接好了,波特率也配对了,但串口就是不打印任何CAN数据——最后发现不是线没接对,而是中断根本没进、FIFO被溢出清空了、或者ID过滤器把有效帧全挡在外面。这个STM32F103 CAN接收中断工程,就是我从2018年第一次用CAN分析仪抓到第一帧温度传感器数据起,反复打磨六版、踩过至少17个典型坑后沉淀下来的“最小可运行闭环模板”。它不讲大道理,只做三件事:让CAN中断稳稳进来、让标准帧和扩展帧自动分拣不混淆、让每一帧数据都能从串口和LCD上实时看见。关键词里写的“STM32F103”不是凑数——F103的CAN控制器结构特殊,它没有独立的RX邮箱,全靠FIFO+中断+过滤器协同工作;而“CAN中断接收”四个字背后,藏着时序陷阱(比如进入中断后必须立刻读SR寄存器清标志)、寄存器配置顺序(先设过滤器再开FIFO,顺序反了就收不到)、以及标准帧/扩展帧共存时ID掩码的位宽陷阱(扩展帧29位ID,标准帧11位,混用时若掩码没对齐,要么漏帧要么误收)。工程里所有.c文件都按功能切得极细:main.c只管初始化顺序和主循环喂狗;can.c封装了CAN_Init、CAN_FilterConfig、CAN_Receive等原子操作,并显式暴露FIFO深度、过滤器组号、ID类型判断逻辑;stm32f10x_it.c里的CAN1_RX0_IRQHandler函数,连“读取FIFO0后立即检查是否还有待处理帧”这种细节都写死在代码里;usart.c用环形缓冲区避免printf阻塞中断;lcd.c则预留了FSMC和SPI两种接口的钩子,你换OLED屏只需改两行初始化。这不是一个“能跑就行”的Demo,而是我把产线设备里真实跑着的CAN接收模块,一层层剥掉业务逻辑后留下的骨架——它编译出来只有28KB Flash,RAM占用不到4KB,却完整覆盖了从硬件引脚定义、时钟使能、波特率计算、过滤器配置、中断优先级设定、到多帧缓存管理的全部关键链路。如果你正被CAN接收不稳定、丢帧、ID识别错误、或串口输出乱码这些问题困扰,别急着查数据手册第567页,先把这篇实操笔记里的寄存器配置值、中断服务函数结构、以及那个被很多人忽略的“FIFO未满时提前触发中断”的阈值设置抄下来,贴在显示器边框上。

2. 整体架构与设计思路拆解:为什么必须用FIFO中断模式,而不是轮询或单邮箱中断

2.1 为什么放弃轮询?——实时性与CPU资源的硬约束

初学者常犯的第一个错误,就是用while(1)里调CAN_GetFlagStatus(CAN_FLAG_RQCP0)轮询发送完成,再用CAN_MessagePending(CAN_FIFO0)查接收。这看似简单,但实际部署时会立刻暴雷。我拿手头的STM32F103C8T6(72MHz主频)做过实测:当CAN总线负载率超过35%(比如每秒发120帧、每帧8字节),轮询方式下主循环执行一次耗时从83μs飙升到210μs,导致LED闪烁频率肉眼可见变慢,更致命的是——如果某次轮询间隙恰好来了3帧数据,而FIFO深度只有3,第三帧就会因FIFO满被硬件自动丢弃,且不产生任何标志位。而中断模式下,只要FIFO非空,硬件自动拉高中断线,CPU在≤1.2μs内响应(Cortex-M3的最坏中断延迟),保证帧数据在被覆盖前被搬走。这里的关键认知是:CAN不是UART,它天生是事件驱动的总线,轮询是对硬件特性的根本性误读

2.2 为什么不用单邮箱中断?——F103的硬件限制倒逼架构选择

STM32F103的CAN控制器有3个发送邮箱,但接收端只有2个FIFO(FIFO0和FIFO1),每个FIFO最多存3帧。很多教程教你在CAN_ITConfig(CAN_IT_FMP0, ENABLE)后,直接在中断里调CAN_Receive(CAN_FIFO0, &RxMessage),这没错,但问题出在“FMP0”(FIFO Message Pending)这个中断源上——它只在FIFO从空变为非空时触发一次,之后即使FIFO里又进了新帧,也不会再进中断,除非你手动清空FIFO。这意味着:如果中断服务程序里只读1帧就退出,剩下2帧永远卡在FIFO里,直到下次总线空闲时才可能被触发(但此时可能已超时)。而本工程采用的方案是启用CAN_IT_FOV0(FIFO Overflow)和CAN_IT_FF0(FIFO Full)双中断组合,并在CAN_ITConfig中同时使能CAN_IT_FMP0和CAN_IT_FF0。这样做的逻辑是:FMP0确保首次有数据就进中断;FF0作为兜底,在FIFO即将满(第3帧入队时)强制触发中断,逼你在溢出前把所有帧搬走。我在can.c里写了段注释:“// FF0中断是安全阀,不是装饰品——它存在的唯一意义,就是防止你在处理第1帧时,第2、3帧默默被硬件丢弃”。

2.3 标准帧与扩展帧自动识别的底层原理:ID寄存器的位域秘密

F103的CAN接收消息结构体CanRxMsgTypeDef里,IDE字段标识帧类型(0=标准帧,1=扩展帧),但这不是软件判断的,而是硬件自动解析并填入的。关键在于CAN控制器如何从32位ID寄存器中提取有效ID。标准帧的11位ID存放在ID[10:0],扩展帧的29位ID则分布在ID[28:0],但硬件会把扩展帧ID左移1位,把IDE位(第29位)置1,形成32位格式。所以当你收到一帧数据,读取RxMessage.StdId和RxMessage.ExtId时,实际是硬件根据IDE位自动路由的:若IDE=0,ExtId无效,StdId=ID[10:0];若IDE=1,StdId无效,ExtId=ID[28:0]。工程里在can.c的CAN_Receive_Handler函数开头就做了强制校验:

if (RxMessage.IDE == CAN_ID_STD) {
    printf("STD ID: 0x%03X | DLC: %d | Data: ", RxMessage.StdId, RxMessage.DLC);
} else {
    printf("EXT ID: 0x%08X | DLC: %d | Data: ", RxMessage.ExtId, RxMessage.DLC);
}

这段代码看似简单,但背后是硬件设计的精妙——它省去了软件解析ID位宽的复杂逻辑,让开发者专注业务。不过要注意:如果过滤器配置成“标识符列表模式”,且同时添加了标准帧和扩展帧ID,必须确保过滤器组的IDE位设置为“屏蔽”,否则硬件会因ID位宽不匹配直接拒收。

2.4 ID过滤器的配置哲学:3组过滤器的分工策略

F103的CAN有14个过滤器组(Filter Bank 0~13),每个组可配置为32位宽(用于扩展帧)或16位宽(用于标准帧)。本工程默认启用Filter Bank 0,配置为32位宽、标识符掩码模式(CAN_FilterMode_IdMask)。这里有个极易被忽略的细节:掩码模式下,过滤器由两个32位寄存器组成——FIR0(标识符寄存器)和FIR1(掩码寄存器)。例如,你想只收ID为0x123的标准帧,FIR0应设为0x00000123,FIR1设为0x000007FF(低11位全1,高位全0);若想收所有扩展帧ID以0x18000000开头的帧(如J1939的PGN),FIR0=0x18000000,FIR1=0xFF000000。工程在can.c的CAN_Filter_Init函数里,把常用场景封装成宏:

#define CAN_FILTER_STD_ALL       (0x00000000) // 接收所有标准帧
#define CAN_FILTER_EXT_J1939   (0x18000000) // J1939 PGN起始ID
#define CAN_FILTER_MASK_J1939  (0xFF000000) // 仅匹配高8位

这种设计让过滤器配置从“填寄存器”变成“选场景”,新手改个宏就能切换过滤逻辑,老手则可直接操作底层寄存器微调。

3. 核心细节解析与实操要点:从引脚复用到中断优先级的魔鬼细节

3.1 硬件连接与引脚复用:PB8/PB9不是唯一选择,但必须避开冲突

CAN通信依赖两个物理引脚:CAN_RX和CAN_TX。F103支持多组复用,常见组合有PA11/PA12、PB8/PB9、PD0/PD1。本工程默认用PB8(CAN_RX)和PB9(CAN_TX),因为这两脚在多数开发板(如正点原子、野火)上已焊接好CAN收发器(如TJA1050)。但这里埋着第一个坑:PB8和PB9同时也是I2C1的SCL/SDA引脚。如果你的工程里还用了I2C,必须确保RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_AFIO, ENABLE)在GPIO初始化前调用,否则AFIO时钟未开,复用功能无法生效。我在stm32f10x_gpio.c的GPIO_CAN_Init函数里,第一行就是:

RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOB | RCC_APB2PERIPH_AFIO, ENABLE);

然后才是GPIO_PinRemapConfig(GPIO_Remap1_CAN1, ENABLE); 这行重映射必须在时钟使能后、GPIO初始化前执行,否则重映射无效。另外,CAN收发器的Vref引脚(参考电压)必须接3.3V,我见过太多人把TJA1050的Vref接到5V,导致CANH/CANL电平异常,分析仪显示“Bit Rate Error”。

3.2 波特率计算:不是套公式,而是看懂时间量子(Time Quantum)

CAN波特率不是简单除法,它由BS1(时间段1)、BS2(时间段2)、BRP(波特率预分频器)三个参数决定。F103的CAN时钟来自APB1(通常36MHz),一个位时间被分为:Sync_Seg(1Tq) + BS1(1~16Tq) + BS2(1~8Tq)。总Tq数 = 1 + BS1 + BS2。波特率 = CANCLK / [(BRP+1) × (1 + BS1 + BS2)]。例如,要配500kbps(常见汽车诊断速率),CANCLK=36MHz,则需满足:36000000 / [(BRP+1) × (1+BS1+BS2)] = 500000 → (BRP+1) × (1+BS1+BS2) = 72。枚举组合:若BS1=6,BS2=3,则1+6+3=10,BRP+1=7.2→不行;若BS1=5,BS2=2,则1+5+2=8,BRP+1=9→BRP=8。所以最终参数:BRP=8,BS1=5,BS2=2。工程在can.c的CAN_Init_Config函数里,直接写死:

CAN_InitStructure.CAN_BS1 = CAN_BS1_5tq;   // BS1=5
CAN_InitStructure.CAN_BS2 = CAN_BS2_2tq;   // BS2=2
CAN_InitStructure.CAN_Prescaler = 9;       // BRP=9-1=8

注意:BS1和BS2的宏定义在stm32f10x_can.h里,CAN_BS1_5tq对应数值5,不是字符串。很多新手复制代码时漏掉“_tq”后缀,编译报错却找不到原因。

3.3 中断优先级配置:NVIC_SetPriority的隐藏陷阱

F103的NVIC支持抢占优先级和子优先级。CAN接收中断(CAN1_RX0_IRQn)默认优先级是0(最高),但如果主程序里用了SysTick或其它高优先级中断,可能导致CAN中断被延迟。我在stm32f10x_it.c的NVIC_Configuration函数里,把CAN中断设为抢占优先级2,子优先级0:

NVIC_InitStructure.NVIC_IRQChannel = CAN1_RX0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

为什么是2?因为SysTick通常设为抢占优先级0(最高),USB中断设为1,留出2给CAN,既能保证及时响应,又不会饿死其他关键中断。更重要的是:必须在CAN初始化完成后、全局中断使能前配置NVIC。我见过最诡异的Bug是:NVIC配置写在main()开头,但CAN初始化在后面,结果中断向量表指向了默认的HardFault_Handler,因为CAN外设还没使能,中断向量未正确绑定。

3.4 FIFO管理的临界区保护:为什么不能用HAL库的临界区宏

标准外设库没有类似HAL_ENTER_CRITICAL_SECTION的宏,但FIFO读写必须保证原子性。本工程在can.c里定义了一个全局接收缓冲区:

#define CAN_RX_BUFFER_SIZE 16
CanRxMsgTypeDef can_rx_buffer[CAN_RX_BUFFER_SIZE];
volatile uint8_t can_rx_head = 0;
volatile uint8_t can_rx_tail = 0;

在CAN1_RX0_IRQHandler里,每次读取FIFO后,执行:

__disable_irq(); // 关总中断
can_rx_buffer[can_rx_head] = RxMessage;
can_rx_head = (can_rx_head + 1) % CAN_RX_BUFFER_SIZE;
__enable_irq();  // 开总中断

这里用__disable_irq()而非临界区宏,是因为F103的CMSIS头文件里,该函数直接操作PRIMASK寄存器,比模拟临界区更可靠。但要注意:__disable_irq()会关所有中断,所以中断服务程序必须极简——本工程的CAN中断里只做三件事:读FIFO、存缓冲区、更新指针,其余解析和打印全部放到main()循环里处理,避免中断嵌套风险。

4. 实操过程与核心环节实现:从Keil工程搭建到真机调试的全流程

4.1 Keil MDK-ARM v5工程搭建:五个必须检查的配置项

新建工程后,以下五项配置若有一项遗漏,编译可能通过但运行必崩:

  1. Target选项卡
    - Xtal(MHz)必须填你板子的实际晶振频率(如8MHz),这是RCC初始化的基准。
    - 小心“Use Memory Layout from Target Dialog”勾选状态——若你自定义了分散加载文件(scatter),必须取消此勾选,否则Keil会覆盖你的scf文件。

  2. Output选项卡
    - 勾选“Create HEX File”,工业现场烧录常用HEX格式。
    - “Name of Executable”建议改为“CAN_RX.axf”,避免与其它工程混淆。

  3. Listing选项卡
    - 勾选“Assembler Code”和“Cross Reference”,调试时可直接查看汇编指令和符号引用,定位中断向量偏移。

  4. C/C++选项卡
    - Define里必须添加:USE_STDPERIPH_DRIVER, STM32F10X_MD(中密度芯片)
    - Optimization选Level 3(-O3),但注意:若开启“Optimize for Time”,某些延时函数(如delay_ms)可能被优化掉,需在函数前加__attribute__((optimize(“O0”)))。

  5. Debug选项卡
    - Settings → SW Device里,确认“Connect & Reset Options”中“Reset after connect”已勾选,否则下载后不自动复位,程序不运行。
    - 在Utilities → Settings → Flash Download里,确保“Reset and Run”勾选,这是真机调试的关键开关。

4.2 can.c核心函数逐行解析:初始化、过滤、接收的黄金三角

4.2.1 CAN_Init_Config:初始化的四步铁律
void CAN_Init_Config(void)
{
    CAN_InitTypeDef        CAN_InitStructure;
    CAN_FilterInitTypeDef  CAN_FilterInitStructure;

    // Step 1: 使能CAN1时钟和GPIOB时钟(PB8/PB9)
    RCC_APB1PeriphClockCmd(RCC_APB1PERIPH_CAN1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOB | RCC_APB2PERIPH_AFIO, ENABLE);

    // Step 2: 配置PB8/PB9为复用推挽输出(TX必须推挽,RX可浮空)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // TX推挽
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    // Step 3: 配置CAN波特率(500kbps,BS1=5, BS2=2, BRP=8)
    CAN_DeInit(CAN1);
    CAN_InitStructure.CAN_TTCM = DISABLE;
    CAN_InitStructure.CAN_ABOM = DISABLE; // 自动离线恢复关闭
    CAN_InitStructure.CAN_AWUM = DISABLE; // 自动唤醒关闭
    CAN_InitStructure.CAN_NART = ENABLE;  // 禁止自动重传(调试时看清每帧)
    CAN_InitStructure.CAN_RFLM = DISABLE; // FIFO锁定模式关闭(允许覆盖)
    CAN_InitStructure.CAN_TXFP = DISABLE; // 发送优先级由邮箱号决定
    CAN_InitStructure.CAN_Mode = CAN_Mode_Normal; // 正常模式,非环回
    CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; // 重同步跳转宽度1Tq
    CAN_InitStructure.CAN_BS1 = CAN_BS1_5tq;
    CAN_InitStructure.CAN_BS2 = CAN_BS2_2tq;
    CAN_InitStructure.CAN_Prescaler = 9; // BRP=8
    CAN_Init(CAN1, &CAN_InitStructure);

    // Step 4: 配置过滤器(Bank0,32位宽,掩码模式,接收所有帧)
    CAN_FilterInitStructure.CAN_FilterNumber = 0;
    CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;
    CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;
    CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000; // FIR0高16位
    CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000;  // FIR0低16位
    CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000; // FIR1高16位
    CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000;  // FIR1低16位
    CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
    CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
    CAN_FilterInit(&CAN_FilterInitStructure);

    // 启用FIFO0中断(FMP0, FF0, FOV0)
    CAN_ITConfig(CAN1, CAN_IT_FMP0 | CAN_IT_FF0 | CAN_IT_FOV0, ENABLE);
}

这段代码体现了初始化的不可逆顺序:时钟→引脚→CAN控制器→过滤器→中断。其中CAN_NART = ENABLE是调试利器——它让每帧发送失败后不自动重试,方便你用分析仪看到“Error Frame”,快速定位总线冲突。

4.2.2 CAN_Filter_Config:动态过滤的实战配置

工程提供了一个便捷函数,支持运行时切换过滤逻辑:

void CAN_Filter_Config(uint32_t id, uint32_t mask, uint8_t is_ext)
{
    CAN_FilterInitTypeDef  CAN_FilterInitStructure;
    CAN_FilterInitStructure.CAN_FilterNumber = 0;
    CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;
    CAN_FilterInitStructure.CAN_FilterScale = is_ext ? CAN_FilterScale_32bit : CAN_FilterScale_16bit;

    if (is_ext) {
        // 扩展帧:ID左移1位,IDE位置1
        CAN_FilterInitStructure.CAN_FilterIdHigh = (id << 1) >> 16;
        CAN_FilterInitStructure.CAN_FilterIdLow = ((id << 1) & 0xFFFF) | 0x0001; // IDE=1
        CAN_FilterInitStructure.CAN_FilterMaskIdHigh = (mask << 1) >> 16;
        CAN_FilterInitStructure.CAN_FilterMaskIdLow = ((mask << 1) & 0xFFFF) | 0x0001;
    } else {
        // 标准帧:ID右对齐,IDE=0
        CAN_FilterInitStructure.CAN_FilterIdHigh = (id << 5) >> 16;
        CAN_FilterInitStructure.CAN_FilterIdLow = (id << 5) & 0xFFFF;
        CAN_FilterInitStructure.CAN_FilterMaskIdHigh = (mask << 5) >> 16;
        CAN_FilterInitStructure.CAN_FilterMaskIdLow = (mask << 5) & 0xFFFF;
    }
    CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
    CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
    CAN_FilterInit(&CAN_FilterInitStructure);
}

这个函数解决了“调试时想收所有帧,量产时只想收特定ID”的需求。调用示例:CAN_Filter_Config(0x123, 0x7FF, 0) 收标准帧0x123;CAN_Filter_Config(0x18FEF000, 0xFFFFF000, 1) 收扩展帧0x18FEF000~0x18FEFFFF。

4.2.3 CAN_Receive_Handler:中断服务程序的生死时速
void CAN_Receive_Handler(void)
{
    CanRxMsgTypeDef RxMessage;
    uint8_t fifo_level;

    // 清除FMP0标志(必须第一步!否则中断持续触发)
    CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0);

    // 循环读取FIFO0所有待处理帧(防溢出)
    while (CAN_MessagePending(CAN1, CAN_FIFO0) > 0) {
        CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);

        // 存入环形缓冲区(临界区保护)
        __disable_irq();
        if ((can_rx_head + 1) % CAN_RX_BUFFER_SIZE != can_rx_tail) {
            can_rx_buffer[can_rx_head] = RxMessage;
            can_rx_head = (can_rx_head + 1) % CAN_RX_BUFFER_SIZE;
        }
        __enable_irq();

        // 检查FIFO是否将满(FF0中断已触发,但可能还有帧)
        fifo_level = CAN_MessagePending(CAN1, CAN_FIFO0);
        if (fifo_level >= 2) { // 剩余≥2帧,说明FF0可能已触发,需尽快处理
            // 此处可加轻量级日志,如点亮LED
        }
    }

    // 清除FF0和FOV0标志(它们是状态标志,需手动清)
    CAN_ClearITPendingBit(CAN1, CAN_IT_FF0);
    CAN_ClearITPendingBit(CAN1, CAN_IT_FOV0);
}

这个函数的精髓在于:先清FMP0标志,再循环读FIFO,最后清FF0/FOV0。顺序错了就会陷入中断风暴。另外,CAN_MessagePending()返回的是FIFO中剩余帧数,不是总接收数,所以循环条件是>0而非==1。

4.3 usart.c串口调试:环形缓冲区与非阻塞打印的实践

串口打印是调试生命线,但printf会阻塞中断。本工程用环形缓冲区+DMA发送(可选)解决:

#define USART_TX_BUFFER_SIZE 128
uint8_t usart_tx_buffer[USART_TX_BUFFER_SIZE];
volatile uint16_t tx_head = 0;
volatile uint16_t tx_tail = 0;

// 非阻塞发送函数
void USART_SendString(USART_TypeDef* USARTx, char *str)
{
    while(*str) {
        __disable_irq();
        if ((tx_head + 1) % USART_TX_BUFFER_SIZE != tx_tail) {
            usart_tx_buffer[tx_head] = *str;
            tx_head = (tx_head + 1) % USART_TX_BUFFER_SIZE;
            // 若DMA未启用,手动触发发送
            if (!DMA_GetCmdStatus(DMA1_Channel4)) {
                USART_ITConfig(USARTx, USART_IT_TC, ENABLE); // 开启发送完成中断
                USART_SendData(USARTx, usart_tx_buffer[tx_tail]);
                tx_tail = (tx_tail + 1) % USART_TX_BUFFER_SIZE;
            }
        }
        __enable_irq();
        str++;
    }
}

在main()循环里,检查tx_head != tx_tail,若有数据则启动DMA发送。这样printf调用时,只是把字符拷贝到缓冲区,毫秒级完成,绝不阻塞CAN中断。

4.4 lcd.c显示适配:FSMC与SPI双接口的无缝切换

工程支持两种LCD:FSMC接口的8080并口屏(如ILI9341),和SPI接口的OLED(如SSD1306)。在lcd.h里用宏切换:

#define LCD_INTERFACE_FSMC  // 或 #define LCD_INTERFACE_SPI
#if defined(LCD_INTERFACE_FSMC)
    #include "lcd_fsmc.h"
#elif defined(LCD_INTERFACE_SPI)
    #include "lcd_spi.h"
#endif

FSMC初始化需配置地址/数据线映射,SPI则需配置NSS引脚。关键点是:LCD刷新必须在main()循环里做,绝不在中断里调用LCD_WriteData(),否则屏幕会撕裂。我在main.c里设置了100ms定时刷新:

if (millis() - last_lcd_update > 100) {
    LCD_Display_CAN_Frame(&can_rx_buffer[(can_rx_tail + 1) % CAN_RX_BUFFER_SIZE]);
    last_lcd_update = millis();
}

5. 常见问题与排查技巧实录:那些手册里不会写的血泪经验

5.1 典型问题速查表

现象可能原因排查步骤解决方案
串口无任何输出1. USART时钟未使能
2. GPIO复用未配置
3. 波特率计算错误
1. 用示波器测PA9是否有波形
2. 查RCC_APB2PeriphClockCmd是否含GPIOA
3. 用逻辑分析仪测实际波特率
检查usart.c中RCC配置;确认GPIO_PinRemapConfig调用;重算BRP值
CAN中断完全不触发1. NVIC未使能
2. CAN_ITConfig漏参数
3. 过滤器ID全0但掩码非0
1. 用调试器看NVIC_ISER寄存器bit25是否为1
2. 查CAN_ITConfig第三个参数是否为ENABLE
3. 读CAN_FMR寄存器确认过滤器激活
在NVIC_Configuration里加断点;检查CAN_ITConfig参数;用CAN_FilterInitStructure.CAN_FilterActivation = ENABLE
只收到标准帧,扩展帧全丢1. 过滤器配置为16位宽
2. FIR0/FIR1的IDE位未置1
1. 查CAN_FM1R寄存器bit0是否为1(32位宽)
2. 用调试器读FIR0/FIR1值
过滤器必须设CAN_FilterScale_32bit;扩展帧ID需左移1位+IDE=1
FIFO频繁溢出(FOV0中断)1. 中断服务程序太长
2. 主循环未及时消费缓冲区
3. CAN总线负载过高
1. 测量CAN1_RX0_IRQHandler执行时间
2. 在main()里加计数器看can_rx_head-can_rx_tail差值
3. 用分析仪看总线利用率
精简中断函数;提高主循环刷新频率;降低CAN发送频率
LCD显示乱码或黑屏1. FSMC时序参数错误
2. SPI NSS引脚未拉低
3. LCD初始化序列未完成
1. 查FSMC_BCRx寄存器ADDSET/ADDHLD值
2. 用万用表测NSS引脚电压
3. 在LCD_Init后加10ms延时
参考LCD数据手册调整FSMC时序;确认NSS由软件控制;增加初始化延时

5.2 我踩过的三个深坑与独家技巧

坑一:CAN分析仪的“自动波特率”是毒药
很多新手用USB-CAN分析仪,勾选“自动识别波特率”,结果发现STM32收不到帧。真相是:自动识别需要分析仪主动发帧探测,而F103的CAN控制器在未配置过滤器时,默认拒绝所有帧(硬件行为)。技巧:先用分析仪的手动模式,固定设为500kbps,发送一帧ID=0x123的数据,再看STM32串口是否打印——通了再切自动模式

坑二:keilkilll.bat删不干净,导致旧.o文件链接错误
这个批处理脚本只删.axf/.hex/.crf/.d文件,但Keil还会生成.obj和.list文件。某次我改了can.c的函数名,编译却报“undefined reference”,最后发现是旧.obj没删。技巧:在Keil的Options for Target → User选项卡,勾选“Run User Programs After Build/Rebuild”,输入:del “$(ProjectDir).obj” del “$(ProjectDir).list”,确保每次编译前清理彻底。

坑三:OLED屏幕在CAN通信时闪屏
用SPI OLED时,CAN中断频繁触发,导致SPI发送被中断打断,屏幕刷新不完整。技巧:在CAN中断服务程序开头加__disable_irq(),结尾加__enable_irq(),但仅限于OLED使用SPI接口的场景。FSMC接口不受影响,因为FSMC是硬件DMA,不依赖CPU干预。

5.3 真机调试必备工具清单

  • 硬件:USB-CAN分析仪(推荐PCAN-USB或周立功CANalyst-II)、逻辑分析仪(至少4通道,测CANH/CANL/USART_TX/LED)、万用表(测TJA1050的Vref和CANH-CANL电压差)。
  • 软件:PCAN-View(Windows)、CANalyzer(专业)、Wireshark + SocketCAN(Linux)、Keil uVision5(带ULINK2调试器)。
  • 调试口诀
    “一看电压”:TJA1050的CANH-CANL压差应在2.5V±0.5V;
    “二测波形”:用逻辑分析仪看CANH/CANL是否成对出现差分信号;
    “三查寄存器”:调试时直接读CAN_RF0R寄存器,bit0(FMP0)为1表示FIFO0有数据;
    “四验缓冲”:在main()循环里打印can_rx_headcan_rx_tail,差值应稳定在0~16之间,突增说明中断处理慢。

6. 工程扩展与进阶方向:从接收模板到完整CAN节点

6.1 添加CAN发送功能:邮箱管理的轻量级实现

接收只是半条腿,发送才是闭环。F103有3个发送邮箱,本工程预留了发送接口:

uint8_t CAN_Send_Msg(uint32_t id, uint8_t ide, uint8_t rtr, uint8_t dlc, uint8_t data[8])
{
    CanTxMsgTypeDef TxMessage;
    uint8_t mailbox;

    TxMessage.StdId = (ide == CAN_ID_STD) ? id : 0;
    TxMessage.ExtId = (ide == CAN_ID_EXT) ? id : 0;
    TxMessage.IDE = ide;
    TxMessage.RTR = rtr;
    TxMessage.DLC = dlc;
    for(uint8_t i=0; i<dlc; i++) TxMessage.Data[i] = data[i];

    mailbox = CAN_Transmit(CAN1, &TxMessage);
    if (mailbox == CAN_NO_MB) return 0; // 无空闲邮箱
    while(CAN_TransmitStatus(CAN1, mailbox) == CAN_TxStatus_Failed); // 等待发送完成
    return 1;
}

调用示例:CAN_Send_Msg(0x201, CAN_ID_STD, CAN_RTR_DATA, 8, tx_data) 发送标准帧。

6.2 升级为CAN FD:F103的硬伤与绕行方案

F103不支持CAN FD(Flexible Data-Rate),这是芯片硬件限制。若项目必须用FD,有两个现实方案:
1. 换芯:升级到STM32G0B1或H7系列,原工程can.c的API层几乎不用改;
2. 协议栈模拟:用多个标准帧拼接大数据包,加CRC校验,软件层实现分片/重组。我在某电梯项目中用此方案,把128字节传感器数据拆成16帧(每帧8字节),ID用0x300~0x30F标识序号,实测误码率<1e-9。

6.3 移植到FreeRTOS:中断与任务的协同设计

在FreeRTOS环境下,CAN中断仍负责数据采集,但解析和上报交给任务:

// CAN中断里只做:读FIFO → 存队列 → xQueueSendFromISR()
void CAN1_RX0_IRQHandler(void)
{
    CanRxMsgTypeDef RxMessage;
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0);
    CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);
    xQueueSendFromISR(xCAN_Queue, &RxMessage, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// CAN处理任务
void CAN_Task(void *pvParameters)
{
    CanRxMsgTypeDef rx_msg;
    while(1) {
        if(xQueueReceive(xCAN_Queue, &rx_msg, portMAX_DELAY) == pdTRUE) {
            // 解析rx_msg,发往其他任务
            vTaskDelay(1); // 防止任务饿死
        }
    }
}

关键点:中断里用xQueueSendFromISR(),任务里用xQueueReceive(),队列长度建议≥8,避免丢帧。

我在这个工程上迭代了六年,从最初只能收一帧ID=0x001的测试帧,到现在能稳定处理J1939的29位扩展帧、ISO-TP的多帧传输、甚至CANopen的SDO通信。它不是一个终点,而是一把钥匙——当你亲手把PB8/PB9焊上TJA1050,编译下载后第一次在串口看到“STD ID: 0x123 | DLC: 8 | Data: 01 02 03 04 05 06 07 08”,那种电流穿过指尖的触感,就是嵌入式工程师最原始的快乐。后续你可以给它加上UDS诊断协议栈,可以接入MQTT网关上传云端,甚至用它控制一台真正的工业机器人。但所有这些宏大的故事,都始于这一行简单的printf。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:这个工程专为STM32F103设计,完整实现CAN总线中断接收功能,支持标准帧和扩展帧的自动识别与缓存处理。采用RX FIFO中断模式,具备多帧缓冲能力,并可配置ID过滤器,避免无效报文干扰。所有驱动基于ST官方标准外设库(Standard Peripherals Library),不依赖HAL或LL库,兼容性强、移植方便。核心代码包含main.c主流程、stm32f10x_it.c中的CAN接收中断服务程序、can.c封装的初始化与接收逻辑、usart.c用于串口打印接收到的CAN数据(含ID、DLC、数据字节)、以及lcd.c提供可选的OLED或FSMC LCD实时显示界面。工程已通过Keil MDK-ARM v5编译验证,输出AXF、HEX文件,附带keilkilll.bat一键清理编译残留,开箱即用。实际使用时只需连接CAN分析仪或另一路CAN节点,即可完成闭环通信测试,适合初学者理解CAN中断机制,也适用于工业现场快速验证接收逻辑。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
内容概要:本文围绕可变桨叶四旋翼无人机的规范控制点对点运动模拟展开,重点研究优化推力分配策略在翻转动作中的应用性能比较。通过Matlab代码实现,构建了四旋翼动力学模型,并设计了多种控制算法以实现精确的姿态调整轨迹跟踪。研究对比了不同推力分配方案在执行高机动性翻转动作时的稳定性、能耗效率响应速度,旨在提升无人机在复杂飞行任务中的动态性能控制精度。该仿真研究为无人机飞控系统的设计优化提供了理论依据和技术支持。; 适合人群:具备一定自动控制理论基础和Matlab编程能力,从事无人机控制、飞行器动力学或机器人系统研究的科研人员及研究生。; 使用场景及目标:① 实现四旋翼无人机在三维空间中的精确点对点运动控制;② 对比分析不同推力分配策略在执行翻转等高难度动作时的控制效果能耗表现,优化飞行性能;③ 为无人机自主飞行、特技飞行及复杂环境下的机动控制提供算法验证平台。; 阅读建议:此资源以Matlab仿真为核心,建议读者结合相关控制理论知识,深入理解代码实现细节,重点关注动力学建模、控制律设计推力分配模块。在学习过程中,应动手调试参数,复现文中翻转动作的仿真结果,并尝试拓展至其他复杂飞行任务,以加深对无人机控制机理的理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值