這邊會用傳統kernel的code驅動DHT11(測量溫度跟溼度)
DHT11的規格與使用方法在此:
PS: 其實Raspberry pi已有內建的溫度測量(不過是測量raspberry pi 機板的溫度,不是室溫喔):在terminal下執行以下命令即可。
/opt/vc/bin/vcgencmd measure_temp
DHT11有四個接腳,接法如下(圖片來源:www.aosong.com):
這邊VDD可以用raspberry pi 提供的3.3V或是5V source(我是用5V,據說會穩定點),Pin2的data out就接gpio。由於DHT11是使用
1-Wire傳輸協定傳送data,所以只需要1個gpio。
1-Wire協定有點像這樣(以下為擬人化示範):
使用者:喂!把資料丟給我阿!(傳送一個已經約定好長度的訊號,然後等接收)
DHT11:OK!我要丟囉!(傳送一個已經約定好長度的訊號表示OK,然後準備資料)
DHT11:OO_OO___OOO__OOO(O表示0,_表示1,全部傳好後你自己翻譯吧)
這邊由於使用者要先送一個訊號出去,DHT11才會開始發送data。如果在DHT11發送data的時候,另一個白目的使用者也送訊號說要data,DHT11就會發生錯亂,這樣前後兩個使用者可能都不會收到正確data(嚴重時會造成crash)。所以我們必須要避免這種情況。
再說DHT11的一些優缺點。
優點:便宜、穩定、體積小、功耗低
缺點:測量範圍略小(20-90%RH,0-50°C)、反應慢(讀取間隔至少1s)、精度低(溼度+-5%RH,溫度+-2°C)、讀取失敗率有點高
基於這些優缺點,DHT11不適合以高頻率(>1/s)密集測量,所以這次的驅動程式規格為:
- gpio pin預設值為2,載入時可更改設定
- 將DHT11設成字元裝置/dev/DHT11
- 當使用者"開啟"/dev/DHT11時,執行測量(並自動檢查數據結果,若是無效數據可以重試五次以內)
- 當使用者"讀取"/dev/DHT11時,告訴使用者測量的結果 (與上一個步驟合併就是"cat"啦)
- 當有另一個使用者想要開啟已經被開啟的/dev/DHT11時,告訴此使用者系統忙碌。
這邊又到了沒圖沒真相的時候:
所以在這個例子裡,struct file_operations需要定義的有:.owner(模組本身),.open(開啟字元設備檔案的動作),.read(讀取字元設備檔案的動作),.release(關閉字元檔案的動作)
static struct file_operations fops = {
.owner = THIS_MODULE,
.read = read_dht11,
.open = open_dht11,
.release = close_dht11
};
開啟字元檔案的時候,要做這些事:
- 如果設備沒有被人佔用,那第一件事就要做個「佔用」的動作(mutex_lock)。
- 重新初始一個「完成量」(completion):由於每讀取一次DHT11的資料至少需要4ms,必須要有一個機制告訴系統「我們做完了,可以繼續了!」,不然就是限時做完不然系統不等了。這邊的completion就是要做這件事。
- 設定指定的gpio為output,停個20ms後(這邊用msleep為宜,這樣CPU就不用守在那)給個約40us的信號,然後轉成input並紀錄時間
- 進入中斷(interrupt)處理程序以便接收資料(後面解釋)
- 接收完就把中斷處理程序取消掉
- 開始處理資料,如果資料不正確就等1s後重新讀取(從"設定指定的gpio為output"開始...)
static int open_dht11(struct inode *inode, struct file *file)
{
char result[3]; //To say if the result is trustworthy or not
int retry = 0, ret;
// 試著"鎖上",如果鎖成功表示沒人佔用,不成功則相反
if (!mutex_trylock(&gpio_mutex)) {
printk(KERN_ERR DHT11_DRIVER_NAME " another process is accessing the device\n");
return -EBUSY;
}
// 重新初始完成量
reinit_completion(&gpio_completion);
printk(KERN_INFO DHT11_DRIVER_NAME " Start setup (read_dht11)\n");
start_read:
nBit = 0;
// gpio output
gpio_direction_output(gpio_pin, 0);
msleep(20); // DHT11 needs min 18mS to signal a startup
gpio_direction_output(gpio_pin, 1);
udelay(40); // Stay high for a bit before swapping to read mode
// gpio轉成input
gpio_direction_input(gpio_pin);
//Start timer to time pulse length
do_gettimeofday(&lasttv);
// Set up interrupts
setup_interrupts();
//Give the dht11 time to reply
//這邊的 HZ是系統給定,在RPi=100,在這裡也就是說過了1/100秒如果還沒完成就不等了
ret = wait_for_completion_killable_timeout(&gpio_completion,HZ);
//取消中斷
clear_interrupts();
//Check if the read results are valid. If not then try again!
if(dht11_decode()==0) sprintf(result, "OK"); // 用checksum確認是否是正確的值
else {
retry++;
sprintf(result, "BAD");
if(retry == 10) goto return_result; //We tried 5 times so bail out
ssleep(1);
goto start_read;
}
return_result:
sprintf(msg, "Humidity: %d.%d%%\nTemperature: %d.%dC\nResult:%s\n", dht11.Humidity[0], dht11.Humidity[1], dht11.Temperature[0], dht11.Temperature[1], result);
msg_Ptr = msg;
printk("strlen is %d", strlen(msg));
return SUCCESS;
}
然後關閉檔案的步驟就超簡單,把互斥鎖解掉就好了:
static int close_dht11(struct inode *inode, struct file *file)
{
mutex_unlock(&gpio_mutex);
printk(KERN_INFO DHT11_DRIVER_NAME ": Device release (close_dht11)\n");
return 0;
}
至於中斷(interrupt)是個超級好用的東西,我們可以想像一個CPU在沒有中斷的情況下要怎麼工作(以下擬人化):
CPU:賈爸鎂? 硬體A:阿鎂
CPU:賈爸鎂? 硬體B:阿鎂
CPU:賈爸鎂? 硬體C:阿鎂
CPU:賈爸鎂? 硬體D:阿鎂
....問到Z以後,重來
(以上就是所謂的「輪詢」(polling))
若是有中斷就好多了:
硬體A:挖巴豆夭阿 CPU:ㄆㄨㄣ來囉
硬體C:挖巴豆夭阿 CPU:ㄆㄨㄣ來囉
硬體F:挖巴豆夭阿 CPU:ㄆㄨㄣ來囉
......(以上就是誰先靠腰誰就先吃的中斷模式)
至於讀取DHT11為何要用到中斷呢?
看DHT11的使用說明,當系統/使用者給了他一個40us的訊號以後轉接收模式,約80us後DHT11會送個約80us長的訊號表示他收到了然後準備輸出。接下來每個bit訊號以50us(Off)開始,若是這50us Off後面接的是約70us的長訊號就是bit1,若是只有約24-26us的短訊號就是bit0(如圖,圖片來源:www.aosong.com)。
所以,當gpio接收的訊號從0轉成1或1轉成0,CPU都要去看看到底發生了啥事(看是給我bit0還是bit1),然後CPU又很忙不可能一天到晚問改變了沒,所以要使用「中斷」-- 當gpio接收的訊號改變,就同時送一個硬體訊號給CPU,告訴CPU來處理。
設定中斷的函式很簡單:
static int setup_interrupts(void)
{
int result;
// 跟系統提出使用中斷的請求
// gpio_to_irq(gpio_pin) 是系統給的irq值
// irq_handler 是告訴CPU如果中斷發生要怎麼處理
result = request_irq(gpio_to_irq(gpio_pin), (irq_handler_t) irq_handler, IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING, DHT11_DRIVER_NAME, NULL);
switch (result) {
case -EBUSY:
printk(KERN_ERR DHT11_DRIVER_NAME ": IRQ %d is busy\n", INTERRUPT_GPIO0);
return -EBUSY;
case -EINVAL:
printk(KERN_ERR DHT11_DRIVER_NAME ": Bad irq number or handler\n");
return -EINVAL;
default:
printk(KERN_INFO DHT11_DRIVER_NAME ": Interrupt %d obtained\n", gpio_to_irq(gpio_pin));
break;
};
return 0;
}
irq_handler長這樣:
// IRQ handler - where the timing takes place
static irqreturn_t irq_handler(int i, void *blah)
{
// 測量中斷發生的時間
do_gettimeofday(&tv);
// 進入前已經先mark好時間,所以訊號持續的時間只要相減就好
int data = (int) ((tv.tv_sec-lasttv.tv_sec)*1000000 + (tv.tv_usec - lasttv.tv_usec));
// 現在的訊號,如果現在是0,就表示剛才是1
int signal = gpio_get_value(gpio_pin);
// mark這次中斷的時間,下次中斷的時候就能用
lasttv = tv; //Save last interrupt time
// use the GPIO signal level
//如果剛才那是1,就把持續時間放在timeBit(資料大小40bit,還要加1bit的開始)裡
if (signal==0) {
timeBit[nBit++] = data;
}
// 傳完了就告訴系統已經完全了
if (nBit >= TOTAL_INT_BLOCK) complete(&gpio_completion);
return IRQ_HANDLED;
}
要注意的是進行中斷處理程序的時候,CPU必須先跳開手上的工作進行中斷,結束後跳回原先的工作。如果這樣不斷跳來跳去,會大幅降低整體系統效能,所以必須極力縮短中斷程序。所以我在中斷處理程序只有紀錄訊號持續時間,解碼等中斷處理程序結束之後再說。
接下來就是解碼了:
static unsigned char dht11_decode_byte(int *timing, int threshold)
{
unsigned char ret = 0;
int i;
for (i = 0; i < 8; ++i) {
ret <<= 1;
if (timing[i] >= threshold) ++ret;
}
return ret;
}
static int dht11_decode(void) {
// 通常如果訊號是50us Off -> ~70us On 表示1
// 50us Off -> 24-26us On 表示0
// 可是如果你嘗試很多次,你會發現DHT11的表現並不總是那樣,會有些bit不長也不短沒法分辨的情況
// 所以我把長度小於短bit(26us)*1.5的定義為0,長度大於長bit(70us)*2/3的定義為1
// 如果遇到非0也非1的case,我會顯示"Poor resolution"然後重試
int i, bitM = DHT11_DATA_BIT_LOW * 3 / 2, bitN = DHT11_DATA_BIT_HIGH*2/3, wtime=0;
for (i=0; ibitM && timeBit[i]0) {
printk("Poor resolution \n");
return 1;
}
// 第1個bit是資料傳送開始所以不算
// 每個資料長8bits,依序為溼度整數位、溼度小數位、溫度整數位、溫度小數位、checksum(前面四個的總和,用以確定資料無誤)
// 不過要注意的是基本上溼度小數位跟溫度小數位都會是0(我也不知道為何,既然沒數據你加了幹嘛?)
dht11.Humidity[0] = dht11_decode_byte(&timeBit[1], bitM);
dht11.Humidity[1] = dht11_decode_byte(&timeBit[9], bitM);
dht11.Temperature[0] = dht11_decode_byte(&timeBit[17], bitM);
dht11.Temperature[1] = dht11_decode_byte(&timeBit[25], bitM);
dht11.checksum = dht11_decode_byte(&timeBit[33], bitM);
if (dht11.Temperature[0]+dht11.Temperature[1]+dht11.Humidity[0]+dht11.Humidity[1] == dht11.checksum) return 0;
else return 2;
}
有了結果就可以讀取了:
static ssize_t read_dht11(struct file *filp, // see include/linux/fs.h
char *buffer, // buffer to fill with data
size_t length, // length of the buffer
loff_t * offset)
{
// 這邊我們使用一個指標來結束read_dht11的動作
// 如果沒有 return 0,那在cat /dev/DHT11的時候,read_dht11會不斷重複。
// (一般使用的時候是該這樣,不過這邊我並不希望dht11被頻繁讀取,因為它不是那麼靈敏)
if (*msg_Ptr == '\0') return 0;
if (copy_to_user(buffer, msg_Ptr, strlen(msg)+1)!=0 ) return -EFAULT;
msg_Ptr += strlen(msg);
return strlen(msg);
}
至於init跟exit跟之前一樣,就不複習了。
編譯後載入模組,然後測試:
pi@raspberrypi ~/work/driver/DHT11 $ sudo insmod ./dht11.ko
pi@raspberrypi ~/work/driver/DHT11 $ sudo cat /dev/DHT11
Humidity: 39.0%
Temperature: 26.0C
Result:OK
pi@raspberrypi ~/work/driver/DHT11 $
原始碼在此:
https://gist.github.com/gnitnaw/744d237d6ea2fd84e756
(PS: 本原始碼參考自
http://www.tortosaforum.com/raspberrypi/dht11km.tar)
參考資料:
http://www.tortosaforum.com/raspberrypi/dht11km.tar
台灣樹莓派 <sosorry@raspberrypi.com.tw>:用 Raspberry Pi 學 Linux 驅動程式