4. GPIO与外部中断:从最简单的GPIO控制开始

各位同学,欢迎来到第四章。这一章我们聊点实在的——GPIO。说白了,就是让芯片的引脚听话,要么输出高低电平,要么读取外部信号。这是嵌入式开发最基础的操作,但也是坑最多的地方之一。

我个人习惯,拿到一块新板子,第一件事就是点个灯。别笑,这能快速验证硬件、软件、调试链路是否正常。今天我们就从点灯开始,一步步深入到外部中断。

4.1 引脚模式:输入、输出与复用

STM32的GPIO引脚,可以配置成多种模式。我刚开始学的时候,总觉得模式太多记不住。后来总结了一下,其实就三类:

  • 输入模式:读取外部电平。比如按键、传感器信号。
  • 输出模式:驱动外部设备。比如LED、蜂鸣器。
  • 复用功能:引脚交给片上外设使用。比如UART的TX/RX、SPI的SCK/MOSI。

嗯,这里要注意:复用功能不是你想用就能用的。每个引脚能复用的功能是固定的,得查数据手册。我在项目中遇到过,有人把USART1的TX接到了PB6上,结果死活调不通——因为PB6的复用功能里根本没有USART1_TX。

4.2 配置GPIO:从点灯开始

先看一个最简单的例子。配置PA5为推挽输出,驱动一个LED。

// 使能GPIOA时钟
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

// 配置PA5为输出模式
GPIOA->MODER &= ~(0x3 << (5 * 2));  // 先清零
GPIOA->MODER |= (0x1 << (5 * 2));   // 设置为01(输出)

// 设置输出类型为推挽
GPIOA->OTYPER &= ~(0x1 << 5);       // 0 = 推挽

// 设置速度为高速
GPIOA->OSPEEDR |= (0x3 << (5 * 2)); // 11 = 高速

// 设置无上拉下拉
GPIOA->PUPDR &= ~(0x3 << (5 * 2)); // 00 = 无

// 输出高电平,点亮LED
GPIOA->ODR |= (0x1 << 5);

你想想看,这几行代码其实做了五件事:开时钟、设模式、选类型、定速度、配上下拉。缺一不可。我曾经在调试一个电机驱动板时,忘了配置速度,结果PWM波形上升沿慢得像蜗牛,电机嗡嗡响就是不转。查了半天才发现是OSPEEDR没设。

核心要点:配置GPIO时,时钟使能是第一步。没有时钟,寄存器写不进去。这是新手最容易忽略的。

4.3 读取输入:按键检测

输出会了,输入也不难。以PC13上的按键为例:

// 使能GPIOC时钟
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN;

// 配置PC13为输入模式
GPIOC->MODER &= ~(0x3 << (13 * 2)); // 00 = 输入

// 使能内部上拉
GPIOC->PUPDR |= (0x1 << (13 * 2));  // 01 = 上拉

// 读取电平
uint8_t key_state = (GPIOC->IDR >> 13) & 0x1;

这里有个坑:按键有抖动。机械按键按下和释放时,电平会跳变几次。如果不做消抖,一次按下可能被误判成多次。我建议用定时器做10-20ms的延时消抖,或者用状态机处理。

小技巧:内部上拉/下拉电阻的阻值一般在40kΩ左右。如果外部信号驱动能力弱,可以考虑用外部上拉,阻值选4.7kΩ到10kΩ。

4.4 外部中断:让CPU不再空等

轮询读取按键,CPU一直在忙。如果按键一秒钟才按一次,CPU就白转了一百万次。这时候就该外部中断出场了。

外部中断的原理很简单:引脚电平变化时,硬件自动触发一个中断,CPU暂停当前任务,执行中断服务函数。处理完再回去继续干活。

配置外部中断的步骤,我总结为四步:

  1. 配置GPIO为输入模式(和上面一样)
  2. 连接GPIO到EXTI线(通过SYSCFG寄存器)
  3. 配置EXTI触发方式(上升沿、下降沿、或双边沿)
  4. 使能NVIC中断(设置优先级、使能中断通道)

看个完整例子,配置PA0为外部中断,下降沿触发:

// 1. 配置GPIO
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
GPIOA->MODER &= ~(0x3 << (0 * 2)); // 输入
GPIOA->PUPDR |= (0x1 << (0 * 2));  // 上拉

// 2. 连接GPIO到EXTI0
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
SYSCFG->EXTICR[0] &= ~SYSCFG_EXTICR1_EXTI0;
SYSCFG->EXTICR[0] |= SYSCFG_EXTICR1_EXTI0_PA;

// 3. 配置EXTI
EXTI->IMR |= EXTI_IMR_IM0;      // 不屏蔽中断线0
EXTI->FTSR |= EXTI_FTSR_TR0;    // 下降沿触发

// 4. 配置NVIC
NVIC_SetPriority(EXTI0_IRQn, 2);
NVIC_EnableIRQ(EXTI0_IRQn);

中断服务函数长这样:

void EXTI0_IRQHandler(void)
{
    if (EXTI->PR & EXTI_PR_PR0)
    {
        // 清除中断标志位
        EXTI->PR = EXTI_PR_PR0;

        // 处理中断事件
        // 比如:翻转LED
        GPIOA->ODR ^= (0x1 << 5);
    }
}

警告:中断服务函数里一定要清除标志位!否则会一直触发中断,系统卡死。我曾经在调试一个产品时,忘了清标志位,结果中断一触发,系统就死循环。查了整整一个下午才发现问题。

4.5 知识体系总览

下面这张图,把GPIO和外部中断的核心逻辑串起来了。建议你保存下来,写代码时对照着看。

GPIO与外部中断知识体系 GPIO引脚配置 输入模式 读取IDR寄存器 配置上拉/下拉 按键检测、传感器 输出模式 写ODR/BSRR寄存器 推挽/开漏选择 LED、蜂鸣器、电机 复用功能 AFR寄存器选择 查数据手册引脚定义 UART、SPI、I2C、定时器 外部中断 (EXTI) SYSCFG连接 → EXTI配置 → NVIC使能 输入模式是中断的基础 输入 输出 复用

4.6 避坑指南

最后,分享几个我踩过的坑:

  • 时钟没开就操作寄存器:写进去的值根本不会生效。调试时读回寄存器全是0,别问我怎么知道的。
  • 中断优先级没配好:多个中断同时触发时,优先级低的可能永远得不到响应。我建议系统中断用0-1,外设中断用2-3。
  • 中断服务函数里做耗时操作:比如printf、延时。中断讲究快进快出,复杂逻辑放到主循环里处理。
  • 忘了清除中断标志位:这个前面说过了,再强调一遍——不清标志位,系统会一直进中断。

个人建议:刚开始学的时候,先用库函数或HAL库把功能调通,再去看寄存器版本。这样既能快速上手,又能深入理解底层。我自己带团队时,也是让新人先用HAL库,两周后再切到寄存器开发。

好了,这一章的内容就到这里。GPIO看似简单,但它是所有外设驱动的基础。把这一章吃透了,后面学定时器、PWM、DMA都会轻松很多。


公众号:蓝海资料掘金营,微信deep3321