在之前的文章里,我們掌握了無設備樹和有設備樹這兩種 platform 驅動的開發方式。
但實際上有現成的,Linux 內核的 LED 燈驅動采用 platform 框架,我們只需要按照要求在設備樹文件中添加相應的 LED 節點即可。
本講內容,我們就來學習如何使用 Linux 內核自帶的 LED 驅動,來驅動 I.MX6U-ALPHA 開發板上的 LED0。
LED 驅動使能
要使用 Linux 內核自帶的 LED 燈驅動首先得先配置 Linux 內核,使能自帶的 LED 燈驅動。
輸入如下命令打開 Linux 配置菜單:
make menuconfig
按照如下路徑打開 LED 驅動配置項:
選擇“LED Support for GPIO connected LEDs”,按下“Y”鍵,將其編譯進 Linux 內核,也即是在此選項上,如圖:
在“LED Support for GPIO connected LEDs”上按下‘?’ 可以打開此選項的幫助信息:
把 Linux 內部自帶的LED 燈驅動編譯進內核以后 ,CONFIG_LEDS_GPIO 就會等于‘y’。
配置好 Linux 內核以后退出配置界面,打開.config 文件,搜索“CONFIG_LEDS_GPIO=y”:
重新編譯 Linux 內核,然后使用新編譯出來的 zImage 鏡像啟動開發板。
LED 驅動分析
驅動框架
LED 燈驅動文件為/drivers/leds/leds-gpio.c,打開/drivers/leds/Makefile文件,可以發現:
obj-$(CONFIG_LEDS_GPIO) += leds-gpio.o
如果定義了 CONFIG_LEDS_GPIO 的話,就會編譯 leds-gpio.c 這個文件,通過圖形化界面我們已經使能LED,因此 leds-gpio.c 驅動文件就會被編譯。
打開 leds-gpio.c 這個驅動文件,采用了 platform 框架:
static const struct of_device_id of_gpio_leds_match[] = {{ .compatible = "gpio-leds", },{},
};
......static struct platform_driver gpio_led_driver = {.probe = gpio_led_probe,.remove = gpio_led_remove,.driver = {.name = "leds-gpio",.of_match_table = of_gpio_leds_match,},
};module_platform_driver(gpio_led_driver);
LED 驅動的匹配表,compatible 內容為“gpio-leds”,因此設備樹中的 LED 燈設備節點的 compatible 屬性值也要為“gpio-leds”,否則設備和驅動匹配不成功,驅動就沒法工作。
probe 函數為 gpio_led_probe,驅動名字為“leds-gpio”,當驅動和設備匹配成功以后 gpio_led_probe 函數就會執行。
在/sys/bus/platform/drivers 目錄下,存在一個名為“leds-gpio”的文件,如圖:
通過 module_platform_driver 函數,向 Linux 內核注冊 gpio_led_driver 這個 platform驅動。
module_platform_driver(gpio_led_driver);
module_platform_driver 函數
在 Linux 內核中,會大量采用 module_platform_driver 來完成向 Linux 內核注冊 platform 驅動的操作。
module_platform_driver 定義在 include/linux/platform_device.h 文件中,為一個宏,定義如下:
#define module_platform_driver(__platform_driver) \module_driver(__platform_driver, platform_driver_register, \platform_driver_unregister)
可以看出, module_platform_driver 依賴 module_driver, module_driver 也是一個宏。
module_driver? 定義在include/linux/device.h 文件中,內容如下:
#define module_driver(__driver, __register, __unregister, ...) \static int __init __driver##_init(void) \{ \return __register(&(__driver), ##__VA_ARGS__); \} \module_init(__driver##_init); \static void __exit __driver##_exit(void) \{ \__unregister(&(__driver), ##__VA_ARGS__); \} \module_exit(__driver##_exit)
將module_platform_driver函數完全展開,也就是:
static int __init gpio_led_driver_init(void)
{return platform_driver_register (&(gpio_led_driver));
}
module_init(gpio_led_driver_init);static void __exit gpio_led_driver_exit(void)
{platform_driver_unregister (&(gpio_led_driver) );
}module_exit(gpio_led_driver_exit);
因此 module_platform_driver 函數的功能,就是完成 platform 驅動的注冊和刪除。
gpio_led_probe 函數
當驅動和設備匹配以后 gpio_led_probe 函數就會執行,此函數主要是從設備樹中獲取 LED燈的 GPIO 信息。
gpio_led_probe 函數,縮減后的函數內容如下所示:
/*** gpio_led_probe - GPIO LED驅動的探測函數* @pdev: 匹配到的平臺設備** 此函數在驅動與設備匹配成功后調用,負責初始化LED設備。* 支持傳統platform_data和設備樹兩種配置方式。*/
static int gpio_led_probe(struct platform_device *pdev)
{struct gpio_led_platform_data *pdata = dev_get_platdata(&pdev->dev);struct gpio_leds_priv *priv;int i, ret = 0;/* 檢查并使用平臺數據(傳統非設備樹方式) */if (pdata && pdata->num_leds) {/* 獲取platform_device信息 */......} else { /* 設備樹方式初始化 */priv = gpio_leds_create(pdev);if (IS_ERR(priv))return PTR_ERR(priv);}/* 將私有數據保存到設備結構 */platform_set_drvdata(pdev, priv);return 0;
}
如果使用設備樹的話,使用 gpio_leds_create 函數從設備樹中提取設備信息,獲取到的 LED 燈 GPIO 信息保存在返回值中。
gpio_leds_create 函數內容如下:
/*** gpio_leds_create - 從設備樹創建GPIO LED設備* @pdev: 平臺設備指針** 該函數解析設備樹節點,為每個子節點創建對應的LED設備* 返回包含所有LED的私有數據結構,錯誤時返回ERR_PTR*/
static struct gpio_leds_priv *gpio_leds_create(struct platform_device *pdev)
{struct device *dev = &pdev->dev;struct fwnode_handle *child; // 設備樹子節點句柄struct gpio_leds_priv *priv;int count, ret;struct device_node *np;/* 1. 獲取子節點數量 */count = device_get_child_node_count(dev);if (!count)return ERR_PTR(-ENODEV); // 無有效子節點/* 2. 分配私有數據結構內存 */priv = devm_kzalloc(dev, sizeof_gpio_leds_priv(count), GFP_KERNEL);if (!priv)return ERR_PTR(-ENOMEM); // 內存分配失敗/* 3. 遍歷所有子節點 */device_for_each_child_node(dev, child) {struct gpio_led led = {};const char *state = NULL;/* 3.1 獲取GPIO描述符 */led.gpiod = devm_get_gpiod_from_child(dev, NULL, child);if (IS_ERR(led.gpiod)) {fwnode_handle_put(child);ret = PTR_ERR(led.gpiod);goto err; // GPIO獲取失敗跳轉錯誤處理}np = of_node(child); // 獲取設備樹節點/* 3.2 解析LED名稱(label屬性優先) */if (fwnode_property_present(child, "label")) {fwnode_property_read_string(child, "label", &led.name);} else {if (IS_ENABLED(CONFIG_OF) && !led.name && np)led.name = np->name; // 使用節點名作為備選if (!led.name)return ERR_PTR(-EINVAL); // 名稱無效}/* 3.3 解析默認觸發器 */fwnode_property_read_string(child, "linux,default-trigger",&led.default_trigger);/* 3.4 解析默認狀態(on/off/keep) */if (!fwnode_property_read_string(child, "default-state", &state)) {if (!strcmp(state, "keep"))led.default_state = LEDS_GPIO_DEFSTATE_KEEP;else if (!strcmp(state, "on"))led.default_state = LEDS_GPIO_DEFSTATE_ON;elseled.default_state = LEDS_GPIO_DEFSTATE_OFF;}/* 3.5 解析電源管理屬性 */if (fwnode_property_present(child, "retain-state-suspended"))led.retain_state_suspended = 1;/* 3.6 創建單個LED設備 */ret = create_gpio_led(&led, &priv->leds[priv->num_leds++],dev, NULL);if (ret < 0) {fwnode_handle_put(child);goto err; // LED創建失敗跳轉錯誤處理}}return priv; // 成功返回私有數據err:/* 錯誤處理:逆向清理已創建的LED設備 */for (count = priv->num_leds - 2; count >= 0; count--)delete_gpio_led(&priv->leds[count]);return ERR_PTR(ret); // 返回錯誤指針
}
- 調用 device_get_child_node_count 函數,統計子節點數量,一般在在設備樹中創建一個節點表示 LED 燈,然后在這個節點下面為每個 LED 燈創建一個子節點。因此子節點數量也是 LED 燈的數量。
- 遍歷每個子節點,獲取每個子節點的信息。
- 獲取 LED 燈所使用的 GPIO 信息。
- 讀取子節點 label 屬性值,因為使用 label 屬性作為 LED 的名字。
- 獲取“linux,default-trigger”屬性值,可以通過此屬性設置某個 LED 燈在Linux 系統中的默認功能,比如作為系統心跳指示燈等等。
- 獲取“default-state”屬性值,也就是 LED 燈的默認狀態屬性。
- 調用 create_gpio_led 函數,創建 LED 相關的 io,其實就是設置 LED 所使用的 io為輸出之類的。 create_gpio_led 函數主要是初始化 led_dat 這個 gpio_led_data 結構體類型變量, led_dat 保存了 LED 的操作函數等內容。
總結,gpio_led_probe 函數主要功能就是獲取 LED 燈的設備信息,然后根據這些信息來初始化對應的 IO,設置為輸出等。
設備樹節點編寫
打開文檔 Documentation/devicetree/bindings/leds/leds-gpio.txt,文檔詳細地講解了 Linux 自帶驅動對應的設備樹節點該如何編寫。
我們在編寫設備節點的時候要注意以下幾點:
- 創建一個節點表示 LED 燈設備,比如 dtsleds,如果板子上有多個 LED 燈的話每個 LED燈都作為 dtsleds 的子節點。
- dtsleds 節點的 compatible 屬性值一定要為“gpio-leds”。
- 設置 label 屬性,此屬性為可選,每個子節點都有一個 label 屬性, label 屬性一般表示LED 燈的名字,比如以顏色區分的話就是 red、 green 等等。
- 每個子節點必須要設置 gpios 屬性值,表示此 LED 所使用的 GPIO 引腳。
- 可以設置“linux,default-trigger”屬性值,也就是設置 LED 燈的默認功能。
- 可以設置“default-state”屬性值,可以設置為 on、 off 或 keep,為 on 的時候 LED 燈默認打開,為 off 的話 LED 燈默認關閉,為 keep 的話 LED 燈保持當前模式。
其中,LED 燈的默認功能,可以查閱Documentation/devicetree/bindings/leds/common.txt 這個文檔來查看可選功能,比如:
- backlight: LED 燈作為背光。
- default-on: LED 燈打開。
- heartbeat: LED 燈作為心跳指示燈,可以作為系統運行提示燈。
- ide-disk: LED 燈作為硬盤活動指示燈。
- timer: LED 燈周期性閃爍,由定時器驅動,閃爍頻率可以修改。
按照上面描述,打開 imx6ull-alientek-emmc.dts文件, 添加如下所示 LED 燈設備節點:
dtsleds {compatible = "gpio-leds";led0 {label = "red";gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;default-state = "off";};
};
在 dtsleds 這個節點下只,有一個子節點led0, LED0 名字為 red,默認關閉。
修改完成以后保存,并重新編譯設備樹:make dtbs,然后用新的設備樹啟動開發板。
運行測試
用新的zImage 和 imx6ull-alientek-emmc.dtb 啟動開發板 , 啟動以后查 看/sys/bus/platform/devices/dtsleds 這個目錄是否存在。
如果存在的話,進入該目錄,如圖:
繼續進入到 leds 目錄中,如圖:
可以看出,在 leds 目錄下有一個名為“red”子目錄,這個子目錄的名字就是我們在設備樹中設置的 label 屬性值。
查看一下系統中有沒有“sys/class/leds/red/brightness”這個文件,存在就說明運行正常,輸入如下命令測試LED燈:
echo 1 > /sys/class/leds/red/brightness //打開 LED0
echo 0 > /sys/class/leds/red/brightness //關閉 LED0
我們也可以設置 LED0 作為系統指示燈,修改設備樹文件:在 dtsleds 這個設備節點中加入“linux,default-trigger”屬性信息即可,屬性值為“heartbeat”,
dtsleds {compatible = "gpio-leds";led0 {label = "red";gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;linux,default-trigger = "heartbeat";default-state = "on";};
};
重新編譯設備樹,使用新的設備樹啟動 Linux 系統。
啟動以后 LED0 就會閃爍,作為系統心跳指示燈,表示系統正在運行。