STM32单片机按键扫描技术全解析:中断、轮询、消抖算法一网打尽
发布时间: 2024-07-05 17:18:25 阅读量: 1022 订阅数: 60
![按键扫描](https://img-blog.csdnimg.cn/img_convert/7b85fe16a20d21b937d27ec8d4a7bd9b.png)
# 1. 按键扫描基础**
按键扫描是单片机与外部世界交互的重要手段,它允许用户通过按键输入控制单片机。按键扫描的基本原理是检测按键的状态,即按键是否被按下。按键扫描技术主要分为两种:中断扫描和轮询扫描。
中断扫描是一种响应式技术,当按键状态发生变化时,单片机会产生一个中断信号。中断处理程序会捕获按键状态并执行相应的操作。轮询扫描是一种主动式技术,单片机定期检查按键的状态,并根据按键状态的变化执行相应的操作。
# 2. 按键扫描方法
### 2.1 中断扫描
#### 2.1.1 中断原理
中断是一种硬件机制,当外部事件或内部事件发生时,会触发中断请求,从而暂停当前正在执行的程序,转而执行中断服务程序(ISR)。按键扫描中断是指当按键状态发生变化时,触发中断请求,从而执行按键扫描处理。
#### 2.1.2 中断配置
STM32单片机提供了丰富的外部中断源,可以将按键连接到外部中断引脚上。中断配置主要包括以下步骤:
- **中断源选择:**根据按键连接的引脚,选择对应的外部中断源。
- **中断优先级设置:**为中断源设置优先级,决定中断响应的先后顺序。
- **中断使能:**使能对应的外部中断源,以便在按键状态变化时触发中断请求。
#### 2.1.3 中断处理
中断处理程序(ISR)是按键扫描中断的入口点。当按键状态发生变化时,会触发中断请求,执行ISR。ISR主要负责以下任务:
- **读取按键状态:**读取按键连接的引脚状态,判断按键是否按下。
- **更新按键状态:**根据读取到的按键状态,更新按键状态变量。
- **执行按键处理:**根据按键状态,执行相应的按键处理逻辑,例如触发按键事件回调、更新按键状态机等。
### 2.2 轮询扫描
#### 2.2.1 轮询原理
轮询扫描是一种通过定期检查按键状态来实现按键扫描的方法。轮询扫描的原理是:在主程序循环中,周期性地读取按键连接的引脚状态,判断按键是否按下。
#### 2.2.2 轮询实现
轮询扫描的实现主要包括以下步骤:
- **定义按键扫描函数:**定义一个按键扫描函数,用于读取按键状态并更新按键状态变量。
- **在主程序循环中调用按键扫描函数:**在主程序循环中,周期性地调用按键扫描函数,检查按键状态。
- **执行按键处理:**根据按键状态,执行相应的按键处理逻辑,例如触发按键事件回调、更新按键状态机等。
#### 2.2.3 轮询优化
轮询扫描的效率较低,因为需要不断地读取按键状态,即使按键状态没有发生变化。为了优化轮询扫描,可以采用以下方法:
- **减少轮询频率:**根据按键的实际使用情况,适当降低轮询频率,以降低功耗。
- **使用事件驱动:**当按键状态发生变化时,触发按键事件,而不是不断地轮询按键状态。
# 3. 按键消抖算法
### 3.1 软件消抖
软件消抖算法通过软件程序来消除按键抖动,实现按键稳定可靠的检测。
#### 3.1.1 延时消抖
延时消抖是最简单的一种软件消抖算法。其原理是在检测到按键按下后,等待一段时间,再进行按键状态判断。如果在等待期间按键状态保持不变,则认为按键有效。
```c
void delay_debounce(void)
{
volatile uint32_t i;
for (i = 0; i < 100000; i++);
}
```
参数说明:
* `i`:循环变量
代码逻辑:
* 循环 100000 次,实现约 10ms 的延时。
#### 3.1.2 计数消抖
计数消抖算法通过记录按键状态变化的次数来消除按键抖动。其原理是,在检测到按键按下后,连续读取按键状态,如果按键状态连续保持一段时间不变,则认为按键有效。
```c
void count_debounce(void)
{
uint8_t count = 0;
while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0)
{
count++;
delay_ms(1);
}
if (count > 10)
{
// 按键有效
}
}
```
参数说明:
* `count`:按键状态变化次数
代码逻辑:
* 循环读取按键状态,如果按键按下,则 `count` 递增。
* 延时 1ms,防止按键抖动。
* 如果 `count` 大于 10,则认为按键有效。
#### 3.1.3 滤波消抖
滤波消抖算法通过对按键状态进行滤波处理来消除按键抖动。其原理是,使用一个低通滤波器对按键状态进行平滑处理,滤除高频噪声,从而获得稳定的按键状态。
```c
void filter_debounce(void)
{
static uint8_t prev_state = 0;
uint8_t curr_state = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);
prev_state = (prev_state * 7 + curr_state) >> 3;
if (prev_state == 0)
{
// 按键按下
}
else if (prev_state == 255)
{
// 按键松开
}
}
```
参数说明:
* `prev_state`:上一次按键状态
* `curr_state`:当前按键状态
代码逻辑:
* 使用一个 3 阶低通滤波器对按键状态进行平滑处理。
* 如果滤波后的按键状态为 0,则认为按键按下。
* 如果滤波后的按键状态为 255,则认为按键松开。
### 3.2 硬件消抖
硬件消抖算法通过外围电路来消除按键抖动,实现按键稳定可靠的检测。
#### 3.2.1 RC电路消抖
RC电路消抖是最简单的一种硬件消抖电路。其原理是在按键和单片机之间接入一个 RC 电路,利用电容的充放电特性来消除按键抖动。
```
+-----------+
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
# 4. 按键扫描应用
### 4.1 按键检测
#### 4.1.1 按键状态检测
按键状态检测是按键扫描中最基本的操作,用于获取按键的当前状态(按下或释放)。
**代码块:**
```c
uint8_t get_key_state(uint8_t key_pin) {
return GPIO_ReadInputDataBit(GPIOx, key_pin);
}
```
**逻辑分析:**
该函数接收一个按键引脚号 `key_pin`,并返回按键的当前状态。如果按键按下,返回 `1`;如果按键释放,返回 `0`。
#### 4.1.2 按键长按检测
按键长按检测用于检测按键是否被持续按下超过指定的时间。
**代码块:**
```c
uint8_t is_key_long_pressed(uint8_t key_pin, uint32_t long_press_time) {
uint32_t start_time = HAL_GetTick();
while (get_key_state(key_pin)) {
if (HAL_GetTick() - start_time > long_press_time) {
return 1;
}
}
return 0;
}
```
**逻辑分析:**
该函数接收两个参数:按键引脚号 `key_pin` 和长按时间 `long_press_time`。函数不断轮询按键状态,如果按键持续按下超过 `long_press_time`,则返回 `1`;否则返回 `0`。
#### 4.1.3 按键组合检测
按键组合检测用于检测多个按键同时被按下的情况。
**代码块:**
```c
uint8_t is_key_combination_pressed(uint8_t key_pin1, uint8_t key_pin2) {
return (get_key_state(key_pin1) && get_key_state(key_pin2));
}
```
**逻辑分析:**
该函数接收两个按键引脚号 `key_pin1` 和 `key_pin2`,并返回 `1` 表示两个按键同时按下;否则返回 `0`。
### 4.2 按键事件处理
#### 4.2.1 按键事件回调
按键事件回调函数用于在按键状态发生变化时执行特定的操作。
**代码块:**
```c
void key_event_callback(uint8_t key_pin, uint8_t event) {
switch (event) {
case KEY_EVENT_PRESSED:
// 按键按下事件处理
break;
case KEY_EVENT_RELEASED:
// 按键释放事件处理
break;
case KEY_EVENT_LONG_PRESSED:
// 按键长按事件处理
break;
default:
break;
}
}
```
**逻辑分析:**
该函数接收两个参数:按键引脚号 `key_pin` 和事件类型 `event`。根据 `event` 的值,执行相应的事件处理操作。
#### 4.2.2 按键事件队列
按键事件队列用于存储按键事件,以便在主循环中统一处理。
**代码块:**
```c
typedef struct {
uint8_t key_pin;
uint8_t event;
} key_event_t;
key_event_t key_event_queue[KEY_EVENT_QUEUE_SIZE];
uint8_t key_event_queue_head = 0;
uint8_t key_event_queue_tail = 0;
void add_key_event(key_event_t event) {
key_event_queue[key_event_queue_tail] = event;
key_event_queue_tail = (key_event_queue_tail + 1) % KEY_EVENT_QUEUE_SIZE;
}
```
**逻辑分析:**
该代码定义了一个按键事件队列,其中 `key_event_t` 结构体包含按键引脚号和事件类型。`add_key_event()` 函数用于将按键事件添加到队列中。
#### 4.2.3 按键事件状态机
按键事件状态机用于跟踪按键的当前状态,并根据状态转换执行相应的操作。
**代码块:**
```c
enum key_state {
KEY_STATE_IDLE,
KEY_STATE_PRESSED,
KEY_STATE_LONG_PRESSED,
KEY_STATE_RELEASED
};
key_state key_state = KEY_STATE_IDLE;
void key_state_machine(uint8_t key_pin) {
switch (key_state) {
case KEY_STATE_IDLE:
if (get_key_state(key_pin)) {
key_state = KEY_STATE_PRESSED;
}
break;
case KEY_STATE_PRESSED:
if (!get_key_state(key_pin)) {
key_state = KEY_STATE_RELEASED;
} else if (is_key_long_pressed(key_pin, LONG_PRESS_TIME)) {
key_state = KEY_STATE_LONG_PRESSED;
}
break;
case KEY_STATE_LONG_PRESSED:
if (!get_key_state(key_pin)) {
key_state = KEY_STATE_RELEASED;
}
break;
case KEY_STATE_RELEASED:
if (get_key_state(key_pin)) {
key_state = KEY_STATE_PRESSED;
}
break;
default:
break;
}
}
```
**逻辑分析:**
该代码定义了一个按键状态机,其中 `key_state` 枚举变量表示按键的当前状态。`key_state_machine()` 函数根据按键状态进行状态转换,并执行相应的操作,如按键按下、释放、长按等。
# 5. 按键扫描优化
### 5.1 性能优化
#### 5.1.1 中断优化
- **优化中断响应时间:**通过使用更高优先级的中断或减少中断处理程序中的代码量来缩短中断响应时间。
- **减少中断次数:**通过使用消抖算法或合并多个按键扫描任务来减少中断的触发频率。
- **使用中断分组:**将具有相似优先级的中断分组到一个中断向量中,以提高中断处理效率。
#### 5.1.2 轮询优化
- **优化轮询频率:**根据按键的预期使用频率和响应时间要求调整轮询频率。
- **使用定时器:**使用定时器来定期触发轮询,而不是在主循环中不断轮询。
- **使用多线程:**在多核处理器上,将轮询任务分配到不同的线程,以提高并行性。
#### 5.1.3 消抖优化
- **优化消抖算法:**选择适合特定按键应用的消抖算法,并根据按键的物理特性调整参数。
- **使用硬件消抖电路:**在硬件上实现消抖电路,以减少软件消抖的开销。
- **使用自适应消抖:**根据按键状态动态调整消抖参数,以提高响应速度和准确性。
### 5.2 功耗优化
#### 5.2.1 中断休眠
- **使用中断优先级分组:**将低优先级的按键扫描中断分组到一个中断向量中,并使用中断优先级分组功能在不使用时关闭中断。
- **使用中断屏蔽:**在不使用按键扫描中断时,使用中断屏蔽功能暂时禁用中断。
#### 5.2.2 轮询休眠
- **使用低功耗模式:**在轮询期间,将处理器置于低功耗模式,如睡眠或待机模式。
- **使用定时器唤醒:**使用定时器唤醒处理器进行轮询,而不是不断轮询。
#### 5.2.3 消抖休眠
- **使用硬件消抖电路:**硬件消抖电路可以在不使用处理器的情况下执行消抖,从而节省功耗。
- **使用自适应消抖:**根据按键状态动态调整消抖参数,以减少不必要的消抖操作。
# 6. 按键扫描实战**
**6.1 按键扫描例程**
**6.1.1 中断扫描例程**
```c
// 中断服务函数
void EXTI0_IRQHandler(void) {
// 清除中断标志位
EXTI->PR |= EXTI_PR_PR0;
// 执行按键处理函数
key_scan();
}
// 按键扫描函数
void key_scan(void) {
// 读取按键状态
uint8_t key_status = GPIOA->IDR & GPIO_IDR_IDR0;
// 处理按键按下事件
if (key_status == 0) {
// 执行按键按下处理函数
key_press_handler();
}
}
```
**6.1.2 轮询扫描例程**
```c
// 按键扫描函数
void key_scan(void) {
// 读取按键状态
uint8_t key_status = GPIOA->IDR & GPIO_IDR_IDR0;
// 处理按键按下事件
if (key_status == 0) {
// 执行按键按下处理函数
key_press_handler();
}
}
```
**6.1.3 消抖算法例程**
```c
// 软件消抖函数
uint8_t key_debounce(void) {
// 读取按键状态
uint8_t key_status = GPIOA->IDR & GPIO_IDR_IDR0;
// 延时消抖
delay_ms(10);
// 再次读取按键状态
uint8_t key_status_new = GPIOA->IDR & GPIO_IDR_IDR0;
// 判断按键状态是否稳定
if (key_status == key_status_new) {
return key_status;
} else {
return 0;
}
}
```
**6.2 按键扫描应用实例**
**6.2.1 按键控制LED**
```c
// 按键按下处理函数
void key_press_handler(void) {
// 切换LED状态
GPIOB->ODR ^= GPIO_ODR_ODR1;
}
```
**6.2.2 按键控制电机**
```c
// 按键按下处理函数
void key_press_handler(void) {
// 控制电机正转或反转
if (GPIOA->IDR & GPIO_IDR_IDR1) {
// 正转
TIM3->CCR1 = 1000;
} else {
// 反转
TIM3->CCR1 = -1000;
}
}
```
**6.2.3 按键控制显示屏**
```c
// 按键按下处理函数
void key_press_handler(void) {
// 切换显示屏显示内容
if (GPIOA->IDR & GPIO_IDR_IDR1) {
// 显示字符串1
LCD_DisplayString("Hello World 1");
} else {
// 显示字符串2
LCD_DisplayString("Hello World 2");
}
}
```
0
0