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暂停当前任务,执行中断服务函数。处理完再回去继续干活。
配置外部中断的步骤,我总结为四步:
- 配置GPIO为输入模式(和上面一样)
- 连接GPIO到EXTI线(通过SYSCFG寄存器)
- 配置EXTI触发方式(上升沿、下降沿、或双边沿)
- 使能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和外部中断的核心逻辑串起来了。建议你保存下来,写代码时对照着看。
4.6 避坑指南
最后,分享几个我踩过的坑:
- 时钟没开就操作寄存器:写进去的值根本不会生效。调试时读回寄存器全是0,别问我怎么知道的。
- 中断优先级没配好:多个中断同时触发时,优先级低的可能永远得不到响应。我建议系统中断用0-1,外设中断用2-3。
- 中断服务函数里做耗时操作:比如printf、延时。中断讲究快进快出,复杂逻辑放到主循环里处理。
- 忘了清除中断标志位:这个前面说过了,再强调一遍——不清标志位,系统会一直进中断。
个人建议:刚开始学的时候,先用库函数或HAL库把功能调通,再去看寄存器版本。这样既能快速上手,又能深入理解底层。我自己带团队时,也是让新人先用HAL库,两周后再切到寄存器开发。
好了,这一章的内容就到这里。GPIO看似简单,但它是所有外设驱动的基础。把这一章吃透了,后面学定时器、PWM、DMA都会轻松很多。
公众号:蓝海资料掘金营,微信deep3321