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);
};
这里要注意,configure 和 set_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() 时,实际流程是这样的:
- 应用接口层检查参数合法性
- 核心抽象层处理超时和回调注册
- 底层驱动层把帧塞进发送邮箱
这种分层设计的好处很明显。我记得有一次,客户要求换芯片,从NXP换到TI。我只需要重写底层驱动层,上面两层完全不用动。项目周期从预估的三个月缩短到三周。
注意:不要绕过API直接调用底层函数。虽然技术上可行,但会破坏分层结构。后续升级Zephyr版本时,你的代码很可能就编译不过了。
3.4 实际项目中的经验
最后分享一点实战经验。我在做车载网关项目时,遇到过CAN驱动的一个典型问题:中断优先级。
Zephyr的CAN驱动默认使用中断方式接收数据。但如果你的系统里还有其他高优先级中断(比如电机控制),CAN接收中断可能会被延迟。延迟时间长了,硬件FIFO就溢出了,帧就丢了。
解决方案有两个:
- 提高CAN中断优先级:在设备树里配置中断优先级,确保CAN中断能及时响应
- 使用DMA:如果芯片支持,可以把CAN接收和DMA结合起来,减少CPU干预
我个人更倾向于第二种方案。为什么呢?因为提高中断优先级可能会影响其他实时任务的调度。用DMA的话,CPU可以腾出手来处理更重要的逻辑。
嗯,今天就先聊到这里。下一节我们会深入CAN帧的发送和接收流程,到时候再结合代码实例详细讲。记住,驱动架构是骨架,理解了骨架,后面填肉就轻松了。