GPIO驱动开发:从控制器原理到实战操作
GPIO,说白了就是芯片最基础的“手脚”。你要让LED亮、检测按键按下、控制外设复位,全得靠它。RK平台上的GPIO控制器,其实比你想的要复杂一些——它不只是简单的电平控制,背后还牵扯到复用、上下拉、驱动能力、中断映射等一系列问题。
我个人习惯,拿到一个新板子,第一件事就是先把GPIO的DTS配置理清楚。为什么?因为后面所有外设的“手脚”都要从这里分出去,一旦配错,调试起来非常痛苦。
GPIO控制器硬件原理
RK的GPIO控制器,每个Bank通常包含32个引脚。以RK3588为例,它有5个GPIO Bank(GPIO0~GPIO4),每个Bank内部结构大致如下:
核心寄存器组:
- DATA寄存器:读写引脚电平,0或1
- DIR寄存器:方向控制,0=输入,1=输出
- PU/PD寄存器:内部上拉/下拉电阻使能
- DRV寄存器:驱动能力,一般有4档可调
- IOMUX寄存器:功能复用选择,这是最关键的
嗯,这里要注意:IOMUX寄存器决定了这个引脚是当普通GPIO用,还是当I2C、UART、SPI等外设功能用。RK的IOMUX设计比较灵活,每个引脚通常有4种复用功能可选。
我曾经在项目里遇到过一个问题:明明DTS里配了UART2,但死活不通。查了半天,发现是IOMUX没切过去,引脚还保持在GPIO模式。从那以后,我每次配完外设都会先读一下IOMUX寄存器确认状态。
DTS配置详解
RK平台的GPIO DTS配置,核心就几个节点。我直接给个典型例子:
// 设备树中GPIO控制器的基本结构
pinctrl: pinctrl {
compatible = "rockchip,rk3588-pinctrl";
rockchip,grf = <&grf>;
#address-cells = <2>;
#size-cells = <2>;
ranges;
gpio0: gpio@fd8a0000 {
compatible = "rockchip,gpio-bank";
reg = <0x0 0xfd8a0000 0x0 0x100>;
interrupts = <GIC_SPI 84 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru PCLK_GPIO0>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
// 其他GPIO Bank类似...
};
你想想看,这里每个GPIO Bank都对应一个中断号,这就是为什么GPIO能触发中断的原因。RK的中断控制器会把GPIO中断统一路由到GIC。
实际使用中,我们更关心的是引脚复用配置:
// 引脚复用配置示例
&pinctrl {
// 定义一组引脚配置
led_pins: led-pins {
rockchip,pins =
// bank, pin, mux, pull, drive
<0 RK_PA0 1 &pcfg_pull_none>,
<0 RK_PA1 1 &pcfg_pull_none>;
};
uart2_pins: uart2-pins {
rockchip,pins =
<0 RK_PB0 2 &pcfg_pull_up>,
<0 RK_PB1 2 &pcfg_pull_up>;
};
};
个人经验:RK_PA0这种宏定义,A代表Bank内的引脚组(每组8个),0是组内偏移。RK_PA0 = Bank0的Pin0。记住这个规律,看DTS就快多了。
GPIO操作接口
内核里操作GPIO,主要有两套API:老式的gpio_xxx系列和新的gpiod_xxx系列。我个人强烈建议用新的gpiod接口,它更安全、更清晰。
来看一个完整的LED驱动示例:
#include <linux/gpio/consumer.h>
#include <linux/of.h>
struct device_node *np;
struct gpio_desc *led_gpio;
// 从设备树获取GPIO
np = of_find_node_by_path("/leds");
led_gpio = devm_gpiod_get_from_of_node(
dev, np, "led-gpios", 0,
GPIOD_OUT_LOW, "led");
if (IS_ERR(led_gpio)) {
dev_err(dev, "Failed to get LED GPIO\n");
return PTR_ERR(led_gpio);
}
// 操作GPIO
gpiod_set_value(led_gpio, 1); // 点亮
gpiod_set_value(led_gpio, 0); // 熄灭
// 读取输入
int val = gpiod_get_value(led_gpio);
避坑指南:我曾经在RK3399上遇到一个问题——用gpio_request老接口申请GPIO,结果和某个外设驱动冲突了。因为老接口不检查设备树里的GPIO所有权。换成gpiod接口后,内核会自动检查GPIO是否已被其他设备占用,安全很多。
GPIO中断处理
GPIO中断在嵌入式开发里太常用了。按键检测、外设状态变化通知,都离不开它。RK的GPIO中断支持上升沿、下降沿、高电平、低电平四种触发方式。
// GPIO中断注册示例
static irqreturn_t gpio_irq_handler(int irq, void *dev_id)
{
struct device *dev = dev_id;
int val = gpiod_get_value(led_gpio);
dev_info(dev, "GPIO interrupt triggered, value=%d\n", val);
// 处理中断...
return IRQ_HANDLED;
}
// 申请中断
int irq = gpiod_to_irq(led_gpio);
ret = devm_request_irq(dev, irq, gpio_irq_handler,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"my_gpio_irq", dev);
这里有个细节:gpiod_to_irq会把GPIO描述符转换成中断号。但要注意,这个转换必须在GPIO已经配置为输入模式后才能成功。
实战:GPIO子系统整体流程
我把整个GPIO驱动开发的流程画了张图,方便你理解:
调试技巧与避坑
调试GPIO,我常用的方法就几个:
- 查看/sys/class/gpio:虽然老接口,但调试时快速验证很方便
- 读寄存器:直接通过devmem2读GPIO的DATA/DIR寄存器,确认硬件状态
- 示波器:硬件问题,示波器一抓就知道电平对不对
我曾经踩过的坑:在RK3568上调试一个I2C设备,发现SCL引脚电平一直拉不高。查了DTS,配了上拉。读寄存器,PU位也置1了。最后拿示波器一看,引脚虚焊了...所以,硬件问题先确认焊接,再查软件。
还有一个常见问题:GPIO中断丢失。RK的GPIO中断是电平触发的,如果中断处理时间太长,或者中断服务程序里没有清除中断标志,就会丢中断。我的建议是,中断服务程序里只做最必要的事,耗时操作放到工作队列或tasklet里。
| 调试方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| /sys/class/gpio | 快速验证GPIO功能 | 无需写代码 | 不支持gpiod接口 |
| devmem2读寄存器 | 确认硬件状态 | 最底层、最准确 | 需要知道寄存器地址 |
| 示波器/逻辑分析仪 | 时序问题、电平异常 | 直观看到波形 | 需要硬件设备 |
| printk/ dev_dbg | 驱动流程跟踪 | 简单易用 | 影响实时性 |
嗯,最后说一句:GPIO看似简单,但它是所有外设的基础。把GPIO搞透了,后面I2C、SPI、UART的驱动开发会顺畅很多。你想想看,如果连引脚电平都控制不好,还谈什么复杂外设呢?