2. GPIO驱动与中断:GPIO模式配置、轮询读取、外部中断触发、中断优先级

好,咱们进入第二章。这一章讲的是智能手表最底层的交互——按键和唤醒。说白了,就是手表上的那几个物理按键,怎么让芯片知道「我被按了」。

我个人习惯把GPIO驱动比作「芯片的触觉神经」。你想想看,手表就那么几个按键,但背后涉及的模式配置、轮询还是中断、优先级打架,坑其实不少。我当年做第一款手表原型时,就因为在中断优先级上偷了个懒,结果按一下表冠,屏幕闪一下,蓝牙断一次……嗯,那场面,至今难忘。

2.1 GPIO模式配置:别让引脚「裸奔」

GPIO的模式配置,是驱动开发的第一步。很多新手上来就写GPIO_Init(),但里面的参数到底怎么填,其实大有讲究。

智能手表的按键,通常连接到一个GPIO引脚。这个引脚需要配置成什么模式?

  • 输入模式:检测按键是高电平还是低电平。
  • 上拉/下拉电阻:按键没按下时,引脚电平不能悬空。我建议内部上拉,这样按键按下时接地,读取到低电平。
  • 施密特触发器:开启。防止按键抖动带来的毛刺误触发。

核心原则:按键引脚,配置为「带上拉电阻的输入模式」,并开启施密特触发。

代码示例(基于STM32 HAL库):

GPIO_InitTypeDef GPIO_InitStruct = {0};

__HAL_RCC_GPIOA_CLK_ENABLE();  // 开启GPIOA时钟

GPIO_InitStruct.Pin = GPIO_PIN_0;          // 按键接在PA0
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;    // 输入模式
GPIO_InitStruct.Pull = GPIO_PULLUP;        // 内部上拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 低速即可

HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

这里有个细节:Speed参数。按键是低频信号,用低速模式就够了。用高速反而可能引入不必要的功耗和噪声。我在项目中遇到过有人把按键引脚配成高速,结果功耗多了几十微安,找了好久才发现。

2.2 轮询读取:简单但「费电」

轮询,就是CPU不停地去读引脚电平。代码写起来最简单:

while (1) {
    if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
        // 按键被按下
        HAL_Delay(20); // 简单消抖
        // 执行按键处理
    }
}

但问题来了——CPU一直在跑。你想想看,手表待机时,CPU本来可以睡觉的,结果因为轮询,每隔几毫秒就得醒一次。这功耗,电池扛不住。

避坑指南:我曾经在一个低功耗项目里用了轮询,结果待机电流从10μA飙到了500μA。后来改成中断,电流直接降回15μA。所以,智能手表这种电池敏感设备,轮询只适合调试阶段,量产必须用中断

轮询不是不能用,但场景有限:

  • 调试阶段,快速验证硬件连接
  • 非低功耗场景,比如充电时
  • 按键频率极高,中断响应不过来(但手表按键不可能)

2.3 外部中断触发:让芯片「睡等」按键

这才是智能手表该用的方式。外部中断,就是让GPIO引脚的电平变化去唤醒CPU。

配置外部中断,需要做几件事:

  1. 配置GPIO为中断模式
  2. 选择触发边沿(上升沿、下降沿、双边沿)
  3. 编写中断服务函数(ISR)
  4. 在中断服务函数中处理按键逻辑

代码示例:

// 配置PA0为外部中断,下降沿触发(按键按下时从高到低)
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// 配置NVIC(嵌套向量中断控制器)
HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0); // 优先级设为2
HAL_NVIC_EnableIRQ(EXTI0_IRQn);

中断服务函数:

void EXTI0_IRQHandler(void) {
    // 检查中断标志位
    if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET) {
        // 清除中断标志位(必须!)
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
        
        // 按键处理——注意:这里要尽量短
        // 通常只设置一个标志位,主循环去处理
        g_button_pressed = 1;
    }
}

我的经验:中断服务函数里,千万别做耗时操作。比如延时、打印、复杂计算。我曾经见过有人在ISR里调printf,结果串口还没发完,下一个中断又来了,直接死机。正确的做法是:ISR里只设标志位,主循环里处理具体逻辑。

2.4 中断优先级:谁先「插队」?

智能手表里不止一个中断。按键中断、定时器中断、蓝牙中断、传感器中断……它们同时发生时,谁先响应?这就是优先级的事。

中断优先级,说白了就是「插队权」。数值越小,优先级越高。

中断源 推荐优先级 理由
按键中断 2(较高) 用户交互,需要快速响应
定时器中断 3(中等) 周期性任务,可容忍少量延迟
蓝牙中断 4(较低) 数据包可以重传,优先级可低
传感器中断 5(最低) 数据采集,延迟几十毫秒没问题

为什么按键中断优先级要高?因为用户按了键,期望立即看到反馈。如果被蓝牙中断堵住了,屏幕半天没反应,用户会以为手表死机了。

我曾经踩过的坑:有一次我把按键中断优先级设成了最低,结果手表在传输大量蓝牙数据时,按表冠要等2秒才有反应。用户反馈说「这手表反应好慢」。后来我把优先级调到最高,问题解决。记住:用户交互相关的中断,优先级一定要高

另外,还要注意中断嵌套。高优先级中断可以打断低优先级中断。但嵌套层数不宜过多,否则栈空间容易溢出。我一般控制在3层以内。

2.5 综合设计:按键唤醒与防抖

智能手表的按键,还有一个特殊需求——唤醒。手表待机时,CPU进入睡眠模式。按键按下时,通过外部中断把CPU唤醒。

实现方式:

  • 配置按键引脚为外部中断,下降沿触发
  • CPU进入睡眠前,使能该中断
  • 按键按下 → 中断触发 → CPU唤醒 → 执行按键处理

但这里有个经典问题——按键抖动。机械按键在按下和释放的瞬间,电平会多次跳变。如果不处理,一次按键可能触发多次中断。

我的做法是:

  1. 中断触发后,立即关闭该中断(防止重复触发)
  2. 启动一个10ms的定时器
  3. 定时器到期后,重新读取引脚电平,确认按键状态
  4. 重新使能中断

代码思路:

void EXTI0_IRQHandler(void) {
    if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET) {
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
        
        // 1. 关闭中断,防止抖动
        HAL_NVIC_DisableIRQ(EXTI0_IRQn);
        
        // 2. 启动10ms定时器做消抖
        HAL_TIM_Base_Start_IT(&htim2);
        // 定时器回调里再读取按键状态
    }
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM2) {
        // 3. 重新读取按键
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
            // 确认按键按下
            g_button_pressed = 1;
        }
        // 4. 重新使能中断
        HAL_NVIC_EnableIRQ(EXTI0_IRQn);
    }
}

总结一下:GPIO驱动看似简单,但涉及模式配置、轮询与中断的选择、优先级规划、防抖处理。每一个细节都直接影响用户体验和功耗。我个人建议,量产产品一律用中断,优先级给足,防抖做扎实。这样,用户按得爽,电池也扛得住。

下一章,我们会讲定时器与PWM——手表的震动马达和屏幕背光,就靠它了。