1. 程式人生 > >linux3.5下I2C驅動

linux3.5下I2C驅動

                                             

知識背景:

1. I2C協議

2. 4412處理器I2C介面說明

3. bus-dev-drv模型(詳見文章-Linux下驅動:分層、分離機制學習筆記)

4. linux核心下驅動設計基本知識


一、驅動框架

      以4412+linux3.5平臺為例,說明Linux下I2C裝置驅動程式。

      如果一條I2C總線上只連線一個I2C裝置,那麼只需要以字元型裝置驅動框架來寫驅動即可(填充file_opreoration結構體中的各個成員),那是典型的字元型裝置驅動程式。

      實際上,一款處理器可能有多條I2C匯流排,多個I2C控制器,每條總線上可能掛接多個裝置,按照典型的字元型驅動框架寫驅動就要為每個這樣的裝置寫一個驅動程式,而且要做很多重複性的工作。

      所以,有很多東西可以抽象的就抽象出來,實現一種框架,這種框架模型還是bus-dev-drv模型。4412處理器有8條可用I2C匯流排,每條總線上掛接的每個I2C裝置都明白它們跟4412通訊資料的含義。4412訪問I2C裝置時,都需要發出Start訊號,都需要發出裝置地址等等,這些共性的東西就可以抽象出來用一些函式實現,這些函式就是在另外一層即核心層驅動程式。

      核心層驅動程式提供統一的I2C裝置操作函式,比如讀寫I2C裝置的操作函式;還有裝置驅動層(file_opreoration結構體相關);另外一層就是適配層,這一層是處理器的每個I2C介面卡(也叫控制器)的驅動,適配層驅動程式提供統一的處理器I2C介面硬體操作函式。

       應用程式需要操作I2C裝置時,根據裝置節點呼叫讀寫函式(系統呼叫),然後就會執行裝置驅動層的相應讀寫函式(它們是file_opreoration結構體的成員),這些讀寫函式裡會呼叫核心層提供的統一的I2C裝置讀寫操作函式,這些函式最終會通過介面卡(I2C控制器)給連線在I2C總線上的I2C裝置傳送相應讀寫命令。介面卡那一層驅動程式就是些I2C控制器的硬體操作,它是根據I2C協議來的(這裡根據的I2C協議往往是硬體已經做好了的,軟體只需要配置CPU的I2C控制器的某個暫存器即可實現I2C協議),是最底層,當I2C控制器的某個暫存器被控制後,I2C控制器就會自動地往I2C線上根據I2C協議發出相應訊號與2IC裝置通訊。

       由此可見,寫裝置驅動程式即實現裝置驅動層時,只要弄清楚bus-dev-drv驅動模型,(呼叫了probe函式後)剩下的就是典型的字元型裝置驅動那一套了(構建file_opreoration結構體、分配主次裝置號、建立裝置節點),file_opreoration的讀寫函式呼叫的方法也由核心層提供。圖一。


      上述三層是通過匯流排-裝置-驅動模型融合到一起的。Linux核心中構建了許多匯流排,但並不是都是真實的,比如平臺匯流排就是虛擬的,平臺匯流排中也有裝置連結串列,驅動連結串列。

      針對I2C匯流排,也有一個I2C匯流排的結構即i2c_bus_type結構,此結構裡面也有裝置連結串列和也有驅動連結串列。裝置連結串列裡存放i2c_client的結構體,這些結構體是註冊i2c_client時加入的,不但要加入這些結構體,還會在匯流排的驅動連結串列中一個一個地比較drv即i2c_driver來判斷是否有匹配的,如果有將呼叫drv裡面的probe函式,匹配函式由匯流排提供;驅動連結串列裡存放i2c_driver結構體,這些結構體是註冊i2c_driver時加入的,不但要加入這些結構體,還會在匯流排的裝置連結串列中一個一個地比較dev即比較i2c_client來判斷是否有匹配的,如果匹配將呼叫drv裡面的probe函式。

     上述的匹配函式就是i2c_bus_type結構裡面的i2c_device_match函式。i2c_device_match函式通過id_table(i2c_driver結構的成員,它表示能支援哪些裝置)來比較dev與drv是否匹配,具體方法是用id_table的name去比較,name分別是i2c_client結構和i2c_driver結構的成員。如果名字相同,就表示此驅動i2c_driver能支援這個裝置i2c_client。

      總的說來,I2C基於bus-dev-drv模型的構建過程如下:

(1)左邊註冊一個裝置,i2c_client

(2)右邊註冊一個驅動,i2c_driver

(3)比較它們的名字,如果相同,則呼叫driver的probe函式。

(4)probe函式裡面做的事情由使用者決定(比如註冊、構建裝置節點等,這些屬於裝置驅動)。


二、設定和註冊i2c_client結構體(EEPROM為例)

      在Linux核心文件(/Documentation/i2c/instantiating-devices)中,介紹了註冊即構造一個i2c_client的4種方法,並且建議使用如下前三種方法,後一種較複雜,萬不得已才用。

1.  通過匯流排號宣告一個I2C裝置

      通過匯流排號宣告一個I2C裝置即構造一個i2c_board_info結構體,它裡面有名字和地址,名字可以用來找到drv結構,地址可以用來訪問哪個I2C裝置,還可以放其他資訊,這些資訊會被將來的probe函式用到,也就是說在probe函式中要使用到哪些資訊,這裡面就可以增加哪些資訊。然後使用i2c_register_board_info函式註冊這個結構體,此函式的第一個引數即匯流排號,表示此裝置屬於哪條匯流排(這就是標題通過匯流排號宣告一個i2c裝置的含義),在此函式中會把此結構體放入連結串列__i2c_board_list

      然後在i2c_scan_static_board_info函式中使用到__i2c_board_list連結串列,即呼叫i2c_new_device函式把連結串列中的每個成員構造成一個i2c_client,並放入bus-dev-drv模型中匯流排中裝置連結串列中去。在i2c_new_device函式中,分配一個i2c_client結構體後,設定它,並呼叫device_register函式註冊,此函式最終會呼叫前面文章中提到device_add函式。i2c_scan_static_board_info函式是被i2c_register_adapter函式呼叫的,所以這裡總的過程為i2c_register_adapter  > i2c_scan_static_board_info > i2c_new_device。

     ft5x06_ts型別的I2C觸控式螢幕驅動中就是使用的這種方法。在核心檔案arch/arm/mach-exynos/mach-tiny4412.c中,對應觸控式螢幕的i2c_register_board_info函式呼叫過程如下:

MACHINE_START(TINY4412, "TINY4412")
    .xxx
    .init_machine    = smdk4x12_machine_init,
    .xxx
MACHINE_END
                                 ↓

         呼叫smdk4x12_machine_init函式

                                 ↓

呼叫i2c_register_board_info(1, smdk4x12_i2c_devs1,ARRAY_SIZE(smdk4x12_i2c_devs1));

       可見,上述i2c_register_board_info函式的第一個引數為1,表示ft5x06_ts型別的I2C觸控式螢幕掛接在處理器的I2C1那條總線上。


這種方法使用限制:在註冊I2C介面卡之前註冊i2c_board_info結構體,即必須在 i2c_register_adapter 之前 i2c_register_board_info,所以不適合動態載入insmod。


2.  直接建立裝置(直接呼叫i2c_new_device、i2c_new_probed_device)

(1)i2c_new_device方法

      第一種方法顯得有些麻煩,這裡就直接呼叫 i2c_new_device或i2c_new_probed_device函式實現。

      i2c_new_device函式總共有兩個引數,第一個為要指定的介面卡i2c_adapter(一個用來標識物理I2C匯流排結構,即用哪個I2C控制器發出I2C訊號,某些CPU有多個I2C介面卡),即要把i2c裝置跟哪個介面卡相連,這樣以後在訪問I2C裝置時,就知道使用哪個介面卡的操作函數了。第二個引數是用來描述I2C裝置的相關資訊的,即i2c_board_info結構體。所以這種方法需要在dev程式中定義i2c_adapter和i2c_board_info結構,i2c_board_info結構可以直接在dev程式中定義,i2c_adapter建立的程式碼如下:

    struct   i2c_adapter *i2c_adap;
    i2c_adap = i2c_get_adapter(0);
    at24cxx_client = i2c_new_device(i2c_adap, &at24cxx_info);
    i2c_put_adapter(i2c_adap);

     先定義一個i2c_adapter結構指標存放i2c_get_adapter(0)的返回值,此函式的引數為0,表示第0條匯流排,i2c_new_device呼叫完後要呼叫  i2c_put_adapter函式。通過這樣的方式,在第0條匯流排下建立了一個新裝置,以後就可以使用i2c_adap這個i2c_adapter的操作函式發出I2C的訊號了,比如起始訊號、停止訊號等。實驗程式碼如下:

at24cxx_dev.c :


     
  1. #include <linux/kernel.h>
  2. #include <linux/module.h>
  3. #include <linux/platform_device.h>
  4. #include <linux/i2c.h>
  5. #include <linux/err.h>
  6. #include <linux/regmap.h>
  7. #include <linux/slab.h>
  8. //0x50表示I2C裝置的地址,一般在 I2C裝置晶片手冊可以查到
  9. static struct i2c_board_info at24cxx_info = { 
  10.  I2C_BOARD_INFO( "at24c08", 0x50), //這個名字要和drv程式中的id_table中名字要一樣
  11. };
  12. static struct i2c_client *at24cxx_client;
  13. static int at24cxx_dev_init(void)
  14. {
  15.   struct i2c_adapter *i2c_adap;
  16.  i2c_adap = i2c_get_adapter( 0); //這裡要實驗的EEPROM是掛接在第0條I2C總線上的,所以這裡的引數是0
  17.  at24cxx_client = i2c_new_device(i2c_adap, &at24cxx_info);
  18.  i2c_put_adapter(i2c_adap);
  19.  
  20.   return 0;
  21. }
  22. static void at24cxx_dev_exit(void)
  23. {
  24.  i2c_unregister_device(at24cxx_client);
  25. }
  26. module_init(at24cxx_dev_init);
  27. module_exit(at24cxx_dev_exit);
  28. MODULE_LICENSE( "GPL");
at24cxx_drv.c :

     
  1. #include <linux/kernel.h>
  2. #include <linux/module.h>
  3. #include <linux/platform_device.h>
  4. #include <linux/i2c.h>
  5. #include <linux/err.h>
  6. #include <linux/regmap.h>
  7. #include <linux/slab.h>
  8. static int __ devinit at24cxx_probe(struct i2c_client *client,
  9.       const struct i2c_device_id *id)
  10. {
  11.  printk( "%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
  12.   return 0;
  13. }
  14. static int __ devexit at24cxx_remove(struct i2c_client *client)
  15. {
  16.  printk( "%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
  17.   return 0;
  18. }
  19. static const struct i2c_device_id at24cxx_id_table[] = {
  20.  { "at24c08", 0 }, //用到哪些就宣告哪些內容,比如driver_data用不到,所以這裡就寫0
  21.  {}
  22. };
  23. /* 1. 分配/設定i2c_driver */
  24. static struct i2c_driver at24cxx_driver = {
  25.  .driver = {
  26.   .name = "100ask", //在這裡,這個名字並不重要,重要的是id_table裡面的名字,所以這裡可以隨便起
  27.   .owner = THIS_MODULE,
  28.  },
  29.  .probe  = at24cxx_probe,
  30.  .remove  = __devexit_p(at24cxx_remove),
  31.  .id_table = at24cxx_id_table,
  32. };
  33. static int at24cxx_drv_init(void)
  34. {
  35.   /* 2. 註冊i2c_driver */
  36.  i2c_add_driver(&at24cxx_driver); //實際使用時一定要判斷返回值
  37.  
  38.   return 0;
  39. }
  40. static void at24cxx_drv_exit(void)
  41. {
  42.  i2c_del_driver(&at24cxx_driver);
  43. }
  44. module_init(at24cxx_drv_init);
  45. module_exit(at24cxx_drv_exit);
  46. MODULE_LICENSE( "GPL");<span style= "display: none; width: 0px; height: 0px;" id= "transmark"></span>

Makefile :


     
  1. KERN_DIR = /home/samba/linuxKernel_ext4Fs_src/linux -3.5 -2015 -8
  2. all:
  3.  make -C $(KERN_DIR) M=`pwd` modules
  4. clean:
  5.  make -C $(KERN_DIR) M=`pwd` modules clean
  6.  rm -rf modules.order
  7. obj-m += at24cxx_dev.o
  8. obj-m += at24cxx_drv.o
 
    

      分別載入at24cxx_dev.ko和at24cxx_drv.ko,at24cxx_probe函式會被呼叫,隨意修改i2c_board_info結構裡面的地址,at24cxx_probe函式照樣被呼叫。所以這種方法不會去真正地驗證連線的I2C裝置的地址是否為i2c_board_info結構裡面的地址。

(2) i2c_new_probed_device方法

     i2c_new_device : 認為裝置肯定存在,實驗時可以用這種方法改掉I2C裝置的地址,改成任意值都是可以的。     i2c_new_probed_device :對於"已經識別出來的裝置"(probed_device),才會建立("new")     i2c_new_probed_device函式主要做如下三件事:                probe(adap, addr_list[i])   /* 如果i2c_new_probed_device最後個引數中沒有指定probe函式,將使用預設probe函式。不管哪個,它都要確定裝置是否真實存在 */               info->addr = addr_list[i];   /* 如果在addr_list中找到了一個地址和現實中連線在I2C總線上的裝置匹配,將這個地址放入i2c_board_info結構,並傳給i2c_new_device */                i2c_new_device(adap, info);     為了實驗,分別編譯如下程式碼,然後分別載入at24cxx_dev.ko和at24cxx_drv.ko,如果在addr_list陣列中有一個地址是I2C總線上裝置的地址,那麼在載入at24cxx_dev.ko驅動模組時,能載入成功,並且載入at24cxx_drv.ko模組後,將呼叫drv的probe函式。如果沒有那個地址,那麼在載入at24cxx_dev.ko驅動模組時會失敗,提示如下資訊:

insmod: can't insert 'at24cxx_dev.ko': No such device

     這就是直接使用i2c_new_device和使用i2c_new_probed_device建立i2c_client的區別。

at24cxx_dev.c :


     
  1. #include <linux/kernel.h>
  2. #include <linux/module.h>
  3. #include <linux/platform_device.h>
  4. #include <linux/i2c.h>
  5. #include <linux/err.h>
  6. #include <linux/regmap.h>
  7. #include <linux/slab.h>
  8. static struct i2c_client *at24cxx_client;
  9. //如果掛接在I2C總線上的i2c裝置的地址在此數組裡面都找不到,將不能載入此驅動模組
  10. static const unsigned short addr_list[] = { 0x60, 0x50,I2C_CLIENT_END };
  11. static int at24cxx_dev_init(void)
  12. {
  13.   struct i2c_adapter *i2c_adap;
  14.   struct i2c_board_info at24cxx_info;
  15.   memset(&at24cxx_info, 0, sizeof(struct i2c_board_info)); 
  16.  strlcpy(at24cxx_info.type, "at24c08", I2C_NAME_SIZE);
  17.  i2c_adap = i2c_get_adapter( 0);
  18.  at24cxx_client = i2c_new_probed_device(i2c_adap, &at24cxx_info, addr_list, NULL);
  19.  i2c_put_adapter(i2c_adap);
  20.   if (at24cxx_client)
  21.    return 0;
  22.   else
  23.    return -ENODEV;
  24. }
  25. static void at24cxx_dev_exit(void)
  26. {
  27.  i2c_unregister_device(at24cxx_client);
  28. }
  29. module_init(at24cxx_dev_init);
  30. module_exit(at24cxx_dev_exit);
  31. MODULE_LICENSE( "GPL");

at24cxx_drv.c :


     
  1. #include <linux/kernel.h>
  2. #include <linux/module.h>
  3. #include <linux/platform_device.h>
  4. #include <linux/i2c.h>
  5. #include <linux/err.h>
  6. #include <linux/regmap.h>
  7. #include <linux/slab.h>
  8. static int __ devinit at24cxx_probe(struct i2c_client *client,
  9.                        const struct i2c_device_id *id)
  10. {
  11.  printk( "%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
  12.   return 0;
  13. }
  14. static int __ devexit at24cxx_remove(struct i2c_client *client)
  15. {
  16.  printk( "%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
  17.   return 0;
  18. }
  19. static const struct i2c_device_id at24cxx_id_table[] = {
  20.  { "at24c08", 0 }, //用到哪些就宣告哪些內容,比如driver_data用不到,所以這裡就寫0
  21.  {}
  22. };