2、GPIO驱动开发:从寄存器到中断,一次讲透
GPIO,说白了就是芯片的“手脚”。你要让灯亮、让电机转、检测按键按下,都离不开它。瑞芯微RK系列平台的GPIO模块,设计得挺巧妙——它把GPIO和中断控制器揉在一起,用起来方便,但坑也不少。
我个人习惯,拿到一个新平台,第一件事就是翻GPIO控制器的寄存器手册。为什么?因为后面所有操作,本质上都是在摆弄这些寄存器。你想想看,gpiolib封装得再好,底层还是得读写那几个地址。
2.1 GPIO控制器寄存器详解
RK平台的GPIO控制器,每个Bank管理32个引脚。以RK3588为例,它有5个GPIO Bank(GPIO0~GPIO4)。每个Bank的寄存器基地址,在芯片手册里都能查到。
核心寄存器就那么几个,我列个表,你一看就明白:
| 寄存器名称 | 偏移地址 | 位宽 | 功能描述 |
|---|---|---|---|
| SWPORTA_DR | 0x0000 | 32位 | 数据输出寄存器。写1拉高,写0拉低。 |
| SWPORTA_DDR | 0x0004 | 32位 | 方向寄存器。1=输出,0=输入。 |
| SWPORA_PSR | 0x0008 | 32位 | 引脚状态寄存器。读这个得到引脚当前电平。 |
| INTEN | 0x0030 | 32位 | 中断使能寄存器。1=使能该引脚中断。 |
| INTTYPE | 0x0038 | 32位 | 中断类型。1=电平触发,0=边沿触发。 |
| INT_POLARITY | 0x003C | 32位 | 中断极性。电平触发时:1=高电平,0=低电平;边沿触发时:1=下降沿,0=上升沿。 |
| INTSTS | 0x0040 | 32位 | 中断状态寄存器。读得知哪个引脚触发了中断,写1清除中断标志。 |
2.2 GPIO操作接口:gpiolib的使用
直接操作寄存器当然可以,但内核提供了gpiolib,把底层封装好了。我建议你尽量用这套接口,代码可移植性好,也省得自己处理并发问题。
gpiolib的核心流程,我画了张图:
看明白了吗?流程很清晰。我重点说说几个容易踩坑的地方。
2.2.1 申请GPIO
用 gpio_request() 之前,你得先知道GPIO编号。RK平台里,GPIO编号的计算公式是:
gpio_number = bank * 32 + pin_offset
比如GPIO1_B5,bank=1,pin_offset=13(B组偏移8,再加5),所以编号是1*32+13=45。
2.2.2 设置方向
设置方向有两个函数:
gpio_direction_input()—— 设为输入gpio_direction_output()—— 设为输出,同时可以设置初始电平
这里有个细节:gpio_direction_output() 的第二个参数是初始值。我建议你显式传参,别依赖默认值。因为不同内核版本,默认行为可能不一样。
2.2.3 读写操作
读写就简单了:
// 读引脚
int val = gpio_get_value(gpio_num);
// 写引脚
gpio_set_value(gpio_num, 1); // 拉高
gpio_set_value(gpio_num, 0); // 拉低
嗯,这里要注意:gpio_set_value() 在原子上下文也能用,但如果你在进程上下文,建议用 gpio_set_value_cansleep(),它内部会处理可能导致的睡眠问题。
2.3 GPIO中断配置与调试
GPIO中断,是驱动开发里最常用的功能之一。按键、传感器、外设状态变化,都靠它来通知CPU。
配置中断的步骤,我总结为四步走:
- 申请GPIO并设为输入 —— 中断引脚必须是输入模式
- 获取中断号 —— 用
gpio_to_irq()把GPIO编号转成中断号 - 注册中断处理函数 —— 用
request_irq()或devm_request_irq() - 实现中断处理 —— 在顶半部做紧急处理,底半部用tasklet或workqueue
代码示例,我写个按键中断的典型实现:
#include <linux/interrupt.h>
#include <linux/gpio.h>
#define KEY_GPIO 45 // GPIO1_B5
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
// 顶半部:只做最紧急的事
// 比如设置标志位,唤醒等待队列
// 不要在这里做耗时操作!
return IRQ_WAKE_THREAD; // 唤醒线程化中断处理
}
static irqreturn_t key_irq_thread(int irq, void *dev_id)
{
// 底半部:在这里做实际处理
// 比如上报输入事件、打印日志
int val = gpio_get_value(KEY_GPIO);
pr_info("Key pressed, value=%d\n", val);
return IRQ_HANDLED;
}
static int __init key_drv_init(void)
{
int ret, irq;
// 1. 申请GPIO
ret = gpio_request(KEY_GPIO, "key-gpio");
if (ret) {
pr_err("Failed to request GPIO %d\n", KEY_GPIO);
return ret;
}
// 2. 设为输入
gpio_direction_input(KEY_GPIO);
// 3. 获取中断号
irq = gpio_to_irq(KEY_GPIO);
if (irq < 0) {
pr_err("Failed to get IRQ for GPIO %d\n", KEY_GPIO);
gpio_free(KEY_GPIO);
return irq;
}
// 4. 注册中断(线程化,下降沿触发)
ret = request_threaded_irq(irq, key_irq_handler, key_irq_thread,
IRQF_TRIGGER_FALLING, "key-irq", NULL);
if (ret) {
pr_err("Failed to request IRQ %d\n", irq);
gpio_free(KEY_GPIO);
return ret;
}
pr_info("Key driver initialized, IRQ=%d\n", irq);
return 0;
}
request_threaded_irq() 而不是 request_irq()?因为线程化中断可以把大部分工作放到内核线程里做,不会阻塞其他中断。这在RK平台上特别有用——它的中断控制器支持嵌套中断,但处理不好容易导致中断丢失。
2.3.1 中断触发类型
RK平台的GPIO中断支持四种触发方式:
| 触发标志 | 含义 | 适用场景 |
|---|---|---|
| IRQF_TRIGGER_RISING | 上升沿触发 | 按键按下(从低到高) |
| IRQF_TRIGGER_FALLING | 下降沿触发 | 按键按下(从高到低) |
| IRQF_TRIGGER_HIGH | 高电平触发 | 电平敏感型外设 |
| IRQF_TRIGGER_LOW | 低电平触发 | 电平敏感型外设 |
2.3.2 中断调试技巧
调试GPIO中断,我常用的三板斧:
- 看中断计数:
cat /proc/interrupts | grep gpio,看中断有没有触发,触发了多少次 - 看GPIO状态:
cat /sys/kernel/debug/gpio,确认引脚方向、电平、使用情况 - 加调试打印: 在中断处理函数里加
pr_info,但注意别打太多,否则影响性能
嗯,这里有个小技巧:如果中断没触发,先别急着查代码。拿万用表量一下引脚电平,确认硬件连接没问题。我遇到过好几次,都是飞线虚焊导致的问题。
2.4 本章小结
GPIO驱动开发,说难不难,说简单也不简单。核心就三件事:
- 寄存器层面: 搞清楚SWPORTA_DR、DDR、PSR、INTEN、INTSTS这几个寄存器怎么用
- 接口层面: 熟练使用gpiolib的request、direction、get/set、to_irq、free这套流程
- 中断层面: 掌握中断配置四步法,注意去抖和线程化处理
把这些搞透了,RK平台上的GPIO驱动,你基本就拿下了一半。剩下的,就是多写多练,踩坑多了自然就熟了。