2. 按键扫描
任务目标
实现一个按键检测案例:裸机方案采用主循环轮询GPIO电平,RTOS方案采用事件标志组在按键中断中置位、任务阻塞等待。通过LED翻转次数与逻辑分析仪(或示波器)观察CPU占用差异。
硬件环境
- MCU:STM32F103C8T6(蓝丸板)
- 按键:PA0接GND(按下为低电平),内部上拉
- LED:PC13(板上LED),低电平点亮
- RTOS:FreeRTOS V10.4.6(CMSIS-OS v2封装)
裸机轮询实现
// main.c - 裸机轮询版本
#include "stm32f1xx_hal.h"
void SystemClock_Config(void);
static void GPIO_Init(void);
int main(void) {
HAL_Init();
SystemClock_Config();
GPIO_Init();
uint8_t key_state = 1; // 1=未按下, 0=按下
uint8_t led_state = 0;
uint32_t last_tick = 0;
while (1) {
// 每10ms轮询一次按键
if (HAL_GetTick() - last_tick >= 10) {
last_tick = HAL_GetTick();
uint8_t current = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
// 边沿检测:仅按下沿触发
if (key_state == 1 && current == 0) {
led_state ^= 1;
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13,
led_state ? GPIO_PIN_RESET : GPIO_PIN_SET);
}
key_state = current;
}
}
}
static void GPIO_Init(void) {
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_0;
gpio.Mode = GPIO_MODE_INPUT;
gpio.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &gpio);
gpio.Pin = GPIO_PIN_13;
gpio.Mode = GPIO_MODE_OUTPUT_PP;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOC, &gpio);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); // LED灭
}
RTOS事件驱动实现
// app_rtos.c - FreeRTOS事件标志版本
#include "FreeRTOS.h"
#include "task.h"
#include "event_groups.h"
#include "stm32f1xx_hal.h"
#define KEY_EVENT_BIT (1 << 0)
static EventGroupHandle_t key_evt_group;
// 按键中断服务函数(在stm32f1xx_it.c中调用)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == GPIO_PIN_0) {
BaseType_t higher_woken = pdFALSE;
// 从ISR置位事件标志
xEventGroupSetBitsFromISR(key_evt_group, KEY_EVENT_BIT,
&higher_woken);
portYIELD_FROM_ISR(higher_woken);
}
}
static void KeyTask(void *arg) {
uint8_t led_state = 0;
for (;;) {
// 阻塞等待事件,超时100ms用于低功耗或看门狗喂狗
EventBits_t bits = xEventGroupWaitBits(
key_evt_group, KEY_EVENT_BIT, pdTRUE, pdFALSE, pdMS_TO_TICKS(100));
if (bits & KEY_EVENT_BIT) {
led_state ^= 1;
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13,
led_state ? GPIO_PIN_RESET : GPIO_PIN_SET);
}
}
}
void RTOS_Init(void) {
key_evt_group = xEventGroupCreate();
xTaskCreate(KeyTask, "key", 128, NULL, 2, NULL);
// 配置PA0外部中断(下降沿触发)
GPIO_InitTypeDef gpio = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
gpio.Pin = GPIO_PIN_0;
gpio.Mode = GPIO_MODE_IT_FALLING;
gpio.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &gpio);
HAL_NVIC_SetPriority(EXTI0_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
CPU占用对比方法
- 在裸机主循环和RTOS空闲任务中分别翻转一个空闲GPIO(如PB0),用逻辑分析仪测量该引脚高电平占比:
裸机:高电平占比 ≈ 99%(仅每10ms轮询瞬间拉低)
RTOS:空闲任务在无事件时持续运行,高电平占比接近100%,按键中断处理时间极短(<5μs) - 响应时间测量:从按键按下到LED翻转的延迟。
裸机:最坏等待一个轮询周期 = 10ms
RTOS:中断触发到任务唤醒典型延迟 = 中断延迟 + 上下文切换 ≈ 5~20μs
验证方法
- 编译烧录后,按下按键PA0,LED(PC13)状态翻转,说明功能正确
- 用逻辑分析仪观察PB0波形:裸机版本每10ms出现一个短暂低脉冲;RTOS版本空闲时持续高电平,仅在按键按下瞬间出现短暂低脉冲
- 用示波器测量从按键按下到LED翻转的时间差:裸机版本应接近10ms,RTOS版本应小于100μs
关键差异说明
| 对比项 | 裸机轮询 | RTOS事件驱动 |
|---|---|---|
| CPU占用 | 持续运行轮询代码,即使无按键也每10ms唤醒一次 | 无事件时任务阻塞,CPU可进入低功耗或执行其他任务 |
| 响应延迟 | 最坏10ms(轮询周期) | 中断触发,微秒级响应 |
| 代码复杂度 | 简单直接 | 需管理事件标志、任务优先级、中断安全API |
| 扩展性 | 按键增多时轮询周期变长或需分时处理 | 每个按键独立中断+事件位,互不阻塞 |
注意:RTOS版本中EXTI0_IRQHandler需要在启动文件中正确映射到HAL_GPIO_EXTI_IRQHandler,并在该函数中调用HAL_GPIO_EXTI_Callback。若使用CMSIS-OS v2,可将xEventGroupSetBitsFromISR替换为osEventFlagsSetFromISR。