3、Linux内核CAN子系统:SocketCAN架构、CAN设备驱动框架、MTK MCP251x/FlexCAN驱动适配
好,我们进入第三章。这一章,咱们要啃的是一块硬骨头——Linux内核里的CAN子系统。
说实话,我早年做车载项目时,对这块也是一知半解。直到有一次,在MTK平台上调试CAN通信,死活收不到数据,折腾了三天。最后发现,问题出在驱动框架的理解上。从那以后,我花了大把时间把SocketCAN的源码啃了一遍。嗯,今天就把这些经验分享给你。
3.1 SocketCAN架构:为什么它是Linux CAN的基石?
SocketCAN,说白了,就是把CAN总线抽象成了一个网络设备。你想想看,Linux对网络设备的支持有多成熟?驱动、协议栈、套接字接口,一整套都是现成的。SocketCAN就是借用了这套体系。
它的核心思想很简单:让应用程序像操作网络socket一样操作CAN总线。
具体来说,SocketCAN在Linux内核中分了三层:
- CAN设备驱动层:负责和硬件打交道,收发CAN帧。比如MCP2515、FlexCAN这些控制器,驱动都在这一层。
- CAN协议族层:实现了PF_CAN协议族,提供了RAW、BCM、ISO-TP等协议。你调用
socket(PF_CAN, SOCK_RAW, CAN_RAW),就是走的这一层。 - 网络设备层:把CAN设备注册成网络接口,比如
can0、can1。这样你就可以用ifconfig、ip link这些工具来管理它。
我个人习惯,把SocketCAN比作一个「万能插座」。不管底层是什么CAN控制器,上层应用看到的接口都是一样的。这给移植带来了极大的便利。
核心要点:SocketCAN不是重新发明轮子,而是把CAN总线「伪装」成了网络设备。理解这一点,后面的驱动开发就顺了。
3.2 CAN设备驱动框架:内核是怎么管理CAN硬件的?
Linux内核的CAN设备驱动,遵循的是网络设备驱动框架。但CAN毕竟不是以太网,所以有一些特殊的处理。
驱动框架的核心结构体是struct net_device和struct can_priv。前者是通用的网络设备结构,后者是CAN设备私有的数据。
我画个简化的流程给你看:
// 驱动入口
static int __init mcp251x_init(void)
{
// 1. 注册SPI驱动(MCP2515是SPI接口)
return spi_register_driver(&mcp251x_driver);
}
// probe函数
static int mcp251x_can_probe(struct spi_device *spi)
{
struct net_device *net;
struct mcp251x_priv *priv;
// 2. 分配net_device结构体
net = alloc_candev(sizeof(struct mcp251x_priv), TX_ECHO_SKB_MAX);
// 3. 设置网络设备操作函数
net->netdev_ops = &mcp251x_netdev_ops;
// 4. 设置CAN控制器的时钟、中断等
priv = netdev_priv(net);
priv->spi = spi;
priv->can.clock.freq = clk_get_rate(clk);
// 5. 注册网络设备
register_candev(net);
return 0;
}
你看,流程很清晰。但这里有个坑——CAN设备的打开和关闭。
普通的网络设备,open函数里可能只是启动一下DMA。但CAN设备不一样,你得配置波特率、设置滤波器、启动CAN控制器。我曾经在MCP2515的驱动里,忘了在open时重新配置波特率,结果每次重启后,波特率都变成了默认值。嗯,这个问题排查了很久。
避坑指南:CAN设备的open函数,一定要重新初始化硬件状态。因为设备可能被close后又open,硬件状态不会自动恢复。
3.3 MTK MCP251x驱动适配:从SPI到CAN的桥梁
MTK平台上的MCP251x适配,核心工作就是把SPI通信封装成CAN网络设备。
MCP2515是一个SPI接口的CAN控制器。MTK的SoC通常有多个SPI控制器,你需要选择一个空闲的,把MCP2515接上去。
驱动适配的关键点有三个:
- SPI通信的可靠性:MCP2515的SPI时钟频率不能太高,我一般设置在10MHz以下。频率高了,数据容易出错。
- 中断处理:MCP2515通过中断引脚通知CPU有数据到达。中断处理函数里,要尽快把数据读出来,否则会被覆盖。
- 波特率配置:MCP2515的波特率由CAN时钟和分频系数决定。MTK平台的CAN时钟可能不是标准的,需要仔细计算。
给你看一段实际的波特率配置代码:
static int mcp251x_set_bittiming(struct net_device *net)
{
struct mcp251x_priv *priv = netdev_priv(net);
struct can_bittiming *bt = &priv->can.bittiming;
u32 brp, tseg1, tseg2, sjw;
// 计算波特率预分频器
brp = priv->can.clock.freq / (bt->bitrate * (bt->tq));
// 设置MCP2515的寄存器
mcp251x_write_reg(priv, CNF1, (sjw << 6) | brp);
mcp251x_write_reg(priv, CNF2, (bt->phase_seg1 << 4) | bt->prop_seg);
mcp251x_write_reg(priv, CNF3, bt->phase_seg2);
return 0;
}
这段代码看起来简单,但实际调试时,我遇到过波特率不准导致通信失败的情况。后来发现,是MTK平台的CAN时钟频率和MCP2515的预期不一致。解决办法是,在设备树里明确指定时钟频率。
注意:MTK平台的时钟管理比较复杂。MCP2515的时钟源可能来自SoC的某个PLL输出,一定要确认这个时钟的频率是否在MCP2515的允许范围内(通常是4MHz-20MHz)。
3.4 FlexCAN驱动适配:MTK平台上的硬核CAN
FlexCAN是NXP(原Freescale)的IP核,集成在i.MX系列处理器中。但MTK平台也有类似的集成CAN控制器,比如MT8676内部就集成了CAN模块。
FlexCAN驱动的适配,和MCP2515完全不同。MCP2515是外挂的SPI设备,而FlexCAN是内存映射的。你直接操作寄存器就行了。
驱动框架的核心是struct flexcan_priv:
struct flexcan_priv {
struct can_priv can; // CAN核心结构体
struct net_device *dev; // 网络设备
void __iomem *regs; // 寄存器基地址
int irq; // 中断号
struct clk *clk; // 时钟
// ... 其他私有数据
};
适配FlexCAN时,我最头疼的是邮箱(Mailbox)的管理。FlexCAN有多个邮箱,每个邮箱可以配置成发送或接收。你得合理分配这些邮箱,才能实现高效的CAN通信。
我个人的做法是:
- 保留2个邮箱用于发送(一个用于正常发送,一个用于紧急发送)
- 其余邮箱全部用于接收
- 每个接收邮箱配置不同的CAN ID滤波器
这样做的好处是,可以同时接收多个不同ID的CAN帧,不会因为邮箱被占满而丢帧。
经验之谈:FlexCAN的邮箱数量是有限的(通常是16个或32个)。如果你的CAN网络上有大量不同ID的报文,一定要合理规划滤波器。否则,你会看到「接收溢出」的错误。
3.5 驱动适配的常见问题与调试技巧
最后,我总结几个驱动适配时容易踩的坑:
| 问题 | 现象 | 解决方法 |
|---|---|---|
| CAN帧发送失败 | 应用程序返回-1,errno为EAGAIN | 检查发送邮箱是否被占满,增加发送超时时间 |
| 接收不到数据 | can0上没有任何报文 | 检查滤波器配置,确认CAN总线终端电阻是否接好 |
| 波特率不匹配 | 通信时好时坏,错误帧增多 | 用示波器测量CAN_H/CAN_L的位时间,确认波特率 |
| 中断丢失 | 偶发性丢帧 | 检查中断触发方式,改为边沿触发 |
调试时,我常用的工具是candump和cansend。这两个命令是SocketCAN自带的,非常方便。
比如,你可以这样测试:
# 启动CAN设备
ip link set can0 up type can bitrate 500000
# 监听CAN总线
candump can0
# 发送一帧数据
cansend can0 123#1122334455667788
如果驱动适配正确,你应该能在candump的终端里看到发送出去的报文。
调试技巧:如果candump收不到数据,先检查ip -details link show can0的输出。看看state是不是ERROR-ACTIVE。如果是BUS-OFF,说明总线有严重错误,赶紧检查硬件连接。
好了,这一章的内容就到这里。SocketCAN的架构、驱动框架、MCP251x和FlexCAN的适配,我都给你捋了一遍。下一章,我们会进入更深入的协议栈移植,到时候再聊。