一、驱动分类
| 类型 | 访问方式 | 例子 |
|---|---|---|
| 字符设备 | 按字节流 | LED、按键、串口 |
| 块设备 | 按块 | 硬盘、SD 卡 |
| 网络设备 | 按包 | 网卡 |
驱动实验主要是字符设备。
二、驱动的两种编译方式
| 方式 | 做法 | 特点 |
|---|---|---|
| 编译进内核 | obj-y |
开机自动加载,改驱动要重编内核 |
| 编译成模块 | obj-m |
insmod 加载,rmmod 卸载,方便调试 |
开发阶段用模块方式。
三、字符设备驱动核心概念
| 概念 | 说明 |
|---|---|
| 设备号 | 主设备号 + 次设备号,标识驱动和设备 |
| cdev | 内核里描述字符设备的结构体 |
| file_operations | 描述驱动支持的操作 |
| 设备文件 | /dev/ 下的文件,应用通过它访问驱动 |
应用层和驱动层的关系
text
应用层:open("/dev/xxx")
↓ 系统调用
驱动层:xxx_open()
↓ 操作硬件
硬件层:GPIO / I2C / SPI
应用调用 open、read、write、close,驱动里对应的函数就会被调用。
四、字符设备驱动框架
1. 注册流程(固定)
text
1. alloc_chrdev_region → 申请设备号
2. cdev_alloc → 申请 cdev
3. pcdev->ops = &fops → 绑定 file_operations
4. cdev_add → 注册字符设备
记忆口诀:号 → 存 → 绑 → 册。
2. 卸载流程(固定)
text
1. cdev_del → 删除 cdev
2. unregister_chrdev_region → 释放设备号
3. file_operations
c
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = xxx_open,
.release = xxx_release,
.read = xxx_read,
.write = xxx_write,
};
| 函数 | 对应系统调用 |
|---|---|
xxx_open |
open |
xxx_release |
close |
xxx_read |
read |
xxx_write |
write |
4. 数据拷贝
| 函数 | 方向 |
|---|---|
copy_from_user |
用户 → 内核(write) |
copy_to_user |
内核 → 用户(read) |
内核不能直接访问用户空间,必须用这两个函数。
五、自动创建设备节点
c
static struct class *pclass;
static struct device *pdevice;
// 加载时
pclass = class_create(THIS_MODULE, "xxx");
pdevice = device_create(pclass, NULL, devno, NULL, "xxx0");
// 卸载时
device_destroy(pclass, devno);
class_destroy(pclass);
加载后 /dev/xxx0 自动创建,不用 mknod。
六、并发控制
| 机制 | 特点 |
|---|---|
| 原子操作 | 简单计数 |
| 自旋锁 | 不睡眠,关中断 |
| 信号量 | 可以睡眠 |
| 互斥体 | 可以睡眠,最常用 |
多进程同时操作设备时,用互斥体保护临界区。
七、sysfs 调试接口
c
static ssize_t xxx_show(struct device *dev, struct device_attribute *attr, char *buf) { ... }
static ssize_t xxx_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { ... }
static struct device_attribute attr = __ATTR(attr, 0664, xxx_show, xxx_store);
// 加载时
device_create_file(pdevice, &attr);
// 卸载时
device_remove_file(pdevice, &attr);
用户通过 echo 和 cat 直接控制设备,不用写应用程序。
八、设备树
1. 为什么用设备树
把硬件信息从驱动中独立出来,实现设备与驱动分离。
2. pinctrl 节点
dts
&iomuxc {
pinctrl_xxx: xxxgrp {
fsl,pins = <
MX6UL_PAD_XXX__GPIOX_IOXX 0x10B0
>;
};
};
3. 设备节点
dts
xxx {
compatible = "vendor,xxx";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_xxx>;
gpio-xxx = <&gpioX Y 1>;
status = "okay";
};
4. 驱动里读设备树
c
plednode = of_find_node_by_path("/xxx");
ledgpiono = of_get_named_gpio(plednode, "gpio-xxx", 0);
九、驱动通用框架(核心)
1. 整体框架
text
头文件
↓
全局变量(保存设备结构)
↓
操作函数(read / write / ioctl)
↓
file_operations
↓
miscdevice
↓
probe(匹配成功时调用)
↓
remove(卸载时调用)
↓
匹配表(of_device_id + xxx_device_id)
↓
xxx_driver 结构体
↓
xxx_register_driver / xxx_unregister_driver
↓
module_init / module_exit
↓
MODULE_LICENSE
platform、I2C、SPI,框架都一样,只是 API 不同。
2. 通用模板
头文件
c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/xxx.h> // 总线头文件
#include <linux/miscdevice.h>
#include <asm/uaccess.h>
全局变量
c
static struct xxx_device *pdev = NULL;
操作函数
c
static ssize_t xxx_read(struct file *fp, char __user *puser, size_t n, loff_t *off)
{
// 1. 硬件操作
// 2. copy_to_user
return sizeof(data);
}
static ssize_t xxx_write(struct file *fp, const char __user *puser, size_t n, loff_t *off)
{
// 1. copy_from_user
// 2. 硬件操作
return n;
}
file_operations
c
static struct file_operations fops = {
.owner = THIS_MODULE,
.read = xxx_read,
.write = xxx_write,
};
miscdevice
c
static struct miscdevice xxx_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "xxx_misc",
.fops = &fops,
};
probe
c
static int xxx_probe(struct xxx_device *pdev_param)
{
pdev = pdev_param;
// 申请资源、初始化硬件
misc_register(&xxx_misc);
return 0;
}
remove
c
static int xxx_remove(struct xxx_device *pdev_param)
{
misc_deregister(&xxx_misc);
return 0;
}
匹配表
c
static struct xxx_device_id xxx_id_table[] = {
{.name = "xxx"},
{},
};
static struct of_device_id xxx_of_match_table[] = {
{.compatible = "vendor,xxx"},
{},
};
driver 结构体
c
static struct xxx_driver xxx_drv = {
.probe = xxx_probe,
.remove = xxx_remove,
.driver = {
.name = "xxx",
.owner = THIS_MODULE,
.of_match_table = xxx_of_match_table,
},
.id_table = xxx_id_table,
};
注册和注销
c
static int __init xxx_drv_init(void)
{
return xxx_register_driver(&xxx_drv);
}
static void __exit xxx_drv_exit(void)
{
xxx_unregister_driver(&xxx_drv);
}
module_init(xxx_drv_init);
module_exit(xxx_drv_exit);
MODULE_LICENSE("GPL");
十、各种总线的对应关系(必背)
| 项目 | platform | I2C | SPI |
|---|---|---|---|
| 设备结构 | platform_device |
i2c_client |
spi_device |
| 驱动结构 | platform_driver |
i2c_driver |
spi_driver |
| 设备 ID 表 | --- | i2c_device_id |
spi_device_id |
| probe 参数 | platform_device * |
i2c_client * |
spi_device * |
| 注册 | platform_driver_register |
i2c_add_driver |
spi_register_driver |
| 注销 | platform_driver_unregister |
i2c_del_driver |
spi_unregister_driver |
| 传输结构 | --- | i2c_msg |
spi_transfer + spi_message |
| 传输函数 | --- | i2c_transfer |
spi_sync |
规律:xxx_device、xxx_driver、xxx_register_driver、xxx_unregister_driver。
十一、写驱动的通用流程
text
1. 确定总线类型(GPIO / I2C / SPI)
2. 选框架(platform / i2c / spi)
3. 写全局变量(保存设备结构)
4. 写操作函数(read / write)
5. 写 file_operations
6. 写 miscdevice
7. 写 probe(存设备、申请资源、注册 misc)
8. 写 remove(注销 misc)
9. 写匹配表(of_device_id + xxx_device_id)
10. 写 xxx_driver 结构体
11. 写注册和注销
12. module_init / module_exit
13. MODULE_LICENSE
十二、操作函数的区别
| 总线 | 操作函数里的硬件操作 |
|---|---|
| platform | gpio_set_value / gpio_get_value / readl / writel |
| I2C | i2c_transfer / master_xfer |
| SPI | spi_write_then_read / spi_sync |
框架不变,操作函数里的硬件代码不同。
十三、设备树模板
dts
xxx {
compatible = "vendor,xxx";
reg = <...>; // I2C 从机地址 或 SPI 片选编号
spi-max-frequency = <...>; // SPI 用
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_xxx>;
gpio-xxx = <&gpioX Y 1>; // GPIO 用
interrupt-parent = <&gpioX>; // 中断用
interrupts = <Y IRQ_TYPE_EDGE_FALLING>;
status = "okay";
};
十四、中断 + 等待队列
1. 为什么用中断
轮询方式 CPU 占用高,响应慢。中断方式硬件触发,CPU 占用低,响应快。
2. 中断处理
c
static irqreturn_t xxx_handler(int irqno, void *pdev)
{
condition = 1;
wake_up_interruptible(&wq);
return IRQ_HANDLED;
}
// probe 里申请中断
irqno = of_irq_get(pdevice->dev.of_node, 0);
devm_request_irq(dev, irqno, xxx_handler, IRQF_TRIGGER_FALLING, "xxx", NULL);
3. 等待队列
c
static wait_queue_head_t wq;
static int condition = 0;
// probe 里初始化
init_waitqueue_head(&wq);
// read 里阻塞等待
condition = 0;
wait_event_interruptible(wq, condition);
4. 流程
text
应用 read
↓
xxx_read 里 wait_event_interruptible → 进程睡眠
↓
硬件触发中断
↓
xxx_handler → condition = 1,唤醒进程
↓
xxx_read 继续,读数据,返回
十五、中断下半部
1. 为什么需要下半部
中断处理函数不能做耗时操作,否则阻塞其他中断。
| 部分 | 作用 | 特点 |
|---|---|---|
| 上半部 | 紧急操作 | 不可打断,要快 |
| 下半部 | 耗时操作 | 可打断 |
2. tasklet 和工作队列
| 方式 | 上下文 | 能否睡眠 |
|---|---|---|
| tasklet | 中断上下文 | ❌ |
| 工作队列 | 进程上下文 | ✅ |
需要在中断下半部睡眠,必须用工作队列。
十六、input 子系统
Linux 统一管理输入设备(键盘、鼠标、触摸屏、按键)。
c
// 1. 申请 input 设备
pkey_dev = input_allocate_device();
// 2. 说明设备能力
input_set_capability(pkey_dev, EV_KEY, KEY_0);
// 3. 注册 input 设备
input_register_device(pkey_dev);
// 4. 上报事件
input_event(pkey_dev, EV_KEY, KEY_0, 1);
input_sync(pkey_dev);
应用程序从 /dev/input/eventX 读取事件。
十七、内核定时器
c
static void timeout(unsigned long data)
{
pr_info("timeout ok!\n");
}
static DEFINE_TIMER(mtimer, timeout, 0, 0);
// 加载时
init_timer(&mtimer);
add_timer(&mtimer);
mod_timer(&mtimer, jiffies + msecs_to_jiffies(100));
// 卸载时
del_timer(&mtimer);
延时执行函数,超时后内核自动调用 timeout。
十八、核心思想
| 思想 | 说明 |
|---|---|
| 设备与驱动分离 | 硬件信息放设备树,驱动代码不写死地址 |
| 一切皆是文件 | 应用通过 /dev/xxx 访问驱动 |
| 分层设计 | 上层应用、中层驱动、下层硬件 |
| 并发控制 | 多进程访问用锁保护 |
| 中断 + 阻塞 | 提高效率,降低 CPU 占用 |