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中断,我总结了一个五步法,照着做基本不会出错:

  1. 使能GPIO时钟——不使能时钟,寄存器写不进去
  2. 配置引脚模式为输入——中断引脚必须是输入模式
  3. 选择触发方式——上升沿、下降沿还是电平触发
  4. 配置中断优先级——设置抢占优先级和子优先级
  5. 使能中断并编写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中断的完整流程串起来了:

GPIO中断驱动知识体系 外部触发源(按键/传感器) 中断控制器(EXTI + NVIC) 优先级分组与嵌套 中断服务函数(ISR) 抢占优先级 / 子优先级 中断屏蔽 / 标志位清除 volatile变量 / 临界区保护 WFI低功耗等待 GPIO驱动API封装层 应用层(按键→LED) 硬件层 中断控制层 配置与实现层 驱动封装层 应用层

从这张图可以看得很清楚:硬件触发 → 中断控制器仲裁 → 优先级判断 → ISR执行 → 驱动API封装 → 应用层调用,每一层各司其职。

最后说一句:GPIO中断看似简单,但优先级配置、临界区保护、ISR效率这些细节,直接决定了系统的稳定性。我建议你在自己的开发板上,把按键中断、定时器中断、串口中断同时跑起来,观察它们之间的嵌套行为——这才是真正理解中断的开始。