大家好,我是快乐学习,开心分享的科技探索员 🎉
前两期我们分别搞懂了 CAN 总线的硬件原理,以及 Linux SocketCAN 框架的使用方法。这一期进入最底层——驱动开发。
一个问题:SocketCAN 是怎么知道"往哪根线上发数据"的?答案就藏在 CAN 控制器驱动里。驱动是硬件和内核之间的翻译官,它向下直接操作寄存器控制 CAN 芯片,向上遵循 Linux net_device 框架提供统一接口,让 SocketCAN 完全感知不到硬件差异。
写一个 Linux 驱动,本质上是在内核规定的框架里"填空"——内核告诉你在哪里写代码,你只需要把硬件操作填进去。
本期以 MCP2515(最经典的 SPI 接口 CAN 控制器)为例,带你看懂 Linux CAN 驱动的完整骨架。
▍一、CAN 驱动的整体架构
先看整体分层,建立宏观认知:
▲ Linux CAN驱动四层架构——SocketCAN核心层、网络设备层、控制器驱动层、硬件层
四层职责一句话概括:
• SocketCAN核心层(can.ko):提供PF_CAN 协议族和套接字接口,与驱动无关
• CAN 网络设备层:把 CAN 控制器抽象成标准 net_device,对上暴露统一的 ndo_* 操作
• CAN 控制器驱动:本期主角,负责初始化芯片、设置波特率、发帧收帧、处理中断
• 硬件层:MCP2515 通过SPI 总线挂在 CPU 上,所有操作最终变成 SPI 读写时序
💡 理解这个分层后,你会发现:不管是 MCP2515(SPI接口)、TJA1050(直接内存映射)还是 SoC 内置的 FlexCAN,驱动骨架完全相同,差别只在底层寄存器操作。换芯片只需换"填空"的内容。
▍二、驱动注册:platform_driver 与 spi_driver
CAN 控制器的驱动类型取决于它的总线接口:
• SPI 外接(MCP2515):使用spi_driver 框架
• 内存映射(SoC 内置FlexCAN、MCAN):使用platform_driver 框架
• USB 接口(PEAK USB CAN):使用 usb_driver 框架
以 MCP2515 的spi_driver 为例,驱动注册与注销流程:
▲ spi_driver 注册流程——从 module_init 到 probe 再到 register_candev 的完整调用链
probe 函数:驱动的"入口"
当内核发现设备树中描述的 MCP2515 设备与驱动匹配时,调用 probe 函数。probe 必须完成以下工作:
• 第一步:alloc_candev(sizeof(私有数据结构), TX队列数) — 分配 CAN 网络设备
• 第二步:填充 net_device_ops(ndo_open、ndo_stop、ndo_start_xmit)
• 第三步:填充 can_bittiming_const(描述芯片支持的波特率参数范围)
• 第四步:硬件初始化(复位芯片、配置振荡器、设置工作模式)
• 第五步:register_candev(ndev) — 向内核注册,can0 出现在系统中
• 第六步:request_irq() — 注册中断处理函数
设备树(DTS)配置
现代 Linux 驱动通过设备树描述硬件,MCP2515的典型 DTS 节点:
• compatible = "microchip,mcp2515":与驱动的 of_device_id 匹配
• reg = <0>:SPI 片选号
• spi-max-frequency = <10000000>:最大 SPI 频率 10MHz
• clocks = <&clk20m>:外部晶振引用,MCP2515需要 8/16/20MHz 时钟
• interrupt-parent / interrupts:中断配置,MCP2515 有一个低电平有效的 INT 引脚
⚠️ MCP2515 的外部晶振频率必须在设备树中正确配置。驱动会根据晶振频率计算波特率分频参数,配错了会导致波特率偏差,轻则通信偶发错误,重则完全无法通信。
▍三、核心操作函数:ndo_open / ndo_stop / ndo_start_xmit
▲ CAN驱动关键代码——发送函数ndo_start_xmit和探测函数probe的核心实现
ndo_open:ip link set can0 up 时调用
打开接口时需要:
• 调用 open_candev(ndev) — 内核检查波特率等参数是否已配置
• 硬件配置波特率(根据 can->bittiming 计算并写入芯片寄存器)
• 使能中断(写 MCP2515 的CANINTE 寄存器,开启 TX/RX 中断)
• 设置芯片为正常工作模式(退出配置模式,进入 Normal 模式)
• 调用 netif_start_queue(ndev) — 通知内核可以开始发送
ndo_stop:ip link set can0 down 时调用
• netif_stop_queue(ndev) — 停止发送队列
• 芯片进入睡眠或复位模式
• close_candev(ndev) — 释放资源
ndo_start_xmit:发送一帧 CAN 报文
这是驱动最核心的函数,当用户调用 write() 发送 CAN 帧时,内核最终调用到这里:
• 从 skb(socket buffer)中取出 can_frame 结构体
• 停止发送队列 netif_stop_queue()(MCP2515 只有3个TX缓冲,发完再开)
• 通过 SPI 将帧数据写入MCP2515 的 TX 缓冲区寄存器
• 写 RTS 命令寄存器触发MCP2515 开始发送
• 返回 NETDEV_TX_OK
💡 skb(socket buffer)是 Linux 网络子系统的核心数据结构,存放要发送或刚收到的网络数据包。在 CAN 驱动里,一个 skb 对应一个can_frame。用 can_get_echo_skb() 和 can_put_echo_skb() 来处理回环报文的统计。
▍四、中断处理:驱动的实时响应核心
MCP2515 有一个 INT 引脚,任何事件(TX完成、RX收到帧、错误)都会拉低这个引脚触发中断。驱动必须在中断处理函数里快速响应:
▲ CAN驱动中断处理——TX发送完成和RX帧接收的并行处理流程
中断处理函数的主体逻辑
• 读取 MCP2515 的CANINTF 寄存器,获知中断原因
• TX 中断(TXIF):发送完成 → netif_wake_queue() 重新开启发送队列 → 更新 net_device stats
• RX 中断(RXIF):读取RXB0/RXB1 缓冲区 → 组装 can_frame → alloc_can_skb() → netif_rx() 送入内核
• 错误中断(ERRIF):读取EFLG 寄存器 → 分析错误类型 → 构造错误 skb → 上报给 SocketCAN 错误处理
• 清除对应中断标志位,否则中断会持续触发
NAPI 优化:高负载下的接收优化
在高波特率(1Mbps)总线满负荷时,每帧都产生一次中断会导致 CPU 频繁进出中断上下文。内核的 NAPI(New API)机制解决了这个问题:
• 第一帧到达触发中断 → 关闭RX 中断 → 调度 napi_schedule()
• 内核在软中断上下文调用 poll 函数,一次性处理多个帧
• 处理完毕后重新开启 RX 中断
Linux 内核的 MCP2515 驱动(drivers/net/can/spi/mcp251x.c)使用了 NAPI,可直接阅读源码学习。
⚠️ 中断处理函数里不能睡眠、不能调用可能阻塞的函数。MCP2515 的 SPI 读写在中断上下文中必须用 spi_sync()(同步非阻塞),而不能用 spi_async(),否则会触发内核 BUG。
▍五、波特率配置:把 500Kbps 翻译成寄存器值
波特率配置是 CAN 驱动里最容易踩坑的地方。内核负责上层计算,驱动负责把计算结果写进寄存器。
CAN 位时序参数
• TQ(Time Quantum):最小时间单位,由振荡器频率和 BRP(波特率预分频)决定
• Sync_Seg:固定 1 TQ,用于同步
• Prop_Seg + Phase_Seg1:传播延迟和相位补偿(采样点之前)
• Phase_Seg2:采样点之后的相位补偿
• 采样点 = (Sync_Seg + Prop_Seg + Phase_Seg1) / (总TQ数),通常设在 75%~87.5%
驱动侧的实现
驱动需要向内核注册 can_bittiming_const,告知芯片支持的参数范围(各 Seg 的最大最小值、BRP范围等)。内核根据用户设置的波特率自动搜索最优参数组合,然后调用驱动的 do_set_bittiming 回调,驱动把参数写入 MCP2515 的 CNF1/CNF2/CNF3 寄存器即可。
💡 不需要手动计算寄存器值!内核的 can_calc_bittiming() 函数会自动完成所有计算,驱动只需实现好can_bittiming_const 和 do_set_bittiming 回调。这是 SocketCAN 框架设计得极为优雅的地方之一。
▍六、实战建议:读懂内核现有驱动源码
最好的学习方式就是直接读 Linux 内核里的真实驱动代码:
• MCP2515 SPI驱动:drivers/net/can/spi/mcp251x.c(约 1400 行,经典教科书级代码)
• 虚拟 CAN 驱动:drivers/net/can/vcan.c(约 200 行,最简单的 CAN 驱动,适合入门)
• SoC FlexCAN驱动:drivers/net/can/flexcan/flexcan-core.c(NXP i.MX系列,功能完整)
推荐阅读顺序:
• 第一步:读 vcan.c,搞清楚最小 CAN 驱动的骨架(open/stop/xmit/register_candev)
• 第二步:读 mcp251x.c,学习真实硬件驱动的完整实现(SPI操作、中断、错误处理)
• 第三步:读 flexcan-core.c,学习内存映射型驱动和 NAPI 的结合使用
💡 在 VSCode 或 Elixir Cross Referencer(https://elixir.bootlin.com)中阅读内核源码,可以跳转到任意函数定义,事半功倍。推荐搭配《Linux设备驱动开发详解》第四版一起读。
▍七、总结:CAN 驱动开发知识速查
• 驱动框架:SPI接口用spi_driver,内存映射用 platform_driver,核心是 net_device + can_priv
• probe 函数:alloc_candev → 填充 ops → 硬件初始化 → register_candev → request_irq
• 发送路径:ndo_start_xmit → 取 can_frame → SPI写TX缓冲 → 触发发送 → TX中断 → wake_queue
• 接收路径:RX中断 → 读RX缓冲 → alloc_can_skb → 填充can_frame → netif_rx
• 波特率:实现 can_bittiming_const + do_set_bittiming,内核自动计算参数,驱动写寄存器
• 参考代码:drivers/net/can/spi/mcp251x.c 是学习 CAN 驱动的最佳范本
下期预告 🔔
CAN总线④:应用层开发实战——真实项目代码全公开。 周期报文调度、UDS诊断服务框架、多节点通信架构设计,带你把前三期的所有知识落地成可运行的工程代码。
喜欢这篇文章?点个「在看」,让更多工程师看到它 👇
关注「科技探索员」,每期深入一个技术点,不讲玄学,只讲干货。