3. CH32V203开发环境

按键输入,是嵌入式开发里最基础、也最容易出坑的环节。你想想看,一个按键按下去,CPU 怎么知道它被按了?按下去之后抖不抖?用轮询还是中断?这些问题搞不清楚,后面做复杂项目就会反复踩坑。这一章,我就带你把这些事彻底理清楚。

3.1 按键输入原理

按键的本质,就是一个机械开关。按下时导通,松开时断开。我们把它接到 CH32V203 的 GPIO 引脚上,再配合上拉或下拉电阻,就能读到高低电平的变化。

常见的接法有两种:

  • 上拉输入:按键一端接 GND,另一端接 GPIO。平时引脚被内部上拉电阻拉到高电平,按下后变成低电平。
  • 下拉输入:按键一端接 VCC,另一端接 GPIO。平时引脚被内部下拉电阻拉到低电平,按下后变成高电平。

我个人习惯用上拉输入,因为 CH32V203 的 GPIO 内部自带可配置的上拉电阻,省掉外部元件,板子更干净。

小提示:CH32V203 的 GPIO 上拉电阻约 40kΩ,下拉约 40kΩ。如果环境干扰大,建议外部再加一个 10kΩ 的上拉电阻,更稳。

3.2 消抖处理机制

按键是机械器件,按下和松开的瞬间,触点会弹跳。你想想看,一个金属片弹来弹去,电平信号就会在高低之间来回跳几次,持续几毫秒到十几毫秒。如果不处理,CPU 可能把一次按键误判成多次。

我在项目中遇到过最夸张的一次,按键按下一次,串口打印了 7 次 "KEY_PRESSED"。嗯,从那以后我再也不敢忽略消抖了。

消抖有两种主流方式:

方式 原理 优点 缺点
硬件消抖 RC 低通滤波 + 施密特触发器 不占 CPU,响应快 增加 BOM 成本
软件消抖 延时采样 / 状态机去抖 零成本,灵活 占用 CPU 时间

大多数项目用软件消抖就够了。我常用的方法是:检测到电平变化后,延时 10~20ms 再读一次,如果电平一致,就确认按键状态有效。

避坑指南:我曾经在延时消抖里直接用了 delay_ms(20),结果系统响应变得很迟钝。后来改用定时器轮询 + 状态机,既消了抖又不阻塞主循环。

3.3 中断与轮询模式对比

检测按键输入,有两种主流模式:轮询和中断。很多初学者觉得中断高大上,轮询很 low,其实不是这么回事。

对比项 轮询模式 中断模式
CPU 占用 持续占用,不断查询 事件触发,空闲时休眠
实时性 取决于轮询周期 立即响应
代码复杂度 简单直观 需要配置 NVIC、中断服务函数
适用场景 按键少、主循环快 按键多、低功耗、高实时

说白了,如果你的主循环跑得很快(比如 1ms 以内),轮询完全够用。但如果你要做低功耗产品,或者按键需要唤醒系统,那就必须用中断。

我个人习惯:原型阶段用轮询,快速验证功能;产品阶段用中断,配合休眠省电。

3.4 按键控制 LED 实战

好了,理论讲完,咱们直接上手。这一节的目标是:按下按键,LED 翻转状态。

硬件连接:

  • PA0 → 按键(上拉输入,按下为低)
  • PC1 → LED(推挽输出,高电平点亮)

3.4.1 轮询模式实现

// 按键初始化
void KEY_Init(void) {
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;  // 上拉输入
    GPIO_Init(GPIOA, &GPIO_InitStructure);
}

// LED 初始化
void LED_Init(void) {
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOC, &GPIO_InitStructure);
    GPIO_ResetBits(GPIOC, GPIO_Pin_1); // 初始熄灭
}

// 带消抖的按键扫描
uint8_t KEY_Scan(void) {
    static uint8_t key_last = 1;  // 上次状态
    uint8_t key_cur = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);
    
    if (key_last != key_cur) {
        Delay_Ms(10);  // 消抖延时
        key_cur = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);
        if (key_last != key_cur) {
            key_last = key_cur;
            if (key_cur == 0) {
                return 1;  // 按下
            }
        }
    }
    return 0;  // 未按下
}

// 主循环
int main(void) {
    KEY_Init();
    LED_Init();
    while(1) {
        if (KEY_Scan()) {
            GPIO_ToggleBits(GPIOC, GPIO_Pin_1);  // 翻转 LED
        }
    }
}

3.4.2 中断模式实现

中断模式需要配置外部中断线。CH32V203 的 PA0 对应 EXTI0 中断线。

// 中断方式按键初始化
void KEY_EXTI_Init(void) {
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    EXTI_InitTypeDef EXTI_InitStructure = {0};
    NVIC_InitTypeDef NVIC_InitStructure = {0};
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
    
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    // 配置 PA0 为 EXTI 源
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
    
    EXTI_InitStructure.EXTI_Line = EXTI_Line0;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStructure);
    
    NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

// 中断服务函数
void EXTI0_IRQHandler(void) {
    if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
        Delay_Ms(10);  // 消抖
        if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0) {
            GPIO_ToggleBits(GPIOC, GPIO_Pin_1);
        }
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}
注意:中断服务函数里尽量少做事。我见过有人在中断里加了大延时,结果系统直接卡死。消抖延时可以用定时器标志替代,或者用状态机去抖。

3.5 知识结构总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作一个快速索引。

GPIO 输入检测知识结构 按键输入检测 按键原理 消抖处理 检测模式 上拉输入 下拉输入 硬件消抖 软件消抖 轮询模式 中断模式 按键控制 LED 实战 轮询实现 中断实现

3.6 写在最后

按键输入看起来简单,但消抖和模式选择这两个点,决定了你产品的稳定性和功耗。我个人建议:

  • 原型阶段用轮询 + 延时消抖,快速出活
  • 产品阶段用中断 + 定时器状态机,兼顾响应和功耗
  • 永远不要在中断里做耗时操作

这一章的内容就到这里。代码可以直接拿到 CH32V203 开发板上跑,有问题欢迎交流。