大家好,这篇笔记是我从零开始学习 CAN 总线的全过程整理,全是新手能看懂、能直接用的干货,从基础概念到 STM32 实操全流程覆盖,适合刚接触 CAN 总线的同学参考,也方便我自己后续复习回顾。

一、先搞懂:CAN 总线到底是什么?

CAN 全称控制器局域网(Controller Area Network),是德国博世公司在 20 世纪 80 年代专门为汽车行业开发的串行通信总线,说白了就是一种能让多个设备之间稳定、高效传输数据的 “通信网线”。

和我们常用的串口、485 总线相比,它最大的优势就是高可靠性、多主控制、抗干扰能力强,所以现在不光是汽车电子,工业控制、航空航天、医疗设备、船舶等领域都在广泛使用。

两个核心国际标准

CAN 总线经过 ISO 标准化后,有两个最核心的标准,区别直接看表格

标准 速率范围 总线类型 核心传输参数 终端电阻 常用场景
ISO-11898(高速 CAN) 125Kbps~1Mbps 闭环总线 1Mbps 速率下,总线最长 40 米 120Ω 汽车动力系统、短距离高实时性控制
ISO-11519(低速 CAN) 10~125Kbps 开环总线 40Kbps 速率下,总线最长 1000 米 2.2kΩ 车身控制、长距离低速率场景

补充一个极限参数:CAN 总线最远传输距离可达 10KM(此时速率要低于 5Kbps),最高速率 1Mbps(距离必须小于 40 米),速率和距离成反比,做硬件布线时要注意。

二、CAN 总线 6 个核心特点

这部分是 CAN 总线的核心优势,也是它和其他总线的核心区别

  1. 多主控制:总线里没有绝对的 “主机” 和 “从机”,所有设备都能主动发数据,就像一个电话会议,所有人都能开口说话。如果多个设备同时说话,会通过报文的 ID 来决定优先级,优先级高的继续说,优先级低的立刻闭嘴转为收听,不会打乱总线通信。
  2. 系统柔软性(扩展性极强):CAN 总线里的设备没有 “地址” 的概念,我们给总线上加新设备时,原来已经接好的设备,不管是硬件、软件还是应用层,全都不用改,新手做项目加节点特别方便。
  3. 完备的错误处理能力:总线上所有设备都能检测通信错误,一旦发现错误,立刻给所有设备发通知;正在发数据的设备发现错误,会立刻停止发送,然后自动重发,直到发成功为止,不用我们软件手动写重发逻辑。
  4. 故障封闭功能:CAN 能区分是临时的总线噪声干扰,还是某个设备硬件坏了导致的持续错误。如果某个设备一直出问题,会自动把这个设备从总线上隔离出去,不会因为一个设备坏了,导致整个总线瘫痪,这个在工业场景里特别重要。
  5. 可连接节点多:理论上总线上能接无数个设备,实际使用中,速率越低,能接的设备越多,高速 CAN 最多能接 30 个设备,完全满足绝大多数项目需求。
  6. 非破坏性仲裁:多个设备同时发数据时,优先级高的报文会完整发出去,不会因为冲突导致报文损坏,保证高优先级的指令能实时传输。

三、物理层核心:显性电平与隐性电平

CAN 总线只用两根线通信:CAN_HCAN_L,一般用双绞线,抗干扰能力强。所有的通信逻辑,都基于这两根线的电平差。

电平定义

电平类型 逻辑值 CAN_H 电压 CAN_L 电压 压差 核心规则
显性电平 0 3.5V 1.5V 2V 只要有一个设备输出显性电平,总线就是显性电平(优先)
隐性电平 1 2.5V 2.5V 0V 只有所有设备都输出隐性电平,总线才是隐性电平(包容)

关键逻辑:CAN 总线执行线与逻辑,显性电平永远比隐性电平优先级高,后面的总线仲裁,全靠这个逻辑实现。

四、CAN 帧类型:数据是怎么打包传输的?

CAN 总线上的所有数据,都是以 “帧” 为单位传输的。

5 种帧类型

帧类型 核心用途
数据帧 发送设备给接收设备传数据,最常用、最核心
遥控帧(远程帧) 接收设备给对应 ID 的发送设备,请求发数据
错误帧 设备检测到错误时,给全网通知错误
过载帧 接收设备告诉发送端:我还没准备好,你先别发
帧间隔 把数据帧 / 遥控帧和前面的帧分开,相当于分隔符

新手入门,精力只需要放在数据帧上,把它搞懂,就能跑通CAN。

数据帧的 7 个组成部分

数据帧分 7 段,每一段都有自己的作用,标准帧(11 位 ID)和扩展帧(29 位 ID)只有仲裁段、控制段有区别,其他段完全一样。

帧段 长度 核心作用 关键规则
帧起始 1 位 要开始发数据了 固定为显性电平(逻辑 0),标准帧和扩展帧完全一样
仲裁段 标准帧 12 位 / 扩展帧 32 位 优先级是多少,发的是什么类型的数据 1. 核心是 ID:标准帧 11 位 ID,扩展帧 29 位 ID,ID 数值越小,优先级越高2. 禁止 ID 高 7 位全是隐性电平(全 1)3. RTR 位:0 = 数据帧,1 = 遥控帧;IDE 位:0 = 标准帧,1 = 扩展帧4. 标准 ID 最大值 0x7FF,扩展 ID 最大值 0x1FFFFFFF,超了会被控制器自动截断,新手很容易踩这个坑
控制段 6 位 要发几个字节的数据 核心是 DLC 数据长度码,取值 0~8,代表数据段的字节数,CAN2.0 最多一次发 8 个字节
数据段 0~64 位(0~8 字节) 要传输的数据 从最高位开始发送,标准帧和扩展帧格式完全一样
CRC 段 16 位 校验数据 15 位 CRC 校验序列 + 1 位界定符,接收端会用同样的算法算 CRC,不一致就认为传输出错
ACK 段 2 位 应答 发送端发 2 个隐性电平,所有正常收到数据的设备,会在 ACK 槽发显性电平应答,发送端收到应答,就知道数据发成功了
帧结束 7 位 我发完了 固定为隐性电平,标准帧和扩展帧完全一样

ID 的 3 个核心作用:

  1. 仲裁优先级:ID 越小,总线优先级越高,同时发数据时先抢到总线;
  2. 接收过滤:接收设备通过过滤器匹配 ID,决定要不要收这个报文;
  3. 数据分类:不同 ID 代表不同类型的数据,比如温度数据用 0x123,报警数据用 0x245,方便区分。

五、总线仲裁机制:多个设备同时发数据,谁说了算?

这是 CAN 总线最核心的设计之一,新手一定要搞懂,否则不理解为什么 CAN 能实现多主控制。

核心仲裁规则(2 条就够)

  1. 总线空闲的时候,谁最先开始发数据,谁就先拿到发送权,一旦开始发送,其他设备就抢不走了;
  2. 多个设备同时开始发数据,从仲裁段的第一位开始,逐位做线与比较,连续输出显性电平(逻辑 0)最多的设备,优先级更高,仲裁获胜的继续发数据,失利的立刻停止发送,转为接收状态。

举个新手能看懂的例子

比如总线上两个设备,设备 1 发 ID=0x01(二进制 00000000001),设备 2 发 ID=0x10(二进制 00001000000),同时开始发送:

1.        前 3 位,两个设备发的都是 0,总线电平是 0,都继续发;

2.        第 4 位,设备 1 发 0,设备 2 发 1,根据线与逻辑,总线电平是 0;

3.        设备 2 发现自己发的是 1,总线却是 0,知道自己仲裁输了,立刻停止发送,转为接收;

4.        设备 1 仲裁获胜,继续把剩下的报文完整发出去。

整个过程完全是硬件自动完成的,不用我们软件干预,而且优先级高的报文不会有任何损坏和延迟,这就是 CAN 的非破坏性仲裁。

六、位时序与波特率

波特率就是 CAN 总线的通信速度,两个设备要通信,波特率必须完全一致,否则绝对收不到数据。很多新手调 CAN,第一步就卡在波特率配置不对,这里我给大家讲明白底层逻辑,再给现成的配置参数,直接用就行。

核心基础:位时序

CAN 总线上的 1 个数据位,不是一个简单的高低电平,而是分成了 4 个段,每个段由最小时间单位Tq(时基单元) 组成,这个就叫位时序。

4 个分段分别是:

  1. 同步段(SS):固定 1Tq,用来让总线上的设备同步时序,信号跳变沿最好落在这个段里;
  2. 传播时间段(PTS):1~8Tq,用来吸收总线的物理传输延迟;
  3. 相位缓冲段 1(PBS1):1~8Tq,用来补偿信号边沿的偏差;
  4. 相位缓冲段 2(PBS2):2~8Tq,用来吸收设备的时钟误差。

重点提醒(STM32):在 STM32 的寄存器配置里,同步段 SS 固定 1Tq,不能配置;传播时间段 PTS 和相位缓冲段 PBS1,会合并成一个参数BS1(TS1),PBS2 就是BS2(TS2),我们配置波特率,只需要配 BS1、BS2 和分频系数就行。

波特率计算公式

  1. 计算 1 个 Tq 的时长:TQ = 1 / (CAN控制器时钟频率 / 分频系数BRP)
  2. 计算 1 个位的总时长(位周期):位周期 = (1 + BS1 + BS2) × TQ = 总TQ数 × TQ
  3. 最终波特率:波特率 = 1 / 位周期 = CAN控制器时钟频率 / (总TQ数 × 分频系数BRP)

七、STM32 bxCAN 实操:从初始化到收发数据

一、先了解 bxCAN 的核心硬件特性

  1. 支持 CAN2.0A/2.0B 主动模式,最高波特率 1Mbps;
  2. 有 3 个发送邮箱,2 个 3 级深度的接收 FIFO;
  3. 最多 28 个可配置的过滤器组(STM32F1 非互联型只有 14 个),双 CAN 共用过滤器组;
  4. 支持正常模式、环回模式、静默模式,环回模式可以自发自收,用于自测。

二、3 种常用工作模式

  1. 初始化模式:用来配置 CAN 的参数,波特率、过滤器都要在这个模式下配置;
  2. 正常模式:正常的双机 / 多机通信模式,能发能收;
  3. 环回模式:自己发的数据自己收,不用接外部硬件,用来自测功能。

三、初始化配置流程(STM32F4标准库)

/*
 * 函数名称:Can_Config
 * 功能描述:CAN 控制器初始化配置
 * 参数说明:
 *   - ts1: 时间段 1(Bit Segment 1),取值范围 CAN_BS1_1tq 到 CAN_BS1_16tq
 *   - ts2: 时间段 2(Bit Segment 2),取值范围 CAN_BS2_1tq 到 CAN_BS2_8tq
 *   - psc: 预分频系数 (Prescaler),实际分频值为 psc-1
 *   - mode: CAN 工作模式,可选值:
 *          CAN_Mode_Normal - 正常通信模式
 *          CAN_Mode_Loopback - 环回测试模式
 *          CAN_Mode_Silent - 静默模式
 *          CAN_Mode_Silent_Loopback - 静默环回模式
 * 返回值:无
 * 硬件连接说明:
 *   - CAN_TX 引脚:PA12(复用推挽输出)
 *   - CAN_RX 引脚:PA11(复用输入)
 * 波特率计算公式:
 *   CAN 波特率 = CAN 时钟频率 / (预分频系数 × (1 + TS1 + TS2))
 *   示例:42MHz / (6 × (1 + 7 + 6)) = 42MHz / 84 = 500Kbit/s
 *   其中:SYNC_SEG=1(固定), TS1=7, TS2=6, BRP=6
 */
void Can_Config(u8 ts1, u8 ts2, u8 psc, u8 mode)  
{
        GPIO_InitTypeDef gpio_InitTypeDef = {0};  
        CAN_InitTypeDef can_InitTypeDef = {0};  
        CAN_FilterInitTypeDef can_FilterInitTypeDef = {0};  
        RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);  
        RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);  

        gpio_InitTypeDef.GPIO_Mode = GPIO_Mode_AF;  // 配置 GPIO 模式为复用功能模式
        gpio_InitTypeDef.GPIO_OType = GPIO_OType_PP;  // 配置 GPIO 输出类型为推挽输出
        gpio_InitTypeDef.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_12;  
        gpio_InitTypeDef.GPIO_Speed = GPIO_High_Speed;  

        GPIO_PinAFConfig(GPIOA, GPIO_PinSource11, GPIO_AF_CAN1);  // 配置 PA11 引脚复用功能为 CAN1,将 PA11 映射到 CAN1 的 RX
        GPIO_PinAFConfig(GPIOA, GPIO_PinSource12, GPIO_AF_CAN1);  // 配置 PA12 引脚复用功能为 CAN1,将 PA12 映射到 CAN1 的 TX

        GPIO_Init(GPIOA, &gpio_InitTypeDef);  

        can_InitTypeDef.CAN_ABOM = ENABLE;  // 自动离线管理使能,总线错误后自动恢复在线
        can_InitTypeDef.CAN_AWUM = DISABLE;  // 自动唤醒模式禁用,禁用后需软件手动唤醒睡眠模式
        can_InitTypeDef.CAN_BS1 = ts1;  // 时间段 1(BS1)的值,范围 1-16 个时间量子
        can_InitTypeDef.CAN_BS2 = ts2;  // 时间段 2(BS2)的值,范围 1-8 个时间量子
        can_InitTypeDef.CAN_Mode = mode;  // CAN 工作模式(正常/环回/静默/静默环回)
        can_InitTypeDef.CAN_NART = ENABLE;  // 禁止自动重传使能,发送失败时不自动重传,只发送一次
        can_InitTypeDef.CAN_Prescaler = psc - 1;  // CAN 预分频系数,实际分频值为 psc-1,范围 1-1024
        can_InitTypeDef.CAN_RFLM = DISABLE;  // 接收 FIFO 锁定模式禁用,FIFO 满时新消息覆盖旧消息
        can_InitTypeDef.CAN_SJW = DISABLE;  // 同步跳转宽度禁用,使用默认值(1 个时间量子)
        can_InitTypeDef.CAN_TTCM = DISABLE;  // 时间触发通信模式禁用,使用普通 CAN 通信模式
        can_InitTypeDef.CAN_TXFP = DISABLE;  // 发送 FIFO 优先级禁用,按请求顺序发送而非优先级

        CAN_Init(CAN1, &can_InitTypeDef);  


        can_FilterInitTypeDef.CAN_FilterActivation = ENABLE;  
        can_FilterInitTypeDef.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;  // 滤波器分配给 FIFO0,匹配该滤波器的消息存到 FIFO0
        can_FilterInitTypeDef.CAN_FilterIdHigh = 0x0000;  // 滤波器 ID 高 16 位为 0x0000,与低 16 位组成 32 位 ID
        can_FilterInitTypeDef.CAN_FilterIdLow = 0x0000;  // 滤波器 ID 低 16 位为 0x0000,32 位全 0 表示匹配所有 ID
        can_FilterInitTypeDef.CAN_FilterMaskIdHigh = 0x0000;  // 滤波器屏蔽位 ID 高 16 位为 0x0000,屏蔽位为 0 表示不检查对应位
        can_FilterInitTypeDef.CAN_FilterMaskIdLow = 0x0000;  // 滤波器屏蔽位 ID 低 16 位为 0x0000,32 位全 0 表示所有位都不检查
        can_FilterInitTypeDef.CAN_FilterMode = CAN_FilterMode_IdMask;  // 滤波器模式为屏蔽位模式,使用 ID 和屏蔽位进行匹配
        can_FilterInitTypeDef.CAN_FilterNumber = 0;  //STM32F4 有多个滤波器,这里使用第 0 号
        can_FilterInitTypeDef.CAN_FilterScale = CAN_FilterScale_32bit;  // 滤波器尺度为 32 位,一个 32 位滤波器或两个 16 位滤波器

        CAN_FilterInit(&can_FilterInitTypeDef);  


}
配置示例

比如我们要接收所有标准 ID 的报文,最简单的配置:32 位屏蔽位模式,过滤器 ID 设为 0x0000,屏蔽值设为 0x0000,意思就是所有位都不用匹配,所有报文都能通过,先这么配,能收到数据了,再慢慢调精准过滤。

四、数据发送流程


/*
 * 函数名称:CAN_Send_Data
 * 功能描述:CAN 总线数据发送函数
 * 参数说明:
 *   - msg: 指向待发送数据的指针,数据长度由 len 指定
 *   - len: 待发送数据的长度,范围 0-8 字节(CAN 帧最大数据长度)
 * 返回值:
 *   - 1: 发送失败(超时或错误)
 *   - 0: 发送成功
 * 注意事项:
 *   - 使用标准帧格式(11 位 ID)
 *   - 固定使用 ID=0x12
 *   - 使用数据帧(非远程帧)
 */
u8 CAN_Send_Data(u8 *msg, u8 len)  
{
        CanTxMsg TxMsg = {0};  // 定义 CAN 发送消息结构体并初始化为 0,包含 ID、数据长度、数据等字段
        u8 i, mag;  // 定义循环变量 i 和发送邮箱索引 mag(0-2)
        u16 cnt = 0;  // 定义超时计数器 cnt,用于检测发送超时,16 位无符号整数
        
        for(i = 0; i < len; i++)  
        {
                TxMsg.Data[i] = msg[i];  // 逐字节复制数据到发送缓冲区,
        }

        TxMsg.DLC = len; 
        TxMsg.ExtId = 0x00;  // 设置扩展 ID 为 0x00,标准帧不使用扩展 ID
        TxMsg.IDE = CAN_Id_Standard;  // 设置标识符类型为标准 ID,CAN_Id_Standard 表示使用 11 位标准帧
        TxMsg.RTR = CAN_RTR_Data;  // 设置远程发送请求为数据帧,CAN_RTR_Data 表示这是数据帧
		//         TxMsg.StdId = 0x12;  // 设置标准 ID 为 0x12,11 位标准标识符,用于标识消息类型和优先级

        mag = CAN_Transmit(CAN1, &TxMsg);  // 启动 CAN 发送,将消息提交到发送邮箱,返回邮箱号(0-2)
        
        while(CAN_TransmitStatus(CAN1, mag) == CAN_TxStatus_Failed && cnt < 0xfff)
                // CAN_TxStatus_Failed 表示发送失败,cnt<0xfff 防止无限循环(超时保护
        {
                cnt++; 
        }
        
        if(cnt >= 0xfff) 
        {
                return 1;  // 返回 1 表示发送失败
        }
        
        return 0;  // 返回 0 表示发送成功
}

五、数据接收流程

/*
 * 函数名称:CAN_Rev_Data
 * 功能描述:CAN 总线数据接收函数
 * 参数说明:
 *   - msg: 指向接收缓冲区的指针,用于存储接收到的数据
 * 返回值:
 *   - 0: 未接收到消息(FIFO 为空)
 *   - 其他值:接收到的数据长度(1-8 字节)
 * 注意事项:
 *   - 从 FIFO0 读取数据
 *   - 轮询方式检查,非中断方式
 */
u8 CAN_Rev_Data(u8 *msg)  
{
        u8 len = 0, i = 0;  
        CanRxMsg RxMsg = {0};  // 定义 CAN 接收消息结构体并初始化为 0,包含 ID、数据长度、数据等字段
        
        if(CAN_MessagePending(CAN1, CAN_FIFO0) == 0)  
                	
                // ==0 表示没有接收到新消息
        {
                return 0;  
        }
        
        CAN_Receive(CAN1, CAN_FIFO0, &RxMsg);  // 从 FIFO0 读取一条 CAN 消息,将消息内容复制到 RxMsg 结构体中

        len = RxMsg.DLC;  // 获取接收到的数据长度,DLC 字段表示数据字节数(0-8)
        
        for(i = 0; i < len; i++)  
        {
                msg[i] = RxMsg.Data[i];  // 逐字节复制数据到用户缓冲区
        }
        
        return len;  // 返回实际接收到的数据长度
}

建议

  1. 先用环回模式,把自发自收调通,确认代码、波特率配置没问题,再接第二个设备做双机通信,最后做多节点测试,一步步来;
  2. 通信不上,用示波器抓 CAN_H 和 CAN_L 的波形,看是没发出来,还是波特率不对,还是硬件接线有问题。

以上就是我学习 CAN 总线的笔记,后续会继续更新modbus协议&RS485的学习内容,欢迎大家一起交流学习,有不对的地方也欢迎大家指正。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐