本篇定位:前面 10 篇铺底,本篇落地到"怎么写一个真驱动"。讲透 platform 驱动(probe/remove)、字符设备(cdev/file_operations)、misc 简化框架、设备树 DTS 匹配。读完能独立写一个完整的字符设备驱动:设备树描述 → platform 驱动匹配 → probe 映射寄存器/注册中断 → file_operations → 用户态 open/read/write/ioctl。
一、Linux 驱动的分类
1.1 三大类驱动
| 类型 | 特点 | 例子 | 接口 |
|---|---|---|---|
| 字符设备 | 按字节流访问,顺序 | UART/SPI/I2C/GPIO/输入 | file_operations(open/read/write/ioctl) |
| 块设备 | 按块访问,可寻址 | 磁盘/SSD/eMMC | bio 请求(13 篇) |
| 网络设备 | 包收发,无 fd | 网卡/WiFi | net_device(14 篇) |
1.2 字符设备BSP 主战场
- SoC 外设(UART/SPI/I2C/GPIO/ADC/DMA 控制器)都是字符设备
- 用户态通过 open/read/write/ioctl 操作
- 本篇重点
1.3 字符设备的访问模型
用户态:
fd = open("/dev/mydev", O_RDWR);
read(fd, buf, n);
write(fd, buf, n);
ioctl(fd, CMD, arg);
close(fd);
内核态(驱动 file_operations):
.open → my_open
.read → my_read
.write → my_write
.ioctl → my_ioctl
.release → my_release
- 用户态操作 /dev/xxx → VFS → 调驱动的 file_operations
- "一切皆文件"在驱动层落地
裸机直接操作寄存器,Linux 经 file_operations
裸机
*(volatile u32*)0x40000000 = 0x55。Linux 用户态不能直接,经 open/read/write/ioctl → 驱动里操作寄存器。多一层 VFS 抽象,带来:① 权限控制;② 统一接口;③ 可重定向。 BSP 要适应"用户态经 fd,内核态经 file_operations"。
二、设备树(DTS)⭐
2.1 为什么需要设备树
- SoC 外设多,寄存器地址/中断/属性各不同
- 老方式:硬编码在内核源码(arch/arm/mach-xxx),每板一份内核,乱
- 设备树:把"硬件描述"从内核代码分离,启动时传给内核
2.2 DTS 结构
dts
// arch/riscv/boot/dts/myboard.dts
/dts-v1/;
/ {
#address-cells = <2>; // 子节点 reg 的地址字段数(64 位)
#size-cells = <2>; // 子节点 reg 的长度字段数
model = "My RISC-V Board";
compatible = "myvendor,myboard";
cpus { ... };
soc {
#address-cells = <2>;
#size-cells = <2>;
compatible = "simple-bus";
ranges;
uart@10000000 {
compatible = "myvendor,uart";
reg = <0x0 0x10000000 0x0 0x100>; // 地址 0x10000000,长度 0x100
interrupts = <10>; // 中断号 10
interrupt-parent = <&plic>;
clock-frequency = <50000000>; // 50MHz
status = "okay";
};
myip@10020000 {
compatible = "myvendor,myip-v1";
reg = <0x0 0x10020000 0x0 0x1000>;
interrupts = <11 12>; // 两个中断
interrupt-parent = <&plic>;
dmas = <&dma 0 1>, <&dma 2 3>; // DMA 引用
my-custom-prop = <42>; // 自定义属性
status = "okay";
};
};
};
2.3 关键属性
| 属性 | 含义 |
|---|---|
compatible |
兼容性字符串,驱动匹配用("vendor,device") |
reg |
寄存器地址 + 长度 |
interrupts |
中断号 |
interrupt-parent |
中断控制器引用 |
clocks |
时钟引用 |
dmas |
DMA 引用 |
status |
"okay"(启用)/"disabled"(禁用) |
phandle |
节点唯一标识(被引用) |
2.4 compatible:匹配的关键
dts
compatible = "myvendor,myip-v1";
- 格式:
"厂商,设备-版本" - 驱动 of_match_table 里写同样的字符串,匹配
- 可多个 :
compatible = "myvendor,myip-v2", "myvendor,myip-v1"(先精确后兼容)
2.5 DTS 编译
bash
# DTS → DTB(二进制)
dtc -I dts -O dtb -o myboard.dtb myboard.dts
# 反编译
dtc -I dtb -O dts myboard.dtb > myboard.dts
# 内核里
make dtbs
- 启动时 U-Boot 把 DTB 传给内核(02 篇)
- 内核解析 DTB,生成 platform_device
2.6 设备树到 platform_device
内核启动:
- 解析 DTB
- status="okay" 的节点(且有 compatible)→ 创建 platform_device
- reg/interrupts 等转成
struct resource - 注册到 platform 总线
- 匹配驱动 → probe
设备树是描述硬件的标准方式
裸机硬件信息硬编码在代码(寄存器地址/中断号)。Linux 用设备树分离------改硬件改 DTS,不动驱动代码。同一驱动跑不同板子(只要 DTS 描述对)。你 BSP 工作:① 写驱动(通用);② 写 DTS(板级)。分离是关键。
三、platform 驱动完整骨架⭐
3.1 最完整 platform 字符设备驱动
c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/mod_devicetable.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/interrupt.h>
#include <linux/slab.h>
#define MY_NAME "myip"
#define MY_BUF_SIZE 256
/* 自定义 ioctl 命令 */
#define MY_IOC_MAGIC 'M'
#define MY_IOC_RESET _IO(MY_IOC_MAGIC, 0)
#define MY_IOC_SETREG _IOW(MY_IOC_MAGIC, 1, int)
#define MY_IOC_GETREG _IOR(MY_IOC_MAGIC, 2, int)
struct my_dev {
struct cdev cdev; // 字符设备
dev_t devt; // 设备号
struct device *dev; // 设备模型
struct class *class;
void __iomem *base; // 寄存器基址(映射后虚拟地址)
int irq; // 中断号
void *buf; // 数据缓冲
size_t buf_len;
spinlock_t lock; // 保护 buf
};
/* ===== file_operations ===== */
static int my_open(struct inode *inode, struct file *filp) {
struct my_dev *dev = container_of(inode->i_cdev, struct my_dev, cdev);
filp->private_data = dev; // 存到 private_data,read/write 用
dev_info(dev->dev, "open\n");
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *off) {
struct my_dev *dev = filp->private_data;
size_t to_copy;
// 简单示例:从内核 buf 拷到用户
if (*off >= dev->buf_len) return 0; // EOF
to_copy = min(count, dev->buf_len - *off);
if (copy_to_user(buf, dev->buf + *off, to_copy))
return -EFAULT;
*off += to_copy;
return to_copy;
}
static ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *off) {
struct my_dev *dev = filp->private_data;
size_t to_copy = min(count, (size_t)MY_BUF_SIZE);
if (copy_from_user(dev->buf, buf, to_copy))
return -EFAULT;
dev->buf_len = to_copy;
// 写寄存器示例
writel(0x55, dev->base + REG_DATA);
return to_copy;
}
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
struct my_dev *dev = filp->private_data;
int val;
// 校验 magic(防错命令)
if (_IOC_TYPE(cmd) != MY_IOC_MAGIC) return -ENOTTY;
switch (cmd) {
case MY_IOC_RESET:
writel(1, dev->base + REG_RESET);
break;
case MY_IOC_SETREG:
if (copy_from_user(&val, (void __user *)arg, sizeof(val)))
return -EFAULT;
writel(val, dev->base + REG_CFG);
break;
case MY_IOC_GETREG:
val = readl(dev->base + REG_CFG);
if (copy_to_user((void __user *)arg, &val, sizeof(val)))
return -EFAULT;
break;
default:
return -ENOTTY;
}
return 0;
}
static int my_release(struct inode *inode, struct file *filp) {
return 0;
}
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
.release = my_release,
};
/* ===== 中断 ===== */
static irqreturn_t my_hardirq(int irq, void *dev_id) {
struct my_dev *dev = dev_id;
u32 status = readl(dev->base + REG_IRQ_STATUS);
if (!status) return IRQ_NONE;
writel(status, dev->base + REG_IRQ_CLEAR); // 清中断
return IRQ_WAKE_THREAD; // 唤醒线程
}
static irqreturn_t my_thread(int irq, void *dev_id) {
struct my_dev *dev = dev_id;
// 可睡的处理
spin_lock(&dev->lock);
// 处理数据...
spin_unlock(&dev->lock);
return IRQ_HANDLED;
}
/* ===== probe / remove ===== */
static int my_probe(struct platform_device *pdev) {
struct my_dev *dev;
struct resource *res;
int ret;
dev_info(&pdev->dev, "probe start\n");
// 1. 分配设备结构(devm_ 自动释放)
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev) return -ENOMEM;
dev->buf = devm_kzalloc(&pdev->dev, MY_BUF_SIZE, GFP_KERNEL);
spin_lock_init(&dev->lock);
platform_set_drvdata(pdev, dev); // 存到 platform_device,remove 用
// 2. 映射寄存器(devm_ 自动取消映射)
dev->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(dev->base)) return PTR_ERR(dev->base);
// 3. 获取中断号 + 注册(devm_ 自动释放)
dev->irq = platform_get_irq(pdev, 0);
ret = devm_request_threaded_irq(&pdev->dev, dev->irq,
my_hardirq, my_thread,
IRQF_ONESHOT | IRQF_TRIGGER_FALLING,
"myip", dev);
if (ret) return ret;
// 4. 申请设备号 + 注册 cdev
ret = alloc_chrdev_region(&dev->devt, 0, 1, MY_NAME);
if (ret) return ret;
cdev_init(&dev->cdev, &my_fops);
dev->cdev.owner = THIS_MODULE;
ret = cdev_add(&dev->cdev, dev->devt, 1);
if (ret) goto err_unreg_chr;
// 5. 创建 class + device(自动建 /dev 节点)
dev->class = class_create(THIS_MODULE, MY_NAME);
if (IS_ERR(dev->class)) { ret = PTR_ERR(dev->class); goto err_del_cdev; }
dev->dev = device_create(dev->class, &pdev->dev, dev->devt, NULL, "myip%d", 0);
if (IS_ERR(dev->dev)) { ret = PTR_ERR(dev->dev); goto err_destroy_class; }
dev_info(&pdev->dev, "probe ok, /dev/myip0\n");
return 0;
err_destroy_class:
class_destroy(dev->class);
err_del_cdev:
cdev_del(&dev->cdev);
err_unreg_chr:
unregister_chrdev_region(dev->devt, 1);
return ret;
}
static int my_remove(struct platform_device *pdev) {
struct my_dev *dev = platform_get_drvdata(pdev);
device_destroy(dev->class, dev->devt);
class_destroy(dev->class);
cdev_del(&dev->cdev);
unregister_chrdev_region(dev->devt, 1);
// devm_ 申请的资源(内存/映射/中断)自动释放,不用手动
dev_info(&pdev->dev, "remove\n");
return 0;
}
/* ===== 设备树匹配表 ===== */
static const struct of_device_id my_of_match[] = {
{ .compatible = "myvendor,myip-v1", },
{ .compatible = "myvendor,myip-v2", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_match); // 用户态 modprobe 也能匹配
/* ===== platform_driver ===== */
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "myip",
.of_match_table = my_of_match,
.owner = THIS_MODULE,
},
};
module_platform_driver(my_driver); // 替代 module_init/exit + register
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Zhaoyuhan");
MODULE_DESCRIPTION("My IP driver");
3.2 骨架的组成部分
| 部分 | 作用 |
|---|---|
| 设备树匹配表 of_match_table | 和 DTS compatible 匹配 |
| platform_driver | probe/remove + driver |
| module_platform_driver | 简化的 module_init/exit |
| probe | 映射寄存器/注册中断/申请设备号/注册 cdev/建设备节点 |
| remove | 反向清理 |
| file_operations | open/read/write/ioctl/release |
| 中断 | hardirq(顶半部) + thread(下半部) |
这个骨架是写 SoC 驱动的模板
写 SoC 外设驱动,90% 套这骨架:① 改 of_match_table;② 改 probe 里的寄存器/中断;③ 改 file_operations。背下来,改着用。面试让你"写个字符设备驱动",就默写这骨架。
四、字符设备(cdev)详解
4.1 设备号(dev_t)
c
dev_t devt;
// 高 12 位:主设备号(major,标识驱动)
// 低 20 位:次设备号(minor,标识实例)
MAJOR(devt); // 取主
MINOR(devt); // 取次
MKDEV(major, minor); // 组合
bash
# 看设备号
ls -l /dev/
# crw-rw-r-- 1 root root 1, 3 Jan 1 00:00 /dev/null
# ^ ^
# 主 次
4.2 申请设备号
c
// 动态申请(推荐)
alloc_chrdev_region(&devt, 0, 1, "mydev");
// baseminor=0, count=1
// 静态(指定主号,老式)
register_chrdev_region(MKDEV(200, 0), 1, "mydev");
// 释放
unregister_chrdev_region(devt, 1);
- 动态推荐(避免冲突)
cat /proc/devices看主设备号
4.3 cdev
c
struct cdev cdev;
cdev_init(&cdev, &fops); // 关联 file_operations
cdev.owner = THIS_MODULE;
cdev_add(&cdev, devt, 1); // 注册到内核(1 个次设备号)
// 之后就活了,open(/dev/xxx) 会调 fops.open
cdev_del(&cdev); // 移除
4.4 file_operations 核心成员
| 成员 | 作用 | 必需 |
|---|---|---|
.owner |
模块引用(THIS_MODULE) | 是 |
.open |
open() 时调 | 通常 |
.release |
close() 时调 | 通常 |
.read |
read() 时调 | 看设备 |
.write |
write() 时调 | 看设备 |
.unlocked_ioctl |
ioctl() 时调 | 控制 |
.poll |
poll/select/epoll | 事件等待 |
.mmap |
映射设备内存 | 高性能 |
.llseek |
改文件偏移 | 看设备 |
.fasync |
异步通知(SIGIO) | 异步 |
.compat_ioctl |
32 位用户态调 64 位驱动 | 兼容 |
4.5 read/write 的参数
c
ssize_t read(struct file *filp, char __user *buf, size_t count, loff_t *off);
// ^ ^ ^ ^ ^
// 文件 用户缓冲 请读字节 当前偏移(可改)
__user:用户态指针,不能直接解引用(06 篇),必须 copy_to_user*off:文件偏移,read/write 后要更新- 返回:实际读/写字节数,0 表 EOF,负数表错误
4.6 copy_to_user / copy_from_user
c
// 用户 → 内核
if (copy_from_user(kernel_buf, user_buf, n)) return -EFAULT;
// 内核 → 用户
if (copy_to_user(user_buf, kernel_buf, n)) return -EFAULT;
- 带地址校验 + page fault 处理
- 非法用户指针返回非 0(不崩)
- 绝对不能直接 memcpy 用户指针(安全漏洞)
4.7 private_data 传递
c
static int my_open(struct inode *inode, struct file *filp) {
struct my_dev *dev = container_of(inode->i_cdev, struct my_dev, cdev);
filp->private_data = dev; // 后续 read/write 用
return 0;
}
static ssize_t my_read(struct file *filp, ...) {
struct my_dev *dev = filp->private_data; // 取回
...
}
- open 时存设备结构到
filp->private_data - read/write/ioctl 取回,不用再查
- 标准模式,所有驱动都这么写
4.8 container_of 宏
c
container_of(ptr, type, member);
// ptr 指向 member,返回指向 type 的指针
- 从内嵌成员指针反推外层结构指针
- 设备模型/字符设备处处用
- C 语言实现继承的关键(从基类指针取派生类)
container_of 是 Linux C-OOP 的核心宏
struct my_dev内嵌struct cdev,内核回调只给你cdev*,你用container_of(cdev_ptr, struct my_dev, cdev)取回my_dev*。这是 C 实现多态的标准手法,内核到处用。你写驱动必会。
五、ioctl:设备控制
5.1 为什么需要 ioctl
- read/write 是数据传输
- "复位/设寄存器/查状态/配置模式"等控制操作 → ioctl
5.2 ioctl 命令编码
c
#include <linux/ioctl.h>
// _IO(type, nr) 无参数
// _IOR(type, nr, size) 读(内核→用户)
// _IOW(type, nr, size) 写(用户→内核)
// _IOWR(type, nr, size) 读写
#define MY_IOC_MAGIC 'M'
#define MY_IOC_RESET _IO(MY_IOC_MAGIC, 0)
#define MY_IOC_SETREG _IOW(MY_IOC_MAGIC, 1, int)
#define MY_IOC_GETREG _IOR(MY_IOC_MAGIC, 2, int)
- magic:1 字节魔数(防错命令,每驱动一个)
- nr:命令序号
- size:参数大小(自动从类型推)
- 编码后 cmd 含 type/nr/size/方向,内核可校验
5.3 ioctl 实现
c
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
if (_IOC_TYPE(cmd) != MY_IOC_MAGIC) return -ENOTTY;
switch (cmd) {
case MY_IOC_RESET: ... break;
case MY_IOC_SETREG: ... break;
default: return -ENOTTY;
}
return 0;
}
六、misc 框架(简化字符设备)
6.1 为什么有 misc
- 字符设备注册步骤多(设备号/cdev/class/device)
- misc 框架简化:一个调用搞定
- 适合"小驱动"(只有一个次设备号,主号 10)
6.2 misc 驱动
c
#include <linux/miscdevice.h>
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.read = my_read,
.ioctl = my_ioctl,
};
static struct miscdevice my_misc = {
.minor = MISC_DYNAMIC_MINOR, // 动态次号
.name = "mydev", // /dev/mydev
.fops = &my_fops,
};
module_misc_device(my_misc); // 一行注册
- misc 自动:申请设备号(主 10)、注册 cdev、建 class/device、建 /dev 节点
- 小驱动首选(开关/LED/简单传感器)
6.3 misc vs 完整 cdev
| misc | 完整 cdev | |
|---|---|---|
| 代码量 | 少(几行) | 多(probe 全套) |
| 多实例 | 难 | 易 |
| 集成设备模型 | 简单 | 完整 |
| 适合 | 单一简单设备 | 复杂/多实例/平台驱动 |
嵌入式视角:小驱动用 misc 省事
写个 LED/按键/简单 ADC 驱动,用 misc 几行搞定,不用完整 platform 骨架。但 SoC 外设驱动(UART/SPI 控制器)用完整 platform,因为要设备树匹配/多实例/资源管理。按复杂度选。
七、probe 里的资源获取
7.1 寄存器映射
c
// 设备树:reg = <0x0 0x10000000 0x0 0x100>;
// 驱动:
void __iomem *base = devm_platform_ioremap_resource(pdev, 0);
// 自动:从 resource 取地址 + ioremap + 映射
// 老式(手动)
struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
void __iomem *base = devm_ioremap_resource(&pdev->dev, res);
// 读写寄存器(映射后用虚拟地址)
writel(0x55, base + REG_DATA);
u32 val = readl(base + REG_DATA);
writew(...); readw(...); // 16 位
writeb(...); readb(...); // 8 位
__iomem:标记这是 I/O 内存(编译器检查,防误用)writel/readl:MMIO 读写(对应你裸机*(volatile u32*))devm_:自动管理(remove 时自动取消映射)
7.2 中断获取
c
// 设备树:interrupts = <10>;
int irq = platform_get_irq(pdev, 0);
devm_request_threaded_irq(&pdev->dev, irq, hardirq, thread, IRQF_ONESHOT, "my", dev);
7.3 时钟/调节器/DMA(子系统)
c
struct clk *clk = devm_clk_get(&pdev->dev, NULL);
clk_prepare_enable(clk);
struct regulator *reg = devm_regulator_get(&pdev->dev, "vdd");
regulator_enable(reg);
- 这些有专门子系统(14 篇网络/其他),devm_ 自动管理
7.4 devm_ 资源管理⭐
c
devm_kzalloc(...) // 内存,remove 自动释放
devm_platform_ioremap_resource // 映射,自动取消
devm_request_threaded_irq // 中断,自动释放
devm_clk_get // 时钟,自动释放
devm_前缀的资源,绑定到 device 生命周期- device 移除时自动释放,不用手动 free
- 大幅简化 probe 错误处理(不用 goto 清理一堆)
devm_ 是现代驱动标配
老驱动:probe 里申请一堆,出错 goto 一路释放,remove 手动释放,易漏。devm_ 自动管理,remove 几乎不用写释放代码。你写新驱动,能用 devm_ 就用。这是 Linux 驱动开发的现代化简化。
7.5 platform_set_drvdata / get_drvdata
c
// probe 里存
platform_set_drvdata(pdev, dev);
// remove / 其他地方取
struct my_dev *dev = platform_get_drvdata(pdev);
- 把设备结构体存到 platform_device
- remove 时取回(释放资源用)
- 标准模式
八、poll/select/epoll(事件等待)
8.1 为什么需要 poll
- read 默认阻塞(没数据等)
- 但有时要"多设备等任一就绪"或"非阻塞"
- poll/select/epoll 解决
8.2 poll 实现
c
static __poll_t my_poll(struct file *filp, poll_table *wait) {
struct my_dev *dev = filp->private_data;
__poll_t mask = 0;
poll_wait(filp, &dev->wait_queue, wait); // 把等待队列登记
if (有数据可读) mask |= EPOLLIN | EPOLLRDNORM;
if (可写) mask |= EPOLLOUT | EPOLLWRNORM;
return mask;
}
- poll_wait 登记等待队列
- 返回当前就绪的事件
- 数据来了(中断里)wake_up(&dev->wait_queue) 唤醒
8.3 用户态
c
poll(&fds, nfds, timeout); // 等待
// 或 select / epoll
九、mmap:设备内存映射
9.1 为什么 mmap
- read/write 要 copy_to/from_user,大数据慢
- mmap 把设备内存映射到用户虚拟地址,用户直接访问,零拷贝
9.2 mmap 实现
c
static int my_mmap(struct file *filp, struct vm_area_struct *vma) {
struct my_dev *dev = filp->private_data;
unsigned long pfn = virt_to_phys(dev->buf) >> PAGE_SHIFT;
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); // 不缓存
return remap_pfn_range(vma, vma->vm_start, pfn,
vma->vm_end - vma->vm_start, vma->vm_page_prot);
}
- 设备内存(寄存器/DMA buffer)映射给用户
- 用户访问该虚拟地址 = 访问设备内存
- 高性能场景必备(GPU/FPGA/视频)
十、异步通知(SIGIO)
10.1 什么是异步通知
- 用户态不用 poll 轮询,设备就绪时内核发 SIGIO 信号
- 用户态装信号处理器,信号来时处理
10.2 驱动实现
c
static int my_fasync(int fd, struct file *filp, int mode) {
struct my_dev *dev = filp->private_data;
return fasync_helper(fd, filp, mode, &dev->fasync_queue);
}
// 中断里发信号
kill_fasync(&dev->fasync_queue, SIGIO, POLL_IN);
10.3 用户态
c
signal(SIGIO, handler);
fcntl(fd, F_SETOWN, getpid());
fcntl(fd, F_SETFL, O_ASYNC); // 开异步
十一、驱动的错误处理(probe 失败)
11.1 goto 错误处理链
c
static int my_probe(...) {
ret = step1(); if (ret) goto err_step1;
ret = step2(); if (ret) goto err_step2;
ret = step3(); if (ret) goto err_step3;
return 0;
err_step3:
undo_step2();
err_step2:
undo_step1();
err_step1:
return ret;
}
- 按申请顺序的反向释放
- 不用 devm_ 时,这是标准模式
- 用 devm_ 后,大部分 goto 消失
11.2 devm_ 简化后
c
static int my_probe(...) {
dev = devm_kzalloc(...); // 自动释放
base = devm_platform_ioremap_resource(...); // 自动
devm_request_threaded_irq(...); // 自动
// 只有非 devm 的要手动管理(cdev/class)
...
}
十二、用户态测试程序
c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define MY_IOC_MAGIC 'M'
#define MY_IOC_RESET _IO(MY_IOC_MAGIC, 0)
int main() {
int fd = open("/dev/myip0", O_RDWR);
char buf[100];
write(fd, "hello", 5);
int n = read(fd, buf, 100);
printf("read %d bytes\n", n);
ioctl(fd, MY_IOC_RESET);
close(fd);
return 0;
}
十三、调试驱动
13.1 看驱动加载
bash
dmesg | tail # 看 pr_info/dev_info 输出
lsmod | grep myip # 模块加载了没
ls /dev/myip* # 设备节点建了没
cat /proc/devices # 主设备号
13.2 看 sysfs
bash
ls /sys/module/myip/ # 模块信息
ls /sys/devices/platform/myip/ # 设备
cat /sys/devices/platform/myip/uevent # uevent
13.3 看设备树
bash
ls /proc/device-tree/ # 解析后的设备树
ls /proc/device-tree/soc/myip@10020000/
cat /proc/device-tree/soc/myip@10020000/compatible
13.4 常见问题
| 现象 | 原因 |
|---|---|
| probe 没调 | compatible 不匹配 / status="disabled" |
| 加载失败 Unknown symbol | 依赖符号没导出 / GPL |
| /dev 没节点 | udev/mdev 没配 / device_create 没调 |
| read 返回 -EFAULT | copy_to_user 失败(用户指针非法) |
| 中断不触发 | 触发类型错 / 没清中断 / 中断号错 |
| 持锁睡 | probe 里持自旋锁调了可睡函数 |
嵌入式视角:BSP 调驱动套路
调裸机驱动靠串口打印/示波器。Linux 加:① dmesg 看日志;② /sys 看状态;③ strace 看用户态调用;④ /proc 看设备树。结合你裸机经验(寄存器/中断/时序)+ Linux 工具,调试效率倍增。
十四、对照总表
| 概念 | 裸机/FreeRTOS | Linux 驱动 |
|---|---|---|
| 硬件描述 | 硬编码 | 设备树 DTS |
| 匹配硬件 | 无 | compatible 匹配 |
| 设备初始化 | main 里调 init | probe |
| 寄存器访问 | *(volatile u32*) |
readl/writel(经 ioremap) |
| 中断注册 | 直接配 NVIC/PLIC | request_irq/devm_request_threaded_irq |
| 用户接口 | 函数调用 | file_operations(open/read/write/ioctl) |
| 用户↔内核数据 | 直接 | copy_to/from_user |
| 设备节点 | 无 | /dev/xxx(udev 自动) |
| 多实例 | 手动 | 次设备号 + platform_get_drvdata |
| 资源释放 | 手动 | devm_ 自动 |
十五、本篇小结
- 三大驱动:字符设备(按字节,你 BSP 主战场)/块设备(按块,13 篇)/网络设备(包,14 篇)
- 设备树 DTS:分离硬件描述,compatible 匹配驱动,reg/interrupts 描述资源
- platform 驱动骨架:of_match_table + platform_driver(probe/remove)+ module_platform_driver
- probe:devm_kzalloc → devm_platform_ioremap_resource → devm_request_threaded_irq → alloc_chrdev_region → cdev_init/add → class_create → device_create
- cdev:dev_t(主+次)/cdev_init/cdev_add;file_operations 关联回调
- file_operations:open/read/write/ioctl/release/poll/mmap;private_data 传设备结构
- copy_to/from_user:用户指针必须经此,不能直接解引用
- container_of:从内嵌成员指针取外层结构,C 实现多态
- ioctl :
_IO/_IOR/_IOW/_IOWR编码命令,magic 防错 - misc:简化字符设备框架,小驱动用
- devm_:资源自动管理,remove 几乎不用写释放
- poll/mmap/fasync:事件等待/零拷贝/异步通知高级接口
- 错误处理:goto 链(老)或 devm_ 简化(新)
- 用户态:open/read/write/ioctl + strace 调试
速查表
| 想干啥 | 用什么 |
|---|---|
| 设备树匹配 | of_match_table(compatible) |
| 寄存器映射 | devm_platform_ioremap_resource |
| 读写寄存器 | readl/writel(8/16/32 位) |
| 申请中断 | devm_request_threaded_irq |
| 申请设备号 | alloc_chrdev_region |
| 注册 cdev | cdev_init + cdev_add |
| 建设备节点 | class_create + device_create |
| 用户→内核 | copy_from_user |
| 内核→用户 | copy_to_user |
| ioctl 命令 | _IO/_IOR/_IOW/_IOWR(magic) |
| 取外层结构 | container_of(ptr, type, member) |
| 存设备结构 | platform_set_drvdata / filp->private_data |
| 简化小驱动 | misc(device + module_misc_device) |
| poll 等 | poll_wait + mask |
| mmap 映射 | remap_pfn_range |
| 异步通知 | fasync_helper + kill_fasync |
| 自动资源 | devm_ 前缀 |
| 看日志 | dmesg |
| 看设备节点 | ls /dev/xxx |
| 看设备树 | /proc/device-tree/ |
| 看主设备号 | cat /proc/devices |
| 跟踪用户调用 | strace ./app |
💡技术之路漫漫,分享是为了更好地交流。如果本文的内容对你有启发,希望能得到你的 点赞 👍 和 收藏 ⭐。
如果你在调试过程中遇到了其他问题,欢迎在 评论区 💬 留言,我们一起探讨。也欢迎 关注 👀 我,一起交流底层开发的那些事儿。