stm32f103学习笔记-13-GPIO输入(按键检测)
在bsp_key.h// KEY2定义#define KEY2_GPIO_PIN GPIO_Pin_1 // 根据实际硬件连接修改#define KEY2_GPIO_PORT GPIOA // 根据实际硬件连接修改在bsp_led.h// 红色LED定义。
一、按键电路原理解说

想象一下,我们要让一个很“娇贵”的大脑(单片机MCU)知道你的手指有没有按下一个开关。这个电路就是为它们搭建的一个安全、可靠的“沟通桥梁”。整个解说的思路,我们可以用一张图来概括,帮助你建立整体概念:

1. 核心目标:按键信号检测
MCU的GPIO引脚(如PA0或PC13)是可配置的数字输入/输出端口,能够检测或输出逻辑电平:"高电平"(通常3.3V,表示"有电")和"低电平"(0V,表示"没电")。本电路的设计目标是:当按键未按下时,GPIO引脚(如PA0)检测到低电平;当按键按下时,检测到高电平,从而向MCU提供明确的输入信号。
2. 电路实现原理
以KEY1(连接PA0)为例,分析电路在两种状态下的行为。
2.1. 按键未按下时(静止状态)
-
电流路径:3.3V电源 → 限流电阻R4(4.7kΩ) → 分流点。
-
主路径:电流通过下拉电阻R7(1kΩ) → 流入地(GND)。
-
支路径:电流为电容C6(0.1μF)充电,但电容很快充满后,支路对直流电视为开路。
-
-
引脚PA0电平:下拉电阻R7将PA0引脚稳定"拉"至地电平(0V),确保引脚检测到低电平。这避免了引脚悬空时的噪声干扰,提供稳定的空闲状态。
2.2. 按键按下时(动作状态)
-
电流路径:3.3V电源 → 按键K1(闭合) → 分流点。
-
路径1(主导):电流直接通过电容C6 → 到达PA0引脚。
-
路径2:电流通过限流电阻R4 → 到达PA0引脚。
-
-
引脚PA0电平:3.3V高电平通过低阻抗路径(按键和电容)施加到PA0引脚,压倒下拉电阻R7的拉低作用,引脚电压迅速上升至3.3V(高电平)。电容C6平滑电压上升过程,抑制按键机械抖动引起的毛刺。
3. 关键元件功能分析
每个元件在电路中扮演特定角色,确保可靠性和安全性。
3.1. 限流电阻R4/R5(4.7kΩ)——过流保护
-
作用:限制从3.3V电源到GPIO引脚的电流,防止按键按下时形成短路电流,损坏MCU内部结构(根据STM32F10xxx数据手册,GPIO引脚最大输入电流通常为25mA)。4.7kΩ电阻将电流限制在安全范围内(约0.7mA),同时允许足够电流驱动输入检测。
3.2. 下拉电阻R7/R11(1kΩ)——电平稳定
-
作用:在按键未按下时,将PA0引脚明确拉至低电平,防止引脚浮空引入电磁干扰或不确定电平。这确保了MCU在空闲状态下不会误触发中断或事件。
3.3. 电容C6/C14(0.1μF)——硬件消抖
-
作用:消除机械按键的抖动效应。按键按下时,金属触点会发生弹跳(持续数毫秒),产生电平波动。电容通过充电和放电过程平滑电压变化:
-
充电阶段:按键按下瞬间,电容充电,电压缓慢上升,过滤抖动毛刺。
-
放电阶段:按键释放时,电容通过下拉电阻R7放电,电压缓慢下降。
-
-
优势:硬件消抖消除了软件中延时的需要,简化编程并提高响应可靠性。
4. 整体工作流程与编程指南
电路与MCU协作实现按键检测:
-
按键按下:手指按下按键K1,3.3V高电平通过按键和电容C6,产生平滑的上升电压信号。
-
电平检测:PA0引脚电平从低电平(0V)变为高电平(3.3V),形成上升沿。
-
MCU响应:MCU程序持续监控PA0引脚(通过读取GPIO输入数据寄存器IDR)。一旦检测到上升沿(通过中断或轮询),判定为有效按键按下。
-
动作执行:程序执行相应操作,如控制LED(通过GPIO输出数据寄存器ODR或位设置寄存器BSRR实现电平翻转)。
5. 为什么设计为上升沿触发?
PA0引脚常用于低功耗模式唤醒(如待机模式),根据STM32F10xxx参考手册第4章,唤醒信号通常要求上升沿触发。为了统一按键行为与唤醒机制,本电路设计为按下时产生高电平(上升沿),确保兼容性和一致性。这避免了混合触发类型的复杂性,简化系统设计。
二、按键检测程序设计详细笔记
1. 硬件连接分析
从“按键电路原理解说”可知:
-
LED_G(绿色LED)连接在GPIOB的Pin0
-
KEY1按键连接在某个GPIO端口(具体端口需查看bsp_key.h定义)
-
需要添加KEY2按键控制红色LED
2. 按键检测原理
2.1 按键硬件连接方式
// 当前KEY1配置为浮空输入模式
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
浮空输入模式适合按键直接连接到GPIO,没有上拉或下拉电阻的情况。
2.2 按键扫描算法分析
uint8_t Key_Scan(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
{
if(GPIO_ReadInputDataBit(GPIOx, GPIO_Pin) == KEY_ON)
{
// 等待按键释放
while(GPIO_ReadInputDataBit(GPIOx, GPIO_Pin) == KEY_ON);
return KEY_ON;
}
else return KEY_OFF;
}
当前算法特点:
-
简单但存在缺陷
-
阻塞式检测:在按键按下期间会一直等待
-
没有消抖处理:容易产生误触发
-
不支持连续按键检测
2.3按键扫描函数与LED切换原理
根据《STM32F10xxx参考手册》第8章“通用和复用功能I/O(GPIO和AFIO)”及第18章“外部中断/事件控制器(EXTI)”的内容,结合文档中的寄存器描述、功能框图及代码实现原理,进行详细分析。
2.3.1 void Key_Scan与 uint8_t Key_Scan函数的区别
这两个函数的主要区别在于返回值类型和功能目的:
-
void Key_Scan(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)此函数通常用于中断方式的按键检测。它通过配置外部中断(EXTI)来触发中断服务函数(ISR),在ISR内部直接处理按键动作(如控制LED),而无需返回状态值。
-
原理:
-
根据文档第18章,EXTI可将GPIO引脚配置为中断线(如上升沿/下降沿触发)。当按键按下时,EXTI产生中断,CPU跳转到ISR执行
Key_Scan函数。 -
函数内部可能直接操作寄存器(如读取GPIOx_IDR)判断引脚电平,但结果不返回,而是直接执行动作(如翻转LED)。
-
示例代码(基于文档18.5节):
// 在EXTI中断服务函数中调用 void Key_Scan(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) { if (GPIO_ReadInputDataBit(GPIOx, GPIO_Pin) == KEY_PRESSED) { LED_Toggle(); // 直接行动,无返回值 } }
-
-
-
uint8_t Key_Scan(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)此函数用于轮询方式的按键检测,通过返回值告知调用者按键状态(如
KEY_ON或KEY_OFF)。-
原理:
-
基于文档第8章的GPIO输入配置(8.1.7节),函数读取GPIO输入数据寄存器(GPIOx_IDR)的对应引脚电平。
-
返回值通常为
uint8_t类型,表示按键状态(例如,1为按下,0为释放)。 -
示例代码(基于文档8.2.3节):
uint8_t Key_Scan(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) { if (GPIO_ReadInputDataBit(GPIOx, GPIO_Pin) == KEY_PRESSED) { return KEY_ON; // 返回状态,由主函数处理 } return KEY_OFF; }
-
-
关键区别总结:
|
特性 |
|
|
|---|---|---|
|
返回值 |
无返回值 |
返回按键状态( |
|
使用场景 |
中断驱动,实时性高 |
轮询检测,灵活性高 |
|
资源占用 |
依赖中断资源 |
占用CPU时间片 |
|
文档依据 |
EXTI中断配置(第18章) |
GPIO输入模式(第8章) |
关联图片:
GPIO输入配置涉及浮空、上拉/下拉模式,其硬件结构见文档图15:

2.3.2 LED_G_TOGGLE功能实现原理
在main.c代码中,LED_G_TOGGLE通过宏定义实现LED状态的切换:
#define LED_G_TOGGLE {LED_G_GPIO_PORT->ODR ^= LED_G_GPIO_PIN;}
其原理基于GPIO输出数据寄存器(ODR)的位操作,具体分析如下:
(1)硬件基础:ODR寄存器作用
-
ODR寄存器功能(文档8.2.4节):
ODR(Output Data Register)用于控制GPIO引脚的输出电平。每个位对应一个引脚:
-
写
0:引脚输出低电平。 -
写
1:引脚输出高电平。
-
-
地址映射:ODR寄存器是32位,但STM32允许按位操作(通过位带别名区或直接访问)。
(2)异或操作(^=)的切换逻辑
-
代码解析:
LED_G_GPIO_PORT->ODR ^= LED_G_GPIO_PIN;-
LED_G_GPIO_PIN是一个宏,表示LED对应引脚的位掩码(如GPIO_Pin_0定义为(1 << 0))。 -
异或操作(
^)的特性:-
若当前ODR的该位为
0,则0 ^ 1 = 1(输出高电平,LED亮)。 -
若当前ODR的该位为
1,则1 ^ 1 = 0(输出低电平,LED灭)。
-
-
因此,每次执行该语句都会翻转引脚电平,实现LED亮/灭切换。
-
(3)配置前提:GPIO必须为输出模式
-
根据文档8.1.8节,使用ODR前需将GPIO配置为输出模式(推挽或开漏):
-
推挽输出:可直接驱动LED(如连接VCC,低电平点亮)。
-
代码中需先调用
LED_GPIO_Config()初始化引脚(如设置GPIOx_CRL/CRH寄存器)。
-
关联图片:
GPIO输出模式的结构见文档图16,展示了ODR如何控制输出驱动器:

(4)完整工作流程
-
初始化(
LED_GPIO_Config()):-
设置GPIO端口时钟(通过RCC_APB2ENR)。
-
配置GPIO为输出模式(如推挽输出,速度10MHz)。
-
-
切换操作(
LED_G_TOGGLE):-
直接修改ODR寄存器,避免使用库函数(减少开销)。
-
示例电平变化:
-
初始ODR位=0 → LED灭 → 执行后ODR位=1 → LED亮。
-
再次执行后ODR位=0 → LED灭,循环切换。
-
-
三、按键检测拓展实践(选看)
1. 改进的按键检测方案
1.1 添加按键消抖功能
// 改进的按键扫描函数(带消抖)
uint8_t Key_Scan_Advanced(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
{
static uint8_t key_state = 0; // 按键状态机
static uint32_t key_time = 0; // 计时器
switch(key_state)
{
case 0: // 等待按键按下
if(GPIO_ReadInputDataBit(GPIOx, GPIO_Pin) == KEY_ON)
{
key_time = Get_Tick(); // 获取当前时间戳
key_state = 1;
}
break;
case 1: // 消抖检测
if((Get_Tick() - key_time) > DEBOUNCE_TIME) // 消抖时间(如10ms)
{
if(GPIO_ReadInputDataBit(GPIOx, GPIO_Pin) == KEY_ON)
{
key_state = 2;
return KEY_ON; // 返回有效的按键按下
}
else
{
key_state = 0; // 抖动,重新检测
}
}
break;
case 2: // 等待按键释放
if(GPIO_ReadInputDataBit(GPIOx, GPIO_Pin) == KEY_OFF)
{
key_state = 0;
}
break;
}
return KEY_OFF;
}
1.2 支持多按键同时检测
typedef struct {
GPIO_TypeDef* GPIOx;
uint16_t GPIO_Pin;
uint8_t last_state;
uint32_t press_time;
uint32_t release_time;
} Key_TypeDef;
Key_TypeDef keys[2] = {
{KEY1_GPIO_PORT, KEY1_GPIO_PIN, KEY_OFF, 0, 0},
{KEY2_GPIO_PORT, KEY2_GPIO_PIN, KEY_OFF, 0, 0}
};
void Keys_Scan(void)
{
for(int i = 0; i < 2; i++)
{
uint8_t current_state = GPIO_ReadInputDataBit(keys[i].GPIOx, keys[i].GPIO_Pin);
if(current_state != keys[i].last_state)
{
if(current_state == KEY_ON)
{
keys[i].press_time = Get_Tick();
}
else
{
keys[i].release_time = Get_Tick();
// 处理按键释放事件
}
keys[i].last_state = current_state;
}
}
}
2. 添加KEY2按键功能
2.1 修改头文件定义
在bsp_key.h中添加:
// KEY2定义
#define KEY2_GPIO_PIN GPIO_Pin_1 // 根据实际硬件连接修改
#define KEY2_GPIO_PORT GPIOA // 根据实际硬件连接修改
#define KEY2_GPIO_CLK RCC_APB2Periph_GPIOA
2.2 修改按键配置函数
void KEY_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
// KEY1初始化
RCC_APB2PeriphClockCmd(KEY1_GPIO_CLK, ENABLE);
GPIO_InitStruct.GPIO_Pin = KEY1_GPIO_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(KEY1_GPIO_PORT, &GPIO_InitStruct);
// KEY2初始化
RCC_APB2PeriphClockCmd(KEY2_GPIO_CLK, ENABLE);
GPIO_InitStruct.GPIO_Pin = KEY2_GPIO_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(KEY2_GPIO_PORT, &GPIO_InitStruct);
}
2.3 添加红色LED定义
在bsp_led.h中添加:
// 红色LED定义
#define LED_R_GPIO_PIN GPIO_Pin_1
#define LED_R_GPIO_PORT GPIOB
#define LED_R_GPIO_CLK RCC_APB2Periph_GPIOB
#define LED_R(a) if(a)\
GPIO_ResetBits(LED_R_GPIO_PORT, LED_R_GPIO_PIN);\
else GPIO_SetBits(LED_R_GPIO_PORT, LED_R_GPIO_PIN);
#define LED_R_TOGGLE {LED_R_GPIO_PORT->ODR ^= LED_R_GPIO_PIN;}
2.4 修改主函数
int main(void)
{
LED_GPIO_Config();
KEY_GPIO_Config();
while(1)
{
// KEY1控制绿色LED
if(Key_Scan(KEY1_GPIO_PORT, KEY1_GPIO_PIN) == KEY_ON)
LED_G_TOGGLE;
// KEY2控制红色LED
if(Key_Scan(KEY2_GPIO_PORT, KEY2_GPIO_PIN) == KEY_ON)
LED_R_TOGGLE;
}
}
2.5 高级功能扩展1:长短按检测
typedef enum {
KEY_EVENT_NONE = 0,
KEY_EVENT_PRESS,
KEY_EVENT_SHORT_PRESS,
KEY_EVENT_LONG_PRESS
} Key_Event_t;
Key_Event_t Get_Key_Event(Key_TypeDef* key)
{
uint32_t current_time = Get_Tick();
uint8_t current_state = GPIO_ReadInputDataBit(key->GPIOx, key->GPIO_Pin);
if(current_state == KEY_ON && key->last_state == KEY_OFF)
{
key->press_time = current_time;
key->last_state = KEY_ON;
return KEY_EVENT_PRESS;
}
else if(current_state == KEY_OFF && key->last_state == KEY_ON)
{
key->last_state = KEY_OFF;
key->release_time = current_time;
if((key->release_time - key->press_time) > LONG_PRESS_TIME)
return KEY_EVENT_LONG_PRESS;
else
return KEY_EVENT_SHORT_PRESS;
}
return KEY_EVENT_NONE;
}
2.6 高级功能扩展2:使用外部中断优化
对于实时性要求高的应用,建议使用外部中断:
void EXTI_Config(void)
{
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 连接EXTI线到按键GPIO
GPIO_EXTILineConfig(KEY1_GPIO_PORT_SOURCE, KEY1_GPIO_PIN_SOURCE);
// 配置EXTI线
EXTI_InitStructure.EXTI_Line = KEY1_EXTI_LINE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
// 配置NVIC
NVIC_InitStructure.NVIC_IRQChannel = KEY1_EXTI_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x0F;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0F;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
更多推荐



所有评论(0)