参考1.书籍《Linux设备驱动开发》约翰·马蒂厄 第14章节 (依据这个章节编写的本文章)
这里这本书由于翻译以及等等原因,有一定噪点;
书籍的内容主要是教会大家使用,而不是成为专家;
第 14章 引脚控制和GPIO子系统
前言
大多数嵌入式linux驱动程序和内核工程师使用GPIO编写代码或使用引脚多路复用技术.
引脚:元件 引出线;
SoC会复用引脚;一个引脚有多重功能:SD3数据线、UART1的cts/rts、Flexcan2的Rx等
引脚工作模式的选择机制称为:引脚多路复用;
引脚控制器:负责选择引脚功能的系统
本章主题涉及:
引脚控制子系统,以及怎样在DT中声明其节点;
探索原来基于整数的GPIO 接口及新的基于描述符的接口API;
处理映射到IRQ的GPIO;
处理专用于GPIO的sysfs接口;
一、引脚控制子系统 PinCtrl
引脚控制子系统能够管理引脚复用;
在DT中需要引脚以某种方式多路复用的设备必须声明他们所需的引脚控制配置。
Pinctrl由两部分组成:引脚复用、引脚配置
引脚复用:同一引脚,用于不同的目的,例如:uart tx引脚、GPIO线 或 HSI数据线。(多路复用会影响引脚组或单个引脚)
引脚配置:引脚的电气特性:上拉、下拉、驱动强度、去抖间隔等。
我们只仅限于介绍使用引脚控制器驱动程序提供的函数, 而不是介绍如何编写引脚控制器驱动程序;
1. pinctrl 和设备树
DTS -> DTB -> pinctrl 进行解析引脚描述,并将配置应用到芯片中。
通常需要一组两个嵌套节点来描述引脚组配置。
第一个节点:描述组的功能(该组将用于什么目的)
第二个节点:保存引脚配置
引脚组在DT中的分配方式,很大程度上取决于平台 以及 引脚控制器驱动程序。
pinctrl-<0>:
pinctrl - 0 = <列表phandle> // 0 pinctrl , 提供设备某种状态所需的pinctrl配置列表;
pinctrl-name:
pinctrl-name = “default”; //默认选项,代表ID 0,标准化的状态列表在include/linux/pinctrl/pinctrl-state.h中找到
这里一个文心一言回答的例子:
usdhc@0219c000 { /* uSDHC4 */
non-removable;
vmmc-supply = <®_3p3v>;
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_usdhc4_1>;
};
gpio-keys {
compatible = "gpio-keys";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_io_foo &pinctrl_io_bar>;
};
iomuxc@020e0000 {
compatible = "fsl, imx6q-iomuxc";
reg = <0x020e0000 0x4000>;
/* 共享 pinctrl 设置 */
usdhc4 {
pinctrl_usdhc4_1: usdhc4grp-1 {
fsl,pins = <
MX6QDL_PAD_SD4_CMD__SD4_CMD 0x17059
MX6QDL_PAD_SD4_CLK__SD4_CLK 0x10059
MX6QDL_PAD_SD4_DAT0__SD4_DATA0 0x17059
MX6QDL_PAD_SD4_DAT1__SD4_DATA1 0x17059
MX6QDL_PAD_SD4_DAT2__SD4_DATA2 0x17059
MX6QDL_PAD_SD4_DAT3__SD4_DATA3 0x17059
MX6QDL_PAD_SD4_DAT4__SD4_DATA4 0x17059
MX6QDL_PAD_SD4_DAT5__SD4_DATA5 0x17059
MX6QDL_PAD_SD4_DAT6__SD4_DATA6 0x17059
MX6QDL_PAD_SD4_DAT7__SD4_DATA7 0x17059
>;
};
};
[...]
uart3 {
pinctrl_uart3_1: uart3grp-1 {
fsl,pins = <
MX6QDL_PAD_EIM_D24__UART3_TX_DATA 0x1b0b1
MX6QDL_PAD_EIM_D25__UART3_RX_DATA 0x1b0b1
>;
};
};
//GPIOs (Inputs)
gpios {
pinctrl_io_foo: pinctrl_io_foo {
fsl,pins = <
MX6QDL_PAD_DISP0_DAT15__GPIO5_IO09 0x1f059
MX6QDL_PAD_DISP0_DAT13__GPIO5_IO07 0x1f059
>;
};
pinctrl_io_bar: pinctrl_io_bar {
/* <PIN_FUNCTION> <PIN_SETTING> */
fsl,pins = <
MX6QDL_PAD_DISP0_DAT11__GPIO5_IO05 0x1f059
MX6QDL_PAD_DISP0_DAT9__GPIO4_IO30 0x1f059
MX6QDL_PAD_DISP0_DAT7__GPIO4_IO28 0x1f059
MX6QDL_PAD_DISP0_DAT5__GPIO4_IO26 0x1f059
>;
};
};
};
引脚配置形式:
<PIN_FUNCTION> <PIN_SETTING>
<PIN_FUNCTION> : MX6QDL_PAD_DISP0_DAT5__GPIO4_IO26 宏,通常定义在arch//boot/dts/xxxx.h 头文件中
imx6ull 关联的 arch/arm/boot/dts/imx6ull-pinfunc.h & imx6ul-pinfunc.h
- The pin function ID is a tuple of <mux_reg conf_reg input_reg mux_mode input_val>
<PIN_SETTING> : 可以用来设置上拉、下拉、保持、驱动强度等. 这个通常需要查手册;
nxp 提供了 iMX Pins Tool 来配置生成<PIN_FUNCTION> <PIN_SETTING>
前面这些dts 节点,从相应的驱动程序相关节点调用。
然而,这些引脚在相应驱动程序初始化期间配置。
选择引脚组状态之前须要先试用pinctrl_get()
函数获取引脚控制, pinctrl_lookpu_state()
检查请求的状态是否存在,最后调用pinctrl_select_state()
应用状态
获取引脚控制,并应用其默认配置的例子:
struct pinctrl *p;
struct pinctrl_state *s;
int ret;
p = pinctrl_get(dev);
if (IS_ERR(p))
return p;
s = pinctrl_lookup_state(p, name);
if (IS_ERR(s)) {
devm_pinctrl_put(p);
return ERR_PTR(PTR_ERR(s));
}
ret = pinctrl_select_state(p, s);
if (ret < 0) {
devm_pinctrl_put(p);
return ERR_PTR(ret);
}
驱动程序初始化期间通常执行这些步骤,改代码合适位置可以放在probe函数内。
pinctrl_select_state-> pinmux_enable_setting->pin_request
引脚控制可以通过 pinctrl_put释放,pinctrl_get_select() & pinctrl_get_select_default()
static struct pinctrl *pinctrl_get_select(struct device *dev, const char *name);
static struct pinctrl *pinctrl_get_select_default(struct device *dev);//对pinctrl_get_select的封装
这里支持的四种name
例子:
dcan1: d_can@481d0000 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&d_can1_pins>;
};
相应驱动程序中
pinctrl = devm_pinctrl_get_select_default(&pdev->dev);
if (IS_ERR(pinctrl))
dev_warn(&pdev->dev, "pins are not configured from the driver\n");
探测到设备后,引脚控制核会自动把pinctrl状态声明为default 。如果设置了init状态,
pinctrl和在probe函数之前自动将pinctrl设置为init状态;然后再probe之后,切换到default状态(除非驱动程序已经显式更改状态)
二、GPIO子系统
从硬件角度看:gpio是功能,是引脚可以运行的模式;
看软件角度看:gpio不过是数字线,可以作为输入输出使用;(只能有0,1两种状态值)
内核GPIO子系统提供在驱动程序中可以想象到的设置和处理GPIO线路的所有功能;
在驱动使用GPIO之前,应该向内核声明它。
获得GPIO所有权的一种方式,可以防止其他驱动程序访问相同的GPIO。
获得所有权后,可以如下操作:
设置方向
输出时,切换输出状态;
输入时,设置去抖间隔,读取状态;映射到IRQ的GPIO线,则可以定义触发中断的边沿/电平,注册中断发生时要运行的处理程序。
内核中两种处理GPIO的方式:
旧的建议弃用的基于整数的接口,GPIO由整数表示。
新的推荐使用基于描述符的接口,GPIO由不透明就结构表示和描述,具有专用API。
1.基于整数的GPIO接口:传统方法
头文件 #include <linux/gpio.h>
1.1 声明和配置GPIO
static int gpio_request(unsigned gpio, const char *label);
void gpio_free(unsigned int gpio);
gpio: 表示感兴趣的GPIO号
label: 表示内核用来表示sysfs中GPIO的标签;
正如/sys/kernel/debug/gpio 中看到的那样;
检测gpio是否有效;
static bool gpio_is_valid(int number);
改变方向
static int gpio_direction_input(unsigned gpio);
static int gpio_direction_output(unsigned gpio, int value);
在输入方向时,去抖间隔函数:
static int gpio_set_debounce(unsigned gpio. unsigned debounce);
debounce:以毫秒为单位的去抖时间
以上函数,都应该在可能睡眠的上下文环境中调用。
在驱动probe函数中声明和配置GPIO是一种很好的做法;
1.2 访问 GPIO—获取/设置值
在原子上下文,中断处理函数中,必须确保控制器回调函数不会睡眠。
设计良好的控制器驱动程序应该能通知其他驱动程序调用其方法是否可以睡眠; 可以使用 gpio_cansleep()
函数进行检查。
原子上下文
static int gpio_get_value(unsigned gpio);//
void gpio_set_value(unsigned int gpio, int value);//对应output
非原子上下文
static int gpio_get_value_cansleep(unsigned gpio);
void gpio_set_value_cansleep(unsigned gpio, int value);
区别,他们能阻止内核在访问gpio时,打印警告信息
1.3 映射到IRQ的GPIO
GPIO控制器负责提供GPIO及IRQ之间的映射。
可以使用gpio_to_irq将gpio编号映射到其IRQ号
int gpio_to_irq(unsigned gpio);
返回值: 是irq号
注册中断处理函数
request_irq();
request_threaded_irq();
例子:
static irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
[...]
return IRQ_HANDLED;
}
[...]
int gpio_int = of_get_gpio(np, 0);
int irq_num = gpio_to_irq(gpio_int);
int error = devm_request_threaded_irq(&client->dev,
irq_num,
NULL,
my_interrupt_handler,
IRQF_TRIGGER_RISING | IRQF_ONESHOT,
input_dev->name,
my_data_struct);
if (error) {
dev_err (&client->dev, "irq %d requested failed, %d\n", client->irq, error);
return error;
}
//补充说明下 IRQF_ONESHOT关键字,来自AI说明相关提示
IRQF_ONESHOT 是 Linux 内核中用于中断处理的一个标志(flag),它确实与中断的处理方式有关,但具体含义和效果可能需要根据上下文和内核版本稍作调整。
在 Linux 内核中,中断处理通常分为两部分:上半部(top half)和下半部(bottom half)。上半部由硬件直接触发,执行快速、必要的操作,如保存中断上下文和触发下半部的执行。下半部则负责处理耗时的操作,如读取设备数据或更新系统状态。
当使用 IRQF_ONESHOT 标志注册一个中断处理程序时,它告诉内核这个中断处理程序是“单次触发”的。这意味着,如果当前的中断处理程序(通常是一个线程化的中断处理函数,即 thread_irq)还没有结束,而同一个中断线(IRQ line)上又发生了新的中断,那么这个新的中断将被丢弃,不会被立即处理。换句话说,新的中断不会打断当前正在执行的中断处理程序。
这种机制通常用于那些需要保证原子性或顺序性的中断处理场景,例如,当处理一个复杂设备的中断时,你可能不希望新的中断打断当前的处理流程,因为这可能会导致数据不一致或状态混乱。
然而,需要注意的是,IRQF_ONESHOT 并不完全意味着“不响应”新的中断。相反,它允许中断处理程序在完成当前任务后,通过某种机制(如重新启用中断或提交新的工作项)来响应后续的中断。这通常是通过在中断处理程序的末尾调用 irq_set_pending(irq) 或类似的函数来实现的,这告诉内核该中断线还有未处理的工作,内核可以在适当的时候再次触发中断处理程序。
总之,IRQF_ONESHOT 标志用于确保中断处理的顺序性和原子性,通过丢弃在当前处理程序执行期间发生的新中断来实现这一点。但请注意,这并不意味着新的中断永远不会被处理;它们只是被延迟到当前处理程序完成后才处理。
1.4 小节
前面将的内容做一个代码例程总结
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/gpio.h> /* 遗留的基于整数的GPIO */
#include <linux/interrupt.h>/* IRQ */
static unsigned int GPIO_LED_RED = 49;
static unsigned int GPIO_BTN1 = 115;
static unsigned int GPIO_BTN2 = 116;
static unsigned int GPIO_LED_GREEN = 120;
static unsigned int irq;
static irq_handler_t btn1_pushed_irq_handler(unsigned int irq,
void *dev_id,
struct pt_regs *regs)
{
int state;
/* 读取BTN2值并更改LED状态 */
state = gpio_get_value(GPIO_BTN2);
gpio_set_value(GPIO_LED_RED, state);
gpio_set_value(GPIO_LED_GREEN, state);
pr_info("GPIO_BTN1 interrupt: Interrupt! GPIO_BTN2 state is %d)\n", state);
return IRQ_HANDLED;
}
static int __init helloworld_init(void)
{
int retval;
/**
* 可以检查GPIO在控制器上是否有效
* 使用gpio_is_valid()函数
* 例:
* if (!gpio_is_valid(GPIO_LED_RED)) {
* pr_infor("Invalid Red LED\n");
* return -ENODEV;
* }
*/
gpio_request(GPIO_LED_GREEN, "green-led");
gpio_request(GPIO_LED_RED, "red-led");
gpio_request(GPIO_BTN1, "button-1");
gpio_request(GPIO_BTN2, "button-2");
/*
* 配置按钮 GPIO 作为输入
* 在此之后,只有当控制器具有该特性时才可以调用gpio_set_debounce()
* 例如,取消延迟200ms的按钮
* gpio_set_debounce(GPIO_BTN1, 200);
*/
gpio_direction_input(GPIO_BTN1);
gpio_direction_input(GPIO_BTN2);
/*
* 将 LED GPIO 设置为输出,初始值设置为0
*/
gpio_direction_output(GPIO_LED_RED, 0);
gpio_direction_output(GPIO_LED_GREEN, 0);
irq = gpio_to_irq(GPIO_BTN1);
retval = request_threaded_irq(irq, NULL, \
btn1_pushed_irq_handler, \
IRQF_TRIGGER_LOW | IRQF_ONESHOT, \
"device-name", NULL);
pr_info("Hello world!\n");
return 0;
}
static void __exit helloworld_exit(void)
{
free_irq(irq, NULL);
gpio_free(GPIO_LED_RED);
gpio_free(GPIO_LED_GREEN);
gpio_free(GPIO_BTN1);
gpio_free(GPIO_BTN2);
pr_info("End of the world\n");
}
module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_LICENSE("GPL");
2. 基于描述符的GPIO接口:新的推荐方式
使用struct gpio_desc结构表征GPIO:
struct gpio_desc {
struct gpio_chip *chip;
unsigned long flags;
const char *label;
};
新接口使用下面头文件:
#include <linux/gpio/consumer.h>
使用描述符的接口时,在分配和获取GPIO的所有权之前,必须映射这些GPIO。
所谓映射,指将他们分配给设备。
内核的3种映射:
平台数据映射: 在开发板文件中实现映射;
设备树:以DT风格实现映射;(本书讨论的映射)
高级配置和电源接口映射(ACPI): 以ACPI风格实现映射。通常基于X86的系统上。
2.1 GPIO描述符映射------设备树
GPIO 描述符映射在消费者设备节点中定义;
GPIO描述符映射的属性必须命名为<name>-gpios 或 <name>-gpio , 命名要足以描述这些GPIO的用途
属性命名后缀始终添加 -gpio 或 -gpios, 因为每个基于描述符的接口函数都依赖于drivers/gpio/gpiolib.h
内定义的gpio_suffixes[ ]
变量,
下面的函数用于在DT设备中查找GPIO描述符映射:
static struct gpio_desc *of_find_gpio(struct device *dev,
const char *con_id,
unsigned int idx,
enum gpio_lookup_flags *flags)
{
char prop_name[32]; /* 32是属性名的最大值 */
enum of_gpio_flags of_flags;
struct gpio_desc *desc;
unsigned int i;
for (i = 0; i < ARRAY_SIZEE(gpio_suffixes); i++) {
if (con_id)
snprintf(prop_name, sizeof(prop_name), "%s-%s",
con_id,
gpio_suffixes[i]);
else
snprintf(prop_name, sizeof(prop_name), "%s",
gpio_suffixes[i]);
desc = of_get_named_gpiod_flags(dev->of_node,
prop_name,
idx,
&of_flags);
if (!IS_ERR(desc) || (PTR_ERR(desc) == -EPROBE_DEFER))
break;
}
if (IS_ERR(desc))
return desc;
if (of_flags & OF_GPIO_ACTIVE_LOW)
*flags |= GPIO_ACTIVE_LOW;
return desc;
}
下载考虑下面的节点,改代码摘自 Documentation/gpio/board.txt :
foo_device {
compatible = "acme, foo";
[...]
led-gpios = <&gpio 15 GPIO_ACTIVE_HIGH>, /* 红色 */
<&gpio 16 GPIO_ACTIVE_HIGH>, /* 绿色 */
<&gpio 17 GPIO_ACTIVE_HIGH>; /* 蓝色 */
power-gpios = <&gpio 1 GPIO_ACTIVE_LOW>;
reset-gpios = <&gpio 1 GPIO_ACTIVE_LOW>;
};
补充, 单元2对应的gpio的标记,GPIO_ACTIVE_HIGH 表示高电平有效;当gpiod_get_value时,实际为高电平1,则值为1;
GPIO_ACTIVE_LOW 低电平有效,gpiod_get_value返回1;
这样映射命名的名字,其名称的意义显而易见;
2.2 分配和使用GPIO
分配
gpiod_get(), gpiod_get_index() 可以分配GPIO描述符:
struct gpio_desc *gpiod_get_index(struct device *dev,
const char *con_id,
unsigned int idx,
enum gpiod_flags flags);//可选参数,用于确定GPIO初始化标志,以配置方向/或输出值.
struct gpio_desc *gpiod_get(struct device *dev,
const char *con_id,//对应DT中描述符的前缀<name>
enum gpiod_flags flags);//返回idx = 0的GPIO描述符
出错:返回-ENOENT;
IS_ERR()宏可检测;
include/linux/gpio/consumer.h
中定义的enum gpiod_flags
的实例
enum gpiod_flags {
GPIOD_ASIS = 0,
GPIOD_IN = GPIOD_FLAGS_BIT_DIR_SET,
GPIOD_OUT_LOW = GPIOD_FLAGS_BIT_DIR_SET |
GPIOD_FLAGS_BIT_DIR_OUT,
GPIOD_OUT_HIGH = GPIOD_FLAGS_BIT_DIR_SET |
GPIOD_FLAGS_BIT_DIR_OUT |
GPIOD_FLAGS_BIT_DIR_VAL,
};
按照之前2.1小节DT中映射分配的GPIO描述符:
struct gpio_desc *red, *green, *blue, *power;
red = gpiod_get_index(dev, "led", 0, GPIOD_OUT_HIGH);
green = gpiod_get_index(dev, "led", 1, GPIOD_OUT_HIGH);
blue = gpiod_get_index(dev, "led", 2, GPIOD_OUT_HIGH);
power = gpiod_get(dev, "power", GPIOD_OUT_HIGH);
分配的反方向gpiod_put()函数
void gpiod_put(struct gpio_desc *desc)
释放red和 blue 的GPIO LED:
gpiod_put(blue);
gpiod_put(red);
相关接口
gpio_request()
gpio_free()
与
gpiod_get()
gpiod_get_index();
gpiod_put()
完全不同接口外:只需要将gpio_前缀改成gpiod_即可实现从基于整数接口到基于描述符接口的API转换。
int gpiod_direction_input(struct gpio_desc *desc);
int gpiod_direction_output(struct gpio_desc *desc, int value);
value是设置为输出时,应用于GPIO的状态。
输入模式时的去抖超时值:
int gpiod_set_debounce(struct gpio_desc *desc, unsigned debounce);
使用描述符的gpio 必须像 整数的gpio 一样注意,换句话说,应该注意是处于原子上下文(不能睡眠),还是非原子上下文中,然后使用相应的函数:
int gpiod_cansleep(const struct gpio_desc *desc);
/* 值get/set 来自睡眠上下文 */
int gpiod_set_value_cansleep(const struct gpio_desc *desc);
void gpiod_set_value_cansleep(struct gpio_desc *desc, int value);
/* 值get/set来自非睡眠上下文 */
int gpiod_get_value(const struct gpio_desc *desc);
void gpiod_set_value(struct gpio_desc *desc, int value);
desc 映射到 IRQ 的中断号,request_irq():
int gpiod_to_irq(const struct gpio_desc *desc);
desc 与 整数gpio 之间的切换,任何给定时间内都可
/* 在旧的gpio_和新的gpiod_接口之间进行转换 */
struct gpio_desc *gpio_to_desc(unsigned gpio);
int desc_to_gpio(const struct gpio_desc *desc);
2.3 小节
基于desc描述符的GPIO例程
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h> /* platform设备 */
#include <linux/gpio/consumer.h> /* GPIO描述符 */
#include <linux/interrupt.h> /* IRQ */
#include <linux/of.h> /* DT */
/*
* 考虑设备树中的一下映射:
* foo_device {
* compatible = "packt, gpio-descriptor-sample";
* led-gpios = <&gpio2 15 GPIO_ACTIVE_HIGH>, //红色
* <&gpio2 15 GPIO_ACTIVE_HIGH>; //蓝色
* btn1-gpios = <&gpio2 1 GPIO_ACTIVE_LOW>,
* <&gpio2 31 GPIO_ACTIVE_LOW>;
* };
*/
static struct gpio_desc *red, *green, *btn1, *btn2;
static unsigned int irq;
static irq_handler_t btn1_pushed_irq_handler(unsigned int irq,
void *dev_id,
struct pt_regs *regs)
{
int state;
/* 读取按钮值并更改LED状态 */
state = gpiod_get_value(btn2);
gpiod_set_value(red, state);
gpiod_set_value(green, state);
pr_info("btn1 interrupt: Interrupt! btn2 state is %d) \n", state);
return IRQ_HANDLED;
}
static const struct of_device_id gpiod_dt_ids[] = {
{ .compatible = "packt, gpio-descriptor-sample", },
{ /* 哨兵 */}
};
static int my_pdrv_probe (struct platform_device *pdev)
{
int retval;
struct device *dev = &pdev->dev;
/*
* 考虑设备树中的一下映射:
* foo_device {
* compatible = "packt, gpio-descriptor-sample";
* led-gpios = <&gpio2 15 GPIO_ACTIVE_HIGH>, //红色
* <&gpio2 15 GPIO_ACTIVE_HIGH>; //蓝色
* btn1-gpios = <&gpio2 1 GPIO_ACTIVE_LOW>,
* <&gpio2 31 GPIO_ACTIVE_LOW>;
* };
*/
red = gpiod_get_index(dev, "led", 0, GPIOD_OUT_LOW);
green = gpiod_get_index(dev, "led", 1, GPIOD_OUT_LOW);
/*
* 将GPIO按钮配置为输入
*
* 在此之后,只有当控制器具有该特性时,才可以调用
* gpiod_set_debounce()
* 例如,取消延迟200ms的按钮
* gpiod_set_debounce(btn1, 200);
*
*/
btn1 = gpiod_get_index(dev, "led", 0, GPIOD_IN);
btn2 = gpiod_get_index(dev, "led", 1, GPIOD_IN);
irq = gpiod_to_irq(btn1);
retval = request_threaded_irq(irq,
NULL,
btn1_pushed_irq_handler,
IRQF_TRIGGER_LOW | IRQF_ONESHOT,
"gpio-descriptor-sample",
NULL);
pr_info("Hello! device probed!\n");
return 0;
}
static void my_pdrv_remove(struct platform_device *pdev)
{
free_irq(irq, NULL);
gpiod_put(red);
gpiod_put(green);
gpiod_put(btn1);
gpiod_put(btn2);
pr_info("good bye reader!\n");
}
static struct platform_driver mypdrv = {
.probe = my_pdrv_probe,
.remove = my_pdrv_remove,
.driver = {
.name = "gpio_descriptor_sample",
.of_match_table = of_match_ptr(gpiod_dt_ids),
.owner = THIS_MODULE,
},
};
module_platform_driver(mypdrv);
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_LICENSE("GPL");
3. GPIO接口和设备树
无论使用GPIO什么接口,如何指定GPIO取决于它们的控制器,尤其是#gpio-cells属性
"#gpio-cells属性",指定了GPIO说明符的单元数。
gpio说明符,至少需要一个控制句柄 + 一个或多个参数
控制器句柄是phandle
一个或多个参数:第一个参数,控制器的gpio偏移号、一个是表示GPIO标志。
这里的极性,表现为gpiod_get_value的返回值的逻辑;
对于数字GPIO,描述为[<name> - ]gpios;
<name> 对应具体的用途
对于描述符GPIO,描述为<name>-gpios
没有[ ]括号,表示name是必须的
gpio1: gpio1 {
gpio-controller;
#gpio-cells = <2>;
};
gpio2: gpio2 {
gpio-controller;
#gpio-cells = <1>;
};
[...]
cs-gpios = <&gpio1 17 0>,
<&gpio2 2>,
<0>, /* 空是允许的,意味着没有GPIO 2 */
<&gpio1 17 0>;
reset-gpios = <&gpio1 30 0>;
cd-gpios = <&gpio2 10>;
3.1 传统的基于整数的接口和设备树
依赖头文件 #include <linux/of_gpio.h>
// 返回设备节点中index对应的gpio数字号
int of_get_named_gpio(struct device_node *np,
const char *propname,
int index);
// 返回节点中propname对应的gpio count数
int of_fet_named_gpio_count(struct device_node *np,
const char* propname);
例子
int n_gpios = of_get_named_gpio_count(dev.of_node, "cs-gpios"); /*return 4*/
int second_gpio = of_get_named_gpio(dev.of_node, "cs-gpio", 1);
int rst_gpio = of_get_named_gpio(dev.of_node, "reset-gpio", 0);
gpio_request(second_gpio, "my-gpio");
有些程序仍然支持旧的说明符,没有[<name> - ] 前缀 或 gpios, 这种情况下使用of_get_gpio()和of_gpio_count()来使用未知名的gpio;
static inline int of_get_gpio(struct device_node *np, int index);
static inline int of_gpio_count(struct device_node *np);
DT 节点像下面这样:
my_node@addr {
compatible = "[...]";
gpios = <&gpio1 2 0>, /* INT */
<&gpio1 5 0>; /* RST */
[...]
};
对应代码(旧的GPIO整数方式)
struct device_node *np = dev->of_node;
if (!np)
return ERR_PTR(-ENOENT);
int n_gpios = of_gpio_count(np); /* 将返回2 */
int gpio_int = of_get_gpio(np, 0);
if (!gpio_is_valid(gpio_int)) {
dev_err(dev, "failed to get interrupt gpio\n");
return ERR_PTR(-EINVAL);
}
gpio_rst = of_get_gpio(np, 1);
if (!gpio_is_valid(pdata->gpio_rst)) {
dev_err(dev, "failed to get reset gpio\n");
return ERR_PTR(-EINVAL);
}
基于整数的of接口(设备树中gpio的描述符有新的[<name> - ] 前缀 , 这里重新的代码是基于有前缀的整数gpio的of接口),重写之前14.2.2.3的代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h> /* platform设备 */
#include <linux/gpio/gpio.h> /* 基于的基于整数的GPIO */
#include <linux/interrupt.h> /* IRQ */
#include <linux/of_gpio.h> /* of_gpio* 函数 */
#include <linux/of.h> /* DT */
/*
* 考虑设备树中的一下映射:
* foo_device {
* compatible = "packt, gpio-descriptor-sample";
* led-gpios = <&gpio2 15 GPIO_ACTIVE_HIGH>, //红色
* <&gpio2 15 GPIO_ACTIVE_HIGH>; //蓝色
* btn1-gpios = <&gpio2 1 GPIO_ACTIVE_LOW>,
* <&gpio2 31 GPIO_ACTIVE_LOW>;
* };
*/
static int gpio_red, gpio_green, gpio_btn1, gpio_btn2;
static unsigned int irq;
static irq_handler_t btn1_pushed_irq_handler(unsigned int irq,
void *dev_id,
struct pt_regs *regs)
{
int state;
/* 读取按钮值并更改LED状态 */
state = gpiod_get_value(btn2);
gpiod_set_value(red, state);
gpiod_set_value(green, state);
pr_info("btn1 interrupt: Interrupt! btn2 state is %d) \n", state);
return IRQ_HANDLED;
}
static const struct of_device_id gpio_dt_ids[] = {
{ .compatible = "packt, gpio-legacy-sample", },
{ /* 哨兵 */}
};
static int my_pdrv_probe (struct platform_device *pdev)
{
int retval;
struct device *dev = &pdev->dev;
struct device_node *np = &pdev->dev.of_node;
if (!np)
return ERR_PTR(-ENOENT);
gpio_red = of_get_named_gpio(np, "led", 0);
gpio_green = of_get_named_gpio(np, "led", 1);
gpio_btn1 = of_get_named_gpio(np, "btn1", 0);
gpio_btn2 = of_get_named_gpio(np, "btn2", 0);
gpio_request(gpio_green, "green-led");
gpio_request(gpio_red, "red-led");
gpio_request(gpio_btn1, "button-1");
gpio_request(gpio_btn2, "button-2");
irq = gpio_to_irq(gpio_btn1);
retval = request_threaded_irq(irq,
NULL,
btn1_pushed_irq_handler,
IRQF_TRIGGER_LOW | IRQF_ONESHOT,
"gpio-legacy-sample",
NULL);
pr_info("Hello! device probed!\n");
return 0;
}
static void my_pdrv_remove(struct platform_device *pdev)
{
free_irq(irq, NULL);
gpiod_put(red);
gpiod_put(green);
gpiod_put(btn1);
gpiod_put(btn2);
pr_info("good bye reader!\n");
}
static struct platform_driver mypdrv = {
.probe = my_pdrv_probe,
.remove = my_pdrv_remove,
.driver = {
.name = "gpio-legacy-sample",
.of_match_table = of_match_ptr(gpiod_dt_ids),
.owner = THIS_MODULE,
},
};
module_platform_driver(mypdrv);
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_LICENSE("GPL");
3.2 设备树内将GPIO映射到IRQ
在设备树内,可将GPIO映射到IRQ,需要两个属性:
interrupt-parent: GPIO的GPIO控制器
interrupts: 中断说明符列表
这里适用于传统数字GPIO与描述符GPIO;
IRQ说明符取决于GPIO的GPIO控制器的#interrupt-cell属性。
该属性指定了中断时使用的单元数。
第一个单元: 映射到IRQ的GPIO编号
第二个单元: 触发中断的电平/边沿
任何情况下,中断说明符,总是依赖于器父结点(设置中断控制器的结点)
gpio4: gpio4 {
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
my_label: node@0 {
reg = <0>;
spi-max-frequency = <1000000>;
interrupt-parent = <&gpio4>;
interrupts = <29 IRQ_TYPE_LEVEL_LOW>;
};
有两种解决方案可以获得相应的IRQ:
设备位于已知总线(IIC或SPI)上: IRQ映射将完成它; 并通过probe函数提供struct i2c_client 或
struct spi_device结构来获得(client.irq 、spi_device.irq)。
设备位于伪平台总线上:将向probe函数提供 struct platform_device. 在其上可以调用platform_get_irq()获得;
int platform_get_irq(struct platform_device *dev, unsigned int num);
4. GPIO 和 sysfs
sysfs GPIO 接口能够通过集合或文件管理来控制GPIO;他位于/sys/class/gpio目录下;
这里大量使用设备模型,其中有3项可用:
/sys/class/gpio这是一切之源;
/sys/class/gpio/下两个特殊文件export、unexport
export:将指定的GPIO编号写入export,就可以让内核将GPIO的控制导出到用户空间;
echo 21 > export
unexport: echo 21 > export 将删除用export导出的所有GPIO21节点;
/sys/class/gpio/gpioN: 进入对应的gpioN文件夹,有一下特性
direction: 方向特性
echo in > direction
echo out > direction// 默认是low电平
//这里对应gpio_export、gpiod_export函数,详情可参考源码
value: 根据方向来获取或设置GPIO线的状态;
out状态时:写入任何非0值,被认为输出High电平设置
edge: none、rising、falling、both四种赋值; (rw),只有当引脚配置为终端产生输入引脚时才存在;
active_low: 0 假,1真;写入任何非0值,将反转读取和写入的value的属性; (相关函数gpio_sysfs_set_active_low())
4.1 从内核代码中导出GPIO
除开使用/sys/class/gpio/export文件将GPIO导出到用户空间之外,还可以使用来自内核代码的gpio_export 或 gpiod_export 来导出已经获得权限的gpio(gpio_request 或 gpiod_get()的gpio),将其导出
int gpio_export(unsigned gpio, bool direction_may_change);
int gpiod_export(struct gpio_desc *desc, bool direction_may_change);
direction_may_change 表示,是否可以更改导出gpio的方向;
void gpio_unexport(unsigned gpio); /* Integer-based 接口*/
void gpiod_unexport(struct gpio_desc *desc); /* Descriptor-based */
使用gpio_export_link() gpiod_export_link() 从sysfs 中的其它位置创建符号链接,这些链接将指向GPIO sysfs节点。
驱动程序可以为sysfs内设备下的接口提供描述性的名称:
int gpio_export_link(struct device *dev,
const char *name,
unsigned gpio);
int gpiod_export_link(struct device *dev,
const char *name,
struct gpio_desc *desc);
对于基于描述符的接口,可以在probe内使用它:
static struct gpio_desc *red, *green, *btn1, *btn2;
static int my_pdrv_probe (struct platform_device *pdev)
{
[...]
red = gpiod_get_index(dev, "led", 0, GPIOD_OUT_LOW);
green = gpiod_get_index(dev, "led", 1, GPIOD_OUT_LOW);
gpiod_export(green , false);
gpiod_export(red , false);
gpio_export_link(&pdev->dev, "Green_LED", green);
gpio_export_link(&pdev->dev, "Red_LED", red);
[...]
return 0;
}
对基于整数的接口
static int gpio_red, gpio_green, gpio_btn1, gpio_btn2;
static int my_pdrv_probe (struct platform_device *pdev)
{
[...]
gpio_red = of_get_named_gpio(np, "led", 0);
gpio_green = of_get_named_gpio(np, "led", 1);
gpio_request(gpio_red, "Red_LED");
gpio_request(gpio_green, "Green_LED");
gpio_export(gpio_red, false);
gpio_export(gpio_red, false);
gpio_export_link(&pdev->dev, "Green_LED", gpio_green );
gpio_export_link(&pdev->dev, "Red_LED", gpio_red );
[...]
return 0;
}
三、总结
介绍了内核中处理GPIO的简单任务,讨论了传统整数接口和新的描述符接口,以及选择适合需求的接口,编写一个功能增强的GPIO驱动程序。
同时介绍了映射到GPIO的IRQ。
对于GPIO 和 sysfs 接口,将在以后继续讨论相关内容。