2016年1月14日 星期四

DHT11 code 攻略


先來一張架構圖



當執行  sudo insmod dht11km.ko 時.... 就會啟動 module driver init function
當執行  sduo rmmod dht11km.ko 時.... 就會啟動  module driver exit function

step 1:    dht11_init_module


         他一開始會先確定你傳進去的gpio_pin 的number 有沒有合法

          // Possible valid GPIO pins
int valid_gpio_pins[] = { 0, 1, 4, 8, 7, 9, 10, 11, 14, 15, 17, 18, 21, 22, 23, 24, 25 };

         他宣告了一個valid_gpio_pins陣列.... 然後去檢查有沒有符合的...

          如果一個都沒有符合...就會

          printk(KERN_ERR DHT11_DRIVER_NAME ": invalid GPIO pin specified!\n");
          goto exit_rpi;
       
         接下來就是將driver 註冊到 kernel 的 VFS層

         result = register_chrdev(driverno, DHT11_DRIVER_NAME, &fops);

         從參考1的網站可以看出有四種註冊的類型...

        在本例子中用的是char type driver
    
         int register_chrdev(unsigned int major, const char * name, struct file_operations *fops):註冊字元型驅動程式。
˙ int register_blkdev(unsigned int major, const char *name, struct file_operations *fops):註冊區塊型驅動程式。
˙ int usb_register(struct usb_driver *new_driver):註冊USB驅動程式。
˙ int pci_register_driver(struct pci_driver *):註冊PCI驅動程式。
      
       
      接著就是呼叫 init_port();

        request_mem_region(GPIO_BASE, SZ_4K, DHT11_DRIVER_NAME) 
        gpio = ioremap_nocache(GPIO_BASE, SZ_4K)

         這是原本的內容 .... 他去呼叫 request_mem_region 去註冊一塊記憶體

         然後用ioremap_nocache() 去map ....最後得到gpio 這個virtual address

         不過由於我在Raspiban 上面實作....發現這個空間已經被註冊了

          所以只好改source code

       把這一行  request_mem_region(GPIO_BASE, SZ_4K, DHT11_DRIVER_NAME)  Mark
  


step 2:   dht11_exit_module

        source code 如下:
    
       if(gpio != NULL) {
iounmap(gpio);
release_mem_region(GPIO_BASE, SZ_4K);
printk(DHT11_DRIVER_NAME ": cleaned up resources\n");
}
            
          如果gpio的數值不為零....  就把  gpio 所使用的這塊記憶體 unmap掉

          然後把記憶體釋放掉

          接下來就是把註冊的driver 取消掉

         unregister_chrdev(driverno, DHT11_DRIVER_NAME);

     

step 3 : protocol 詳解





  從上面2圖來看....   從算 bit 0 和  bit 1 都是需要兩個動作...

   第一個動作是把voltage 拉 low 50us ...接下來再把 voltage 拉 high...

   如果維持時間有  26-28 us ...就是代表value 0

   如果維持時間有  70 us ...就是代表value 1



    在DHT11 的整個傳送封包來看....總共有40個bit 

       8 bit 濕度整數  + 8 bit 濕度小數
       8 bit 溫度整數   + 8 bit 溫度小事
       8 bit CRC...

     接下來看  raspberry 和 DHT11 的溝通protocol

      

      Raspberry 這邊簡稱  Host...    DHT11 這邊簡稱 Device

     一開始 Host 先拉low ...維持 18ms ....  然後拉High 維持20-40 us.....

    然後把 Raspberry 把gpio 設成 input mode...把控制權交給Device

     接著device 先把電位拉low 維持80us ...再把 電位拉 high 維持 80us  ..

   這個動作是在告訴host ...我已經接收到命令....接下來就是 傳送40 各 bit....

step 4 :  GPIO  function

先來一張 soc chip 在gpio 有關的register table


     

   以 GPFSEL0  來看   (offset 0x0)....  控制 gpio 0 到 9
   
   他可以控制10 根GPIO....    bit[2:0] 控制 gpio0   ..  bit[5:3] 控制 gpio1 ...以此類推

    雖然有三個bit ...有8種可能...但這邊我們只會用到  input mode 或是 output mode

    put '0'   ->  input mode
    
    put '1'   ->  output mode

    決定了 GPIO  的 input mode 或是 ouput mode 之後 ..... 以output mode為例子

   

  從上圖可以得知....    

   GPSET0 (offset 0x1C)....   GPSET1(offset 0x20)

  GPSET0 決定了 gpio[31:0] 的控制.....

  假設你要讓 gpio 10 的值為 '1' ....

   你就需要  GPSET0[10] 設成 '1'....   寫 value 0 是沒有效果的...

   


GPCLR0 (offset 0x28)....   GPCLR1(offset 0x2C)

假設你要讓 gpio 10 的值為 '0' ....

   你就需要  GPCLR0[10] 設成 '1'....   寫 value 0 是沒有效果的...

  接下來看當設成  input mode 時...

  如果你要得到GPIO[n] 的數值.....  請 read  GPLEV0 (0x38) 和 GPLEV1 (0x3C)

    
  假如你要得到GPIO[10] 的數值....就是 GPLEV0[10] 的value

  
  


   接下來看設定 rising edge    GPREN(0x40)和 falling edge   GPFEN(0x4C)

   假設你要設定 GPIO 4 為  rising edge event trigger 

    GPREN[4] 為 '1'....   如果要disable他.... GPREN[4] 為 '0'

   假設你要設定 GPIO 4 為  falling edge event trigger 

   GPFEN[4] 為 '1'....   如果要disable他.... GPREN[4] 為 '0'





  接下來要把來看   GPIO Event status Register   ...GPEDS0(0x40)

  他會被硬體設成'1'如果符合下面兩種狀況...

  第一種  :  gpio edge 的行為被偵測到了並且符合你設定rising 或是 falling edge 的行為

  第二種  :  gpio level 的行為被偵測到了並且符合你設定 level的狀況....
   
  假設你要清除GPIO 4 所引起的中斷結果.....    GPESD0[4] = 1;  就這麼簡單




看完上述的data sheet之後...再回來看source code...你就會知道為什麼code要這樣寫了...

// set GPIO pin g as input
#define GPIO_DIR_INPUT(g) *(gpio+((g)/10)) &= ~(7<<(((g)%10)*3))
// set GPIO pin g as output
#define GPIO_DIR_OUTPUT(g) *(gpio+((g)/10)) |=  (1<<(((g)%10)*3))
// get logical value from gpio pin g
#define GPIO_READ_PIN(g) (*(gpio+13) & (1<<(g))) && 1
// sets   bits which are 1 ignores bits which are 0
#define GPIO_SET_PIN(g) *(gpio+7) = 1<<g;
// clears bits which are 1 ignores bits which are 0
#define GPIO_CLEAR_PIN(g) *(gpio+10) = 1<<g;
// Clear GPIO interrupt on the pin we use
#define GPIO_INT_CLEAR(g) *(gpio+16) = (*(gpio+16) | (1<<g));
// GPREN0 GPIO Pin Rising Edge Detect Enable/Disable
#define GPIO_INT_RISING(g,v) *(gpio+19) = v ? (*(gpio+19) | (1<<g)) : (*(gpio+19) ^ (1<<g))
// GPFEN0 GPIO Pin Falling Edge Detect Enable/Disable

#define GPIO_INT_FALLING(g,v) *(gpio+22) = v ? (*(gpio+22) | (1<<g)) : (*(gpio+22) ^ (1<<g))

=====================================================================
 GPIO_DIR_INPUT 是把指定的GPIO n 所對應到的 bit 清成 0

舉例來說.... 假設  我們用 gpio 4....      g  為  4   

 (g%10) *3 = 12 ...  bit[14:12] 就是我們要設定的位置....

7 <<12  =  0x00 00 07 00  (16進位)    

~(7<<12) = 0xff ff  f8 ff    ..剛好就是  bit[14:12] 為0....其他都為1

然後這邊清成0的方式是用  and 的方式....  就可以把 bit[14:12] 清成 0

GPIO_DIR_OUTPUT(g) 也是類似的原理....這邊就不多說 ....

GPIO_SET_PIN(g)    ->  *(gpio+7) = 1<<g;

首先   offset = 7*4 = 28 -> 0x1C    ....然後針對你要gpio 的number 去設定

GPIO_CLEAR_PIN(g) -> *(gpio+10) = 1<<g;

首先   offset = 10*4 = 40 -> 0x28    ....然後針對你要gpio 的number 去設定

GPIO_READ_PIN(g)   -> (*(gpio+13) & (1<<(g))) && 1

首先   offset = 13*4 = 52  -> 0x34    ....把數值讀回來後...  用  mask 1<<g 去做and

把只有那個bit 的數值留下來...其他都變成0....然後再和 1做 && 的運算

如果不為零....就是1....如果為零...就是0.....

 GPIO_INT_CLEAR(g) *(gpio+16) = (*(gpio+16) | (1<<g));

就是打開  Event status register ....      把你要的相對應的bit 設成1

 GPIO_INT_RISING(g,v) *(gpio+19) = v ? (*(gpio+19) | (1<<g)) : (*(gpio+19) ^ (1<<g))

  這個macro 是說....  如果v 為 1...也就是要enable rising edge ....就是把相對應的bit 設成 '1'
                                    如果v 為 0...也就是要disable rising edge ....就是把相對應的bit 設成 '0'

 GPIO_INT_FALLING(g,v) *(gpio+22) = v ? (*(gpio+22) | (1<<g)) : (*(gpio+22) ^ (1<<g))

  這個macro 是說....  如果v 為 1...也就是要enable falling edge ....就是把相對應的bit 設成 '1'
                                    如果v 為 0...也就是要disable falling edge ....就是把相對應的bit 設成 '0'

step 5 :  read function 實作


    先看  source code :

    GPIO_DIR_OUTPUT(gpio_pin); // Set pin to output
    GPIO_CLEAR_PIN(gpio_pin); // Set low
    mdelay(20); // DHT11 needs min 18mS to signal a startup
    GPIO_SET_PIN(gpio_pin); // Take pin high
    udelay(40); // Stay high for a bit before swapping to read mode
    GPIO_DIR_INPUT(gpio_pin); // Change to read

     和上面的步驟一樣.....一開始先設成   output mode

      然後設成low ...... 等待 18ms ... mdealy(20)

     然後拉high...... 等待 40 us....   udelay(40)

    然後設成 input mode...把控制權交給  device
   
   接下來的動作為

    do_gettimeofday(&lasttv);
    setup_interrupts();
    mdelay(10);

   if((dht[0] + dht[1] + dht[2] + dht[3] == dht[4]) & (dht[4] > 0))
sprintf(result, "OK");
   else 
           ....
    do_gettimeofday  為 linux 內建的function ...可以取得目前的時間資訊

    setup_interrupts() 為自己設定的sub-function...接下來會詳細說明

    mdelay(10)    這個是在等待device 傳資訊..... 由於剛剛已經設定了  interrupt  ...

     當gpio 產生變化時...就會啟動ISR 去處理....這邊就只是不處理了

    接下來就是check CRC .... 因為總共會傳 40 bit 也就是  5 byte...

   分別放到  dht[0]..  dht[1]..  dht[2]...  dht[3]... dht[4]...  然後dht[4]也就是CRC值

   看起來應該是全部加起來會等於CRC..... 所以就去check值有沒有對...
    
   接下來解釋    setup_interrupts()

   result = request_irq(INTERRUPT_GPIO0, (irq_handler_t) irq_handler, 0, DHT11_DRIVER_NAME, (void*) gpio);
  // GPREN0 GPIO Pin Rising Edge Detect Enable 
GPIO_INT_RISING(gpio_pin, 1);
// GPFEN0 GPIO Pin Falling Edge Detect Enable 
GPIO_INT_FALLING(gpio_pin, 1);
// clear interrupt flag 
GPIO_INT_CLEAR(gpio_pin);
spin_unlock_irqrestore(&lock, flags);


 第一步是先註冊  irq....   
    
   request_irq(INTERRUPT_GPIO0, (irq_handler_t) irq_handler, 0, DHT11_DRIVER_NAME, (void*) gpio);
  
request_irq 為linux 內建的function....

當irq來的時候.... 會啟動   irq_handler() 去執行....

 接下來是設定  gpio_pin 的觸發行為為 Rising edge 和 falling edge....

然後清除 之前的中斷狀態....

   
  irq_handler的內容如下:

// use the GPIO signal level 
signal = GPIO_READ_PIN(gpio_pin);

/* reset interrupt */
GPIO_INT_CLEAR(gpio_pin);

 do_gettimeofday(&tv);

// get time since last interrupt in microseconds 
deltv = tv.tv_sec-lasttv.tv_sec;
data = (int) (deltv*1000000 + (tv.tv_usec - lasttv.tv_usec));
lasttv = tv; //Save last interrupt time


  首先先去讀目前gpio的value......   把value存放在 signal...

   然後清除目前的中斷狀態......

  接下來讀一下目前的時間....並且計算出和上次產生中斷的時間的差值....

   並且把這個數值存放到 data 裡面....單位  us.....

   接下來就是判斷....

  if((signal == 1)&(data > 40))
{
started = 1;
return IRQ_HANDLED;
}

   如果signal 為'1' ...代表這個中斷是rising edge.... 並且check拉low 的時間有大於 40 us

    就直接返回...等待下一次的中段...

if((signal == 0)&(started==1))
{
if(data > 80)
return IRQ_HANDLED; //Start/spurious? signal
if(data < 15)
return IRQ_HANDLED; //Spurious signal?
if (data > 60)//55 
dht[bytecount] = dht[bytecount] | (0x80 >> bitcount); //Add a 1 to the data byte
//Uncomment to log bits and durations - may affect performance and not be accurate!
//printk("B:%d, d:%d, dt:%d\n", bytecount, bitcount, data);
bitcount++;
if(bitcount == 8)
{
bitcount = 0;
bytecount++;
}
}

  如果signal 為 0...代表目前是 falling edge...

     if 時間大於80 us....可能是 開始或是結束的header...不是我們要的資料...就返回

    else if 時間小於15 us.  可能是Spurious signal   不是我們要的資料...就返回

    else if時間大於 60 us ....代表傳送的bit為 '1'..

               .dht[bytecount] = dht[bytecount] | (0x80 >> bitcount);

    else   代表傳送的bit 為 '0'

       bit_count ++....確認一下byte_count.....



Reference 1: http://www.jollen.org/blog/2006/05/linux_5_virtual_device_driver.html
Reference 2 : http://www.slideshare.net/raspberrypi-tw/write-adevicedriveronraspberrypihowto
Reference 3 : http://silverfoxkkk.pixnet.net/blog/post/44691351-lddp%3A%E4%B9%9D%E3%80%81%E8%A8%88%E6%99%82%E5%99%A8

DHT11 實作 on Raspberry

我的硬體是 RPI2

OS是用  2015-11-21 Raspiban -jessie

linux kernel 是   4.1.15-v7+

按照範例.....把DHT11 掛上去

sudo insmod dht11km.ko gpio_pin=4 format=3

結果出現的message如下



check 了一下 dht11.c 檔...


應該是 request_mem_region 這邊要不到記憶體....

google 了一下

發覺很可能是GPIO_BASE 所使用的記憶體空間已經被使用...

使用指令查詢一下目前系統已經使用的記憶體空間

cat /proc/iomem




發覺  0x3f20_0000 已經有人用了

那為什麼GPIO_BASE 是從  0x3f20_0000開始呢?

參考這篇文章 http://sourceforge.net/p/raspberry-gpio-python/code/ci/default/tree/source/c_gpio.c#l132

#define BCM2709_PERI_BASE_DEFAULT 0x3f000000
#define GPIO_BASE_OFFSET 0x200000

  把兩個加起來就是   0x3f20_0000  ....(其實你也可以在driver code 裡面直接印出來看)

這邊解法就是反正那塊記憶體已經被人註冊使用.....就不需要自己註冊

就直接去使用就好...反正我們已經有實體的位置....

所以把原本的程式碼Mark掉

if (request_mem_region(GPIO_BASE, SZ_4K, DHT11_DRIVER_NAME) == NULL) {
                printk(KERN_ERR DHT11_DRIVER_NAME ": unable to obtain GPIO I/O memory address\n");
                return -EBUSY;
        }

   


這樣就可以啦.....

重開機....編譯.....載入

make

sudo insmod ./dht11km.ko gpio_pin=4 format=3


然後再掛上去節點

 sudo mknod /dev/dht11 c 80 0

然後查詢他

cat /dev/dht11

得到的message 如下:




這一看就知道不對...... 一定是那裏有問題

打印一下message ...輸入 

dmesg



大概也是這個message .....

google   了一下

http://silverfoxkkk.pixnet.net/blog/post/45000257-lddp%3A%E5%8D%81%E4%B8%80%E3%80%81%E4%B8%AD%E6%96%B7

從 /proc/interrupts 可以看到 IRQ 的狀態,最左邊的數值即是 IRQ 編號: 
cat /proc/interrupts 

輸入

cat /proc/interrupts

 得到下面的訊息

  看起來irq 49 已經被註冊了.....   3f200000.gpio:bank0


  查一下已經安裝的  package  list, 輸入

 dpkg --get-selections | grep -v deinstall

 目前看起來好像有沒甚麼特別的幫助......

 後來查了一下書本 (LINUX 驅動程式 第10章)

 他說在  request_irq(irq,  irq_handler,  flags, *dev_name, dev_id);

  最常見的錯誤大概就是 -EBUSY.... 就是你要求的IRQ已經被其他驅動程式佔用了....

  其中  構成flags 的各個位元,分述如下

  SA_INTERRUPT :  表示此ISR是"快速型"..也就是能夠在中段失效期間執行完畢所有處理工

                                     作的ISR
  SA_SHIRQ   : 此位元表示可以被不同裝置共享


 SA_SAMPLE_RANDOM : ....  

把原本的程式改成

 result = request_irq(INTERRUPT_GPIO0, (irq_handler_t) irq_handler, IRQF_SHARED, DHT11_DRIVER_NAME, (void*)gpio);

但為什麼是 IROF_SHARED...  因為用SA_SHIRQ 會compile error....查了一下google 要改用 IROF_SHARED...



可以看到message 為 Interrupt 0031 obtained.....

這裡的數字是16進制...其實就是49 .....

不過從message 來看...還是有很奇怪的地方就是....他一直在free irq 49....

然後一直沒有產生中斷.........

後來才發現是我自己加了一段  free_irq的code.....

然後沒有收到中斷的原因是dht11 沒有反應....也就是沒有  rising 或是 falling edge 的變化

所以不會產生中斷....接下來就是需要示波器的出馬... 可惜我還沒有去買.....

==============================
2015/01/20

我一直寫信給賣我DHT11 module 的廠商...問他問題...結果他傳了一個網址給我

https://hackpad.com/KY-015-DHT11-29-fyNqEnzoZ5q

我一看....暈倒....完了....腳位不一樣....我往網路上查的是

(VDD , DATA, GND)

結果它們重新relayout成 (DATA,VDD,GND)

難怪不會動....氣死我了......

他們還提供一個在user space使用的範例

https://github.com/adafruit/Adafruit-Raspberry-Pi-Python-Code/blob/master/Adafruit_DHT_Driver/Adafruit_DHT.c

這個我之前也有試過....但是之前弄得時候不會動...才一直研究下去....

原來就是腳位弄錯......

把對的腳位弄上去....再跑一次....


終於有答案出來了...好高興....

但溫度好像怪怪的.......

再多讀幾次就ok.....


感覺像是bit有被offset到.....可能要等到示波器來會比較知道what happen...



Reference : https://www.raspberrypi.org/forums/viewtopic.php?t=110737&p=799913
Reference : Low Level Programming of the Raspberry Pi in C

在Raspberry pi上面寫個Hello world 的linux driver吧


強烈建議自己重新編譯一次 linux kernel...

詳細步驟請參考   在Raspberr pi2上面編譯 Kernel

做完上面的步驟後.....

接下來寫一個 hello world的程式

hello.c :

#include<linux/module.h>
#include<linux/kernel.h>
int hello_init(void)
{
        printk("hello world!\n");
        return(0);
}

void hello_exit(void)
{
        printk("goodbye world!\n");
}

MODULE_LICENSE("GPL");
module_init(hello_init);
module_exit(hello_exit);

這個程式註冊了 init 和 exit function

其中init function 裡面只有 print "Hello world"

.....

然後Makefile 的內容如下

obj-m += hello.o

all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean


注意....  Makefile  是用 tab 而不是空白鍵.....

 然後輸入

make

你就會看到如下的檔案



輸入

sudo insmod ./hello.ko

dmesg

就會看到 "Hello world!"