从“点亮”到“感知”:STM32F103按键输入入门——让开发板听懂你的指令
关键词: STM32F103,按键输入,按键检测,按键消抖,标准库,寄存器操作,GPIO输入,外部中断,矩阵键盘,嵌入式入门教程
从硬件原理到代码实战,手把手教你STM32F103按键输入。涵盖GPIO配置、按键消抖、标准库与寄存器两种实现,并深入讲解矩阵键盘与外部中断。附完整代码,嵌入式新手入门必看!
上一次我们用点亮的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到检测按键,我们从“输出”走向了“输入”。这不仅是技术的一小步,更是思维的一大步——单片机开始能够感知外部世界的变化,并根据这些变化做出响应。这正是嵌入式系统的核心魅力所在。
评论 (0)
登录后即可发表评论