1. 程式人生 > >i2c裝置與驅動匹配過程

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_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}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)) {1280return -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_core:i2c_add_numbered_adapter。

至此,probe函式完成。

2.2.2  sun6i_i2c_core_process

i2c控制器的中斷服務程式sun6i_i2c_handler呼叫了sun6i_i2c_core_process,i2c匯流排的實際傳輸控制也是在該函式裡完成的。

主要流程:

  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_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,建立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_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