4. 硬件基础与接口协议

GPIO,说白了就是芯片的“手脚”。你要让LED亮、检测按键按下、控制外设的复位,都得靠它。在OpenHarmony里,GPIO驱动分两层:一是控制器驱动,管硬件寄存器;二是操作接口,给上层应用调用。今天咱们就把这两层掰开揉碎了讲。

本章核心脉络: 控制器驱动(HDF + GPIO Chip)→ 操作接口(GPIO Pin API)→ 实战案例(按键中断 + LED控制)

4.1 GPIO控制器驱动:从寄存器到HDF模型

我刚开始做嵌入式时,GPIO驱动就是直接操作寄存器地址。但在OpenHarmony里,你得遵循HDF(Hardware Driver Foundation)框架。控制器驱动要注册一个GpioChip,告诉内核:我这芯片有多少个引脚、每个引脚怎么读写、怎么配置中断。

来看一个典型的控制器驱动结构体:

struct GpioChip {
    struct GpioMethod *ops;   // 操作方法集
    uint16_t start;           // 起始引脚号
    uint16_t count;           // 引脚数量
    void *priv;               // 私有数据(寄存器基址等)
};

嗯,这里要注意:start和count决定了引脚号范围。比如你的芯片有32个GPIO,从0号开始,那start=0, count=32。如果芯片有多个GPIO控制器,每个控制器管一组引脚,就得分别注册。

操作方法集是核心,它定义了这些回调函数:

回调函数 功能 我在项目中踩过的坑
request 申请引脚资源 忘记检查引脚是否已被占用,导致两个驱动打架
release 释放引脚 释放后没清中断,下次申请时中断还挂着
setDir 设置方向(输入/输出) 输出模式没配置驱动能力,带不动LED
getDir 获取方向 调试时用来确认配置是否生效
write 输出电平 写之前没判断方向,输出模式写无效
read 读取电平 引脚浮空时读到的值不稳定,需要上拉
setIrq 配置中断触发方式 边沿触发和电平触发搞混,中断丢了一堆

注册控制器驱动的流程,我习惯用这张图来理解:

GPIO控制器驱动注册流程 1. 实现 GpioMethod 回调 2. 填充 GpioChip 结构体 3. 调用 GpioChipAdd() 注册 4. 系统分配引脚号 & 挂入链表 GpioChipAdd() 内部会分配引脚号,并挂到全局链表,供上层 API 查找
我的习惯: 写控制器驱动时,先实现 read 和 write,因为这两个最简单,能快速验证寄存器地址对不对。然后再搞 setDir 和 setIrq,最后补全剩下的回调。

4.2 GPIO操作接口:上层怎么用这些引脚?

控制器驱动注册好后,上层应用就能通过统一的API操作GPIO了。OpenHarmony提供了一套标准的GPIO接口,你想想看,不管底层是什么芯片,上层代码都一样,这就是框架的好处。

核心API一览:

// 设置引脚方向
int32_t GpioSetDir(uint16_t gpio, uint16_t dir);
// dir: GPIO_DIR_IN 或 GPIO_DIR_OUT

// 读取电平
uint16_t GpioRead(uint16_t gpio);

// 写入电平
int32_t GpioWrite(uint16_t gpio, uint16_t val);
// val: GPIO_VAL_LOW 或 GPIO_VAL_HIGH

// 配置中断
int32_t GpioSetIrq(uint16_t gpio, uint16_t mode, GpioIrqFunc func, void *arg);
// mode: 上升沿、下降沿、高电平、低电平

// 使能/禁止中断
int32_t GpioEnableIrq(uint16_t gpio);
int32_t GpioDisableIrq(uint16_t gpio);

这些接口内部怎么工作的?我简单说一下:

  • GpioSetDir 根据引脚号找到对应的 GpioChip,然后调用 chip->ops->setDir。
  • GpioRead 和 GpioWrite 同理,都是通过回调函数转发到底层。
  • 中断相关的接口稍微复杂点,要注册回调函数,还要管理中断的使能和屏蔽。
我曾经踩过的坑: 在中断回调函数里调用 GpioRead 或 GpioWrite,结果死锁了。因为中断上下文和普通进程上下文用的锁不一样。记住:中断回调里尽量只做标记,不要调用GPIO API。

4.3 实战案例:按键中断控制LED

光说不练假把式。咱们来个经典案例:按一下按键,LED状态翻转。这个例子涵盖了GPIO的输入、输出、中断三大核心功能。

硬件连接:

外设 GPIO引脚 说明
LED GPIO_12 高电平点亮,串联330Ω电阻
按键 GPIO_13 按下为低电平,外部上拉

代码实现:

#include "gpio_if.h"

#define LED_PIN  12
#define KEY_PIN  13

static int32_t g_ledState = GPIO_VAL_LOW;

// 按键中断回调函数
static void KeyIrqHandler(void *arg)
{
    // 注意:中断上下文,不要调用GpioRead/GpioWrite
    // 只做标记,由其他任务处理
    g_ledState = (g_ledState == GPIO_VAL_LOW) ? GPIO_VAL_HIGH : GPIO_VAL_LOW;
}

// 初始化GPIO
static void GpioDemoInit(void)
{
    // 配置LED为输出,初始低电平
    GpioSetDir(LED_PIN, GPIO_DIR_OUT);
    GpioWrite(LED_PIN, GPIO_VAL_LOW);

    // 配置按键为输入,下降沿触发中断
    GpioSetDir(KEY_PIN, GPIO_DIR_IN);
    GpioSetIrq(KEY_PIN, GPIO_IRQ_TRIGGER_FALLING, KeyIrqHandler, NULL);
    GpioEnableIrq(KEY_PIN);
}

// 主循环中处理LED状态
void GpioDemoTask(void)
{
    static int32_t lastState = GPIO_VAL_LOW;

    if (g_ledState != lastState) {
        lastState = g_ledState;
        GpioWrite(LED_PIN, g_ledState);
    }
}
关键点:
  • 中断回调里只改状态变量,不直接操作GPIO。这是为了避免死锁和中断响应过长。
  • 主循环或单独的任务里检查状态变化,再调用 GpioWrite。
  • 按键用了下降沿触发,因为按下时电平从高变低。如果按键按下是低电平,松开是高电平,那就用上升沿。

你可能会问:为什么不在中断里直接写GPIO?嗯,我刚开始也这么干,结果系统偶尔卡死。后来查了HDF源码才发现,GPIO API内部用了互斥锁,中断上下文拿不到锁就死等了。所以,中断里只做标记,上下文切换后再操作硬件,这是嵌入式开发的铁律。

4.4 调试技巧:怎么确认GPIO配置对了?

写驱动最怕的就是“灯不亮、按键没反应”。我分享几个调试方法:

  • 读寄存器确认: 直接读GPIO控制器的寄存器,看方向配置和电平值对不对。比如 *(volatile uint32_t *)0x12345678。
  • 用示波器或逻辑分析仪: 这是最直观的。量一下引脚电平,看波形对不对。我曾经花了两小时查代码,最后发现是示波器探头没接地。
  • 加打印日志: 在 setDir、write、read 回调里加 HDF_LOGI,看调用顺序和参数。
  • 检查引脚复用: 很多芯片的GPIO默认是其他功能(比如UART、I2C)。你得先配置引脚复用寄存器,把它切到GPIO模式。
我的调试三板斧: 第一板斧看寄存器,第二板斧看波形,第三板斧看日志。三板斧下去,90%的问题都能定位。

好了,GPIO驱动开发的核心内容就这些。从控制器驱动的注册,到上层API的使用,再到实战案例,你跟着走一遍应该能上手了。记住:先让灯亮起来,再搞中断,最后再优化。这是我从无数次失败中总结出来的经验。