4. 硬件基础与接口协议
GPIO,说白了就是芯片的“手脚”。你要让LED亮、检测按键按下、控制外设的复位,都得靠它。在OpenHarmony里,GPIO驱动分两层:一是控制器驱动,管硬件寄存器;二是操作接口,给上层应用调用。今天咱们就把这两层掰开揉碎了讲。
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 |
配置中断触发方式 | 边沿触发和电平触发搞混,中断丢了一堆 |
注册控制器驱动的流程,我习惯用这张图来理解:
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模式。
好了,GPIO驱动开发的核心内容就这些。从控制器驱动的注册,到上层API的使用,再到实战案例,你跟着走一遍应该能上手了。记住:先让灯亮起来,再搞中断,最后再优化。这是我从无数次失败中总结出来的经验。