【四】独立按键(查询方式)实验
1. 项目概述
本项目演示如何使用 STM32F103C8T6 微控制器通过查询方式检测独立按键,并根据按键状态控制LED灯的点亮。系统包含4个独立按键(KEY1-KEY4)和8个LED指示灯,每个按键对应控制不同的LED灯。采用软件消抖和按键状态记忆,实现可靠的按键检测。
技术要点
- ✅ GPIO配置(输入上拉模式)
- ✅ 查询方式按键检测
- ✅ 软件消抖(5ms延时)
- ✅ 按键状态记忆(避免重复触发)
- ✅ 按键释放检测(阻塞等待)
- ✅ LED指示灯控制
- ✅ JTAG引脚复用(释放PB3、PB4)
应用场景
- 🔘 用户输入界面
- 🔘 菜单选择系统
- 🔘 设备控制面板
- 🔘 功能切换按钮
- 🔘 简单人机交互
- 🔘 参数设置界面
2. 硬件平台
主控芯片
- 型号: STM32F103C8T6
- 内核: ARM Cortex-M3
- 主频: 72 MHz
- Flash: 64 KB
- SRAM: 20 KB
外设资源
独立按键(4个)
| 按键 | 引脚 | 控制LED | 说明 |
|---|---|---|---|
| KEY1 | PB8 | LED5 | 按键1 |
| KEY2 | PB9 | LED6 | 按键2 |
| KEY3 | PB10 | LED7 | 按键3 |
| KEY4 | PB11 | LED8 | 按键4 |
LED指示灯(8个)
| LED | 引脚 | 说明 |
|---|---|---|
| LED1 | PB0 | 指示灯1 |
| LED2 | PB1 | 指示灯2 |
| LED3 | PB2 | 指示灯3 |
| LED4 | PB3 | 指示灯4 |
| LED5 | PB4 | 指示灯5(KEY1控制) |
| LED6 | PB5 | 指示灯6(KEY2控制) |
| LED7 | PB6 | 指示灯7(KEY3控制) |
| LED8 | PB7 | 指示灯8(KEY4控制) |
硬件连接
STM32F103C8T6 独立按键
PB8 <---------- KEY1 (按下接GND)
PB9 <---------- KEY2 (按下接GND)
PB10 <---------- KEY3 (按下接GND)
PB11 <---------- KEY4 (按下接GND)
STM32F103C8T6 LED指示灯
PB0-PB7 ----------> LED1-LED8 (高电平点亮)
按键电路说明:
按键电路(以KEY1为例):
VCC
|
╱ 10KΩ上拉电阻
|
PB8 ─┤
|
╱ KEY1(按键)
|
GND
工作原理:
- 按键未按下:PB8 = 高电平(VCC)
- 按键按下:PB8 = 低电平(GND)
- 上拉电阻:确保未按下时有确定的高电平
注意事项:
- 按键使用上拉输入模式(GPIO_Mode_IPU)
- 按键按下时,引脚读取为低电平(0)
- PB3和PB4默认为JTAG功能,需要重映射才能作为GPIO使用
- LED使用推挽输出模式,高电平点亮
3. 项目结构
4A_独立按键(查询)/
├── Libraries/ # STM32标准外设库
│ ├── CMSIS/ # Cortex微控制器软件接口标准
│ └── STM32F10x_StdPeriph_Driver/ # STM32F10x标准外设驱动
├── MDK/ # Keil工程文件
│ ├── TEST_CODE.uvproj # Keil项目文件
│ ├── TEST_CODE.uvopt # 项目配置选项
│ ├── Output/ # 编译输出目录
│ └── list/ # 列表文件目录
├── Readme/ # 说明文档目录
└── User/ # 用户代码目录
├── main.c # 主程序入口
├── stm32f10x_conf.h # 外设库配置文件
├── stm32f10x_it.c # 中断服务函数
├── stm32f10x_it.h # 中断服务函数头文件
├── system_stm32f10x.c # 系统初始化文件
└── TF_App/ # 应用层驱动
├── TF_KeyBoard.c # 按键驱动实现
├── TF_KeyBoard.h # 按键驱动头文件
├── TF_LED.c # LED驱动实现
├── TF_LED.h # LED驱动头文件
├── TF_Delay.c # 延时函数实现
├── TF_Delay.h # 延时函数头文件
├── TF_System_Cfg.c # 系统配置实现
└── TF_System_Cfg.h # 系统配置头文件
4. 功能说明
系统工作流程
系统上电
↓
LED初始化
(配置PB0-PB7为推挽输出)
↓
按键初始化
(配置PB8-PB11为上拉输入)
↓
主循环开始
↓
按键扫描(KeyBoard_Scan)
├─ 检测KEY1(PB8)
│ ├─ 读取引脚电平
│ ├─ 低电平?→ 延时5ms消抖
│ ├─ 再次确认低电平?
│ ├─ 检查上次状态
│ └─ 首次按下?→ 关闭所有LED,点亮LED5
│
├─ 检测KEY2(PB9)
│ └─ 类似KEY1 → 点亮LED6
│
├─ 检测KEY3(PB10)
│ └─ 类似KEY1 → 点亮LED7
│
├─ 检测KEY4(PB11)
│ └─ 类似KEY1 → 点亮LED8
│
└─ 所有按键释放?→ 清除状态记忆
↓
循环继续
功能特点
-
查询方式检测:
- 主循环不断查询按键状态
- 无需中断配置
- 响应速度取决于循环周期
-
软件消抖:
- 延时5ms后再次确认
- 有效过滤按键抖动
- 提高检测可靠性
-
状态记忆:
- 使用
g_Key_old变量记录上次状态 - 避免重复触发
- 只在首次按下时响应
- 使用
-
阻塞等待释放:
while循环等待按键释放- 确保按键完全抬起
- 防止长按重复触发
-
LED指示:
- 按键按下时,关闭所有LED
- 点亮对应的LED
- 直观反馈按键状态
按键与LED对应关系
按键映射表:
┌──────┬──────┬──────────┬──────────┐
│ 按键 │ 引脚 │ 控制LED │ LED引脚 │
├──────┼──────┼──────────┼──────────┤
│ KEY1 │ PB8 │ LED5 │ PB4 │
│ KEY2 │ PB9 │ LED6 │ PB5 │
│ KEY3 │ PB10 │ LED7 │ PB6 │
│ KEY4 │ PB11 │ LED8 │ PB7 │
└──────┴──────┴──────────┴──────────┘
工作逻辑:
1. 按下KEY1 → 关闭所有LED → 点亮LED5
2. 按下KEY2 → 关闭所有LED → 点亮LED6
3. 按下KEY3 → 关闭所有LED → 点亮LED7
4. 按下KEY4 → 关闭所有LED → 点亮LED8
5. 按键检测原理
独立按键 vs 矩阵按键
| 特性 | 独立按键 | 矩阵按键 |
|---|---|---|
| 接线方式 | 每个按键占用一个IO | 多个按键共享行列IO |
| IO占用 | 多(n个按键需要n个IO) | 少(m×n按键需要m+n个IO) |
| 检测方式 | 直接读取IO电平 | 扫描行列组合 |
| 响应速度 | 快 | 稍慢(需要扫描) |
| 适用场景 | 按键数量少(< 10个) | 按键数量多(> 10个) |
| 成本 | 高(IO资源宝贵) | 低(节省IO) |
按键抖动现象
按键抖动波形:
┌─────────────稳定高电平
│
按键按下 → ─────┐ ┌┐ ┌┐ ┌───────稳定低电平
│ ││ ││ │
└───┘└─┘└─┘
↑←5-10ms→↑
抖动期间
按键释放 → ──────────┐ ┌┐ ┌┐ ┌──────稳定高电平
│ ││ ││ │
└─┘└─┘└─┘
↑←5-10ms→↑
抖动期间
抖动原因:
1. 按键机械结构问题
2. 触点弹跳
3. 电气噪声
4. 触点氧化
抖动影响:
1. 一次按键被识别为多次
2. 状态不稳定
3. 逻辑错误
4. 用户体验差
消抖方法
1. 硬件消抖
RC滤波电路:
VCC
|
╱ R (10KΩ)
|
PB8 ─┼─────┤├─ C (0.1μF)
| |
╱ KEY GND
|
GND
原理:
- RC电路延缓电平变化
- 抖动的高频信号被滤除
- 成本增加(每个按键需要RC)
2. 软件消抖
// 方法1:延时消抖(本项目采用)
if(按键按下)
{
延时5-10ms; // 等待抖动结束
if(按键仍然按下) // 再次确认
{
执行按键功能;
}
}
// 方法2:状态机消抖
enum {KEY_IDLE, KEY_PRESS, KEY_DEBOUNCE, KEY_RELEASE};
state = KEY_IDLE;
switch(state)
{
case KEY_IDLE:
if(按键按下) state = KEY_DEBOUNCE;
break;
case KEY_DEBOUNCE:
延时5ms;
if(按键按下) state = KEY_PRESS;
else state = KEY_IDLE;
break;
case KEY_PRESS:
执行按键功能;
state = KEY_RELEASE;
break;
case KEY_RELEASE:
if(按键释放) state = KEY_IDLE;
break;
}
// 方法3:定时器消抖
// 使用定时器周期性检测按键状态
// 连续N次检测到相同状态才认为有效
6. 代码详解
6.1 主程序 (main.c)
完整代码
#include "TF_System_Cfg.h"
#include "TF_KeyBoard.h"
#include "TF_LED.h"
int main(void)
{
LED_Init(); // LED初始化
GPIO_Key_Init(); // 按键初始化
while(1)
{
KeyBoard_Scan(); // 查询方式实现按键扫描
}
}
主函数要点:
-
初始化顺序:
LED_Init(); // 先初始化LED(输出)
GPIO_Key_Init(); // 再初始化按键(输入) -
主循环:
while(1)
{
KeyBoard_Scan(); // 不断查询按键状态
} -
查询方式特点:
- 主循环不断执行扫描函数
- CPU一直在查询按键状态
- 无需中断配置
- 响应时间取决于循环周期
6.2 按键驱动 (TF_KeyBoard)
TF_KeyBoard.h - 头文件
#ifndef __TF_KeyBoard_H__
#define __TF_KeyBoard_H__
#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_exti.h"
#include "TF_Delay.h"
/************************************************************************************************/
// 按键引脚定义
#define KEY1_PIN GPIO_Pin_8
#define KEY1_GPIO GPIOB
#define KEY1_RCC RCC_APB2Periph_GPIOB
#define KEY2_PIN GPIO_Pin_9
#define KEY2_GPIO GPIOB
#define KEY2_RCC RCC_APB2Periph_GPIOB
#define KEY3_PIN GPIO_Pin_10
#define KEY3_GPIO GPIOB
#define KEY3_RCC RCC_APB2Periph_GPIOB
#define KEY4_PIN GPIO_Pin_11
#define KEY4_GPIO GPIOB
#define KEY4_RCC RCC_APB2Periph_GPIOB
/************************************************************************************************/
// 函数声明
extern void GPIO_Key_Init(void);
extern void KeyBoard_Scan(void);
#endif
按键初始化
uint8_t g_Key_old = 0; // 全局变量:记录上次按键状态
void GPIO_Key_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 使能GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 配置按键引脚
GPIO_InitStructure.GPIO_Pin = KEY1_PIN | KEY2_PIN | KEY3_PIN | KEY4_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
初始化要点:
-
GPIO模式选择:
GPIO_Mode_IPU // 上拉输入模式(Input Pull-Up)
特点:
- 内部上拉电阻约40KΩ
- 无按键时读取为高电平(1)
- 按键按下时读取为低电平(0) -
为什么使用上拉输入:
- 按键电路简单(按键一端接GND即可)
- 无需外部上拉电阻(节省成本)
- 引脚有确定的电平(避免悬空)
- 功耗低(按键未按下时无电流)
按键扫描函数
void KeyBoard_Scan(void)
{
// 检测KEY1(PB8)
if(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0) // 按键按下(低电平)
{
Delay_nms(5); // 延时5ms消抖
if(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0) // 再次确认
{
// 检查上次按键状态,避免重复触发
if((g_Key_old & 1) != 1)
{
g_Key_old |= 1; // 记录按键状态(bit0置1)
LED_Off(0); // 关闭所有LED
LED_On(5); // 点亮LED5
}
}
// 等待按键释放(阻塞)
while(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0);
}
//--------------------------------------------------------------------------
// 检测KEY2(PB9)
if(GPIO_ReadInputDataBit(KEY2_GPIO, KEY2_PIN) == 0)
{
Delay_nms(5);
if(GPIO_ReadInputDataBit(KEY2_GPIO, KEY2_PIN) == 0)
{
if((g_Key_old & 2) != 2)
{
g_Key_old |= 2; // 记录按键状态(bit1置1)
LED_Off(0);
LED_On(6);
}
}
while(GPIO_ReadInputDataBit(KEY2_GPIO, KEY2_PIN) == 0);
}
// 检测KEY3(PB10)
if(GPIO_ReadInputDataBit(KEY3_GPIO, KEY3_PIN) == 0)
{
Delay_nms(5);
if(GPIO_ReadInputDataBit(KEY3_GPIO, KEY3_PIN) == 0)
{
if((g_Key_old & 3) != 3) // 注意:这里应该是(g_Key_old & 4) != 4
{
g_Key_old |= 3; // 记录按键状态
LED_Off(0);
LED_On(7);
}
}
while(GPIO_ReadInputDataBit(KEY3_GPIO, KEY3_PIN) == 0);
}
// 检测KEY4(PB11)
if(GPIO_ReadInputDataBit(KEY4_GPIO, KEY4_PIN) == 0)
{
Delay_nms(5);
if(GPIO_ReadInputDataBit(KEY4_GPIO, KEY4_PIN) == 0)
{
if((g_Key_old & 4) != 4)
{
g_Key_old |= 4; // 记录按键状态(bit2置1)
LED_Off(0);
LED_On(8);
}
}
while(GPIO_ReadInputDataBit(KEY4_GPIO, KEY4_PIN) == 0);
}
//--------------------------------------------------------------------------
// 所有按键都释放时,清除状态记忆
else
{
g_Key_old = 0;
}
}
扫描函数详解:
-
按键检测流程:
步骤1:读取引脚电平
↓
步骤2:判断是否为低电平(按键按下)
↓
步骤3:延时5ms(消抖)
↓
步骤4:再次读取引脚电平
↓
步骤5:再次判断是否为低电平
↓
步骤6:检查上次状态(避免重复触发)
↓
步骤7:首次按下?执行按键功能
↓
步骤8:等待按键释放 -
状态记忆机制:
uint8_t g_Key_old = 0; // 8位状态变量
位定义:
bit0 = KEY1状态(1=已按下,0=未按下)
bit1 = KEY2状态
bit2 = KEY4状态
bit3-7 = 未使用
操作:
g_Key_old |= 1; // 设置KEY1状态(bit0置1)
g_Key_old |= 2; // 设置KEY2状态(bit1置1)
g_Key_old |= 4; // 设置KEY4状态(bit2置1)
g_Key_old = 0; // 清除所有状态
检查:
(g_Key_old & 1) != 1 // KEY1是否首次按下
(g_Key_old & 2) != 2 // KEY2是否首次按下
(g_Key_old & 4) != 4 // KEY4是否首次按下 -
阻塞等待释放:
while(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0);
作用:
- 等待按键完全释放
- 阻塞CPU,不执行其他代码
- 防止长按重复触发
缺点:
- CPU一直等待,浪费资源
- 无法处理其他任务
- 响应其他按键的速度变慢 -
代码问题:
// KEY3的状态检查有误
if((g_Key_old & 3) != 3) // 错误:应该是 (g_Key_old & 4) != 4
{
g_Key_old |= 3; // 错误:应该是 g_Key_old |= 4
}
正确写法:
if((g_Key_old & 4) != 4)
{
g_Key_old |= 4;
}
6.3 LED驱动 (TF_LED)
TF_LED.h - 头文件
#ifndef __TF_LED_H__
#define __TF_LED_H__
#include "stm32f10x_gpio.h"
/************************************************************************************************/
// LED引脚定义
#define LED1_PIN GPIO_Pin_0
#define LED1_PORT GPIOB
#define LED2_PIN GPIO_Pin_1
#define LED2_PORT GPIOB
// ... LED3-LED8类似定义 ...
#define LED_PIN GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | \
GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7
#define LED_PORT GPIOB
#define LED_CLK RCC_APB2Periph_GPIOB
/************************************************************************************************/
// 函数声明
extern void LED_Init(void);
void LED_On(unsigned char Led_PIN);
void LED_Off(unsigned char Led_PIN);
#endif
LED初始化
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 使能GPIOB时钟
RCC_APB2PeriphClockCmd(LED_CLK, ENABLE);
// 使能AFIO时钟(用于JTAG重映射)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
// 禁用JTAG,释放PB3、PB4为普通GPIO
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
// 配置LED引脚
GPIO_InitStructure.GPIO_Pin = LED_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(LED_PORT, &GPIO_InitStructure);
// 初始化时关闭所有LED
GPIO_ResetBits(LED_PORT, LED_PIN);
}
初始化要点:
-
JTAG引脚重映射:
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
作用:
- 禁用JTAG功能
- 保留SWD功能(仍可调试)
- 释放PB3、PB4、PA15为GPIO
引脚映射:
JTAG_TMS → PA13(保留为SWD)
JTAG_TCK → PA14(保留为SWD)
JTAG_TDI → PA15(释放)
JTAG_TDO → PB3(释放)
JTAG_TRST → PB4(释放) -
推挽输出模式:
GPIO_Mode_Out_PP // 推挽输出模式
特点:
- 可输出高电平(VCC)和低电平(GND)
- 驱动能力强(最大25mA)
- 适合驱动LED
LED控制函数
void LED_On(unsigned char Led_PIN)
{
switch(Led_PIN)
{
case 1: GPIO_SetBits(LED1_PORT, LED1_PIN); break;
case 2: GPIO_SetBits(LED2_PORT, LED2_PIN); break;
case 3: GPIO_SetBits(LED3_PORT, LED3_PIN); break;
case 4: GPIO_SetBits(LED4_PORT, LED4_PIN); break;
case 5: GPIO_SetBits(LED5_PORT, LED5_PIN); break;
case 6: GPIO_SetBits(LED6_PORT, LED6_PIN); break;
case 7: GPIO_SetBits(LED7_PORT, LED7_PIN); break;
case 8: GPIO_SetBits(LED8_PORT, LED8_PIN); break;
default: GPIO_SetBits(LED_PORT, LED_PIN); break; // 点亮所有LED
}
}
void LED_Off(unsigned char Led_PIN)
{
switch(Led_PIN)
{
case 1: GPIO_ResetBits(LED1_PORT, LED1_PIN); break;
case 2: GPIO_ResetBits(LED2_PORT, LED2_PIN); break;
case 3: GPIO_ResetBits(LED3_PORT, LED3_PIN); break;
case 4: GPIO_ResetBits(LED4_PORT, LED4_PIN); break;
case 5: GPIO_ResetBits(LED5_PORT, LED5_PIN); break;
case 6: GPIO_ResetBits(LED6_PORT, LED6_PIN); break;
case 7: GPIO_ResetBits(LED7_PORT, LED7_PIN); break;
case 8: GPIO_ResetBits(LED8_PORT, LED8_PIN); break;
default: GPIO_ResetBits(LED_PORT, LED_PIN); break; // 关闭所有LED
}
}
控制函数说明:
LED_On(0): 点亮所有LEDLED_Off(0): 关闭所有LEDLED_On(1-8): 点亮指定LEDLED_Off(1-8): 关闭指定LED
7. 查询方式与中断方式对比
两种方式对比
| 特性 | 查询方式 | 中断方式 |
|---|---|---|
| CPU占用 | 高(一直查询) | 低(按键时才响应) |
| 响应速度 | 取决于循环周期 | 快(立即响应) |
| 实时性 | 差 | 好 |
| 编程复杂度 | 简单 | 稍复杂(需配置中断) |
| 功耗 | 高 | 低(可配合休眠) |
| 适用场景 | 简单应用 | 复杂应用、低功耗应用 |
| 可靠性 | 一般(可能漏检) | 高(不会漏检) |
查询方式优缺点
优点:
- ✅ 编程简单,易于理解
- ✅ 无需中断配置
- ✅ 适合简单应用
- ✅ 调试方便
缺点:
- ❌ CPU一直查询,浪费资源
- ❌ 响应速度受限于循环周期
- ❌ 实时性差
- ❌ 功耗高
- ❌ 可能漏检快速按键
中断方式优缺点
优点:
- ✅ CPU占用低
- ✅ 响应速度快
- ✅ 实时性好
- ✅ 功耗低
- ✅ 不会漏检
缺点:
- ❌ 编程稍复杂
- ❌ 需配置中断
- ❌ 调试稍困难
- ❌ 可能中断嵌套冲突
8. 按键消抖技术
消抖原理
按键抖动时序:
时间 →
0ms 5ms 10ms 15ms 20ms 25ms 30ms
↓ ↓ ↓ ↓ ↓ ↓ ↓
按下 ┐┌┐┌──────────────────────────────────稳定低电平
││││
└┘└┘
↑抖动期(5-10ms)
消抖策略:
1. 检测到按键按下(低电平)
2. 延时5-10ms(等待抖动结束)
3. 再次检测按键状态
4. 仍然是低电平?→ 确认按键有效
5. 否则?→ 是抖动,忽略
软件消抖实现
方法1:延时消抖(本项目)
if(按键按下)
{
Delay_nms(5); // 延时5ms
if(按键仍然按下) // 再次确认
{
执行按键功能;
}
}
优点:
- 实现简单
- 效果好
- 可靠性高
缺点:
- 阻塞CPU(延时期间无法做其他事)
- 延时时间需要调整
方法2:状态机消抖
typedef enum {
KEY_IDLE, // 空闲状态
KEY_DEBOUNCE, // 消抖状态
KEY_PRESS, // 按下状态
KEY_RELEASE // 释放状态
} KeyState_t;
KeyState_t key_state = KEY_IDLE;
uint32_t debounce_time = 0;
void KeyBoard_StateMachine(void)
{
switch(key_state)
{
case KEY_IDLE:
if(按键按下)
{
debounce_time = GetTick();
key_state = KEY_DEBOUNCE;
}
break;
case KEY_DEBOUNCE:
if(GetTick() - debounce_time >= 10) // 10ms后
{
if(按键仍然按下)
{
执行按键功能;
key_state = KEY_PRESS;
}
else
{
key_state = KEY_IDLE;
}
}
break;
case KEY_PRESS:
if(按键释放)
{
key_state = KEY_RELEASE;
}
break;
case KEY_RELEASE:
if(GetTick() - debounce_time >= 10)
{
key_state = KEY_IDLE;
}
break;
}
}
优点:
- 非阻塞
- 可处理其他任务
- 逻辑清晰
缺点:
- 实现稍复杂
- 需要定时器支持
方法3:计数器消抖
#define DEBOUNCE_COUNT 5 // 连续5次检测到相同状态
uint8_t key_count = 0;
uint8_t key_last_state = 1; // 上次状态(1=释放,0=按下)
void KeyBoard_CounterDebounce(void)
{
uint8_t key_current = GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN);
if(key_current == key_last_state)
{
key_count = 0; // 状态相同,计数清零
}
else
{
key_count++;
if(key_count >= DEBOUNCE_COUNT) // 连续N次不同
{
key_last_state = key_current; // 更新状态
key_count = 0;
if(key_current == 0) // 按键按下
{
执行按键功能;
}
}
}
}
// 在定时器中断中调用(如1ms一次)
void TIM_IRQHandler(void)
{
KeyBoard_CounterDebounce();
}
优点:
- 非阻塞
- 可靠性高
- 适合定时器中断
缺点:
- 需要定时器支持
- 响应稍慢(N×定时器周期)
9. 使用说明
9.1 硬件连接
连接步骤
-
连接按键
KEY1 → PB8(按下接GND)
KEY2 → PB9(按下接GND)
KEY3 → PB10(按下接GND)
KEY4 → PB11(按下接GND) -
连接LED
LED1 → PB0(高电平点亮)
LED2 → PB1
LED3 → PB2
LED4 → PB3
LED5 → PB4
LED6 → PB5
LED7 → PB6
LED8 → PB7 -
连接调试器
J-Link/ST-Link:
SWDIO → PA13
SWCLK → PA14
GND → GND
VCC → 3.3V
9.2 编译与下载
编译工程
- 打开
MDK/TEST_CODE.uvproj - 选择目标芯片:STM32F103C8
- 编译:Project → Build Target (F7)
- 检查编译结果:0 Error(s), 0 Warning(s)
下载程序
- 配置调试器:Options → Debug → 选择 J-Link/ST-Link
- 下载程序:Flash → Download (F8)
- 复位运行
9.3 运行效果
预期现象:
- 按下KEY1 → 所有LED熄灭 → LED5点亮
- 按下KEY2 → 所有LED熄灭 → LED6点亮
- 按下KEY3 → 所有LED熄灭 → LED7点亮
- 按下KEY4 → 所有LED熄灭 → LED8点亮
- 保持按下不放 → LED保持点亮(不会重复触发)
- 释放按键 → LED保持当前状态
9.4 测试方法
测试1:基本功能
步骤:
1. 上电复位
2. 依次按下KEY1、KEY2、KEY3、KEY4
3. 观察对应LED是否点亮
预期结果:
- KEY1 → LED5点亮
- KEY2 → LED6点亮
- KEY3 → LED7点亮
- KEY4 → LED8点亮
测试2:消抖效果
步骤:
1. 快速连续按下同一按键
2. 观察LED是否闪烁
预期结果:
- LED不应闪烁(只响应一次)
- 释放后再按才会再次响应
测试3:状态记忆
步骤:
1. 按下KEY1并保持
2. 观察LED5是否重复触发
预期结果:
- LED5点亮后保持
- 不会闪烁(状态记忆有效)
10. 常见问题
问题1:按键无响应
可能原因:
- ❌ 按键连接错误
- ❌ GPIO配置错误
- ❌ 引脚定义错误
- ❌ 按键损坏
解决方案:
// 调试1:检查GPIO配置
void Test_GPIO_Config(void)
{
// 读取GPIO配置寄存器
uint32_t crl = GPIOB->CRL;
uint32_t crh = GPIOB->CRH;
// PB8-PB11应该配置为上拉输入(CNF=10, MODE=00)
printf("GPIOB_CRH = 0x%08X\r\n", crh);
}
// 调试2:直接读取引脚电平
void Test_Key_Read(void)
{
while(1)
{
uint8_t key1 = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_8);
uint8_t key2 = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9);
printf("KEY1=%d, KEY2=%d\r\n", key1, key2);
Delay_ms(500);
}
}
问题2:按键重复触发
原因: 消抖不足或状态记忆失效
解决方案:
// 增加消抖延时
Delay_nms(10); // 从5ms增加到10ms
// 检查状态记忆逻辑
if((g_Key_old & 1) != 1) // 确保条件正确
{
g_Key_old |= 1;
}
// 确保释放时清除状态
else
{
g_Key_old = 0;
}
问题3:LED不点亮
原因: LED配置或JTAG重映射问题
解决方案:
// 确保JTAG重映射
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
// 测试LED
void Test_LED(void)
{
LED_Init();
while(1)
{
LED_On(5);
Delay_ms(500);
LED_Off(5);
Delay_ms(500);
}
}
问题4:按键响应慢
原因: 阻塞等待释放时间长
解决方案:
// 方案1:减少消抖延时
Delay_nms(3); // 从5ms减少到3ms
// 方案2:移除阻塞等待(改为状态机)
// 不推荐:可能导致重复触发
// while(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0);
问题5:KEY3功能异常
原因: 代码中KEY3的状态检查有误
解决方案:
// 修正KEY3的代码
if(GPIO_ReadInputDataBit(KEY3_GPIO, KEY3_PIN) == 0)
{
Delay_nms(5);
if(GPIO_ReadInputDataBit(KEY3_GPIO, KEY3_PIN) == 0)
{
// 错误代码:
// if((g_Key_old & 3) != 3)
// {
// g_Key_old |= 3;
// }
// 正确代码:
if((g_Key_old & 4) != 4) // 检查bit2
{
g_Key_old |= 4; // 设置bit2
LED_Off(0);
LED_On(7);
}
}
while(GPIO_ReadInputDataBit(KEY3_GPIO, KEY3_PIN) == 0);
}
11. 扩展应用
11.1 长按检测
#define LONG_PRESS_TIME 2000 // 长按时间2秒
void KeyBoard_LongPress(void)
{
static uint32_t press_time = 0;
static uint8_t long_press_flag = 0;
if(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0)
{
if(press_time == 0)
{
press_time = GetTick(); // 记录按下时间
}
else if(GetTick() - press_time >= LONG_PRESS_TIME)
{
if(!long_press_flag)
{
long_press_flag = 1;
// 长按功能
printf("KEY1 Long Press\r\n");
}
}
}
else
{
if(press_time > 0 && !long_press_flag)
{
// 短按功能
printf("KEY1 Short Press\r\n");
}
press_time = 0;
long_press_flag = 0;
}
}
11.2 组合按键
void KeyBoard_Combination(void)
{
uint8_t key1 = !GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN);
uint8_t key2 = !GPIO_ReadInputDataBit(KEY2_GPIO, KEY2_PIN);
if(key1 && key2) // KEY1和KEY2同时按下
{
Delay_nms(5);
if(key1 && key2)
{
// 组合按键功能
printf("KEY1 + KEY2 Pressed\r\n");
LED_On(0); // 点亮所有LED
}
}
}
11.3 连击检测
#define DOUBLE_CLICK_TIME 300 // 连击间隔300ms
void KeyBoard_DoubleClick(void)
{
static uint32_t last_press_time = 0;
static uint8_t click_count = 0;
if(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0)
{
Delay_nms(5);
if(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0)
{
uint32_t current_time = GetTick();
if(current_time - last_press_time <= DOUBLE_CLICK_TIME)
{
click_count++;
if(click_count >= 2)
{
// 双击功能
printf("Double Click\r\n");
click_count = 0;
}
}
else
{
click_count = 1;
}
last_press_time = current_time;
while(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0);
}
}
}
11.4 菜单系统
typedef struct {
char *name;
void (*function)(void);
} MenuItem;
MenuItem menu[] = {
{"1.LED Control", LED_Control_Menu},
{"2.System Info", Show_System_Info},
{"3.Settings", Settings_Menu},
{"4.Exit", NULL},
};
uint8_t current_item = 0;
uint8_t menu_count = sizeof(menu) / sizeof(menu[0]);
void Menu_System(void)
{
// KEY1: 上
if(Key_Pressed(KEY1))
{
if(current_item > 0)
{
current_item--;
Display_Menu();
}
}
// KEY2: 下
if(Key_Pressed(KEY2))
{
if(current_item < menu_count - 1)
{
current_item++;
Display_Menu();
}
}
// KEY3: 确认
if(Key_Pressed(KEY3))
{
if(menu[current_item].function != NULL)
{
menu[current_item].function();
}
}
// KEY4: 返回
if(Key_Pressed(KEY4))
{
Return_To_Main();
}
}
11.5 状态切换
typedef enum {
STATE_IDLE,
STATE_RUNNING,
STATE_PAUSE,
STATE_STOP
} SystemState_t;
SystemState_t system_state = STATE_IDLE;
void State_Machine(void)
{
switch(system_state)
{
case STATE_IDLE:
if(Key_Pressed(KEY1)) // 启动
{
system_state = STATE_RUNNING;
Start_Process();
}
break;
case STATE_RUNNING:
if(Key_Pressed(KEY2)) // 暂停
{
system_state = STATE_PAUSE;
Pause_Process();
}
else if(Key_Pressed(KEY4)) // 停止
{
system_state = STATE_STOP;
Stop_Process();
}
break;
case STATE_PAUSE:
if(Key_Pressed(KEY1)) // 继续
{
system_state = STATE_RUNNING;
Resume_Process();
}
else if(Key_Pressed(KEY4)) // 停止
{
system_state = STATE_STOP;
Stop_Process();
}
break;
case STATE_STOP:
if(Key_Pressed(KEY1)) // 重新启动
{
system_state = STATE_RUNNING;
Start_Process();
}
break;
}
}
11.6 参数调整
int16_t parameter = 50; // 参数范围:0-100
void Parameter_Adjust(void)
{
// KEY1: 增加
if(Key_Pressed(KEY1))
{
parameter++;
if(parameter > 100)
{
parameter = 100;
}
Display_Parameter();
}
// KEY2: 减少
if(Key_Pressed(KEY2))
{
parameter--;
if(parameter < 0)
{
parameter = 0;
}
Display_Parameter();
}
// KEY3: 快速增加(+10)
if(Key_Pressed(KEY3))
{
parameter += 10;
if(parameter > 100)
{
parameter = 100;
}
Display_Parameter();
}
// KEY4: 快速减少(-10)
if(Key_Pressed(KEY4))
{
parameter -= 10;
if(parameter < 0)
{
parameter = 0;
}
Display_Parameter();
}
}
11.7 模式切换
typedef enum {
MODE_AUTO,
MODE_MANUAL,
MODE_DEBUG
} OperationMode_t;
OperationMode_t current_mode = MODE_AUTO;
void Mode_Switch(void)
{
if(Key_Pressed(KEY1))
{
current_mode = (current_mode + 1) % 3; // 循环切换
switch(current_mode)
{
case MODE_AUTO:
printf("Mode: AUTO\r\n");
LED_On(5);
LED_Off(6);
LED_Off(7);
break;
case MODE_MANUAL:
printf("Mode: MANUAL\r\n");
LED_Off(5);
LED_On(6);
LED_Off(7);
break;
case MODE_DEBUG:
printf("Mode: DEBUG\r\n");
LED_Off(5);
LED_Off(6);
LED_On(7);
break;
}
}
}