news 2026/10/31 11:10:04

【IMX6ULL 驱动开发】从系统移植到 platform + 设备树,手把手写一个 DHT11 温湿度传感器驱动

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【IMX6ULL 驱动开发】从系统移植到 platform + 设备树,手把手写一个 DHT11 温湿度传感器驱动

开发平台:正点原子 IMX6ULL(Cortex-A7)|内核版本:Linux 4.1.15(NXP BSP)交叉工具链:arm-linux-gnueabihf-gcc|启动方式:tftp 下载内核 + NFS 挂载根文件系统适合读者:正在学嵌入式 Linux 驱动、准备驱动开发岗面试的同学

本文把「系统移植 → 三种驱动框架 → 设备树 → 完整工程代码」串成一条线,最后用DHT11 温湿度传感器做一个 platform + 设备树驱动的完整实例(含 Makefile、驱动层、应用层全部代码)。


目录

  • 一、系统移植:内核是怎么起来的
  • 1.1 Bootloader(U-Boot)启动流程
  • 1.2 Linux 内核镜像与启动过程
  • 1.3 根文件系统 rootfs
  • 1.4 init 进程
  • 二、Linux 驱动开发框架
  • 2.1 驱动的三种分类
  • 2.2 字符设备驱动框架
  • 2.3 混杂设备 misc 驱动框架
  • 2.4 platform + 设备树驱动框架
  • 2.5 三种框架对比
  • 三、驱动开发完整流程(DHT11 实战)
  • 四、完整工程代码
  • 五、面试要点速答
  • 六、易错点与踩坑记录
  • 七、总结

一、系统移植:内核是怎么起来的

系统移植是驱动开发的前置背景。只有清楚内核从何处启动、在什么阶段接管硬件、驱动在哪个环节被加载,才能准确定位驱动问题。

1.1 Bootloader(U-Boot)启动流程

以 IMX6ULL 为例,U-Boot 的启动分为七个阶段:

① 芯片上电执行 BootROM

BootROM 是 SoC(System on Chip)内部固化的程序,厂商烧写、用户不可修改。它把uboot.imx搬移到 OCRAM(片上 RAM),并完成 DDR(内存,掉电数据丢失)初始化,随后加载完整的 U-Boot。

② 初始化核心硬件

初始化时钟、内存、CPU 工作模式。CPU 上电复位后默认进入 SVC 模式(Supervisor,管理员模式,权限最高,也是操作系统内核运行的模式);同时关闭看门狗、关闭中断、关闭 D-Cache(数据高速缓存)、关闭 MMU(内存管理单元,负责物理地址与虚拟地址的转换)。

③ 初始化板级硬件

完成串口、网口的初始化,为后续打印与网络下载做准备。

④ 初始化软件环境

完成网络协议栈初始化,使能 tftp 下载能力。

⑤ 搬移内核与设备树

将内核镜像和设备树下载到内存的约定地址:

tftp 0x80800000 zImage tftp 0x83000000 imx6ull-alientek-emmc.dtb

⑥ 向内核传参

通过bootargs环境变量把根文件系统位置、网络参数、控制台设备等传递给内核:

setenv bootargs root=/dev/nfs nfsroot=/home/linux/nfs/rootfs \ serverip=192.168.1.100 ip=192.168.1.50 \ console=ttymxc0 init=/linuxrc
字段含义说明
root=/dev/nfs根文件系统挂载方式NFS(Network File System)网络文件系统
nfsroot=根文件系统所在路径服务端导出的根目录绝对路径
serverip=服务端 IP提供 NFS 与 tftp 服务的主机地址
ip=开发板 IP与 serverip 处于同一网段
console=控制台设备ttymxc0对应 IMX6ULL 的 UART1
init=用户空间第一个进程通常为/linuxrc或/sbin/init

⑦ 引导内核启动

跳转到内核入口地址,把 CPU 控制权交给 Linux 内核。

1.2 Linux 内核镜像与启动过程

Linux 内核(本平台 4.1.15 版本)负责文件系统管理、多任务调度(时间片轮询)、网络管理与内存管理。内核源码经make编译后生成arch/arm/boot/zImage:

Linux 内核源码 → make → arch/arm/boot/zImage
镜像类型构成特点适用场景
Image原生内核镜像体积大,不便于移植与网络传输调试、本地烧写
zImageImage + 自解压代码体积小,便于移植,运行时自行解压嵌入式主流选择
uImagezImage + 64 字节头部头部含 CPU 架构、加载地址、镜像大小等信息U-Boot 专用格式

内核启动流程:

  1. 跳转到内核入口点;
  2. 执行第一个 C 语言函数start_kernel()(位于init/main.c)。

start_kernel()按固定顺序完成内核初始化:

① 平台与架构初始化

setup_arch()匹配设备树(imx6ul.dtsi)、初始化时钟树与内存描述,并通过of_platform_populate把设备树节点展开为 platform 设备;trap_init()建立 ARM 异常向量表。

② 内存管理初始化

mm_init()完成 memblock → 页表 → buddy 分配器 → slab 分配器的建立,此后kmalloc才可用。

③ 调度器初始化

sched_init()初始化 CFS 调度器,之后系统才具备进程切换能力。

④ 中断与控制台初始化

开启中断,console_init()初始化串口控制台,内核打印开始可见。

⑤ 创建 init 进程

rest_init()创建 1 号内核线程kernel_init()与 2 号线程kthreadd(),当前进程退化为 idle(0 号进程),调度正式开始。

1.3 根文件系统 rootfs

根文件系统是内核启动后挂载的第一个文件系统,提供/dev、/proc、/sys等目录以及用户空间程序。

常用制作工具为busybox或buildroot:前者轻量、手工配置度高,后者可一键生成完整系统。

1.4 init 进程

init是用户空间的第一个进程(PID 为 1),由内核在挂载根文件系统后启动,负责按配置文件拉起各类系统服务,并作为所有用户态进程的父进程。


二、Linux 驱动开发框架

按「字符设备 → 混杂设备 → platform + 设备树」的递进顺序梳理,重点给出每一步的核心结构体、注册函数与执行时机。

2.1 驱动的三种分类

  • 字符设备驱动:按字节流顺序访问,如传感器、按键;
  • 块设备驱动:按数据块随机访问,如 eMMC、SD 卡;
  • 网络设备驱动:面向报文收发,如以太网卡。

嵌入式传感器开发以字符设备驱动与混杂设备驱动为主。

2.2 字符设备驱动框架

字符设备驱动不会自动生成设备节点,加载后需手工创建设备文件:

insmod sensor.ko # 加载驱动模块 mknod /dev/sensor c 250 0 # 创建设备节点:c 表示字符设备,250 为主设备号,0 为次设备号

实现步骤如下:

① 构建 cdev 结构体

描述一个具体的字符设备:设备号(主设备号 12 bit + 次设备号 20 bit)、设备名称、file_operations结构体。

② 构建 file_operations 结构体

实现.open、.read、.write、.release等函数指针,即驱动层操作函数集合。

③ 实现驱动入口函数(insmod时被调用)

申请设备号 → 初始化并注册 cdev → 创建类与设备节点 → 申请硬件资源。

④ 实现驱动注销函数(rmmod时被调用)

按入口的逆序释放资源。

⑤ 绑定入口与出口

通过module_init()与module_exit()宏绑定入口函数与注销函数。

⑥ 实现具体文件操作

完成open、read、write、close等函数,供应用层通过系统调用访问。

步骤入口函数(init)注销函数(exit)
申请/释放设备号alloc_chrdev_region()(动态)或register_chrdev_region()(静态)unregister_chrdev_region()
注册/删除 cdevcdev_init()+cdev_add()cdev_del()
创建/销毁类与节点class_create()+device_create()class_destroy()+device_destroy()
申请/释放硬件资源gpio_request()、request_irq()、spin_lock_init()gpio_free()、free_irq()、spin_lock_destroy()

2.3 混杂设备 misc 驱动框架

混杂设备本质是主设备号固定为 10的字符设备,注册时由内核自动创建/dev下的设备节点,省去手工mknod的步骤,适合单一功能的传感器驱动。实现步骤如下:

① 构建 miscdevice 结构体

填写次设备号(20 bit)、设备名称与file_operations结构体指针。

② 构建 file_operations 结构体

实现.open、.read、.write、.release。

③ 实现入口函数

调用misc_register()完成注册,并申请 GPIO、中断、自旋锁等硬件资源。

④ 实现注销函数

调用misc_deregister()注销,并释放硬件资源。

⑤ 绑定入口与出口

使用module_init()与module_exit()。

⑥ 实现文件操作函数

完成open、read、write、close。

2.4 platform + 设备树驱动框架

该框架是实际产品开发的主流方案,其核心是把设备信息与驱动代码解耦,交由 platform 总线在运行时完成匹配。

2.4.1 设备驱动分离思想

platform 总线把"设备信息"与"驱动信息"分开管理:设备信息由设备树(.dts)描述,驱动信息由驱动程序实现,二者通过compatible属性匹配。设备树变更无需重新编译驱动,同一驱动也可复用于多块板卡。

2.4.2 设备树需添加的内容
需添加内容写法示例作用驱动端获取方式必要性
设备节点dht11 { … };放根节点;I2C 设备挂i2c1节点下生成 platform 设备或 i2c 客户端—必需
compatiblecompatible = "myboard,dht11";与of_match_table匹配,触发probe()驱动中定义一致字符串必需
pinctrlpinctrl-0 = <&pinctrl_dht11>;引脚复用与电气属性,进入 probe 前自动生效pinctrl 子系统自动应用GPIO 设备必需
gpiosdht11-gpios = <&gpio5 1 GPIO_ACTIVE_HIGH>;指定使用的 GPIO 引脚of_get_named_gpio()GPIO 设备必需
regreg = <0x23>;I2C 从机地址或 SPI 片选client->addr或platform_get_resource()总线设备必需
interruptsinterrupts = <&gpio5 2 IRQ_TYPE_EDGE_FALLING>;声明中断资源platform_get_irq()可选
clocksclocks = <&clks IMX6UL_CLK_GPT1>;外设时钟使能devm_clk_get()可选
statusstatus = "okay";节点使能开关—必需

关于 gpio 属性的三个 cell:dht11-gpios = <&gpio1 3 0>中,&gpio1是所属 GPIO 控制器 phandle,3是控制器内引脚号(即 GPIO1_IO03),0是有效电平标志(0 = 高电平有效)。驱动端用of_get_named_gpio()把这个"设备树描述"换算成全局引脚编号。

与pinctrl的分工:pinctrl 管引脚复用与电气属性,gpio-xxx属性管逻辑编号。

2.4.3 驱动端实现要点

① 构建 platform_driver 结构体

填写.probe、.remove,以及.driver中的匹配规则(.name、.of_match_table)。

② 构建 of_device_id 结构体

.compatible必须与设备树完全一致,例如"dallas,ds18b20"(不含空格、全小写)。

③ 注册与注销驱动

使用module_platform_driver()宏,其内部展开为platform_driver_register()与platform_driver_unregister()。

④ 实现 probe 函数(设备与驱动匹配成功后自动执行)

① 注册混杂设备 misc_register() ② 通过 of_get_named_gpio() 从设备树取得 GPIO 编号 ③ 用 devm_gpio_request() 申请 GPIO(该接口在驱动卸载时自动释放) ④ 完成其余初始化

⑤ 实现 remove 函数(设备或驱动从总线注销时执行)

misc_deregister()注销混杂设备,释放相关资源。

2.5 三种框架对比

三种框架的差异集中在设备号管理、节点创建方式与硬件信息来源上。实际开发中优先选择 platform + 设备树框架,学习阶段可先掌握字符设备与 misc 框架。

对比项字符设备框架misc 框架platform + 设备树框架
主设备号动态或静态申请固定为 10由内部注册的设备类型决定
设备节点需mknod手工创建自动创建自动创建(内部多基于 misc)
硬件信息硬编码在驱动中硬编码在驱动中由设备树描述,驱动只读
匹配机制无无compatible匹配后调probe()
适用场景学习框架、简单设备单一功能传感器实际产品开发(推荐)

三、驱动开发完整流程(DHT11 实战)

以 DHT11 温湿度传感器为例,一次完整的驱动开发包含四个环节:读手册与原理图、写设备树、编译部署、加载验证。四个环节的顺序不可颠倒,因为设备树是驱动probe被调用的前提。

3.1 阅读手册与原理图

先阅读传感器芯片手册,确认功能、接口类型(I2C / SPI / GPIO / ADC)、关键参数、数据格式与时序要求;再阅读 IMX6ULL 底板原理图,确认传感器实际连接的引脚与总线,为设备树编写提供依据。

DHT11 关键参数与时序:

项目参数
供电3.3V ~ 5.5V
测量范围温度 0~50 ℃,湿度 20~90 %RH
分辨率温度 1 ℃,湿度 1 %RH
通信方式单总线(1-wire),需外接 4.7 kΩ 上拉
采样周期≥ 1 s
数据格式40 bit = 湿度整数 + 湿度小数 + 温度整数 + 温度小数 + 校验和

通信时序:

主机:拉低 ≥18ms → 释放总线(拉高 20~40us) DHT11:响应 80us 低 → 80us 高 数据:每位以 50us 低电平开始,随后高电平 高电平 26~28us = '0',约 70us = '1'

驱动里用udelay(10)计数判断高电平持续长度,time > 4(>40us)即判为 1。

3.2 添加设备树节点

进入内核源码的设备树目录,编辑对应板级 dts 文件:

cd ~/imx6ull/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/boot/dts vi imx6ull-alientek-emmc.dts

在iomuxc节点中添加引脚复用,并在根节点下新增设备节点:

/* 1. 引脚复用:把 GPIO1_IO03 这个 PAD 配置为 GPIO 功能 */ &iomuxc { pinctrl_dht11: dht11grp { fsl,pins = <MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0>; }; }; /* 2. 设备节点:compatible 必须与驱动 of_match_table 完全一致 / dht11 { compatible = "pute,dht11"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_dht11>; gpio-dht11 = <&gpio1 3 0>; / GPIO1_IO03,0 = 高电平有效 */ status = "okay"; };

#address-cells/#size-cells用来规定子节点reg属性中"地址"与"长度"各占几个 32 位字(一字 4 字节、半字 2 字节、双字 8 字节),DHT11 这类 GPIO 设备不需要reg,可省略。

3.3 编译与部署

# 回到内核根目录,仅重新编译设备树 make dtbs # 拷贝到 tftp 服务目录 cp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb ~/tftpboot/

重启开发板,U-Boot 通过 tftp 重新下载 dtb,新节点即生效。

验证节点是否生效:板上执行ls /sys/firmware/devicetree/base/,能看到dht11目录即可。

3.4 加载与验证

insmod dht11_drv.ko # 加载驱动 dmesg | tail # 查看 probe 是否执行、初始化是否成功 ls /dev/misc_dht11 # 混杂设备节点应自动出现 ./dht11_app # 运行测试程序,读取温湿度

若probe()未被调用,按以下顺序排查:

① compatible 字符串是否与驱动完全一致

注意大小写、逗号后不能有空格。

② 节点位置是否正确

GPIO 类设备放根节点,I2C 设备必须挂在i2c1之类总线节点下。

③ status 是否为 okay

disabled状态下节点不参与设备创建。

④ 引脚是否已被其他节点占用

同一 PAD 被两个节点复用会冲突。


四、完整工程代码

4.1 目录结构

dht11/ ├── Makefile # 顶层 Makefile,统一调度 app 与 drv ├── dht11_app/ │ ├── Makefile # 应用层交叉编译 │ └── dht11_app.c # 测试程序 └── dht11_drv/ ├── Makefile # 内核模块编译 └── dht11_drv.c # 平台驱动 + 设备树

4.2 Makefile 三件套

(1)顶层 Makefile
# ============================================================ # 顶层 Makefile:统一编译 DHT11 应用程序 + 内核驱动模块 # 用法: make -> 依次编译 app 和 drv 两个子目录 # make clean -> 清理两个子目录的普通编译产物 # make distclean -> 深度清理(含 .o/.ko/.cmd/.order 等中间文件) # ============================================================ 模块名前缀,用于拼接出两个子目录的名字 MODULENAME := dht11 应用程序目录:dht11_app MODULE_APP := $(MODULENAME)_app 驱动模块目录:dht11_drv MODULE_DRV := $(MODULENAME)_drv 导出变量,让子目录里的 Makefile 也能读到(驱动编译时常用) export MODULE_APP MODULE_DRV 默认目标 all:make -C <目录> 等价于先 cd 进该目录再执行 make 任一子目录编译报错会立即中止,不会继续往下编 all: make -C $(MODULE_APP) make -C $(MODULE_DRV) 声明伪目标:防止目录下恰好存在同名文件 clean/distclean 时目标失效 .PHONY: all clean distclean 清理普通编译产物(应用可执行文件、驱动 .o 等) clean: make -C $(MODULE_APP) clean make -C $(MODULE_DRV) clean 深度清理:连 .o.bak、Module.symvers、.tmp_versions 等内核编译中间文件一并删掉 distclean: make -C $(MODULE_APP) distclean make -C $(MODULE_DRV) distclean
(2)应用层 Makefile(dht11_app/Makefile)
# ============================================================ # dht11_app 子目录 Makefile:交叉编译 DHT11 测试应用程序 # 依赖顶层的 export 变量:$(MODULE_APP) = dht11_app # ============================================================ 交叉编译工具链:编译 ARM 平台(IMX6ULL)可执行文件 CC := arm-linux-gnueabihf-gcc 目标规则:目标 dht11_app 依赖于同名源文件 dht11_app.c $^ 代表全部依赖(dht11_app.c),$@ 代表目标名(dht11_app) $(MODULE_APP): $(MODULE_APP).c $(CC) $^ -o $@ # 编译完成后自动把可执行文件拷到 NFS 根文件系统 # 开发板通过 NFS 挂载该目录,即可直接运行 ./dht11_app 测试 cp $(MODULE_APP) ~/nfs/rootfs 声明伪目标,防止同名文件干扰 .PHONY: clean distclean 清理本地编译产物 clean: rm -f $(MODULE_APP) 深度清理:同时删除 NFS 根文件系统里已部署的程序 distclean: rm -f $(MODULE_APP) rm -f ~/nfs/rootfs/$(MODULE_APP)
(3)驱动 Makefile(dht11_drv/Makefile)
# ============================================================ # dht11_drv 子目录 Makefile:把驱动编译成内核模块 .ko # 依赖顶层的 export 变量:$(MODULE_DRV) = dht11_drv # ============================================================ 告诉内核 Kbuild:把 dht11_drv.o 编译成外部模块,产物为 dht11_drv.ko obj-m += $(MODULE_DRV).o 内核源码顶层目录:模块编译必须"借"内核的构建系统 注意:路径要与开发板实际运行的内核版本完全一致,否则 insmod 报版本不匹配 KERDIR := /home/linux/imx6ull/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek 当前目录(驱动源码所在目录),用 shell pwd 在 make 执行时展开 MODULEDIR := $(shell pwd) 1. -C $(KERDIR):先进入内核源码目录,借用其顶层 Makefile 2. M=$(MODULEDIR):告诉 Kbuild 回到本目录编译外部模块 3. 编译成功后把 .ko 拷到 NFS 根文件系统 all: make -C $(KERDIR) M=$(MODULEDIR) modules cp $(MODULE_DRV).ko ~/nfs/rootfs 声明伪目标 .PHONY: clean distclean 清理:调用内核构建系统清除本目录的 .o/.ko/.cmd 等中间文件 clean: make -C $(KERDIR) M=$(MODULEDIR) clean rm -f ~/nfs/rootfs/$(MODULE_DRV).ko

为什么编驱动要进内核目录:模块不能独立编译,它链接的符号(printk、gpio_to_irq等)来自内核,必须用与板上运行同版本的内核源码 + 同一交叉编译器,vermagic一致才能insmod。

4.3 驱动代码 dht11_drv.c

/* ================= 头文件 ================= */ #include <linux/module.h> // 模块框架:THIS_MODULE、MODULE_LICENSE #include <linux/platform_device.h> // platform 总线:platform_driver、probe/remove #include <linux/of_gpio.h> // 设备树 GPIO 接口:of_get_named_gpio() #include <linux/miscdevice.h> // 混杂设备:miscdevice、misc_register() #include <linux/fs.h> // file_operations #include <linux/cdev.h> // 字符设备相关(本驱动用 misc,未直接用到) #include <linux/device.h> // 设备模型:struct device #include <linux/gpio.h> // GPIO API:gpio_direction_*/gpio_get/set_value #include <linux/delay.h> // 延时:msleep、udelay #include <linux/kernel.h> // pr_info 等内核打印 #include <linux/init.h> // __init/__exit 标记 #include <asm/uaccess.h> // copy_to_user(4.1.15 内核路径) #include <asm/delay.h> // udelay 底层实现 /* dht11 数据引脚编号,probe 时从设备树读出 */ int dht11_no = 0; /* ============ 复位:发起一次通信 ============ DHT11 起始时序:主机拉低 >=18ms -> 释放总线(拉高 20~40us) -> 等待 DHT11 回 80us 低 + 80us 高的响应信号 */ static int dht11_reset(void) { int time = 0; /* 1. 引脚设为输出,默认高(总线空闲态) */ gpio_direction_output(dht11_no, 1); /* 2. 拉低 18ms:告诉 DHT11 "开始通信" */ gpio_set_value(dht11_no, 0); msleep(18); /* 3. 释放总线:拉高后改输入,交出总线控制权 */ gpio_set_value(dht11_no, 1); gpio_direction_input(dht11_no); /* 4. 等 DHT11 的响应:总线被拉低(超时 ~200us 判无响应) / while (gpio_get_value(dht11_no) && time < 20) { udelay(10); time++; } if (time >= 20) { / 超时:传感器未应答 */ pr_info("[driver]: dht11 no response!\n"); return -1; } /* 5. 响应信号结构:80us 低 -> 80us 高 -> 开始发数据 / while (!gpio_get_value(dht11_no)); / 等低电平结束 / while (gpio_get_value(dht11_no)); / 等高电平结束 */ return 0; } /* ============ 读 40bit 数据 ============ DHT11 位编码:每位以 50us 低电平开始,随后高电平 高电平 ~26-28us = '0',~70us = '1'(用 40us 阈值区分) 数据格式:湿度整数+湿度小数+温度整数+温度小数+校验和,共 5 字节 */ static int read_dht11_data(unsigned char *pdata, int len) { int i = 0, n = 0, time = 0; unsigned char data[5] = {0}; /* 1. 引脚设为输入(总线由 DHT11 主导) */ gpio_direction_input(dht11_no); /* 2. 逐位读取 5 字节 × 8 位,高位在前(n 从 7 到 0) / for (i = 0; i < 5; i++) { for (n = 7; n >= 0; n--) { time = 0; while (!gpio_get_value(dht11_no)); / 等 50us 低电平结束 / while (gpio_get_value(dht11_no)) { / 测量高电平持续时间 / udelay(10); time++; } if (time > 4) / >40us 即 '1'('0' 只有 ~27us) */ data[i] |= (1 << n); } } /* 3. 读完拉高引脚并设回输出,恢复空闲态 */ gpio_direction_output(dht11_no, 1); /* 4. 校验和:前 4 字节之和的低 8 位应等于第 5 字节 */ if (data[0] + data[1] + data[2] + data[3] != data[4]) { pr_info("[driver]:dht11_check failed!\n"); return -1; } /* 5. 把湿度+温度(前 4 字节)拷给调用者 */ memcpy(pdata, data, len); return 0; } /* ============ 驱动层 read ============ 应用 read("/dev/misc_dht11") 时执行: 复位 -> 读 4 字节 -> copy_to_user 送回应用层 */ static ssize_t dht11_read(struct file *fp, char __user *puser, size_t n, loff_t *off) { int ret = 0; unsigned long nsize = 0; unsigned char data[4] = {0}; ret = dht11_reset(); /* 1. 起始时序 */ if (ret) { pr_info("[driver]:dht11_reset failed!\n"); return ret; } ret = read_dht11_data(data, 4); /* 2. 读温湿度 */ if (ret) { pr_info("[driver]:dht11_read failed!\n"); return ret; } nsize = copy_to_user(puser, data, 4); /* 3. 内核数据 -> 用户空间 / if (nsize) { / 返回值 = 未拷贝成功的字节数 */ pr_info("[driver]:copy_to_user failed!\n"); return nsize; } return sizeof(data); /* 成功返回 4 */ } /* 文件操作集合:只实现 read */ static struct file_operations dht11_fops = { .read = dht11_read, }; /* 混杂设备:主设备号固定 10,自动创建 /dev/misc_dht11 节点 / static struct miscdevice misc_dht11 = { .minor = MISC_DYNAMIC_MINOR, / 动态分配次设备号 / .name = "misc_dht11", / 设备名 -> /dev/misc_dht11 */ .fops = &dht11_fops, }; /* probe:设备树 compatible 与 of_match_table 匹配成功后自动执行 */ int dht11_probe(struct platform_device *pdev) { int ret = 0; /* 1. 注册混杂设备,创建 /dev/misc_dht11 */ misc_register(&amp;misc_dht11); /* 2. 从设备树 dht11 节点的 "gpio-dht11" 属性取引脚编号 */ dht11_no = of_get_named_gpio(pdev-&gt;dev.of_node, "gpio-dht11", 0); if (dht11_no &lt; 0) { pr_info("[driver]:of_get_named_gpio failed!\n"); return -1; } /* 3. 申请 GPIO(devm_ 前缀:随设备生命周期自动释放) */ ret = devm_gpio_request(misc_dht11.this_device, dht11_no, "dht11_gpio"); if (ret) { pr_info("[driver]:devm_gpio_request failed!\n"); return ret; } /* 4. 设为输出高:总线空闲态 */ gpio_direction_output(dht11_no, 1); pr_info("dht11_probe is ok!\n"); return 0; } /* remove:设备或驱动注销时执行,与 probe 逆序释放资源 */ int dht11_remove(struct platform_device pdev) { misc_deregister(&misc_dht11); / 注销混杂设备 */ pr_info("dht11_remove is ok!\n"); return 0; } /* 无设备树时的匹配方式:总线按 .name 比对 / static struct platform_device_id dht11_id_table[] = { { .name = "putedht11" }, { }, / 结尾必须留空项 */ }; /* 设备树匹配:与 dts 节点 compatible = "pute,dht11" 一致才进入 probe */ static struct of_device_id dht11_match_table[] = { { .compatible = "pute,dht11" }, { }, }; /* platform_driver:probe/remove + 两种匹配规则 / static struct platform_driver dht11_drv = { .probe = dht11_probe, .remove = dht11_remove, .driver = { .name = "pute_dht11", / 无设备树时的匹配名 */ .owner = THIS_MODULE, .of_match_table = dht11_match_table, }, .id_table = dht11_id_table, }; /* 一键注册宏:insmod -> platform_driver_register() rmmod -&gt; platform_driver_unregister() */ module_platform_driver(dht11_drv); MODULE_LICENSE("GPL"); MODULE_AUTHOR("PUTE");

驱动的编写顺序(自底向上,先能跑通再完善时序):

  1. 写头文件:platform、misc、gpio、of_gpio、delay、uaccess 六大类;
  2. 搭匹配框架:of_device_id→platform_driver→module_platform_driver(),先保证insmod能触发 probe;
  3. 写 probe:注册 misc → 取 GPIO → 申请 GPIO → 初始化引脚(remove 按逆序释放);
  4. 写 fops:file_operations→miscdevice,挂到 probe 里注册;
  5. 最后实现时序:dht11_read→read_dht11_data→dht11_reset,单总线时序对延时敏感,放最底层。

4.4 应用层代码 dht11_app.c

#include <fcntl.h> // open 及 O_RDWR 标志 #include <stdio.h> // printf、perror #include <sys/types.h> // ssize_t 等基础类型 #include <unistd.h> // read、sleep、close int main(void) { int fd = 0; /* 设备文件描述符 / ssize_t nsize = 0; / read 返回值:实际读到的字节数 / unsigned char data[4] = {0}; / 湿度整数、湿度小数、温度整数、温度小数 / double temp = 0; / 温度(℃) / double humity = 0; / 湿度(%RH),拼写应为 humidity */ /* 1. 打开设备节点:必须是驱动 misc_register 创建的那个名字 * 路径 /dev/misc_dht11 与驱动 .name = "misc_dht11" 严格一致 */ fd = open("/dev/misc_dht11", O_RDWR); if (fd &lt; 0) { perror("[APP]:fail to open misc_dht11!"); /* 打印 errno 对应原因 */ return -1; } /* 2. 循环采集:每次 read 会触发一次完整的单总线时序 * 两次读取间隔必须 &gt;=1s(DHT11 采样周期),这里 2s 更稳妥 */ while (1) { nsize = read(fd, data, 4); if (nsize != 4) { /* 读失败(校验和错/无响应)时不退出,继续下一次尝试 */ perror("[APP]:fail to read dht11!"); continue; } /* 3. 数据解析: * data[0] = 湿度整数,data[1] = 湿度小数 * data[2] = 温度整数,data[3] = 温度小数 * DHT11 分辨率为 1,小数位恒为 0;若是 DHT22 应改为 /10.0 */ humity = data[0] + (data[1] / 100.0); temp = data[2] + (data[3] / 100.0); printf("temp = %.2lf, humity = %.2lf\n", temp, humity); sleep(2); /* 满足 DHT11 采样间隔要求 */ } return 0; }

运行效果:

[root@imx6ull /]# ./dht11_app temp = 26.00, humity = 58.00 temp = 26.00, humity = 58.00 temp = 27.00, humity = 57.00

五、面试要点速答

Q U-Boot 的作用是什么?

A 初始化 DDR 与板级硬件、提供下载与调试能力、把内核与设备树搬移到内存、通过 bootargs 传参并引导内核。

Q zImage 与 uImage 的区别?

A zImage 是带自解压代码的压缩内核;uImage 在 zImage 前加了 64 字节 U-Boot 专用头部。

Q start_kernel() 之后内核做了哪些初始化?

A 平台与架构、内存管理、调度器、中断与控制台,最后创建 init 进程开始调度。

Q 字符设备的主次设备号各占多少位?

A 主设备号 12 bit,次设备号 20 bit。

Q misc 设备与普通字符设备的关键差别?

A 主设备号固定为 10,注册时自动创建设备节点。

Q platform 驱动何时进入 probe()?

A 设备与驱动的 compatible 匹配成功时由总线调用。

Q 为什么要用设备树?

A 实现设备信息与驱动代码分离,改硬件不必改驱动,一份驱动可适配多块板卡。

Q devm_ 前缀接口的优势?

A 资源与设备生命周期绑定,驱动卸载时自动释放,减少错误路径的资源泄漏。


六、易错点与踩坑记录

整理初稿时发现若干拼写与术语错误,这些细节在面试手写代码环节极易失分:

易错写法正确写法说明
root=dev/nfsroot=/dev/nfs挂载点必须是绝对路径
/dev/xxx.cmknod /dev/xxx c 250 0设备节点无.c后缀,需指明类型与主次设备号
platfromplatform拼写错误
moudle_platform_drivermodule_platform_driver拼写错误
of_get_name_gpioof_get_named_gpio函数名含 named
开启中端开启中断术语错误
"dallas, ds18B20""dallas,ds18b20"compatible 不含空格,按惯例全小写
altientek/romovealientek/remove拼写错误

实际调试中踩到的坑:

  1. insmod报 "Invalid module format":内核源码版本 / 交叉编译器与板上内核不一致,vermagic对不上;
  2. /dev/misc_dht11不出现:misc_register()前就return了,或probe根本没执行(查 compatible);
  3. 读到数据全是 0xFF 或校验失败:单总线时序对延时极敏感,udelay(10)的计数阈值要按实测微调;同时确认上拉电阻已焊;
  4. copy_to_user返回值非 0:应用层传的缓冲区不在用户空间(如传了内核地址);
  5. Makefile里rm ~/nfs/rootfs:误把整个根文件系统目录当文件删,正确写法是rm -f ~/nfs/rootfs/$(MODULE_APP)。

七、总结

一条完整的链路:

BootROM → U-Boot(初始化 DDR/串口/网络)→ tftp 下载 zImage+dtb → start_kernel()(架构/内存/调度/中断 → init 进程) → 挂载 rootfs → init → 内核解析设备树(of_platform_populate 展开 platform 设备) → 驱动模块 compatible 匹配成功 → probe() → misc_register 创建 /dev 节点 → 应用 open/read 访问硬件

驱动开发记住三句话:

  1. 框架先跑通,再写时序:probe 能打印is ok比读对数据更重要;
  2. 硬件信息写设备树,代码只读不写死:换板子只改 dts;
  3. 资源申请与释放严格逆序,优先用devm_系列减少泄漏。

如果这篇笔记对你有帮助,欢迎点赞收藏,评论区交流~

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 11:10:02

AI 录音卡怎么选?逛完这个 GitHub 仓库,答案就清楚了

录音卡&#xff0c;到底应该怎么选&#xff1f;怎么看&#xff1f; 看参数表&#xff0c;人人会写&#xff1b;看宣传图&#xff0c;人人会修&#xff1b;看测评视频&#xff0c;还可能被恰饭带偏。高销量的产品&#xff0c;价格却让人犹豫不决&#xff1b;真遇上涉密会议&…

作者头像 李华
网站建设 2026/10/31 11:09:25

10.29实习日志

测量高通率板&#xff0c;看看其波形与功率要测功率&#xff0c;在负载端接一个可以忽略不记的电阻&#xff0c;这里用的是10欧姆电阻&#xff0c;这样通过两个表笔&#xff0c;就能测得电压及其电流&#xff0c;算出功率。

作者头像 李华
网站建设 2026/10/31 11:07:06

instanceof 还能拆字段?Java 21 记录模式把对象一行拆平

instanceof 还能拆字段&#xff1f;Java 21 记录模式把对象一行拆平系列&#xff1a;Java 后端实战精通营 作者&#xff1a;鱼宵 配套课程仓库&#xff1a;https://gitee.com/j67mk2/java21-journey &#xff08;第 6 课&#xff09;先看一段"两层嵌套就看不下去"的…

作者头像 李华
网站建设 2026/10/31 10:55:57

08-JDK 21 虚拟线程正式发布:并发写法彻底变了

JDK 21 是继 17 之后的第三个新制度 LTS&#xff0c;也是"现代 Java 真正成熟"的里程碑。它把酝酿了四年的虚拟线程正式转正&#xff0c;配合分代 ZGC、分代 Shenandoah、密钥增强、序列集合&#xff08;SequencedCollection&#xff09;等&#xff0c;一口气补齐了并…

作者头像 李华