DHT11 驱动开发实录:从零搭建 Linux 字符设备驱动框架(保姆级教学)

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 硬件接线与通信协议![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c21ecada72e74afbae71e5a8840d0f8b.png)](#文章目录 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 existsGPIO 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 发起,过程如下:

  1. MCU 发出开始信号
    MCU 将 DATA 拉低 ≥ 18ms,然后拉高 20~40μs,随后释放总线(改为输入状态)。
  2. DHT11 应答
    DHT11 检测到起始信号后,拉低 80μs,再拉高 80μs 作为应答。
  3. DHT11 发送 40bit 数据
    每 bit 由一段约 50μs 的低电平加一段高电平组成。
    位值的区分 完全取决于高电平的持续时间:
    • 高电平持续 26~28μs → 表示 "0"
    • 高电平持续 70μs → 表示 "1"
  4. 传输结束
    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_keyread 里调用,两者无需额外加锁,因为这里的生产者只有中断,消费者只有进程,且中断不会嵌套(同一个 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 > 50000 ns (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: -1read 可能返回 -EINVAL(size!=2)、-EIO(中断注册失败)或 -ERESTARTSYS(被信号打断且没有处理)。
  • 时而成功时而失败 :揭示了中断注册的间歇性失败。一次 read 执行期间如果没有外部干扰,可能正好能成功注册中断、接收数据。但如果前一次 read 中断未释放,本次 read 就会注册失败返回 -EIO
  • 重复得到相同数据 :当解析成功,缓冲区被写入温湿度后,如果应用只读了两个数就退出,下次 read 可能又从缓冲区读到上一次遗留的数据。这里的 get dht11: -1 中间偶尔出现正常值,也验证了缓冲区可能有残留数据。

改进方向 :每次 read 前应该清空缓冲区,或者使用标志位保证数据的"新鲜度"。更彻底的方法是重构为中断长期注册。


6. 如何改进:更稳健的驱动设计

上面的代码虽然能跑通,但经不起频繁调用和异常断开。正确的框架姿势如下:

  1. 中断长期注册 :在 openinitrequest_irq,在 releaseexitfree_irq
  2. 使用 completion 或单一生产者标志 :引入 struct completion,用 complete / wait_for_completion 替代 wait_event,这样可以避免缓冲区残留问题。
  3. 加入 mutex 防止重入 :确保一个设备同一时刻只有一个 read 操作。
  4. 超时机制wait_event_interruptible_timeout 设 5 秒超时,超时后返回 -ETIMEDOUT
  5. 初始化时配置 GPIO 为输入 :可以在 init 中先 gpio_requestgpio_direction_input,然后直接 free_irq 时再释放。GPIO 方向只在发起始信号时临时切换为输出,用完立刻切回输入。
  6. 清理资源顺序 :严格对称,并在 exit 中确认 GPIO 完全释放。

这样修改后,驱动在多进程并发、异常退出等情况下依然稳健。


7. 回顾与自测

实验效果

8. 回顾与自测

写完整个驱动并排完错,我们再来问自己几个问题,能回答出来说明真的掌握了:

  1. 为什么 register_chrdev 参数 major=0
  2. 等待队列如果没有 wake_up,谁来唤醒 read
  3. 解析函数为什么从 i=4 开始,而不是 i=0
  4. 环形缓冲区满时 put_key 直接丢弃,这样会丢数据,可不可行?(提示:生产者只有中断,消费者是进程,正常情况下进进程读取比中断慢得多,所以缓冲区设计要足够大)
  5. 模块卸载后 /sys/class/100ask_dht11_class 目录残留如何彻底避免?
  6. 如果 request_irqread 里调用,被 Ctrl+C 打断时,free_irq 没执行会有什么后果?怎么修复?

答案都在本文中,翻回去看就是一次有效复习。

相关推荐
艾莉丝努力练剑2 小时前
【Linux网络】计算机网络入门:网络通信——跨主机的进程间通信(IPC)与Socket编程入门
linux·运维·服务器·网络·c++·学习·计算机网络
七夜zippoe2 小时前
2026年4月横评:远程软件内卷破局!UU 远程凭实力成为远程工具综合首选
运维·服务器·负载均衡·远程·协助
炘爚2 小时前
Linux :进程间通信(IPC)与信号
linux·进程间通信
Lfei51202 小时前
Centos 9 stream部署zabbix7.0.25(最新)
linux·运维·centos
枫叶落雨2222 小时前
服务器下载两个jdk
linux·运维·服务器
Elivs.Xiang2 小时前
基于docker安装MySQL、RabbitMQ、ElasticSearch、minio
linux·mysql·elasticsearch·docker·rabbitmq
极光1312 小时前
DevOps学习
运维·学习·devops
TechMasterPlus2 小时前
Claude Code CLI 使用教程:从安装到项目自动化实践
运维·自动化
William Dawson2 小时前
Jenkins 操作文档及使用方法(新手入门\+实战详解)
运维·jenkins