3、GPIO驱动封装(下):输出控制与输入读取封装、中断回调机制设计、中断优先级分组实战、实现GPIO_IRQ_Register回调注册接口
上一节我们把GPIO的初始化封装搞定了。这一节,咱们来啃剩下的硬骨头——输出控制、输入读取,还有中断回调机制。说实话,中断回调这块是很多初学者翻车的地方,我当年也在这上面栽过跟头。
3.1 输出控制与输入读取的封装思路
先看输出控制。STM32的GPIO输出,说白了就是往ODR寄存器写0或1。但直接操作寄存器有个问题——可读性太差。你想想看,代码里到处是GPIOA->ODR |= 0x01,三个月后你自己都看不懂。
我习惯的做法是这样:
/* gpio_driver.h */
typedef enum {
GPIO_PIN_RESET = 0,
GPIO_PIN_SET = 1
} GPIO_PinState;
void GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t Pin, GPIO_PinState State);
GPIO_PinState GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t Pin);
void GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t Pin);
实现起来其实很简单:
/* gpio_driver.c */
void GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t Pin, GPIO_PinState State)
{
if (State == GPIO_PIN_SET) {
GPIOx->BSRR = Pin; /* 用BSRR设置,原子操作 */
} else {
GPIOx->BRR = Pin; /* 用BRR复位,也是原子操作 */
}
}
GPIO_PinState GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t Pin)
{
return (GPIO_PinState)((GPIOx->IDR & Pin) ? GPIO_PIN_SET : GPIO_PIN_RESET);
}
void GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t Pin)
{
GPIOx->ODR ^= Pin;
}
输入读取更简单,直接读IDR寄存器就行。但要注意:读取前要确保引脚已经配置成输入模式,并且外部有确定电平。悬空引脚读出来的值是不确定的。
3.2 中断回调机制设计
接下来是重头戏——中断回调。STM32的EXTI中断可以配置成上升沿、下降沿或双边沿触发。但HAL库那套回调机制太臃肿了,我们自己写一个轻量级的。
我的设计思路是这样的:
- 用户注册一个回调函数,绑定到某个EXTI线
- 中断发生时,驱动层自动调用注册的回调
- 支持多个中断源,优先级可配置
先定义回调函数类型:
/* gpio_driver.h */
typedef void (*GPIO_IRQ_Callback)(void *arg);
typedef struct {
uint8_t irq_line; /* EXTI线号 0~15 */
GPIO_IRQ_Callback callback; /* 回调函数指针 */
void *arg; /* 回调参数 */
uint8_t priority; /* 中断优先级 */
uint8_t preempt_prio; /* 抢占优先级 */
uint8_t sub_prio; /* 子优先级 */
} GPIO_IRQ_Config_t;
3.3 中断优先级分组实战
STM32的中断优先级分组是个容易踩坑的地方。Cortex-M3/M4支持最多8位优先级,但具体用几位由分组决定。我见过有人把抢占优先级和子优先级搞混,导致中断嵌套完全乱套。
优先级分组由SCB->AIRCR寄存器控制:
| 分组 | 抢占优先级位数 | 子优先级位数 | 优先级总数 |
|---|---|---|---|
| NVIC_PriorityGroup_0 | 0 | 4 | 16 |
| NVIC_PriorityGroup_1 | 1 | 3 | 16 |
| NVIC_PriorityGroup_2 | 2 | 2 | 16 |
| NVIC_PriorityGroup_3 | 3 | 1 | 16 |
| NVIC_PriorityGroup_4 | 4 | 0 | 16 |
我个人习惯用NVIC_PriorityGroup_2,也就是2位抢占优先级、2位子优先级。这样有4级抢占嵌套,每级下面有4个子优先级,大多数场景够用了。
设置分组的代码:
/* 在系统启动时调用一次 */
void GPIO_IRQ_SetPriorityGroup(uint32_t group)
{
/* 设置优先级分组 */
NVIC_SetPriorityGrouping(group);
}
3.4 实现GPIO_IRQ_Register回调注册接口
好了,前面铺垫了这么多,终于到核心接口了。这个接口要做三件事:
- 配置EXTI触发方式
- 设置NVIC中断优先级
- 注册回调函数到内部表
先看内部数据结构:
/* gpio_driver.c */
#define MAX_EXTI_LINES 16
static GPIO_IRQ_Callback s_irq_callbacks[MAX_EXTI_LINES] = {NULL};
static void *s_irq_args[MAX_EXTI_LINES] = {NULL};
注册接口实现:
/* gpio_driver.c */
int GPIO_IRQ_Register(GPIO_IRQ_Config_t *config)
{
uint8_t line = config->irq_line;
if (line >= MAX_EXTI_LINES) {
return -1; /* 参数错误 */
}
/* 1. 配置EXTI触发方式 */
EXTI->IMR |= (1 << line); /* 取消中断屏蔽 */
if (config->trigger & EXTI_TRIGGER_RISING) {
EXTI->RTSR |= (1 << line); /* 上升沿触发 */
}
if (config->trigger & EXTI_TRIGGER_FALLING) {
EXTI->FTSR |= (1 << line); /* 下降沿触发 */
}
/* 2. 配置NVIC优先级 */
uint32_t preempt, sub;
NVIC_DecodePriority(config->priority, config->preempt_prio,
&preempt, &sub);
NVIC_SetPriority(EXTI0_IRQn + line,
NVIC_EncodePriority(config->priority,
config->preempt_prio,
config->sub_prio));
NVIC_EnableIRQ(EXTI0_IRQn + line);
/* 3. 注册回调 */
s_irq_callbacks[line] = config->callback;
s_irq_args[line] = config->arg;
return 0;
}
中断服务函数里调用回调:
/* gpio_driver.c */
void EXTI0_IRQHandler(void)
{
if (EXTI->PR & (1 << 0)) {
EXTI->PR = (1 << 0); /* 清除中断标志 */
if (s_irq_callbacks[0]) {
s_irq_callbacks[0](s_irq_args[0]);
}
}
}
使用示例:
/* 用户代码 */
void Button_IRQ_Handler(void *arg)
{
/* 按键中断处理 */
uint8_t *key_state = (uint8_t *)arg;
*key_state = 1;
}
void main(void)
{
GPIO_IRQ_Config_t btn_config;
btn_config.irq_line = 0; /* PA0, EXTI0 */
btn_config.callback = Button_IRQ_Handler;
btn_config.arg = &g_key_state;
btn_config.trigger = EXTI_TRIGGER_FALLING;
btn_config.preempt_prio = 1;
btn_config.sub_prio = 0;
GPIO_IRQ_Register(&btn_config);
while(1) {
if (g_key_state) {
/* 处理按键事件 */
g_key_state = 0;
}
}
}
下面这张图展示了整个GPIO中断回调的流程:
嗯,到这里GPIO驱动封装的核心内容就讲完了。从初始化到输出控制,从输入读取到中断回调,我们一步步搭起了一个轻量级但完整的驱动框架。这套设计思路不仅适用于GPIO,后面讲定时器、UART、SPI时都可以复用。