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

沒有留言:

張貼留言