在 Ubuntu 系统中,驱动开发本质上是 Linux 内核驱动开发(Ubuntu 基于 Linux 内核)。CAN 驱动作为一种特殊的硬件驱动,其开发流程遵循 Linux 内核驱动的通用规范,同时需适配 CAN 总线特性(如 SocketCAN 子系统)。以下是详细的开发流程:
一、通用 Linux 驱动开发流程(Ubuntu 环境)
Ubuntu 驱动开发的核心是为 Linux 内核编写驱动模块(.ko 文件),流程如下:
1. 开发环境搭建
- 安装内核源码:驱动需与内核版本匹配,需安装目标内核的源码和开发包:
# 查看当前内核版本 uname -r # 安装对应版本的内核源码和开发包 sudo apt-get install linux-source-$(uname -r) linux-headers-$(uname -r) - 安装编译工具:
sudo apt-get install build-essential gcc make libssl-dev bc flex bison - 配置开发目录:将内核源码解压到工作目录(如
/usr/src/linux-source-xxx),并生成配置文件:cd /usr/src/linux-source-xxx sudo cp /boot/config-$(uname -r) .config # 复用当前内核配置 sudo make oldconfig # 更新配置(默认即可)
2. 需求分析与硬件调研
- 明确硬件特性:确定 CAN 硬件类型(如集成在 SoC 的 CAN 控制器、USB-to-CAN 适配器、PCIe CAN 卡等),获取硬件手册(datasheet),了解:
- 寄存器布局(初始化、发送、接收、中断等寄存器);
- 通信时序(波特率生成逻辑、时钟源);
- 中断/DMA 机制(数据收发的触发方式);
- 支持的 CAN 协议(CAN 2.0A/B、CAN FD 等)。
- 确定驱动类型:CAN 驱动在 Linux 中通常作为 网络设备驱动(因 SocketCAN 将 CAN 设备抽象为网络接口),需遵循 Linux 网络设备模型(
struct net_device)。
3. 驱动框架设计
基于 Linux 内核的网络设备驱动框架和 SocketCAN 子系统,设计驱动核心模块:
- 模块入口/出口:
module_init()注册驱动,module_exit()注销驱动。 - 设备匹配:通过
platform_driver或usb_driver等框架(根据硬件总线类型)与硬件匹配(如设备树节点、USB VID/PID)。 - 核心功能模块:
- 设备初始化(
open()):配置 CAN 控制器时钟、波特率、工作模式; - 数据发送(
hard_start_xmit()):将上层数据(SocketCAN 传递的 CAN 帧)写入硬件发送缓冲区; - 数据接收(中断/DMA 处理):硬件接收数据后触发中断,驱动读取数据并提交给 SocketCAN;
- 中断处理(
irq_handler_t):处理发送完成、接收完成、错误等中断; - 设备关闭(
stop()):停止 CAN 控制器,释放资源。
- 设备初始化(
4. 编码实现
遵循 Linux 内核编码规范(如缩进、命名、注释),核心代码结构示例:
#include <linux/module.h>
#include <linux/netdevice.h>
#include <linux/can/dev.h> // SocketCAN 设备接口
#include <linux/platform_device.h>
// CAN 控制器私有数据结构
struct my_can_priv {
struct net_device *dev; // 网络设备对象
void __iomem *regs; // 寄存器映射地址
int irq; // 中断号
// ... 其他硬件相关状态(发送缓冲区、接收缓冲区等)
};
// 发送函数:将 CAN 帧写入硬件
static netdev_tx_t my_can_xmit(struct sk_buff *skb, struct net_device *dev) {
struct my_can_priv *priv = netdev_priv(dev);
struct can_frame *cf = (struct can_frame *)skb->data;
// 1. 检查硬件发送缓冲区是否空闲
// 2. 将 cf 中的 ID、数据等写入硬件发送寄存器
// 3. 启动发送
// ...
netdev_kfree_skb(skb); // 释放 skb
return NETDEV_TX_OK;
}
// 中断处理函数
static irqreturn_t my_can_irq(int irq, void *dev_id) {
struct net_device *dev = dev_id;
struct my_can_priv *priv = netdev_priv(dev);
u32 status = readl(priv->regs + STATUS_REG); // 读取状态寄存器
if (status & RX_INT) { // 接收中断
// 1. 从硬件接收寄存器读取 CAN 帧
// 2. 封装为 skb 并提交给 SocketCAN(can_rx(dev, skb))
} else if (status & TX_INT) { // 发送完成中断
// 清理发送状态,唤醒等待队列等
}
// ... 处理错误中断等
return IRQ_HANDLED;
}
// 设备打开/初始化函数
static int my_can_open(struct net_device *dev) {
struct my_can_priv *priv = netdev_priv(dev);
// 1. 使能 CAN 控制器时钟
// 2. 配置波特率(根据 dev->can.ctrlmode 中的参数)
// 3. 使能接收/发送中断
// ...
netif_start_queue(dev); // 启动发送队列
return 0;
}
// 网络设备操作集
static const struct net_device_ops my_can_netdev_ops = {
.ndo_open = my_can_open,
.ndo_stop = my_can_stop, // 需实现关闭逻辑
.ndo_start_xmit = my_can_xmit,
// ... 其他操作(如设置波特率、获取统计信息等)
};
// 平台驱动 probe 函数(硬件匹配时调用)
static int my_can_probe(struct platform_device *pdev) {
struct net_device *dev;
struct my_can_priv *priv;
int ret;
// 1. 分配网络设备
dev = alloc_candev(sizeof(struct my_can_priv), 0); // SocketCAN 专用分配函数
if (!dev) return -ENOMEM;
// 2. 映射寄存器地址(从设备树获取物理地址)
priv = netdev_priv(dev);
priv->regs = devm_ioremap_resource(&pdev->dev, &res); // res 从设备树解析
if (IS_ERR(priv->regs)) return PTR_ERR(priv->regs);
// 3. 获取中断号(从设备树)
priv->irq = platform_get_irq(pdev, 0);
ret = devm_request_irq(&pdev->dev, priv->irq, my_can_irq, 0, "my_can", dev);
if (ret) return ret;
// 4. 初始化网络设备操作集
dev->netdev_ops = &my_can_netdev_ops;
// ... 初始化其他 CAN 相关参数(如支持的模式、最大数据长度等)
// 5. 注册网络设备
ret = register_netdev(dev);
if (ret) {
free_candev(dev);
return ret;
}
platform_set_drvdata(pdev, dev);
return 0;
}
// 平台驱动结构体
static const struct platform_driver my_can_driver = {
.probe = my_can_probe,
.remove = my_can_remove, // 需实现注销逻辑
.driver = {
.name = "my-can-controller", // 与设备树中 compatible 匹配
.of_match_table = my_can_of_match, // 设备树匹配表
},
};
module_platform_driver(my_can_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
5. 编写编译脚本(Makefile)
驱动需与内核源码一起编译,Makefile 示例:
obj-m += my_can_driver.o # 驱动模块名
KERNELDIR ?= /lib/modules/$(shell uname -r)/build # 内核源码路径
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules # 编译模块
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean # 清理编译产物
6. 编译与加载测试
- 编译驱动:
make # 生成 my_can_driver.ko - 加载驱动模块:
sudo insmod my_can_driver.ko # 加载模块 lsmod | grep my_can_driver # 确认模块已加载 dmesg | tail # 查看内核日志(检查初始化是否成功) - 验证设备:若驱动正常,会生成 CAN 网络接口(如
can0):ip link show # 查看是否存在 can0 接口
7. 功能测试
使用 SocketCAN 工具测试驱动功能:
- 安装工具:
sudo apt-get install can-utils - 配置 CAN 接口:
sudo ip link set can0 type can bitrate 500000 # 设置波特率 sudo ip link set can0 up # 启动接口 - 发送/接收测试:
若终端1能收到终端2发送的帧,说明驱动基本功能正常。# 终端1:接收 CAN 帧 candump can0 # 终端2:发送 CAN 帧(ID=0x123,数据=0x11 0x22) cansend can0 123#1122
8. 调试与优化
- 内核日志调试:通过
printk()在关键流程打印日志(注意级别,如KERN_INFO、KERN_ERR),用dmesg查看。 - 工具调试:
ftrace:跟踪函数调用流程,分析性能瓶颈;devmem:直接读写硬件寄存器,验证配置是否正确;netstat -i:查看 CAN 接口的统计信息(发送/接收帧数量、错误数)。
- 问题修复:针对测试中发现的问题(如丢帧、波特率错误、中断异常),优化驱动逻辑(如增加缓冲区、调整中断优先级、修复时序配置)。
9. 驱动部署
- 若需长期使用,可将驱动模块复制到
/lib/modules/$(uname -r)/kernel/drivers/net/can/目录,并通过depmod -a更新依赖。 - 若需开机自动加载,可在
/etc/modules中添加模块名。
二、CAN 驱动开发的特殊注意事项
- SocketCAN 子系统集成:CAN 驱动需使用
can_dev.h提供的接口(如alloc_candev()、can_rx()),而非通用网络设备接口,确保与 SocketCAN 协议栈兼容。 - 波特率配置:需根据硬件时钟源和 CAN 协议要求,实现波特率计算逻辑(如通过
ndo_set_config()回调处理用户设置的波特率)。 - 错误处理:CAN 总线错误(如仲裁丢失、位错误)需通过
can_error_report()上报给 SocketCAN,供上层诊断。 - 硬件差异适配:
- 对于 USB-to-CAN 设备,需基于
usb_driver框架开发,通过 USB 端点收发数据; - 对于 PCIe CAN 卡,需基于
pci_driver框架,通过 PCIe 配置空间获取资源。
- 对于 USB-to-CAN 设备,需基于
三、参考资源
- Linux 内核文档:
Documentation/networking/can.rst(SocketCAN 子系统说明); - 内核源码中的 CAN 驱动示例:
drivers/net/can/(如虚拟 CAN 驱动vcan.c、常用控制器驱动slcan.c); - CAN 工具手册:
man candump、man cansend(测试工具用法)。
通过以上流程,可在 Ubuntu 环境中完成 CAN 驱动的开发、测试与部署,核心是遵循 Linux 内核驱动规范并适配 SocketCAN 子系统的要求。

3614

被折叠的 条评论
为什么被折叠?



