4. GPIO与中断:从模式配置到中断嵌套

各位同学,今天我们来啃一块硬骨头——GPIO和中断。说实话,这部分内容在GD32V上玩好了,后面低功耗和FreeRTOS移植才能走得顺。我刚开始做嵌入式那会儿,总觉得GPIO不就是个高低电平嘛,直到有一次产品量产了才发现中断响应慢了一拍,那叫一个惨。好,咱们开始。

4.1 GPIO模式配置:推挽、开漏、上拉、下拉

GD32V的GPIO引脚,说白了就是个多面手。每个引脚可以配置成多种模式,你得根据实际电路需求来选。我个人习惯,先看外设手册,再定模式,省得后面改来改去。

4.1.1 推挽输出 vs 开漏输出

模式 输出电平能力 典型应用 注意事项
推挽输出 强拉高、强拉低 LED驱动、普通IO 不能线与,否则短路
开漏输出 只能拉低,高阻需外接上拉 I2C总线、多设备共享线 必须配外部上拉电阻

推挽输出,内部PMOS和NMOS交替导通,输出0就是0,输出1就是1,干脆利落。开漏输出呢,只用了NMOS,输出0时拉低,输出1时引脚直接变高阻——你想想看,这时候电平全靠外部上拉电阻决定。

我的经验:做I2C总线时,千万别用推挽模式。我曾经有个项目,新手同事把I2C的SCL和SDA配成了推挽输出,结果两个从设备同时拉低时,电流直接飙到几十毫安,芯片烫得能煎鸡蛋。

4.1.2 上拉与下拉电阻

GD32V内部集成了可编程的上拉和下拉电阻,大约40kΩ左右。这个值不算大,但对付浮空输入足够了。

  • 上拉输入:引脚默认高电平,外部拉低时触发
  • 下拉输入:引脚默认低电平,外部拉高时触发
  • 浮空输入:啥都不接,电平不确定——千万别用
避坑指南:我曾经在按键检测电路里用了浮空输入,结果手指还没碰到按键,电平就自己跳来跳去。后来加了内部上拉,世界清净了。记住:浮空输入只适合有外部强驱动源的信号,比如SPI的MISO。

4.1.3 代码示例:GPIO模式配置

// 配置PA0为推挽输出,50MHz速度
gpio_mode_set(GPIOA, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_0);
gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0);

// 配置PB1为开漏输出,带上拉
gpio_mode_set(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_PIN_1);
gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_2MHZ, GPIO_PIN_1);

// 配置PC2为输入,下拉
gpio_mode_set(GPIOC, GPIO_MODE_INPUT, GPIO_PUPD_PULLDOWN, GPIO_PIN_2);

嗯,这里要注意:GPIO_OSPEED不是越高越好。50MHz适合高速信号,但功耗也大。低功耗场景下,我一般用2MHz,够用就行。

4.2 外部中断(EXTI)配置

外部中断,就是让GPIO引脚在电平变化时通知CPU。GD32V的EXTI支持16个独立中断线,每个中断线可以映射到任意一个GPIO引脚——但同一中断线不能同时映射多个引脚。

4.2.1 EXTI配置步骤

  1. 使能GPIO时钟和SYSCFG时钟
  2. 配置GPIO模式为输入
  3. 将GPIO引脚连接到EXTI线
  4. 配置EXTI触发方式(上升沿、下降沿、双边沿)
  5. 使能EXTI中断
  6. 配置NVIC中断优先级
  7. 编写中断服务函数

核心要点:EXTI线0~15对应GPIO的PIN0~PIN15。比如PA0和PB0都映射到EXTI线0,但只能选一个用。我习惯把同一功能的外设放在不同PIN上,避免冲突。

4.2.2 代码示例:按键中断

// 使能时钟
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_SYSCFG);

// 配置PA0为输入,上拉
gpio_mode_set(GPIOA, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, GPIO_PIN_0);

// 连接EXTI线0到PA0
syscfg_exti_line_config(EXTI_SOURCE_GPIOA, EXTI_SOURCE_PIN0);

// 配置EXTI:下降沿触发(按键按下)
exti_init(EXTI_0, EXTI_INTERRUPT, EXTI_TRIG_FALLING);
exti_interrupt_flag_clear(EXTI_0);
exti_interrupt_enable(EXTI_0);

// 配置NVIC优先级
nvic_irq_enable(EXTI0_IRQn, 2, 0);

4.2.3 中断服务函数

void EXTI0_IRQHandler(void)
{
    if(exti_interrupt_flag_get(EXTI_0) != RESET)
    {
        // 处理按键事件
        // 注意:中断里别做复杂操作
        // 我一般只设一个标志位
        key_pressed = 1;

        // 清中断标志
        exti_interrupt_flag_clear(EXTI_0);
    }
}
我曾经踩过的坑:中断服务函数里忘了清标志位,结果中断一直触发,CPU卡死在中断里,主循环根本跑不起来。后来我养成了习惯:进中断第一件事就是清标志,处理完再清一次,双重保险。

4.3 中断优先级与嵌套

GD32V的NVIC支持最多16级抢占优先级和16级子优先级。说白了,就是给中断排个队——谁重要谁先上。

4.3.1 优先级分组

分组 抢占优先级位数 子优先级位数 说明
NVIC_PRIGROUP_0 0 4 只有子优先级,无抢占
NVIC_PRIGROUP_1 1 3 2级抢占,8级子优先级
NVIC_PRIGROUP_2 2 2 4级抢占,4级子优先级
NVIC_PRIGROUP_3 3 1 8级抢占,2级子优先级
NVIC_PRIGROUP_4 4 0 16级抢占,无子优先级

我个人习惯用NVIC_PRIGROUP_2,4级抢占够用,4级子优先级也能处理一些微调需求。你想想看,如果所有中断都是同一抢占优先级,那就只能排队,高优先级的中断没法打断低优先级的——这在实时系统里很要命。

4.3.2 中断嵌套规则

  • 抢占优先级决定能否打断:数字越小优先级越高
  • 子优先级决定同时到达时谁先响应
  • 相同抢占优先级的中断不能互相打断
我的建议:把时间敏感的中断(比如定时器、通信接收)设高抢占优先级,把非关键的中断(比如按键)设低优先级。我在一个电机控制项目里,把PWM中断设成抢占优先级0,按键中断设成3,这样电机控制永远不会被按键耽误。

4.3.3 代码示例:设置中断优先级

// 设置优先级分组:4级抢占,4级子优先级
nvic_priority_group_set(NVIC_PRIGROUP_2);

// 使能EXTI0中断,抢占优先级0,子优先级0
nvic_irq_enable(EXTI0_IRQn, 0, 0);

// 使能USART0中断,抢占优先级1,子优先级0
nvic_irq_enable(USART0_IRQn, 1, 0);

// 使能TIMER2中断,抢占优先级2,子优先级0
nvic_irq_enable(TIMER2_IRQn, 2, 0);

嗯,这里要注意:中断优先级配置必须在使能中断之前完成。我见过有人先使能中断再配优先级,结果中断一使能就触发,优先级还没配好,乱套了。

4.4 知识体系总览

下面这张图把本章的核心逻辑串起来了,你可以对照着看。

GPIO与中断知识体系 GPIO与中断知识体系 GPIO模式配置 推挽输出 开漏输出 上拉输入 下拉输入 EXTI外部中断 触发方式 中断线映射 中断使能 标志位清除 NVIC中断优先级与嵌套 抢占优先级 子优先级 中断嵌套规则 GPIO作为中断源 中断请求 数字越小优先级越高 抢占可打断,子优先不可

从图上你能看到,GPIO模式配置是基础,EXTI中断是桥梁,NVIC优先级是调度核心。这三层搞明白了,GD32V的中断系统你就拿下了。

本章核心要点:

  • 推挽输出强驱动,开漏输出适合总线
  • 上拉下拉防浮空,浮空输入慎用
  • EXTI配置分六步,标志位清除不能忘
  • 抢占优先级决定打断能力,子优先级决定排队顺序
  • 中断服务函数要短小精悍,别在里面做复杂运算