3、GPIO接口驱动开发:从控制器模型到按键中断实战
GPIO,说白了就是芯片的“手脚”。你要让传感器上报数据,或者让执行器干活,最终都得通过GPIO来操作。这一章,我们就来啃下这块硬骨头。
3.1 GPIO控制器模型:鸿蒙怎么管这些“手脚”
鸿蒙的GPIO驱动,不是让你直接去操作寄存器地址。它抽象了一套模型,叫GPIO控制器。我个人习惯,先把这个模型搞清楚,再写代码会顺手很多。
每个GPIO控制器,在鸿蒙里对应一个 GpioController 结构体。它管着一组GPIO管脚,比如一个GPIO0到GPIO31。控制器里会注册好操作函数:读、写、设置方向、配置中断。
你想想看,如果每个芯片都直接操作寄存器,那代码移植起来得多痛苦?鸿蒙这套模型,就是让你换芯片时,只换底层操作函数,上层逻辑纹丝不动。
核心概念: GPIO控制器 = 管脚集合 + 操作函数集。驱动开发者只需要实现
GpioController 里的回调,剩下的交给框架。
3.2 GPIO管脚配置:别小看这一步
配置管脚,看似简单,坑却不少。我记得刚做驱动时,有个传感器死活读不到数据,查了两天,最后发现是管脚复用没配对。
鸿蒙里配置一个GPIO管脚,通常需要这几步:
- 获取控制器句柄:通过设备树或平台数据拿到
GpioController。 - 设置方向:输入还是输出?用
GpioSetDir()。 - 设置输出值:如果是输出,设高或低。
- 配置上下拉:有些场景需要内部上拉或下拉。
- 配置驱动能力:电流驱动强度,一般默认就行。
| 配置项 | API函数 | 说明 |
|---|---|---|
| 设置方向 | GpioSetDir(gpio, dir) |
dir: GPIO_DIR_IN / GPIO_DIR_OUT |
| 读取电平 | GpioRead(gpio, &val) |
val: 0 或 1 |
| 写入电平 | GpioWrite(gpio, val) |
val: GPIO_VAL_LOW / GPIO_VAL_HIGH |
| 设置中断触发 | GpioSetIrq(gpio, mode, func, arg) |
mode: 上升沿、下降沿、双边沿、高电平、低电平 |
我的经验: 配置管脚时,一定要先确认这个管脚有没有被其他外设占用。我曾经在调试I2C时,发现SCL管脚被GPIO模块抢了,导致总线一直拉低。用
cat /sys/kernel/debug/gpio 可以快速查看管脚状态。
3.3 中断注册与响应:让CPU别傻等
轮询?那是下下策。真正高效的驱动,都是用中断。按键按下,CPU才来处理,平时该干嘛干嘛。
鸿蒙的中断注册流程,我总结为三步:
- 申请中断号:通过
GpioToIrq(gpio)把GPIO管脚映射成中断号。 - 注册中断处理函数:调用
request_irq(irq, handler, flags, name, dev)。 - 使能中断:调用
enable_irq(irq)。
中断处理函数里,有个铁律:快进快出。不要在中断上下文里做耗时操作,比如打印、内存分配。真要处理数据,用工作队列或tasklet推到下半部去。
避坑指南: 我曾经在中断处理函数里直接调了
printk,结果系统直接卡死。原因是printk本身可能引起调度,而中断上下文不允许调度。记住,中断里只做标记,别干重活。
3.4 实战:按键中断驱动
光说不练假把式。我们写一个真实的按键中断驱动。假设按键接在GPIO5,按下为低电平。
先看代码框架:
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/of.h>
static int key_gpio = 5;
static int key_irq;
static struct work_struct key_work;
// 工作队列处理函数(下半部)
static void key_work_handler(struct work_struct *work)
{
int val;
gpio_get_value(key_gpio, &val);
printk("Key pressed! value = %d\n", val);
// 这里可以上报事件给上层
}
// 中断上半部
static irqreturn_t key_isr(int irq, void *dev_id)
{
// 调度下半部
schedule_work(&key_work);
return IRQ_HANDLED;
}
static int __init key_drv_init(void)
{
int ret;
// 1. 申请GPIO
ret = gpio_request(key_gpio, "key-gpio");
if (ret) {
printk("gpio request failed\n");
return ret;
}
// 2. 设置方向为输入
gpio_direction_input(key_gpio);
// 3. 映射中断号
key_irq = gpio_to_irq(key_gpio);
// 4. 注册中断,下降沿触发
ret = request_irq(key_irq, key_isr, IRQF_TRIGGER_FALLING,
"key_irq", NULL);
if (ret) {
printk("request irq failed\n");
gpio_free(key_gpio);
return ret;
}
// 5. 初始化工作队列
INIT_WORK(&key_work, key_work_handler);
printk("key driver init ok\n");
return 0;
}
static void __exit key_drv_exit(void)
{
free_irq(key_irq, NULL);
gpio_free(key_gpio);
printk("key driver exit\n");
}
module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");
这段代码,我拆开讲几个关键点:
- gpio_to_irq:把物理GPIO转成系统中断号,不同平台映射方式不同,但API统一了。
- IRQF_TRIGGER_FALLING:下降沿触发,按键按下时电平从高变低。
- schedule_work:把耗时操作推到进程上下文,避免中断里卡死。
调试技巧: 加载驱动后,用
cat /proc/interrupts 查看中断是否注册成功。如果按键按下,中断计数应该增加。如果没反应,先量一下管脚电平,确认硬件连接没问题。
嗯,这里要注意,工作队列里的 printk 只是演示。实际产品中,你应该用 input_report_key 上报给输入子系统,或者通过netlink发消息给用户态。
这张图把整个中断响应链路画清楚了。从硬件按键按下,到最终事件上报,每一步都有明确的职责。你写驱动时,脑子里要有这张图,就知道代码该往哪个环节塞。
个人建议: 刚开始学中断驱动,别急着写复杂逻辑。先让中断处理函数里只打印一句话,确认中断能触发。然后再慢慢加工作队列、上报事件。一步一个脚印,出问题了也好定位。