3、Linux内核CAN子系统:SocketCAN架构、CAN协议栈、CAN设备驱动模型
好,咱们今天来聊聊Linux内核里的CAN子系统。说实话,我刚接触这块的时候,也被那一堆层次结构搞得有点晕。但后来我发现,只要抓住SocketCAN这个核心,其他东西就都顺了。
SocketCAN,说白了就是把CAN总线抽象成了一个网络设备。你想想看,我们平时用socket发TCP数据包,现在用同样的接口来发CAN帧,是不是很优雅?我个人觉得,这是Linux内核设计最巧妙的地方之一。
3.1 SocketCAN架构概览
SocketCAN不是凭空冒出来的。在它出现之前,Linux上的CAN驱动基本都是各玩各的——每个芯片厂商提供一套自己的字符设备接口,应用程序得针对不同硬件写不同的代码。那叫一个乱。
SocketCAN把CAN设备纳入了网络子系统,统一用PF_CAN协议族。这样一来,应用程序只需要打开一个socket,剩下的交给内核去调度。我在项目中遇到过好几次,从SJA1000换到MCP2515,应用层代码一行都不用改,这就是架构的力量。
核心思想: CAN设备 → 网络设备 → Socket接口 → 应用层
整个架构分三层:
- 应用层: 用户程序通过socket()、bind()、read()/write()操作CAN数据
- 协议层: 内核中的CAN协议栈,负责帧封装、过滤、协议解析
- 驱动层: 具体硬件驱动,负责收发CAN帧,处理中断
嗯,这里要注意,SocketCAN不是简单地把CAN帧塞进网络协议栈,它有自己的地址族和协议类型。你打开一个CAN socket时,用的是AF_CAN(或者PF_CAN),而不是AF_INET。
3.2 CAN协议栈:从帧到socket
协议栈这部分,我建议你把它理解成一个加工流水线。原始CAN帧从硬件上来,经过协议栈的层层处理,最后变成应用程序能读的数据。
先看一个最基本的CAN帧结构:
struct can_frame {
canid_t can_id; /* CAN ID,32位,包含EFF/RTR/ERR标志 */
__u8 can_dlc; /* 数据长度,0-8 */
__u8 __pad; /* 填充字节 */
__u8 __res0; /* 保留 */
__u8 __res1; /* 保留 */
__u8 data[8] __attribute__((aligned(8)));
};
这个结构体,你写驱动的时候天天见。can_id里藏了很多信息——标准帧还是扩展帧、远程帧还是数据帧、错误帧。我刚开始做的时候,就踩过一个坑:直接拿can_id去比较,结果把扩展帧的标志位也算进去了。
避坑指南: 我曾经在过滤CAN ID时,忘了屏蔽EFF标志位,导致标准帧和扩展帧匹配混乱。正确的做法是用can_id & CAN_EFF_MASK提取纯ID值。
协议栈里还有几个关键组件:
- RAW socket: 最常用,直接收发原始CAN帧,不做任何协议解析
- BCM socket: 广播管理器,适合周期性发送和接收过滤
- ISOTP: 基于CAN的ISO-TP协议,处理长数据分包
- CAN网关: 内核中的路由转发,实现CAN网桥
你想想看,如果没有SocketCAN,这些功能都得应用层自己实现,那得多痛苦。
3.3 CAN设备驱动模型
驱动模型这块,我直接给你画个图,比说一万句话都管用。
这张图你看明白了吗?从上到下,每一层都只跟相邻层打交道。应用层不知道你用的是SPI接口的MCP2515还是内存映射的SJA1000,它只管发数据。
驱动层里,最核心的是struct net_device_ops。你写驱动时,主要就是填充这个结构体:
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,
};
这四个回调函数,基本就是驱动的主干。ndo_open里做硬件初始化、申请中断;ndo_stop里关中断、停硬件;ndo_start_xmit负责把上层传下来的skb发出去。
个人经验: 我建议你在ndo_open里先调用open_candev(),这个函数会帮你做很多通用检查,比如设备是否已经在运行、时钟是否有效。别自己从头写,内核已经帮你铺好路了。
3.4 关键数据结构一览
写CAN驱动,有几个数据结构你必须烂熟于心。我整理了一个表格,方便你对照:
| 数据结构 | 所在文件 | 作用 | 关键字段 |
|---|---|---|---|
struct can_priv |
include/linux/can/dev.h | CAN设备私有数据,驱动核心 | state, ctrlmode, bittiming, data_bittiming |
struct can_frame |
include/uapi/linux/can.h | CAN帧数据结构,用户态和内核态共用 | can_id, can_dlc, data[8] |
struct net_device |
include/linux/netdevice.h | 网络设备通用结构,CAN设备继承它 | netdev_ops, mtu, flags, dev_addr |
struct sk_buff |
include/linux/skbuff.h | 网络缓冲区,CAN帧封装在其中 | data, len, protocol, dev |
struct can_bittiming |
include/linux/can/dev.h | CAN位时序参数 | bitrate, sample_point, tq, prop_seg, phase_seg1, phase_seg2, sjw |
这里我要特别说一下struct can_priv。这个结构体是CAN驱动的"户口本",你所有的硬件状态、配置参数都放在里面。我习惯在驱动probe函数里,用alloc_candev()分配它,而不是自己手动malloc。
3.5 注册与注销流程
驱动加载和卸载的流程,我总结成几个步骤,你照着这个顺序写,基本不会出错:
- probe阶段: 调用
alloc_candev()分配设备,设置netdev_ops和can_priv参数,最后register_candev()注册 - open阶段: 上电、配置位时序、启动硬件、申请中断、调用
netif_start_queue() - 发送阶段:
ndo_start_xmit里把skb里的can_frame取出来,写入硬件发送缓冲区,发送完成后调用can_put_echo_skb()和can_get_echo_skb() - 接收阶段: 中断处理函数里读取硬件接收缓冲区,构造skb,调用
netif_rx()或napi_gro_receive() - close阶段: 关中断、停硬件、调用
netif_stop_queue() - remove阶段:
unregister_candev()注销,free_candev()释放
注意: 发送完成中断里,一定要调用can_put_echo_skb()来管理回显skb。我曾经漏掉这一步,结果导致发送完成后系统崩溃——因为内核以为skb还在飞,实际上硬件已经发完了。
3.6 位时序与波特率配置
CAN总线的波特率配置,说难不难,说简单也不简单。它取决于三个参数:TQ(时间量子)、位段分割和采样点。
内核里用struct can_bittiming来描述这些参数。你可以在驱动里硬编码,也可以通过ip link set can0 bitrate 500000让内核自动计算。我个人建议用后者,省心。
但自动计算不是万能的。我记得有一次,客户要求采样点必须在75%,默认算法算出来是70%。最后我只能手动指定phase_seg1和phase_seg2的值,才把采样点调上去。
/* 手动配置位时序示例 */
struct can_bittiming bt = {
.bitrate = 500000,
.sample_point = 75,
.tq = 125, /* 时间量子,单位ns */
.prop_seg = 2,
.phase_seg1 = 3,
.phase_seg2 = 2,
.sjw = 1,
};
你想想看,如果不懂这些参数背后的原理,遇到这种问题就只能干瞪眼。
3.7 错误处理与状态管理
CAN驱动里,错误处理是重头戏。硬件随时可能报错——总线关闭、错误被动、ACK错误、填充错误……你的驱动得能处理这些情况。
内核提供了can_change_state()函数来统一管理设备状态。状态机是这样的:
- CAN_STATE_ERROR_ACTIVE: 正常状态,可以正常收发
- CAN_STATE_ERROR_WARNING: 错误计数超过96,但还能工作
- CAN_STATE_ERROR_PASSIVE: 错误计数超过127,只能接收不能发送
- CAN_STATE_BUS_OFF: 错误计数超过255,完全离线
- CAN_STATE_STOPPED: 手动停止
- CAN_STATE_SLEEPING: 休眠模式
我建议你在中断处理函数里,每次收发完成后都检查一下硬件错误寄存器,然后调用can_change_state()更新状态。这样上层协议栈才能及时知道总线状况。
小技巧: 总线关闭后,不要立即重新启动。硬件需要等待128个11个隐性位(Bus-Off Recovery Sequence)。你可以用can_restart()配合定时器来实现自动恢复。