第四章 GPIO驱动开发:从寄存器到用户态的全链路实战
GPIO,说白了就是芯片最基础的“手脚”。拉高拉低,检测中断,控制外设,样样离不开它。我在海思平台上做过不下几十个GPIO相关的驱动,从简单的LED闪烁到复杂的按键中断唤醒,踩过的坑还真不少。今天咱们就把GPIO驱动开发的完整链路捋一遍。
4.1 GPIO控制器寄存器映射
海思芯片的GPIO控制器,通常挂在APB总线上。每个GPIO bank管理8个或16个引脚。我个人习惯,拿到芯片手册第一件事就是看寄存器基地址和偏移。
典型的GPIO寄存器包括:
- SW_PORT_DR:数据寄存器,写1输出高,写0输出低
- SW_PORT_DDR:方向寄存器,1为输出,0为输入
- EXT_PORT:外部引脚状态寄存器,读引脚实际电平
- INTEN:中断使能寄存器
- INT_TYPE:中断触发类型(电平/边沿)
- INT_POLARITY:中断极性(高/低,上升/下降)
- INT_STATUS:中断状态寄存器,写1清中断
嗯,这里要注意:海思的GPIO寄存器地址映射,不同芯片系列可能不一样。比如Hi3516和Hi3559的基地址就不同。我建议你写驱动时,把基地址定义成宏,方便移植。
/* 以Hi3516DV300为例 */
#define GPIO0_BASE 0x120D0000
#define GPIO1_BASE 0x120D1000
#define GPIO2_BASE 0x120D2000
#define GPIO_SW_PORT_DR 0x0000
#define GPIO_SW_PORT_DDR 0x0004
#define GPIO_EXT_PORT 0x0040
#define GPIO_INTEN 0x0070
#define GPIO_INT_TYPE 0x0074
#define GPIO_INT_POLARITY 0x0078
#define GPIO_INT_STATUS 0x007C
/* 寄存器映射 */
static void __iomem *gpio_base;
gpio_base = ioremap(GPIO0_BASE, 0x1000);
4.2 通用GPIO操作接口
内核提供了标准的GPIO操作接口,说白了就是gpiolib那一套。但海思平台有些特殊需求,比如快速翻转、批量操作,这时候我会自己封装底层函数。
4.2.1 高低电平控制
/* 设置GPIO输出高电平 */
static inline void gpio_set_high(unsigned int gpio)
{
unsigned int bank = gpio / 8;
unsigned int pin = gpio % 8;
void __iomem *base = gpio_bases[bank];
writel(readl(base + GPIO_SW_PORT_DR) | (1 << pin),
base + GPIO_SW_PORT_DR);
}
/* 设置GPIO输出低电平 */
static inline void gpio_set_low(unsigned int gpio)
{
unsigned int bank = gpio / 8;
unsigned int pin = gpio % 8;
void __iomem *base = gpio_bases[bank];
writel(readl(base + GPIO_SW_PORT_DR) & ~(1 << pin),
base + GPIO_SW_PORT_DR);
}
/* 读取GPIO电平 */
static inline int gpio_get_value(unsigned int gpio)
{
unsigned int bank = gpio / 8;
unsigned int pin = gpio % 8;
void __iomem *base = gpio_bases[bank];
return (readl(base + GPIO_EXT_PORT) >> pin) & 0x1;
}
你想想看,为什么读引脚要用EXT_PORT而不是SW_PORT_DR?因为SW_PORT_DR是你写进去的值,而EXT_PORT才是引脚实际的电平。我见过有人读SW_PORT_DR来判断外部电平,结果死活读不对。
4.2.2 中断配置
GPIO中断是驱动开发的重点。海思的GPIO中断控制器支持多种触发方式。我个人习惯把中断配置封装成一个函数,参数清晰明了。
/* GPIO中断配置结构体 */
struct gpio_irq_config {
unsigned int gpio;
int trigger; /* 0: 低电平, 1: 高电平, 2: 下降沿, 3: 上升沿, 4: 双边沿 */
int enable;
};
/* 配置GPIO中断 */
static int gpio_irq_configure(struct gpio_irq_config *cfg)
{
unsigned int bank = cfg->gpio / 8;
unsigned int pin = cfg->gpio % 8;
void __iomem *base = gpio_bases[bank];
u32 val;
/* 设置触发类型:0-电平,1-边沿 */
val = readl(base + GPIO_INT_TYPE);
if (cfg->trigger & 0x2) /* 边沿触发 */
val |= (1 << pin);
else
val &= ~(1 << pin);
writel(val, base + GPIO_INT_TYPE);
/* 设置极性 */
val = readl(base + GPIO_INT_POLARITY);
if (cfg->trigger & 0x1) /* 高电平/上升沿 */
val |= (1 << pin);
else
val &= ~(1 << pin);
writel(val, base + GPIO_INT_POLARITY);
/* 使能/禁能中断 */
val = readl(base + GPIO_INTEN);
if (cfg->enable)
val |= (1 << pin);
else
val &= ~(1 << pin);
writel(val, base + GPIO_INTEN);
return 0;
}
4.3 设备树GPIO节点配置
海思平台现在都用设备树来描述硬件。GPIO节点的配置,说白了就是告诉内核:哪个引脚、什么功能、要不要上拉。
/* 设备树GPIO控制器节点 */
gpio0: gpio@120d0000 {
compatible = "hisilicon,hi3516-gpio";
reg = <0x120d0000 0x1000>;
interrupts = <0 64 4>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
/* 用户设备使用GPIO */
my_device {
compatible = "my-company,my-device";
/* 使用GPIO0_3作为输出,控制LED */
led-gpios = <&gpio0 3 GPIO_ACTIVE_HIGH>;
/* 使用GPIO1_5作为输入中断,检测按键 */
key-gpios = <&gpio1 5 GPIO_ACTIVE_LOW>;
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
};
嗯,这里要注意#gpio-cells = <2>的含义。第一个cell是引脚号,第二个cell是标志位(比如GPIO_ACTIVE_HIGH/LOW)。我在项目中遇到过,有人把cells设成3,结果解析出来的GPIO号全乱了。
4.4 用户态GPIO控制
有时候我们不想写内核驱动,直接在用户态控制GPIO。海思平台支持两种方式:老的sysfs接口和新的gpiolib字符设备接口。
4.4.1 sysfs接口(传统方式)
sysfs接口操作简单,但效率低。我一般用来调试,正式产品很少用。
# 导出GPIO
echo 24 > /sys/class/gpio/export
# 设置方向
echo out > /sys/class/gpio/gpio24/direction
# 输出高电平
echo 1 > /sys/class/gpio/gpio24/value
# 读取电平
cat /sys/class/gpio/gpio24/value
# 释放GPIO
echo 24 > /sys/class/gpio/unexport
4.4.2 gpiolib字符设备接口(推荐方式)
gpiolib字符设备接口是内核推荐的方式。它通过ioctl操作,支持批量读写、事件监听,性能好很多。
#include <linux/gpio.h>
#include <sys/ioctl.h>
#include <fcntl.h>
int gpio_control(int gpio, int value)
{
struct gpiohandle_request req;
struct gpiohandle_data data;
int fd, ret;
/* 打开GPIO字符设备 */
fd = open("/dev/gpiochip0", O_RDWR);
if (fd < 0) {
perror("open gpiochip0");
return -1;
}
/* 请求GPIO为输出 */
req.flags = GPIOHANDLE_REQUEST_OUTPUT;
req.lines = 1;
req.lineoffsets[0] = gpio;
req.default_values[0] = value;
strcpy(req.consumer_label, "my-app");
ret = ioctl(fd, GPIO_GET_LINEHANDLE_IOCTL, &req);
if (ret < 0) {
perror("ioctl get line handle");
close(fd);
return -1;
}
/* 设置输出值 */
data.values[0] = value;
ret = ioctl(req.fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
if (ret < 0) {
perror("ioctl set line values");
}
close(req.fd);
close(fd);
return ret;
}
你想想看,为什么gpiolib接口比sysfs好?因为它支持事件监听。比如你要检测按键按下,用sysfs你得轮询,用gpiolib可以注册事件,有中断才通知你,CPU占用率低很多。
4.5 实战经验总结
做GPIO驱动开发,我总结了几个要点:
- 寄存器操作要加锁:多核CPU下,不加锁会导致寄存器读写错乱。我习惯用spinlock保护寄存器操作。
- 中断服务要快:GPIO中断服务函数里,只做最必要的操作。复杂逻辑放到工作队列或tasklet里。
- 设备树要规范:GPIO的命名、极性、中断类型,都要在设备树里明确。别在驱动里硬编码。
- 用户态接口选型:调试用sysfs,产品用gpiolib字符设备。
核心要点:GPIO驱动看似简单,但涉及寄存器映射、中断处理、设备树配置、用户态接口等多个层面。每个环节都有坑,但只要掌握了底层原理,遇到问题就能快速定位。我在海思平台上,用这套方法解决过不下50个GPIO相关的bug。
好了,GPIO驱动开发就讲到这里。下一章咱们聊聊I2C驱动开发,那个比GPIO复杂多了,但套路是一样的。记住:先看手册,再写代码,最后测试。别问我怎么知道的,都是血泪教训。