3. GPIO驱动开发:从管脚配置到中断处理
GPIO,说白了就是芯片的“手脚”。传感器要跟主控通信,第一步往往就是通过GPIO来传递状态信号。比如一个简单的按键传感器,按下时电平变化,GPIO检测到跳变,系统就知道该干活了。
我在鸿蒙南向开发中,GPIO驱动这块踩过的坑不少。今天咱们就把它拆开揉碎了讲清楚——从控制器驱动、管脚配置,到上下拉、驱动能力,再到中断注册与处理。
3.1 GPIO控制器驱动:先让芯片“认识”管脚
每个芯片内部都有一个或多个GPIO控制器。控制器驱动的作用,就是把这些硬件寄存器暴露给上层。鸿蒙的HDF(硬件驱动框架)里,GPIO控制器驱动需要实现标准的操作接口。
核心接口清单:
GpioSetDir— 设置输入/输出方向GpioGetDir— 读取当前方向GpioWrite— 输出高低电平GpioRead— 读取输入电平GpioSetIrq— 配置中断触发方式GpioUnsetIrq— 注销中断
我习惯先把控制器基地址、管脚总数这些参数写在设备树或HCS配置里。这样换芯片时,改配置就行,不用动代码。
// 伪代码:GPIO控制器驱动注册
static struct GpioMethod g_gpioMethod = {
.setDir = HiGpioSetDir,
.getDir = HiGpioGetDir,
.write = HiGpioWrite,
.read = HiGpioRead,
.setIrq = HiGpioSetIrq,
.unsetIrq = HiGpioUnsetIrq,
};
int32_t HiGpioBind(struct HdfDeviceObject *device) {
// 绑定方法表
return HDF_SUCCESS;
}
我的经验: 注册控制器时,记得检查管脚号是否越界。我曾经遇到过上层传了个-1进来,直接写寄存器导致系统挂死。加个边界判断,省心很多。
3.2 GPIO管脚配置:方向、模式与复用
管脚配置是GPIO开发里最琐碎的部分。一个管脚可能同时是GPIO、UART TX、I2C SCL。你得告诉芯片:我要用哪个功能。
配置流程一般分三步:
- 设置复用功能 — 把管脚切换到GPIO模式
- 设置方向 — 输入还是输出
- 设置初始电平 — 输出时确定默认高/低
// 示例:配置GPIO3_5为输出,初始高电平
int32_t GpioConfigOutput(uint16_t gpio, uint8_t initVal) {
// 1. 复用配置(寄存器地址因芯片而异)
GpioSetMux(gpio, GPIO_MUX_GPIO);
// 2. 设置方向为输出
GpioSetDir(gpio, GPIO_DIR_OUT);
// 3. 设置初始电平
GpioWrite(gpio, initVal);
return HDF_SUCCESS;
}
注意: 复用配置必须在设置方向之前完成。顺序反了,管脚可能还停留在外设模式,写电平根本写不进去。我调试一个I2C转GPIO的传感器时,卡了整整半天,最后发现是复用没先配好。
3.3 上下拉与驱动能力设置
上下拉电阻,很多人觉得不重要。其实恰恰相反——传感器信号不稳,十有八九是上下拉没配对。
鸿蒙的GPIO驱动里,上下拉配置通常通过 GpioSetPull 接口实现。驱动能力则通过 GpioSetDriveStrength 来设置。
| 配置项 | 可选值 | 典型场景 |
|---|---|---|
| 上拉 | PULL_UP / PULL_NONE | 按键输入、开漏输出 |
| 下拉 | PULL_DOWN / PULL_NONE | 中断输入(低电平触发) |
| 驱动能力 | 0~3 或 0~7(芯片相关) | LED驱动用高驱动,普通信号用低驱动 |
// 示例:配置上拉输入
int32_t GpioConfigInputPullUp(uint16_t gpio) {
GpioSetMux(gpio, GPIO_MUX_GPIO);
GpioSetDir(gpio, GPIO_DIR_IN);
GpioSetPull(gpio, GPIO_PULL_UP); // 内部上拉
GpioSetDriveStrength(gpio, 1); // 驱动能力设为1级
return HDF_SUCCESS;
}
避坑指南: 我曾经在驱动一个温湿度传感器时,发现数据线电平总是毛刺不断。查了半天,原来是内部上拉没使能,外部也没加上拉电阻。信号浮空,啥数据都读不准。记住:输入管脚,要么内部上拉,要么外部上拉,别让它悬空。
3.4 中断注册与处理:让CPU不再“空转”
传感器最常用的通信方式就是中断。比如运动传感器检测到动作,拉高一个GPIO,CPU收到中断后立刻读取数据。这比轮询省电得多。
鸿蒙里注册中断的流程:
- 申请中断号(从设备树或HCS获取)
- 设置触发方式(上升沿、下降沿、双边沿、高电平、低电平)
- 注册中断处理函数
- 使能中断
// 中断处理函数
static int32_t GpioIrqHandler(uint16_t gpio, void *data) {
// 读取传感器数据
uint8_t val = GpioRead(gpio);
// 通知上层
printf("GPIO %d interrupt triggered, value = %d\n", gpio, val);
return HDF_SUCCESS;
}
// 注册中断
int32_t GpioRegisterIrq(uint16_t gpio) {
// 配置为输入
GpioSetDir(gpio, GPIO_DIR_IN);
GpioSetPull(gpio, GPIO_PULL_UP);
// 注册下降沿中断
int32_t ret = GpioSetIrq(gpio, OSAL_IRQF_TRIGGER_FALLING,
GpioIrqHandler, NULL);
if (ret != HDF_SUCCESS) {
printf("GpioSetIrq failed, ret = %d\n", ret);
return ret;
}
return HDF_SUCCESS;
}
重要: 中断处理函数里不要做耗时操作。读寄存器、清标志位、发消息给任务队列就够了。千万别在中断里调用 printf 或 malloc——我见过有人这么干,系统直接死机。中断讲究“快进快出”。
3.5 知识结构总览
下面这张图把GPIO驱动开发的整体脉络画出来了。你可以对照着看,心里有个全局感。
3.6 实战要点总结
- 配置顺序不能乱: 复用 → 方向 → 上下拉 → 中断。跳步容易出怪问题。
- 中断触发方式选对: 传感器一般用边沿触发(上升沿或下降沿),电平触发容易产生重复中断。
- 驱动能力别贪大: 不是越大越好。驱动能力过强,信号反射严重,反而影响通信质量。我一般先从最低档试起,够用就行。
- 调试时加打印要克制: 中断里别加
printf,用HDF_LOGE或标志位代替。
个人习惯: 我每次写完GPIO驱动,都会先写一个简单的测试用例——循环读电平、写电平、触发中断。确认每个管脚都正常工作了,再往上挂传感器逻辑。这一步能省掉后面80%的调试时间。