第14章 CAN总线通信全解:底层原理、帧结构与双机CAN通信实战

前言

上一章我们掌握了SPI高速串行总线的底层原理与大容量Flash驱动开发,而CAN总线是工业控制、汽车电子领域的核心通信总线,是嵌入式设备实现高可靠、长距离、多节点通信的必备技术。对应51单片机开发,其无内置CAN控制器,必须外接SJA1000独立控制器+TJA1050收发器,电路复杂、软件需手动处理底层时序与协议逻辑,CPU占用率高、稳定性差,最高速率仅能稳定在125Kbps;而STM32F103内置bxCAN控制器,原生支持CAN2.0A/B标准,硬件完成帧处理、总线仲裁、错误重传、硬件滤波,仅需外接收发器即可实现稳定通信,最高速率可达1Mbps,完美适配工业现场的高可靠通信需求。新手入门CAN普遍面临三大痛点:帧结构与滤波机制理解困难、回环模式正常但实际通信失败、总线频繁离线、配置正确却收不到数据。本章将严格遵循「先寄存器原理拆解,再HAL库封装逻辑」的顺序,联动51单片机对应知识点,从底层到实战全面吃透CAN总线技术,完成双机高可靠通信全场景开发。

本章目录

  • 一、本章学习目标
  • 二、核心知识点
    • 2.1 CAN总线基础与51单片机驱动方案核心对比
    • 2.2 CAN总线底层工作原理与核心通信机制
    • 2.3 CAN帧结构详解与C语言数据封装联动
    • 2.4 STM32 bxCAN控制器架构与核心寄存器拆解
    • 2.5 HAL库CAN封装逻辑与核心API深度解析
  • 三、STM32CubeMX+Keil5保姆式实操:双机CAN通信全场景实战
    • 3.1 工程创建与基础配置
    • 3.2 CAN外设与相关功能图形化配置
    • 3.3 工程代码生成与核心驱动函数编写
    • 3.4 双机通信业务代码实现
    • 3.5 编译、烧录与效果验证
  • 四、保姆式排错指南
  • 五、我的踩坑记录
  • 六、课后小练习(附完整标准答案)
    • 6.1 基础巩固练习
    • 6.2 进阶实战练习
  • 七、核心知识点速记
  • 八、本章小结

一、本章学习目标

  1. 掌握CAN总线的差分通信原理、多主仲裁机制、错误处理规则,对比51单片机外接CAN方案的核心差异,理解工业高可靠通信的工程价值
  2. 吃透CAN标准帧/扩展帧、数据帧/远程帧的完整结构,联动C语言结构体、数组知识点,完成CAN数据帧的标准化封装与解析
  3. 掌握STM32 bxCAN控制器的内核架构、位时序配置、硬件滤波机制,理解寄存器级工作原理,能独立完成CAN外设的底层配置
  4. 熟练使用HAL库完成CAN外设的初始化、滤波配置、中断收发功能,实现回环自测、双机点对点通信、多节点数据交互,符合工业级开发规范
  5. 能独立排查CAN通信失败、收不到数据、总线离线、数据错乱等高频问题,掌握CAN总线硬件设计与软件调试的核心方法

二、核心知识点

2.1 CAN总线基础与51单片机驱动方案核心对比

术语通俗解释 :CAN全称控制器局域网,是一种差分串行多主通信总线,通过CAN_H、CAN_L两根差分线实现数据传输,专为工业现场、汽车电子等强干扰、高可靠性要求的场景设计。类比工业现场的对讲机通信网络:总线上的每个节点都是平等的,总线空闲时均可发送数据;数据带优先级标识,高优先级数据优先传输,不会发生冲突;单个节点故障不会影响整个总线网络,抗干扰能力极强,支持最长10km的远距离传输。

CAN总线核心工业级特性:

  • 差分通信:抗电磁干扰能力极强,适合工业现场、汽车等强干扰环境;
  • 多主架构:无主从之分,总线空闲时任意节点均可发起通信,组网灵活;
  • 非破坏性仲裁:通过ID优先级实现总线仲裁,高优先级数据无延迟传输,不会出现数据冲突丢失;
  • 硬件错误处理:自动检测5类总线错误,出错数据自动重传,严重故障时节点自动脱离总线,不影响全网通信;
  • 硬件滤波:通过掩码/列表模式过滤数据,CPU仅需处理目标数据,大幅降低系统开销;
  • 传输特性:支持最高1Mbps@40m、125Kbps@1km的传输速率,适配不同距离的工业场景。

我们从51单片机的驱动方案出发,无缝衔接理解STM32内置CAN控制器的核心优势:

驱动方案 51单片机外接CAN方案 STM32F103内置bxCAN方案 对开发的核心影响
硬件架构 51单片机+SJA1000 CAN控制器+TJA1050收发器,外围电路复杂,占用大量IO与PCB空间 仅需STM32+TJA1050收发器,外围电路极简,仅需2个IO引脚 硬件成本大幅降低,电路可靠性提升,避免外接控制器的硬件故障风险
协议处理 所有协议逻辑、帧封装、滤波、错误处理均需软件实现,代码量大,逻辑复杂 硬件原生完成帧封装/解析、总线仲裁、错误重传、硬件滤波,软件仅需处理应用层数据 开发难度大幅降低,开发周期缩短70%以上,避免软件协议栈的bug风险
CPU占用 通信全程CPU需频繁读写外接控制器寄存器,处理中断与协议逻辑,占用率100% 通信全程硬件自动完成,仅收发完成时触发中断,CPU占用率不足5% 系统实时性大幅提升,CPU可专注于核心控制逻辑,避免通信阻塞业务执行
通信稳定性 软件时序易受中断干扰,最高仅能稳定125Kbps速率,长距离传输丢包严重 硬件时序不受软件干扰,最高稳定1Mbps速率,支持长距离传输,错误率低于百万分之一 完美适配工业现场高可靠、高速率、长距离的通信需求,避免数据丢包导致的控制异常
多节点支持 软件滤波能力有限,多节点组网时CPU开销剧增,最多支持3-5个节点 28个硬件滤波组,原生支持多节点组网,最多可挂载110个节点,CPU无额外开销 可灵活搭建工业级多节点通信网络,适配复杂的现场控制场景

2.2 CAN总线底层工作原理与核心通信机制

1. 差分电平通信机制

CAN总线仅需CAN_H、CAN_L两根信号线,通过差分电压实现数据传输,这是其强抗干扰能力的核心:

  • 隐性电平(逻辑1):CAN_H与CAN_L均为2.5V,差分电压为0V,总线空闲状态为隐性电平;
  • 显性电平(逻辑0):CAN_H为3.5V,CAN_L为1.5V,差分电压为2V,显性电平会覆盖隐性电平。

核心规则:当总线上有节点发送显性电平0时,整个总线都会被拉为显性电平,只有所有节点都发送隐性电平1时,总线才会处于隐性状态,这是CAN总线仲裁机制的物理基础。

2. 非破坏性总线仲裁机制

CAN总线采用CSMA/CA+ID优先级仲裁机制,解决多节点同时发送数据的冲突问题:

  1. 节点发送数据前,先监听总线,只有总线处于空闲状态(隐性电平)时,才能发起发送;
  2. 若两个节点同时发送数据,会逐位对比仲裁段的ID值:
    • 发送隐性电平1的节点,检测到总线为显性电平0时,会立即停止发送,退出总线竞争;
    • 发送显性电平0的节点,继续发送数据,不会受到任何影响,完成完整的帧传输;
  3. 仲裁结束后,获胜的高优先级节点无延迟完成传输,失败的低优先级节点会在总线空闲后自动重传,整个过程无数据丢失,无传输延迟,这就是非破坏性仲裁。

核心规则:CAN帧的ID数值越小,优先级越高。因为ID的高位先发送,数值越小,高位出现0的概率越高,仲裁时越容易获胜。

3. 位时序与波特率计算

CAN总线的每一位数据都分为4个时间段,用于同步、采样、容错,这是波特率配置的核心基础:

时间段 简称 固定长度 核心功能
同步段 SYNC_SEG 1个时间单元(TQ) 用于总线同步,每个位的起始跳变沿在此段
时间段1 BS1 1~16个TQ 用于补偿信号传播延迟,采样点在此段结束时
时间段2 BS2 1~8个TQ 用于补偿数据保持时间,为下一位做准备
同步跳转宽度 SJW 1~4个TQ 用于同步时的位宽调整,最大可调整1个SJW长度

波特率计算公式(联动C语言数值计算知识点):

复制代码
1TQ = APB1总线时钟 / CAN分频系数
位总时间 = SYNC_SEG + BS1 + BS2 = (1 + tbs1 + tbs2) × TQ
波特率 = 1 / 位总时间 = APB1时钟 / (分频系数 × (1 + tbs1 + tbs2))

核心说明:STM32F103的APB1总线时钟固定为36MHz,这是波特率计算的基准,新手最容易误用72MHz系统时钟计算,导致波特率错误。

工业常用波特率标准配置(STM32F103,APB1=36MHz):

目标波特率 分频系数 BS1 BS2 SJW 位总时间 实际波特率 误差
1Mbps 2 13TQ 4TQ 1TQ 18TQ 36M/(2×18)=1Mbps 0%
500Kbps 4 13TQ 4TQ 1TQ 18TQ 500Kbps 0%
250Kbps 8 13TQ 4TQ 1TQ 18TQ 250Kbps 0%
125Kbps 16 13TQ 4TQ 1TQ 18TQ 125Kbps 0%
4. 总线终端电阻规则

CAN总线两端必须各接一个120Ω的终端电阻,用于匹配总线特性阻抗,吸收信号反射,避免信号畸变导致的通信错误。

  • 核心规则:仅总线的两个端点节点接120Ω电阻,中间节点禁止接电阻;
  • 新手易错点:所有节点都接电阻,导致总线阻抗过低,驱动芯片无法正常输出电平,通信失败。

2.3 CAN帧结构详解与C语言数据封装联动

CAN总线共有5种帧类型,工业开发中最常用的是数据帧远程帧,我们重点拆解核心帧结构,联动C语言数据封装知识点。

1. 数据帧(核心,用于节点间数据传输)

数据帧由7个段组成,分为标准帧 (11位ID,CAN2.0A)和扩展帧(29位ID,CAN2.0B),结构如下:

帧段 长度 核心功能
帧起始(SOF) 1位 固定为显性电平0,标志一帧数据的开始,用于节点同步
仲裁段 11位/29位 包含帧ID、RTR位、IDE位,决定帧的优先级与类型
控制段 6位 包含保留位与DLC数据长度码,DLC为4位,指定数据段的字节数(0~8)
数据段 0~64位 实际传输的数据,长度由DLC决定,一帧最多传输8字节数据,这是CAN总线的核心限制
CRC段 16位 15位CRC校验码+1位界定符,用于硬件校验数据传输的正确性
ACK段 2位 发送端发送2位隐性电平,接收端正确接收后,在ACK槽发送显性电平,告知发送端数据接收成功
帧结束(EOF) 7位 固定为7位隐性电平,标志一帧数据的结束

仲裁段核心位说明

  • RTR位:0=数据帧,1=远程帧;
  • IDE位:0=标准帧,1=扩展帧。
2. 远程帧(用于请求指定节点发送数据)

远程帧与数据帧结构基本一致,核心差异:

  • RTR位为1,标识远程帧;
  • 无数据段,DLC位为请求的目标数据长度;
  • 核心作用:节点通过发送远程帧,请求总线上对应ID的节点发送指定数据,实现主从式数据交互。
3. C语言CAN帧封装联动

我们通过C语言结构体封装CAN数据帧,实现标准化的帧数据处理,联动结构体、数组、枚举知识点:

c 复制代码
// CAN帧类型枚举
typedef enum {
  CAN_FRAME_STD = 0, // 标准帧
  CAN_FRAME_EXT = 1  // 扩展帧
} CAN_Frame_Type;

// CAN帧传输类型枚举
typedef enum {
  CAN_FRAME_DATA = 0, // 数据帧
  CAN_FRAME_REMOTE = 1 // 远程帧
} CAN_Frame_RTR;

// CAN数据帧结构体
typedef struct {
  uint32_t id;          // 帧ID,标准帧低11位有效,扩展帧低29位有效
  CAN_Frame_Type type;  // 帧类型:标准/扩展
  CAN_Frame_RTR rtr;    // 传输类型:数据/远程
  uint8_t len;          // 数据长度,0~8
  uint8_t data[8];      // 数据缓存,最多8字节
} CAN_Frame_Typedef;

核心规则:CAN一帧最多传输8字节数据,超过8字节的数据必须分包传输,这是新手最容易踩的坑。

2.4 STM32 bxCAN控制器架构与核心寄存器拆解

STM32F103内置的bxCAN(基本扩展CAN)控制器,完全支持CAN2.0A/B协议,最高1Mbps速率,核心架构分为3大部分:发送单元、接收单元、滤波单元

1. bxCAN核心架构
  • 3个发送邮箱:用于存储待发送的CAN帧,硬件自动选择优先级最高的帧优先发送,支持发送时间戳;
  • 2个接收FIFO:每个FIFO有3级深度,最多可缓存3帧完整的CAN帧,硬件自动完成帧接收、校验、过滤,满帧后可配置覆盖或锁定模式;
  • 28个可配置滤波组 :每个滤波组可配置为32位或16位宽度,支持列表模式 (精确ID匹配)和掩码模式(掩码匹配),硬件过滤不需要的帧,仅将目标帧存入接收FIFO,大幅降低CPU开销。
2. 核心寄存器与C语言位操作联动

我们以CAN1为例,拆解核心寄存器的功能与位操作实现,联动C语言位操作知识点:

寄存器名称 结构体成员 读写属性 核心功能与位操作详解
主控制寄存器 CAN1->MCR 读写 CAN核心控制寄存器,关键位: - 位0(INRQ):初始化模式请求,写1进入初始化模式,硬件置位INAK位应答 - 位1(SLEEP):睡眠模式请求 - 位6(ABOM):自动总线离线管理,写1开启,总线离线后自动恢复 - 位7(NART):自动重传禁止,写0开启出错自动重传
位时序寄存器 CAN1->BTR 读写 波特率与模式配置寄存器,关键位: - 位31:30(SILM/LBKM):模式配置,00=正常模式,01=静默模式,10=回环模式,11=回环静默模式 - 位24(SJW):同步跳转宽度配置 - 位19:16(TS2):时间段2配置 - 位3:0(TS1):时间段1配置 - 位9:0(BRP):分频系数,0~1023,实际分频值=BRP+1
发送邮箱寄存器 CAN1->sTxMailBox[0].TIR 读写 发送邮箱标识符寄存器,配置帧ID、IDE、RTR位,置位TXRQ位启动发送
发送邮箱数据长度寄存器 CAN1->sTxMailBox[0].TDTR 读写 配置DLC数据长度,0~8
发送邮箱数据寄存器 CAN1->sTxMailBox[0].TDLR/TDHR 读写 存储待发送的8字节数据,TDLR存低4字节,TDHR存高4字节
接收FIFO寄存器 CAN1->sFIFOMailBox[0].RIR 只读 存储接收到的帧ID、IDE、RTR位
接收FIFO数据长度寄存器 CAN1->sFIFOMailBox[0].RDTR 只读 存储接收到的帧数据长度、时间戳
接收FIFO数据寄存器 CAN1->sFIFOMailBox[0].RDLR/RDHR 只读 存储接收到的8字节数据
滤波寄存器 CAN1->FM1R/FS1R/FFA1R/FA1R 读写 滤波组配置寄存器,配置滤波模式、宽度、映射FIFO、使能
滤波组寄存器 CAN1->sFilterRegister[0].FR1/FR2 读写 滤波组的ID与掩码配置寄存器
3. 硬件滤波机制详解(新手核心难点)

bxCAN的28个滤波组,每个滤波组可独立配置为两种模式,通俗解释如下:

  1. 列表模式(精确匹配)

    • 原理:设置一个或多个目标ID,只有接收到的ID与设置的ID完全一致时,才会被存入接收FIFO;
    • 类比:手机通讯录白名单,只有名单里的号码才能打进来;
    • 适用场景:总线上节点少,仅需接收少数固定ID的帧。
  2. 掩码模式(范围匹配)

    • 原理:设置一个ID基准值和一个掩码值,掩码位为1的位置,接收到的ID必须与基准值对应位一致;掩码位为0的位置,接收到的ID对应位不做要求;
    • 类比:快递地址匹配,掩码为1的位是省市区,必须一致;掩码为0的位是详细地址,不做要求;
    • 示例:基准ID=0x123,掩码=0x7F0,那么ID范围0x120~0x12F的帧都会被接收;
    • 适用场景:总线上节点多,需要接收某一ID范围的帧,工业开发最常用。

2.5 HAL库CAN封装逻辑与核心API深度解析

HAL库将bxCAN的底层寄存器操作封装为标准化的结构体与API函数,无需手动处理复杂的寄存器配置,仅需简单调用即可完成稳定的CAN通信,大幅提升开发效率。

1. CAN核心配置结构体

HAL库用CAN_HandleTypeDef结构体封装CAN外设的所有配置参数,与寄存器一一对应,联动C语言结构体知识点:

c 复制代码
typedef struct {
  CAN_TypeDef                 *Instance;    // CAN外设基地址,CAN1
  CAN_InitTypeDef             Init;         // CAN核心初始化参数
  CAN_TxHeaderTypeDef         TxHeader;     // 发送帧头配置
  CAN_RxHeaderTypeDef         RxHeader;     // 接收帧头配置
  __IO HAL_CAN_StateTypeDef   State;        // CAN运行状态
  HAL_LockTypeDef             Lock;         // 锁保护
  __IO uint32_t               ErrorCode;    // 错误代码
} CAN_HandleTypeDef;

// CAN核心初始化参数结构体
typedef struct {
  uint32_t Prescaler;          // 分频系数,BRP+1
  uint32_t Mode;                // 工作模式:正常/回环/静默
  uint32_t SyncJumpWidth;       // 同步跳转宽度
  uint32_t TimeSeg1;            // 时间段1配置
  uint32_t TimeSeg2;            // 时间段2配置
  FunctionalState AutoBusOff;   // 自动总线离线管理,ENABLE开启
  FunctionalState AutoRetransmission; // 自动重传,ENABLE开启
  FunctionalState AutoWakeUp;   // 自动唤醒
  FunctionalState ReceiveFifoLocked; // 接收FIFO锁定模式
  FunctionalState TimeTriggeredMode; // 时间触发模式
} CAN_InitTypeDef;
2. HAL库CAN核心API与底层对应关系

工业开发中最常用的核心API,分为初始化、滤波配置、收发、中断四大类:

HAL库API函数 核心功能 底层寄存器操作 适用场景
HAL_CAN_Init(CAN_HandleTypeDef *hcan) CAN外设基础初始化,配置波特率、工作模式、自动管理功能 进入初始化模式,配置MCR、BTR寄存器,设置位时序与工作模式 工程初始化阶段CAN外设配置
HAL_CAN_ConfigFilter(CAN_HandleTypeDef *hcan, CAN_FilterTypeDef *sFilterConfig) 配置硬件滤波组,设置滤波模式、ID、掩码、映射FIFO 配置滤波寄存器,设置滤波模式、ID、掩码,使能滤波组 初始化阶段必须配置,否则接收不到任何数据
HAL_CAN_Start(CAN_HandleTypeDef *hcan) 启动CAN外设,退出初始化模式,进入正常工作模式 清零INRQ位,退出初始化模式,等待硬件进入正常模式 初始化、滤波配置完成后,必须调用此函数启动CAN
HAL_CAN_Stop(CAN_HandleTypeDef *hcan) 停止CAN外设,进入初始化模式 置位INRQ位,进入初始化模式 重新配置CAN参数时调用
HAL_CAN_AddTxMessage(CAN_HandleTypeDef *hcan, CAN_TxHeaderTypeDef *pHeader, uint8_t aData[], uint32_t *pTxMailbox) 发送一帧CAN数据,自动寻找空的发送邮箱,启动发送 配置发送邮箱的TIR、TDTR、TDLR、TDHR寄存器,置位TXRQ位启动发送 所有CAN数据发送场景,工业开发核心API
HAL_CAN_GetRxMessage(CAN_HandleTypeDef *hcan, uint32_t RxFifo, CAN_RxHeaderTypeDef *pHeader, uint8_t aData[]) 从接收FIFO中读取一帧接收到的CAN数据 读取接收FIFO的RIR、RDTR、RDLR、RDHR寄存器,释放FIFO邮箱 轮询模式接收数据,或中断回调中读取数据
HAL_CAN_ActivateNotification(CAN_HandleTypeDef *hcan, uint32_t ActiveITs) 开启CAN中断,如接收FIFO挂起中断、发送完成中断、错误中断 置位CAN_IER寄存器的对应中断使能位 中断模式收发,工业开发首选,避免轮询开销
3. 核心中断回调函数
c 复制代码
// 发送邮箱发送完成回调函数
void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan);
void HAL_CAN_TxMailbox1CompleteCallback(CAN_HandleTypeDef *hcan);
void HAL_CAN_TxMailbox2CompleteCallback(CAN_HandleTypeDef *hcan);
// 接收FIFO有新消息挂起回调函数,核心接收回调
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan);
void HAL_CAN_RxFifo1MsgPendingCallback(CAN_HandleTypeDef *hcan);
// 错误回调函数,总线错误时触发
void HAL_CAN_ErrorCallback(CAN_HandleTypeDef *hcan);

三、STM32CubeMX+Keil5保姆式实操:双机CAN通信全场景实战

本次实操适配STM32F103C8T6核心板,使用TJA1050 CAN收发器,实现双机CAN通信:A机采集ADC电位器数据,通过CAN总线发送给B机;B机接收数据后通过串口上报,同时发送指令控制A机的LED亮灭,全程无跳步,零基础可零报错跟随完成。

硬件说明

外设型号 引脚分配 核心参数 接线说明
TJA1050 CAN收发器 TXD→PA12(CAN_TX)、RXD→PA11(CAN_RX) 支持最高1Mbps速率,5V供电 VCC接5V,GND接核心板GND,CAN_H、CAN_L接总线,两端节点各接120Ω终端电阻
电位器ADC采集 PA0(ADC1_IN0) 10k线性电位器 两端接3.3V与GND,中间抽头接PA0
板载LED PC13 推挽输出,低电平点亮 核心板自带,无需额外接线
串口USART1 PA9(TX)、PA10(RX) 波特率115200 接USB-TTL模块,TX-RX交叉接线,GND共地
双机总线接线 A机CAN_H接B机CAN_H,A机CAN_L接B机CAN_L 双绞线传输 总线两端各接120Ω终端电阻,中间节点不接

3.1 工程创建与基础配置

  1. 打开STM32CubeMX,点击ACCESS TO MCU SELECTOR,搜索选择STM32F103C8T6,点击Start Project创建工程。
  2. 调试接口配置:点击左侧System Core -> SYS,Debug选项选择Serial Wire,开启SWD串行调试。
  3. 时钟配置:点击RCC,HSE选项选择Crystal/Ceramic Resonator(外部8MHz晶振);进入Clock Configuration选项卡,配置PLL倍频为x9,系统时钟设置为72MHz,APB1总线时钟36MHz,无红色错误提示。

3.2 CAN外设与相关功能图形化配置

  1. CAN外设配置:
    • 点击左侧Connectivity -> CAN,激活CAN外设;
    • 核心参数配置(1Mbps波特率,匹配工业标准):
      • Mode:Normal Mode(正常模式,回环测试选Loopback Mode)
      • Prescaler:2
      • Sync Jump Width:CAN_SJW_1TQ
      • Time Seg1:CAN_BS1_13TQ
      • Time Seg2:CAN_BS2_4TQ
      • Auto Bus-Off Management:Enable
      • Auto Retransmission:Enable
      • Auto Wake-Up:Disable
      • Receive FIFO Locked:Disable
      • Time Triggered Communication Mode:Disable
    • 引脚自动映射为PA11(CAN_RX)、PA12(CAN_TX),复用推挽输出模式。
  2. 辅助功能配置:
    • ADC1:点击Analog -> ADC1,勾选IN0通道,配置为单通道、单次转换模式;
    • USART1:点击Connectivity -> USART1,Mode选择Asynchronous,波特率115200,8位数据、1位停止位、无校验;
    • GPIO:PC13引脚选择GPIO_Output,推挽输出、默认高电平,User Label设为LED
  3. NVIC中断配置:
    • 点击左侧System Core -> NVIC,优先级分组选择Priority Group 2
    • 勾选CAN RX0 interruptUSART1 global interruptADC1 and ADC2 global interrupts,抢占优先级均设为1。

3.3 工程代码生成与核心驱动函数编写

  1. 工程生成配置:进入Project Manager,设置全英文无空格的工程名与保存路径,Toolchain/IDE选择MDK-ARM V5;进入Code Generator,勾选Generate peripheral initialization as a pair of '.c/.h' files per peripheralKeep User Code when re-generating,点击GENERATE CODE生成工程,完成后点击Open Project打开Keil5工程。
  2. 核心驱动函数编写:所有代码必须写在用户代码区,避免重新生成时被覆盖。
全局变量与CAN帧结构体定义(main.c文件)
c 复制代码
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include <string.h>
/* USER CODE END Includes */

/* USER CODE BEGIN PV */
// 重定向printf到USART1
int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
  return ch;
}

// CAN句柄外部声明
extern CAN_HandleTypeDef hcan;

// CAN发送/接收帧头配置
CAN_TxHeaderTypeDef can_tx_header;
CAN_RxHeaderTypeDef can_rx_header;
uint32_t tx_mailbox; // 发送邮箱号
uint8_t can_tx_buf[8] = {0}; // 发送数据缓存
uint8_t can_rx_buf[8] = {0}; // 接收数据缓存

// ADC采集变量
uint16_t adc_val = 0;
float voltage = 0.0f;

// 节点ID定义
#define NODE_A_ID 0x123 // A机ID
#define NODE_B_ID 0x124 // B机ID
/* USER CODE END PV */
CAN初始化与滤波配置函数(main.c文件)
c 复制代码
/* USER CODE BEGIN 0 */
// CAN滤波配置函数
void CAN_Filter_Init(void)
{
  CAN_FilterTypeDef can_filter;
  // 滤波配置:32位掩码模式,接收目标ID的帧
  can_filter.FilterBank = 0; // 滤波组0
  can_filter.FilterMode = CAN_FILTERMODE_IDMASK; // 掩码模式
  can_filter.FilterScale = CAN_FILTERSCALE_32BIT; // 32位宽度
  // 基准ID:接收NODE_A_ID和NODE_B_ID的帧
  can_filter.FilterIdHigh = 0x0000;
  can_filter.FilterIdLow = NODE_A_ID << 21;
  // 掩码:ID的高11位必须匹配,其他位不关心
  can_filter.FilterMaskIdHigh = 0x0000;
  can_filter.FilterMaskIdLow = 0x7FF << 21;
  can_filter.FilterFIFOAssignment = CAN_RX_FIFO0; // 映射到FIFO0
  can_filter.FilterActivation = ENABLE; // 使能滤波组
  can_filter.SlaveStartFilterBank = 14; // 单CAN设备,无意义
  // 配置滤波
  HAL_CAN_ConfigFilter(&hcan, &can_filter);
}

// CAN接收中断回调函数,FIFO0有新消息时触发
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
  // 读取接收到的CAN帧
  HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &can_rx_header, can_rx_buf);
  // 判断帧ID,处理对应逻辑
  if(can_rx_header.StdId == NODE_A_ID)
  {
    // B机处理:收到A机的ADC数据,串口上报
    uint16_t recv_adc = (can_rx_buf[0] << 8) | can_rx_buf[1];
    float recv_volt = (float)recv_adc / 4095.0f * 3.3f;
    printf("Received ADC Data: %d, Voltage: %.2fV\r\n", recv_adc, recv_volt);
  }
  else if(can_rx_header.StdId == NODE_B_ID)
  {
    // A机处理:收到B机的LED控制指令
    if(can_rx_buf[0] == 0x01)
    {
      HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); // 点亮LED
      printf("LED ON\r\n");
    }
    else if(can_rx_buf[0] == 0x00)
    {
      HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); // 熄灭LED
      printf("LED OFF\r\n");
    }
  }
}

// CAN错误回调函数
void HAL_CAN_ErrorCallback(CAN_HandleTypeDef *hcan)
{
  printf("CAN Error Occurred, Error Code: 0x%08lX\r\n", hcan->ErrorCode);
}
/* USER CODE END 0 */

3.4 双机通信业务代码实现

A机(发送端,ADC采集)主函数业务代码
c 复制代码
int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_ADC1_Init();
  MX_CAN_Init();
  MX_USART1_UART_Init();

  /* USER CODE BEGIN 2 */
  // CAN滤波初始化
  CAN_Filter_Init();
  // 启动CAN外设
  HAL_CAN_Start(&hcan);
  // 开启CAN RX FIFO0挂起中断
  HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING | CAN_IT_ERROR);
  // 配置CAN发送帧头
  can_tx_header.StdId = NODE_A_ID; // 标准帧ID
  can_tx_header.ExtId = 0x00;
  can_tx_header.RTR = CAN_RTR_DATA; // 数据帧
  can_tx_header.IDE = CAN_ID_STD; // 标准帧
  can_tx_header.DLC = 2; // 数据长度2字节
  can_tx_header.TransmitGlobalTime = DISABLE;

  printf("Node A Start, CAN 1Mbps Init Success\r\n");
  /* USER CODE END 2 */

  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    // 采集ADC数据
    HAL_ADC_Start(&hadc1);
    HAL_ADC_PollForConversion(&hadc1, 100);
    adc_val = HAL_ADC_GetValue(&hadc1);
    HAL_ADC_Stop(&hadc1);
    // 填充发送缓存,高字节在前
    can_tx_buf[0] = adc_val >> 8;
    can_tx_buf[1] = adc_val & 0xFF;
    // 发送CAN帧
    HAL_CAN_AddTxMessage(&hcan, &can_tx_header, can_tx_buf, &tx_mailbox);
    printf("Send ADC Data: %d\r\n", adc_val);
    // 500ms发送一次
    HAL_Delay(500);
  }
  /* USER CODE END 3 */
}
B机(接收端,串口上报+指令发送)主函数业务代码
c 复制代码
int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_CAN_Init();
  MX_USART1_UART_Init();

  /* USER CODE BEGIN 2 */
  // CAN滤波初始化
  CAN_Filter_Init();
  // 启动CAN外设
  HAL_CAN_Start(&hcan);
  // 开启CAN RX FIFO0挂起中断
  HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING | CAN_IT_ERROR);
  // 配置CAN发送帧头
  can_tx_header.StdId = NODE_B_ID; // 标准帧ID
  can_tx_header.ExtId = 0x00;
  can_tx_header.RTR = CAN_RTR_DATA; // 数据帧
  can_tx_header.IDE = CAN_ID_STD; // 标准帧
  can_tx_header.DLC = 1; // 数据长度1字节
  can_tx_header.TransmitGlobalTime = DISABLE;

  printf("Node B Start, CAN 1Mbps Init Success\r\n");
  printf("Send 01 to turn on LED, 00 to turn off LED\r\n");
  /* USER CODE END 2 */

  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    // 串口接收上位机指令,发送CAN控制帧
    uint8_t uart_rx_data;
    if(HAL_UART_Receive(&huart1, &uart_rx_data, 1, 10) == HAL_OK)
    {
      if(uart_rx_data == '0' || uart_rx_data == '1')
      {
        can_tx_buf[0] = uart_rx_data - '0';
        HAL_CAN_AddTxMessage(&hcan, &can_tx_header, can_tx_buf, &tx_mailbox);
        printf("Send Control Cmd: %d\r\n", can_tx_buf[0]);
      }
    }
    HAL_Delay(10);
  }
  /* USER CODE END 3 */
}

3.5 编译、烧录与效果验证

  1. 点击Keil顶部Build按钮(F7快捷键)编译工程,底部提示0 Error(s), 0 Warning(s),说明编译成功。
  2. 仿真器配置:点击魔法棒图标(Alt+F7快捷键),Debug选项卡选择ST-Link Debugger,Settings中确认Port为SW,能识别到芯片;Flash Download选项卡勾选Reset and Run,点击OK保存。
  3. 硬件接线核心注意事项:
    • TJA1050收发器必须接5V供电,3.3V供电会导致驱动能力不足,通信异常;
    • CAN总线两端的A、B机必须各接一个120Ω终端电阻,中间节点禁止接电阻;
    • CAN_H接CAN_H,CAN_L接CAN_L,严禁接反;
    • 两块核心板、USB-TTL模块必须共地,保证电平基准一致。
  4. 效果验证:
    • 将A机代码烧录到第一块核心板,B机代码烧录到第二块核心板;
    • 打开两个串口助手,分别连接A、B机的串口,配置115200波特率;
    • 上电后,两个串口均打印初始化成功信息,A机每500ms发送一次ADC数据;
    • B机串口实时打印接收到的ADC值与电压,旋转A机的电位器,数值同步变化;
    • B机串口发送1,A机板载LED点亮,串口打印LED ON;发送0,LED熄灭,串口打印LED OFF
    • 通信过程无丢包、无错乱,总线稳定,验证双机CAN通信完全正常。

四、保姆式排错指南

异常现象/报错信息 核心根因 一步到位解决方法
CAN回环模式正常,实际双机通信完全无数据 1. CAN_H与CAN_L接反;2. 总线两端未接120Ω终端电阻;3. 两块板波特率不匹配;4. TJA1050供电错误,接了3.3V;5. 两块板未共地,电平基准不一致 1. 严格核对接线:CAN_H接CAN_H,CAN_L接CAN_L,严禁接反;2. 总线两端各接一个120Ω终端电阻;3. 核对两块板的CAN分频、BS1、BS2配置,确保波特率完全一致;4. TJA1050必须接5V供电;5. 两块核心板的GND必须可靠连接
CAN发送成功,但接收不到任何数据 1. 未配置硬件滤波,所有帧都被过滤;2. 滤波配置错误,ID/掩码不匹配;3. 未开启接收FIFO中断,或中断优先级配置错误;4. 帧ID不匹配,接收端过滤了发送端的ID;5. 总线电平异常,接收端无法识别数据 1. 初始化阶段必须配置滤波组,否则所有帧都会被过滤;2. 配置32位掩码模式,掩码设为0x0000,关闭过滤,先验证通信正常再细化滤波;3. 调用HAL_CAN_ActivateNotification开启接收中断,配置合理的抢占优先级;4. 核对发送端与接收端的帧ID,确保滤波配置包含目标ID;5. 用示波器测量CAN总线电平,确认显性/隐性电平正常
总线频繁进入离线状态,通信一段时间后完全失效 1. 未开启自动总线离线管理ABOM,出错后无法自动恢复;2. 总线阻抗不匹配,无终端电阻或电阻数量错误;3. 总线受到强干扰,错误率过高;4. 收发器损坏,或接线接触不良;5. 波特率误差过大,采样错误 1. 初始化时开启AutoBusOffManagement=ENABLE,总线离线后自动恢复;2. 仅总线两端接120Ω终端电阻,中间节点禁止接;3. 总线使用双绞线,远离强干扰源,增加TVS管防护;4. 更换TJA1050收发器,检查接线是否牢固;5. 严格按APB1=36MHz计算波特率,使用0误差的配置参数
接收的数据只有前几个字节正确,后面全是错乱的 1. 发送的DLC长度与实际数据长度不匹配;2. 一帧数据超过8字节,超出部分被截断;3. 大小端格式错误,高低字节颠倒;4. 发送与接收的数据缓存长度不匹配;5. 多帧数据发送间隔过短,前一帧未完成就发送下一帧 1. 严格设置DLC长度与实际发送的字节数一致,范围0~8;2. CAN一帧最多传输8字节,超过8字节必须分包发送;3. 发送与接收统一使用高字节在前的格式,确保大小端一致;4. 发送与接收的缓存长度必须匹配;5. 发送前检查发送邮箱是否为空,确保前一帧发送完成再发送下一帧
CAN中断不触发,收不到数据,轮询模式正常 1. 未调用HAL_CAN_ActivateNotification开启对应中断;2. NVIC中未使能CAN RX中断;3. 中断回调函数名拼写错误,未正确重写弱函数;4. 中断优先级配置错误,被其他中断永久屏蔽;5. 接收FIFO溢出,中断被锁定 1. 启动CAN后必须调用HAL_CAN_ActivateNotification开启RX FIFO挂起中断;2. 核对CubeMX配置,NVIC中已勾选CAN RX0中断;3. 回调函数名必须严格为HAL_CAN_RxFifo0MsgPendingCallback,无拼写错误;4. 配置合理的中断抢占优先级,避免被高优先级中断屏蔽;5. 开启FIFO溢出中断,溢出时及时读取释放FIFO
短距离通信正常,超过10米就丢包严重 1. 未接终端电阻,信号反射严重;2. 波特率过高,长距离传输衰减过大;3. 总线使用平行线,未用双绞线,抗干扰能力差;4. 收发器驱动能力不足,总线负载过大 1. 总线两端必须接120Ω终端电阻,吸收信号反射;2. 长距离传输降低波特率,1km使用125Kbps,2km使用50Kbps;3. 总线必须使用双绞线,提升抗干扰能力;4. 减少总线上的节点数量,确保总线负载不超过30个节点

五、我的踩坑记录

  1. 踩坑现象 :CAN回环模式完全正常,收发数据都对,但实际接双机就完全不通,示波器测总线电平也正常。
    底层原因 :我忘记在总线两端接120Ω终端电阻,近距离时信号反射不明显,示波器看电平正常,但实际采样时出现畸变,接收端无法正确识别数据。我误以为终端电阻是可选的,只有长距离才需要,完全忽略了阻抗匹配的核心作用。
    最终解决方案:在A、B机的CAN_H和CAN_L之间各接一个120Ω的终端电阻,重新上电后,双机通信立即正常,即使20米双绞线也无丢包。

  2. 踩坑现象 :CAN初始化完成后,无论怎么发送,接收端都收不到任何数据,轮询读取FIFO也是空的,回环模式也不行。
    底层原因 :我完全忘记了配置硬件滤波,bxCAN控制器默认所有滤波组都是关闭的,所有接收到的帧都会被过滤,不会存入接收FIFO,自然收不到任何数据。51单片机外接SJA1000时默认是接收所有帧,我照搬了这个思路,完全没考虑STM32的硬件滤波机制。
    最终解决方案:在初始化阶段增加滤波配置函数,先配置为32位掩码模式,掩码设为0,接收所有ID的帧,验证通信正常后,再细化滤波配置,修改后接收完全正常。

  3. 踩坑现象 :波特率配置时,我用72MHz系统时钟计算,设置分频4,BS1=13,BS2=4,结果双机通信完全不通,示波器测位宽完全不对。
    底层原因 :STM32F103的CAN控制器挂载在APB1总线上,APB1时钟最大36MHz,不是72MHz的系统时钟,我用72MHz计算波特率,导致实际波特率是预期的2倍,收发双方波特率完全不匹配,自然无法通信。
    最终解决方案:严格按APB1=36MHz计算波特率,1Mbps配置为分频2,BS1=13TQ,BS2=4TQ,重新配置后,波特率完全准确,双机通信正常。

  4. 踩坑现象 :通信一段时间后,总线突然离线,再也无法通信,必须复位芯片才能恢复,错误回调打印总线离线错误。
    底层原因 :我初始化时没有开启自动总线离线管理(ABOM),当总线出现连续错误时,控制器进入离线状态,必须软件复位才能恢复,而工业现场的干扰会导致偶发错误,频繁出现离线问题。同时我关闭了自动重传,出错的帧不会自动重发,加剧了总线错误。
    最终解决方案:初始化时开启AutoBusOffManagement=ENABLE,AutoRetransmission=ENABLE,总线出错后会自动恢复,同时增加TVS管和终端电阻,提升总线抗干扰能力,修改后连续运行72小时无离线、无丢包。


六、课后小练习(附完整标准答案)

6.1 基础巩固练习

练习1:实现CAN回环模式自测,自发自收,串口打印收发数据,验证CAN外设工作正常。

标准答案

c 复制代码
/* USER CODE BEGIN 2 */
// CAN滤波配置,接收所有帧
CAN_FilterTypeDef can_filter;
can_filter.FilterBank = 0;
can_filter.FilterMode = CAN_FILTERMODE_IDMASK;
can_filter.FilterScale = CAN_FILTERSCALE_32BIT;
can_filter.FilterIdHigh = 0x0000;
can_filter.FilterIdLow = 0x0000;
can_filter.FilterMaskIdHigh = 0x0000;
can_filter.FilterMaskIdLow = 0x0000;
can_filter.FilterFIFOAssignment = CAN_RX_FIFO0;
can_filter.FilterActivation = ENABLE;
can_filter.SlaveStartFilterBank = 14;
HAL_CAN_ConfigFilter(&hcan, &can_filter);

// 配置CAN为回环模式
hcan.Init.Mode = CAN_MODE_LOOPBACK;
HAL_CAN_Init(&hcan);
// 启动CAN,开启中断
HAL_CAN_Start(&hcan);
HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);

// 配置发送帧头
can_tx_header.StdId = 0x123;
can_tx_header.RTR = CAN_RTR_DATA;
can_tx_header.IDE = CAN_ID_STD;
can_tx_header.DLC = 4;
// 填充发送数据
can_tx_buf[0] = 0x11;
can_tx_buf[1] = 0x22;
can_tx_buf[2] = 0x33;
can_tx_buf[3] = 0x44;
// 发送数据
HAL_CAN_AddTxMessage(&hcan, &can_tx_header, can_tx_buf, &tx_mailbox);
printf("Send Data: 0x%02X 0x%02X 0x%02X 0x%02X\r\n", can_tx_buf[0], can_tx_buf[1], can_tx_buf[2], can_tx_buf[3]);
/* USER CODE END 2 */

// 接收回调函数
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
  HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &can_rx_header, can_rx_buf);
  printf("Received Data: 0x%02X 0x%02X 0x%02X 0x%02X\r\n", can_rx_buf[0], can_rx_buf[1], can_rx_buf[2], can_rx_buf[3]);
  // 数据校验
  if(memcmp(can_tx_buf, can_rx_buf, 4) == 0)
    printf("Loopback Test Success!\r\n");
  else
    printf("Loopback Test Failed!\r\n");
}
练习2:实现CAN远程帧功能,B机发送远程帧,A机收到后返回ADC采集数据。

标准答案

c 复制代码
// A机接收回调函数,处理远程帧
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
  HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &can_rx_header, can_rx_buf);
  // 判断是否为目标ID的远程帧
  if(can_rx_header.StdId == NODE_B_ID && can_rx_header.RTR == CAN_RTR_REMOTE)
  {
    // 采集ADC数据
    HAL_ADC_Start(&hadc1);
    HAL_ADC_PollForConversion(&hadc1, 100);
    uint16_t adc_val = HAL_ADC_GetValue(&hadc1);
    HAL_ADC_Stop(&hadc1);
    // 填充数据帧
    can_tx_header.StdId = NODE_A_ID;
    can_tx_header.RTR = CAN_RTR_DATA;
    can_tx_header.DLC = 2;
    can_tx_buf[0] = adc_val >> 8;
    can_tx_buf[1] = adc_val & 0xFF;
    // 发送数据帧
    HAL_CAN_AddTxMessage(hcan, &can_tx_header, can_tx_buf, &tx_mailbox);
  }
}

// B机发送远程帧函数
void CAN_Send_Remote_Frame(void)
{
  can_tx_header.StdId = NODE_B_ID;
  can_tx_header.RTR = CAN_RTR_REMOTE;
  can_tx_header.IDE = CAN_ID_STD;
  can_tx_header.DLC = 2; // 请求2字节数据
  HAL_CAN_AddTxMessage(&hcan, &can_tx_header, NULL, &tx_mailbox);
  printf("Send Remote Frame\r\n");
}

// B机主循环,每秒发送一次远程帧
while (1)
{
  CAN_Send_Remote_Frame();
  HAL_Delay(1000);
}
练习3:配置CAN滤波列表模式,仅接收2个指定ID的帧,过滤其他所有ID。

标准答案

c 复制代码
void CAN_Filter_List_Init(void)
{
  CAN_FilterTypeDef can_filter;
  // 16位列表模式,2个16位ID,精确匹配
  can_filter.FilterBank = 0;
  can_filter.FilterMode = CAN_FILTERMODE_IDLIST; // 列表模式
  can_filter.FilterScale = CAN_FILTERSCALE_16BIT; // 16位宽度
  // 配置2个目标ID,标准帧,数据帧
  can_filter.FilterIdHigh = NODE_A_ID << 5;
  can_filter.FilterIdLow = NODE_B_ID << 5;
  can_filter.FilterMaskIdHigh = 0x0000; // 未使用
  can_filter.FilterMaskIdLow = 0x0000;  // 未使用
  can_filter.FilterFIFOAssignment = CAN_RX_FIFO0;
  can_filter.FilterActivation = ENABLE;
  can_filter.SlaveStartFilterBank = 14;
  HAL_CAN_ConfigFilter(&hcan, &can_filter);
}

6.2 进阶实战练习

练习1:实现CAN双机心跳包机制,定时发送心跳包,检测节点在线状态,离线时串口报警。

标准答案

c 复制代码
/* USER CODE BEGIN PV */
#define HEARTBEAT_INTERVAL 1000 // 心跳间隔1s
#define OFFLINE_THRESHOLD 3000  // 3s未收到心跳判定离线
uint32_t last_heartbeat_time = 0;
uint8_t node_online = 0;
/* USER CODE END PV */

// 心跳包发送函数,主循环定时调用
void CAN_Send_Heartbeat(void)
{
  static uint32_t last_send_time = 0;
  if(HAL_GetTick() - last_send_time >= HEARTBEAT_INTERVAL)
  {
    can_tx_header.StdId = NODE_A_ID;
    can_tx_header.RTR = CAN_RTR_DATA;
    can_tx_header.DLC = 1;
    can_tx_buf[0] = 0x55; // 心跳包标识
    HAL_CAN_AddTxMessage(&hcan, &can_tx_header, can_tx_buf, &tx_mailbox);
    last_send_time = HAL_GetTick();
  }
}

// 接收回调函数,处理心跳包
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
  HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &can_rx_header, can_rx_buf);
  if(can_rx_header.StdId == NODE_B_ID && can_rx_buf[0] == 0x55)
  {
    last_heartbeat_time = HAL_GetTick();
    if(node_online == 0)
    {
      node_online = 1;
      printf("Node B Online!\r\n");
    }
  }
}

// 主循环在线状态检测
while (1)
{
  CAN_Send_Heartbeat();
  // 离线检测
  if(node_online == 1 && HAL_GetTick() - last_heartbeat_time >= OFFLINE_THRESHOLD)
  {
    node_online = 0;
    printf("Warning: Node B Offline!\r\n");
  }
  HAL_Delay(10);
}
练习2:基于CAN总线实现Modbus-RTU协议,主从机读写保持寄存器功能。

标准答案

c 复制代码
// Modbus-RTU帧结构体
typedef struct {
  uint8_t slave_addr;
  uint8_t func_code;
  uint16_t reg_addr;
  uint16_t reg_num;
  uint16_t crc;
} Modbus_Frame_Typedef;

// 保持寄存器
uint16_t hold_reg[10] = {0x1234, 0x5678, 0xABCD, 0xEF01, 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005};

// Modbus CRC16校验函数
uint16_t Modbus_CRC16(uint8_t *buf, uint8_t len)
{
  uint16_t crc = 0xFFFF;
  for(uint8_t i=0; i<len; i++)
  {
    crc ^= buf[i];
    for(uint8_t j=0; j<8; j++)
    {
      if(crc & 0x0001) crc = (crc >> 1) ^ 0xA001;
      else crc >>= 1;
    }
  }
  return crc;
}

// 从机Modbus帧处理函数
void Modbus_Slave_Process(uint8_t *buf, uint8_t len)
{
  Modbus_Frame_Typedef frame;
  frame.slave_addr = buf[0];
  frame.func_code = buf[1];
  frame.reg_addr = (buf[2] << 8) | buf[3];
  frame.reg_num = (buf[4] << 8) | buf[5];
  frame.crc = (buf[len-1] << 8) | buf[len-2];
  // 校验从机地址与CRC
  if(frame.slave_addr != 0x01) return;
  if(Modbus_CRC16(buf, len-2) != frame.crc) return;
  // 03功能码:读保持寄存器
  if(frame.func_code == 0x03)
  {
    uint8_t tx_buf[256] = {0};
    uint8_t tx_idx = 0;
    tx_buf[tx_idx++] = 0x01;
    tx_buf[tx_idx++] = 0x03;
    tx_buf[tx_idx++] = frame.reg_num * 2;
    // 填充寄存器数据
    for(uint16_t i=0; i<frame.reg_num; i++)
    {
      tx_buf[tx_idx++] = hold_reg[frame.reg_addr + i] >> 8;
      tx_buf[tx_idx++] = hold_reg[frame.reg_addr + i] & 0xFF;
    }
    // 添加CRC
    uint16_t crc = Modbus_CRC16(tx_buf, tx_idx);
    tx_buf[tx_idx++] = crc & 0xFF;
    tx_buf[tx_idx++] = crc >> 8;
    // 通过CAN发送响应帧
    can_tx_header.StdId = 0x200;
    can_tx_header.DLC = tx_idx;
    memcpy(can_tx_buf, tx_buf, tx_idx);
    HAL_CAN_AddTxMessage(&hcan, &can_tx_header, can_tx_buf, &tx_mailbox);
  }
}

// 接收回调函数,处理Modbus帧
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
  HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &can_rx_header, can_rx_buf);
  if(can_rx_header.StdId == 0x200)
  {
    Modbus_Slave_Process(can_rx_buf, can_rx_header.DLC);
  }
}

七、核心知识点速记

  1. CAN是差分串行多主总线,CAN_H与CAN_L传输差分信号,显性电平代表逻辑0,隐性代表逻辑1,抗干扰能力极强,适合工业现场长距离传输。
  2. CAN一帧最多传输8字节数据,分为标准帧(11位ID)和扩展帧(29位ID),ID数值越小,总线仲裁优先级越高。
  3. STM32F103的CAN控制器挂载在APB1总线,波特率计算基准为36MHz,1Mbps标准配置:分频2,BS1=13TQ,BS2=4TQ,0误差。
  4. CAN总线两端必须各接一个120Ω终端电阻,匹配总线阻抗,避免信号反射导致的通信异常,中间节点禁止接电阻。
  5. STM32 bxCAN必须配置硬件滤波组,否则所有帧都会被过滤,支持列表模式(精确匹配)和掩码模式(范围匹配),工业开发首选掩码模式。
  6. HAL库CAN核心流程:初始化→配置滤波→启动CAN→开启接收中断→发送/接收数据,必须开启自动总线离线管理,避免永久离线。
  7. CAN发送用HAL_CAN_AddTxMessage(),接收用HAL_CAN_GetRxMessage(),中断模式在RxFifo0MsgPending回调中读取数据,工业开发首选中断模式。
  8. CAN核心错误排查顺序:接线与终端电阻→波特率匹配→滤波配置→中断开启→总线电平与收发器供电。
  9. TJA1050收发器必须接5V供电,3.3V供电会导致驱动能力不足,长距离通信丢包严重。

八、本章小结

本章我们深入拆解了CAN总线的差分通信机制、仲裁原理与帧结构,对比了51单片机外接CAN方案与STM32内置bxCAN控制器的核心差异,掌握了CAN位时序配置、硬件滤波机制与寄存器级工作原理,吃透了HAL库CAN的封装逻辑与核心API,完成了双机CAN通信全场景实战,解决了总线离线、收不到数据、滤波配置错误等高频问题。CAN是工业现场控制的核心总线,下一章我们将学习物联网通信入门,基于ESP8266 WiFi模块与MQTT协议,实现STM32接入阿里云平台的远程通信实战,完成设备的远程监控与控制。

相关推荐
客卿1232 小时前
用两个栈实现队列
android·java·开发语言
leaves falling2 小时前
C++模板初阶:让代码“复制粘贴”自动化
开发语言·c++·自动化
java1234_小锋2 小时前
Java高频面试题:谈谈你对SpringBoot的理解?
java·开发语言·spring boot
2301_816651222 小时前
C++模块化设计原则
开发语言·c++·算法
Ulyanov2 小时前
Python GUI工程化实战:从tkinter/ttk到可复用的现代化组件架构
开发语言·python·架构·gui·tkinter
competes2 小时前
深圳程序员职业生涯
java·大数据·开发语言·人工智能·java-ee
coderYYY2 小时前
git push报错Authentication failed for ‘xxx’也不会弹要求输入用户名密码的最终解决方法
前端·git·gitee·github
沐欣工作室_lvyiyi3 小时前
基于单片机的智能浴室(论文+源码)
stm32·单片机·嵌入式硬件·智能浴室
小小小米粒3 小时前
Collection(单列集合)、Map(双列集合),容易搞混的 Collections 工具类。
java·开发语言