3. I2C总线协议核心原理
GPIO输入和中断,是嵌入式开发里最基础也最常用的功能。说白了,就是让芯片「感知」外部世界的变化。我刚开始学STM32时,第一个实验就是按键控制LED——按下点亮,松开熄灭。别看简单,这里面把GPIO输入模式、中断配置、优先级管理全串起来了。
今天我们就从实战角度,把这块彻底讲透。
3.1 GPIO输入模式:不只是「读引脚」那么简单
很多人觉得GPIO输入就是调用个HAL_GPIO_ReadPin()完事。嗯,其实没那么简单。输入模式有好几种,选错了,轻则信号抖动,重则直接读不到正确电平。
3.1.1 四种输入模式对比
| 模式 | 上拉/下拉 | 典型场景 | 注意事项 |
|---|---|---|---|
| 浮空输入 | 无 | 外部已有上拉/下拉 | 引脚悬空时电平不确定 |
| 上拉输入 | 内部上拉(约40kΩ) | 按键接地 | 默认高电平,按下变低 |
| 下拉输入 | 内部下拉(约40kΩ) | 按键接VCC | 默认低电平,按下变高 |
| 模拟输入 | 无 | ADC采样 | 数字功能关闭 |
我个人习惯:按键接地 + 上拉输入。为什么?因为大多数开发板按键设计就是一端接地,另一端接GPIO。你想想看,如果选浮空输入,手指碰一下引脚电平就乱跳,那还怎么玩?
3.1.2 输入配置的CubeMX操作
打开CubeMX,选中你要用的GPIO引脚,在右侧的GPIO Output Level选项里——等等,这里要注意:输入模式没有Output Level,你要选的是GPIO mode为Input mode。然后在下方的GPIO Pull-up/Pull-down里选Pull-up。
举个例子,配置PA0为按键输入:
/* 这是CubeMX自动生成的代码,我们看看就好 */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
代码很简单对吧?但实际项目中,我建议你在初始化之后加个延时,等电平稳定再读。尤其是按键接长线的时候,寄生电容会让电平建立变慢。
3.2 外部中断(EXTI):让CPU别再傻等
轮询读按键有个大问题:CPU得一直盯着那个引脚,啥也干不了。你想想看,如果主循环里有个1秒的延时,按键按下去得等1秒才有反应,这体验也太差了。
中断就是来解决这个问题的。当引脚电平变化时,硬件自动暂停当前任务,跳转到中断服务函数。处理完再回来,就像接了个电话。
3.2.1 EXTI的触发方式
| 触发方式 | 说明 | 适用场景 |
|---|---|---|
| 上升沿触发 | 低→高跳变时触发 | 按键释放检测 |
| 下降沿触发 | 高→低跳变时触发 | 按键按下检测 |
| 双边沿触发 | 任意跳变都触发 | 编码器、脉冲计数 |
我在项目中遇到过一个问题:用下降沿触发按键,结果按一次触发了两次中断。后来发现是按键机械抖动导致的。所以硬件上要加RC滤波,软件上要做消抖处理。
3.2.2 CubeMX中配置EXTI
在CubeMX里配置EXTI很简单:
- 选中GPIO引脚,比如PA0
- GPIO mode选
External Interrupt Mode with Rising/Falling edge trigger detection - 根据需求选上升沿、下降沿或双边沿
- GPIO Pull-up/Pull-down选Pull-up(按键接地场景)
- 在NVIC Settings选项卡里使能EXTI line中断
生成的初始化代码大致是这样:
/* EXTI初始化 */
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, 0, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
3.3 中断优先级管理:别让重要的事被插队
中断优先级,说白了就是「谁更紧急」。STM32的中断优先级分为抢占优先级和子优先级。抢占优先级高的可以打断低的,同级的按子优先级排队。
3.3.1 优先级分组
STM32用4位二进制数表示优先级,通过NVIC_PriorityGroupConfig()来分配:
| 分组 | 抢占优先级位数 | 子优先级位数 | 典型应用 |
|---|---|---|---|
| Group 0 | 0 | 4 | 所有中断同级 |
| Group 1 | 1 | 3 | 简单系统 |
| Group 2 | 2 | 2 | 常用配置 |
| Group 3 | 3 | 1 | 多优先级场景 |
| Group 4 | 4 | 0 | 强抢占需求 |
我个人习惯用Group 2,也就是4级抢占优先级、4级子优先级。够用,也不容易搞乱。
3.3.2 在CubeMX中设置优先级
在NVIC Settings选项卡里,每个中断都可以单独设置Preemption Priority和Sub Priority。CubeMX会自动检查优先级分组是否一致,如果不一致会报错。
举个例子,我们给EXTI0中断设优先级为2,子优先级为1:
HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 1);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
注意:HAL_NVIC_SetPriority()的第一个参数是IRQn号,第二个是抢占优先级,第三个是子优先级。数值越小,优先级越高。
3.4 HAL库GPIO回调函数:中断处理的优雅写法
早期标准库时代,我们直接在中断服务函数里写业务逻辑。结果就是中断函数又长又乱,还容易出问题。HAL库引入了回调机制,把中断处理和业务逻辑分开了。
3.4.1 回调函数的工作流程
当EXTI中断触发时,硬件跳转到对应的中断服务函数(如EXTI0_IRQHandler())。这个函数由HAL库实现,它会:
- 检查中断标志位
- 清除中断标志位
- 调用
HAL_GPIO_EXTI_Callback()回调函数
我们只需要重写这个回调函数,在里面写自己的业务逻辑就行。
/* 在用户文件中重写回调函数 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0)
{
/* 按键按下,翻转LED */
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
/* 这里可以加消抖延时 */
HAL_Delay(20);
/* 等待按键释放 */
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET);
}
}
3.4.2 多引脚共用回调
多个EXTI中断共用一个回调函数,通过GPIO_Pin参数区分。比如PA0和PC13都配置为EXTI中断:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
switch(GPIO_Pin)
{
case GPIO_PIN_0:
/* PA0按键处理 */
break;
case GPIO_PIN_13:
/* PC13按键处理 */
break;
default:
break;
}
}
这样写清晰多了。每个引脚的处理逻辑独立,互不干扰。
3.5 知识结构图:GPIO输入与中断全景
下面这张图把本章的核心知识点串起来了。从GPIO输入模式的选择,到EXTI中断的配置,再到优先级管理和回调函数的使用,一目了然。
3.6 实战要点总结
说了这么多,最后给你几个实战中一定会用到的要点:
- 输入模式选型:按键接地用上拉输入,按键接VCC用下拉输入,外部已有上拉/下拉用浮空输入
- 中断触发选择:按键按下用下降沿,按键释放用上升沿,编码器用双边沿
- 消抖不能省:硬件加RC滤波,软件加10-20ms延时或定时器消抖
- 优先级分配:时间敏感的中断给高优先级,按键等交互中断给低优先级
- 回调函数:只设标志位,不做耗时操作,真正的处理放到主循环