3. AUTOSAR架构总览

驱动架构这东西,说白了就是决定你的CAN控制器怎么跟CPU打交道。选对了,系统稳如老狗;选错了,丢帧、卡顿、中断风暴全来了。我这些年调过的CAN驱动,从裸机到Linux,从8位MCU到多核ARM,踩过的坑能写满一本笔记本。今天咱们就把这层窗户纸捅破。

3.1 Linux SocketCAN框架:为什么它成了事实标准

早些年做Linux下的CAN,各家驱动自己搞一套,应用层接口五花八门。直到SocketCAN出现,大家才统一了玩法。它把CAN设备抽象成网络套接字,你想想看,用read()/write()或者sendmsg()/recvmsg()就能收发CAN帧,是不是很优雅?

核心思路:SocketCAN在Linux网络协议栈里注册了一个新的协议族——AF_CAN。它不经过TCP/IP那套东西,直接走原始CAN帧。延迟低,效率高。

我习惯把SocketCAN的驱动架构分成三层来看:

层级 模块 职责
应用层 用户程序 通过socket API收发CAN帧,设置过滤规则
协议层 AF_CAN协议族 管理CAN套接字、路由、过滤、RAW/BCM/ISO-TP协议
驱动层 net_device + can_priv 硬件寄存器操作、中断处理、DMA传输、错误统计

驱动层需要实现一个struct net_device,挂上struct can_priv私有数据。最关键的是open()、close()、start_xmit()这几个回调。我记得第一次写SocketCAN驱动时,start_xmit()里忘了处理NETDEV_TX_BUSY的返回,结果上层一直重传,把CPU吃满了。嗯,这种低级错误犯一次就够了。

// SocketCAN驱动核心结构示意
static const struct net_device_ops can_netdev_ops = {
    .ndo_open       = can_open,
    .ndo_stop       = can_close,
    .ndo_start_xmit = can_start_xmit,
    .ndo_change_mtu = can_change_mtu,
};

static int can_open(struct net_device *dev)
{
    struct can_priv *priv = netdev_priv(dev);
    // 开启CAN控制器,配置波特率,申请中断
    priv->can.state = CAN_STATE_ERROR_ACTIVE;
    netif_start_queue(dev);
    return 0;
}

static netdev_tx_t can_start_xmit(struct sk_buff *skb,
                                   struct net_device *dev)
{
    struct can_priv *priv = netdev_priv(dev);
    struct can_frame *cf = (struct can_frame *)skb->data;
    // 把cf写入CAN控制器的发送缓冲区
    // 触发发送
    priv->write_frame(priv, cf);
    // 如果硬件发送FIFO满了,返回NETDEV_TX_BUSY
    return NETDEV_TX_OK;
}

个人经验:SocketCAN的过滤机制非常强大。你可以在驱动层用硬件过滤,也可以在协议层用软件过滤。我建议能硬件过滤就别用软件,尤其是总线负载高的时候。曾经有个项目,软件过滤导致CPU占用率飙升到40%,切到硬件过滤后直接降到5%。

3.2 裸机驱动分层设计:小而美的哲学

不是所有场景都能跑Linux。MCU上做CAN驱动,我坚持一个原则:分层,但别过度设计。裸机环境资源有限,分层是为了可移植性和可维护性,不是为了炫技。

我常用的裸机CAN驱动分层是这样的:

  • 硬件抽象层(HAL):直接操作寄存器。读、写、修改位。这一层跟芯片绑定。
  • 驱动核心层(Core):管理状态机、FIFO、中断处理。这一层跟CAN控制器型号绑定。
  • 接口层(API):提供CAN_Send()、CAN_Receive()、CAN_Config()。这一层给应用用,跟硬件无关。

举个例子,HAL层可能长这样:

// 硬件抽象层 - 以STM32 bxCAN为例
static inline void can_hal_write_tx_mailbox(CAN_TypeDef *instance,
                                            uint32_t mailbox,
                                            uint32_t tid,
                                            uint32_t tdlr,
                                            uint32_t tdhr)
{
    instance->sTxMailBox[mailbox].TIR = tid;
    instance->sTxMailBox[mailbox].TDTR = tdlr;
    instance->sTxMailBox[mailbox].TDLR = (uint32_t)(tdhr & 0xFFFF);
    instance->sTxMailBox[mailbox].TDHR = (uint32_t)(tdhr >> 16);
}

Core层则负责更高级的逻辑:

// 驱动核心层 - 发送接口
int can_core_send_frame(CAN_Device *dev, const CAN_Frame *frame)
{
    // 检查总线状态
    if (dev->state != CAN_STATE_ACTIVE)
        return CAN_ERR_BUS_OFF;

    // 找一个空闲的发送邮箱
    int mb = can_hal_get_free_tx_mailbox(dev->instance);
    if (mb < 0)
        return CAN_ERR_TX_FIFO_FULL;

    // 写入硬件
    can_hal_write_tx_mailbox(dev->instance, mb,
                             frame->id, frame->dlc,
                             frame->data[0] | (frame->data[1] << 8));
    // 请求发送
    can_hal_request_tx(dev->instance, mb);
    return CAN_OK;
}

避坑指南:我曾经在Core层和HAL层之间加了一层“缓冲层”,想着提高性能。结果中断上下文里malloc导致堆碎片,系统跑了三天就挂了。裸机驱动里,中断服务函数中绝对不要动态分配内存。用静态环形缓冲区,提前分配好。

3.3 中断与轮询模式选择:没有银弹

这个问题,每次做新项目都会被问到。我的回答永远是:看场景。没有哪个模式绝对好,只有哪个模式更适合你的系统。

先看一张对比表:

特性 中断模式 轮询模式
CPU占用率 低(仅事件触发时占用) 高(持续占用)
响应延迟 低(微秒级) 取决于轮询周期
实现复杂度 高(需处理嵌套、优先级、临界区) 低(主循环里调用)
适用场景 高负载、多任务、实时性要求高 低负载、简单系统、资源极度受限
风险 中断风暴、优先级反转 丢帧、响应不及时

我个人习惯这样选:

  • 总线负载超过30%,或者有实时性要求(比如电机控制、安全气囊),必须用中断。
  • 总线负载低于10%,且CPU资源紧张(比如8位MCU跑一堆任务),可以考虑轮询。
  • 如果系统既有高优先级CAN消息,又有低优先级诊断消息,我会用中断+轮询混合模式。高优先级消息触发中断,低优先级消息在主循环里轮询读取。

说到中断,有个东西必须提——中断合并(Interrupt Coalescing)。有些CAN控制器支持这个功能,比如多个接收帧合并成一个中断。我曾在高负载项目里用过,效果显著。但要注意,合并会引入额外延迟,实时性要求高的消息不能等。

关键指标:中断服务函数执行时间必须小于最短消息间隔。假设总线是500kbps,一个标准帧约130μs。如果你的ISR执行超过130μs,下一个帧的中断就会丢失。我曾经用逻辑分析仪抓过,发现ISR里有个printf,直接干到200μs。去掉后降到20μs,问题解决。

3.4 一张图看懂CAN驱动架构

下面这张SVG图,把Linux SocketCAN和裸机驱动的架构放在一起对比。你看一眼就能明白两者的异同。

CAN驱动架构对比:Linux SocketCAN vs 裸机分层驱动 Linux SocketCAN 架构 应用层:socket(AF_CAN, SOCK_RAW, CAN_RAW) 协议层:AF_CAN协议族 / RAW / BCM / ISO-TP 驱动层:net_device + can_priv + 中断处理 硬件层:CAN控制器寄存器 / DMA / PHY 裸机分层驱动架构 API接口层:CAN_Send() / CAN_Receive() / CAN_Config() 驱动核心层:状态机 / FIFO管理 / 中断分发 硬件抽象层:寄存器读写 / 位操作 / 宏定义 硬件层:CAN控制器寄存器 / PHY芯片 共同点:都遵循“应用-核心-硬件”分层思想 差异:SocketCAN通过网络协议栈统一接口;裸机驱动更轻量、更可控 中断 vs 轮询:负载 > 30% 用中断,负载 < 10% 可轮询 ISR执行时间 < 最短消息间隔(约130μs @500kbps)

3.5 实战中的选择建议

说了这么多,最后给几条实在的建议:

  • 新项目选型:如果跑Linux,无脑用SocketCAN。驱动框架成熟,社区支持好,工具链完善(candump、cansend、can-utils)。
  • MCU项目:优先用中断。除非你的CAN消息频率极低(比如每秒几条),否则轮询的代价远高于中断。
  • 混合模式:如果控制器支持多个FIFO或邮箱,把高优先级消息映射到中断通道,低优先级消息走轮询。我曾在车载网关项目里这么干过,效果很好。
  • 调试手段:不管用哪种模式,第一件事是拿逻辑分析仪抓CAN总线波形,确认硬件通信正常。然后再看驱动层的中断响应时间和丢帧情况。

一个小技巧:在驱动里加一个统计计数器,记录中断次数、发送成功次数、发送失败次数、接收丢帧次数。这些数据在调试阶段能救命。我习惯把这些统计暴露到/proc或sysfs(Linux)或者通过串口打印(裸机),方便实时监控。

驱动架构选对了,后面的调优工作才能事半功倍。别急着写代码,先把架构想清楚。嗯,今天就聊到这儿。