嵌入式Linux驱动学习笔记(第3-6天):从misc封装、platform总线到设备树、中断与单总线外设实战

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、驱动框架演进:三层逐步实现解耦与复用
    • [1.1 第一层:原生字符设备驱动(cdev)](#1.1 第一层:原生字符设备驱动(cdev))
    • [1.2 第二层:misc子系统------字符设备的轻量化封装](#1.2 第二层:misc子系统——字符设备的轻量化封装)
    • [1.3 第三层:platform总线------驱动与硬件的彻底解耦](#1.3 第三层:platform总线——驱动与硬件的彻底解耦)
    • [1.4 三层框架对比总结](#1.4 三层框架对比总结)
  • 二、设备树(DTS)深度实战与常见问题
    • [2.1 设备树的文件形态](#2.1 设备树的文件形态)
    • [2.2 设备树节点的标准写法](#2.2 设备树节点的标准写法)
    • [2.3 引脚复用配置(pinctrl)](#2.3 引脚复用配置(pinctrl))
    • [2.4 编译与部署全流程](#2.4 编译与部署全流程)
    • [2.5 常见编译错误与排坑](#2.5 常见编译错误与排坑)
    • [2.6 vim编辑设备树的踩坑记录](#2.6 vim编辑设备树的踩坑记录)
  • 三、中断与阻塞I/O:按键驱动深度实践
    • [3.1 中断的分层处理:顶半部与底半部](#3.1 中断的分层处理:顶半部与底半部)
    • [3.2 工作队列实现底半部](#3.2 工作队列实现底半部)
    • [3.3 阻塞I/O与等待队列](#3.3 阻塞I/O与等待队列)
    • [3.4 GPIO子系统标准接口](#3.4 GPIO子系统标准接口)
  • 四、单总线外设驱动:DHT11驱动开发
    • [4.1 单总线通信原理](#4.1 单总线通信原理)
    • [4.2 GPIO模拟时序实现](#4.2 GPIO模拟时序实现)
    • [4.3 延时函数的选择](#4.3 延时函数的选择)
    • [4.4 稳定性优化:重试机制](#4.4 稳定性优化:重试机制)
    • [4.5 框架复用](#4.5 框架复用)
  • 五、工程化实践:编译、部署与调试
    • [5.1 内核模块Makefile编写规范](#5.1 内核模块Makefile编写规范)
    • [5.2 应用层交叉编译](#5.2 应用层交叉编译)
    • [5.3 两种部署方式对比](#5.3 两种部署方式对比)
    • [5.4 驱动加载与卸载调试](#5.4 驱动加载与卸载调试)
    • [5.5 常见加载失败问题排查](#5.5 常见加载失败问题排查)
  • 六、学习总结与方法论
    • [6.1 核心收获](#6.1 核心收获)
    • [6.2 调试方法论](#6.2 调试方法论)
    • [6.3 后续方向](#6.3 后续方向)
  • 附录:本阶段踩坑清单速查

前言

这一阶段的学习围绕Linux字符设备驱动的工程化分层思想逐步深入,从最基础的字符设备驱动封装,到misc子系统简化模板,再到platform总线+设备树实现驱动与硬件彻底解耦,同时引入中断分层处理、阻塞I/O与等待队列、GPIO子系统、单总线时序模拟等核心知识点。全程以LED、按键、DHT11温湿度传感器三个外设为实战载体,覆盖从设备树编写、驱动代码实现、交叉编译、部署到调试排错的完整链路,记录了开发过程中大量真实踩坑点与解决方案。


一、驱动框架演进:三层逐步实现解耦与复用

Linux驱动开发的核心设计思想是分层、封装、解耦,内核将通用逻辑逐层下沉,驱动开发者只需要关注硬件差异和业务逻辑,避免重复实现相同的模板代码,同时提升驱动的可移植性。

1.1 第一层:原生字符设备驱动(cdev)

这是字符设备驱动的最原始形态,直接使用内核提供的cdev接口完成设备注册。

  • 核心流程:手动分配设备号 → 创建cdev结构体 → 绑定file_operations操作集 → 向内核添加cdev → 创建设备节点
  • 特点:所有逻辑全部自己实现,硬件引脚、寄存器直接写死在驱动代码中
  • 缺点:代码冗余度高,硬件改动必须修改驱动源码,可移植性差
  • 对应学习阶段:第1-2天的基础LED字符设备驱动

1.2 第二层:misc子系统------字符设备的轻量化封装

misc(杂项设备)是内核对字符设备驱动的一层标准封装,自动处理了设备号分配、cdev注册、设备节点创建等固定流程,是字符设备驱动最常用的简化框架。

  • 核心结构体:struct miscdevice

    复制代码
    static struct miscdevice led_misc = {
        .minor = MISC_DYNAMIC_MINOR,  // 自动分配次设备号
        .name = "led_misc",           // 设备名,对应/dev下的节点名
        .fops = &fops,                // 绑定文件操作集
    };
  • 核心接口:misc_register / misc_deregister,一行代码完成注册与注销

  • 优势:

    1. 省去手动分配设备号、cdev_init、cdev_add、创建类等模板化代码
    2. 统一由内核管理杂项设备,资源更规范
    3. 代码量比原生cdev减少近一半,结构更清晰
  • 适用场景:简单字符设备、小型外设驱动,LED、按键、传感器等都非常适合

1.3 第三层:platform总线------驱动与硬件的彻底解耦

这是本阶段学习的核心,也是现代Linux驱动的标准开发模式。platform是一条虚拟总线,将硬件描述信息 和驱动逻辑代码 完全拆分开,通过compatible属性进行匹配。

两端分工
角色 存放位置 内容
设备端 设备树(DTS) 硬件引脚、中断号、引脚复用、时钟等板级硬件信息
驱动端 内核驱动代码 通用驱动逻辑、probe初始化、remove资源释放、文件操作接口
核心机制
  1. 内核启动时解析设备树,注册所有platform设备
  2. 驱动加载时,向platform总线注册驱动
  3. 总线自动对比设备的compatible字符串和驱动的匹配表,匹配成功则调用驱动的probe函数
  4. probe函数中完成硬件初始化、资源申请、设备注册等工作
优势
  1. 驱动通用化:一套驱动可以适配多块不同引脚的开发板,只需修改设备树,无需改动驱动源码
  2. 硬件信息统一管理:所有板级硬件描述集中在设备树中,符合Linux内核"板级信息下移"的发展方向
  3. 资源自动管理 :配合devm_系列接口,驱动卸载时自动释放申请的GPIO、中断、内存等资源,减少内存泄漏

1.4 三层框架对比总结

框架 代码量 可移植性 硬件耦合度 适用场景
原生cdev 大 差 高 学习原理、特殊定制设备
misc封装 中 一般 中 简单字符设备
platform+misc+设备树 小 好 低 标准外设驱动、产品级开发

本阶段所有外设驱动均采用 platform总线 + misc设备 + 设备树 的标准架构,这也是工业界最常用的字符设备驱动开发范式。


二、设备树(DTS)深度实战与常见问题

设备树是本阶段学习的重点之一,也是很多新手的第一个卡点,从文件位置、编辑方法、语法规则到编译部署,每一步都有容易踩的坑。

2.1 设备树的文件形态

文件类型 全称 位置 作用
.dts 设备树源码 内核源码arch/arm/boot/dts/ 人类可编辑的文本文件,描述板级硬件信息
.dtsi 设备树头文件 内核源码arch/arm/boot/dts/ 描述SOC级通用硬件信息,板级dts通过#include引用
.dtb 设备树二进制 编译后生成在同目录 内核可直接解析的二进制文件,开发板启动加载的就是它
.dtc 设备树编译器 内核源码脚本目录 编译工具,将dts编译为dtb

2.2 设备树节点的标准写法

以三个外设为例,标准节点包含以下要素:

复制代码
/* 1. LED设备节点 */
myled0 {
    compatible = "pute,myled";      // 驱动匹配字符串
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_myled0>;   // 引用引脚复用配置
    gpio-myled0 = <&gpio1 3 0>;      // GPIO引脚:组号+引脚号+默认电平
    status = "okay";                 // 启用节点
};
/* 2. 按键设备节点(带中断) */
putekey {
compatible = "pute,putekey";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_putekey>;
gpio-key = <&gpio1 18 0>;
interrupt-parent = <&gpio1>;     // 中断控制器
interrupts = <18 IRQ_TYPE_EDGE_FALLING>; // 中断号+触发方式
status = "okay";
};
/* 3. DHT11单总线设备节点 */
putedht11 {
compatible = "pute,putedht11";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_putedht11>;
gpio-dht11 = <&gpio1 4 0>;
status = "okay";
};

2.3 引脚复用配置(pinctrl)

仅在设备节点里指定GPIO是不够的,还需要在&iomuxc节点中配置引脚的复用功能,将引脚设置为通用GPIO模式并配置电气属性。

复制代码
&iomuxc {
    imx6ul-evk {
        pinctrl_myled0: myled0grp {
            fsl,pins = <
                MX6UL_PAD_GPIO1_IO03__GPIO1_IO03    0x10B0
            >;
        };
    };
};
  • 命名规则:pinctrl_xxx 标签名要和设备节点里的引用名一致
  • 语法格式:PAD名__复用功能 + 电气配置值
  • 注意:标签名是全局唯一的,重复定义会导致编译失败

2.4 编译与部署全流程

  1. 编辑源码 :在内核源码的arch/arm/boot/dts/目录下修改板级dts文件

  2. 编译设备树 :必须在内核源码根目录 执行命令

    复制代码
    make dtbs

    输出DTC arch/arm/boot/dts/xxx.dtb即为编译成功

  3. 部署到启动目录 :将编译好的dtb拷贝到TFTP共享目录

    复制代码
    cp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb ~/tftpboot/
  4. 重启生效:开发板断电重启,u-boot会从TFTP加载新的dtb文件

  5. 验证节点 :开发板进入系统后验证

    复制代码
    ls /proc/device-tree/节点名

    能看到compatible、gpio等属性文件,说明节点已被内核成功解析

2.5 常见编译错误与排坑

问题1:Duplicate label 编译错误
  • 报错信息:ERROR (duplicate_label): Duplicate label 'pinctrl_xxx'
  • 原因:设备树中带冒号的标签是全局唯一标识符,两个节点使用了相同的标签名
  • 解决:删除重复的节点,只保留一个正确配置;或者修改其中一个标签名,同步修改设备节点中的引用
问题2:修改设备树后不生效
  • 原因:只修改了dts源码,没有重新编译dtb;或者编译了没有拷贝到启动目录;或者拷贝了但没有重启开发板
  • 解决:严格按照「修改→编译→拷贝→重启」四步执行,缺一不可
问题3:驱动probe函数不执行
  • 优先排查:设备树的compatible字符串和驱动的匹配表是否完全一致,包括大小写、逗号分隔
  • 其次排查:设备节点status是否为okay,pinctrl配置是否正确

2.6 vim编辑设备树的踩坑记录

设备树通常用vim编辑,新手很容易在模式切换上卡住,这是本阶段高频问题。

问题1:输入的文字删不掉
  • 原因:还停留在插入模式,按键盘只会继续输入字符,不会执行删除命令
  • 解决:先按Esc回到正常命令模式 ,再执行删除操作
    • 删单个字符:光标移到目标字符,按x
    • 删整行:光标移到目标行,连续按两次d(dd命令)
    • 删多行:ndd删除n行
问题2:打开文件出现swap file提示
  • 现象:打开dts文件时提示Found a swap file by the name ...
  • 原因:之前编辑文件时异常退出(直接关闭终端、断电等),vim留下了临时交换文件
  • 解决:按D键删除交换文件,即可正常编辑
问题3:查找定位内容
  • 正常模式下输入/关键词,回车即可跳转到第一个匹配位置
  • 按n跳转到下一个匹配项,按N跳转到上一个
  • 编辑设备树时非常适合快速定位节点、引脚配置

三、中断与阻塞I/O:按键驱动深度实践

按键驱动是学习中断、阻塞I/O的最佳载体,核心是理解Linux中断的分层处理思想,以及用户态与内核态的同步机制。

3.1 中断的分层处理:顶半部与底半部

Linux中断设计的核心原则是:顶半部快进快出,耗时逻辑下移到底半部。

  • 顶半部 :中断触发后立即执行,只做最紧急、最耗时短的操作

    复制代码
    <ul>
  • 例如:清除中断标志、标记事件状态、唤醒等待队列

  • 执行上下文:中断上下文,不可睡眠

  • 底半部 :处理耗时的业务逻辑,在系统空闲时执行
    • 例如:数据处理、上报用户态、日志打印
    • 执行上下文:进程上下文(工作队列)或中断上下文(tasklet)

3.2 工作队列实现底半部

工作队列是最常用的底半部机制,运行在进程上下文,可以睡眠,使用灵活。

  • 定义工作队列与工作项

    复制代码
    static struct workqueue_struct *key_wq;
    static struct work_struct key_work;
  • 顶半部调度工作

    复制代码
    static irqreturn_t key_isr(int irq, void *dev)
    {
        schedule_work(&key_work); // 调度底半部
        return IRQ_HANDLED;
    }
  • 底半部处理函数

    复制代码
    static void key_work_func(struct work_struct *work)
    {
        // 按键消抖、状态更新、唤醒等待队列等耗时逻辑
    }

3.3 阻塞I/O与等待队列

实现用户态read函数的阻塞效果:没有数据时进程休眠,有数据时自动唤醒。

  • 定义等待队列头

    复制代码
    static DECLARE_WAIT_QUEUE_HEAD(key_wq);
  • read函数中实现阻塞

    复制代码
    static ssize_t key_read(struct file *fp, char __user *puser, size_t n, loff_t *off)
    {
        wait_event_interruptible(key_wq, 事件标志); // 不满足条件则休眠
        copy_to_user(...); // 唤醒后拷贝数据到用户空间
    }
  • 中断底半部唤醒队列

    复制代码
    wake_up_interruptible(&key_wq);

3.4 GPIO子系统标准接口

本阶段所有GPIO操作均使用内核标准GPIO子系统API,不直接操作寄存器。

API 作用
of_get_named_gpio 从设备树节点中解析获取GPIO编号
devm_gpio_request 申请GPIO引脚,devm前缀自动管理资源
gpio_direction_input 设置引脚为输入模式
gpio_direction_output 设置引脚为输出模式并指定默认电平
gpio_get_value 读取引脚电平
gpio_set_value 设置引脚电平
gpio_to_irq GPIO引脚映射为中断号

四、单总线外设驱动:DHT11驱动开发

DHT11温湿度传感器采用单总线协议,通过一根GPIO引脚实现双向通信,是练习GPIO方向切换、时序模拟的绝佳案例。

4.1 单总线通信原理

单总线只有一根数据线SDA,主机和从机分时复用,通信全程由主机发起,从机应答。

  • 起始信号:主机拉低总线至少18ms,再拉高20~40us
  • 响应信号:从机拉低80us,再拉高80us
  • 数据传输:40bit数据,高位先出,每一位以50us低电平开始,高电平时长区分0和1
    • 数据0:高电平持续23~27us
    • 数据1:高电平持续68~74us
  • 校验:前4字节(湿度高、湿度低、温度高、温度低)之和等于第5字节校验和

4.2 GPIO模拟时序实现

通过不断切换GPIO的输入输出方向,配合微秒级延时,软件模拟单总线时序。

  1. 主机发送起始信号:配置为输出模式,拉低18ms,拉高30us
  2. 切换输入模式:等待从机响应信号
  3. 逐位读取数据 :每一位先等待低电平过去,延时40us后读取电平
    • 此时还是高电平 → 数据1
    • 此时已经变低 → 数据0
  4. 组装数据:移位组装成5字节数据
  5. 校验验证:比对校验和,数据合法则提取温湿度

4.3 延时函数的选择

  • mdelay:毫秒级延时,用于起始信号的18ms延时
  • udelay:微秒级延时,用于单总线时序判断,是单总线驱动的核心
  • 注意:内核态延时并非绝对精准,受系统调度影响,微秒级延时会有偏差

4.4 稳定性优化:重试机制

单总线靠软件模拟时序,内核态可能被调度、中断打断,导致时序偏移,出现校验失败,属于正常现象。

  • 优化方法:读取函数外层增加3次重试,失败后延时10ms重新读取
  • 效果:成功率可以从60%左右提升到95%以上,满足日常使用

4.5 框架复用

DHT11驱动完全沿用了platform + misc + 设备树的标准架构,和LED、按键驱动的框架高度一致,只需要修改probe中的硬件初始化和读写函数逻辑,充分体现了分层设计的复用优势。


五、工程化实践:编译、部署与调试

驱动开发不止是写代码,工程化的编译、部署、调试能力同样重要,本阶段积累了大量实操经验。

5.1 内核模块Makefile编写规范

标准模板
复制代码
# 内核源码路径
kerdir := /home/linux/zyt/驱动/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
# 当前目录
curdir := $(shell pwd)
# 模块名
MODULE_NAME := dht11_platform_drv
all:
make -C $(kerdir) M=$(curdir) modules
clean:
make -C $(kerdir) M=$(curdir) clean
常见问题
  • clean报错 :如果是自定义clean规则,rm命令一定要加-f参数,文件不存在时不会报错

    复制代码
    <pre>

    clean: rm -f $(MODULE_NAME).ko

  • 路径错误:内核路径必须指向已经编译过的完整内核源码树,不能是压缩包或空目录

5.2 应用层交叉编译

  • 必须使用ARM交叉编译器:arm-linux-gnueabihf-gcc

  • 不能使用Ubuntu主机自带的gcc,否则编译出的x86程序在ARM开发板上无法运行

  • 编译命令:

    复制代码
    arm-linux-gnueabihf-gcc app.c -o app

5.3 两种部署方式对比

开发板有两种常用的文件部署方式,各有适用场景。

方式 原理 特点 适用场景
TFTP下载 开发板通过网络从Ubuntu的TFTP目录下载文件到本地内存 文件临时存在,重启消失 快速调试、单次验证
NFS根文件系统 开发板根目录直接挂载Ubuntu上的rootfs文件夹,文件实时同步 文件永久存在,重启不消失 正式使用、长期测试
TFTP方式操作
  1. Ubuntu端:把文件拷贝到~/tftpboot/目录

  2. 开发板端:

    复制代码
    tftp -g -r 文件名 服务器IP
NFS方式操作
  1. Ubuntu端:把文件直接拷贝到rootfs目录

    复制代码
    cp xxx.ko ~/zyt/rootfs/
  2. 开发板端:直接在根目录就能看到文件,无需下载

5.4 驱动加载与卸载调试

加载驱动
复制代码
insmod xxx.ko
  • 无任何输出即为加载成功
  • 常见报错No such file or directory:当前目录下没有该ko文件,切换到文件所在目录或写绝对路径
验证加载成功
  1. 查看设备节点:ls /dev/设备名
  2. 查看内核日志:dmesg | tail,看到probe成功打印
  3. 查看sysfs设备:ls /sys/class/misc/设备名
卸载驱动
复制代码
rmmod xxx
  • 不需要加.ko后缀
  • 卸载后设备节点自动消失,资源自动释放

5.5 常见加载失败问题排查

  1. insmod提示文件不存在:优先检查当前工作目录,或者写文件的绝对路径
  2. 加载后没有设备节点:检查设备树节点是否存在、compatible是否匹配、probe函数是否执行成功
  3. probe执行失败:逐行排查GPIO申请、中断申请、misc注册等步骤,通过打印定位失败点
  4. 应用执行格式错误:确认是用ARM交叉编译器编译的,不是主机gcc

六、学习总结与方法论

6.1 核心收获

  1. 分层设计思想:从原生cdev到misc封装,再到platform总线+设备树,本质是通用逻辑的不断下沉。学习驱动不能只关注功能实现,更要理解内核的设计思想。
  2. 设备树是标配:现代Linux驱动开发中,设备树已经是硬件描述的标准方式,驱动与硬件分离是必须掌握的开发范式。
  3. 资源管理意识 :优先使用devm_系列接口,利用内核的自动资源管理,减少内存泄漏、资源未释放的问题。
  4. 中断处理原则:顶半部快进快出,耗时逻辑全部下沉到底半部,避免影响系统响应性能。
  5. 时序模拟能力:通过GPIO模拟单总线时序,是嵌入式驱动的基本功,对延时精度、时序逻辑有很高要求。

6.2 调试方法论

驱动开发问题排查要分层定位、逐步缩小范围:

  1. 先确认设备树节点是否正常加载
  2. 再确认驱动是否加载成功、probe是否执行
  3. 然后验证硬件操作是否正常(GPIO读写、中断触发)
  4. 最后调试业务逻辑与数据交互

不要一开始就陷入代码细节,先从宏观层面排除环境、配置、路径问题。

6.3 后续方向

接下来可以继续深入学习的方向:

  • 更复杂的外设总线:I2C、SPI子系统驱动
  • 字符设备高级功能:ioctl、mmap、异步通知
  • 输入子系统:input设备驱动框架
  • 设备树高级语法:节点引用、属性解析、中断控制器

附录:本阶段踩坑清单速查

  1. vim编辑删不掉内容 → 先按Esc回到正常模式
  2. 设备树编译重复标签 → 删除重复节点,标签全局唯一
  3. dtb拷贝找不到tftpboot → 用~/tftpboot,不是/tftpboot
  4. insmod找不到文件 → 检查当前目录或写绝对路径
  5. 应用运行报错 → 确认用ARM交叉编译器编译
  6. 驱动probe不执行 → 核对compatible字符串完全一致
  7. Makefile clean报错 → rm命令加-f参数
  8. 设备树修改不生效 → 重新编译dtb+拷贝+重启开发板
  9. DHT11读取失败率高 → 增加3次重试机制
  10. 单总线时序不准 → 调整udelay延时参数,结合示波器校准
相关推荐
一木 之林1 小时前
Agent智能体学习总结:从一次调用到模型自主规划、工具调用与 ReAct 循环的项目实战-6/6
前端·人工智能·学习·react.js
雪落漂泊1 小时前
Linux基础开发工具(一)软件包管理器与Vim编辑器篇
linux·运维·服务器
崽崽..1 小时前
【Linux】网络编程的调试工具以及连接失败的具体排查方法
linux·网络
承渊政道1 小时前
CentOS 7单机部署Elasticsearch 7.17.29:从启动报错、CRUD验证到固定公网地址的完整过程
linux·elasticsearch·centos·内网穿透·cpolar·单机部署
自不量力的A同学1 小时前
MooTool Next 1.3.0 发布
笔记
进击的荆棘1 小时前
Linux系统——库的原理与制作之ELF文件的形成与加载的过程(上)
linux·elf·动静态库
来包番茄沙司1 小时前
【网盘资源分享】2026新版尚硅谷AI Agent智能体极速版|全套课程资料+面经
人工智能·学习
额额额对了1 小时前
Linux 内核定时器与 DHT11
linux·运维·服务器
每天题库1 小时前
塔式起重机安装拆卸工考试题库 真题模拟考前押题精准覆盖
学习·安全·考试·题库·考证