CAN总线入门指南:从原理到实践

1 CAN通信基础概述

CAN(Controller Area Network)是一种串行通信协议,由德国BOSCH公司于1986年专门为汽车分布式控制系统开发。它最初的目标是减少汽车中的线束数量,降低整车重量和成本。经过30多年的发展,CAN已经成为汽车电子领域的标准总线,并广泛应用于工业自动化、医疗设备、智能建筑等领域。

1.1 网络拓扑与特点

CAN网络采用总线型拓扑结构,所有节点并联在一对双绞线上。这种结构具有以下特点:

  1. 分布式网络:没有主从关系,任何节点都可以在总线空闲时发送数据
  2. 多点广播:一个节点发送的消息可以被总线上所有节点接收
  3. 实时性强:采用事件触发机制,响应速度快
  4. 错误检测能力强:具有5种错误检测机制,出错率低至10^-11
  5. 网络柔性好:可以在不影响其他节点的情况下添加或移除节点

1.2 通信速率与距离

CAN总线的通信速率与传输距离成反比:

  • 1Mbps:最大总线长度约40米
  • 500kbps:最大总线长度约100米
  • 250kbps:最大总线长度约250米
  • 125kbps:最大总线长度约500米
  • 10kbps:最大总线长度可达10千米

这种速率与距离的关系主要受信号传播延迟和位定时要求的影响。在实际应用中,需要根据具体需求选择合适的通信速率。

1.3 消息类型

CAN协议定义了四种类型的帧:

  1. 数据帧:用于传输实际数据,最多可传输8字节
  2. 远程帧:用于请求其他节点发送特定ID的数据帧
  3. 错误帧:当检测到总线错误时发送
  4. 过载帧:用于提供额外的延迟时间

1.4 标识符与寻址

CAN使用基于消息的寻址方式,而不是基于节点的寻址:

  • 标准帧:11位标识符,可表示2048个不同的消息ID
  • 扩展帧:29位标识符,可表示超过5亿个不同的消息ID 标识符不仅用于识别消息,还决定了消息的优先级:标识符数值越小,优先级越高。

1.5 总线访问机制

CAN采用CSMA/CD+AMP(带非破坏性仲裁的载波侦听多路访问)机制:

  1. 载波侦听:节点在发送前检测总线是否空闲
  2. 多路访问:允许多个节点同时开始发送
  3. 冲突检测:通过位仲裁机制解决冲突
  4. 非破坏性仲裁:优先级高的消息继续发送,优先级低的自动退出

1.6 数据一致性

CAN网络确保所有节点收到的数据一致性通过以下机制:

  1. 同步机制:硬同步和再同步确保所有节点位定时一致
  2. 位填充:每5个相同极性位后插入一个相反极性位
  3. CRC校验:发送端计算并发送CRC,接收端验证
  4. 应答机制:所有正确接收消息的节点都要发送应答位

1.7 应用层协议

在CAN基础协议之上,还有多种应用层协议:

  • CANopen:主要用于工业自动化领域
  • J1939:用于商用车辆
  • DeviceNet:用于工业设备互联
  • NMEA 2000:用于船舶电子设备 这些协议定义了更高层的通信规则,使得不同厂家的设备能够互相通信。

1.8 CAN控制器结构

典型的CAN控制器包含以下功能模块:

  1. 协议控制逻辑:处理CAN协议相关的操作
  2. 消息缓冲器:存储待发送和接收到的消息
  3. 位时序逻辑:控制位采样和同步
  4. 错误管理逻辑:处理错误检测和恢复
  5. 滤波器:过滤不需要的消息

2 数据帧格式

CAN通信中最常用的是数据帧,其结构如下:

  1. 帧起始(SOF):1位显性位
  2. 仲裁段:标准格式11位标识符或扩展格式29位标识符
  3. 控制段:6位,包含数据长度码(DLC)
  4. 数据段:0-8字节
  5. CRC段:15位CRC序列和1位界定符
  6. 应答段:2位
  7. 帧结束(EOF):7位隐性位

2.1 起始位

**数据帧是CAN总线通信中最常用的帧类型,用于传输实际的数据信息。**一个完整的数据帧由七个不同的字段组成,它们按照严格的顺序排列,共同构成了信息传输的基本单元。数据帧以帧起始位(SOF)开始,这是一个单独的显性位(逻辑0),用于标志新数据帧的开始,同时也用于节点间的位同步。

2.2 仲裁段

紧接着帧起始位的是仲裁段,它包含了消息的标识符和RTR(远程传输请求)位。 在标准格式中,标识符长度为11位;而在扩展格式中,标识符总长度为29位,由11位基本标识符和18位扩展标识符组成。标识符不仅用于识别消息的内容和来源,还决定了消息在总线竞争中的优先级。RTR位用于区分数据帧(显性位)和远程帧(隐性位)。

2.3 控制段

控制段紧随仲裁段之后,包含IDE(标识符扩展)位、r0(保留位)和4位的DLC(数据长度码)。IDE位用于区分标准格式(显性)和扩展格式(隐性),r0是保留位,必须以显性位发送。DLC表示后续数据段中数据字节的数量,取值范围为0-8。尽管DLC编码允许表示最多15个字节,但CAN协议规定数据长度不得超过8字节。

2.4 数据段

数据段携带实际的数据内容,长度为0-8字节(0-64位)。每个数据字节中的位按照从最高有效位(MSB)到最低有效位(LSB)的顺序传输。数据段的内容完全由应用层决定,CAN协议本身不对数据内容做任何限制或解释。

2.5 CRC段

在数据段之后是CRC段,它由15位的CRC序列和1位CRC界定符组成。CRC序列是根据从帧起始一直到数据段结束的所有先前传输的位计算得出,用于接收节点检测传输错误。CRC界定符始终为隐性位,用于分隔CRC序列和后续的应答段。

2.6 应答段

应答段由两位组成:应答槽位和应答界定符。在应答槽位中,发送节点发送隐性位,而所有成功接收该消息的节点则发送显性位,通过这种方式实现了消息接收的确认。应答界定符始终为隐性位,用于界定应答槽位。

2.7 帧结束

最后是帧结束(EOF)段,由7个连续的隐性位组成,表示数据帧的结束。在EOF之后是帧间隔(IFS),它包含至少3个隐性位,用于分隔相邻的数据帧。这种帧结构设计确保了数据传输的可靠性和完整性。

结构体定义:

cpp 复制代码
struct CANFrame {
    uint16_t id;   // 11位ID
    uint8_t rtr;   // 1位,0或1
    uint8_t dlc;   // 4位,0-8
    uint8_t data[8]; // 数据,最多8字节
};

打包函数:

cpp 复制代码
void packCANFrame(struct CANFrame* frame, uint8_t* buffer) {
    // 检查ID范围
    if(frame->id > 0x7FF) {
        // 处理错误
        return;
    }
    // 检查dlc范围
    if(frame->dlc > 8 || frame->dlc < 0) {
        // 处理错误
        return;
    }
    // 组合仲裁场:ID(11位) + RTR(1位)
    uint16_t arbitration = (frame->id << 1) | frame->rtr;
    // 分成两个字节
    buffer[0] = (arbitration >> 8) & 0xFF; // 高8位
    buffer[1] = arbitration & 0xFF;        // 低8位
    // 控制场:DLC(4位)
    buffer[2] = frame->dlc & 0x0F;
    // 数据场
    for(int i = 0; i < frame->dlc; i++) {
        buffer[3 + i] = frame->data[i];
    }
    // 如果需要,填充剩余数据为0
    for(int i = frame->dlc; i < 8; i++) {
        buffer[3 + i] = 0;
    }
}

解包函数:

cpp 复制代码
void unpackCANFrame(uint8_t* buffer, struct CANFrame* frame) {
    // 从buffer[0]和buffer[1]提取仲裁场
    uint16_t arbitration = (buffer[0] << 8) | buffer[1];
    frame->id = arbitration >> 1;
    frame->rtr = arbitration & 0x01;
    // 提取DLC
    frame->dlc = buffer[2] & 0x0F;
    // 提取数据
    for(int i = 0; i < frame->dlc; i++) {
        frame->data[i] = buffer[3 + i];
    }
}

测试函数:

cpp 复制代码
void testCANFrame() {
    struct CANFrame frame;
    frame.id = 0x123;
    frame.rtr = 0;
    frame.dlc = 3;
    frame.data[0] = 0x55;
    frame.data[1] = 0xAA;
    frame.data[2] = 0xFF;
    
    uint8_t buffer[11];
    packCANFrame(&frame, buffer);
    
    // 打印打包后的buffer
    printf("Packed buffer: ");
    for(int i = 0; i < 11; i++) {
        printf("%02X ", buffer[i]);
    }
    printf("\n");
    
    // 解包
    struct CANFrame unpacked;
    unpackCANFrame(buffer, &unpacked);
    
    // 打印解包后的帧
    printf("Unpacked Frame:\n");
    printf("ID: %03X, RTR: %d, DLC: %d, Data: ", unpacked.id, unpacked.rtr, unpacked.dlc);
    for(int i = 0; i < unpacked.dlc; i++) {
        printf("%02X ", unpacked.data[i]);
    }
    printf("\n");
}

3 优先级仲裁机制

CAN总线采用非破坏性总线仲裁机制:

  • 当多个节点同时发送消息时,通过标识符进行仲裁
  • 标识符越小,优先级越高
  • 显性位(0)优先级高于隐性位(1)

CAN总线采用一种独特的非破坏性总线仲裁机制,这种机制基于CSMA/CD+AMP(带优先级的载波侦听多路访问/冲突检测)原理 。在此机制中,较低数值的标识符具有较高的优先级,这是通过位优先的概念来实现的:显性位(逻辑0)的优先级高于隐性位(逻辑1)。当多个节点同时开始传输时,它们会在发送每一位的同时监听总线电平,通过比较自己发送的位值和总线实际电平来参与仲裁过程。

3.1 仲裁过程

仲裁过程发生在数据帧的仲裁段,主要包括标识符位和RTR位。 当多个节点同时发送时,它们会逐位比较各自的标识符。如果某个节点发送隐性位(1),而在总线上检测到显性位(0),说明有其他节点正在发送更高优先级的消息,此节点将立即停止发送,转为接收模式。而发送显性位并检测到显性位的节点则继续参与仲裁。通过这种方式,优先级最高的消息将"赢得"总线访问权,并继续完成其传输过程。

3.2 仲裁机制

这种仲裁机制的一个重要特点是非破坏性,即不会损失任何总线带宽。失去仲裁的节点会自动停止发送,而无需重发已发送的内容。更重要的是,优先级最高的消息总能在第一时间发送出去,这对于实时控制系统来说尤为重要。当一个节点在仲裁过程中失败后,它会等待当前消息传输完成,然后在总线再次空闲时重新尝试发送。

在实际应用中,**标识符的分配需要综合考虑消息的重要性和时间紧迫性。**例如,发动机转速、制动系统状态等关键信息应该分配较低的标识符值,以确保这些消息能够优先传输。而诸如车窗位置、空调温度等非关键信息则可以分配较高的标识符值。此外,还需要考虑避免标识符冲突,确保每个消息都有唯一的标识符。

CAN的优先级仲裁机制还具有良好的可扩展性。在标准帧格式中,11位标识符可以定义2048个不同优先级的消息;而在扩展帧格式中,29位标识符更是提供了超过5亿个优先级层次。这种大范围的优先级划分使得系统能够非常灵活地处理各种实时性要求的消息。

值得注意的是,虽然优先级仲裁机制能够确保高优先级消息的及时传输,但也可能导致低优先级消息的饥饿问题。如果高优先级消息过于频繁,低优先级消息可能长时间无法获得发送机会。因此,在系统设计时需要合理规划消息的发送频率和优先级分配,确保所有消息都能在可接受的时间范围内完成传输。

简单的CAN发送示例代码:

cpp 复制代码
// CAN消息结构体
typedef struct {
    uint32_t StdId;      // 标准标识符
    uint32_t ExtId;      // 扩展标识符
    uint8_t IDE;         // 标识符类型
    uint8_t RTR;         // 远程传输请求
    uint8_t DLC;         // 数据长度
    uint8_t Data[8];     // 数据
} CAN_Message;

// 发送CAN消息
void CAN_SendMessage(CAN_Message* msg) {
    // 等待发送邮箱空闲
    while(CAN_GetTxMailboxFreeLevel() == 0);
    
    // 配置发送邮箱
    CAN_TxHeaderTypeDef header;
    header.StdId = msg->StdId;
    header.ExtId = msg->ExtId;
    header.IDE = msg->IDE;
    header.RTR = msg->RTR;
    header.DLC = msg->DLC;
    
    // 发送数据
    uint32_t mailbox;
    HAL_CAN_AddTxMessage(&hcan1, &header, msg->Data, &mailbox);
}

4 错误处理机制

CAN协议设计了一套完整而强大的错误处理机制,包括错误检测、错误标志、错误计数以及节点状态管理。这套机制能够确保数据传输的可靠性,使得CAN网络即使在恶劣的电磁环境下也能保持稳定工作。CAN协议实现了五种不同的错误检测方法,它们共同构成了一个全面的错误检测体系。

4.1 位错误检测

位错误检测是最基本的错误检测方式,发送节点会持续监控总线电平。除了仲裁段和应答段外,如果节点检测到的总线电平与其发送的电平不同,就会产生位错误。这种机制可以有效检测出由电磁干扰或硬件故障导致的信号失真。在实际应用中,位错误检测对于发现总线上的电气故障特别有效。

4.2 填充错误检测

填充错误检测基于CAN协议的位填充规则。在发送过程中,当连续出现5个相同极性的位时,需要自动插入一个相反极性的位。接收节点会检查这个规则是否得到遵守,如果发现连续6个相同极性的位,就会产生填充错误。这种机制不仅保证了数据流中有足够的电平跳变用于同步,还能够检测出位丢失或额外位插入等错误。

4.3 CRC错误检测

CRC错误检测**使用15位的循环冗余校验码,用于检测传输过程中的数据改变。**发送节点会根据从帧起始到数据段的所有位计算出CRC序列,接收节点则通过相同的算法重新计算并比较。如果计算结果不匹配,就会产生CRC错误。这种校验方法能够发现绝大多数的数据篡改错误。

4.4 帧格式错误检测

帧格式错误检测**通过检查固定格式的位(如CRC界定符、ACK界定符和帧结束标志等)来实现。**这些位必须是特定的值(通常是隐性位),如果检测到非预期的值,就会产生帧格式错误。这种检测方法可以发现帧结构被破坏的情况。

4.5 应答错误检测

应答错误检测发生在数据帧或远程帧的应答槽位。**如果发送节点在应答槽位未能检测到任何接收节点的应答(显性位),就会产生应答错误。**这表明没有节点正确接收到消息,可能是由于通信故障或者接收节点故障导致的。

4.6 应答过程

当检测到错误时,节点会立即发送错误帧,中断当前的数据传输。错误帧由6-12个连续的显性位组成的错误标志和8个连续的隐性位组成的错误界定符构成。这种结构必然会违反位填充规则,因此能够被所有节点识别为错误帧,确保所有节点都意识到传输错误的发生。

每个CAN节点都维护着两个错误计数器:发送错误计数器(TEC)和接收错误计数器(REC)。这些计数器的值会根据检测到的错误类型和节点状态进行调整 。基于这些计数器的值,节点可能处于三种状态之一**:错误主动、错误被动或总线关闭**。在错误主动状态下,节点正常参与总线通信;在错误被动状态下,节点需要等待额外的时间才能发送;在总线关闭状态下,节点将完全停止发送。

5 波特率计算

CAN总线波特率计算公式:

Baud Rate = Fclk / (Prescaler * (1 + TS1 + TS2))

其中:
Fclk: CAN控制器时钟频率
Prescaler: 分频系数
TS1: 时间段1
TS2: 时间段2

采样点位置一般位于位时间的75%-80%处,可以通过调整TS1和TS2来实现。

6 滤波器配置

CAN控制器通常具有硬件滤波器,用于过滤不需要的消息:

cpp 复制代码
void CAN_ConfigFilter(void) {
    CAN_FilterTypeDef filter;
    filter.FilterIdHigh = 0x0000;
    filter.FilterIdLow = 0x0000;
    filter.FilterMaskIdHigh = 0x0000;
    filter.FilterMaskIdLow = 0x0000;
    filter.FilterScale = CAN_FILTERSCALE_32BIT;
    filter.FilterFIFOAssignment = CAN_FILTER_FIFO0;
    filter.FilterBank = 0;
    filter.FilterMode = CAN_FILTERMODE_IDMASK;
    filter.FilterActivation = ENABLE;
    
    HAL_CAN_ConfigFilter(&hcan1, &filter);
}

7 总线终端电阻

为了防止信号反射,CAN总线两端需要接入120Ω终端电阻。在实际应用中,建议使用金属膜电阻,以确保阻值稳定性。总线特性阻抗计算公式:

Z = √(L/C)

其中:
Z: 特性阻抗
L: 单位长度电感
C: 单位长度电容

8 性能优化建议

8.1 波特率与采样点优化

在CAN通信中,波特率配置直接影响系统性能。选择波特率时需要综合考虑以下因素:

  1. 总线长度:随着总线长度增加,信号传播延迟增大,需要相应降低波特率
  2. 采样点位置:建议将采样点设置在位时间的75%-80%处,这样可以:
    • 为信号传播和电平稳定预留足够时间
    • 提供足够的余量应对相位抖动
  3. 时间量化:尽量使用较小的时间量化单位,提高定时精度
  4. 重同步跳跃宽度:建议设置为1-4个时间量化单位,平衡抗干扰能力和同步性能

8.2 标识符分配优化

合理的标识符分配策略对提升网络性能至关重要:

  1. 优先级分层
    • 紧急消息(如安全相关):最高优先级,0-127
    • 实时控制消息:次高优先级,128-511
    • 参数数据:中等优先级,512-1023
    • 配置数据:较低优先级,1024-1899
    • 诊断数据:最低优先级,1900-2047
  2. 消息分组
    • 按功能模块分组,便于管理和维护
    • 相关消息使用连续的标识符,提高可读性
    • 预留适当空间以便future扩展
  3. 发送频率管理
    • 高优先级消息的发送周期不宜过短
    • 错开不同消息的发送时间,避免总线拥塞
    • 实现消息的周期性调度,平衡总线负载

8.3 硬件设计优化

高质量的硬件设计是性能优化的基础:

  1. 线缆选择
    • 使用符合ISO 11898标准的双绞线
    • 铜芯截面积建议不小于0.34mm²
    • 特性阻抗保持在108-132Ω范围内
    • 使用屏蔽线缆,提高抗干扰能力
  2. 网络拓扑
    • 严格遵循总线型拓扑,避免星型或环型连接
    • 支线长度不超过0.3米
    • 节点间距不小于0.1米
    • 终端电阻采用1%精度的120Ω电阻

8.4 电磁兼容性优化

EMC设计对系统稳定性至关重要:

  1. PCB设计
    • CAN收发器靠近连接器布置
    • 差分线对等长布线,减小共模干扰
    • 适当使用接地层隔离,避免串扰
    • 关键信号远离高频干扰源
  2. 隔离设计
    • 必要时使用光耦或数字隔离器
    • 电源隔离,避免地环流
    • 提供足够的抗静电保护
    • 合理布置TVS管等保护器件

8.5 滤波器优化

合理配置滤波器可显著减轻处理器负担:

  1. 掩码模式
    • 使用掩码过滤特定组消息
    • 根据应用需求设计过滤规则
    • 充分利用硬件滤波能力
  2. 列表模式
    • 针对离散的标识符进行过滤
    • 最大化利用滤波器数量
    • 定期更新过滤规则适应系统变化

8.6 软件优化

软件层面的优化同样重要:

  1. 中断处理
    • 使用FIFO缓冲,避免数据丢失
    • 中断服务程序尽量简短
    • 适当使用消息队列机制
  2. 数据打包
    • 相关数据合并发送,减少帧数
    • 根据数据变化频率调整发送策略
    • 实现数据压缩,提高带宽利用率
  3. 错误处理
    • 实现错误恢复机制
    • 记录错误日志便于诊断
    • 合理设置错误计数阈值

8.7 调试与监控

为持续优化性能,需要建立完善的监控体系:

  1. 总线负载监控
    • 实时监测总线利用率
    • 记录峰值负载情况
    • 分析消息统计数据
  2. 错误监控
    • 跟踪错误频率和类型
    • 分析错误模式
    • 建立预警机制
  3. 性能指标
    • 监控消息延迟
    • 统计丢包率
    • 评估系统实时性
相关推荐
深圳市青牛科技实业有限公司 小芋圆2 小时前
GC8872 是一款带故障报告功能的刷式直流电机驱动芯片, 适用于打印机、电器、工业设备以及其他小型机器。
人工智能·科技·stm32·单片机·嵌入式硬件·机器人
数维学长9864 小时前
C++ STL 中的 vector 总结
开发语言·c++
7yewh5 小时前
【LeetCode】力扣刷题热题100道(26-30题)附源码 轮转数组 乘积 矩阵 螺旋矩阵 旋转图像(C++)
c语言·数据结构·c++·算法·leetcode·哈希算法·散列表
白鹭float.7 小时前
【OpenGL/C++】面向对象扩展——测试环境
c++·图形学·opengl
冰糖雪莲IO7 小时前
【江协STM32】9-4/5 USART串口数据包、串口收发HEX数据包&串口收发文本数据包
网络·stm32·嵌入式硬件
小wanga7 小时前
【C++】类型转换
jvm·c++
无聊到发博客的菜鸟7 小时前
STM32中的MCO
stm32·单片机·嵌入式硬件
Echo_cy_7 小时前
STM32 I2C通信外设
stm32·单片机·嵌入式硬件
半个番茄7 小时前
STM32 : PWM 基本结构
stm32·单片机·嵌入式硬件