2. GPIO与按键中断基础

各位同学,咱们今天来聊聊嵌入式开发里最基础、也最常用的两个外设——GPIO和外部中断。说白了,就是怎么让芯片“看”到外部世界的变化,再“动”起来。

我刚开始学STM32的时候,觉得GPIO不就是个高低电平嘛,有啥好学的?结果第一个项目就栽在按键抖动上,LED死活不按我预想的亮灭。嗯,从那以后,我再也不敢小看这些基础操作了。

2.1 GPIO输出驱动LED——让芯片“说话”

GPIO,全称General Purpose Input Output,通用输入输出口。你可以把它想象成芯片的“手脚”——用来对外输出信号,或者读取外部信号。

驱动LED,是GPIO输出最典型的应用。我们以STM32F103系列为例,看看具体怎么配置。

2.1.1 GPIO模式选择

STM32的GPIO有好几种模式,我列个表,大家一看就明白:

模式 说明 典型应用
推挽输出 可以输出高电平和低电平,驱动能力强 驱动LED、蜂鸣器
开漏输出 只能输出低电平,高电平需要外部上拉 I2C总线、多路信号线与
浮空输入 引脚电平不确定,容易受干扰 基本不用
上拉输入 默认高电平,外部拉低时读取到低电平 按键检测(按下为低)
下拉输入 默认低电平,外部拉高时读取到高电平 按键检测(按下为高)
模拟输入 用于ADC采集模拟信号 电压测量、传感器

驱动LED,我们选推挽输出。为什么?因为推挽输出既能灌电流也能拉电流,驱动LED最合适。

2.1.2 代码实战:点亮LED

直接上代码,我习惯用寄存器操作,因为这样能让你真正理解底层在干什么。当然,HAL库也可以,但咱们做裸机调度,寄存器更直接。

// 开启GPIOB时钟(挂在APB2总线上)
RCC->APB2ENR |= (1 << 3);  // 位3对应GPIOB

// 配置PB0为推挽输出,50MHz速度
GPIOB->CRL &= ~(0x0F << 0);  // 先清除配置位
GPIOB->CRL |= (0x03 << 0);   // 0x03 = 推挽输出,50MHz

// 点亮LED(低电平点亮)
GPIOB->BRR = (1 << 0);  // 复位PB0,输出低电平

// 熄灭LED
GPIOB->BSRR = (1 << 0); // 置位PB0,输出高电平
小提示: 我刚开始写代码时,总搞混BSRR和BRR。记住:BSRR是“置位”,让引脚输出高电平;BRR是“复位”,让引脚输出低电平。BSRR的高16位也可以复位,但BRR更直观。

2.2 外部中断EXTI配置——让芯片“听”到外部信号

光会输出还不够,芯片还得能“听”到外部事件。比如按键按下、传感器触发,这些都需要中断来响应。

外部中断EXTI,就是专门干这个的。它可以把GPIO引脚上的电平变化,转化成中断请求,让CPU暂停当前任务,去处理这个事件。

2.2.1 EXTI的触发方式

EXTI支持三种触发方式:

  • 上升沿触发: 信号从低变高时触发
  • 下降沿触发: 信号从高变低时触发
  • 双边沿触发: 上升沿和下降沿都触发

按键中断,我一般用下降沿触发。为什么?因为按键按下时,电平从高变低,下降沿正好对应这个动作。当然,如果你需要同时检测按下和松开,那就用双边沿。

2.2.2 配置步骤

配置EXTI,我总结了四个步骤,按顺序来,不容易出错:

  1. 配置GPIO为输入模式——告诉芯片这个引脚是用来读的
  2. 配置EXTI线——把GPIO引脚映射到对应的EXTI线
  3. 配置触发方式——选择上升沿、下降沿还是双边沿
  4. 配置NVIC——使能中断通道,设置优先级

来看代码:

// 1. 配置PA0为输入模式(上拉输入)
RCC->APB2ENR |= (1 << 2);  // 开启GPIOA时钟
GPIOA->CRL &= ~(0x0F << 0); // 清除配置
GPIOA->CRL |= (0x08 << 0);  // 0x08 = 上拉输入

// 2. 配置EXTI线0(PA0对应EXTI0)
AFIO->EXTICR[0] &= ~(0x0F << 0); // 清除配置
AFIO->EXTICR[0] |= (0x00 << 0);  // 0x00 = PA口

// 3. 配置下降沿触发
EXTI->FTSR |= (1 << 0);  // 下降沿触发寄存器

// 4. 使能EXTI线0
EXTI->IMR |= (1 << 0);   // 中断屏蔽寄存器

// 5. 配置NVIC
NVIC->ISER[0] |= (1 << 6); // EXTI0中断通道号是6
注意: 配置AFIO之前,必须先开启AFIO时钟!否则EXTICR寄存器写不进去。我当初就因为这个bug,查了整整一个下午。
// 开启AFIO时钟
RCC->APB2ENR |= (1 << 0);  // 位0对应AFIO

2.3 中断服务函数编写规范——别在中断里“磨洋工”

中断服务函数,也就是ISR(Interrupt Service Routine),是中断处理的核心。但很多新手容易犯一个错误——在中断里写太多代码。

我给大家一个原则:中断服务函数要短、平、快。为什么?因为中断是在打断主程序执行的,你在中断里待得越久,主程序就被耽误得越久。如果中断里再嵌套中断,那系统响应就乱套了。

2.3.1 ISR的基本结构

一个规范的ISR,应该包含以下部分:

  • 清除中断标志位——告诉硬件“我已经处理完了”
  • 处理紧急事件——比如读取数据、设置标志位
  • 退出中断——返回主程序

来看一个按键中断的ISR示例:

void EXTI0_IRQHandler(void)
{
    // 1. 清除中断标志位(必须!)
    if (EXTI->PR & (1 << 0))
    {
        EXTI->PR = (1 << 0);  // 写1清除
    }

    // 2. 处理紧急事件——设置一个标志位
    // 不要在中断里做复杂操作,比如延时、打印
    g_button_pressed = 1;

    // 3. 退出中断(硬件自动完成,不需要手动写)
}
核心要点: 中断里只做“标记”,不做“处理”。真正的业务逻辑,放到主循环里去做。这样既保证了中断的快速响应,又避免了中断阻塞。

2.3.2 避坑指南——我踩过的那些坑

做嵌入式这么多年,我在中断上栽过不少跟头。分享几个典型的:

  • 忘记清除标志位: 中断标志位不清除,中断会反复触发,系统直接卡死。我曾经因为这个,在客户现场调试到凌晨两点。
  • 在中断里用printf: printf是阻塞的,而且耗时很长。在中断里用printf,相当于把整个系统按了暂停键。我建议用串口DMA或者环形缓冲区。
  • 中断里调用HAL_Delay: HAL_Delay依赖SysTick中断,而SysTick中断优先级可能比当前中断低,导致死锁。这个坑我踩过两次才长记性。

2.4 本章知识体系

为了让大家更直观地理解GPIO和中断的关系,我画了一张图:

GPIO与外部中断知识体系 GPIO输出驱动LED 配置推挽输出模式 设置输出电平(BSRR/BRR) 控制LED亮灭 外部中断EXTI配置 配置GPIO输入模式 映射EXTI线 配置触发方式 配置NVIC优先级 协同工作 中断服务函数编写规范 核心原则:中断里只做标记,不做处理;业务逻辑放到主循环 避坑:忘记清除标志位、在中断里用printf/HAL_Delay

2.5 小结

这一章,我们讲了GPIO输出驱动LED、外部中断EXTI的配置,以及中断服务函数的编写规范。说白了,就是让芯片既能“动”(输出),又能“听”(中断)。

我个人觉得,这两块是嵌入式开发的“基本功”。你想想看,如果连GPIO和中断都搞不定,那后面更复杂的任务调度、多任务管理,根本无从谈起。