从沙子到车辙(4.3):板级通信——CAN / CAN-FD

4.3 板级通信:CAN / CAN-FD

📚 本文内容摘自本人的开源书《从沙子到车辙 - 一个工程师的理解》

🔗 在线阅读/下载:from-sand-to-ruts

bash 复制代码
git clone https://github.com/Lularible/from-sand-to-ruts

⭐ 如果对您有帮助,欢迎 Star 支持,也欢迎通过 GitHub Issues 交流讨论。

1983年,博世工程师的"够了"

1983年。奔驰W126的线束,超过了50公斤。

发动机控制、ABS、仪表盘、空调、电动窗------每个新增的电子功能,都要拉一对信号线到另一个ECU。线束像藤蔓一样在车身里疯长。博世的工程师算了一笔账:照这个趋势,到1990年,豪华车的线束会吃掉整车成本的15%。

他们说:够了。我们需要一条总线。

不是更快,不是更大------是"够用"。一对双绞线,把所有ECU挂上去。带宽1Mbps就够了------当时最复杂的实时信号也不过几百个字节每秒。节点成本必须极低------每个ECU的MCU只有几KB RAM,没有处理复杂协议栈的余力。

他们的答案是CAN(Controller Area Network)。1986年首次在SAE大会上发表。今天,地球上每辆车出厂时都装着至少一条CAN总线。

1983年的博世工程师递出了一根接力棒。40年后,你还在用它。你写的每一行CAN驱动代码,都在接过这根棒继续跑。

显性能覆盖隐性------这是物理层的核心秘密

CAN用一对差分线:CAN_H和CAN_L。总线两端各接120Ω终端电阻。

发送"显性"位(逻辑0):收发器驱动CAN_H到约3.5V、CAN_L到约1.5V。差分电压ΔV≈2V。收发器的驱动能力典型值是能向60Ω负载(两个120Ω终端并联+线缆损耗)注入至少1.5V差分电压------相当于至少25mA的驱动电流。

发送"隐性"位(逻辑1):收发器释放总线。两个120Ω终端电阻并联等效60Ω,把CAN_H和CAN_L都拉到约2.5V(VCC/2,通常VCC=5V)。差分电压ΔV≈0V。

关键是:显性能覆盖隐性。 如果有两个节点同时发,一个发显性(0)另一个发隐性(1),总线上一定是显性。这不是协议规则------是物理定律。差分驱动器主动向60Ω负载注入电流,电流在终端电阻上产生电压降------这是欧姆定律。终端电阻的被动上拉无法对抗主动驱动。

这个物理特性让CAN实现了CSMA/CR ------载波侦听多路访问/冲突解决。与以太网的CSMA/CD不同:CAN在冲突时不丢数据、无退避延时。仲裁失败者自动退出发送、转为接收。帧不损坏。这是CAN区别于所有其他总线的核心特性------它让总线利用率在理论上可以接近100%(实际工程中通常<60%以达到可接受的延时)。

差分信号的物理直觉

为什么用差分信号?因为汽车是一个电磁地狱。

火花塞放电------击穿电压10-30kV,放电电流可达安培级,上升时间小于1ns。PWM驱动的电机------di/dt可达1A/ns。大电流在车身金属结构上产生磁场,磁通变化耦合到任何附近的导体上。

单端信号(如SPI的MOSI------一根线对地): 外部磁场在信号线上感应的噪声电压直接叠加在信号上。如果噪声幅度超过VIL/VIH阈值------bit错误。

差分信号(CAN_H和CAN_L): 外部电磁场在两根线上感应出几乎相等的共模噪声。因为两根线绞在一起,在空间的每个点上它们到噪声源的距离几乎相等。差分接收器只关心CAN_H - CAN_L的差值------共模噪声被抵消。这个抵消的程度用CMRR(共模抑制比)衡量:CAN收发器的CMRR>60dB(1000倍),即1V的共模噪声在差分输出端只有1mV残差。汽车级的CAN收发器通常>70dB。

双绞线的绞合频率也经过了精心设计。 典型的CAN线束每英寸绞合2-4次。如果绞合太密------线太硬,成本高。如果绞合太疏------共模抑制效果差。2-4次/英寸是在抗扰度和机械柔性之间的工程最优。

终端电阻为什么是120Ω? 因为典型的CAN双绞线特征阻抗约120Ω。终端电阻的值必须等于线缆的特征阻抗------否则信号到达线缆末端时产生反射。反射波叠加在原始信号上,产生过冲或台阶。CAN仲裁依赖所有节点在同一bit时间内看到一致的总线电平------反射导致的振铃会破坏仲裁的确定性。120Ω不是"某种标准规定的"------它是线缆的物理属性规定的。

共模扼流圈(Common Mode Choke)。 在严重EMI环境中(如发动机舱内),CAN收发器和总线之间会串一个共模扼流圈。它由两个绕在同一磁芯上的线圈组成------差模信号(CAN_H-CAN_L)产生的磁通互相抵消,扼流圈呈现低阻抗。共模噪声(CAN_H和CAN_L同向)产生的磁通互相叠加,扼流圈呈现高阻抗(通常>500Ω在1-50MHz范围),有效衰减共模噪声。这使得CAN在发动机舱里------离火花塞不到30cm------照样稳定通信。

ID仲裁:一个字段,两个使命

CAN帧里有一个11位(标准帧)或29位(扩展帧)的ID域。它同时做两件事:

一、标识帧的内容。 协议本身不规定ID的含义------这是OEM自定义的。但行业惯例是:每个CAN ID对应一组特定的信号。ID 0x3E8 = 发动机状态帧(包含转速、冷却液温度、节气门位置等),ID 0x180 = 轮速帧(四个轮子的速度)。

二、仲裁优先级。 在仲裁阶段,所有待发节点同时往外推自己的ID。每一位送出后,立刻回读总线的实际电平。如果自己送的是隐性(1)但回读到显性(0)------说明有另一个节点在送优先级更高的ID(ID更小,0比1优先)------立即退出,转为监听模式。

用一个具体例子来说。

节点A要发送ID=0x3E8(= 01111101000b)。节点B要发送ID=0x180(= 00110000000b)。

位序 节点A 节点B 总线实际 结果
10(SOF后第1位) 0 0 0 两位都是0(显性),都继续
9 1 0 0 A发1(隐性),B发0(显性) → B的显性覆盖A的隐性 → 总线是0。A回读看到0,但自己发的是1------A仲裁失败,立即退出。
8 --- 1 1 A已退出。B继续发1。
... --- ... ... B完全不受阻碍地发送整个帧。

整个过程在ID域的前几位内完成。没有帧被"撞坏"------A只是发现自己优先级不够,主动让路。B甚至不知道有人和自己竞争。这就是CSMA/CR的优雅:冲突被消解,而不是被检测后重发。

整个过程在ID域内完成,不损耗任何帧。确定性极强:最高优先级帧的最坏延迟 = 帧长时间 + 3个隐性位的帧间间隔(Intermission)。

这是CAN最天才的设计。 ID域用同一个字段解决了"这是什么数据"和"谁先说"两个问题。没有中央调度器。不需要令牌传递。不需要主站轮询。硬件自己搞定一切。这是信息论级的优雅------一个字段承担了寻址和调度两个正交的语义。

穿透:追踪一个发动机转速信号

你在OEM给的DBC(CAN数据库)文件里看到这一行:

复制代码
BO_ 0x3E8 EMS_1: 8 Engine
  SG_ EngineSpeed : 16|16@0+ (1,0) [0|8000] "rpm" 仪表盘

翻译:CAN ID 0x3E8的帧里,起始位16、长度16位、Motorola格式、无符号、因子1.0、偏移0。范围0-8000rpm。仪表盘接收。

下面是用DBC描述解析CAN信号的完整C函数:

c 复制代码
// DBC 信号解析: 从 CAN 帧的 8 字节中提取一个有符号或无符号整数
// Layout: Intel (little-endian) 或 Motorola (big-endian)
// start_bit: DBC 中的起始位号 (0-indexed, 从 byte 0 bit 0 开始)
// length:    信号长度 (bits)
// is_signed: 1 = 有符号, 0 = 无符号
// is_motorola: 1 = Motorola 格式, 0 = Intel 格式

uint64_t can_extract_signal(const uint8_t data[8],
                            uint8_t start_bit, uint8_t length,
                            uint8_t is_signed, uint8_t is_motorola)
{
    uint64_t raw = 0;
    uint8_t bit_pos = start_bit;

    // 逐位提取
    for (uint8_t i = 0; i < length; i++) {
        uint8_t byte_idx = bit_pos / 8;
        if (byte_idx >= 8) return 0;  // 越界保护

        uint8_t bit_in_byte = 7 - (bit_pos % 8);  // 从 MSB 开始数

        if (data[byte_idx] & (1 << bit_in_byte))
            raw |= (1ULL << i);

        if (is_motorola) {
            // Motorola: 字节内的位从高到低, 字节间地址递增
            // 但跨越字节边界时 "回绕" 到上一字节的 LSB
            if ((bit_pos % 8) == 0) {
                // 刚完成一个字节的 MSB, 跳到下一字节的 LSB
                bit_pos -= 15;  // -8 to next byte, -7 to its LSB
            } else {
                bit_pos++;
            }
        } else {
            // Intel: LSB first, 简单递增
            bit_pos++;
        }
    }

    // 符号扩展 (有符号信号)
    if (is_signed && (raw & (1ULL << (length - 1)))) {
        uint64_t sign_mask = ~((1ULL << length) - 1);
        raw |= sign_mask;
    }

    return raw;
}

// 使用示例: 从 CAN 帧解析发动机转速
void can_rx_callback(uint32_t id, uint8_t *data, uint8_t dlc)
{
    if (id == 0x3E8) {
        // EngineSpeed: start_bit=16, length=16, Motorola, unsigned
        uint64_t raw = can_extract_signal(data, 16, 16, 0, 1);
        float rpm = raw * 1.0f + 0.0f;  // factor=1.0, offset=0.0
        update_tacho_needle(rpm);
    }
}

这段代码的背后,你看不到的地方,发生了什么?

发动机ECU一侧------

应用层: EMS控制软件把当前转速1847 rpm写入CAN发送缓冲区(实际上是一个mailbox的Data字段)。

CAN控制器硬件: 把1847编码为0x0737,填入data2=0x07、data3=0x37。组装帧头:SOF(1位显性)→ ID=0x3E8(11位)=01111101000b→ IDE=0(标准帧)→ R0(保留位)→ DLC=8(4位=1000b)→ 数据8字节→ CRC15→ CRC分隔符→ ACK槽(1位, 发送方发隐性, 接收方拉低表示收到)→ ACK分隔符→ EOF(7位隐性)。接着在位填充(Bit Stuffing):如果连续5个相同位,自动插入一个相反的填充位。接收方自动去除。这是为了确保足够的边沿密度让各节点的PLL时钟恢复能锁定。

位时序: 每一位分成4个时段------Sync段(固定1Tq)、Prop段(补偿总线传播延迟+收发器延迟)、Phase Seg1和Phase Seg2(用于微调采样点位置)。1 Tq(Time Quantum)是CAN控制器的时钟周期。采样点在Phase Seg1和Seg2的交界处------通常设在75%-87.5%位宽处。这是延时和抗噪声的权衡:采样点越靠后,容忍的总线延迟越大(长线缆);采样点越靠前,容忍的信号振动越小(高噪声环境)。

你可以直观地理解这四个段:Sync段是裁判鸣哨------所有节点同时开始。Prop段是"给信号跑路的时间"------信号从总线一头跑到另一头需要时间,在这段时间里不能采样。PS1和PS2是采样窗口------在PS1结束时采样总线电平。如果采样点太早,信号还没稳定;太晚,下一个bit已经开始。CAN的采样点通常设在75%-87.5%的位置------经过大量实车验证的最优区间。

CAN收发器: 把TX引脚的单端逻辑(0/3.3V)转换为差分驱动。显性位(0)→驱动CAN_H到约3.5V、CAN_L到约1.5V。隐性位(1)→释放总线。收发器内部的主要电路是一个波形整形器+推挽输出级(由CANH和CANL两只大功率MOS管驱动)。

物理层: 差分电压在双绞线上传播。信号在双绞线上的传播速度约0.55c-0.65c(c=光速)------因为在FR4 PCB或PVC线缆的介质中,电磁波传播速度 = c/√εr。典型的绝缘材料εr≈3-4,所以速度约0.5c-0.6c。绞合使电气长度略有增加,等效速度约0.55c(约16.5cm/ns)。以此计算,5米长的线缆,信号单向传播延迟约30ns。仲裁需要双向传播------一方发送bit,信号传到另一方,另一方采样后可能同时发送------所以最坏往返延迟约60ns。CAN的位时间必须大于这个往返延迟,否则仲裁失效。这就是为什么1Mbps CAN的最大总线长度约40米、而5Mbps CAN-FD的数据段只能在短总线(<5m)上实现。

一辆5米长的车,信号从车头发动机ECU传到车尾ABS ECU: 5m × 5ns/m ≈ 25ns。这是单向传播延迟。仲裁需要双向------A 发送 bit,信号传到 B,B 采样,B 可能同时发送------所以最坏往返延迟 ≈ 50ns。CAN 的位时间必须大于这个往返延迟,否则仲裁失效。这就是为什么 1Mbps CAN 的最大总线长度约 40 米、而 5Mbps CAN-FD 的数据段只能在短总线(<5m)上实现。

你的ECU一侧(倒序)------

CAN收发器: 接收差分电压。内部比较器的阈值通常设在0.5V-0.9V差分:ΔV>0.9V→显性(0),ΔV<0.5V→隐性(1)。转为单端逻辑送RX引脚。收发器同时进行总线故障保护------检测CAN_H和CAN_L对电源/地的短路,检测显性位持续时间是否超过限值。

CAN控制器硬件: 逐位接收。边收边做CRC校验。如果CRC正确,在ACK槽发送显性位。把完整帧存入硬件RX mailbox,置位接收中断标志。同时检查错误计数器(TEC和REC)------当TEC或REC超过127时进入Error Passive状态,超过255时进入Bus Off状态(自动断开与总线的连接)。这是CAN的故障隔离机制------一个节点不能因为不停地发错误而把整条总线拖垮。

中断服务程序: NVIC(嵌套向量中断控制器)将CPU从主循环中拉出,跳转到CAN接收ISR。ISR读取mailbox------得到ID=0x3E8、DLC=8、data8={...,0x07,0x37,...}。调用你的can_rx_callback。

你的代码: can_extract_signal从data2和data3拼出0x0737=1847。乘以factor 1.0,加offset 0.0。更新仪表盘指针。

从EMS软件写下1847,到仪表盘指针移动------穿越了CAN控制器的位时序状态机、位填充器、CRC校验器、收发器的差分驱动器、双绞线上0.2c传播的电磁场、另一端收发器的比较器、控制器的硬件mailbox、NVIC中断路由、你的回调函数。不到10毫秒。十层硬件,一行C代码。

S32K上FlexCAN的配置与收发

S32K14x上用的是FlexCAN模块。下面是配置CAN通信、发送一帧、接收一帧的完整寄存器级代码:

c 复制代码
// ========== FlexCAN0 初始化 ==========
// S32K144: FlexCAN0 基址 = 0x40024000
#define CAN0_BASE  0x40024000

#define CAN_MCR     (*(volatile uint32_t *)(CAN0_BASE + 0x00))  // Module Config
#define CAN_CTRL1   (*(volatile uint32_t *)(CAN0_BASE + 0x04))  // Control 1
#define CAN_TIMER   (*(volatile uint32_t *)(CAN0_BASE + 0x08))  // Free Running Timer
#define CAN_RXGMASK (*(volatile uint32_t *)(CAN0_BASE + 0x10))  // Rx Global Mask
#define CAN_IFLAG1  (*(volatile uint32_t *)(CAN0_BASE + 0x30))  // Interrupt Flags 1
#define CAN_IMASK1  (*(volatile uint32_t *)(CAN0_BASE + 0x28))  // Interrupt Mask 1

// Mailbox 区域: 每个 MB 4个 32-bit 寄存器 (CS, ID, WORD0, WORD1)
// MB0-MB7 基址 = CAN0_BASE + 0x80
#define MB_CS(n)    (*(volatile uint32_t *)(CAN0_BASE + 0x80 + (n)*0x10 + 0x0))
#define MB_ID(n)    (*(volatile uint32_t *)(CAN0_BASE + 0x80 + (n)*0x10 + 0x4))
#define MB_WORD0(n) (*(volatile uint32_t *)(CAN0_BASE + 0x80 + (n)*0x10 + 0x8))
#define MB_WORD1(n) (*(volatile uint32_t *)(CAN0_BASE + 0x80 + (n)*0x10 + 0xC))

// CAN_CTRL1中的位时间配置
// 设 PE时钟=48MHz → 1 Tq = 1/48MHz ≈ 20.83ns
// 目标 500kbps: 位时间 = 2μs = 96 Tq
// 分配: Sync=1, PropSeg=40(41Tq含Sync), PSEG1=32, PSEG2=22, RJW=22
// 采样点 = (1+40+32)/(1+40+32+22) ≈ 76.8%

void flexcan0_init_750kbps(void)
{
    // 1. 进入冻结模式
    CAN_MCR |= (1 << 24);    // FRZ = 1 (请求冻结)
    CAN_MCR |= (1 << 25);    // HALT = 1 (或MDIS=0保持模块时钟)
    while (!(CAN_MCR & (1 << 24))); // 等待 FRZACK = 1 (已进入冻结)

    // 2. 使能模块 (退出软复位)
    CAN_MCR &= ~(1 << 25);   // 清除 MDIS (使能模块)
    while (CAN_MCR & (1 << 25));   // 等待 LPMACK=0 (退出低功耗)

    // 3. 配置位时序
    //    CTRL1: 只能在冻结模式下修改
    //    PRESDIV=3 → Sclock = 48MHz / (3+1) = 12MHz → Tq=83.33ns
    //    目标: 16 Tq/bit → 12MHz/16 = 750kbps
    // 但实际设置更仔细:
    CAN_CTRL1 = (3 << 0)      // PRESDIV = 3 (Tq=83.33ns @48MHz)
              | (5 << 8)      // PSEG1 = 5 (Phase Seg1=6 Tq)
              | (4 << 12)     // PSEG2 = 4 (Phase Seg2=5 Tq)
              | (3 << 16)     // PROPSEG = 3 (Prop Seg=4 Tq)
              | (3 << 20)     // RJW = 3 (同步跳转宽度=4 Tq)
              | (0 << 22);    // SMP = 0 (单次采样 @采样点)
    // 总 Tq/bit = 1(Sync) + 4(Prop) + 6(PS1) + 5(PS2) = 16
    // 采样点 = (1+4+6)/16 = 68.75%
    // 位率 = 12MHz / 16 = 750kbps

    // 4. 配置 MB0 为接收 (RX), MB1 为发送 (TX)
    //    MB0 接收所有 ID (全局掩码先清零)
    CAN_RXGMASK = 0x00000000;  // 全局掩码 = 0 (不屏蔽任何位)

    // MB0: 接收 mailbox --- CODE=EMPTY(0x4), 激活后自动接收
    MB_CS(0) = 0x00400000;  // CODE=Rx Empty (0x4 << 24)
    MB_ID(0) = 0;           // ID=0 (将被RXGMASK不过滤, 接收所有帧)

    // MB1: 发送 mailbox --- CODE=INACTIVE(0x8)
    MB_CS(1) = 0x00880000;  // CODE=Tx Inactive (0x8 << 24)
                            // SRR=1(替换远程请求位, 标准帧用)

    // 5. 清除所有中断标志, 使能 MB0 接收中断
    CAN_IFLAG1 = 0xFFFFFFFF;  // 写1清除所有中断标志
    CAN_IMASK1 = (1 << 0);    // 使能 MB0 的接收中断

    // 6. 退出冻结模式, 进入正常模式
    CAN_MCR &= ~(1 << 24);    // 清除 FRZ
    while (CAN_MCR & (1 << 24)); // 等待 FRZACK=0

    // 等待模块准备好
    while (!(CAN_MCR & (1 << 23)));  // 等待 NOTRDY=0
}

你刚刚算出来的PROP_SEG+PHASE_SEG1+PHASE_SEG2,最终变成CAN控制器内部的一个硬件定时器链。每个时间段对应一串触发器------到了预设的TQ数就切换到下一个段。整个CAN网络上所有节点的位时序加起来,决定了谁能在下一位抢占总线。

c 复制代码
// ========== CAN 发送一帧 ==========
void can_send_frame(uint32_t id, uint8_t *data, uint8_t dlc)
{
    // 等待 MB1 空闲 (检查 CODE 字段不是 TX 状态)
    while (((MB_CS(1) >> 24) & 0xF) == 0xC);  // CODE=0xC=TX In Progress

    // 填充 ID
    MB_ID(1) = (id << 18) & 0x1FFC0000;  // 标准帧: ID 放在 bit[28:18]
              // (1 << 14);  // 如果扩展帧

    // 填充数据: FlexCAN 的 Byte 顺序是 Motorola
    // MB_WORD0 = data[0-3], MB_WORD1 = data[4-7]
    MB_WORD0(1) = (data[0] << 24) | (data[1] << 16)
                | (data[2] << 8)  | data[3];
    MB_WORD1(1) = (data[4] << 24) | (data[5] << 16)
                | (data[6] << 8)  | data[7];

    // 设置 CODE=Tx (0xC) + DLC + 数据段长度
    // DLC 放在 CS 的 bit[3:0]
    MB_CS(1) = (0xC << 24)         // CODE=TX Once
             | (dlc & 0xF)         // DLC (数据长度)
             | (0x0 << 16);        // 不使用 RTR, IDE=0 (标准帧)
}

CAN0->IFLAG1 这个寄存器是一个边沿触发的中断标志。当CAN控制器检测到ACK slot期间总线上出现显性位(差分电压>0.9V)时,硬件自动把对应的IFLAG bit置1。你读这个bit的时候,读到的是一根AHB总线上的电平------它在不到100纳秒前还是CAN收发器比较器输出端的一个电压跳变。

c 复制代码
// ========== CAN 接收中断处理 ==========
// (在 NVIC 中使能 CAN0_ORed 中断)
void CAN0_ORed_IRQHandler(void)
{
    // 检查 MB0 的中断标志
    if (CAN_IFLAG1 & (1 << 0)) {
        // 读取接收到的数据
        uint8_t data[8];
        uint8_t dlc = MB_CS(0) & 0xF;  // DLC
        uint32_t id = (MB_ID(0) >> 18) & 0x7FF;  // 标准ID

        data[0] = (MB_WORD0(0) >> 24) & 0xFF;
        data[1] = (MB_WORD0(0) >> 16) & 0xFF;
        data[2] = (MB_WORD0(0) >> 8)  & 0xFF;
        data[3] = MB_WORD0(0) & 0xFF;
        data[4] = (MB_WORD1(0) >> 24) & 0xFF;
        data[5] = (MB_WORD1(0) >> 16) & 0xFF;
        data[6] = (MB_WORD1(0) >> 8)  & 0xFF;
        data[7] = MB_WORD1(0) & 0xFF;

        // 处理接收到的帧
        can_rx_callback(id, data, dlc);

        // 清除 MB0 中断标志, 重新激活接收
        CAN_IFLAG1 = (1 << 0);       // 写1清除
        MB_CS(0) = 0x00400000;       // 重新设置为 Rx Empty
    }
}

这段FlexCAN驱动代码------每一条MB_CS(1) = 0xC8000008------都在驱动CAN控制器内部的状态机从一个mailbox取数据、组装帧、推到位引擎(bit engine)、驱动收发器、在双绞线上产生差分电压。 而你可能只在应用层写了一个can_send_frame(0x3E8, rpm_data, 8)

CAN-FD:同一对线,八倍速率

2012年,博世推出了CAN-FD。它保持物理层不变------同一条双绞线、同样的收发器------但做了两个关键改动:

一、数据段变速。 仲裁阶段仍然用原速率(如500kbps),让所有节点都能参与仲裁。但在仲裁结束后,由发送节点单方面将速率切换到更高频率(如5Mbps)。为什么数据段可以加速?因为仲裁阶段需要所有节点同步监测总线上每bit------速率上限 = 1 / (2 × 总线往返传播延迟)。但数据段只有发送节点在驱动------接收节点只需要采样,不需要在每bit通过"读回"来判断仲裁------所以可以加速。

切换机制:仲裁阶段结束后,在BRS(Bit Rate Switch)位发送隐性------CAN-FD控制器检测到BRS=隐性后,在BRS位的采样点与CRC分隔符之间切换时钟分频器。接收节点也在同一时刻切换。整个过程在1 bit的时间内完成。对收发器完全透明------收发器看到的只是更快的差分电压翻转。

二、数据长度扩展到64字节。 经典CAN最多8字节。DLC字段在CAN-FD中被重新编码------DLC>8时使用非线性编码(9→12, 10→16, 11→20, 12→24, 13→32, 14→48, 15→64)。CRC也做了增强:17位CRC(数据≤16字节)或21位CRC(数据>16字节),加上4位stuff count和奇偶校验------错误检测概率从CAN的4.7×10⁻¹¹提升到10⁻²⁷。

效果:传输64字节从~1.3ms(CAN 1Mbps)压缩到~170μs(CAN-FD 5Mbps数据段)。对OTA诊断、固件刷写------这是质的提升。

数据段速率从1Mbps翻到5Mbps,意味着每个bit从1μs压缩到200ns。在这200ns里,收发器必须完成:驱动差分电压到显性电平(>1.5V)、让信号传播到总线另一端(~25ns for 5m cable)、接收器的比较器做出判决、为下一位准备好。在5Mbps下,信号的眼图开始闭合------不是因为协议有问题,而是因为物理层的RC时间常数和收发器的转换速率(slew rate)赶不上了。这就是为什么CAN-FD只在数据段加速------仲裁段仍然用低速,因为仲裁需要所有节点在同一时间看到同一电平。加速段没有仲裁,只有两个节点在对话。

有限资源的最优解

我们来复盘博世工程师在1983年面对的所有约束:

  • 线束重量必须降到十分之一 → 必须用共享总线。
  • 收发器芯片成本必须极低(每节点<$1)→ 双绞线+差分驱动,最简单可靠。
  • 发动机控制和ABS需要确定性延迟(<10ms最坏情况)→ 必须有硬件优先级仲裁,不能随机退避。
  • MCU只有几KB RAM、几十MHz → IP协议栈不可能,复杂状态机不可能。CAN控制器必须用硬件状态机直接实现到硅片。
  • 电磁环境极端恶劣(火花塞30kV放电)→ 差分信号+120Ω终端+双绞线,物理层天然抗共模干扰。

CAN是所有这些约束的交点。 它不快(1Mbps),不灵活(无地址寻址、无路由),不通用(8字节数据、11/29位ID)。但在汽车的约束空间里------它是最优的。

这不是教科书里"比较各种协议的优劣"------这是"在有限资源下,找到唯一解"。博世的工程师不是选了CAN------他们是推导出了CAN。

40年后的今天,CAN仍在每辆新车里活着。每个MCU里都焊着一个CAN控制器。每一条动力总成CAN上都有几十个帧在不同的时间周期内重复广播。它能在跑20年不出通信故障。它的出错率低到需要专门的测试工具(CANoe的Error Frame Injection功能)才能人为制造。

40年前博世工程师画在纸上的协议------今天在你的S32K的FlexCAN Silicon里、在你的PCB的CAN收发器里、在车身的双绞线里------每一比特都在忠实地运行。

有限资源 + 正确设计 = 一台车跑20年不出通信问题。 这不是运气------这是工程的胜利。


本篇小结

今天我们做了一件事:还原博世工程师在1983年面对的所有约束,理解CAN为什么不是"被选的"而是"被推导出来的"。

关键结论:

  1. CAN是汽车约束空间的唯一交点:线束重量必须降到十分之一、收发器成本必须<$1、必须确定性延迟------CAN的每一个设计决策都是在回答一个具体的物理和经济约束。
  2. ID域的仲裁是协议层的天才设计:一个字段同时解决"这是什么数据"和"谁先说"------逐位仲裁在物理层就完成了优先级判决,无需中央调度器。
  3. CAN-FD在保持物理层不变的前提下实现8倍加速:数据段变速(BRS位触发)、数据长度扩展到64字节------对OTA诊断和固件刷写是质的提升。

下一节,当CAN的1Mbps带宽碰到ADAS域控制器500MB的OTA固件包------差距三个数量级。车载以太网进入汽车,不是替代CAN,而是与它分工协作。

【下集预告】

CAN是汽车的主干神经------可靠、确定、低成本。但它只有1Mbps。CAN-FD顶到8Mbps,但对ADAS域控制器的500MB OTA固件包------还是远远不够。

雷达、摄像头的原始数据流------CAN的带宽差了三个数量级。

2015年,某德系OEM的E/E架构部门面临一个选择:继续用CAN做一切,还是引入一个"新物种"?他们选择引入车载以太网------不是双绞线的变种,是真正的TCP/IP栈搬进汽车。SOME/IP让服务自己"喊"I'm Here。DoIP让诊断仪通过IP地址找到ECU,一次刷入几百MB固件。AVB/TSN让多摄像头视频流精准同步。

但这条路上有一个根本性的问题还没回答:CAN是信号总线,以太网是数据网络。 一辆车需要两个神经系统吗?还是说------它们不是竞争关系,而是分工协作?CAN管"活着"(发动机不熄火、刹车不失灵),以太网管"聪明"(自动驾驶感知、云端更新、娱乐)。

相关推荐
求知喻1 小时前
KEIL5进行MSPM0开发
嵌入式
带娃的IT创业者1 小时前
企业架构治理的“隐形骨架”:从 Thunderbird/Thunderbolt 看开源工具如何重塑采购与合规
架构·开源·数字化转型·开源工具·企业架构·合规治理·供应商采购
程序员打怪兽2 小时前
嵌入式C语言
嵌入式
code 小楊3 小时前
AI Agent 核心范式 ReAct 深度详解:原理、流程、源码、实战与工程优化
人工智能·科技·开源
cdbqss13 小时前
VB2026 动态生成工具栏类 BqGetToolStrip
数据库·oracle·开源·.net·学习方法·教育电商·basic
妄想出头的工业炼药师3 小时前
追踪定位大模型
算法·开源
该昵称用户已存在3 小时前
从 GitHub 到产线:MyEMS 开源能源管理系统在制造现场的部署实录
开源·github·能源
凉、介4 小时前
深入理解 ARMv8-A|异常/中断处理
笔记·学习·嵌入式·arm
济6174 小时前
ROS开发专栏---基于 NAV2 实现仿真环境自主导航实验--适配Ubuntu 22.04
嵌入式硬件·嵌入式·ros2·机器人方向