第一章 GPIO基础与按键输入
各位同学,欢迎来到《从零搭建血压计嵌入式开发环境》的第一课。今天咱们聊聊嵌入式开发里最基础、也最容易被忽视的部分——GPIO和按键输入。
说实话,我刚开始做嵌入式那会儿,觉得GPIO不就是个高低电平嘛,有啥好学的?直到有一次,一个简单的按键检测程序,让我在产线上折腾了整整两天……嗯,从那以后,我再也不敢小看这个“基础”了。
1.1 GPIO模式详解
GPIO,全称是General Purpose Input Output,通用输入输出口。说白了,就是芯片上那些可以编程控制的引脚。
但你知道吗?同一个引脚,在不同模式下,电气特性是完全不同的。我见过不少新手,上来就配成推挽输出,结果接了个开漏设备,信号死活不对。
| GPIO模式 | 典型应用 | 注意事项 |
|---|---|---|
| 推挽输出 | 驱动LED、蜂鸣器 | 能输出高低电平,驱动能力强 |
| 开漏输出 | I2C总线、电平转换 | 只能输出低电平,高电平靠上拉 |
| 浮空输入 | 按键检测(需外接上拉) | 电平不确定,容易误触发 |
| 上拉输入 | 按键检测(常用) | 内部上拉电阻,省外部元件 |
| 下拉输入 | 检测高电平有效信号 | 内部下拉电阻 |
| 模拟模式 | ADC采样 | 关闭数字功能,降低功耗 |
个人经验:在血压计项目中,按键输入我习惯用上拉输入模式。为什么?因为按键按下时接地,松开时被拉到高电平,逻辑清晰,而且省掉了外部上拉电阻,PCB面积能省一点是一点。
1.2 按键消抖原理
按键消抖,这是个老生常谈的问题。但你真的理解为什么要消抖吗?
机械按键按下和松开的瞬间,触点会弹跳。你想想看,一个金属片和另一个金属片碰撞,怎么可能一下子就稳定接触?这个弹跳过程大概持续5-20ms,期间会产生多个高低电平脉冲。
如果不做消抖,会发生什么?
- 按一次键,程序检测到多次触发
- 血压计可能一次测量启动好几次
- 用户界面乱跳,体验极差
消抖的方法主要有两种:
硬件消抖:在按键两端并联一个电容(通常0.1μF),利用电容的充放电特性滤除毛刺。这个方法简单粗暴,但会增加BOM成本。
软件消抖:检测到电平变化后,延时10-20ms再读一次。如果电平一致,才认为是有效按键。我个人更推荐软件消抖,因为零成本,而且灵活。
避坑指南:我曾经在一个项目中,用了10ms延时消抖,结果用户按键太快,程序反应不过来。后来改成“连续采样3次,两次一致即有效”的算法,效果好了很多。
1.3 外部中断配置
轮询检测按键,说白了就是CPU一直在问“你按了吗?你按了吗?”——这太浪费了。更好的方式是外部中断:按键按下时,主动通知CPU。
配置外部中断,一般需要以下几个步骤:
- 使能GPIO时钟和中断控制器时钟
- 配置GPIO为输入模式(上拉或下拉)
- 将GPIO映射到外部中断线
- 设置触发方式(上升沿、下降沿、双边沿)
- 配置中断优先级
- 使能中断
以STM32为例,代码大概是这样的:
// 配置PA0为外部中断,下降沿触发
void EXTI_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
EXTI_InitTypeDef EXTI_InitStruct;
NVIC_InitTypeDef NVIC_InitStruct;
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
// 2. 配置GPIO
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 映射中断线
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
// 4. 配置EXTI
EXTI_InitStruct.EXTI_Line = EXTI_Line0;
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStruct);
// 5. 配置NVIC
NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0x01;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0x01;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
}
注意:同一个中断线不能同时映射多个GPIO。比如PA0和PB0不能同时使用EXTI0线。这是硬件限制,没得商量。
1.4 中断服务函数编写
中断服务函数,就是CPU响应中断后执行的代码。写这个函数有几个铁律:
- 快进快出——不要在中断里做复杂运算
- 清除标志位——否则会一直进中断
- 避免延时——中断里用delay,等于自杀
我见过最离谱的代码,是在中断里调了个printf,结果整个系统卡死。为什么?因为printf本身可能触发中断,形成嵌套,最后栈溢出。
正确的做法是:中断里只做标记,具体处理放到主循环里。
// 全局标志位
volatile uint8_t g_key_pressed = 0;
// 中断服务函数
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 简单的软件消抖:延时10ms再检测
// 注意:这里不能用HAL_Delay,要用简单的循环延时
for(volatile uint32_t i = 0; i < 10000; i++);
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0)
{
g_key_pressed = 1; // 只做标记
}
EXTI_ClearITPendingBit(EXTI_Line0); // 清除标志位
}
}
// 主循环中处理
int main(void)
{
// 初始化...
EXTI_Config();
while(1)
{
if(g_key_pressed)
{
g_key_pressed = 0; // 清除标记
// 在这里处理按键逻辑
// 比如启动血压测量、切换显示模式等
Key_Process();
}
// 其他任务...
}
}
小技巧:中断里用的变量,记得加volatile关键字。否则编译器优化时,可能认为这个变量永远不会被修改,导致逻辑错误。我踩过这个坑,debug了一下午才发现。
好了,这一章的内容就到这里。GPIO看似简单,但它是嵌入式系统的基石。按键输入更是人机交互的第一步。把这些基础打牢,后面的血压计项目才能走得稳。
下一章,咱们聊聊定时器和PWM——怎么用它们来控制血压计的充气泵和泄气阀。到时候见。