??本人從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這一類函數又非常需要時間。所以對于這種單線的最好的處理方式就是輪詢。