上一次我们用点亮的LED向它致敬。但嵌入式系统的魅力,不仅在于“输出”,更在于“感知”。今天,让我们为这块老将赋予“听觉”——通过按键输入,让开发板听懂你的指令,用按键控制LED的亮灭。

本文将从硬件原理出发,分别用标准库和寄存器两种方式实现按键检测,并深入讲解按键消抖、支持连按与不支持连按两种扫描模式,带你全面掌握STM32的GPIO输入功能。

一、准备工作:软硬件清单
硬件准备
  • STM32F103开发板一块(正点原子战舰/精英/Mini均可)
  • STLINK或J-Link仿真器(用于下载程序)
  • 杜邦线若干(如需外接按键)
软件准备
  • 开发环境:Keil MDK(本文以Keil uVision5为例)
  • 固件库:STM32F1xx标准外设库
  • 核心资料:开发板的原理图PDF(查找按键引脚的关键)
特别提醒:不同开发板的按键连接引脚可能不同。正点原子精英版通常将KEY0、KEY1、KEY2连接在PE4、PE3、PE2,KEY_UP连接在PA0;而战舰版可能有所不同。务必以你的开发板原理图为准。
二、硬件分析:按键是如何被“感知”的?
按键电路设计:
在嵌入式系统中,按键的硬件连接通常有两种方式:
1、一端接GND,另一端接GPIO(低电平有效)
  • 未按下时:GPIO引脚通过上拉电阻接到VCC,为高电平
  • 按下时:GPIO引脚与GND导通,为低电平
  • 需配置GPIO为上拉输入模式
2、一端接VCC,另一端接GPIO(高电平有效)
  • 未按下时:GPIO引脚通过下拉电阻接到GND,为低电平
  • 按下时:GPIO引脚与VCC导通,为高电平
  • 需配置GPIO为下拉输入模式
以正点原子精英版为例:
  • KEY0、KEY1:一端接GND,按下为低电平 → 需配置上拉输入
  • KEY_UP:一端接VCC 3.3V,按下为高电平 → 需配置下拉输入
KEY_UP:

KEY0、KEY1、KEY2:

按键抖动的秘密:
机械按键有一个“天生缺陷”——抖动。当按键按下或松开时,由于机械触点的弹性,信号不会瞬间稳定,而是会产生多次抖动,持续时间一般为5-10ms。
如果不做处理,一次按键可能会被误判为多次触发,造成严重后果。因此,按键消抖是嵌入式开发中必不可少的一环。
消抖方法:
  • 硬件消抖:在按键电路上加电容滤波(部分开发板已集成)
  • 软件消抖:检测到按键状态变化后,延时10-20ms再次检测
三、代码实战:标准库实现按键检测
下面是代码内容,延时函数,gpio.h等都已省略。
1 、创建按键驱动文件
在工程目录下,新建 gpio.c文件。下面是LED和按键初始化函数:
void LED_Init(void)
{
    GPIO_InitTypeDef  GPIO_InitStructure;
 	
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOE, ENABLE);	 //使能PB,PE端口时钟
	
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;				 //LED0-->PB.5 端口配置
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 //推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		 //IO口速度为50MHz
    GPIO_Init(GPIOB, &GPIO_InitStructure);					 //根据设定参数初始化GPIOB.5
    GPIO_SetBits(GPIOB,GPIO_Pin_5);						    //PB.5 输出高

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;	    		 //LED1-->PE.5 端口配置, 推挽输出
    GPIO_Init(GPIOE, &GPIO_InitStructure);	  				 //推挽输出 ,IO口速度为50MHz
    GPIO_SetBits(GPIOE,GPIO_Pin_5); 						 //PE.5 输出高 
}

//按键初始化函数
void KEY_Init(void) //IO初始化
{ 
 	GPIO_InitTypeDef GPIO_InitStructure;
 
 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOE,ENABLE);//使能PORTA,PORTE时钟

	GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4;//KEY0-KEY2
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入
 	GPIO_Init(GPIOE, &GPIO_InitStructure);//初始化GPIOE2,3,4

	//初始化 WK_UP-->GPIOA.0	  下拉输入
	GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0设置成输入,默认下拉	  
	GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.0

}
//按键处理函数
//返回按键值
//mode:0,不支持连续按;1,支持连续按;
//0,没有任何按键按下
//1,KEY0按下
//2,KEY1按下
//3,KEY2按下 
//4,KEY3按下 WK_UP
//注意此函数有响应优先级,KEY0>KEY1>KEY2>KEY3!!
u8 KEY_Scan(u8 mode)
{	 
	static u8 key_up=1;//按键按松开标志
	if(mode)key_up=1;  //支持连按		  
	if(key_up&&(KEY0==0||KEY1==0||KEY2==0||WK_UP==1))
	{
		delay_ms(10);//去抖动 
		key_up=0;
		if(KEY0==0)return KEY0_PRES;
		else if(KEY1==0)return KEY1_PRES;
		else if(KEY2==0)return KEY2_PRES;
		else if(WK_UP==1)return WKUP_PRES;
	}else if(KEY0==1&&KEY1==1&&KEY2==1&&WK_UP==0)key_up=1; 	    
 	return 0;// 无按键按下
}
下面是主函数内容:
int main(void)
 {
 	vu8 key=0;	
	delay_init();	    	 //延时函数初始化	  
 	LED_Init();			     //LED端口初始化
	KEY_Init();          //初始化与按键连接的硬件接口
	BEEP_Init();         	//初始化蜂鸣器端口
	LED0=0;					//先点亮红灯
	while(1)
	{
 		key=KEY_Scan(0);	//得到键值
	   	if(key)
		{						   
			switch(key)
			{				 
				case WKUP_PRES:	//控制蜂鸣器
					BEEP=!BEEP;
					break;
				case KEY2_PRES:	//控制LED0翻转
					LED0=!LED0;
					break;
				case KEY1_PRES:	//控制LED1翻转	 
					LED1=!LED1;
					break;
				case KEY0_PRES:	//同时控制LED0,LED1翻转 
					LED0=!LED0;
					LED1=!LED1;
					break;
			}
		}else delay_ms(10); 
	}	 
}
四、深入理解:寄存器方式实现按键检测
如果想更深入地理解底层原理,可以直接操作寄存器来实现按键检测。
1、 寄存器版按键初始化:
void KEY_Init_Reg(void)
{
    /* 使能GPIO时钟:RCC_APB2ENR寄存器 */
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPEEN;  // 使能PORTA和PORTE时钟
    
    /* 配置PE4为上拉输入(KEY0) */
    GPIOE->CRL &= ~(0xF << (4 * 4));   // 清除PE4配置位
    GPIOE->CRL |= (0x8 << (4 * 4));    // 0x8表示上拉/下拉输入模式
    
    /* 配置PE3为上拉输入(KEY1) */
    GPIOE->CRL &= ~(0xF << (3 * 4));
    GPIOE->CRL |= (0x8 << (3 * 4));
    
    /* 配置PA0为下拉输入(KEY_UP) */
    GPIOA->CRL &= ~(0xF << (0 * 4));
    GPIOA->CRL |= (0x8 << (0 * 4));    // 先设置为上拉/下拉输入模式
    
    /* 通过ODR寄存器设置上下拉方向:
       对应位写1为上拉,写0为下拉 */
    GPIOA->ODR &= ~(1 << 0);  // PA0下拉
    GPIOE->ODR |= (1 << 3);   // PE3上拉
    GPIOE->ODR |= (1 << 4);   // PE4上拉
}
2、 寄存器版读取按键:
/* 读取按键状态 */
// 读取PE4
#define KEY0_GET_REG()  ((GPIOE->IDR & (1 << 4)) ? 1 : 0)
// 读取PE3
#define KEY1_GET_REG()  ((GPIOE->IDR & (1 << 3)) ? 1 : 0)
// 读取PA0
#define KEY_UP_GET_REG() ((GPIOA->IDR & (1 << 0)) ? 1 : 0)
寄存器操作虽然代码量稍大,但执行效率最高,且能帮助你深入理解STM32参考手册中GPIO寄存器的含义
五、进阶玩法:矩阵键盘与中断
1、 矩阵键盘扫描
当需要大量按键时,为节省GPIO资源,通常会使用矩阵键盘。以4×4矩阵键盘为例,只需8个GPIO即可实现16个按键
矩阵键盘扫描原理:
将4行设置为输出,4列设置为输入(带上拉)
逐行输出低电平,读取列值
根据行号和列号确定按下的按键
// 矩阵键盘扫描函数示例
uint8_t MatrixKey_Scan(void)
{
    uint8_t row, col;
    
    for (row = 0; row < 4; row++) {
        // 将所有行置高,再将当前行置低
        GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3);
        GPIO_ResetBits(GPIOA, (1 << row));
        
        // 读取列值
        uint8_t col_val = GPIO_ReadInputData(GPIOB) & 0x0F;
        
        if (col_val != 0x0F) {  // 有按键按下
            delay_ms(10);       // 消抖
            // 再次确认...
            // 计算按键值并返回
        }
    }
    return 0;  // 无按键
}
2、 外部中断方式
在实际产品中,轮询方式会一直占用CPU,效率较低。更优的方案是使用外部中断:当按键按下时触发中断,在中断服务函数中处理按键事件
外部中断配置要点:
  • 配置GPIO为输入模式
  • 配置EXTI中断线,选择触发边沿(上升沿、下降沿或双边沿)
  • 配置NVIC中断优先级
  • 编写中断服务函数
// 外部中断初始化示例(PA0)
void EXTI0_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    EXTI_InitTypeDef EXTI_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;
    
    /* 使能时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
    
    /* 配置PA0为输入 */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;  // 下拉输入
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    /* 配置EXTI线 */
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
    EXTI_InitStructure.EXTI_Line = EXTI_Line0;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;  // 上升沿触发
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStructure);
    
    /* 配置NVIC */
    NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}
中断函数:
// 中断服务函数
void EXTI0_IRQHandler(void)
{
    if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
        delay_ms(10);  // 消抖
        if (KEY_UP_GET() == 1) {  // 再次确认
            // 处理按键事件
            LED0_TOGGLE();
        }
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}
六、常见问题与调试技巧
1、 按键误触发
  • 原因:未做消抖或消抖时间不足
  • 解决:增加10-20ms延时消抖
2、 按键无响应
  • 原因:GPIO模式配置错误(上拉/下拉方向反了)
  • 解决:对照原理图检查电路连接,确认按键有效电平
3、 一次按键多次响应
  • 原因:未使用“不支持连按”模式
  • 解决:使用static变量记录按键状态,或采用松手检测
4、 ADC按键采样不准
如果使用ADC方式扫描按键(多个按键通过分压电阻连接到一个ADC引脚),需注意信号抖动问题。官方建议加入延时待信号稳定后再采样,或使用一阶递归滤波算法平滑采样值
七、结语:从感知世界到理解世界
        从点亮LED到检测按键,我们从“输出”走向了“输入”。这不仅是技术的一小步,更是思维的一大步——单片机开始能够感知外部世界的变化,并根据这些变化做出响应。这正是嵌入式系统的核心魅力所在。