当前位置: 首页 > news >正文

从0开始学linux韦东山教程Linux驱动入门实验班(7)

  本人从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.lryc.cn/news/605049.html

相关文章:

  • 不止 “听懂”,更能 “感知”!移远通信全新AI 音频模组 重新定义智能家居“听觉”逻辑
  • 【Datawhale AI夏令营】科大讯飞AI大赛(大模型技术)/夏令营:让AI理解列车排期表(Task3)
  • 如何将DICOM文件制作成在线云胶片
  • 一句话指令实现“2D转3D”、“图片提取线稿”
  • Kong API Gateway深度解析:插件系统与微服务架构的技术基石
  • Python爬虫05_Requests肯德基餐厅位置爬取
  • 企业微信API接口发消息实战:从0到1的技术突破之旅
  • 新注册企业信息查询“数据大集网”:驱动企业增长的源头活水
  • 笔试——Day23
  • C++ 项目 QML QtQuick.Controls“ is not installed
  • 【C语言】深度剖析指针(二):指针与数组,字符,函数的深度关联
  • 基于 Amazon Bedrock 与 Anthropic Claude 3 智能文档处理方案:从扫描件提取到数据入库全流程实践
  • C++入门基础 1
  • 【MySQL 数据库】MySQL索引特性(二)页目录(B和B+树)(非)聚簇索引 索引操作
  • 293F细胞是什么?
  • Service Mesh
  • 使用HaiSnap做了一款取件码App(一键生成)
  • 修改Windows鼠标滚轮方向
  • Haproxy 七层代理深度解析
  • 《校园生活平台从 0 到 1 的搭建》第五篇:商品后端
  • Qt 嵌入式 Linux 系统定制全指南
  • Nuxt3 全栈作品【通用信息管理系统】用户管理(含重置密码)
  • 第十二天:C++ 标准库函数分类总结
  • spark入门-helloword
  • 干货 | ANSYS复合材料前后处理
  • 跨云部署实战:前端、后端 + RSYNC、全栈场景统一落地方案
  • Nestjs框架: 关于 OOP / FP / FRP 编程
  • Map 集合
  • 高可靠液晶屏系统解决方案深度解析
  • AI 驱动的软件测试革新:框架、检测与优化实践