Linux外设驱动开发1 - 单总线驱动开发__dht11
设备树描述硬件 + 驱动解析协议 + 应用层处理数据
硬件信息由设备树描述,硬件操作由驱动实现,业务逻辑由应用处理
一、单总线介绍
1、单总线协议是一种巧妙的设计,它只用一根数据线(外加地线)就能实现设备间的双向通信
2、通信模式半双工,主从结构,所有信号由主机发起
3、关键优势布线极简、成本低、易于扩展、支持“寄生供电”主要局限带宽受限、时序要求严格、长距离传输需抗干扰
二、DHT11测温湿度传感器
1、基本信息
核心功能 | 同时测量环境温度和湿度 |
输出信号 | 单总线数字信号,与微控制器连接简单 |
测量范围 | 湿度:20% ~ 90% RH;温度:0℃ ~ 50℃ |
测量精度 | 湿度:±5% RH;温度:±2℃ |
分辨率 | 湿度:1% RH;温度:1℃ |
工作电压 | 3.3V ~ 5.5V,兼容常见的单片机逻辑电平 |
功耗 | 平均工作电流约0.5mA,功耗很低 |
引脚封装 | 常见为3针或4针单排引脚(4针款有一引脚悬空) |
2、时序图
1)主机发送起始信号(总线空闲时状态是拉高)
- 步骤 1:主机将总线拉低 至少 18ms
- 唤醒 DHT11,确保传感器检测到信号
- 步骤 2:主机释放总线(拉高),等待 20~40us
- 给传感器准备响应的时间
- 步骤 3:主机将总线切换为输入模式,等待传感器响应
2)从机响应信号(传感器收到主机信号并答复)
- 步骤 1:传感器拉低总线 80us
- 告诉主机 “已收到请求”
- 步骤 2:传感器拉高总线 80us
- 告诉主机 “准备发送数据”
3)数据传输时序:
传感器响应后,连续发送 40 位数据(高位在前),40 位数据的最后 8 位是 “校验和”,其值等于前 4 字节(32 位)之和,格式为:
[8 位湿度整数] + [8 位湿度小数] + [8 位温度整数] + [8 位温度小数] + [8 位校验和]
通常校验:校验和 = 湿度整数 + 湿度小数 + 温度整数 + 温度小数
单个 bit 的表示方式:
- 数据 “0”:传感器拉低总线 50us左右,然后拉高总线 26~28us。
- 数据 “1”:传感器拉低总线 50us左右,然后拉高总线 68-74us。
4)完整时序图:

三、DHT11单总线外设驱动开发过程
1、设备树准备过程:
- 修改设备树,添加传感器硬件描述配置(arch/arm/boot/dts/imx6ull-alientek-emmc.dts)
- 设备树的根节点或对应子系统节点下:添加设备节点
- 设备树的
iomuxc
节点下:配置GPIO 引脚的 复用功能、电器特性
- 设备树的根节点或对应子系统节点下:添加设备节点
- 编译设备树
- make dtbs
- 将编译好的(arch/arm/boot/dts/imx6ull-alientek-emmc.dtb)复制到tftpboot目录下
- tftpboot:利用TFTP协议实现开发板的远程文件加载
- TFTP(简单文件传输协议):是一种基于 UDP 的轻量级协议,专为嵌入式开发设计,支持无认证的文件下载 / 上传,常用于开发板的远程启动和文件更新。
- 目录作用:开发板(如 IMX6ULL)在启动阶段(如 U-Boot 环境下),可通过网络从
tftpboot
目录下载所需文件,无需依赖本地存储(如 SD 卡、NAND Flash),极大提升开发调试效率。
- tftpboot:利用TFTP协议实现开发板的远程文件加载
2、代码大体框架:
- dht11_app(应用层程序)
- dht11_app.c
- makefile
- dht11_drv(驱动层程序)
- dht11_drv.c
- makefiel
- makefie
1)dht11_app(应用层程序)
- dht11_app.c
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>int main(void)
{
int fd = 0;
ssize_t nret = 0;
unsigned char data[4] = {0};
char hum[8] = {0};
char temp[8] = {0};//名字别错了
fd = open("/dev/dht11", O_RDWR);
if (-1 == fd)
{
perror("fail to open");
return -1;
}while (1)
{
nret = read(fd, data, sizeof(data));
if (4 == nret)
{
sprintf(hum, "%d.%d", data[0], data[1]);
sprintf(temp, "%d.%d", data[2], data[3]);
printf("temp = %s, hum = %s\n", temp, hum);
}sleep(2);
}close(fd);
return 0;
}
- makefile
modulename:=dht11_app
cc:=arm-linux-gnueabihf-gcc
#$()引用变量或调用函数
$(modulename):$(modulename).c
$(cc) $^ -o $@
cp $(modulename) ~/nfs/imx6/rootfs
#将可执行文件复制到该目录下.PHONY:
clean:
rm $(modulename) -r
distclean:
rm $(modulename) -r
rm ~/nfs/imx6/rootfs/$(modulename) -r
2)dht11_drv(驱动层程序)
- dht11_drv.c
#include <asm/delay.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include "asm-generic/gpio.h"
#include "asm/gpio.h"
#include "linux/mod_devicetable.h"
#include "linux/nodemask.h"
#include "linux/printk.h"static int gpio_dht11 = 0;
extern void msleep(unsigned int msecs);// -7.1- MCU发送复位函数(启动信号)
static void dht11_reset(void)
{
/* "敲门告诉传感器,我要准备读数据"
1.初始化GPIO为输出模式并置高
2.主机拉低总线,发送起始信号
3.保持低电平至少18ms
4.主机释放总线,等待传感器响应*/gpio_direction_output(gpio_dht11, 1);//设置刚开始是空闲拉高状态
gpio_set_value(gpio_dht11, 0);
msleep(20);
gpio_set_value(gpio_dht11, 1);return;
}
// -7.2- 检测传感器是否响应MCU (先拉低80微秒,在拉高80微秒)
static int recv_dht11_respone(void)
{
int timeout = 0;gpio_direction_input(gpio_dht11); // GPIO引脚设置为输入模式,准备读取传感器信号
timeout = 0;
while (gpio_get_value(gpio_dht11) && timeout <= 10) //等待DHT11将数据线拉低(100微秒) &&逻辑与不是按位与
{
udelay(10); //微秒
timeout++;
}
if (timeout > 10) //超时未处理,失败返回
{
return -1;
}
while (!gpio_get_value(gpio_dht11)); //等待结束低电平
while (gpio_get_value(gpio_dht11)); //等待结束高电平return 0;
}
// -7.3-
static unsigned char recv_dht11_byte(void)
{
int cnt = 0;
//在计算机内部本身就是以二进制形式存储
unsigned char value = 0;
int n = 0;gpio_direction_input(gpio_dht11);
for (n = 7; n >= 0; n--)
{
while (!gpio_get_value(gpio_dht11));cnt = 0;
while (gpio_get_value(gpio_dht11))
{
udelay(10);
cnt++;
}// value的初始状态是0,当判断出某位是1时,才需要进行对应某位进行"或"
if (cnt > 5)
{
value |= (0x1 << n);
}
}
return value;
}static void recv_dht11_data(unsigned char *pdata)
{
//湿度整数部分
pdata[0] = recv_dht11_byte();
//湿度小数部分
pdata[1] = recv_dht11_byte();
//温度整数部分
pdata[2] = recv_dht11_byte();
//温度小数部分
pdata[3] = recv_dht11_byte();
//校验位
pdata[4] = recv_dht11_byte();return;
}
// -7- 调函数
/*fp: puser: n: off:*/
static ssize_t dht11_read(struct file *fp, char __user *puser, size_t n, loff_t *off)
{
int ret = 0;
long nret = 0;
unsigned char data[5] = {0};dht11_reset();
ret = recv_dht11_respone();
if (ret)
{
return ret;
}
recv_dht11_data(data);
gpio_direction_output(gpio_dht11, 1);if (data[0] + data[1] + data[2] + data[3] != data[4])
{
return -2;
}nret = copy_to_user(puser, data, 4);
if (nret)
{
pr_info("copy_to_user failed\n");
return -3;
}return 4;
}// -6- 设备操作函数集
static struct file_operations dht11_fops = {
.owner = THIS_MODULE, //安全机制,避免多次加载
.read = dht11_read, //调函数
};// -5- 注册设备
static struct miscdevice misc_dht11 = {
.minor = MISC_DYNAMIC_MINOR, //动态分配"次设备号"
.name = "dht11", //"设备节点名",将在/dev下生成
.fops = &dht11_fops, //指向"结构体"文件操作函数集(open,read)
};// -3- 注册
static int dht11_probe(struct platform_device *pdev)
{
int ret = 0;
struct device_node *pht11node = NULL;pr_info("dht11_probe: start\n");
ret = misc_register(&misc_dht11);
if (ret)
{
pr_err("misc_register failed, ret=%d\n", ret);
return -1;
}pht11node = of_find_node_by_path("/fddht11");
if (NULL == pht11node)
{
pr_info("of_find_node_by_path failed\n");
return -1;
}gpio_dht11 = of_get_named_gpio(pht11node, "gpio-dht11", 0);
if (gpio_dht11 < 0)
{
pr_info("of_get_named_gpio failed\n");
return -1;
}ret = devm_gpio_request(misc_dht11.this_device, gpio_dht11, "fd-dht11");
if (ret)
{
pr_info("devm_gpio_request failed\n");
return -1;
}gpio_direction_output(gpio_dht11, 1);
pr_info("dht11_probe: success\n");return 0;
}// -4- 销毁
static int dht11_remove(struct platform_device *pdev)
{
int ret = 0;ret = misc_deregister(&misc_dht11);
if (ret)
{
pr_info("misc_deregister failed\n");
return -1;
}return 0;
}// -2- 匹配模式
//用于设备树匹配,匹配顺序:最高_会优先进行这个匹配
static struct of_device_id dht11_of_match_table[] = {
{.compatible = "fd,fddht11"},
{},
};
//用于非设备树或备选匹配,匹配顺序:次高(不支持设备树的旧系统,增加兼容性)
static struct platform_device_id dht11_id_table[] = {
{.name = "fddht11"},
{},
};// -1-
static struct platform_driver dht11_driver = {
.probe = dht11_probe,
.remove = dht11_remove,
.driver =
{
.name = "fddht11",
.owner = THIS_MODULE,
.of_match_table = dht11_of_match_table,
},
.id_table = dht11_id_table,
};module_platform_driver(dht11_driver); //带参宏
MODULE_LICENSE("GPL");
MODULE_AUTHOR("FD");
- makefile
#定义模块名字
modulename:=dht11_drv#内核源码路径
kerdir:=/home/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek#获得当前makefile的路径
curdir:=$(shell pwd)#obj-m:表示将该文件编译为动态加载模块(.ko)
obj-m+=$(modulename).o#-C :切换到指定目录下
#M=$(curdir) 告诉内核makefile,模块源码在该目录下
all:
make -C $(kerdir) M=$(curdir) modules
cp $(modulename).ko ~/nfs/imx6/rootfs#伪目标:强制 Make 执行目标对应的命令,忽略是否存在同名文件
.PHONY:
#清理过程性生成的中间文件,留下ko
clean:
make -C $(kerdir) M=$(curdir) modules clean#彻底清理
distclean:
make -C $(kerdir) M=$(curdir) modules clean
rm ~/nfs/rootfs/$(modulename).ko
3)makefile
modulename:=dht11
all:
make -C $(modulename)_app
make -C $(modulename)_drv.PHONY:
clean:
make -C $(modulename)_app clean
make -C $(modulename)_drv clean
distclean:
make -C $(modulename)_app distclean
make -C $(modulename)_drv distclean
3、嵌入式应用程序加载与执行
1)通过串口通信工具(如 minicom)连接嵌入式开发板(目标板)
2)将编译生成的内核模块(.ko 文件)加载到目标板的操作系统内核中(insmod dht11_drv.ko)
3)随后运行在目标板上的可执行程序,以验证模块功能或应用程序逻辑(./dht11_app)