歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> Linux技術

linux下i2c驅動筆記

原文http://blog.sina.com.cn/s/blog_63f31f340101byb2.html

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_algorithm的master_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_type的probe函數,最終會調用設備驅動的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}

1272
1273 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}

1278
1279 if (!request_mem_region(res->start, resource_size(res), res->name)) {

1280return -ENOMEM;
1281 }

首先得到當前設備的私有數據指針,並將其保留在pdata;進而通過platform_get_resource得到該設備占用的內存資源,並申請:request_mem_region。同時將irq資源也保留下來。

1288
1289 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_core:i2c_add_numbered_adapter。

至此,probe函數完成。

2.2.2 sun6i_i2c_core_process

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

主要流程:

讀取i2c控制器當前狀態,twi_query_irq_status,保留在state中;

根據state的值進行分支跳轉,控制i2c的工作狀態;

傳輸完成,調用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_private是device_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_internal將kobject添加到對應的kset中;需要主要的是,如果kobject的parent如果為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_type,dev->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,創建clienti2c_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_attach和bus_probe_device);因此不管是先注冊驅動還是先注冊設備,最後都能夠將合適的驅動和設備進行綁定。

有兩種方式進行設備注冊:

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

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

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

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

Copyright © Linux教程網 All Rights Reserved