GPIO 外设驱动开发:从点灯到中断
GPIO,说白了就是芯片的「数字手脚」。你想让它输出高电平就输出,想读取外部信号就读取。鸿蒙的 GPIO 驱动模型把这事包装得挺清爽,但底层那些坑,一个都不少。
我最早接触鸿蒙 GPIO 驱动时,第一反应是「这不就是读写寄存器吗?」后来发现,鸿蒙的 HDF(Hardware Driver Foundation)框架加了一层抽象,让驱动可以跨芯片复用。嗯,这个设计思路是对的,但你要是没搞懂底层映射关系,调试起来照样抓瞎。
GPIO 基础概念
GPIO 全称 General Purpose Input Output,通用输入输出口。每个 GPIO 引脚可以独立配置为输入或输出模式。输入模式下,你可以读取外部电平(0 或 1);输出模式下,你可以控制引脚输出高或低电平。
但实际项目中,GPIO 远不止这么简单。你想想看,一个引脚还涉及:
- 上拉/下拉电阻:引脚悬空时,电平不确定。内部上拉电阻把它拉到高电平,下拉拉到低电平。
- 驱动能力:有些引脚能输出 2mA,有些能输出 8mA。驱动 LED 或继电器时,这个参数很关键。
- 复用功能:同一个引脚可能是 GPIO,也可能是 I2C、UART、PWM 的通道。配置错了,功能就乱了。
鸿蒙 GPIO 驱动模型
鸿蒙的 GPIO 驱动模型分三层:
| 层级 | 说明 | 我遇到过的问题 |
|---|---|---|
| 应用层 | 通过 HDF IO Service 调用 GPIO 接口 | 权限配置不对,接口返回 -1 |
| 驱动框架层 | HDF 统一管理 GPIO 资源,提供标准 API | 引脚号映射错误,导致操作了错误的寄存器 |
| 芯片适配层 | 对接具体芯片的 GPIO 寄存器操作 | 不同芯片的 GPIO 控制器基地址不同,要仔细核对 |
核心 API 就几个:GpioWrite、GpioRead、GpioSetDir、GpioIrqRequest。但每个 API 背后都对应着一串寄存器操作。鸿蒙帮你封装好了,可你要是不知道底层在干什么,出了问题根本无从下手。
关键点:鸿蒙 GPIO 驱动使用「引脚号」而非「物理引脚」来标识。引脚号 = 控制器编号 × 每组引脚数 + 组内偏移。这个映射关系搞错了,点灯可能点到隔壁引脚上。
GPIO 输出控制 LED
点灯是嵌入式开发的「Hello World」。鸿蒙里控制 LED 的流程很直接:
- 获取 GPIO 设备句柄
- 设置引脚方向为输出
- 写入高电平或低电平
代码示例:
#include "gpio_if.h"
int32_t LedControl(uint16_t gpioNum, uint8_t status)
{
int32_t ret;
// 设置引脚为输出方向
ret = GpioSetDir(gpioNum, GPIO_DIR_OUT);
if (ret != 0) {
printf("Set dir failed, ret=%d\n", ret);
return ret;
}
// 写入电平状态
ret = GpioWrite(gpioNum, status);
if (ret != 0) {
printf("Write gpio failed, ret=%d\n", ret);
return ret;
}
return 0;
}
这段代码看起来简单,但我曾经踩过一个坑:GpioSetDir 和 GpioWrite 之间没有延时,有些芯片的 GPIO 方向切换需要几个时钟周期才能稳定。如果你紧接着写电平,可能写了个寂寞。
GPIO 输入读取按键状态
读取按键状态,核心是处理「抖动」。机械按键按下和释放时,电平会快速跳变几次,这就是抖动。如果不做消抖处理,一次按键可能被误判为多次。
读取流程:
- 设置引脚方向为输入
- 读取引脚电平
- 软件消抖(延时 + 再次读取确认)
代码示例:
#include "gpio_if.h"
#include "osal_time.h"
int32_t ReadKey(uint16_t gpioNum)
{
uint8_t value;
int32_t ret;
// 设置输入方向
ret = GpioSetDir(gpioNum, GPIO_DIR_IN);
if (ret != 0) {
return ret;
}
// 第一次读取
ret = GpioRead(gpioNum, &value);
if (ret != 0) {
return ret;
}
// 延时 10ms 消抖
OsalMSleep(10);
// 第二次读取,确认电平稳定
ret = GpioRead(gpioNum, &value);
if (ret != 0) {
return ret;
}
return value;
}
消抖延时多少合适?我一般用 10ms~20ms。太短了消不干净,太长了影响响应速度。你想想看,如果用户按一下键要等 50ms 才有反应,体验就很差了。
中断模式驱动开发
轮询读取按键,CPU 一直在忙等,浪费资源。中断模式才是正经做法:引脚电平变化时,硬件自动触发中断,CPU 暂停当前任务去处理。
鸿蒙 GPIO 中断配置步骤:
- 注册中断回调函数
- 设置中断触发条件(上升沿、下降沿、双边沿、高电平、低电平)
- 使能中断
代码示例:
#include "gpio_if.h"
// 中断回调函数
int32_t GpioIrqHandler(uint16_t gpioNum, void *data)
{
printf("GPIO %d interrupt triggered!\n", gpioNum);
// 在这里处理按键逻辑
return 0;
}
int32_t InitGpioIrq(uint16_t gpioNum)
{
int32_t ret;
// 设置输入方向
ret = GpioSetDir(gpioNum, GPIO_DIR_IN);
if (ret != 0) return ret;
// 注册中断,触发条件为上升沿
ret = GpioIrqRequest(gpioNum, GPIO_IRQ_TRIGGER_RISING, GpioIrqHandler, NULL);
if (ret != 0) {
printf("Irq request failed, ret=%d\n", ret);
return ret;
}
// 使能中断
ret = GpioIrqEnable(gpioNum);
if (ret != 0) {
printf("Irq enable failed, ret=%d\n", ret);
return ret;
}
return 0;
}
中断触发条件怎么选?按键按下时电平从高变低,用下降沿触发;释放时从低变高,用上升沿触发。如果你既要检测按下又要检测释放,就用双边沿触发。
OsalMSleep,结果系统直接死机。中断里不能调用会引起调度或阻塞的函数,这是铁律。另外,多个 GPIO 共用同一个中断号时,要在回调里判断是哪个引脚触发的。
本章知识结构
这张图把 GPIO 驱动开发的核心脉络理清楚了。从基础概念到驱动模型,再到输出、输入和中断,每个分支都有对应的知识点和注意事项。我个人建议你把这图打印出来贴在工位上,写代码时扫一眼,能少走很多弯路。