3. GPIO外设驱动开发(下):GPIO中断配置、中断优先级与嵌套、GPIO驱动代码封装、实战:按键中断控制LED
各位好,我们接着往下聊。上一节我们把GPIO的推挽、开漏、上下拉这些基础输出输入模式讲透了。这一节,咱们要动真格的了——中断。
说实话,中断才是嵌入式开发的灵魂。没有中断,CPU就得一直轮询,效率低得吓人。我早年做第一个项目时,就是靠轮询读按键,结果主循环里稍微加点逻辑,按键就响应迟钝。后来换成中断,整个世界清净了。
3.1 GPIO中断的基本概念
GPIO中断,说白了就是:引脚电平发生变化时,硬件自动触发一个异常,CPU暂停当前任务,跳去执行中断服务函数。
CMS32系列MCU的GPIO中断支持以下几种触发方式:
| 触发方式 | 说明 | 典型应用 |
|---|---|---|
| 上升沿触发 | 电平从低到高跳变时触发 | 按键释放检测 |
| 下降沿触发 | 电平从高到低跳变时触发 | 按键按下检测 |
| 双边沿触发 | 上升沿和下降沿都触发 | 编码器信号捕获 |
| 低电平触发 | 引脚为低电平时持续触发 | 紧急停止信号 |
| 高电平触发 | 引脚为高电平时持续触发 | 唤醒信号 |
我的经验:按键中断尽量用下降沿触发,配合10~20ms的硬件消抖电容。如果用电平触发,按键抖动会导致中断反复进入,CPU根本忙不过来。
3.2 中断优先级与嵌套
中断优先级,这是个容易踩坑的地方。CMS32的中断控制器支持抢占优先级和子优先级,概念上和ARM Cortex-M的NVIC类似,但寄存器配置略有不同。
3.2.1 优先级分组
CMS32把优先级分为4个组,通过系统寄存器配置:
| 分组 | 抢占优先级位数 | 子优先级位数 | 优先级总数 |
|---|---|---|---|
| Group 0 | 0 bit | 4 bits | 16 |
| Group 1 | 1 bit | 3 bits | 8 |
| Group 2 | 2 bits | 2 bits | 4 |
| Group 3 | 3 bits | 1 bit | 2 |
我一般习惯用Group 2,也就是2位抢占优先级、2位子优先级。这样既能保证高优先级中断及时响应,又能给同优先级的中断留出排队空间。
3.2.2 中断嵌套规则
中断嵌套,说白了就是:一个中断正在执行,另一个更高优先级的中断来了,CPU要不要理它?
规则其实很简单:
- 只有抢占优先级更高的中断才能打断当前中断
- 抢占优先级相同的中断,不能互相打断
- 子优先级只在抢占优先级相同时,决定谁先执行
我曾经踩过的坑:有一次我把定时器中断和GPIO中断设成了相同的抢占优先级,结果定时器中断里刚好在操作GPIO寄存器,GPIO中断来了之后,两个中断互相等待资源,直接死锁。后来我把定时器中断的抢占优先级调高一级,问题解决。
3.3 GPIO中断配置步骤
配置GPIO中断,我总结了一个五步法,照着做基本不会出错:
- 使能GPIO时钟——不使能时钟,寄存器写不进去
- 配置引脚模式为输入——中断引脚必须是输入模式
- 选择触发方式——上升沿、下降沿还是电平触发
- 配置中断优先级——设置抢占优先级和子优先级
- 使能中断并编写ISR——开中断,写服务函数
下面是一个具体的代码示例,配置PA0引脚为下降沿触发中断:
/* 步骤1:使能GPIOA时钟 */
RCC->APB2ENR |= (1 << 2); // 使能GPIOA时钟
/* 步骤2:配置PA0为输入模式 */
GPIOA->CRL &= ~(0x0F << 0); // 清除配置
GPIOA->CRL |= (0x08 << 0); // 输入模式,上拉/下拉
/* 步骤3:选择下降沿触发 */
EXTI->FTSR |= (1 << 0); // 下降沿触发选择
/* 步骤4:配置中断优先级 */
NVIC_SetPriority(EXTI0_IRQn, 0x20); // 抢占优先级2,子优先级0
/* 步骤5:使能中断 */
NVIC_EnableIRQ(EXTI0_IRQn);
EXTI->IMR |= (1 << 0); // 中断屏蔽寄存器使能
3.4 GPIO驱动代码封装
裸机开发最忌讳的就是把寄存器操作散落在各个.c文件里。我建议统一封装成驱动层,上层应用只调用API,不碰寄存器。
这是我的GPIO驱动封装思路:
3.4.1 结构体定义
typedef struct {
uint8_t port; // GPIO端口号
uint8_t pin; // 引脚号
uint8_t mode; // 输入/输出/复用
uint8_t pull; // 上拉/下拉/浮空
uint8_t trigger; // 中断触发方式
uint8_t priority; // 中断优先级
} GPIO_Config_t;
3.4.2 API接口设计
| 函数名 | 功能 | 参数 |
|---|---|---|
GPIO_Init() |
初始化GPIO引脚 | GPIO_Config_t结构体指针 |
GPIO_WritePin() |
设置引脚输出电平 | 端口、引脚、电平值 |
GPIO_ReadPin() |
读取引脚输入电平 | 端口、引脚 |
GPIO_EnableInterrupt() |
使能引脚中断 | 端口、引脚、触发方式 |
GPIO_DisableInterrupt() |
禁用引脚中断 | 端口、引脚 |
封装的好处:换MCU平台时,只需要重写底层这几个函数,上层的业务逻辑一行代码都不用改。我在一个项目里从CMS32换到STM32,只花了半天就完成了驱动移植。
3.5 实战:按键中断控制LED
理论说完了,咱们来点实际的。这个实战的目标是:按下按键,LED状态翻转;松开按键,LED保持当前状态。
3.5.1 硬件连接
- 按键:接PA0,外部下拉10kΩ电阻到GND
- LED:接PB0,阳极通过330Ω电阻接VCC,阴极接PB0
3.5.2 完整代码
/* gpio_driver.h */
#ifndef __GPIO_DRIVER_H
#define __GPIO_DRIVER_H
#include "cms32.h"
void GPIO_Init(GPIO_Config_t *config);
void GPIO_WritePin(uint8_t port, uint8_t pin, uint8_t level);
uint8_t GPIO_ReadPin(uint8_t port, uint8_t pin);
void GPIO_EnableInterrupt(uint8_t port, uint8_t pin, uint8_t trigger);
void GPIO_DisableInterrupt(uint8_t port, uint8_t pin);
#endif
/* main.c */
#include "gpio_driver.h"
volatile uint8_t led_state = 0;
void EXTI0_IRQHandler(void)
{
/* 检查中断标志位 */
if (EXTI->PR & (1 << 0))
{
/* 翻转LED状态 */
led_state = !led_state;
GPIO_WritePin(GPIO_PORTB, 0, led_state);
/* 清除中断标志位 */
EXTI->PR = (1 << 0);
}
}
int main(void)
{
GPIO_Config_t btn_config, led_config;
/* 配置按键:PA0,输入,下拉,下降沿触发 */
btn_config.port = GPIO_PORTA;
btn_config.pin = 0;
btn_config.mode = GPIO_MODE_INPUT;
btn_config.pull = GPIO_PULL_DOWN;
btn_config.trigger = GPIO_TRIGGER_FALLING;
btn_config.priority = 0x20;
GPIO_Init(&btn_config);
/* 配置LED:PB0,推挽输出 */
led_config.port = GPIO_PORTB;
led_config.pin = 0;
led_config.mode = GPIO_MODE_OUTPUT_PP;
led_config.pull = GPIO_PULL_NONE;
GPIO_Init(&led_config);
/* 使能按键中断 */
GPIO_EnableInterrupt(GPIO_PORTA, 0, GPIO_TRIGGER_FALLING);
while (1)
{
/* 主循环可以干别的事,LED由中断控制 */
__WFI(); // 等待中断,降低功耗
}
}
3.5.3 代码要点解析
led_state用volatile修饰——中断和主循环都可能访问它,防止编译器优化- 中断服务函数里只做最少的操作——翻转状态、写引脚、清标志,绝不调用延时函数
- 主循环用
__WFI()——没有中断时CPU进入休眠,省电
注意:中断服务函数里不要做复杂运算,不要调用printf,不要操作临界资源而不加保护。我见过有人直接在ISR里调malloc,结果堆碎片把系统搞崩了。
3.6 本章知识体系
下面这张图,把GPIO中断的完整流程串起来了:
从这张图可以看得很清楚:硬件触发 → 中断控制器仲裁 → 优先级判断 → ISR执行 → 驱动API封装 → 应用层调用,每一层各司其职。
最后说一句:GPIO中断看似简单,但优先级配置、临界区保护、ISR效率这些细节,直接决定了系统的稳定性。我建议你在自己的开发板上,把按键中断、定时器中断、串口中断同时跑起来,观察它们之间的嵌套行为——这才是真正理解中断的开始。