DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学)
目录
### 文章目录
- [DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学)](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [目录](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [@[TOC]](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [1. 前言](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [2. DHT11 硬件接线与通信协议](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [2.1 接线图](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [2.2 单总线通信时序](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [2.3 数据格式与校验](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [3. 驱动框架总览:一套经典模板](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [4. 代码逐行精讲](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [4.1 头文件──工具库](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [4.2 设备描述结构体](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [4.3 全局变量与环形缓冲区](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [4.4 缓冲区操作函数](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [4.5 文件操作集:read 函数实现](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [4.6 中断服务程序](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [4.7 数据解析函数](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [4.8 模块初始化与注销](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [5. 现场实录:加载、运行、排错](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [5.1 insmod 报错 "File exists"](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [5.2 GPIO 输出与中断冲突](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [5.3 读取数据时有时无、返回 -1](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [6. 如何改进:更稳健的驱动设计](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [7. 回顾与自测](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [实验效果](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测) - [8. 回顾与自测](#文章目录 DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学) 目录 @[TOC] 1. 前言 2. DHT11 硬件接线与通信协议
2.1 接线图 2.2 单总线通信时序 2.3 数据格式与校验 3. 驱动框架总览:一套经典模板 4. 代码逐行精讲 4.1 头文件──工具库 4.2 设备描述结构体 4.3 全局变量与环形缓冲区 4.4 缓冲区操作函数 4.5 文件操作集:read 函数实现 4.6 中断服务程序 4.7 数据解析函数 4.8 模块初始化与注销 5. 现场实录:加载、运行、排错 5.1 insmod 报错 “File exists” 5.2 GPIO 输出与中断冲突 5.3 读取数据时有时无、返回 -1 6. 如何改进:更稳健的驱动设计 7. 回顾与自测 实验效果 8. 回顾与自测)
1. 前言
DHT11 是数字温湿度传感器里的"入门经典",只用一根数据线就能双向通信。但要在 Linux 下写出一个稳定好用的驱动,不能只靠运气,必须理解字符设备驱动框架 和中断+等待队列 的配合。
本文将以百问网 IMX6ULL 开发板为硬件平台,从接线图、时序分析开始,完整搭建一个 DHT11 字符设备驱动,并真实还原开发过程中遇到的 insmod File exists、GPIO tied to IRQ、数据返回 -1 等坑,最终给出正确、健壮的驱动骨架。
本文特色:
- 保姆级代码注释,每一行都有意义说明。
- 从框架的高度讲解每个内核机制的作用,而不是只记函数。
- 附带实战终端记录,教你根据内核日志精准排错。
2. DHT11 硬件接线与通信协议
2.1 接线图
DHT11 一般有四个引脚(有的模块只有三个),标准接线如下:

- VDD 接 3.3V ~ 5V
- GND 接地
- DATA 引脚通过 5kΩ 上拉电阻 接到 VDD,然后直连 MCU 的任意 GPIO(本例使用 GPIO115)
- NC 悬空
为什么必须加上拉电阻?
DHT11 的 DATA 引脚是开漏输出,只能输出低电平,不能主动输出高电平。主机和传感器都是通过拉低总线来通信,空闲时靠上拉电阻保持高电平。没有上拉电阻,数据线将无法恢复高电平,通信必然失败。
2.2 单总线通信时序
一次完整的温度读取由 MCU 发起,过程如下:
- MCU 发出开始信号
MCU 将 DATA 拉低 ≥ 18ms,然后拉高 20~40μs,随后释放总线(改为输入状态)。 - DHT11 应答
DHT11 检测到起始信号后,拉低 80μs,再拉高 80μs 作为应答。 - DHT11 发送 40bit 数据
每 bit 由一段约 50μs 的低电平加一段高电平组成。
位值的区分 完全取决于高电平的持续时间:- 高电平持续 26~28μs → 表示 "0"
- 高电平持续 70μs → 表示 "1"
- 传输结束
DHT11 拉低 50μs,然后释放总线,由上拉电阻拉高。
下图清晰地说明了每个阶段的波形关系:

驱动要做的关键工作:利用 GPIO 边沿中断记录每一个上升沿和下降沿的精确时刻,再通过时间差计算出高低电平宽度,从而解码出 0 和 1。
2.3 数据格式与校验
40bit 数据排列顺序:
| 字节0 | 字节1 | 字节2 | 字节3 | 字节4 |
|---|---|---|---|---|
| 湿度整数 | 湿度小数 | 温度整数 | 温度小数 | 校验和 |
校验规则 :
校验和 = (湿度整数 + 湿度小数 + 温度整数 + 温度小数) 的低 8 位
实际 DHT11 模块中,小数部分通常全为 0,仅用作扩展。
3. 驱动框架总览:一套经典模板
在真正动手写代码之前,我们先画出整个驱动的生命线。字符设备驱动 + 中断 + 等待队列 + 环形缓冲区 这套组合可以应付几乎所有"主机发起请求、外设异步返回数据"的场景。
text
[加载模块]
→ 获取 GPIO 对应的中断号
→ 注册字符设备驱动(register_chrdev)
→ 创建设备类和节点(class_create / device_create)
→ 生成 /dev/mydht11
[用户调用 read(fd, buf, 2)]
→ 拉低 18ms 发出测量命令
→ 注册双边沿中断(request_irq)
→ 在等待队列上阻塞(wait_event_interruptible)
→ 传感器数据到来,中断记录时间戳
→ 收齐 84 次边沿后解析成温湿度
→ 数据放入环形缓冲区,唤醒等待队列
→ read 被唤醒,从缓冲区取数
→ 通过 copy_to_user 把数据返回给应用
→ 释放中断(free_irq)
[卸载模块]
→ 销毁设备节点和类
→ 注销字符设备号
这套框架中,中断负责快采集,进程负责慢处理,等待队列充当中介,环形缓冲区则解决了"一次未读完,新数据又到"的覆盖问题。
4. 代码逐行精讲
下面按文件从上到下的顺序,逐一解释每一段代码的目的 和每个内核函数的含义。
4.1 头文件──工具库
c
#include <linux/module.h> // 模块必备
#include <linux/fs.h> // file_operations 等
#include <linux/interrupt.h> // request_irq, free_irq
#include <linux/gpio.h> // gpio_request, gpio_to_irq, gpio_direction_output...
#include <linux/of_gpio.h> // 设备树 gpio 支持(本例未深入)
#include <linux/slab.h> // kmalloc 等(本例未用)
#include <linux/kernel.h> // printk, mdelay
#include <linux/delay.h> // mdelay
#include <linux/wait.h> // wait_event_interruptible, wake_up
#include <linux/cdev.h> // 部分老旧接口不用也可
#include <linux/device.h> // class_create, device_create
#include <linux/uaccess.h> // copy_to_user
要点:写驱动时,用到哪个函数就去查它对应的头文件,全部放在文件开头。这就像做饭前把调料全部备齐。
4.2 设备描述结构体
c
struct dht11_desc {
int gpio; // GPIO 编号(例如 115)
int irq; // 对应的软中断号(由 gpio_to_irq 得到)
char *name; // 设备名字,用于申请资源时的标签
};
static struct dht11_desc dht11 = {115, 0, "dht11"};
为什么用结构体?
把一根 GPIO 相关的所有信息打包在一起,以后如果接多个传感器,只要声明一个数组即可。中断号稍后在 init 里赋值。
4.3 全局变量与环形缓冲区
c
#define BUF_LEN 128
static int g_keys[BUF_LEN]; // 内核环形缓冲区
static int r, w; // 读指针、写指针
static u64 g_irq_time[84]; // 记录 84 次边沿的时间戳(纳秒)
static int g_irq_cnt = 0; // 当前已记录的中断次数
static DECLARE_WAIT_QUEUE_HEAD(gpio_wait); // 等待队列头
static struct class *gpio_class; // 类指针,用于自动创建设备节点
static int major = 0; // 动态分配的主设备号
g_irq_time数组:必须记录每个边沿的时刻,才能计算出高电平宽度,从而区分 0 和 1。84 这个数字来源于 DHT11 一次完整通信产生的边沿数量。DECLARE_WAIT_QUEUE_HEAD:声明一个等待队列头,之后read可以在上面睡觉,中断可以把它唤醒。这是阻塞 I/O 的核心。- 环形缓冲区 :中断里不能直接调用
copy_to_user(因为中断上下文不能睡眠,也不能访问用户空间地址),所以采取"中断生产,进程消费"的模式。
4.4 缓冲区操作函数
c
#define NEXT_POS(x) ((x+1) % BUF_LEN)
static bool is_empty(void) { return r == w; }
static bool is_full(void) { return r == NEXT_POS(w); }
static void put_key(int val) {
if (!is_full()) {
g_keys[w] = val;
w = NEXT_POS(w);
}
}
static int get_key(void) {
int val = 0;
if (!is_empty()) {
val = g_keys[r];
r = NEXT_POS(r);
}
return val;
}
环形缓冲区原理 :读写指针分别移动,空条件是 r == w,满条件是 (w+1)%BUF_LEN == r(牺牲一个单元来区分空满)。
put_key 在中断里调用,get_key 在 read 里调用,两者无需额外加锁,因为这里的生产者只有中断,消费者只有进程,且中断不会嵌套(同一个 IRQ 不会重入)。
4.5 文件操作集:read 函数实现
Linux 应用看到的设备文件,背后对应一组 file_operations,我们先定义这个结构体:
c
static struct file_operations dht11_fops = {
.owner = THIS_MODULE,
.read = dht11_read,
};
下面实现 dht11_read,它是整个驱动最长、最关键的函数。
c
static ssize_t dht11_read(struct file *file, char __user *buf,
size_t size, loff_t *off)
{
int data[2];
if (size != 2) // 约定一次必须读 2 个 int(湿度和温度)
return -EINVAL;
参数说明:
buf:用户空间地址,驱动不能直接写,必须用copy_to_user。size:用户想读取的字节数。这里我们强制要求是 2(两个int,温湿度各一个)。off:文件偏移,简单驱动一般不处理。
步骤 1:发出起始信号
c
gpio_request(dht11.gpio, dht11.name);
gpio_direction_output(dht11.gpio, 0);
mdelay(18); // 拉低至少 18ms
gpio_free(dht11.gpio); // 释放引脚,恢复为输入
gpio_request:向内核申请使用该 GPIO,防止他人占用。第一个参数是 GPIO 号,第二个是标签。gpio_direction_output(gpio, 0):将引脚设为输出,并立刻输出 0(低电平)。mdelay(18):忙等待 18ms(DHT11 要求 ≥18ms)。gpio_free:释放 GPIO。多数 SoC 的 GPIO 被释放后自动变成高阻输入状态,正好用于接收传感器应答。
隐患:
gpio_free后若马上注册中断,需确保 GPIO 控制器已经切换为输入,一般没有问题。
步骤 2:注册中断
c
int ret = request_irq(dht11.irq, dht11_isr,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
dht11.name, &dht11);
if (ret) {
printk(KERN_ERR "DHT11 request_irq failed: %d\n", ret);
return -EIO;
}
request_irq参数详解:dht11.irq:中断号(由gpio_to_irq获取)。dht11_isr:我们编写的中断服务函数。IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING:双边沿触发,上升和下降都产生中断,这样才能记录完整的电平变化。dht11.name:显示在/proc/interrupts里的名字。&dht11:作为dev_id传给dht11_isr,让它知道是哪个设备触发的。
步骤 3:阻塞等待数据
c
wait_event_interruptible(gpio_wait, !is_empty());
wait_event_interruptible(wq, condition)机制:- 当前进程加入
gpio_wait等待队列,然后schedule()出去。 - 当另一个执行上下文调用
wake_up_interruptible(&gpio_wait)时,该进程被唤醒。 - 唤醒后会重新检查 condition ,只有当
!is_empty()为真(缓冲区非空)时,才从函数返回,继续往下执行。
- 当前进程加入
_interruptible变体:可被信号(如 Ctrl+C)中断,如果被信号唤醒,条件不满足则返回-ERESTARTSYS,我们的代码没有处理这种情况,可能导致中断未释放(后面详解)。
步骤 4:取出数据并交给用户
c
data[0] = get_key(); // 湿度
data[1] = get_key(); // 温度
if (copy_to_user(buf, data, 2 * sizeof(int)))
return -EFAULT;
get_key从环形缓冲区取出一个int,顺序是先放入的湿度、后放入的温度。copy_to_user(to, from, n):安全地将内核数据复制到用户空间地址。返回值非0表示部分或全部未复制,返回-EFAULT。
步骤 5:清理中断
c
free_irq(dht11.irq, &dht11);
return 2; // 实际读取的字节数
}
free_irq释放之前注册的中断,第二个参数必须与注册时的dev_id一致。
思考 :为什么把中断注册放在 read 里而不是 init 里?
设计意图是每次读取都单独注册中断,读完释放,这样可以保证只接收当前笔数据。但多次快速调用时极易产生我们后面看到的资源冲突,正确的做法是把中断注册放在 open 里或初始化里,并配合互斥锁。
4.6 中断服务程序
c
static irqreturn_t dht11_isr(int irq, void *dev_id)
{
u64 time = ktime_get_ns();
g_irq_time[g_irq_cnt] = time;
g_irq_cnt++;
if (g_irq_cnt == 84) {
parse_dht11_data(); // 解析数据
g_irq_cnt = 0;
}
return IRQ_HANDLED;
}
ktime_get_ns():返回系统上电以来的纳秒数(monotonic time),精度非常高,完全满足微秒级判别。- 每来一次中断,就把时间戳存入数组,计数器加1。
- 当收集满 84 次后,调用解析函数,并重置计数器,以备下一次采集。
为什么正好是 84 次?
一次 DHT11 通信的理论边沿数为:起始+应答 4 次 + 40bit × 2 = 80 次,总共 84 次。如果板子时序略有差异,可以适当调整这个阈值,但 84 经过验证是稳定的。
4.7 数据解析函数
c
static void parse_dht11_data(void)
{
u64 low_time, high_time;
u8 val = 0;
int bit_count = 0, byte_count = 0;
u8 bytes[5] = {0};
// 跳过前 4 个边沿(主机释放、应答)
for (int i = 4; i < 84; i += 2) {
high_time = g_irq_time[i] - g_irq_time[i-1];
low_time = g_irq_time[i-1] - g_irq_time[i-2];
val <<= 1;
if (high_time > 50000) // 50μs = 50000ns
val |= 1;
bit_count++;
if (bit_count == 8) {
bytes[byte_count++] = val;
val = 0;
bit_count = 0;
}
}
/* 校验和检查 */
if (((bytes[0] + bytes[1] + bytes[2] + bytes[3]) & 0xFF) == bytes[4]) {
put_key(bytes[0]); // 湿度整数
put_key(bytes[2]); // 温度整数
wake_up_interruptible(&gpio_wait);
} else {
printk(KERN_WARNING "DHT11 checksum error\n");
// 校验失败时不唤醒,让 read 继续等待或超时处理(需增加超时机制)
}
}
解析逻辑:
i=4开始:因为前 4 个跳变对应起始信号结束和传感器应答,不是数据。- 每次循环
i+=2,取出一个 bit 的high_time(高电平宽度)和low_time。 - 如果
high_time > 50000ns (50μs),该 bit 为 1,否则为 0。 - 每拼满 8 位即为一个字节,按顺序得到湿度整数、湿度小数、温度整数、温度小数、校验和。
- 校验通过后,将湿度和温度整数放入环形缓冲区,并唤醒等待队列 ,
read得以继续。 - 若不通过,数据丢弃,此时
read那边会一直阻塞。实际产品需加入超时返回错误。
4.8 模块初始化与注销
c
static int __init dht11_init(void)
{
dht11.irq = gpio_to_irq(dht11.gpio); // GPIO 硬件号转中断号
major = register_chrdev(0, "dht11", &dht11_fops);
if (major < 0) {
printk(KERN_ERR "register_chrdev failed\n");
return major;
}
gpio_class = class_create(THIS_MODULE, "100ask_dht11_class");
if (IS_ERR(gpio_class)) {
unregister_chrdev(major, "dht11");
return PTR_ERR(gpio_class);
}
device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "mydht11");
return 0;
}
gpio_to_irq:通过 GPIO 编号得到对应的软中断号(与硬件相关,不同的 GPIO 对应不同的中断号,系统中唯一)。register_chrdev:向系统注册字符设备,第一个参数填 0 表示自动分配主设备号。第二个参数是驱动名,会在/proc/devices中出现。第三个参数是 file_operations 指针。返回主设备号。class_create:在/sys/class下建立类,配合device_create可以让系统自动创建/dev/mydht11设备节点 ,省去手动 mknod。类名称 "100ask_dht11_class" 是自定义的,会出现在/sys/class/下。device_create:生成设备节点。第一个参数是类指针,第三个参数MKDEV(major, 0)组合主/次设备号,最后一个参数是设备文件名。
c
static void __exit dht11_exit(void)
{
device_destroy(gpio_class, MKDEV(major, 0));
class_destroy(gpio_class);
unregister_chrdev(major, "dht11");
}
- 严格按照创建的相反顺序销毁:device_destroy → class_destroy → unregister_chrdev。
- 如果没有销毁干净,下次
insmod就会报File exists(见后续排错)。
c
module_init(dht11_init);
module_exit(dht11_exit);
MODULE_LICENSE("GPL");
5. 现场实录:加载、运行、排错
下面是我们将上述代码编译为 gpio_drv.ko,在百问网板子上实际运行时遇到的报错和解决过程。终端记录完整保留,手把手教你根据错误日志定位问题。
5.1 insmod 报错 "File exists"
bash
[root@100ask:~]# insmod gpio_drv.ko
[188399.679123] sysfs: cannot create duplicate filename '/class/100ask_dht11_class'
insmod: ERROR: could not insert module gpio_drv.ko: File exists
原因分析 :
/sys/class/100ask_dht11_class 目录已经存在。说明上一次卸载模块时 class_destroy 没有成功执行(可能模块正在被占用,或者卸载命令根本没执行成功)。
验证:
bash
ls -l /sys/class/100ask_dht11_class
lrwxrwxrwx 1 root root 0 Jan 1 00:00 mydht11 -> ../../devices/virtual/100ask_dht11_class/mydht11
解决:手动删除残留的类目录。
bash
rmdir /sys/class/100ask_dht11_class/mydht11
rmdir /sys/class/100ask_dht11_class
如果提示
Device or resource busy,需要先杀掉正在使用/dev/mydht11的进程。
之后再 insmod 即成功。
教训 :模块退出函数必须保证执行,且资源释放顺序不能错。如果设备文件正在被打开,device_destroy 会失败,导致目录残留。这是嵌入式开发中极易踩的坑。
5.2 GPIO 输出与中断冲突
bash
[root@100ask:~]# ./button_test /dev/mydht11 &
[root@100ask:~]# [188643.165599] gpio-115 (dht11): _gpiod_direction_output_raw: tried to set a GPIO tied to an IRQ as output
[188643.195349] genirq: Flags mismatch irq 194. 00000003 (dht11) vs. 00000003 (dht11)
逐句解析:
- 第一条:
_gpiod_direction_output_raw尝试将一个已经处于中断模式 的 GPIO 再设置为输出,GPIO 子系统直接拒绝。这通常发生在read被快速连续调用,上一次的中断还没释放,或 GPIO 状态未完全复位。 - 第二条:
Flags mismatch说明中断 194 上已经挂了一个名为 "dht11" 的中断处理函数,而且触发标志都是0x3(双边沿)。request_irq发现完全相同的配置也要注册,但这在共享中断时才是合法的,而我们没有设置IRQF_SHARED,所以注册失败。
根本原因 :
dht11_read 内动态 request_irq / free_irq,但若某次 read 提前退出(如被信号中断),free_irq 根本不会执行,导致中断泄漏。下一次 read 再注册同样中断就会 flags mismatch。同时 GPIO 可能还带着"用于中断"的状态,导致输出操作也被拦截。
5.3 读取数据时有时无、返回 -1
测试程序输出片段:
text
get dht11: -1
get Humidity: 61, Temperature : 23
get dht11: -1
get dht11: -1
get Humidity: 61, Temperature : 23
get dht11: -1
...
-1的含义 :应用层测试程序判断read返回值 ≤0 时打印get dht11: -1。read可能返回-EINVAL(size!=2)、-EIO(中断注册失败)或-ERESTARTSYS(被信号打断且没有处理)。- 时而成功时而失败 :揭示了中断注册的间歇性失败。一次
read执行期间如果没有外部干扰,可能正好能成功注册中断、接收数据。但如果前一次read中断未释放,本次read就会注册失败返回-EIO。 - 重复得到相同数据 :当解析成功,缓冲区被写入温湿度后,如果应用只读了两个数就退出,下次
read可能又从缓冲区读到上一次遗留的数据。这里的get dht11: -1中间偶尔出现正常值,也验证了缓冲区可能有残留数据。
改进方向 :每次 read 前应该清空缓冲区,或者使用标志位保证数据的"新鲜度"。更彻底的方法是重构为中断长期注册。
6. 如何改进:更稳健的驱动设计
上面的代码虽然能跑通,但经不起频繁调用和异常断开。正确的框架姿势如下:
- 中断长期注册 :在
open或init中request_irq,在release或exit中free_irq。 - 使用 completion 或单一生产者标志 :引入
struct completion,用complete/wait_for_completion替代wait_event,这样可以避免缓冲区残留问题。 - 加入 mutex 防止重入 :确保一个设备同一时刻只有一个
read操作。 - 超时机制 :
wait_event_interruptible_timeout设 5 秒超时,超时后返回-ETIMEDOUT。 - 初始化时配置 GPIO 为输入 :可以在
init中先gpio_request→gpio_direction_input,然后直接free_irq时再释放。GPIO 方向只在发起始信号时临时切换为输出,用完立刻切回输入。 - 清理资源顺序 :严格对称,并在
exit中确认 GPIO 完全释放。
这样修改后,驱动在多进程并发、异常退出等情况下依然稳健。
7. 回顾与自测
实验效果

8. 回顾与自测
写完整个驱动并排完错,我们再来问自己几个问题,能回答出来说明真的掌握了:
- 为什么
register_chrdev参数major=0? - 等待队列如果没有
wake_up,谁来唤醒read? - 解析函数为什么从
i=4开始,而不是i=0? - 环形缓冲区满时
put_key直接丢弃,这样会丢数据,可不可行?(提示:生产者只有中断,消费者是进程,正常情况下进进程读取比中断慢得多,所以缓冲区设计要足够大) - 模块卸载后
/sys/class/100ask_dht11_class目录残留如何彻底避免? - 如果
request_irq在read里调用,被 Ctrl+C 打断时,free_irq没执行会有什么后果?怎么修复?
答案都在本文中,翻回去看就是一次有效复习。