2、GPIO驱动接口封装(一):标准GPIO读写接口定义,输入/输出/中断模式配置。
各位同学,咱们今天正式进入驱动封装的第一站——GPIO。
GPIO 这东西,说简单是真简单,无非就是读个电平、写个电平。但说复杂也复杂,因为它是 MCU 和外界交互最基础、最频繁的通道。我见过太多项目,前期 GPIO 接口定义得随意,后期移植到另一颗芯片时,改代码改到怀疑人生。
所以,这一章的核心目标就一个:定义一套干净、通用、不依赖具体芯片的 GPIO 抽象接口。这套接口,以后会贯穿我们整个课程。
2.1 接口设计的核心原则
在写代码之前,我们先聊聊设计思路。我个人习惯,做抽象层驱动时,脑子里始终绷着三根弦:
- 原子性:一个函数只做一件事。读就是读,写就是写,别把配置和操作混在一起。
- 可移植性:接口参数里不能出现某个芯片特有的寄存器名或位定义。要用枚举、结构体把这些东西包起来。
- 易用性:调用者不需要知道底层是怎么操作的。他只需要说“我要把 PA0 设为输出高”,剩下的交给驱动。
你想想看,如果每个工程师都按自己的喜好来定义接口,那项目一大了,维护成本得多高?
2.2 标准 GPIO 读写接口定义
我们先从最基础的读写操作开始。一个 GPIO 引脚,无非就是读它的电平状态,或者写一个电平状态出去。
下面是我在多个项目中沉淀下来的一套接口原型。注意看参数类型,我用的是 uint8_t 和 uint16_t,而不是某个芯片的 GPIO_PinState。
/* gpio_interface.h */
#ifndef __GPIO_INTERFACE_H__
#define __GPIO_INTERFACE_H__
#include <stdint.h>
/* 电平状态定义 */
#define GPIO_PIN_RESET 0u
#define GPIO_PIN_SET 1u
/* 读取指定引脚的电平 */
uint8_t GPIO_ReadPin(uint16_t port, uint16_t pin);
/* 设置指定引脚的电平 */
void GPIO_WritePin(uint16_t port, uint16_t pin, uint8_t state);
/* 翻转指定引脚的电平 */
void GPIO_TogglePin(uint16_t port, uint16_t pin);
#endif /* __GPIO_INTERFACE_H__ */
设计要点:
port和pin用uint16_t,足够覆盖绝大多数 MCU 的 GPIO 数量。- 返回值统一用
uint8_t,0 表示低电平,非0 表示高电平。简单直接。 GPIO_TogglePin这个函数很实用。我在做 LED 闪烁或者按键状态机时,经常用它,省去一次读操作。
2.3 输入/输出/中断模式配置
读写操作之前,得先把引脚配置好。GPIO 的模式配置,是移植过程中最容易出问题的地方。
为什么?因为不同 MCU 的 GPIO 模式定义差异太大了。有的芯片有 8 种模式,有的有 16 种。我们不能把这些差异暴露给上层。
我的做法是:定义一套精简的、通用的模式枚举,然后在底层适配时,再映射到具体的芯片寄存器。
/* gpio_interface.h (续) */
/* GPIO 通用模式枚举 */
typedef enum {
GPIO_MODE_INPUT = 0x00, /* 浮空输入或上拉/下拉输入(由PU/PD决定) */
GPIO_MODE_OUTPUT_PP = 0x01, /* 推挽输出 */
GPIO_MODE_OUTPUT_OD = 0x02, /* 开漏输出 */
GPIO_MODE_AF_PP = 0x03, /* 复用推挽 */
GPIO_MODE_AF_OD = 0x04, /* 复用开漏 */
GPIO_MODE_IT_RISING = 0x05, /* 上升沿中断输入 */
GPIO_MODE_IT_FALLING = 0x06, /* 下降沿中断输入 */
GPIO_MODE_IT_RISING_FALLING = 0x07, /* 双边沿中断输入 */
} GPIO_Mode_t;
/* GPIO 输出速度枚举 */
typedef enum {
GPIO_SPEED_LOW = 0x00,
GPIO_SPEED_MEDIUM = 0x01,
GPIO_SPEED_HIGH = 0x02,
GPIO_SPEED_VERY_HIGH = 0x03,
} GPIO_Speed_t;
/* GPIO 内部上下拉枚举 */
typedef enum {
GPIO_PULL_NONE = 0x00,
GPIO_PULL_UP = 0x01,
GPIO_PULL_DOWN = 0x02,
} GPIO_Pull_t;
/* GPIO 初始化结构体 */
typedef struct {
uint16_t Pin; /* 引脚号,支持按位或,如 GPIO_PIN_0 | GPIO_PIN_1 */
GPIO_Mode_t Mode; /* 工作模式 */
GPIO_Speed_t Speed; /* 输出速度,仅在输出模式有效 */
GPIO_Pull_t Pull; /* 上下拉配置 */
} GPIO_Init_t;
/* 初始化/反初始化GPIO */
void GPIO_Init(uint16_t port, GPIO_Init_t* init);
void GPIO_DeInit(uint16_t port, uint16_t pin);
我曾经踩过的坑:
早期我做 STM32 到 NXP LPC 的移植时,直接把 STM32 的 8 种模式枚举拿过来用。结果 LPC 的 GPIO 模式寄存器结构完全不同,映射起来非常痛苦。后来我学乖了,抽象层只保留最通用的 7 种模式,把芯片特有的“模拟模式”、“事件输出”等特性,放到芯片级驱动里单独处理。
记住:抽象层不是万能的,它只解决 80% 的通用场景。剩下的 20% 特殊需求,可以通过扩展接口或者直接操作寄存器来实现。
2.4 中断回调机制
GPIO 中断是嵌入式系统里最常用的异步事件处理方式。抽象层的中断接口,需要解决两个问题:
- 如何注册中断回调函数?
- 如何区分是哪个引脚触发了中断?
我的方案是:提供一个统一的注册接口,让用户为每个引脚绑定一个回调函数。底层的中断服务程序(ISR)负责查询中断状态,然后调用对应的回调。
/* gpio_interface.h (续) */
/* 中断回调函数类型 */
typedef void (*GPIO_IRQ_Callback_t)(uint16_t pin);
/* 注册中断回调函数 */
void GPIO_RegisterIRQCallback(uint16_t port, uint16_t pin, GPIO_IRQ_Callback_t callback);
/* 注销中断回调函数 */
void GPIO_UnregisterIRQCallback(uint16_t port, uint16_t pin);
/* 全局中断使能/禁能 */
void GPIO_IRQ_Enable(uint16_t port, uint16_t pin);
void GPIO_IRQ_Disable(uint16_t port, uint16_t pin);
个人经验:
回调函数的执行时间一定要短。我在一个项目中,把延时较长的按键消抖逻辑直接写在了回调里,结果导致其他中断响应变慢。正确的做法是:回调里只设置一个事件标志,真正的处理逻辑放在主循环或者任务中执行。
2.5 本章知识体系总览
下面这张图,把 GPIO 抽象层的核心模块和它们之间的关系梳理了一下。你可以把它当作本章的“地图”。
2.6 接口使用示例
光说不练假把式。我们来看一个完整的例子:配置一个按键(输入中断)和一个 LED(输出)。
/* main.c 示例 */
#include "gpio_interface.h"
/* 按键中断回调 */
void Key_IRQHandler(uint16_t pin)
{
if(pin == GPIO_PIN_0) {
/* 翻转LED */
GPIO_TogglePin(GPIO_PORTB, GPIO_PIN_1);
}
}
void main(void)
{
GPIO_Init_t led_init, key_init;
/* 配置 LED: PB1, 推挽输出, 高速, 无上下拉 */
led_init.Pin = GPIO_PIN_1;
led_init.Mode = GPIO_MODE_OUTPUT_PP;
led_init.Speed = GPIO_SPEED_HIGH;
led_init.Pull = GPIO_PULL_NONE;
GPIO_Init(GPIO_PORTB, &led_init);
/* 配置按键: PA0, 上升沿中断, 内部上拉 */
key_init.Pin = GPIO_PIN_0;
key_init.Mode = GPIO_MODE_IT_RISING;
key_init.Speed = GPIO_SPEED_LOW; /* 输入模式,速度无效 */
key_init.Pull = GPIO_PULL_UP;
GPIO_Init(GPIO_PORTA, &key_init);
/* 注册中断回调 */
GPIO_RegisterIRQCallback(GPIO_PORTA, GPIO_PIN_0, Key_IRQHandler);
/* 使能中断 */
GPIO_IRQ_Enable(GPIO_PORTA, GPIO_PIN_0);
while(1) {
/* 主循环做其他事情 */
}
}
你看,整个使用过程,完全不需要知道底层是 STM32 还是 GD32。这就是抽象层的力量。
2.7 本章小结
这一章我们完成了 GPIO 抽象层的核心接口定义。总结一下关键点:
- 读写接口:
ReadPin、WritePin、TogglePin,原子操作,参数通用。 - 配置接口:通过枚举和结构体,统一了输入、输出、中断模式的配置方式。
- 中断接口:提供了回调注册机制,让中断处理与硬件解耦。