4、GPIO与外部中断:GPIO寄存器配置、中断控制器、按键中断实战
好,咱们今天聊点硬核的。GPIO和外部中断,这俩东西在嵌入式里就像吃饭喝水一样平常。但越是基础的东西,坑往往越多。我刚开始做NXP项目那会儿,就因为在GPIO配置上少看了一行寄存器描述,结果板子死活不响应中断,折腾了一整天。嗯,后来发现是引脚复用没配对。
说白了,GPIO就是芯片和外界打交道的最基本方式。而外部中断,就是让芯片能“主动”响应外部事件,不用你一直轮询。今天咱们就把它彻底搞明白。
4.1 GPIO寄存器配置:别光看,得动手
NXP的MCU,比如i.MX RT系列或者LPC系列,GPIO寄存器其实大同小异。我个人习惯,拿到一个新芯片,先看三样东西:方向寄存器(DIR)、数据寄存器(DR)、和置位/清零寄存器(PSR/CLR)。
你想想看,一个引脚要当输入还是输出,不就是改DIR里对应的那一位吗?输出高电平还是低电平,就是写DR。但这里有个小技巧——我建议你尽量用置位/清零寄存器来操作,而不是直接写DR。为什么?因为多任务环境下,直接读写DR可能会被中断打断,造成数据不一致。用PSR和CLR是原子操作,安全得多。
核心寄存器速查表
| 寄存器 | 功能 | 我常用的操作 |
|---|---|---|
| GPIOx_DIR | 方向控制(0=输入,1=输出) | 先读-改-写,或者直接写 |
| GPIOx_DR | 数据寄存器,读写引脚电平 | 读输入,写输出 |
| GPIOx_PSR | 置位寄存器,写1对应位输出高 | 只写,不影响其他位 |
| GPIOx_CLR | 清零寄存器,写1对应位输出低 | 只写,不影响其他位 |
配置步骤其实很简单,就三步:
- 使能时钟——别忘了,NXP的GPIO模块通常有门控时钟,不打开时钟,写寄存器等于白写。
- 配置引脚复用——把引脚从默认功能(比如JTAG、ADC)切换到GPIO模式。这一步在IOMUXC寄存器里配。
- 设置方向和初始电平——DIR设好,DR或PSR/CLR设好初始值。
来,看个实际代码。这是我在i.MX RT1052上配置一个LED引脚为输出的例子:
// 使能GPIO1时钟
CCM->CCGR1 |= CCM_CCGR1_CG15(0x3);
// 引脚复用为GPIO(假设是GPIO1_IO03)
IOMUXC->SW_MUX_CTL_PAD[IOMUXC_GPIO_AD_B0_03_GPIO1_IO03] = 5;
// 设置方向为输出
GPIO1->DIR |= (1 << 3);
// 初始输出高电平(LED灭)
GPIO1->PSR = (1 << 3);
嗯,这里要注意:IOMUXC的索引值每个芯片都不一样,一定要查数据手册。我曾经因为看错了引脚号,把SW_MUX_CTL配到了隔壁引脚上,结果灯死活不亮。排查了半天才发现是索引搞错了。
4.2 中断控制器:NVIC才是幕后老板
GPIO本身只能检测电平变化,但要让CPU停下来处理,就得靠中断控制器。NXP的ARM Cortex-M内核用的是NVIC(嵌套向量中断控制器)。说白了,NVIC就是个总调度,它决定哪个中断能打断CPU,哪个得排队等着。
我个人觉得,理解NVIC的关键就三个寄存器组:
- ISER(中断使能寄存器)——写1使能某个中断。这是第一步。
- IP(中断优先级寄存器)——设置抢占优先级和子优先级。注意,优先级数值越小,优先级越高。
- ICER(中断清除寄存器)——临时关掉某个中断,但不用改ISER。
你可能会问:“为什么要有ICER?直接用ISER清零不行吗?”行是行,但ICER是原子操作。在多中断环境下,用ISER清零可能会被其他中断干扰。我建议你养成习惯:使能用ISER,禁能用ICER。
我的小技巧: 调试中断时,先关掉所有中断优化,把优先级设成一样(比如0),跑通了再慢慢调优先级。这样可以排除优先级反转带来的诡异问题。
4.3 按键中断实战:从按下到响应,每一步都得对
好,理论说完了,咱们来真的。做一个按键中断,按下按键,LED翻转。这个实验我做了不下十次,每次教课都拿它当例子。因为它麻雀虽小,五脏俱全。
先理一下硬件连接:
- 按键接GPIO1_IO04,外部上拉,按下为低电平。
- LED接GPIO1_IO03,低电平点亮。
配置步骤分四块:
4.3.1 GPIO初始化
跟刚才一样,使能时钟、配复用、设方向。但这次按键引脚要设成输入,并且要开启内部上拉(如果外部没有上拉电阻的话)。
// 按键引脚:输入,内部上拉
IOMUXC->SW_MUX_CTL_PAD[IOMUXC_GPIO_AD_B0_04_GPIO1_IO04] = 5;
IOMUXC->SW_PAD_CTL_PAD[IOMUXC_GPIO_AD_B0_04_GPIO1_IO04] =
IOMUXC_SW_PAD_CTL_PAD_PKE(1) | // 使能上下拉
IOMUXC_SW_PAD_CTL_PAD_PUE(1) | // 选择上拉
IOMUXC_SW_PAD_CTL_PAD_PUS(0); // 100K上拉
GPIO1->DIR &= ~(1 << 4); // 输入
4.3.2 GPIO中断配置
NXP的GPIO模块自带中断功能。每个引脚可以配置为上升沿、下降沿、或者双边沿触发。这里按键按下是下降沿,松开是上升沿。我一般用双边沿触发,这样按下和松开都能检测到。但如果你只关心按下动作,用下降沿就够了。
// 使能GPIO1中断功能
GPIO1->IMR |= (1 << 4); // 使能引脚4的中断
// 配置触发方式:下降沿触发
GPIO1->ICR1 &= ~(0x3 << (4*2)); // 先清零
GPIO1->ICR1 |= (0x2 << (4*2)); // 下降沿触发
// 清除可能遗留的中断标志
GPIO1->ISR = (1 << 4);
这里有个坑:ICR寄存器是按两个bit配置一个引脚的。GPIO1_IO04对应ICR1的bit[9:8]。我刚开始总是算错偏移量,后来干脆写了个宏,省得每次手算。
4.3.3 NVIC配置
GPIO中断准备好了,但NVIC还不知道呢。得告诉它:GPIO1的中断号是多少,优先级多高。
// 使能GPIO1组合中断(i.MX RT中GPIO1中断号是64)
NVIC_EnableIRQ(GPIO1_Combined_0_15_IRQn);
// 设置优先级为2(0最高,15最低)
NVIC_SetPriority(GPIO1_Combined_0_15_IRQn, 2);
注意:不同NXP芯片的GPIO中断号不一样。LPC系列可能是单独的引脚中断,i.MX RT系列是组合中断(一组引脚共用一个中断号)。一定要查芯片的《中断向量表》。
4.3.4 中断服务函数
最后,写中断服务函数。进去第一件事:清中断标志。不清的话,会一直进中断,CPU就卡死了。我见过有人忘了清标志,结果程序跑起来像死机了一样,其实就是中断风暴。
void GPIO1_Combined_0_15_IRQHandler(void)
{
// 检查是不是引脚4触发的中断
if(GPIO1->ISR & (1 << 4))
{
// 消抖:延时10ms再检测一次
delay_ms(10);
if((GPIO1->DR & (1 << 4)) == 0) // 确实按下了
{
// 翻转LED
GPIO1->DR ^= (1 << 3);
}
// 清中断标志
GPIO1->ISR = (1 << 4);
}
}
警告: 中断服务函数里不要做复杂操作!延时10ms已经是极限了。如果你要处理更复杂的逻辑,建议设一个标志位,在主循环里处理。否则会影响其他中断的响应时间。
4.4 避坑指南:我踩过的那些雷
讲到这里,我分享几个实战中遇到的坑,你遇到了可以少走弯路:
- 引脚复用冲突: 有些引脚默认是JTAG或调试功能。比如i.MX RT的GPIO1_IO00-03,默认是JTAG。你得先在IOMUXC里把它们切到GPIO模式,否则中断根本不会触发。
- 中断优先级分组: NVIC的优先级分组(Group Priority)和子优先级(Sub Priority)要搞清楚。默认分组可能不是你想要的。我建议统一用4位抢占优先级,0位子优先级,简单粗暴。
- 中断嵌套: 如果你的系统有多个中断,记得在进入临界区时关中断。用__disable_irq()和__enable_irq(),但别长时间关中断,否则实时性就没了。
- 按键消抖: 机械按键按下时会有抖动,持续时间大概5-20ms。硬件上可以加RC滤波,软件上就是延时再检测。我一般用10ms延时,够用。
好了,GPIO和外部中断就聊到这儿。说白了,就是配寄存器、连NVIC、写服务函数这三板斧。但每一板斧都有细节,细节决定成败。你回去拿块开发板,照着代码敲一遍,遇到问题再回来翻翻这节课。嗯,动手才是最好的学习方式。