linux下i2c驅動

linux下i2c驅動

1. 幾個基本概念

1.1. 設備模型

由 總線(bus_type) 設備(device) 驅動(device_driver) 組成,在該模型下,所有的設備通過總線連接起來,即使有些設備沒有連接到一根物理總線上,linux爲其設置了一個內部的、虛擬的platform總線,用以維持總線、驅動、設備的關係。

因此,對於實現一個linux下的設備驅動,可以劃分爲兩大步:

1、設備註冊;

2、驅動註冊。

 

當然,其中還有一些細節問題:

1、驅動的probe函數

2、驅動和設備是怎麼進行綁定的。

1.2. i2c設備驅動的幾個數據結構

 

i2c_adapter

每一個i2c_adapter對應一個物理上的i2c控制器,在i2c總線驅動probe函數中動態創建。通過i2c_add_adapter註冊到i2c_core

 

i2c_algorithm

i2c_algorithm中的關鍵函數master_xfer(),以i2c_msg爲單位產生i2c訪問需要的信號。不同的平臺所對應的master_xfer()是不同的,需要根據所用平臺的硬件特性實現自己的xxx_xfer()方法以填充i2c_algorithmmaster_xfer指針;在A31上即是sun6i_i2c_algorithm函數。

 

i2c_client

代表一個掛載到i2c總線上的i2c從設備,包含該設備所需要的數據:

i2c從設備所依附的i2c控制器 struct i2c_adapter *adapter

i2c從設備的驅動程序struct i2c_driver *driver

i2c從設備的訪問地址addr, name

i2c從設備的名稱name

2. i2c總線驅動

2.1. 功能劃分

從硬件功能上可劃分爲:i2c控制器和i2c外設(從設備)。每個i2c控制器總線上都可以掛載多個i2c外設。Linux中對i2c控制器和外設分開管理:通過 i2c-sun6i.c 文件完成了i2c控制器的設備註冊和驅動註冊;通過i2c-core.c爲具體的i2c外設提供了統一的設備註冊接口和驅動註冊接口,它分離了設備驅動device driver和硬件控制的實現細節(如操作i2c的寄存器)。

2.2. i2c-sun6i.c

該文件是與具體硬件平臺相關的,對應於A3x系列芯片。該文件實際上是i2c總線驅動的實現,本質上就是向內核註冊i2c總線設備、註冊總線驅動、實現總線傳輸的時序控制算法。i2c控制器被註冊爲Platform設備,如下:

 if (twi_used_mask & TWI0_USED_MASK) platform_device_register(&sun6i_twi0_device); if (twi_used_mask & TWI1_USED_MASK) platform_device_register(&sun6i_twi1_device); if (twi_used_mask & TWI2_USED_MASK) platform_device_register(&sun6i_twi2_device); if (twi_used_mask & TWI3_USED_MASK) platform_device_register(&sun6i_twi3_device); if (twi_used_mask) return platform_driver_register(&sun6i_i2c_driver);

 

需要注意的是:設備與驅動的對應關係是多對一的;即如果設備類型是一樣的,會共用同一套驅動,因此上面代碼只是註冊了一次驅動platform_driver_register(&sun6i_i2c_driver) 

 

設備註冊:

i2c控制器設備註冊爲platform設備,爲每一個控制器定義一個struct platform_device數據結構,並且把.name都設置爲"sun6i-i2c"(後面會通過名字進行匹配驅動的),然後是調用platform_device_register()將設備註冊到platform bus上。

 

設備註冊完成後其直觀的表現就是在文件系統下出現:/sys/bus/platform/devices/sun6i-i2c.0

 

通過platform_device_register()進行的註冊過程,說到底就是對struct platform_device這個數據結構的更改,逐步完成.dev.parent.dev.kobj.dev.bus的賦值,然後將.dev.kobj加入到platform_bus->kobj的鏈表上。

 

驅動註冊:

步驟和設備註冊的步驟類似,也是爲驅動定義了一個數據結構:

struct platform_driver sun6i_i2c_driver;

 

因爲一個驅動是可以對應多個設備的,而在系統裏的3個控制器基本上是一致的(區別就是寄存器的地址不一樣),所以上面註冊的3個設備共享的是同一套驅動。

 

設備與驅動匹配

1.match過程

i2c_add_driver-->i2c_register_driver-->i2c_bus_type-->.match->i2c_device_match-->of_driver_match_device/i2c_match_id(比較i2c_driver->id_table->name和client->name,如果相同,則匹配上,匹配上之後,運行driver_register調用driver_probe_device進行設備與驅動綁定。),

 

2.probe過程

初始化.probe.remove函數,然後調用i2c_add_driver進行驅動註冊。主要函數調用流程:

i2c_add_driver-->i2c_register_driver --> driver_register --> bus_add_driver --> driver_attach-->driver_probe_device-->really_probe(裏面講設備的驅動指針指向驅動,如果匹配成功,執行dev->bus->probe即設備驅動裏的probe),-->driver_bound(綁定)

 

需要注意的是driver_attach,這個函數遍歷了總線上(platform_bus_type)的所有設備,尋找與驅動匹配的設備,並把滿足條件的設備結構體上的驅動指針指向驅動,從而完成了驅動和設備的匹配(__driver_attach函數完成)。

 

如果匹配到設備,這時就需要執行platform_bus_typeprobe函數,最終會調用設備驅動的probe函數(sun6i_i2c_probe)。

2.2.1  sun6i_i2c_probe

sun6i_i2c_probe函數中完成了大量的工作,包括硬件初始化、中斷註冊、爲每個i2c控制器創建i2c_adapter等。

 1268 pdata = pdev->dev.platform_data;1269 if (pdata == NULL) {1270 return -ENODEV;1271 }12721273 res = platform_get_resource(pdev, IORESOURCE_MEM, 0);1274 irq = platform_get_irq(pdev, 0);1275 if (res == NULL || irq < 0) {1276 return -ENODEV;1277 }12781279 if (!request_mem_region(res->start, resource_size(res), res->name)) {1280 return -ENOMEM;1281 }

 

  • 首先得到當前設備的私有數據指針,並將其保留在pdata;進而通過platform_get_resource得到該設備佔用的內存資源,並申請:request_mem_region。同時將irq資源也保留下來。 
 12881289 strlcpy(i2c->adap.name, "sun6i-i2c", sizeof(i2c->adap.name));1290 i2c->adap.owner = THIS_MODULE;1291 i2c->adap.nr = pdata->bus_num;1292 i2c->adap.retries = 3;1293 i2c->adap.timeout = 5*HZ;1294 i2c->adap.class = I2C_CLASS_HWMON | I2C_CLASS_SPD;1295 i2c->bus_freq = pdata->frequency;1296 i2c->irq = irq;1297 i2c->bus_num = pdata->bus_num;1298 i2c->status = I2C_XFER_IDLE;1299 i2c->suspended = 0;1300 spin_lock_init(&i2c->lock);1301 init_waitqueue_head(&i2c->wait);
  • 初始化i2c_adapter,並初始化一個工作隊列 init_waitqueue_head 
  • 通過ioremap申請IO資源;
  • 通過request_irq申請irq資源,中斷的處理服務函數是:sun6i_i2c_handler
  • sun6i_i2c_hw_init,對i2c控制進行硬件初始化;
  • i2c->adap.algo = &sun6i_i2c_algorithm,初始化控制器的總線傳輸算法,設備驅動調用;
  • 將初始化好的i2c_adapter註冊到i2c_corei2c_add_numbered_adapter

至此,probe函數完成。

2.2.2  sun6i_i2c_core_process

i2c控制器的中斷服務程序sun6i_i2c_handler調用了sun6i_i2c_core_processi2c總線的實際傳輸控制也是在該函數裏完成的。

主要流程:

  1. 讀取i2c控制器當前狀態,twi_query_irq_status,保留在state中;
  2. 根據state的值進行分支跳轉,控制i2c的工作狀態;
  3. 傳輸完成,調用sun6i_i2c_xfer_complete,喚醒工作隊列。

 

2.2.3  sun6i_i2c_xfer

每一個i2c控制器設備,在驅動綁定後,都會創建一個i2c_adapter,用以描述該控制器,i2c_adapter的建立與初始化是在驅動probe的時候建立的。每一個i2c_adapter包含了一個i2c_algorithm結構體的指針,i2c_algorithm是用來對外提供操作i2c控制器的函數接口的,主要是master_xfer函數,對應於i2c-sun6i.c,實際就是:

 

 static int sun6i_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)

該函數的功能是通知i2c_adapter需要對外設進行數據交換,需要交換的信息通過struct i2c_msg *msgs傳入。sun6i_i2c_xfer實際上是調用了sun6i_i2c_do_xfer進行傳輸。

因爲i2c總線讀寫速率有限,sun6i_i2c_do_xfer啓動i2c傳輸後,通過wait_event_timeout進入休眠,直到中斷喚醒或者超時;中斷喚醒是由sun6i_i2c_xfer_complete完成的。

3. i2c設備驅動

3.1. 驅動註冊

i2c從設備的驅動註冊,使用的是i2c-core.c提供的接口:i2c_register_driver;其調用如下:

 

i2c_register_driver --> driver_register --> bus_add_driver

 

bus_add_driver進行分析:

  • 關於device_driver數據結構的 struct driver_private *p

設備驅動模型是通過kobject對設備驅動進行層次管理的,因此device_driver應該包含kobject成員,linux是將kobject包含在struct driver_private中,再在device_driver中包含struct driver_private;我們可以理解driver_privatedevice_driver的私有數據,由內核進行操作。

struct driver_private 是在驅動註冊的開始,動態申請,並初始化的。

  • klist_init(&priv->klist_devices, NULL, NULL);

初始化設備鏈表,每一個與該驅動匹配的device都會添加到該鏈表下。

  • priv->kobj.kset = bus->p->drivers_kset;

指定該驅動所屬的kset

  • kobject_init_and_add

初始化kobject,並將kobject添加到其對應的kset集合中(即bus->p->drivers_kset)。

該函數最終是調用kobject_add_internalkobject添加到對應的kset中;需要主要的是,如果kobjectparent如果爲NULL,在此會將其parent設置爲所屬kset集合的kobject

parent = kobject_get(&kobj->kset->kobj)

 

接下來是爲kobject創建文件夾:create_dir(kobj);從而能從/sys/目錄下顯示。

  • driver_attach,將驅動和設備進行綁定

將遍歷總線上的設備鏈表,查找可以匹配的設備,並綁定。

driver_attach -->  bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);

將函數指針__driver_attach傳入bus_for_each_dev,將每個查找得到的device進行驅動匹配。

 

bus_for_each_dev

遍歷總線上的所有設備,因爲總線上的設備都是bus->p->klist_devices鏈表上的一個節點,因此該函數其實就是對鏈表的遍歷,具體可以參考klist

 

__driver_attach(源碼位置drivers/base/dd.c):

進行設備和驅動匹配,如果匹配成功,嘗試進行綁定。

 

1. 首先進行匹配確認:driver_match_device(drv,  dev)

調用關係: --> drv->bus->match --> i2c_device_match 

-->  of_driver_match_device

  i2c_match_id

 

可以看出,最終有兩種方式進行驅動匹配查詢:

方法一:通過of_driver_match_device對比of_device_id

方法二:通過i2c_match_id對比id_table

方法二實際上就是對比

i2c_driver->id_table->name client->name是否一致。

 

2. 如果匹配確認,進行驅動與設備綁定:driver_probe_device

調用關係: driver_probe_device --> really_probe

 --> dev->bus->probe

    driver_bound

 

really_probe中,首先將設備的驅動指針指向該驅動:dev->driver = drv

對應於i2c_bus_typedev->bus->probe 即是:i2c_device_probe,最終調用驅動的probe函數。

 

最後是driver_bound,將驅動與設備進行綁定:

其實就是調用klist_add_tail:將設備節點添加到驅動的klist_devices;

 

  • 調用klist_add_tail,將被註冊的驅動添加到總線的klist_drivers上;

klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);

 

  •  module_add_driver(drv->owner,  drv)

sysfs創建drivers目錄

3.2. 設備註冊

方式一:i2c設備動態發現註冊

i2c_register_driver的最後:

 INIT_LIST_HEAD(&driver->clients); i2c_for_each_dev(driver, __process_new_driver);
 

 

觀察i2c_for_each_dev 

 int i2c_for_each_dev(void *data, int (*fn)(struct device *, void *)){ int res; mutex_lock(&core_lock); res = bus_for_each_dev(&i2c_bus_type, NULL, data, fn); mutex_unlock(&core_lock); return res;} 

 

其實就是遍歷i2c總線上的klist_devices鏈表,對得到的每一個device,執行__process_new_driver 

跟蹤 __process_new_driver --> i2c_do_add_adapter --> i2c_detect

i2c_detect實現了i2c設備發現:在註冊驅動後,通過i2c_detect檢測是否有適合的設備連接在總線上。i2c_detect實現如下:

  • 在每一個adapter上遍歷驅動給出的地址列表(address_list),由i2c_detect_address函數完成;最終會調用driver->detect(即設備驅動提供的設備發現函數);
  • 如果發現滿足條件的設備,執行i2c_new_device,爲設備建立i2c_client;並且將設備添加到i2c_bus_type->p->klist_devices鏈表上(device_register),通過bus_add_device函數完成,最後調用bus_probe_device,嘗試綁定驅動。
  • client添加到驅動的設備鏈表上:list_add_tail(&client->detected, &driver->clients)

 

方式二:i2c設備之靜態註冊

Linux 3.3 提供了靜態定義的方式來註冊設備,接口原型:linux-3.3/drivers/i2c/i2c-boardinfo.c

 int __initi2c_register_board_info(int busnum, struct i2c_board_info const *info, unsigned len)

核心內容: 

  • 申請struct i2c_devinfo,用以描述一個i2c外設;
  • list_add_tail(&devinfo->list, &__i2c_board_list),將devinfo加入鏈表__i2c_board_list,以供後續查找;

掃描__i2c_board_list,創建client

i2c_register_board_info只是把設備描述符加入到了__i2c_board_list,並沒有創建client,當調用i2c_register_adapter註冊adapter時,會掃描__i2c_board_list,創建client;具體調用:

i2c_register_adapter 

--> i2c_scan_static_board_info 

--> i2c_new_device 

--> device_register

 

在 i2c_new_device完成了client創建,以及設備註冊device_register

 

PS

由上面的註冊流程可知,i2c_register_board_info應該在i2c_register_adapter之前完成,否則__i2c_board_list中的節點不會被掃描到。

 

總結:

  • 由上述分析可知,i2c設備驅動是通過i2c_register_driver註冊的,i2c設備是通過i2c_new_device註冊的,在最後,這兩個函數都嘗試進行驅動和設備綁定(driver_attachbus_probe_device);因此不管是先註冊驅動還是先註冊設備,最後都能夠將合適的驅動和設備進行綁定。
  • 有兩種方式進行設備註冊:

1、通過i2c_register_board_info,在系統啓動之初靜態地進行i2c設備註冊(axp電源驅動就是這樣做的);

2、實現i2c設備驅動的detect函數,在驅動加載的時候動態檢測創建設備,aw平臺的觸摸屏驅動gt82x.ko就是通過這種方式。

  • Linux是通過在驅動數據結構中內嵌kobjectkset,完成了設備驅動的層次管理的,理解kobjectkset對理解設備驅動模型很重要。

4. i2c驅動架構圖

 

 

1、i2c_add_adapter

2、i2c_new_device/i2c_register_board_info

3、i2c_add_driver

4、調用i2c bus中註冊的match函數進行匹配

5、調用platform bus中註冊的match函數進行匹配

6、i2cdev_attach_adapter

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章