Linux驅動開發08:【裝置樹】MPU6050驅動和i2c驅動
介紹
上一節在nanopi裝置樹的I2C節點下增加了一個MPU6050的子節點,並在sysfs中檢視到了該節點已經被正確解析,這一節我們來修改之前的MPU6050驅動,使之能夠匹配到我們的裝置樹節點,然後再分析裝置樹節點是如何載入到i2c總線上的。
MPU6050驅動的變更
在之前的MPU6050驅動中,為了方便測試,我們是在模組的init函式中臨時註冊了一個i2c_client到i2c總線上,該i2c_client的資訊由i2c_board_info指定,包括i2c_client的名稱和從機地址。然後我們再註冊了i2c_driver到i2c總線上,系統通過比較i2c_driver.id_table和i2c_client.name判斷裝置和驅動是否匹配,如果匹配那麼執行probe函式,整個過程如下所示:
static struct i2c_client *temp_client;
static int __init mpu6050_i2c_init(void)
{
struct i2c_adapter *adapter;
adapter = i2c_get_adapter(I2C_0);
if (!adapter)
printk(KERN_INFO "fail to get i2c-%d\n", I2C_0);
// 臨時註冊一個i2c_client
temp_client = i2c_new_device(adapter, &mpu6050_i2c_info);
if (!temp_client)
printk(KERN_INFO "fail to registe %s\n", mpu6050_i2c_info.type);
pr_info(KERN_INFO "mpu6050 i2c init\n");
return i2c_add_driver(&mpu6050_i2c_driver);
}
static void __exit mpu6050_i2c_exit(void)
{
i2c_unregister_device(temp_client);
i2c_del_driver(&mpu6050_i2c_driver);
}
而現在由於我們已經在裝置樹中添加了MPU6050子節點,系統已經正確解析,並且我們在/sys/bus/i2c/device
目錄下能夠看到該節點的資訊,這說明這個子節點已經作為了一個i2c_client被載入到了i2c總線上,我們只需要修改驅動使之能夠匹配這個i2c_client即可。因此我先刪除之前的i2c_client註冊部分,只保留i2c驅動的註冊。
static int __init mpu6050_i2c_init(void)
{
return i2c_add_driver(&mpu6050_i2c_driver);
}
static void __exit mpu6050_i2c_exit(void)
{
i2c_del_driver(&mpu6050_i2c_driver);
}
然後在i2c_driver中加入對裝置樹節點的匹配
static const struct of_device_id mpu6050_of_match[] = {
{ .compatible = "inv,mpu6050", },
{ }
};
static struct i2c_driver mpu6050_i2c_driver = {
.driver = {
.name = "mpu6050",
.owner = THIS_MODULE,
.of_match_table = mpu6050_of_match,
},
.probe = mpu6050_i2c_probe,
.remove = mpu6050_i2c_remove,
.id_table = mpu6050_i2c_ids,
};
之前的id_table可以不用管,因為根據i2c匯流排的i2c_device_match()函式,如果裝置樹匹配上就不會進行id_table的匹配。注意of_device_id中的compatible屬性,系統通過這個欄位來判斷是否和已經註冊的i2c_client.dev.of_node中的compatible欄位匹配,如果匹配則執行probe函式。
經過這兩項修改,該驅動已經可以支援裝置樹了,測試方法和結果跟之前一致,我們能夠正確地讀取到感測器的值,這裡就不截圖了。
我們可以看到使用了裝置樹後驅動明顯變簡單了,並且更有條理性,驅動中可以增加多個of_device_id來匹配多個裝置樹節點,從而支援多個裝置。而我們驅動的改動很小,這是因為作業系統幫我們做了大部分的事情。
到這裡我們至少要提出兩個問題:
- 這個MPU6050的i2c_client是何時加入到i2c匯流排的?
- 之前我們在i2c_board_info中明確指定了從機地址,這個裝置樹的從機地址是何時賦值到i2c_client中的?
i2c_client註冊的分析
首先說結論,裝置樹中i2c節點下的子節點是在i2c_adapter註冊的時候一同被註冊到i2c匯流排的,因為從驅動框架來看i2c_client是掛接到i2c_adapter上的,因此註冊i2c_adapter時將總線上的i2c_client新增到系統合情合理。註冊i2c_client時會找到子節點中的reg屬性並賦值到i2c_client.addr中。具體分析見以下程式碼樹(注意,對於不同的硬體平臺,目錄可能有差別)。
--- drivers --- i2c --- busses --- i2c-mv64xxx.c --- mv64xxx_i2c_probe( --- drv_data = devm_kzalloc();
| struct platform_device *pd) |- drv_data->adapter.algo = &mv64xxx_i2c_algo
| |- drv_data->adapter.class = I2C_CLASS_DEPRECATED
| |- drv_data->adapter.nr = pd->id
| |* drv_data->adapter.dev.of_node = pd->dev.of_node
| |- i2c_add_numbered_adapter(&drv_data->adapter))
|
|- i2c_core.c --- i2c_add_numbered_adapter( --- __i2c_add_numbered_adapter(adap)
| struct i2c_adapter *adap)
|- __i2c_add_numbered_adapter( --- i2c_register_adapter(adap)
| struct i2c_adapter *adap)
|- i2c_register_adapter( --- *of_i2c_register_devices(adap)//新增裝置樹子節點上的裝置
| struct i2c_adapter *adap)
|- of_i2c_register_devices( --- struct device_node *bus
| struct i2c_adapter *adap) |- struct i2c_client *client
| |- bus = of_node_get(adap->dev.of_node)
| |- for_each_available_child_of_node(bus, node)
| |* client = of_i2c_register_device(adap, node)
|- of_i2c_register_device( --- struct i2c_board_info info = {};
struct i2c_adapter *adap, |- addr_be = of_get_property(node, "reg", &len)//獲取reg屬性
struct device_node *node) |- addr = be32_to_cpup(addr_be)
|- info.addr = addr
|- info.of_node = of_node_get(node)
|* i2c_new_device(adap, &info)
i2c-mv64xxx.c
是針對nanopi的全志H3平臺的i2c_adapter的初始化,對於不同的平臺,初始化過程也不相同,所以需要找到所使用平臺的i2c-xxx.c
檔案再分析。
該驅動是一個platform驅動,i2c_adapter是platform_device,它通過裝置樹新增到系統的platform總線上,註冊platform_driver時只要compatible匹配就會執行這裡的mv64xxx_i2c_probe
函式。至於系統是如何將i2c_adapter註冊到platform匯流排的,我們下來再分析,這裡只看註冊i2c介面卡時是如何將子節點作為i2c_client新增到i2c總線上的。
以上加了星號的是重點,這個過程總結起來就是先將I2C介面卡的裝置樹節點放到i2c_adapter.dev.of_node中,然後註冊這個i2c_adapter,註冊時,遍歷了該節點下的子節點,對所有子節點執行of_i2c_register_device(adap, node)
,這個函式中,先對子節點進行解析,獲取其reg屬性的值,然後填充i2c_board_info結構體,最後用i2c_new_device
將其註冊到i2c匯流排。
i2c_adapter註冊的分析
上面提到,這裡的i2c_adapter是一個platform_device,通過與platform_driver的匹配來執行probe函式,這個platform_driver定義如下:
static const struct of_device_id mv64xxx_i2c_of_match_table[] = {
{ .compatible = "allwinner,sun4i-a10-i2c", .data = &mv64xxx_i2c_regs_sun4i},
{ .compatible = "allwinner,sun6i-a31-i2c", .data = &mv64xxx_i2c_regs_sun4i},
{ .compatible = "marvell,mv64xxx-i2c", .data = &mv64xxx_i2c_regs_mv64xxx},
{ .compatible = "marvell,mv78230-i2c", .data = &mv64xxx_i2c_regs_mv64xxx},
{ .compatible = "marvell,mv78230-a0-i2c", .data = &mv64xxx_i2c_regs_mv64xxx},
{}
};
static struct platform_driver mv64xxx_i2c_driver = {
.probe = mv64xxx_i2c_probe,
.remove = mv64xxx_i2c_remove,
.driver = {
.name = MV64XXX_I2C_CTLR_NAME,
.pm = mv64xxx_i2c_pm_ops,
.of_match_table = mv64xxx_i2c_of_match_table,
},
};
裝置樹中的i2c_adapter定義如下,這個節點將來會被系統作為一個platform_device註冊到系統
i2c0: [email protected]01c2ac00 {
compatible = "allwinner,sun6i-a31-i2c";
reg = <0x01c2ac00 0x400>;
interrupts = <GIC_SPI 6 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&ccu CLK_BUS_I2C0>;
resets = <&ccu RST_BUS_I2C0>;
pinctrl-names = "default";
pinctrl-0 = <&i2c0_pins>;
status = "disabled";
#address-cells = <1>;
#size-cells = <0>;
};
全志H3晶片有三個i2c控制器,這裡只列出其中的一個節點。可以看到該節點的compatible = "allwinner,sun6i-a31-i2c"
,再看platform_driver中也有”allwinner,sun6i-a31-i2c”這個compatible,因此它們可以匹配。
為什麼這個i2c_adapter會被系統識別為platform_device呢?因為這個i2c控制器節點是soc
的子節點,而soc
的compatible = "simple_bus"
,這就是上一節提到過的:核心在解析裝置樹時遇到”simple-bus”時,會繼續解析這個節點的子節點,並將各個子節點註冊為一個platform_device
放到platform_bus_type
中。我們來看具體程式碼是如何寫的。
const struct of_device_id of_default_bus_match_table[] = {
{ .compatible = "simple-bus", },
{ .compatible = "simple-mfd", },
{ .compatible = "isa", },
#ifdef CONFIG_ARM_AMBA
{ .compatible = "arm,amba-bus", },
#endif /* CONFIG_ARM_AMBA */
{} /* Empty terminated list */
};
of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL)
--- drivers --- of --- platform.c --- of_platform_populate( --- struct device_node *child
| struct device_node *root, |- root = root ? of_node_get(root) :
| const struct of_device_id *matches, | of_find_node_by_path("/");
| const struct of_dev_auxdata *lookup, |- for_each_child_of_node(root, child)
| struct device *parent) | of_platform_bus_create(child, matches,
| | lookup, parent, true)
|- of_platform_bus_create( --- struct platform_device *dev
| struct device_node *bus, |- void *platform_data = NULL
| const struct of_device_id *matches, |- const char *bus_id = NULL
| const struct of_dev_auxdata *lookup, |- dev = of_platform_device_create_pdata(
| struct device *parent, bool strict) |- bus, bus_id, platform_data, parent)
| | // 對該節點建立platform_device
| |- if (!dev || !of_match_node(matches, bus))
| | return 0;
| |- for_each_child_of_node(bus, child)
| | of_platform_bus_create(child, matches,
| | lookup, &dev->dev, strict);
| | // 如果該節點.compatible = "simple-bus"
| | // 遍歷子節點,遞迴呼叫,繼續註冊子節點
|- of_platform_device_create_pdata( --- struct platform_device *dev
| struct device_node *np, |- dev = of_device_alloc(np, bus_id, parent)
| const char *bus_id, |- dev->dev.bus = &platform_bus_type
| void *platform_data, |- dev->dev.platform_data = platform_data
| struct device *parent) |- of_device_add(dev)
|- of_device_alloc( --- struct platform_device *dev
| struct device_node *np, |- dev = platform_device_alloc("", -1)
| const char *bus_id, |- dev->dev.of_node = of_node_get(np)
| struct device *parent) |- dev->dev.parent = parent ? : &platform_bus
| |- return dev
|- of_device_add( --- device_add(&ofdev->dev)
struct platform_device *ofdev)
系統通過呼叫of_platform_populate
函式解析整個裝置樹,該函式在customize_machine
中通過machine_desc->init_machine()
間接呼叫。
static int __init customize_machine(void)
{
/*
* customizes platform devices, or adds new ones
* On DT based machines, we fall back to populating the
* machine from the device tree, if no callback is provided,
* otherwise we would always need an init_machine callback.
*/
if (machine_desc->init_machine)
machine_desc->init_machine();
return 0;
}
arch_initcall(customize_machine);
machine_desc
是一個全域性變數,在setup_arch()
函式中被賦值,init_machine()
由平臺定義,其中會呼叫of_platform_populate
函式將根節點下的裝置樹節點註冊為platform_devcie。值得注意的是of_platform_bus_create
函式,它首先將節點自身註冊為platform_device,然後執行of_match_node(matches, bus)
判斷該節點是否和matches匹配,這裡的matches就是of_default_bus_match_table
,其中有一個欄位是.compatible = "simple-bus"
,就是說如果該節點的.compatible = "simple-bus"
,那麼就不會返回,接下來遍歷該節點下的子節點,遞迴呼叫of_platform_bus_create
,將所有子節點都註冊為platform_device,這樣i2c-0的裝置樹節點就被註冊為了一個platform_device,後面的platform_driver的匹配就得以執行。
疑問
在sunxi的板級檔案中,我始終沒有找到對init_machine的賦值,這裡的machine_desc定義如下:
static const char * const sun8i_board_dt_compat[] = {
"allwinner,sun8i-a23",
"allwinner,sun8i-a33",
"allwinner,sun8i-a83t",
"allwinner,sun8i-h2-plus",
"allwinner,sun8i-h3",
"allwinner,sun8i-v3s",
NULL,
};
DT_MACHINE_START(SUN8I_DT, "sun8i")
.init_time = sun6i_timer_init,
.dt_compat = sun8i_board_dt_compat,
MACHINE_END
其中沒有對init_machine屬性的賦值,如果這個屬性是空,那麼就不會執行of_platform_populate函式,也不會將裝置樹下的節點註冊為platform_device,但是實際上肯定是註冊了的,要不然無法使用cpu上所有的外設資源。這裡留下一個疑問,等學了核心除錯方法再在實際的除錯中解決該問題