1. Linux驱动开发概述
Linux驱动开发是嵌入式系统开发中的核心技能之一。作为连接硬件与操作系统的桥梁,驱动程序的质量直接影响系统的稳定性和性能。Linux内核将设备驱动分为三大类:字符设备驱动、块设备驱动和网络设备驱动,每类驱动都有其独特的特点和应用场景。
在嵌入式领域,驱动开发工程师需要深入理解硬件工作原理,同时掌握Linux内核的驱动框架。与裸机开发不同,Linux驱动开发需要遵循内核提供的接口规范,这使得驱动具有更好的可移植性和可维护性。典型的驱动开发流程包括:硬件寄存器操作、设备号管理、文件操作接口实现、中断处理等。
2. Linux驱动分类详解
2.1 字符设备驱动
字符设备是Linux中最常见的设备类型,其特点是按字节流进行顺序读写。常见的字符设备包括:
- 简单IO设备:LED、按键、GPIO等
- 串行通信接口:UART、I2C、SPI等
- 音频设备:声卡、音频编解码器等
- 显示设备:LCD控制器、帧缓冲等
字符设备驱动的核心是实现file_operations结构体中的各种操作函数,如open、read、write、ioctl等。开发者需要根据具体硬件特性实现这些函数接口。
2.2 块设备驱动
块设备驱动用于管理存储设备,其特点是:
- 数据以固定大小的块为单位进行读写
- 支持随机访问
- 通常带有缓存机制
常见的块设备包括:
- 硬盘驱动器(HDD/SSD)
- SD卡、eMMC存储
- U盘等USB存储设备
- 虚拟块设备(如ramdisk)
由于块设备驱动的复杂性,大多数情况下开发者直接使用内核提供的标准驱动,如MMC子系统、USB大容量存储驱动等。
2.3 网络设备驱动
网络设备驱动处理各种网络接口的通信,包括:
- 有线网络:以太网控制器(如FEC、MACB等)
- 无线网络:WiFi、蓝牙等
- 特殊网络协议:CAN总线等
网络设备驱动不遵循文件操作接口,而是通过net_device结构体与内核网络栈交互。一个物理设备可能同时属于多种设备类型,如USB WiFi模块既是字符设备(USB接口),又是网络设备(网络功能)。
3. 字符设备驱动开发详解
3.1 传统字符设备注册方法
传统方法使用register_chrdev函数注册字符设备:
c复制static int __init mydriver_init(void)
{
int ret;
ret = register_chrdev(MAJOR_NUM, "mydriver", &fops);
if (ret < 0) {
printk(KERN_ERR "Failed to register char device\n");
return ret;
}
return 0;
}
这种方法简单直接,但存在以下问题:
- 需要手动分配主设备号,容易冲突
- 会占用整个主设备号范围(256个次设备号),造成资源浪费
- 需要手动创建设备节点(通过mknod命令)
3.2 现代字符设备注册方法
现代驱动推荐使用cdev接口,流程如下:
3.2.1 设备号分配
c复制dev_t devid;
if (alloc_chrdev_region(&devid, 0, 1, "mydriver") < 0) {
return -EFAULT;
}
动态分配避免了设备号冲突问题,更灵活可靠。
3.2.2 初始化cdev结构
c复制struct cdev mycdev;
cdev_init(&mycdev, &fops);
mycdev.owner = THIS_MODULE;
cdev_init函数将file_operations与cdev绑定。
3.2.3 添加cdev到系统
c复制if (cdev_add(&mycdev, devid, 1) < 0) {
unregister_chrdev_region(devid, 1);
return -EFAULT;
}
3.2.4 自动创建设备节点
c复制struct class *myclass;
struct device *mydevice;
myclass = class_create(THIS_MODULE, "mydriver_class");
if (IS_ERR(myclass)) {
cdev_del(&mycdev);
unregister_chrdev_region(devid, 1);
return PTR_ERR(myclass);
}
mydevice = device_create(myclass, NULL, devid, NULL, "mydriver");
if (IS_ERR(mydevice)) {
class_destroy(myclass);
cdev_del(&mycdev);
unregister_chrdev_region(devid, 1);
return PTR_ERR(mydevice);
}
通过udev机制自动创建/dev下的设备节点,无需手动mknod。
3.3 文件操作接口实现
典型的file_operations实现示例:
c复制static const struct file_operations fops = {
.owner = THIS_MODULE,
.open = mydriver_open,
.release = mydriver_release,
.read = mydriver_read,
.write = mydriver_write,
.unlocked_ioctl = mydriver_ioctl,
.poll = mydriver_poll,
};
每个函数指针都需要根据硬件特性实现对应的功能。
4. 驱动开发关键技术
4.1 内存管理与MMU
在启用MMU的系统中,驱动程序不能直接访问物理地址,必须通过以下步骤:
- 使用ioremap映射物理地址到内核虚拟地址空间
c复制void __iomem *regs = ioremap(PHYS_ADDR, SIZE);
- 使用标准IO访问函数读写寄存器
c复制u32 val = readl(regs + OFFSET);
writel(new_val, regs + OFFSET);
- 使用iounmap释放映射
c复制iounmap(regs);
4.2 中断处理
Linux中断处理分为上半部和下半部:
- 申请中断号
c复制request_irq(irq_num, handler, flags, name, dev);
- 实现中断处理函数
c复制static irqreturn_t myhandler(int irq, void *dev_id)
{
/* 上半部处理:紧急任务 */
tasklet_schedule(&mytasklet); /* 触发下半部 */
return IRQ_HANDLED;
}
- 实现下半部机制(如tasklet)
c复制DECLARE_TASKLET(mytasklet, tasklet_func, 0);
void tasklet_func(unsigned long data)
{
/* 下半部处理:非紧急任务 */
}
4.3 并发控制
常用并发控制机制包括:
- 自旋锁(spinlock):适用于短时间锁定
c复制spinlock_t mylock;
spin_lock_init(&mylock);
spin_lock(&mylock);
/* 临界区 */
spin_unlock(&mylock);
- 互斥锁(mutex):可睡眠的锁
c复制struct mutex mymutex;
mutex_init(&mymutex);
mutex_lock(&mymutex);
/* 临界区 */
mutex_unlock(&mymutex);
- 信号量(semaphore):控制资源访问数量
c复制struct semaphore mysem;
sema_init(&mysem, 1);
down(&mysem);
/* 临界区 */
up(&mysem);
5. 设备树在驱动中的应用
5.1 设备树基础
现代Linux驱动广泛使用设备树(Device Tree)来描述硬件配置,主要优点:
- 硬件描述与内核代码分离
- 支持动态硬件配置
- 提高驱动可移植性
设备树源文件(.dts)编译后生成二进制文件(.dtb),由Bootloader传递给内核。
5.2 驱动中解析设备树
驱动中获取设备树节点的典型流程:
- 定义of_device_id匹配表
c复制static const struct of_device_id mydriver_of_match[] = {
{ .compatible = "vendor,mydriver" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, mydriver_of_match);
- 在probe函数中解析节点
c复制static int mydriver_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
u32 reg_val;
if (!np)
return -ENODEV;
if (of_property_read_u32(np, "reg", ®_val))
return -EINVAL;
/* 使用解析到的参数初始化硬件 */
...
}
- 注册platform驱动
c复制static struct platform_driver mydriver_driver = {
.driver = {
.name = "mydriver",
.of_match_table = mydriver_of_match,
},
.probe = mydriver_probe,
.remove = mydriver_remove,
};
module_platform_driver(mydriver_driver);
6. 高级驱动技术
6.1 阻塞与非阻塞I/O
驱动需要同时支持阻塞和非阻塞访问模式:
c复制static ssize_t mydriver_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
struct mydevice *dev = filp->private_data;
if (filp->f_flags & O_NONBLOCK) {
/* 非阻塞模式:立即返回 */
if (!device_ready(dev))
return -EAGAIN;
} else {
/* 阻塞模式:等待数据就绪 */
if (wait_event_interruptible(dev->waitq, device_ready(dev)))
return -ERESTARTSYS;
}
/* 数据传输 */
...
}
6.2 异步通知
实现异步通知需要以下步骤:
- 支持fasync操作
c复制static int mydriver_fasync(int fd, struct file *filp, int on)
{
struct mydevice *dev = filp->private_data;
return fasync_helper(fd, filp, on, &dev->async_queue);
}
- 在适当时候发送信号
c复制if (dev->async_queue)
kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
- 在file_operations中添加fasync支持
c复制.fasync = mydriver_fasync,
6.3 工作队列
对于需要延后执行的复杂任务,可以使用工作队列:
c复制static struct workqueue_struct *my_wq;
static DECLARE_WORK(my_work, my_work_fn);
static void my_work_fn(struct work_struct *work)
{
/* 在进程上下文执行的任务 */
...
}
/* 调度工作 */
queue_work(my_wq, &my_work);
7. 驱动调试技巧
7.1 printk调试
printk是驱动调试的基本工具,支持不同日志级别:
c复制printk(KERN_DEBUG "Debug message\n"); /* 调试信息 */
printk(KERN_INFO "Informational\n"); /* 普通信息 */
printk(KERN_NOTICE "Normal condition\n"); /* 正常但重要 */
printk(KERN_WARNING "Warning\n"); /* 警告信息 */
printk(KERN_ERR "Error condition\n"); /* 错误条件 */
printk(KERN_CRIT "Critical condition\n"); /* 严重情况 */
可以通过/proc/sys/kernel/printk调整控制台日志级别。
7.2 动态调试
动态调试(Dynamic Debug)更灵活:
- 在代码中添加pr_debug()
c复制pr_debug("Value=%d\n", val);
-
编译时开启CONFIG_DYNAMIC_DEBUG
-
运行时控制
bash复制echo 'file mydriver.c +p' > /sys/kernel/debug/dynamic_debug/control
7.3 使用proc和sysfs
导出调试信息到用户空间:
c复制/* proc接口示例 */
static int mydriver_proc_show(struct seq_file *m, void *v)
{
seq_printf(m, "Driver status:\n");
seq_printf(m, "Registers: 0x%08x\n", read_reg());
return 0;
}
/* sysfs接口示例 */
static ssize_t debug_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
return sprintf(buf, "%d\n", debug_value);
}
8. 驱动开发实践建议
- 代码规范:遵循内核编码风格(Documentation/process/coding-style.rst)
- 错误处理:全面检查所有可能失败的操作,提供有意义的错误码
- 资源管理:确保在出错路径和模块卸载时释放所有资源
- 版本兼容:考虑不同内核版本的API变化,使用适当的宏和条件编译
- 文档注释:为关键函数和数据结构添加详细注释
- 性能考量:避免在中断上下文中进行耗时操作,合理使用缓冲和缓存
- 安全防护:验证所有用户空间传入参数,防止缓冲区溢出等安全问题
通过掌握这些Linux驱动开发的核心概念和技术,开发者能够为各种硬件设备创建高效、稳定的驱动程序,满足嵌入式系统和通用计算平台的需求。