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。
配置外部中断,需要做几件事:
- 配置GPIO为中断模式
- 选择触发边沿(上升沿、下降沿、双边沿)
- 编写中断服务函数(ISR)
- 在中断服务函数中处理按键逻辑
代码示例:
// 配置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唤醒 → 执行按键处理
但这里有个经典问题——按键抖动。机械按键在按下和释放的瞬间,电平会多次跳变。如果不处理,一次按键可能触发多次中断。
我的做法是:
- 中断触发后,立即关闭该中断(防止重复触发)
- 启动一个10ms的定时器
- 定时器到期后,重新读取引脚电平,确认按键状态
- 重新使能中断
代码思路:
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——手表的震动马达和屏幕背光,就靠它了。