最近处理es8336声卡问题,最后排查是spk_ctl_gpio和hp_det_gpio这两个gpio导致的,所以恶补了一下gpio相关的知识,现在总结一下。
源代码使用的是飞腾的gitee上开源的内核:https://gitee.com/phytium_embedded/phytium-linux-kernel.git
1. 概述
-
设备驱动层:定义了与硬件无关的GPIO API,包括GPIO的注册、卸载和控制等功能,而实现了某个模块的具体实现,比如led灯、按键等等。
-
gpiolib抽象层:GPIO框架中的核心抽象层,它的作用是为设备驱动层和控制器层提供一致的接口,该层提供了包括上层设备驱动和下层控制器驱动的API接口。
-
控制器层:GPIO控制器的实现和管理,在该层中实现特定GPIO控制器的底层硬件操作和功能实现包括GPIO控制器的初始化、操作和管理等。负责GPIO寄存器的读写操作和GPIO中断的处理等。
其中gpiolib抽象层是GPIO框架中的核心层,也是linux内核自己实现的,一般情况下没有人会修改这部分的代码,控制器层一般是芯片厂家BSP工程师实现的,设备驱动层是驱动工程师根据开发版的实际情况实现的。优秀的BSP工程师和驱动工程师可以把驱动写得与硬件解耦,把硬件的信息填充到设备树中,驱动读取设备树的信息进行各种操作。
2. 控制器
这里以飞腾e2000为例子看看gpio控制器的驱动是怎么样的,在文件drivers/gpio/gpio-phytium-platform.c中,但是gpio的操作函数写在drivers/gpio/gpio-phytium-core.c和drivers/gpio/gpio-phytium-core.h中。
2.1 设备树
gpio0: gpio@28034000 {
compatible = "phytium,gpio";
reg = <0x0 0x28034000 0x0 0x1000>;
interrupts = <GIC_SPI 108 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 109 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 110 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 111 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 112 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 113 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 114 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 115 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 116 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 117 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 118 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 119 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 120 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 121 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 122 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 123 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller;
#gpio-cells = <2>;
#address-cells = <1>;
#size-cells = <0>;
porta {
compatible = "phytium,gpio-port";
reg = <0>;
ngpios = <16>;
};
};
gpio3: gpio@28037000 {
compatible = "phytium,gpio";
reg = <0x0 0x28037000 0x0 0x1000>;
interrupts = <GIC_SPI 156 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller;
#gpio-cells = <2>;
#address-cells = <1>;
#size-cells = <0>;
status = "disabled";
porta {
compatible = "phytium,gpio-port";
reg = <0>;
ngpios = <16>;
};
};
这里是gpio0和gpio3这个两个gpio模块,其实e2000有0到5,一共6个gpio模块,每个模块都有16个gpio,其中gpio0-2每个gpio都有一个硬件中断号,而gpio3-5则是这个组共享一个硬件中断号。当然,我看其他arm64的gpio都有复用功能,所以gpio的设备树写在pinctrl里面,现在e2000的gpio没有复用到其他模块,所以没有使用pinctrl。
2.2 驱动的数据结构
以我多年的驱动经验来看,所有的gpio控制器驱动做的事情不外乎以下几种:
- 分配私有数据结构体,这个结构体会包含struct gpio_chip结构体
- 读取设备树信息,填充gpio_chip结构体
- 把gpio_chip结构体绑定到平台设备中
实际上gpio的驱动跟中断和pinctrl放在一起的,初始化的时候会把irq_chip和 struct pinctrl_desc 和pinctrl_dev 一起填充的,这里主要讲解gpio,就不多扩展了。我们先看struct gpio_chip结构体:
struct gpio_chip {
const char *label; //gpio控制器的名字
struct gpio_device *gpiodev; //gpio设备描述符
struct device *parent; //gpio的父设备
struct module *owner;
//下面是一系列的操作函数
int (*get_direction)(struct gpio_chip *gc,unsigned int offset);
int (*direction_input)(struct gpio_chip *gc,unsigned int offset);
int (*direction_output)(struct gpio_chip *gc,unsigned int offset, int value);
int (*get)(struct gpio_chip *gc,unsigned int offset);
void (*set)(struct gpio_chip *gc,unsigned int offset, int value);
int base; //gpio引脚基值
u16 ngpio; //gpio引脚个数
const char *const *names; //每一个引脚的名字
bool can_sleep; //控制器是否能睡眠
void __iomem *reg_dat; //gpio数据寄存器基地址
void __iomem *reg_set; //gpio设置寄存器基地址
void __iomem *reg_clr; //gpio控制寄存器基地址
void __iomem *reg_dir_out; //gpio输出寄存器基地址
void __iomem *reg_dir_in; //gpio输入寄存器基地址
};
gpio_chip结构体中包含了对应GPIO端口的硬件基地址、引脚数、GPIO组数、GPIO编号和IRQ号等重要信息。同时,它还包括了访问GPIO寄存器的函数指针,例如读取和写入寄存器等函数,以及描述GPIO的信息和配置的特定标志。其中最重要的是struct gpio_device *gpiodev; 他表示gpio设备描述符。这个结构体存放着gpio控制器的设备信息和每个引脚的描述符结构体,我们来看看struct gpio_device结构体:
struct gpio_device {
int id; //表示这是系统中第几个GPIO控制器
struct device dev;
struct cdev chrdev;
struct device *mockdev;
struct module *owner;
struct gpio_chip *chip; //gpio管理硬件的结构体,记录寄存器信息及其操作函数
struct gpio_desc *descs; //引脚的描述符指针,每一个引脚对应一个gpio_desc结构体
int base; //gpio号码基值
u16 ngpio; //gpio个数
const char *label; //gpio控制器的名字
void *data;
struct list_head list;
struct blocking_notifier_head notifier;
};
gpio_device结构体中包含了与GPIO设备(GPIO控制器)相关的重要信息,例如GPIO控制器ID、GPIO号、GPIO个数、GPIO管理硬件的结构体和gpio引脚描述符结构体。最重要的是GPIO管理硬件的结构体gpio_chip 就是刚刚第一个介绍的数据结构,gpio设备是通过gpio_chip 找到对用的硬件信息和操作方法的;其次是gpio_desc结构体,每一个引脚对应一个gpio_desc结构体,他们通过数组的形式排列,我们看看gpio_desc结构体:
struct gpio_desc {
struct gpio_device *gdev; //属于哪个GPIO控制器
unsigned long flags; //gpio引脚属性,比如是否被使用、是否开漏等等
/* Connection label */
const char *label;
/* Name of the GPIO */
const char *name; //引脚名字
#ifdef CONFIG_OF_DYNAMIC
struct device_node *hog;
#endif
#ifdef CONFIG_GPIO_CDEV
/* debounce period in microseconds */
unsigned int debounce_period_us;
#endif
};
gpio_desc结构体主要记录GPIO引脚的硬件信息和状态信息。
这3个结构体的关系如上图所示,以上这3个结构体就包含了gpio控制器的硬件信息、状态信息和操作方法集合,这就我们的设备驱动提供了底层的基础,我们的驱动就是通过gpiolib抽象层提供的API调用到这个操作方法的。
3. 设备驱动
在控制器驱动准备好的情况下,我们使用gpio是一件很简单的事情,就拿es8336这个网卡驱动举个例子,先看设备树:
mio14: i2c@28030000 {
...
codec0:es8336@10 {
det-gpios = <&gpio2 5 0>;
sel-gpios = <&gpio2 6 0>;
...
};
};
其中gpio的设备树就两行,其他不重要的就忽略了。
驱动文件在sound/soc/codecs/es8336.c:
es8336->spk_ctl_gpio = devm_gpiod_get_index_optional(&i2c->dev, "sel", 0,
GPIOD_OUT_HIGH);
ret = of_property_read_u8(i2c->dev.of_node, "mic-src", &es8336->mic_src);
if (ret != 0) {
dev_dbg(&i2c->dev, "mic1-src return %d", ret);
es8336->mic_src = 0x20;
}
dev_dbg(&i2c->dev, "mic1-src %x", es8336->mic_src);
if (!es8336->spk_ctl_gpio)
dev_info(&i2c->dev, "Can not get spk_ctl_gpio\n");
else
es8336_enable_spk(es8336, false);
es8336->hp_det_gpio = devm_gpiod_get_index_optional(&i2c->dev, "det", 0,
GPIOD_IN);
if (!es8336->hp_det_gpio) {
dev_info(&i2c->dev, "Can not get hp_det_gpio\n");
} else {
INIT_DELAYED_WORK(&es8336->work, hp_work);
hp_irq = gpiod_to_irq(es8336->hp_det_gpio);
ret = devm_request_threaded_irq(&i2c->dev, hp_irq, NULL,
es8336_irq_handler,
IRQF_TRIGGER_FALLING |
IRQF_TRIGGER_RISING |
IRQF_ONESHOT,
"es8336_interrupt", es8336);
if (ret < 0) {
dev_err(&i2c->dev, "request_irq failed: %d\n", ret);
return ret;
}
}
其实设备驱动都是使用gpiolib提供的API:
- gpiod_get_indexed:通过索引号获取GPIO设备引脚。
- gpiod_get_optional:尝试获取GPIO设备引脚,如果失败返回NULL,不会导致注册失败。
- gpiod_get_optional_indexed:与gpiod_get_optional类似,但通过索引号获取GPIO设备引脚。
- gpiod_get_raw:通过GPIO编号获取GPIO设备引脚,不进行方向和值的配置。
- devm_gpiod_get:这个函数自动为一个特定的设备申请所需的GPIO,不需要手动释放,适合临时使用的GPIO资源。
- devm_gpiod_get_index:这个函数允许为使用多个GPIO的设备申请多个GPIO引脚,返回一个struct gpiod_hanlde数组。
- devm_gpiod_get_optional:如果存在,允许驱动程序获取所需的GPIO,而不会阻止设备与其余的GPIO资源一起初始化。
- devm_gpiod_get_optional_index:类似于devm_gpiod_get_optional,支持索引GPIO。
- gpio_request:向内核申请一个GPIO引脚。
- gpio_free:释放一个已经使用的GPIO引脚。
- gpio_direction_input:配置GPIO引脚为输入模式。
- gpio_direction_output:配置GPIO引脚为输出模式。
- gpio_get_value:读取GPIO引脚状态。如果 GPIO 引脚已配置为输出模式,则返回当前输出值。如果 GPIO 引脚未配置或配置为输入模式,则返回实际引脚上的输入值。
- gpio_set_value:设置GPIO引脚状态为高或低电平。如果 GPIO 引脚已配置为输出模式,则设置GPIO引脚状态为用户指定电平;如果 GPIO 引脚是输入模式,此函数没有作用。
- gpio_to_irq:将 GPIO 引脚转换为专用中断号。
- gpio_request_one:请求单个GPIO。
- gpio_free_array:释放一组由gpio_request_array()调用请求的GPIO。
- gpio_direction_input_array:将一组GPIO方向设置为输入模式。
- gpio_direction_output_array:将一组GPIO方向设置为输出模式。
- gpio_get_array:将一组GPIO值读入缓冲区中。
- gpio_set_array:将一组GPIO指定的值写入用户指定的缓冲区中。
- gpio_get_value_cansleep:读取GPIO引脚状态,如果引脚已配置为输出模式,则将与其关联的电平值复制到调用函数的参数变量中,如果引脚已配置为输入模式,则等待GPIO中断或超时发生后将其值复制到调用函数的参数变量中。
- gpio_set_value_cansleep:设置GPIO端口电平,如果引脚仍被配置为输入模式,则什么也不做。