跳到主要内容

【四】独立按键(查询方式)实验

1. 项目概述

本项目演示如何使用 STM32F103C8T6 微控制器通过查询方式检测独立按键,并根据按键状态控制LED灯的点亮。系统包含4个独立按键(KEY1-KEY4)和8个LED指示灯,每个按键对应控制不同的LED灯。采用软件消抖和按键状态记忆,实现可靠的按键检测。

技术要点

  • ✅ GPIO配置(输入上拉模式)
  • ✅ 查询方式按键检测
  • ✅ 软件消抖(5ms延时)
  • ✅ 按键状态记忆(避免重复触发)
  • ✅ 按键释放检测(阻塞等待)
  • ✅ LED指示灯控制
  • ✅ JTAG引脚复用(释放PB3、PB4)

应用场景

  1. 🔘 用户输入界面
  2. 🔘 菜单选择系统
  3. 🔘 设备控制面板
  4. 🔘 功能切换按钮
  5. 🔘 简单人机交互
  6. 🔘 参数设置界面

2. 硬件平台

主控芯片

  • 型号: STM32F103C8T6
  • 内核: ARM Cortex-M3
  • 主频: 72 MHz
  • Flash: 64 KB
  • SRAM: 20 KB

外设资源

独立按键(4个)

按键引脚控制LED说明
KEY1PB8LED5按键1
KEY2PB9LED6按键2
KEY3PB10LED7按键3
KEY4PB11LED8按键4

LED指示灯(8个)

LED引脚说明
LED1PB0指示灯1
LED2PB1指示灯2
LED3PB2指示灯3
LED4PB3指示灯4
LED5PB4指示灯5(KEY1控制)
LED6PB5指示灯6(KEY2控制)
LED7PB6指示灯7(KEY3控制)
LED8PB7指示灯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)
- 上拉电阻:确保未按下时有确定的高电平

注意事项:

  1. 按键使用上拉输入模式(GPIO_Mode_IPU)
  2. 按键按下时,引脚读取为低电平(0)
  3. PB3和PB4默认为JTAG功能,需要重映射才能作为GPIO使用
  4. 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

└─ 所有按键释放?→ 清除状态记忆

循环继续

功能特点

  1. 查询方式检测:

    • 主循环不断查询按键状态
    • 无需中断配置
    • 响应速度取决于循环周期
  2. 软件消抖:

    • 延时5ms后再次确认
    • 有效过滤按键抖动
    • 提高检测可靠性
  3. 状态记忆:

    • 使用g_Key_old变量记录上次状态
    • 避免重复触发
    • 只在首次按下时响应
  4. 阻塞等待释放:

    • while循环等待按键释放
    • 确保按键完全抬起
    • 防止长按重复触发
  5. 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(); // 查询方式实现按键扫描
}
}

主函数要点:

  1. 初始化顺序:

    LED_Init();          // 先初始化LED(输出)
    GPIO_Key_Init(); // 再初始化按键(输入)
  2. 主循环:

    while(1)
    {
    KeyBoard_Scan(); // 不断查询按键状态
    }
  3. 查询方式特点:

    • 主循环不断执行扫描函数
    • 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);
}

初始化要点:

  1. GPIO模式选择:

    GPIO_Mode_IPU  // 上拉输入模式(Input Pull-Up)

    特点:
    - 内部上拉电阻约40
    - 无按键时读取为高电平(1
    - 按键按下时读取为低电平(0
  2. 为什么使用上拉输入:

    • 按键电路简单(按键一端接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. 按键检测流程:

    步骤1:读取引脚电平

    步骤2:判断是否为低电平(按键按下)

    步骤3:延时5ms(消抖)

    步骤4:再次读取引脚电平

    步骤5:再次判断是否为低电平

    步骤6:检查上次状态(避免重复触发)

    步骤7:首次按下?执行按键功能

    步骤8:等待按键释放
  2. 状态记忆机制:

    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是否首次按下
  3. 阻塞等待释放:

    while(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0);

    作用:
    - 等待按键完全释放
    - 阻塞CPU,不执行其他代码
    - 防止长按重复触发

    缺点:
    - CPU一直等待,浪费资源
    - 无法处理其他任务
    - 响应其他按键的速度变慢
  4. 代码问题:

    // 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);
}

初始化要点:

  1. 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(释放)
  2. 推挽输出模式:

    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): 点亮所有LED
  • LED_Off(0): 关闭所有LED
  • LED_On(1-8): 点亮指定LED
  • LED_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 硬件连接

连接步骤

  1. 连接按键

    KEY1 → PB8(按下接GND)
    KEY2 → PB9(按下接GND)
    KEY3 → PB10(按下接GND)
    KEY4 → PB11(按下接GND)
  2. 连接LED

    LED1 → PB0(高电平点亮)
    LED2 → PB1
    LED3 → PB2
    LED4 → PB3
    LED5 → PB4
    LED6 → PB5
    LED7 → PB6
    LED8 → PB7
  3. 连接调试器

    J-Link/ST-Link:
    SWDIO → PA13
    SWCLK → PA14
    GND → GND
    VCC → 3.3V

9.2 编译与下载

编译工程

  1. 打开 MDK/TEST_CODE.uvproj
  2. 选择目标芯片:STM32F103C8
  3. 编译:Project → Build Target (F7)
  4. 检查编译结果:0 Error(s), 0 Warning(s)

下载程序

  1. 配置调试器:Options → Debug → 选择 J-Link/ST-Link
  2. 下载程序:Flash → Download (F8)
  3. 复位运行

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:按键无响应

可能原因:

  1. ❌ 按键连接错误
  2. ❌ GPIO配置错误
  3. ❌ 引脚定义错误
  4. ❌ 按键损坏

解决方案:

// 调试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;
}
}
}