1、前言
在嵌入式Linux開發中,對嵌入式SoC中的GPIO進行控制非常重要,Linux內核中提供了GPIO子系統,驅動開發者在驅動代碼中使用GPIO子系統提供的API函數,便可以達到對GPIO控制的效果,例如將IO口的方向設置為輸入或輸出,當IO口的方向為輸入時,可以通過調用API函數獲取相應的IO口電平,當IO口設置為輸出方向時,可以調用相關的API函數去設置IO口電平,本文將簡單描述如何去使用Linux內核中GPIO子系統的API接口。
下圖是Linux內核中GPIO子系統的軟件驅動分層圖:
2、常用API接口
當我們在驅動代碼中要使用內核中提供的GPIO子系統,需要在驅動代碼中包含<linux/gpio.h>頭文件,另外,關於API接口函數的實現在內核源碼drivers/gpio/gpiolib.c文件中,關於GPIO子系統的使用說明文檔為Documentation/gpio.txt,該文檔具有更詳細的使用說明,接下來,將簡單介紹一下常用的API接口。
/* * "valid" GPIO numbers are nonnegative and may be passed to * setup routines like gpio_request(). only some valid numbers * can successfully be requested and used. * * Invalid GPIO numbers are useful for indicating no-such-GPIO in * platform data and other tables. */ static inline bool gpio_is_valid(int number) { return number >= 0 && number < ARCH_NR_GPIOS; }
函數gpio_is_valid()用來判斷獲取到的gpio號是否是有效的,只有有效的gpio號,才能向內核中進行申請使用,因此,當我們從設備樹的設備節點獲取到gpio號,可以使用該函數進行判斷是否有效。
/* Always use the library code for GPIO management calls, * or when sleeping may be involved. */ extern int gpio_request(unsigned gpio, const char *label); extern void gpio_free(unsigned gpio);
上面這兩個函數用來向系統中申請GPIO和釋放已經申請的GPIO,在函數gpio_request()中傳入的形參中,gpio為IO號,label為向系統中申請GPIO使用的標簽,類似於GPIO的名稱。
/** * struct gpio - a structure describing a GPIO with configuration * @gpio: the GPIO number * @flags: GPIO configuration as specified by GPIOF_* * @label: a literal description string of this GPIO */ struct gpio { unsigned gpio; unsigned long flags; const char *label; };
結構體struct gpio用來描述一個需要配置的GPIO。
extern int gpio_request_one(unsigned gpio, unsigned long flags, const char *label); extern int gpio_request_array(const struct gpio *array, size_t num); extern void gpio_free_array(const struct gpio *array, size_t num);
上面的3個函數也是用來向系統申請或者釋放GPIO資源,函數gpio_request_one()用來申請單個GPIO,但是在申請的時候可以設置flag標志,例如,該函數在申請GPIO資源的同時,直接將GPIO的方向設置為輸入或者輸出,函數gpio_request_array()和gpio_free_array()用來向系統中申請或者釋放多個GPIO資源。
/* CONFIG_GPIOLIB: bindings for managed devices that want to request gpios */ struct device; int devm_gpio_request(struct device *dev, unsigned gpio, const char *label); int devm_gpio_request_one(struct device *dev, unsigned gpio, unsigned long flags, const char *label); void devm_gpio_free(struct device *dev, unsigned int gpio);
上面的3個函數也是用來向系統申請或者釋放GPIO資源,但是函數帶有devm_前綴,也就是說,這是帶設備資源管理版本的函數,因此在使用上面的函數時,需要指定設備的struct device指針。
static inline int gpio_direction_input(unsigned gpio) { return gpiod_direction_input(gpio_to_desc(gpio)); } static inline int gpio_direction_output(unsigned gpio, int value) { return gpiod_direction_output_raw(gpio_to_desc(gpio), value); }
當我們使用gpio_request()函數向系統中申請了GPIO資源后,可以使用上面的函數進行GPIO的方向設置,函數gpio_direction_input()用來設置GPIO的方向為輸入,函數gpio_direction_output()用來設置GPIO的方向為輸出,並且通過value值可以設置輸出的電平。
static inline int gpio_get_value(unsigned int gpio) { return __gpio_get_value(gpio); } static inline void gpio_set_value(unsigned int gpio, int value) { __gpio_set_value(gpio, value); }
當我們將GPIO的方向設置為輸入時,可以使用上面的函數gpio_get_value()來獲取當前的IO口電平值,當GPIO的方向設置為輸出時,使用函數gpio_set_value()可以設置IO口的電平值。
static inline int gpio_cansleep(unsigned int gpio) { return __gpio_cansleep(gpio); }
使用函數gpio_cansleep()判斷是否能處於休眠狀態,當該函數返回非零值時,說明讀或寫GPIO的電平值時能夠處於休眠狀態。
static inline int gpio_get_value_cansleep(unsigned gpio) { return gpiod_get_raw_value_cansleep(gpio_to_desc(gpio)); } static inline void gpio_set_value_cansleep(unsigned gpio, int value) { return gpiod_set_raw_value_cansleep(gpio_to_desc(gpio), value); }
上面的函數同樣是獲取或者設置GPIO的電平值,只不過是帶休眠版本的函數。
static inline int gpio_to_irq(unsigned int gpio) { return __gpio_to_irq(gpio); }
函數gpio_to_irq()用於將當前已經申請GPIO號轉換為IRQ號,也就是獲取當前GPIO的中斷線,函數調用成功后,將返回對應的IRQ號。
以上就是Linux內核中GPIO子系統的常用的API接口,關於其代碼的實現,可以進一步分析Linux內核源碼。
3、實例說明
在上面,已經分析過了Linux驅動中GPIO子系統的常用API接口函數,接下來,將通過一個具體的實例來講解GPIO子系統中API接口如何使用。
首先,先了解一下GPIO的使用思路,如下所示:
#include <linux/module.h> #include <linux/init.h> #include <linux/gpio.h> ... struct gpio_drvdata { /* gpio號 */ int gpio_num; ... }; static int __init gpio_init(void) { struct gpio_drvdata *ddata; int ret; ddata = kzalloc(sizeof(*ddata), GFP_KERNEL); if (!ddata) return -ENOMEM; ... /* gpio初始化 */ if (gpio_is_valid(ddata->gpio_num)) { /* 申請gpio資源 */ ret = gpio_request(ddata->gpio_num, "test-gpio"); if (ret) { printk("failed to request gpio\n"); return ret; } /* 設置gpio的方向(輸出) */ ret = gpio_direction_output(ddata->gpio_num, 0); if (ret) { printk("failed to set output direction\n"); return ret; } /* 在sysfs中導出gpio(方向能改變) */ ret = gpio_export(ddata->gpio_num, true); if (ret) { printk("failed to export gpio in sysfs\n"); return ret; } /* 設置gpio電平值(高電平) */ gpio_set_value(ddata->gpio_num, 1); } ... return 0; } static void __exit gpio_exit(void) { ... /* 釋放已經申請的gpio資源 */ if (gpio_is_valid(ddata->gpio_num)) gpio_free(ddata->gpio_num); ... } module_init(gpio_init); module_exit(gpio_exit);
上面的代碼已經很清楚地體現了GPIO的使用思路,當驅動模塊加載的時候,需要獲取要使用的GPIO號,然后需要向系統申請使用GPIO資源,資源申請成功后,我們需要設置GPIO的方向(輸入或者輸出),此外,還能使用gpio_export()函數在sysfs中導出GPIO,導出的好處在於可以方便地debug代碼,當驅動模塊卸載時,需要將已經申請的GPIO資源進行釋放掉,基本的使用思路就這樣,比較簡單。
接下來,給出具體的實例,功能為簡單的GPIO控制,驅動程序中嵌入platform_driver框架,另外,在設備節點中導出ctrl和gpio兩個屬性文件,應用層對ctrl屬性文件進行讀寫操作,能夠獲取和設置GPIO的電平狀態,對gpio讀操作,能夠獲取使用的GPIO號,下面是具體實例的實現過程:
先定義相關的設備節點,如下:
dev_gpio { status = "okay"; compatible = "dev-gpio"; label = "test_gpio"; gpios = <&msm_gpio 68 0>; };
使用了GPIO_68這個引腳,compatible屬性的值用於和驅動程序進行匹配,接下來是驅動代碼的實現:
#include <linux/module.h> #include <linux/init.h> #include <linux/of.h> #include <linux/of_gpio.h> #include <linux/platform_device.h> #include <linux/kernel.h> #include <linux/slab.h> #include <linux/errno.h> #include <linux/gpio.h> #include <linux/sysfs.h> struct gpio_platform_data { const char *label; unsigned int gpio_num; enum of_gpio_flags gpio_flag; }; struct gpio_drvdata { struct gpio_platform_data *pdata; bool gpio_state; }; static ssize_t ctrl_show(struct device *dev, struct device_attribute *attr, char *buf) { struct gpio_drvdata *ddata = dev_get_drvdata(dev); int ret; if (ddata->gpio_state) ret = snprintf(buf, PAGE_SIZE - 2, "%s", "enable"); else ret = snprintf(buf, PAGE_SIZE - 2, "%s", "disable"); buf[ret++] = '\n'; buf[ret] = '\0'; return ret; } static ssize_t ctrl_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { struct gpio_drvdata *ddata = dev_get_drvdata(dev); bool state = ddata->gpio_state; if (!strncmp(buf, "enable", strlen("enable"))) { if (!state) { gpio_set_value(ddata->pdata->gpio_num, !state); ddata->gpio_state = !state; goto ret; } } else if (!strncmp(buf, "disable", strlen("disable"))) { if (state) { gpio_set_value(ddata->pdata->gpio_num, !state); ddata->gpio_state = !state; goto ret; } } return 0; ret: return strlen(buf); } static DEVICE_ATTR(ctrl, 0644, ctrl_show, ctrl_store); static ssize_t gpio_show(struct device *dev, struct device_attribute *attr, char *buf) { struct gpio_drvdata *ddata = dev_get_drvdata(dev); int ret; ret = snprintf(buf, PAGE_SIZE - 2, "gpio-number: GPIO_%d", ddata->pdata->gpio_num - 911); buf[ret++] = '\n'; buf[ret] = '\0'; return ret; } static DEVICE_ATTR(gpio, 0444, gpio_show, NULL); static struct attribute *gpio_attrs[] = { &dev_attr_ctrl.attr, &dev_attr_gpio.attr, NULL }; static struct attribute_group attr_grp = { .attrs = gpio_attrs, }; static struct gpio_platform_data * gpio_parse_dt(struct device *dev) { int ret; struct device_node *np = dev->of_node; struct gpio_platform_data *pdata; pdata = kzalloc(sizeof(*pdata), GFP_KERNEL); if (!pdata) { dev_err(dev, "failed to alloc memory of platform data\n"); return NULL; } ret = of_property_read_string(np, "label", &pdata->label); if (ret) { dev_err(dev, "failed to read property of lable\n"); goto fail; } pdata->gpio_num = of_get_named_gpio_flags(np, "gpios", 0, &pdata->gpio_flag); if (pdata->gpio_num < 0) { dev_err(dev, "invalid gpio number %d\n", pdata->gpio_num); ret = pdata->gpio_num; goto fail; } return pdata; fail: kfree(pdata); return ERR_PTR(ret); } static int gpio_probe(struct platform_device *pdev) { struct gpio_drvdata *ddata; struct gpio_platform_data *pdata; struct device *dev = &pdev->dev; struct device_node *np = dev->of_node; int ret; printk("[%s]==========gpio_probe start==========\n", __func__); if (!np) { dev_err(dev, "failed to find device node of gpio device\n"); return -ENODEV; } ddata = kzalloc(sizeof(*ddata), GFP_KERNEL); if (!ddata) { dev_err(dev, "failed to alloc memory for driver data\n"); return -ENOMEM; } pdata = gpio_parse_dt(dev); if (IS_ERR(pdata)) { dev_err(dev, "failed to parse device node\n"); ret = PTR_ERR(pdata); goto fail1; } if (gpio_is_valid(pdata->gpio_num)) { ret = gpio_request(pdata->gpio_num, pdata->label); if (ret) { dev_err(dev, "failed to request gpio number %d\n", pdata->gpio_num); goto fail2; } ret = gpio_direction_output(pdata->gpio_num, 0); if (ret) { dev_err(dev, "failed to set gpio direction for output\n"); goto fail3; } ret = gpio_export(pdata->gpio_num, false); if (ret) { dev_err(dev, "failed to export gpio %d\n", pdata->gpio_num); goto fail3; } } ddata->gpio_state = false; ddata->pdata = pdata; platform_set_drvdata(pdev, ddata); ret = sysfs_create_group(&dev->kobj, &attr_grp); if (ret) { dev_err(dev, "failed to create sysfs files\n"); goto fail3; } printk("[%s]==========gpio_probe over==========\n", __func__); return 0; fail3: gpio_free(pdata->gpio_num); fail2: kfree(pdata); fail1: kfree(ddata); return ret; } static int gpio_remove(struct platform_device *pdev) { struct gpio_drvdata *ddata = platform_get_drvdata(pdev); struct gpio_platform_data *pdata = ddata->pdata; sysfs_remove_group(&pdev->dev.kobj, &attr_grp); if (gpio_is_valid(pdata->gpio_num)) gpio_free(pdata->gpio_num); kfree(pdata); pdata = NULL; kfree(ddata); ddata = NULL; return 0; } static struct of_device_id device_match_table[] = { { .compatible = "dev-gpio",}, { }, }; MODULE_DEVICE_TABLE(of, device_match_table); static struct platform_driver dev_gpio_driver = { .probe = gpio_probe, .remove = gpio_remove, .driver = { .name = "dev-gpio", .owner = THIS_MODULE, .of_match_table = device_match_table, }, }; module_platform_driver(dev_gpio_driver); MODULE_AUTHOR("HLY"); MODULE_LICENSE("GPL v2");
實現的思路和前面給出的模板一樣,只不過是嵌入了platform_driver這個驅動框架,另外,在設備節點中導出了ctrl和gpio屬性文件,便可以很方便地在應用層進行設備的GPIO控制了。
接下來,看看實現的效果,首先是生成的設備節點信息,可以使用下面的命令:
# ls -al
# cat uevent
輸出如下:
通過uevent可以看到設備節點的路徑以及驅動和設備匹配的屬性值,此外,在上面圖片中,也可以看到ctrl和gpio屬性文件已經被成功導出到了該設備節點下面,使用下面的命令可以進行GPIO的控制:
##將GPIO置高電平 # echo "enable" > ctrl ##將GPIO置低電平 # echo "disable" > ctrl
控制的效果如下所示:
另外,在驅動程序中,我們使用了函數gpio_export()在sysfs中導出相關的GPIO信息,我們可以到/sys/class/gpio/gpioN目錄下查看相關的GPIO信息,如下:
屬性文件value保存了當前GPIO的電平值,當我們調用gpio_export()函數時,將第二個形參傳入為true時,表示GPIO的方向還能改變,將在上面的目錄中生成direction屬性文件,里面保存了當前GPIO的方向,我們還能使用echo命令對文件進行寫操作,從而改變GPIO的方向。
4、小結
本文簡單介紹了Linux中GPIO子系統中的常用的API接口函數,並且給出了驅動程序中使用GPIO子系統的思路,另外還通過一個簡單的實例進行說明。