4. CAN驱动开发:Linux SocketCAN框架、CAN设备注册、数据收发API
好,咱们今天聊点硬核的——CAN驱动开发。很多工程师一听到“驱动”两个字就头大,觉得那是内核开发者的专利。其实不然,在Linux环境下,SocketCAN框架已经把大部分脏活累活都封装好了。你只需要搞清楚几个关键环节,就能让CAN设备跑起来。
我个人习惯把CAN驱动开发分成三层来看:设备注册层、网络协议层、应用接口层。今天咱们重点啃设备注册和数据收发API这两块。
核心观点:SocketCAN不是让你从零写驱动,而是让你在现成的框架里“填空”。你填对结构体,调对API,CAN报文就能在应用层和总线之间自由穿梭。
4.1 SocketCAN框架概览
SocketCAN是Linux内核为CAN总线专门打造的一套网络协议栈。它把CAN设备当作网络设备来管理,用套接字(socket)作为用户空间的访问接口。说白了,你操作CAN就像操作TCP/IP一样自然。
框架的核心组件包括:
- CAN设备驱动:负责硬件初始化、中断处理、寄存器读写
- CAN核心层:提供设备注册、协议族管理、过滤规则匹配
- CAN协议族:AF_CAN,定义原始套接字、广播管理套接字等类型
- CAN网络设备:以net_device结构体呈现,通过rtnetlink管理
我记得第一次接触SocketCAN时,最让我惊讶的是它居然用struct sock来管理CAN连接。你想想看,一个面向流媒体的网络栈,硬是被拿来处理CAN这种短帧、实时性要求高的总线——Linux内核社区的工程师们确实有两把刷子。
4.2 CAN设备注册流程
设备注册是整个驱动开发的起点。没有注册,内核就不知道你的CAN控制器存在。注册流程大致分四步:
- 分配net_device结构体:调用
alloc_candev(),指定私有数据大小和TX队列深度 - 填充操作函数集:实现
struct net_device_ops中的ndo_open、ndo_stop、ndo_start_xmit - 设置硬件参数:配置时钟频率、位时序、中断号、DMA通道
- 注册到内核:调用
register_candev(),让系统识别这个设备
来看一段典型的注册代码:
static int mycan_probe(struct platform_device *pdev)
{
struct net_device *ndev;
struct mycan_priv *priv;
int err;
// 1. 分配CAN网络设备
ndev = alloc_candev(sizeof(struct mycan_priv), TX_ECHO_SKB_MAX);
if (!ndev)
return -ENOMEM;
priv = netdev_priv(ndev);
priv->ndev = ndev;
// 2. 设置网络设备操作函数
ndev->netdev_ops = &mycan_netdev_ops;
ndev->flags |= IFF_ECHO; // 需要硬件回显
// 3. 设置硬件相关参数
priv->can.clock.freq = 8000000; // 8MHz时钟
priv->can.bittiming.bitrate = 500000; // 500kbps
priv->can.do_set_bittiming = mycan_set_bittiming;
// 4. 注册CAN设备
err = register_candev(ndev);
if (err) {
free_candev(ndev);
return err;
}
platform_set_drvdata(pdev, ndev);
return 0;
}
经验之谈:我曾经在某个项目里忘了设置IFF_ECHO标志,结果上层应用收不到自己发送的报文,排查了整整两天。这个标志告诉内核:硬件会回显发送的报文,不需要软件模拟。如果你的CAN控制器支持硬件回显,一定要加上。
4.3 数据收发API详解
设备注册好了,接下来就是收发数据。SocketCAN的数据收发API分为内核态和用户态两套。咱们先看内核态,再看用户态。
4.3.1 内核态发送:ndo_start_xmit
这是驱动发送报文的入口函数。当上层调用dev_queue_xmit()时,最终会回调到这里。你需要做的是:
- 从sk_buff中提取CAN帧
- 将帧数据写入硬件发送寄存器
- 触发硬件发送
- 处理发送完成中断
static netdev_tx_t mycan_start_xmit(struct sk_buff *skb, struct net_device *ndev)
{
struct mycan_priv *priv = netdev_priv(ndev);
struct can_frame *cf = (struct can_frame *)skb->data;
u32 id, dlc, data[2];
// 检查硬件是否空闲
if (priv->tx_busy) {
netdev_err(ndev, "TX buffer full\n");
return NETDEV_TX_BUSY;
}
// 解析CAN帧
id = cf->can_id;
dlc = cf->can_dlc;
data[0] = le32_to_cpu(*(u32 *)&cf->data[0]);
data[1] = le32_to_cpu(*(u32 *)&cf->data[4]);
// 写入硬件寄存器
mycan_write_tx_regs(priv, id, dlc, data);
// 触发发送
mycan_start_tx(priv);
// 记录回显skb
can_put_echo_skb(skb, priv->ndev, 0);
return NETDEV_TX_OK;
}
4.3.2 内核态接收:中断处理
接收通常由中断驱动。当硬件收到报文,触发中断,你在中断处理函数里读取数据,然后通过netif_rx()或napi_gro_receive()把sk_buff上交给协议栈。
static irqreturn_t mycan_rx_irq(int irq, void *dev_id)
{
struct net_device *ndev = dev_id;
struct mycan_priv *priv = netdev_priv(ndev);
struct sk_buff *skb;
struct can_frame *cf;
u32 id, dlc, data[2];
// 读取硬件接收寄存器
mycan_read_rx_regs(priv, &id, &dlc, data);
// 分配sk_buff
skb = alloc_can_skb(priv->ndev, &cf);
if (!skb) {
netdev_err(ndev, "RX: OOM\n");
return IRQ_HANDLED;
}
// 填充CAN帧
cf->can_id = id;
cf->can_dlc = dlc;
*(u32 *)&cf->data[0] = cpu_to_le32(data[0]);
*(u32 *)&cf->data[4] = cpu_to_le32(data[1]);
// 上交协议栈
netif_rx(skb);
return IRQ_HANDLED;
}
注意:中断处理函数里不要做耗时操作。我曾经见过有人在中断里打印调试信息,结果CAN总线直接超载,丢帧率飙升到30%。中断里只做最必要的事——读数据、构造skb、上交,其他事情交给下半部或工作队列。
4.3.3 用户态收发API
用户态编程就简单多了。你只需要创建一个原始CAN套接字,绑定到具体设备,然后就能用read()/write()或send()/recv()来收发报文。
#include <linux/can.h>
#include <linux/can/raw.h>
#include <sys/socket.h>
#include <net/if.h>
int main()
{
int s;
struct sockaddr_can addr;
struct ifreq ifr;
struct can_frame frame;
// 1. 创建套接字
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
// 2. 指定设备
strcpy(ifr.ifr_name, "can0");
ioctl(s, SIOCGIFINDEX, &ifr);
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
// 3. 绑定
bind(s, (struct sockaddr *)&addr, sizeof(addr));
// 4. 发送
frame.can_id = 0x123;
frame.can_dlc = 4;
frame.data[0] = 0xDE;
frame.data[1] = 0xAD;
frame.data[2] = 0xBE;
frame.data[3] = 0xEF;
write(s, &frame, sizeof(frame));
// 5. 接收
read(s, &frame, sizeof(frame));
printf("Received ID: 0x%03X, DLC: %d\n", frame.can_id, frame.can_dlc);
close(s);
return 0;
}
这段代码看起来简单,但实际项目中要注意的点不少。比如,你需要在发送前设置CAN帧的can_id格式——标准帧用11位,扩展帧用29位,还要设置CAN_EFF_FLAG标志。我见过有人把扩展帧的ID直接塞进11位字段里,结果接收方死活解析不对。
4.4 核心数据结构对比
为了让你更清晰地理解,我把几个关键数据结构整理成了一张表:
| 数据结构 | 所在层级 | 主要用途 | 关键字段 |
|---|---|---|---|
struct net_device |
内核网络层 | 表示一个网络设备,包含操作函数、状态、统计信息 | netdev_ops, flags, mtu, dev_addr |
struct can_priv |
CAN核心层 | CAN设备私有数据,包含时钟、位时序、状态、中断 | clock, bittiming, state, restart_ms |
struct can_frame |
协议层 | CAN帧的抽象表示,用于用户态和内核态之间传递 | can_id, can_dlc, data[8] |
struct sk_buff |
内核网络层 | 套接字缓冲区,承载网络层和驱动层之间的数据 | data, len, dev, protocol |
这张表我建议你保存下来。每次写驱动时对照着看,能少走很多弯路。
4.5 SocketCAN驱动架构图
下面这张SVG图展示了SocketCAN驱动的完整架构和数据流向。你可以看到用户空间、内核空间、硬件之间的协作关系。
4.6 避坑指南
最后,分享几个我在实际项目中踩过的坑:
- 回显skb管理:发送完成后一定要调用
can_put_echo_skb()和can_get_echo_skb(),否则内核会内存泄漏。我曾经因为漏了can_put_echo_skb,系统跑了三天后OOM崩溃。 - 中断上下文限制:中断里不能调用
alloc_can_skb()?其实可以,但要用GFP_ATOMIC标志。用GFP_KERNEL会导致睡眠,内核直接报错。 - 位时序配置:不要硬编码位时序参数。不同晶振、不同波特率下,采样点位置差异很大。我建议用
can_calc_bittiming()辅助函数自动计算。 - 设备命名冲突:如果系统里有多个CAN设备,注册顺序可能导致
can0、can1混乱。建议在设备树或平台数据中指定别名。
调试技巧:用ip -details link show can0可以查看CAN设备的详细状态,包括错误计数器、位时序参数、状态寄存器。这个命令比任何调试器都好用。
嗯,关于CAN驱动开发的核心内容就这些。SocketCAN框架的设计思路很清晰——把CAN设备当作网络设备来管理,复用Linux内核成熟的网络栈。你只要掌握了设备注册、数据收发API、中断处理这三个环节,就能写出稳定可靠的CAN驱动。