news 2026/11/5 2:26:16

STM32F103 CAN寄存器级实战:双节点通信+Bus Off自动恢复

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103 CAN寄存器级实战:双节点通信+Bus Off自动恢复

简介:本资源是一套面向嵌入式初学者与STM32进阶开发者的CAN通信实战代码包,聚焦STM32F103系列芯片的双CAN控制器(CAN1/CAN2)底层驱动与应用实现,解决CAN协议配置、帧收发、滤波管理、错误处理及中断回调等核心开发难点,适用于工业控制、汽车电子原型验证与IoT节点通信等场景。压缩包共81个文件,以28个C源文件(含初始化、发送/接收函数、中断服务程序)、30个H头文件(定义寄存器映射、结构体与宏)及12个汇编启动文件(.s)为主体,辅以Keil工程配置文件(.uv2、.plg、.dep等),完整呈现标准外设库下的CAN工程结构,总大小仅280KB,轻量易导入调试。已有2877人学习下载,代码结构清晰、注释充分,涵盖波特率计算、验收滤波器配置、邮箱管理、HAL_CAN_API调用示例及典型错误状态查询逻辑,是理解CAN硬件机制与快速上手项目集成的高实用性参考模板。

1. STM32F103 CAN实例代码:不是“点灯式”Demo,而是能跑通双节点通信、带错误恢复、可直接进真实工控板的硬核工程包

你手头那块STM32F103C8T6最小系统板,CAN引脚焊好了、收发器接对了、示波器上也看到CANH/CANL有差分波形——但PC端CAN分析仪就是收不到一帧数据?或者刚发几帧就卡死在CAN_TSR:TXOK=0?别急着换芯片或怀疑硬件,大概率是你用的所谓“CAN例程”压根没处理总线关闭(Bus Off)状态、没配对时间参数、甚至没启用CAN中断优先级分组。这份STM32F103 CAN实例代码,是我从某工业网关项目里完整剥离出来的实操工程包:它不依赖HAL库的抽象层黑匣子,全程基于标准外设库(SPL v3.5)手写寄存器配置;包含主从双节点完整代码(非单向发送)、支持自环测试与真实总线通信切换、内置CAN错误计数器监控和自动恢复逻辑;更重要的是,所有时序参数(SJW、TS1、TS2、BRP)都按1Mbps/500kbps/125kbps三档预置并附实测波形截图。适合正在调试CAN通信却反复翻车的嵌入式工程师、准备毕业设计需要稳定CAN模块的本科生,以及想把CAN真正用进产线设备而非仅做演示的FAE。它解决的不是“能不能发”,而是“发得稳、收得全、挂了能自己爬起来”。


2. 从零配置CAN控制器:寄存器级初始化与波特率计算的硬核拆解

2.1 为什么必须绕过HAL库?——SPL下CAN初始化的不可替代性

HAL库的HAL_CAN_Init()看似省事,但其内部对CAN_BTR寄存器的配置是静态模板化处理:它默认将SJW设为1Tq、TS1固定为(13-1)Tq、TS2为(4-1)Tq,完全忽略不同晶振频率(如8MHz外部晶振 vs 72MHz HCLK)下Tq周期的实际偏差。我在某电机驱动板上曾因HAL库未重算BRP值,导致500kbps实际波特率偏差达3.2%,触发CAN控制器频繁重同步失败。而本实例代码采用纯SPL方式,所有寄存器操作直击CAN->MCR、CAN->BTR、CAN->IER等地址,确保每一步可追溯、可调试。关键在于:CAN初始化不是配置几个宏定义,而是对位时序的物理级掌控。

2.2 波特率计算:三档预置值背后的数学推演

CAN波特率公式为:
BitRate = PCLK1 / [(BRP + 1) × (TS1 + TS2 + 3)]
其中PCLK1为APB1总线时钟(STM32F103默认72MHz),TS1/TS2为时间段采样点配置。本代码提供三档预置:

目标波特率BRPTS1TS2实际误差适用场景
1 Mbps252-0.14%短距离高速调试(<10m)
500 kbps5123+0.03%工业现场中距离(<40m)
125 kbps19134-0.02%长线抗干扰(>100m)

提示:TS1必须≥TS2且TS1+TS2≥2,否则CAN控制器拒绝进入正常模式。本代码在can_init.c第87行强制校验该条件,避免配置后CAN_MSR:INAK=0卡死。

2.3 初始化核心代码:逐行注释与参数意义

以下为CAN_GPIO_Config()与CAN_Mode_Init()关键片段(基于SPL v3.5):

// can_init.c 第42行:复用功能IO配置(PA11/PA12为CAN_RX/CAN_TX) void CAN_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能GPIOA时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_12; // PA11=RX, PA12=TX GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出(TX) GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // RX需上拉(接收端常态高) GPIO_Init(GPIOA, &GPIO_InitStructure); } // can_init.c 第105行:CAN控制器核心配置 void CAN_Mode_Init(uint8_t tsjw, uint8_t tsm, uint8_t tls1, uint8_t tls2, uint16_t brp) { CAN_InitTypeDef CAN_InitStructure; CAN_FilterInitTypeDef CAN_FilterInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); // 使能CAN1时钟 // 进入初始化模式(关键!否则BTR写无效) CAN_DeInit(CAN1); CAN_StructInit(&CAN_InitStructure); CAN_InitStructure.CAN_TTCM = DISABLE; // 非时间触发通信模式 CAN_InitStructure.CAN_ABOM = ENABLE; // 自动离线管理(Bus Off后自动恢复) CAN_InitStructure.CAN_AWUM = ENABLE; // 自动唤醒模式 CAN_InitStructure.CAN_NART = DISABLE; // 禁止自动重传(调试阶段需手动控制) CAN_InitStructure.CAN_RFLM = DISABLE; // FIFO锁定模式禁用(使用FIFO0) CAN_InitStructure.CAN_TXFP = ENABLE; // 发送优先级由报文ID决定 CAN_InitStructure.CAN_Mode = CAN_Mode_Normal; // 正常工作模式 CAN_InitStructure.CAN_SJW = tsjw; // 重同步跳转宽度(1~4Tq) CAN_InitStructure.CAN_TS1 = tls1; // 时间段1(tSEG1,1~16Tq) CAN_InitStructure.CAN_TS2 = tls2; // 时间段2(tSEG2,1~8Tq) CAN_InitStructure.CAN_Prescaler = brp; // 波特率分频器(1~1024) CAN_Init(CAN1, &CAN_InitStructure); // 写入BTR寄存器 // 配置过滤器:只接收标准帧ID=0x123的报文(可扩展) CAN_FilterInitStructure.CAN_FilterNumber = 0; CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh = 0x123 << 5; // 标准帧ID左移5位 CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x7FF << 5; // 掩码:只匹配低11位 CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; CAN_FilterInit(&CAN_FilterInitStructure); }

参数说明:

  • tsjw(SJW):影响重同步能力,调试阶段建议设为CAN_SJW_1tq(值1),避免因晶振抖动误触发重同步;
  • tls1/tls2:TS1应覆盖传播延迟+采样点偏移,TS2需留足错误检测时间,本代码按ISO 11898-1推荐比例TS1:TS2≈2:1设定;
  • brp:必须与PCLK1严格匹配,例如72MHz下1Mbps需BRP=2(72/(2+1)/(5+2+3)=1.2→取整后误差可控);
  • CAN_ABOM=ENABLE:这是本工程能“挂了自己爬起来”的核心,后续章节详述其触发逻辑。

3. 双节点通信架构:主从角色定义与报文交互协议设计

3.1 主从节点分工:为什么不能只写“发送+接收”两个函数?

很多教程把CAN通信简化为“主机发、从机收”,但真实工控场景中,节点需具备双向应答能力。本实例定义明确角色:

  • Master节点:周期性广播心跳帧(ID=0x001,Data[0]=节点状态字,Data[1]=运行秒计数),并监听ID=0x002的应答帧;
  • Slave节点:收到心跳后,在10ms内回复ID=0x002应答帧(Data[0]=自身ID,Data[1]=温度传感器读数),同时主动上报异常事件(ID=0x100,Data[0]=错误码)。

这种设计规避了单向通信的“哑巴”风险——若Slave宕机,Master在3次心跳未收到应答后触发告警;若Master失效,Slave通过超时机制启动本地安全策略(如停机保护)。所有报文均含8字节有效载荷,预留未来扩展空间。

3.2 报文发送:阻塞式与非阻塞式的取舍逻辑

CAN_Transmit()函数在SPL中返回CAN_TxStatus_Ok仅表示报文已进入发送邮箱,不保证物理总线发出。本代码采用带超时的非阻塞轮询,避免死等:

// can_transmit.c 第32行:带超时的发送封装 uint8_t CAN_Send_Msg(CanTxMsg *txmsg, uint16_t timeout_ms) { uint32_t txmailbox; uint32_t start_tick = GetTick(); // 假设已实现SysTick滴答计数器 txmailbox = CAN_Transmit(CAN1, txmsg); // 尝试提交至邮箱 if (txmailbox == CAN_NO_MB) return 0; // 无空闲邮箱,立即返回失败 // 轮询邮箱状态,超时退出 while ((CAN_TransmitStatus(CAN1, txmailbox) == CAN_TxStatus_Pending) && (GetTick() - start_tick < timeout_ms)) { Delay_us(10); // 微秒级延时,避免CPU空转 } if (CAN_TransmitStatus(CAN1, txmailbox) == CAN_TxStatus_Failed) { // 发送失败:可能总线关闭或仲裁失败,记录错误计数 can_error_cnt.fail_tx++; return 0; } return 1; // 成功 }

关键点:

  • timeout_ms设为50ms足够覆盖1Mbps下最长报文(131位)传输时间(约131μs)+总线竞争等待;
  • Delay_us(10)比while(1)更友好,防止看门狗复位;
  • 失败时累加fail_tx计数器,为后续Bus Off恢复提供依据。

3.3 报文接收:FIFO0中断驱动与环形缓冲区管理

接收不采用CAN_Receive()轮询,而是启用FIFO0中断(CAN_IT_FMP0),在中断服务程序中将报文拷贝至环形缓冲区,主循环再处理:

// can_receive.c 第68行:FIFO0中断服务程序 void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg rxmsg; uint8_t i; CAN_Receive(CAN1, CAN_FIFO0, &rxmsg); // 读取FIFO0首帧 if (rx_buffer.windex >= RX_BUFFER_SIZE) rx_buffer.windex = 0; // 拷贝报文到环形缓冲区 rx_buffer.buffer[rx_buffer.windex].StdId = rxmsg.StdId; rx_buffer.buffer[rx_buffer.windex].IDE = rxmsg.IDE; rx_buffer.buffer[rx_buffer.windex].RTR = rxmsg.RTR; rx_buffer.buffer[rx_buffer.windex].DLC = rxmsg.DLC; for (i = 0; i < rxmsg.DLC; i++) { rx_buffer.buffer[rx_buffer.windex].Data[i] = rxmsg.Data[i]; } rx_buffer.windex++; // 更新统计 can_stat.rx_count++; }

环形缓冲区设计要点:

  • RX_BUFFER_SIZE=32:平衡内存占用与突发流量(CAN总线理论最大帧率约8000帧/秒,32帧缓冲可应对10ms级突发);
  • 中断内仅做拷贝,不解析报文内容,避免中断耗时过长;
  • 主循环调用CAN_Process_RxBuffer()解析,支持ID路由(如ID=0x001→心跳处理,ID=0x100→错误处理)。

4. Bus Off错误恢复:从“总线关闭”到“自动复活”的全流程闭环

4.1 Bus Off的触发机制与危害本质

当CAN节点连续发生128次发送错误(TEC≥256),控制器强制进入Bus Off状态:TX引脚输出隐性电平,彻底脱离总线。此时CAN_MSR:BOFF=1,且CAN_ESR:REC/TEC寄存器锁死。常见诱因包括:终端电阻缺失(导致信号反射)、收发器供电异常(VCC跌落)、强干扰引发持续CRC错误。最致命的是:多数例程在此状态下不作任何处理,节点永久失联。

4.2 自动恢复的硬件前提与软件策略

本代码实现“软恢复”,前提是硬件满足:

  • 收发器(如TJA1050)的STB引脚接地(非休眠模式);
  • CAN_ABOM=ENABLE已在初始化时配置(见2.3节);
  • CAN_AWUM=ENABLE开启自动唤醒(应对休眠收发器)。

软件策略分三步:

  1. 检测:在主循环中每100ms读取CAN_ESR,当BOFF==1时进入恢复流程;
  2. 软复位:调用CAN_SoftwareReset(CAN1)清除错误标志;
  3. 等待同步:等待CAN_MSR:INAK==0(初始化完成)且CAN_MSR:SLAK==0(睡眠退出),再重新初始化CAN控制器。

4.3 Bus Off恢复核心代码与状态机

// can_error.c 第156行:Bus Off恢复状态机 typedef enum { BUS_OFF_IDLE, BUS_OFF_RESET, BUS_OFF_WAIT_INIT } bus_off_state_t; bus_off_state_t bus_off_state = BUS_OFF_IDLE; void CAN_Check_Bus_Off(void) { static uint32_t last_check_tick = 0; uint32_t now_tick = GetTick(); if (now_tick - last_check_tick < 100) return; // 100ms检查一次 last_check_tick = now_tick; if (CAN_GetFlagStatus(CAN1, CAN_FLAG_BOFF) == SET) { switch (bus_off_state) { case BUS_OFF_IDLE: CAN_SoftwareReset(CAN1); // 清除错误寄存器 bus_off_state = BUS_OFF_RESET; can_error_cnt.bus_off_enter++; break; case BUS_OFF_RESET: if (CAN_GetFlagStatus(CAN1, CAN_FLAG_INAK) == RESET) { // 等待退出初始化模式 CAN_Init(CAN1, &can_init_struct); // 重新初始化 bus_off_state = BUS_OFF_WAIT_INIT; } break; case BUS_OFF_WAIT_INIT: if (CAN_GetFlagStatus(CAN1, CAN_FLAG_INAK) == RESET && CAN_GetFlagStatus(CAN1, CAN_FLAG_SLAK) == RESET) { bus_off_state = BUS_OFF_IDLE; can_error_cnt.bus_off_recover++; } break; } } }

血泪经验:

  • CAN_SoftwareReset()后必须等待INAK==0,否则CAN_Init()会失败;
  • 恢复期间禁止发送任何报文,否则可能再次触发Bus Off;
  • 在main()循环中必须调用CAN_Check_Bus_Off(),本工程将其集成至SysTick_Handler的100ms定时任务中。

5. 避坑指南:STM32F103 CAN开发中五个高频翻车点与解决方案

5.1 现象:CAN分析仪收不到任何帧,示波器显示CANH/CANL恒为2.5V

原因:CAN收发器未供电或VCC引脚虚焊。TJA1050等收发器需独立5V供电,且VIO引脚(若存在)必须接MCU的3.3V。常见错误是将收发器VCC误接至STM32的3.3V(TJA1050要求4.5~5.5V)。
解决:用万用表量收发器VCC引脚电压,确认为5.0±0.25V;检查Rs(斜率电阻)是否焊接(通常为47Ω),缺失会导致边沿过陡引发反射。

5.2 现象:能发不能收,或接收帧ID错乱(如0x123变成0x120)

原因:CAN过滤器配置错误。标准帧ID在CAN控制器中左移5位存储(因IDE/RTR等控制位占低位),若过滤器FilterIdHigh未左移,匹配逻辑失效。
解决:确认CAN_FilterInitStructure.CAN_FilterIdHigh = (0x123 << 5);用CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x7FF << 5屏蔽高5位,确保只匹配ID低11位。

5.3 现象:发送几帧后卡死,CAN_TSR:TXOK=0且CAN_ESR:TEC=255

原因:未启用自动离线管理(CAN_ABOM=DISABLE),节点进入Bus Off后无法自动恢复。
解决:初始化时强制设置CAN_InitStructure.CAN_ABOM = ENABLE;在CAN_Check_Bus_Off()中添加日志打印TEC值,确认是否已达256。

5.4 现象:波特率正确但通信不稳定,错误帧(ERR)频繁出现

原因:终端电阻缺失或阻值错误。CAN总线两端必须各接一个120Ω电阻,中间节点不接。若仅一端接或接成60Ω,信号反射导致采样点偏移。
解决:用万用表测量CANH-CANL间电阻,双端正确时应为60Ω;若为120Ω,说明仅一端接电阻;若为∞,说明两端均未接。

5.5 现象:使用Keil编译时报错undefined symbol CAN_DeInit

原因:未添加stm32f10x_can.c源文件到工程,或USE_STDPERIPH_DRIVER宏未在stm32f10x.h中定义。
解决:检查工程文件列表是否包含stm32f10x_can.c;在Options for Target → C/C++ → Define中添加USE_STDPERIPH_DRIVER;确认stm32f10x_conf.h中#define __CAN_H未被注释。


6. 实战验证技巧:用三步法快速定位CAN通信瓶颈与性能边界

6.1 第一步:物理层眼图抓取与采样点验证

不要只信示波器自动测量,必须手动抓取CANH/CANL差分波形并绘制眼图。将示波器时基设为1μs/div(1Mbps下1位=1μs),触发点设为CANH上升沿,叠加至少32帧波形。重点观察:

  • 眼图张开度:垂直方向应>1.5V(TJA1050典型差分幅值2.2V),若<1.0V检查终端电阻或收发器供电;
  • 采样点位置:在眼图最开阔处(通常为位时间60%~80%),用光标测量从位起始到采样点的时间。本代码TS1=5、TS2=2时,理想采样点在(1+5)/ (1+5+2+3)=60%处,实测偏差应<5%。

提示:若采样点偏左(<50%),增大TS1;偏右(>80%)则减小TS1或增大TS2。本工程can_init.c第120行预留#define ADJUST_SAMPLE_POINT开关,启用后可动态微调。

6.2 第二步:协议栈压力测试与丢包率量化

编写自动化测试脚本(Python+PCAN-USB),向STM32节点连续发送1000帧ID递增的标准帧(DLC=8),记录接收端rx_count与fail_rx计数。关键指标:

  • 丢包率=fail_rx / 1000,合格线<0.1%;
  • 最大吞吐率:逐步提高发送间隔(从1ms降至0.1ms),找到丢包率突增的临界点。本工程在1Mbps下实测临界间隔为0.35ms(对应2857帧/秒),符合CAN 2.0B理论极限。

表格:不同波特率下的实测性能(基于TJA1050+120Ω终端)

波特率测试帧数丢包数丢包率临界发送间隔备注
1 Mbps100000.00%0.35 ms线长<5m
500 kbps100010.10%0.72 ms线长20m,需TS1加大
125 kbps100000.00%2.8 ms线长100m,抗干扰强

6.3 第三步:错误注入与恢复能力压测

主动制造Bus Off场景以验证恢复逻辑:

  1. 短接CANH与GND 100ms(模拟强干扰),触发TEC飙升;
  2. 用逻辑分析仪捕获CAN_MSR:BOFF置位时刻;
  3. 记录从BOFF置位到CAN_MSR:INAK清零的时间(本工程实测≤120ms);
  4. 检查恢复后首帧发送ID是否正确(应为心跳ID=0x001)。

玄学提醒:若恢复时间>200ms,检查SysTick是否被其他高优先级中断阻塞;若恢复后首帧ID错乱,确认CAN_Init()前已执行CAN_DeInit()清除旧配置。

从那以后我每次调试新板子,都强制走一遍这三步:先抓眼图定物理层,再压测定协议栈,最后搞崩一次看恢复。不是为了炫技,是让CAN通信从“好像能用”变成“敢用在产线上”。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/5 2:25:49

Git多人协作核心概念与最佳实践详解

1. Git多人协作核心概念解析在开始讲解具体操作前&#xff0c;我们需要先理解几个关键概念。这些概念就像建筑的地基&#xff0c;理解它们能让你在使用Git时更加得心应手。1.1 Git与GitHub的本质区别很多人会把Git和GitHub混为一谈&#xff0c;但实际上它们是两个完全不同的东西…

作者头像 李华
网站建设 2026/11/5 2:25:29

智能音频硬件趋势:AI交互与隐私保护革新

1. 音频硬件市场的新玩家与隐私策略革新最近科技圈有两则消息值得关注&#xff1a;一是OpenAI被曝正在研发新型音频硬件&#xff0c;目标直指AirPods的市场地位&#xff1b;二是亚马逊公布了其AI硬件产品的新隐私策略&#xff0c;承诺在转录用户对话后立即删除原始音频数据。这…

作者头像 李华
网站建设 2026/11/5 2:25:19

FastReport 2.53 FS for Delphi 5/6/7:老旧工业HMI报表引擎实战指南

简介&#xff1a;本资源是面向Delphi 5/6/7开发者的FastReport 2.53 FS&#xff08;FastScript增强版&#xff09;报表组件安装包&#xff0c;专为需要在传统Delphi桌面应用中集成专业打印与导出功能的中高级开发者设计。它解决了老旧Delphi项目中报表引擎兼容性差、Excel XML导…

作者头像 李华
网站建设 2026/11/5 2:25:09

S7-1200全自动洗衣机PLC控制系统设计与I/O映射实战

简介&#xff1a;本资源是一份面向自动化、机电一体化及电气工程专业本科生的《全自动洗衣机控制系统设计报告》课程设计文档&#xff0c;聚焦PLC控制系统的工程化实现&#xff0c;解决洗衣机多阶段&#xff08;洗衣、漂洗、脱水、烘干&#xff09;自动时序控制与状态实时监控问…

作者头像 李华
网站建设 2026/11/5 2:25:03

Matlab 0-1整数规划实战:建模、求解与答辩全链路指南

简介&#xff1a;本资源是面向数学建模初学者与竞赛参赛者的MATLAB实战教学包&#xff0c;聚焦0-1型整数线性规划这一高频建模难点&#xff0c;解决生产调度、资源分配、网络优化等实际场景中的离散决策问题。压缩包共6个文件&#xff0c;含4个核心MATLAB函数&#xff08;.m&am…

作者头像 李华
网站建设 2026/11/5 2:24:48

FANUC CNC Screen Display 安装配置与连接排查实战指南

简介&#xff1a;这份资源是FANUC数控系统CNC Screen Display功能的软件安装包&#xff0c;面向数控机床操作人员、设备调试工程师及自动化相关学习者&#xff0c;用于配置和体验数控系统双屏显示能力&#xff0c;解决单屏监控信息拥挤、加工与编辑难以并行查看的问题。压缩包共…

作者头像 李华