【Linux内核专栏 11】驱动框架:platform / char / misc / 设备树

本篇定位:前面 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

内核启动:

  1. 解析 DTB
  2. status="okay" 的节点(且有 compatible)→ 创建 platform_device
  3. reg/interrupts 等转成 struct resource
  4. 注册到 platform 总线
  5. 匹配驱动 → 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

💡技术之路漫漫,分享是为了更好地交流。如果本文的内容对你有启发,希望能得到你的 点赞 👍 和 收藏 ⭐。

如果你在调试过程中遇到了其他问题,欢迎在 评论区 💬 留言,我们一起探讨。也欢迎 关注 👀 我,一起交流底层开发的那些事儿。


相关推荐
倒头就睡的小比特1 小时前
Linux基础指令初步认识
linux
2301_777998341 小时前
TCP协议:滑动窗口与流量控制
服务器·网络·网络协议·tcp/ip·php
Starry-sky(jing)1 小时前
watchdog: BUG: soft lockup - CPU#3 stuck for 22s! 怎么排查——从寄存器 dump 到自旋锁争用根因
linux·内核·服务器运维·soft lockup·性能排查
DongQiShanRen2 小时前
裁决台账双向互校(上):名册与实物的第一道对账
java·linux·运维·数据库·人工智能·自然语言处理·数据挖掘
ShineWinsu2 小时前
对于Redis:AOF持久化的解析
linux·数据库·redis·缓存·面试·持久化·aof
归秋1422 小时前
2026企业AI办公工具选型指南:从场景匹配到平台评估
大数据·运维·人工智能
wuminyu2 小时前
Virtual Thread重投递至ForkJoinPool任务队列过程解析
java·linux·c语言·jvm·c++
xuhe22 小时前
Codex解决新模型无法使用/model选择的问题
linux·ai·codex
well06123 小时前
Linux 文件相关底层知识
linux·运维·服务器