本人从0开始学习linux,使用的是韦东山的教程,在跟着课程学习的情况下的所遇到的问题的总结,理论虽枯燥但是是基础。本人将前几章的内容大致学完之后,考虑到后续驱动方面得更多的开始实操,后续的内容将以韦东山教程Linux驱动入门实验班的内容为主,学习其中的代码并手敲。做到锻炼动手能力的同时钻研其中的理论知识点。
摘要:DHT11这篇文章是我第一次结合视频靠自己去分析原理图去撰写驱动代码
摘要关键词:DHT11

1.DHT11原理分析

在这里插入图片描述
这段引脚的说明本人是从DHT11 产品手册截取出来的。

在这里插入图片描述
手册中写道当你使用此模块的时候一般是推荐接一个4.7K上拉电阻的,多数模块是配有这个上拉电阻的。
在这里插入图片描述
手册中写道数据为40位,以及数据的构成。
在这里插入图片描述
手册中是这么写的,单片机起始信号先拉低至少18ms,然后单片机会接收到传感器发来的拉低83us,再拉高87us。然后发送40位数据。

在这里插入图片描述
其实你从它的示例中看的很清楚,校验位是由这几个数相加得到的。具体的计算步骤也很容易,就是将整数计算得到整数,小数计算得到小数。
在这里插入图片描述
手册中的这张图很抽象?我就直说吧,不管它。手册里面将单片机要干的事情写的很清楚。
步骤一:
DHT11上电后(DHT11上电后要等待1S以越过不稳定状态在此期间不能发送任何指令),测
试环境温湿度数据,并记录数据,同时DHT11的DATA数据线由上拉电阻拉高一直保持高电平;
此时DHT11的DATA引脚处于输入状态,时刻检测外部信号。

步骤二:
微处理器的I/O设置为输出同时输出低电平,且低电平保持时间不能小于18ms(最大不得
超过30ms),然后微处理器的I/O设置为输入状态,由于上拉电阻,微处理器的I/O即DHT11的
DATA数据线也随之变高,等待DHT11作出回答信号。发送信号如图4所示:
在这里插入图片描述

按道理来说,引脚应该设置成输入模式,不应该是输出模式输出高电平。输入模式理论上也有高低电平,这是stm32能做到的。后来发现linux不行,只能设置输出高低电平或者输入读取。

步骤三:
DHT11的DATA引脚检测到外部信号有低电平时,等待外部信号低电平结束,延迟后DHT11的
DATA引脚处于输出状态,输出83微秒的低电平作为应答信号,紧接着输出87微秒的高电平通知
外设准备接收数据,微处理器的I/O此时处于输入状态,检测到I/O有低电平(DHT11回应信号)
后,等待87微秒的高电平后的数据接收,发送信号如图5所示:
在这里插入图片描述
说直白一点就是模块在这发消息让你接告诉你,等会会发数据给你了。

步骤四:
由DHT11的DATA引脚输出40位数据,微处理器根据I/O电平的变化接收40位数据,
位数据“0”的格式为:54微秒的低电平和23-27微秒的高电平
位数据“1”的格式为:54微秒的低电平加68-74微秒的高电平。
位数据“0”、“1”格式信号如图6所示:
在这里插入图片描述
这里告诉你了数据中的0,1是怎么构造的。

结束信号:

DHT11的DATA引脚输出40位数据后,继续输出低电平54微秒后转为输入状态,由于上拉电
阻随之变为高电平。但DHT11内部重测环境温湿度数据,并记录数据,等待外部信号的到来。

也就是你叫我一次,我告诉你一次。54微秒后转为输入状态,别忘了你是接了一个上拉电阻的,所以输入状态变为1了。
小结:通过以上你应该明白,怎么叫醒dht11了,叫醒后它会回答一个“到!”,然后它就给你汇报它的内容,汇报的内容格式你也知道怎么处理了。汇报完成后它会回复一个”汇报结束!”这就是以上它的工作原理了。根据原理设计驱动程序。

驱动程序设计

首先驱动程序设计得先制定思路,板子只要3个引脚VCC,GND,GPIO4_19,设置为输入模式。需要用引脚的边缘触发去读取信息,也就需要环形缓冲区去处理数据。可能需要定时器中断,当我异常阻塞的时候,当作看门狗跳出程序。以上就是大致思路。
84的计算由来:

DHT11通信协议时序:
起始信号后DHT11的响应:
80μs低电平(产生下降沿)
80μs高电平(产生上升沿)
边沿数:240位数据(5字节)传输:
每1位数据由2个边沿组成:
50μs低电平(下降沿)
高电平持续时间决定数据值:
26-28μs:表示"0"(产生上升沿)
70μs:表示"1"(产生上升沿)
边沿数:40位 × 2 = 80结束信号:
50μs低电平(下降沿)
释放总线(上升沿)
边沿数:2

1.驱动初始化

头文件初始化,注册dht11中断服务函数;注册引脚配置结构体,设置功能;环形缓冲区。

#include "asm-generic/errno-base.h"
#include "asm-generic/gpio.h"
#include "linux/jiffies.h"
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/delay.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/timer.h>struct gpio_desc{int gpio;int irq;char *name;int key;struct timer_list key_timer;
} ;static struct gpio_desc gpios[] = {{115, 0, "dht11", },
};/* 主设备号                                                                 */
static int major = 0;
static struct class *gpio_class;static u64 g_dht11_irq_time[84];
static int g_dht11_irq_cnt = 0;/* 环形缓冲区 */
#define BUF_LEN 128
static char g_keys[BUF_LEN];
static int r, w;struct fasync_struct *button_fasync;static irqreturn_t dht11_isr(int irq, void *dev_id);
static void parse_dht11_datas(void);#define NEXT_POS(x) ((x+1) % BUF_LEN)static int is_key_buf_empty(void)
{return (r == w);
}static int is_key_buf_full(void)
{return (r == NEXT_POS(w));
}static void put_key(char key)
{if (!is_key_buf_full()){g_keys[w] = key;w = NEXT_POS(w);}
}static char get_key(void)
{char key = 0;if (!is_key_buf_empty()){key = g_keys[r];r = NEXT_POS(r);}return key;
}

2.驱动初始化入口函数

初始化引脚功能,初始化定时器中断。设置引脚中断,将引脚设置为输出,拉高引脚。

/* 在入口函数 */
static int __init dht11_init(void)
{int err;int i;int count = sizeof(gpios)/sizeof(gpios[0]);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);for (i = 0; i < count; i++){		gpios[i].irq  = gpio_to_irq(gpios[i].gpio);/* 设置DHT11 GPIO引脚的初始状态: output 1 */err = gpio_request(gpios[i].gpio, gpios[i].name);gpio_direction_output(gpios[i].gpio, 1);gpio_free(gpios[i].gpio);setup_timer(&gpios[i].key_timer, key_timer_expire, (unsigned long)&gpios[i]);//timer_setup(&gpios[i].key_timer, key_timer_expire, 0);//gpios[i].key_timer.expires = ~0;//add_timer(&gpios[i].key_timer);//err = request_irq(gpios[i].irq, dht11_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpios[i]);}/* 注册file_operations 	*/major = register_chrdev(0, "100ask_dht11", &dht11_drv);  /* /dev/gpio_desc */gpio_class = class_create(THIS_MODULE, "100ask_dht11_class");if (IS_ERR(gpio_class)) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);unregister_chrdev(major, "100ask_dht11");return PTR_ERR(gpio_class);}device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "mydht11"); /* /dev/mydht11 */return err;
}

3.dht11读取函数

首先发送18ms低脉冲后,引脚变为输入方向, 由上拉电阻拉为1,注册引脚边沿触发中断,以及定时器中断。休眠等待数据,调用 wait_event_interruptible 进入休眠,等待条件 !is_key_buf_empty() 成立(即环形缓冲区有数据)。释放引脚中断,设置引脚为高电平。将kern_buf读取环形缓冲区的数据,kern_buf[0] 读取湿度整数,kern_buf[1]读取温度整数。 先计划读取datas[0]和datas[2]。最后将kern_buf的数据给应用程序。
datas[0] // 湿度整数
datas[1] // 湿度小数(DHT11固定为0)
datas[2] // 温度整数
datas[3] // 温度小数(DHT11固定为0)
datas[4] // 校验和

/* 实现对应的open/read/write等函数,填入file_operations结构体                   */
static ssize_t dht11_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{int err;char kern_buf[2];if (size != 2)return -EINVAL;g_dht11_irq_cnt = 0;/* 1. 发送18ms的低脉冲 */err = gpio_request(gpios[0].gpio, gpios[0].name);gpio_direction_output(gpios[0].gpio, 1);mdelay(30);gpio_direction_output(gpios[0].gpio, 0);mdelay(20);gpio_direction_output(gpios[0].gpio, 1);udelay(40);gpio_direction_input(gpios[0].gpio);  /* 引脚变为输入方向, 由上拉电阻拉为1 *//* 2. 注册中断 */err = request_irq(gpios[0].irq, dht11_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, gpios[0].name, &gpios[0]);mod_timer(&gpios[0].key_timer, jiffies + 20);	/* 3. 休眠等待数据 */wait_event_interruptible(gpio_wait, !is_key_buf_empty());free_irq(gpios[0].irq, &gpios[0]);//printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);/* 设置DHT11 GPIO引脚的初始状态: output 1 */err = gpio_request(gpios[0].gpio, gpios[0].name);if (err){printk("%s %s %d, gpio_request err\n", __FILE__, __FUNCTION__, __LINE__);}gpio_direction_output(gpios[0].gpio, 1);gpio_free(gpios[0].gpio);/* 4. copy_to_user */kern_buf[0] = get_key();kern_buf[1] = get_key();printk("get val : 0x%x, 0x%x\n", kern_buf[0], kern_buf[1]);if ((kern_buf[0] == (char)-1) && (kern_buf[1] == (char)-1)){printk("get err val\n");return -EIO;}err = copy_to_user(buf, kern_buf, 2);return 2;
}

4.dht11中断服务函数

当边沿触发时,记录触发的时间,g_dht11_irq_time数组中记录每一次变化的时间。次数足够: 解析数据调用数据处理函数, 放入环形buffer, 唤醒APP,关闭定时器中断。

static irqreturn_t dht11_isr(int irq, void *dev_id)
{struct gpio_desc *gpio_desc = dev_id;u64 time;/* 1. 记录中断发生的时间 */time = ktime_get_ns();g_dht11_irq_time[g_dht11_irq_cnt] = time;/* 2. 累计次数 */g_dht11_irq_cnt++;/* 3. 次数足够: 解析数据, 放入环形buffer, 唤醒APP */if (g_dht11_irq_cnt == 84){del_timer(&gpio_desc->key_timer);parse_dht11_datas();}return IRQ_HANDLED;
}

5.数据处理函数

这个时候你就得想到,前面的中断可能丢失了,可能是81,82,83,84。但是低于82位的数据其实从某种意义来说已经没有意义了。
当数据个数小于81时,反馈出错,读取失败,终止阻塞。当数据大于81位时,从i = g_dht11_irq_cnt - 80位开始,i+=2位移1。
这个时候就得明白为什么高电平是high_time = g_dht11_irq_time[i] - g_dht11_irq_time[i-1];首先图5中可以看到,当接收数据的时候已经进入低电平了,也就是第0位g_dht11_irq_time[i-1]就是第一次触发的那个上升沿触发。而g_dht11_irq_time[i]则是那个下降沿,通过计算这个上升沿和下降沿之间的时间从而得到其为0,还是1。每次循环首先左移一位data,bits++。最终将datas[0]和datas[2]放入环形缓冲区。完成以上初级处理后 唤醒APPwake_up_interruptible(&gpio_wait);

static void parse_dht11_datas(void)
{int i;u64 high_time;unsigned char data = 0;int bits = 0;unsigned char datas[5];int byte = 0;unsigned char crc;/* 数据个数: 可能是81、82、83、84 */if (g_dht11_irq_cnt < 81){/* 出错 */put_key(-1);put_key(-1);// 唤醒APPwake_up_interruptible(&gpio_wait);g_dht11_irq_cnt = 0;return;}// 解析数据for (i = g_dht11_irq_cnt - 80; i < g_dht11_irq_cnt; i+=2){high_time = g_dht11_irq_time[i] - g_dht11_irq_time[i-1];data <<= 1;if (high_time > 50000) /* data 1 */{data |= 1;}bits++;if (bits == 8){datas[byte] = data;data = 0;bits = 0;byte++;}}// 放入环形buffercrc = datas[0] + datas[1] + datas[2] + datas[3];if (crc == datas[4]){put_key(datas[0]);put_key(datas[2]);}else{put_key(-1);put_key(-1);}g_dht11_irq_cnt = 0;// 唤醒APPwake_up_interruptible(&gpio_wait);
}

6.终止退出函数

/* 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数*/
static void __exit dht11_exit(void)
{int i;int count = sizeof(gpios)/sizeof(gpios[0]);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);device_destroy(gpio_class, MKDEV(major, 0));class_destroy(gpio_class);unregister_chrdev(major, "100ask_dht11");for (i = 0; i < count; i++){//free_irq(gpios[i].irq, &gpios[i]);//del_timer(&gpios[i].key_timer);}
}/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */module_init(dht11_init);
module_exit(dht11_exit);MODULE_LICENSE("GPL");

应用程序

应用程序只要读取即可,读取2字节的数据。


#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <signal.h>static int fd;/** ./button_test /dev/100ask_button0**/
int main(int argc, char **argv)
{char buf[2];int ret;int i;/* 1. 判断参数 */if (argc != 2) {printf("Usage: %s <dev>\n", argv[0]);return -1;}/* 2. 打开文件 */fd = open(argv[1], O_RDWR | O_NONBLOCK);if (fd == -1){printf("can not open file %s\n", argv[1]);return -1;}while (1){if (read(fd, buf, 2) == 2)printf("get Humidity: %d, Temperature : %d\n", buf[0], buf[1]);elseprintf("get dht11: -1\n");sleep(1);}//sleep(30);close(fd);
}

命令行

make
adb push gpio_drv.ko button_test root
insmod  gpio_drv.ko
rmmod gpio_drv.ko 
./button_test /dev/mydht11

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
可以看到我的中断也经常丢,3666-3593=74,连数据都丢了。而阻塞的优势:对于微秒级信号,使用内核gpiod_get_value轮询比中断更可靠,而 gpio_direction_output,gpio_request这一类函数又非常需要时间。所以对于这种单线的最好的处理方式就是轮询。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/91216.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/91216.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/91216.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

国内AI IDE竞逐:腾讯CodeBuddy、阿里通义灵码、字节跳动TRAE、百度文心快码

国内AI IDE竞逐&#xff1a;腾讯CodeBuddy、阿里通义灵码、字节跳动TRAE、百度文心快码 随着人工智能技术的不断发展&#xff0c;各大科技公司纷纷推出自家的AI IDE&#xff0c;推动软件开发进入全新的智能化时代。腾讯的 CodeBuddy IDE、阿里云的 通义灵码 AI IDE、字节跳动的…

git rebase使用教程 以及和merge的区别

Merge和Rebase概念概述 rebase 和 merge 相似&#xff0c;但又不完全相同&#xff0c;本质上都是用来合并分支的命令&#xff0c;区别如下 merge合并分支会多出一条merge commit记录&#xff0c;而rebase不会merge的提交树是非线性的&#xff0c;会有分叉&#xff0c;而rebase的…

React中的合成事件解释和理解

什么是合成事件&#xff08;Synthetic event&#xff09;?它和原生事件有什么区别?解题思路:解释合成事件&#xff0c;然后对比原生事件&#xff0c;然后再说他的优势1.一致性 在 react里面&#xff0c;这个合成事件是非常重要的&#xff0c;因为它就是为了解决浏览器之间与事…

【Python系列】使用 memory_profiler 诊断 Flask 应用内存问题

博客目录一、内存分析的重要性二、memory_profiler 基础使用安装与基本配置理解分析报告三、在 Flask 应用中使用 memory_profiler装饰视图函数使用 mprof 进行长期监控四、高级内存分析技巧精确测量代码块内存定期内存采样结合 objgraph 分析对象引用五、常见内存问题及解决方…

vue3【组件封装】超级表单 S-form.vue

最终效果 代码实现 components/SUI/S-form.vue <script lang"ts" setup> import type { FormInstance } from "element-plus";// 使用索引签名定义对象类型 type GenericObject {[key: string]: any; };const props defineProps<{Model?: Gen…

Android Studio Memory Monitor内存分析核心指标详解

Depth、Native Size、Shallow Size、Retained Size 解析 一、指标定义与对比指标定义计算逻辑重要性Shallow Size对象自身实例占用的内存基本类型字段大小 引用指针 内存对齐对象的基础内存成本Retained Size回收该对象可释放的总内存量&#xff08;含所有依赖对象&#xff0…

vue中使用wavesurfer.js绘制波形图和频谱图(支持.pcm)

新的实现方式&#xff1a;vue使用Canvas绘制频谱图 安装wavesurfer.js npm install wavesurfer.js第一版&#xff1a; 组件特点&#xff1a; 一次性加载好所有的数据&#xff1b; <template><div class"audio-visualizer-container"><div class&…

go mod教程、go module

什么是go mod go mod 是go语言的包管理工具&#xff0c;类似java 的maven&#xff0c;go mod的出现可以告别goPath&#xff0c;使用go module来管理项目&#xff0c;有了go mod账号就不需要非得把项目放到gopath/src目录下了&#xff0c;你可以在磁盘的任何位置新建一个项目 go…

150-SWT-MCNN-BiGRU-Attention分类预测模型等!

150-SWT-MCNN-BiGRU-Attention分类预测模型!基于多尺度卷积神经网络(MCNN)双向长短期记忆网络(BiGRU)注意力机制(Attention)的分类预测模型&#xff0c;matlab代码&#xff0c;直接运行使用&#xff01;1、模型介绍&#xff1a;针对传统方法在噪声环境下诊断精度低的问题&#…

MySQL数据一致性与主从延迟深度解析:从内核机制到生产实践

在高并发分布式系统中&#xff0c;数据一致性与复制延迟如同硬币的两面。本文深入剖析MySQL持久化机制与主从同步原理&#xff0c;并提供可落地的调优方案。一、数据持久化核心机制&#xff1a;双日志协同 1. Redo Log&#xff1a;崩溃恢复的生命线刷新策略&#xff08;innodb_…

【I】题目解析

目录 单选题 多选题 判断题 单选题 1.reg[7:0]A; A2hFF;则A&#xff08;&#xff09; A.8b11111110 B.8b03 C.8b00000011 D.8b11111111 C 2hFF实际上等效于2位二进制2b11&#xff0c;赋值给8位寄存器A之后&#xff0c;低位赋值&#xff0c;高位补0 A8b00000011 AMD FPG…

《Foundation 面板:设计、功能与最佳实践解析》

《Foundation 面板:设计、功能与最佳实践解析》 引言 在当今数字化时代,用户界面(UI)设计的重要性不言而喻。其中,Foundation 面板作为一种流行的前端框架,因其灵活性和高效性而被众多开发者所青睐。本文将深入解析 Foundation 面板的设计理念、功能特点以及最佳实践,…

React服务端渲染 Next 使用详解

1. Next.js 概述 Next.js 是一个基于 React 的开源框架&#xff0c;专注于服务器端渲染&#xff08;SSR&#xff09;和静态站点生成&#xff08;SSG&#xff09;&#xff0c;提供开箱即用的 SSR 功能&#xff0c;简化 React 应用的开发与部署。 2. Next.js 的核心特性 SSR 支…

Deforum Stable Diffusion,轻松实现AI视频生成自由!

摘要&#xff1a; 你是否曾被那些充满想象力、画面流畅的AI视频所震撼&#xff1f;你是否也想亲手创造出属于自己的AI动画&#xff1f;本文将为你提供一份“保姆级”的详尽教程&#xff0c;从环境配置到参数调整&#xff0c;一步步带你复现强大的Deforum Stable Diffusion模型&…

不同环境安装配置redis

不同环境安装配置redis windows 环境安装redis redis所有下载地址 windows版本redis下载&#xff08;GitHub&#xff09;&#xff1a; https://github.com/tporadowski/redis/releases &#xff08;推荐使用&#xff09;https://github.com/MicrosoftArchive/redis/releases]官…

汇川Easy系列PLC算法系列(回溯法ST语言实现)

Easy系列PLC 3次多项式轨迹插补算法 Easy系列PLC 3次多项式轨迹插补算法(完整ST代码)_plc连续插补算法-CSDN博客文章浏览阅读122次。INbExecuteBOOLOFFOFF不保持1INrStartPosREAL0.0000000.000000不保持起始位置unit2INrEndPosREAL0.0000000.000000不保持结束位置unit3INrStar…

Linux C:构造数据类型

目录 一、结构体&#xff08;struct&#xff09; 1.1类型定义 1.2 结构体变量定义 1.3 结构体元素初始化 1.4 结构体成员访问 1.5 结构体的存储&#xff08;内存对齐&#xff09; 1.6 结构体传参 本文主要记录了C语言中构造数据类型部分的内容&#xff0c;今天暂时只写了…

Python:self

在Python面向对象编程中&#xff0c;self是一个指向类实例自身的引用参数&#xff1a;‌1. 本质与作用‌‌身份标识‌&#xff1a;self是类实例化后对象的"身份证"&#xff0c;代表当前实例本身&#xff0c;用于区分不同实例的属性和方法‌‌自动传递‌&#xff1a;调…

【SpringMVC】SpringMVC的概念、创建及相关配置

什么是SpringMVC 概述 中文翻译版&#xff1a;Servlet 栈的 Web 应用 Spring MVC是Spring Framework的一部分&#xff0c;是基于Java实现MVC的轻量级Web框架。 查看官方文档&#xff1a;https://docs.spring.io/spring/docs/5.2.0.RELEASE/spring-framework-reference/web.h…

浅谈存储过程

问题引入 面试的时候有时候会问到知不知道存储过程&#xff0c;用没用过&#xff1f; 是什么 存储过程&#xff08;Stored Procedure&#xff09;是在大型数据库系统中&#xff0c;一组为了完成特定功能的SQL 语句集&#xff0c;它存储在数据库中&#xff0c;一次编译后永久…