第十八章:CAN总线与车载网络驱动

各位同学,今天我们来聊聊车载网络里最核心的东西——CAN总线。说实话,我在座舱领域摸爬滚打这么多年,CAN总线就像汽车的“神经系统”。你想想看,方向盘一转、油门一踩,这些信号怎么传到各个ECU?靠的就是CAN。

18.1 CAN协议基础:别被那些术语吓到

CAN(Controller Area Network)是博世在80年代发明的。到现在,它依然是车载网络的主力。为什么?因为它可靠、实时、成本低。

我个人习惯把CAN协议拆成三块来理解:

  • 物理层:差分信号,两条线(CAN_H、CAN_L)。电压差决定逻辑0和1。
  • 数据链路层:报文格式、仲裁机制、错误检测。
  • 应用层:比如J1939、CANopen,或者各家OEM自己定义的协议。

这里有个关键点——CAN的仲裁机制。为什么多个节点同时发消息不会冲突?因为CAN总线是“线与”逻辑。显性位(逻辑0)会覆盖隐性位(逻辑1)。ID越小,优先级越高。我在项目中遇到过一个问题:某个ECU的CAN ID设成了0x000,结果它一说话,其他节点全闭嘴了。嗯,这就是典型的“总线霸权”。

CAN报文帧结构(标准帧)

字段长度说明
SOF1 bit帧起始
ID11 bit标识符,决定优先级
RTR1 bit远程帧标志
DLC4 bit数据长度(0-8字节)
Data0-64 bit实际数据
CRC15 bit循环冗余校验
ACK2 bit应答
EOF7 bit帧结束

你可能会问:为什么数据段最多8个字节?这是早期设计时的限制,为了满足实时性。现在CAN FD(灵活数据速率)已经支持64字节了,但传统CAN依然广泛存在。

18.2 QNX CAN驱动实现:从硬件到应用

在QNX下写CAN驱动,我建议你走这个路径:

  1. 硬件初始化:配置CAN控制器(比如M_CAN、SJA1000)的时钟、波特率、过滤器。
  2. 中断处理:接收中断、发送完成中断、错误中断。
  3. 资源管理:用QNX的Resource Manager框架暴露设备节点(比如/dev/can0)。
  4. IOCTL接口:提供配置、过滤、环回模式等控制命令。

我记得有一次调试QNX CAN驱动,发现接收中断老是丢包。查了半天,原来是中断优先级设得太低,被别的外设抢了。后来我把CAN中断优先级提到最高,问题就解决了。这种坑,你不踩一次真的记不住。

下面是一个简化的QNX CAN驱动初始化代码片段:

// QNX CAN驱动初始化示例
#include <sys/iofunc.h>
#include <sys/dispatch.h>

int can_init(can_device_t *dev) {
    // 1. 映射寄存器地址
    dev->base = mmap_device_io(CAN_REG_SIZE, CAN_BASE_ADDR);
    if (dev->base == MAP_DEVICE_FAILED) {
        return -1;
    }

    // 2. 复位CAN控制器
    out32(dev->base + CAN_CTRL_REG, CAN_MODE_RESET);
    delay_us(100);

    // 3. 配置波特率(500kbps为例)
    out32(dev->base + CAN_BTR_REG, 
          CAN_BTR_BRP(4) | CAN_BTR_TSEG1(13) | CAN_BTR_TSEG2(2));

    // 4. 设置接收过滤器(接收所有报文)
    out32(dev->base + CAN_FILTER_REG, 0x00000000);

    // 5. 进入正常模式
    out32(dev->base + CAN_CTRL_REG, CAN_MODE_NORMAL);

    // 6. 注册中断
    InterruptAttach(dev->irq, can_isr, dev, 0, 0);

    return 0;
}

避坑指南:我曾经在波特率配置上栽过跟头。两个节点波特率必须完全一致,差1%都不行。别问我怎么知道的——示波器上看波形,眼图都睁不开。

18.3 Android Vehicle Network HAL:连接上层应用

到了Android Automotive这边,事情就变得“抽象”了。Google搞了个Vehicle Network HAL,说白了就是给上层应用一个统一的接口,不管底层是CAN、LIN还是以太网。

核心接口就几个:

  • IVehicleNetwork:网络管理接口
  • IVehicleNetworkListener:事件监听回调
  • VehicleNetworkMessage:报文数据结构

实现这个HAL时,我建议你重点关注两点:

  1. 报文订阅机制:上层应用只关心特定的CAN ID,不要一股脑全发上去。
  2. 信号路由:从CAN报文里提取出具体的信号值(比如车速、转速),然后分发给对应的服务。

下面是一个Vehicle Network HAL的典型实现骨架:

// Android Vehicle Network HAL 实现片段
class VehicleNetworkImpl : public IVehicleNetwork {
public:
    // 订阅CAN报文
    Return<StatusCode> subscribe(VehicleNetworkMessageType type,
                                  int32_t canId) override {
        // 检查CAN ID是否在允许范围内
        if (!isValidCanId(canId)) {
            return StatusCode::INVALID_ARG;
        }

        // 注册到底层CAN驱动
        mCanDriver->addFilter(canId, this);

        // 记录订阅信息
        mSubscriptions[canId] = type;

        return StatusCode::OK;
    }

    // 接收CAN报文回调
    void onCanMessage(const CanMessage& msg) {
        // 解析报文中的信号
        VehicleNetworkMessage vMsg;
        vMsg.canId = msg.id;
        vMsg.timestamp = msg.timestamp;

        // 信号提取(以车速为例)
        if (msg.id == 0x1A0) {
            // 车速信号位于字节2-3,因子0.01,偏移0
            int16_t raw = (msg.data[2] << 8) | msg.data[3];
            vMsg.speed = raw * 0.01f;
        }

        // 通知所有监听者
        for (auto& listener : mListeners) {
            listener->onMessageReceived(vMsg);
        }
    }

private:
    std::map<int32_t, VehicleNetworkMessageType> mSubscriptions;
    std::vector<sp<IVehicleNetworkListener>> mListeners;
};

18.4 CAN报文解析与信号路由:实战中的艺术

报文解析这件事,说难不难,说简单也不简单。你想想看,一个CAN报文8个字节,可能包含3个、5个甚至8个不同的信号。怎么把它们拆出来?

我一般用DBC文件来描述这些信号。DBC是Vector公司定义的CAN数据库格式,里面包含了:

  • 报文ID、周期、长度
  • 每个信号的起始位、长度、字节序(Intel/Motorola)
  • 缩放因子、偏移量、单位
  • 取值范围、默认值

解析的核心逻辑其实就三步:

  1. 位提取:从原始数据中取出指定范围的bit。
  2. 值转换:根据字节序(大端/小端)重组数值。
  3. 物理换算:乘以缩放因子,加上偏移量。

举个例子,假设车速信号定义如下:

// DBC定义示例
// BO_ 256 VCU_1: 8 VCU
//  SG_ VehicleSpeed : 16|16@1+ (0.01,0) [0|655.35] "km/h"  NEO

// 解析代码
float parse_vehicle_speed(uint8_t *data) {
    // 起始位16,长度16,Intel格式(@1+)
    uint16_t raw = (data[2] << 8) | data[3];
    // 缩放因子0.01,偏移0
    return raw * 0.01f;
}

注意:字节序是个大坑。Intel格式(小端)和Motorola格式(大端)的解析方式完全不同。我曾经因为搞混了字节序,导致车速显示成300多km/h——还好只是测试车,不然就闹笑话了。

信号路由这块,我习惯用发布-订阅模式。每个信号都有一个唯一的ID(比如用CAN ID+信号名组合),上层服务按需订阅。这样做的优点是:

  • 解耦:信号生产者和消费者互不依赖
  • 高效:只处理关心的信号,减少CPU开销
  • 可扩展:新增信号不影响现有逻辑

下面我用一张SVG图来总结CAN信号处理的整个流程:

CAN报文解析与信号路由流程 CAN控制器 M_CAN / SJA1000 QNX CAN驱动 中断处理 / 资源管理 Vehicle Network HAL 订阅 / 分发 CAN报文解析引擎 DBC解析 → 位提取 → 物理换算 字节序处理 / 缩放因子 / 偏移量 信号路由(发布-订阅模式) 车速信号 → 仪表盘服务 转向信号 → ADAS服务 上层应用 仪表 / ADAS / 中控 诊断反馈 错误处理

这张图把整个链路串起来了。从CAN控制器收到原始报文,到驱动层处理中断,再到HAL层做订阅分发,最后解析成具体的信号值路由给上层应用。每一步都有坑,但每一步也都有章可循。

我的建议:刚开始做CAN驱动时,别急着写代码。先拿一个CAN分析仪(比如PCAN、Kvaser)抓抓报文,看看真实的数据长什么样。你亲眼看到0x1A0报文里第2个字节随着油门变化,比看一百页文档都管用。

好了,CAN总线这块的内容就讲到这里。记住一句话:CAN不难,但细节决定成败。波特率、字节序、过滤器配置,任何一个地方出问题,车就跑不起来。做车载开发,稳字当头。


公众号:蓝海资料掘金营,微信deep3321