3、Zephyr CAN驱动架构:CAN控制器驱动模型、设备树绑定、API分层设计

好,咱们今天聊聊Zephyr的CAN驱动架构。说实话,这部分内容我当年刚接触时也绕了好一阵子。Zephyr的驱动模型跟Linux有点像,但又不完全一样。你如果之前做过Linux驱动开发,可能会觉得亲切;如果是从裸机直接跳过来的,嗯,可能需要一点时间适应。

我个人习惯把CAN驱动架构拆成三个层面来看:驱动模型设备树绑定API分层。这三块搞明白了,后面写应用代码就顺手多了。

3.1 CAN控制器驱动模型

Zephyr的CAN驱动模型,说白了就是一套标准化的接口规范。它定义了你需要实现哪些函数,数据怎么传递,中断怎么处理。

核心结构体是 can_driver_api,里面挂了一堆函数指针。你想想看,每个芯片的CAN外设寄存器都不一样,但Zephyr通过这层抽象,让上层应用不用关心底层差异。

struct can_driver_api {
    int (*configure)(const struct device *dev, can_mode_t mode, can_bitrate_t bitrate);
    int (*send)(const struct device *dev, const struct can_frame *frame, k_timeout_t timeout, can_tx_callback_t callback, void *user_data);
    int (*add_rx_filter)(const struct device *dev, can_rx_callback_t callback, void *user_data, const struct can_filter *filter);
    int (*remove_rx_filter)(const struct device *dev, int filter_id);
    int (*get_state)(const struct device *dev, enum can_state *state, struct can_bus_err_cnt *err_cnt);
    int (*set_mode)(const struct device *dev, can_mode_t mode);
    void (*set_timing)(const struct device *dev, const struct can_timing *timing);
    int (*get_capabilities)(const struct device *dev, can_mode_t *cap);
};

这里要注意,configureset_mode 是分开的。为什么?我在项目中遇到过一个问题:有些芯片在运行时需要动态切换模式(比如从正常模式切到只听模式),但波特率不需要重新配置。如果合在一起,每次切换模式都得重新配一遍波特率,效率就低了。

关键点:驱动模型只定义接口,不关心具体实现。你用的是STM32的bxCAN还是NXP的FlexCAN,对上层来说都一样。

3.2 设备树绑定

设备树绑定,说白了就是告诉Zephyr:你的CAN控制器接在哪个引脚上,用哪个中断,波特率是多少。这些信息写在 .dts 文件里,编译时会被解析成C结构体。

一个典型的CAN设备树节点长这样:

&can1 {
    status = "okay";
    bus-speed = <500000>;
    sample-point = <875>;
    sjw = <1>;
    pinctrl-0 = <&can1_rx_pa11 &can1_tx_pa12>;
    pinctrl-names = "default";
};

这里我特别想提醒一点:bus-speed 的单位是bps,不是kbps。我曾经见过有人写成 500,结果CAN总线怎么都跑不起来。查了半天才发现是单位搞错了。嗯,这种坑踩过一次就记住了。

设备树绑定的好处是什么?你想想看,硬件改版了,换个引脚,只需要改 .dts 文件,驱动代码一行都不用动。这就是硬件描述和软件实现的解耦。

小技巧:调试设备树时,可以用 west build -t guiconfig 打开图形化配置工具,直观地查看和修改设备树参数。

3.3 API分层设计

Zephyr的CAN API分了三层,我习惯叫它们:底层驱动层核心抽象层应用接口层

层级 文件 职责
底层驱动层 drivers/can/can_xxx.c 直接操作寄存器,实现can_driver_api
核心抽象层 include/zephyr/drivers/can.h 封装通用逻辑,如超时处理、回调管理
应用接口层 include/zephyr/canbus/can.h 提供can_send、can_add_rx_filter等用户API

为什么这么分?说白了就是职责分离。底层驱动只关心怎么读写寄存器,核心抽象层处理那些通用的逻辑(比如超时等待、错误处理),应用接口层给用户一个清爽的调用方式。

举个例子,你调用 can_send() 时,实际流程是这样的:

  1. 应用接口层检查参数合法性
  2. 核心抽象层处理超时和回调注册
  3. 底层驱动层把帧塞进发送邮箱

这种分层设计的好处很明显。我记得有一次,客户要求换芯片,从NXP换到TI。我只需要重写底层驱动层,上面两层完全不用动。项目周期从预估的三个月缩短到三周。

注意:不要绕过API直接调用底层函数。虽然技术上可行,但会破坏分层结构。后续升级Zephyr版本时,你的代码很可能就编译不过了。

3.4 实际项目中的经验

最后分享一点实战经验。我在做车载网关项目时,遇到过CAN驱动的一个典型问题:中断优先级

Zephyr的CAN驱动默认使用中断方式接收数据。但如果你的系统里还有其他高优先级中断(比如电机控制),CAN接收中断可能会被延迟。延迟时间长了,硬件FIFO就溢出了,帧就丢了。

解决方案有两个:

  • 提高CAN中断优先级:在设备树里配置中断优先级,确保CAN中断能及时响应
  • 使用DMA:如果芯片支持,可以把CAN接收和DMA结合起来,减少CPU干预

我个人更倾向于第二种方案。为什么呢?因为提高中断优先级可能会影响其他实时任务的调度。用DMA的话,CPU可以腾出手来处理更重要的逻辑。

嗯,今天就先聊到这里。下一节我们会深入CAN帧的发送和接收流程,到时候再结合代码实例详细讲。记住,驱动架构是骨架,理解了骨架,后面填肉就轻松了。