3、GPIO与中断:GPIO配置模式(推挽、开漏、上拉下拉),外部中断触发机制与优先级设置
好,咱们今天聊点硬核的。GPIO和中断,这俩东西你看着简单,但真到了项目里,坑一个接一个。我这些年调试过的板子,少说也有几十种了,每次翻车,十有八九都跟GPIO配置或者中断优先级有关。
说白了,GPIO就是芯片跟外界打招呼的手。而中断,就是芯片被外界拍了一下肩膀,得停下来看看谁在叫它。这两者配合好了,你的系统就稳如老狗;配合不好,那就是各种灵异现象。
3.1 GPIO配置模式:推挽、开漏、上拉下拉
先说说GPIO的模式。很多人觉得这有啥好讲的,不就是输入输出吗?嗯,你要是这么想,那后面肯定要吃亏。
3.1.1 推挽输出(Push-Pull)
推挽输出,说白了就是芯片内部有两个管子,一个负责推(输出高电平),一个负责挽(输出低电平)。你让它输出高,它就给你怼到VCC;你让它输出低,它就给你拉到GND。
特点:
- 输出能力强,驱动电流大
- 高低电平都很干脆,没有中间状态
- 不能多个输出直接连在一起(会打架)
我个人的习惯:控制LED、蜂鸣器这类负载,我基本都用推挽输出。简单粗暴,效果直接。
举个例子,驱动一个LED:
// 推挽输出配置(以STM32为例)
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 点亮LED
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);
3.1.2 开漏输出(Open-Drain)
开漏输出就有点意思了。它只有拉低的能力,没有拉高的本事。你想让它输出高电平?对不起,它只能把管子断开,让外部上拉电阻去拉高。
为什么要这么设计?我遇到过这样一个场景:两个设备要共用一个信号线,一个说高,一个说低,那不就短路了吗?开漏输出就能解决这个问题——谁想拉低就拉低,谁想释放就释放,大家和平共处。
典型应用场景:
- I2C总线(SDA和SCL都是开漏)
- 多个设备共享的中断信号线
- 电平转换(3.3V和5V系统互联)
小提示:用开漏输出时,千万别忘了外部上拉电阻。我见过有人忘了加上拉,结果总线一直浮空,数据全乱套了。电阻值一般选4.7kΩ到10kΩ,具体看总线速度和负载电容。
// 开漏输出配置
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出
GPIO_InitStruct.Pull = GPIO_NOPULL; // 外部上拉,内部不拉
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
3.1.3 上拉与下拉电阻
这个其实不算是输出模式,而是输入模式的配置。但很多人把它跟输出模式搞混了,我单独拎出来讲。
上拉电阻,就是把引脚通过一个电阻接到VCC。下拉电阻,就是接到GND。它们的作用只有一个:防止引脚浮空。
你想想看,一个输入引脚啥都没接,它的电平是多少?鬼知道。可能是高,可能是低,还可能随着周围的电磁场乱跳。这就是浮空,是很多奇怪bug的根源。
| 配置 | 默认电平 | 适用场景 |
|---|---|---|
| 上拉输入 | 高电平 | 按键检测(按下为低) |
| 下拉输入 | 低电平 | 按键检测(按下为高) |
| 浮空输入 | 不确定 | 外部已有上拉/下拉的场合 |
注意:内部上拉电阻的阻值一般在30kΩ~50kΩ之间,比较弱。如果你的环境干扰很强,建议外部再加一个10kΩ的上拉。我曾经在一个电机驱动项目里,就因为偷懒用了内部上拉,结果信号被电机产生的EMI干扰得一塌糊涂。
3.2 外部中断触发机制
好,GPIO讲完了,咱们聊聊中断。中断这东西,说白了就是让CPU别傻等着,有事再叫它。
3.2.1 触发方式
外部中断的触发方式,常见的有这么几种:
- 上升沿触发:信号从低变高的那一刻触发
- 下降沿触发:信号从高变低的那一刻触发
- 双边沿触发:不管上升还是下降,变了就触发
- 低电平触发:只要信号是低电平就一直触发
- 高电平触发:只要信号是高电平就一直触发
你可能会问,边沿触发和电平触发有啥区别?我举个例子你就明白了。
假设你用一个按键来控制灯的开关。如果用下降沿触发,按一下触发一次,灯亮;再按一下,又触发一次,灯灭。干净利落。
但如果用低电平触发呢?你按下去,灯亮;你一直按着,CPU就一直响应中断,别的活都干不了。等你松手,灯灭。这体验就很糟糕了。
我的经验:按键检测用下降沿触发,配合软件消抖。传感器信号变化用边沿触发。需要持续响应的场景(比如紧急停止)才用电平触发。
3.2.2 硬件消抖
说到按键,就不得不提抖动。机械按键按下和释放的瞬间,会有一个短暂的抖动期,大概5-20ms。如果不处理,一次按键可能触发好几次中断。
硬件消抖的方法很简单:加一个RC低通滤波器。电阻选10kΩ,电容选0.1μF,时间常数τ=RC=1ms,基本能把抖动滤掉。
// 软件消抖示例(在中断服务函数中)
void EXTI0_IRQHandler(void)
{
// 检查中断标志
if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET)
{
// 延时10ms消抖
HAL_Delay(10);
// 再次读取引脚状态,确认确实是按下
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
{
// 执行按键处理逻辑
key_press_handler();
}
// 清除中断标志
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
}
}
小技巧:中断服务函数里尽量少做事,尤其是别用HAL_Delay这种阻塞函数。我一般只在中断里设置一个标志位,真正的处理放到主循环里去做。这样既不会错过中断,也不会影响系统响应。
3.3 中断优先级设置
中断优先级,这是个容易让人头大的东西。但搞懂了,你的系统就能有条不紊地工作。
3.3.1 抢占优先级与子优先级
大多数ARM Cortex-M内核的MCU,中断优先级分为两部分:
- 抢占优先级:数值越小,优先级越高。高抢占优先级的中断可以打断低优先级的中断。
- 子优先级:当抢占优先级相同时,子优先级高的先执行。但子优先级不能打断同级的中断。
举个例子,你正在处理一个串口中断(抢占优先级2),突然来了一个定时器中断(抢占优先级1)。因为定时器的抢占优先级更高,它会打断串口中断,先执行完再回来继续处理串口。
| 中断源 | 抢占优先级 | 子优先级 | 能否打断 |
|---|---|---|---|
| 定时器 | 0 | 0 | 能打断所有 |
| 外部中断 | 1 | 0 | 能打断串口 |
| 串口 | 2 | 0 | 不能打断别人 |
3.3.2 优先级分组
STM32里有个叫NVIC的东西,它允许你把优先级分成不同的组。比如:
- 分组0:所有位都是子优先级(没有抢占)
- 分组1:1位抢占,3位子优先级
- 分组2:2位抢占,2位子优先级
- 分组3:3位抢占,1位子优先级
- 分组4:所有位都是抢占优先级
我个人习惯用分组2,也就是2位抢占、2位子优先级。这样最多有4级抢占,4级子优先级,够用了。
// 设置优先级分组
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
// 配置外部中断优先级
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0); // 抢占1,子优先级0
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
// 配置定时器中断优先级
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0); // 抢占0,最高优先级
HAL_NVIC_EnableIRQ(TIM2_IRQn);
避坑指南:我曾经在一个项目中,把所有中断的抢占优先级都设成了0。结果一个高频中断把系统拖死了,低优先级的任务永远得不到执行。后来我学乖了:实时性要求高的(比如电机控制)给高优先级,非实时的(比如按键、串口)给低优先级。
3.3.3 中断嵌套的注意事项
中断嵌套虽然好用,但也要小心。嵌套太深,栈空间可能不够用。我见过有人嵌套了5层中断,结果栈溢出,程序跑飞了。
另外,中断服务函数里尽量不要调用那些可能引起阻塞的函数。比如printf、HAL_Delay、malloc这些,能不用就不用。实在要用,确保中断优先级足够低,不会被其他中断打断。
嗯,GPIO和中断这块,内容其实挺多的。但只要你理解了推挽和开漏的区别,掌握了上拉下拉的用法,搞清楚了中断触发方式和优先级设置,大部分项目都能应付了。剩下的,就是在实际项目中慢慢积累了。