3. GPIO与中断工程实践
在MCX系列MCU上,GPIO是所有外设交互的起点:按键、LED、继电器、传感器使能脚、片选信号,最终都落到某个端口的某一位。工程上真正需要掌握的不是“会点亮一个LED”,而是把输入采样、中断响应、优先级调度、机械抖动抑制和状态机任务组织成一套可复现、可扩展的结构。本节以“按键控制LED状态机”为主线,把GPIO输入输出模式、PORT中断、NVIC优先级和去抖动处理串起来。
一、GPIO输入输出模式与寄存器视角
MCX的GPIO由PORT模块统一管理,每个引脚通过PCR(Pin Control Register)配置,关键位包括MUX(引脚复用选择)、PE(上拉/下拉使能)、PS(上拉或下拉选择)、DSE(驱动强度)、ODE(开漏使能)和LK(锁定)。当MUX=1时引脚作为GPIO使用,此时方向由GPIO模块的PDDR(Port Data Direction Register)决定:写1为输出,写0为输入。输出电平由PDOR决定,输入电平从PDIR读取,置位/清零可用PSOR、PCOR,翻转可用PTOR。
按键接法直接决定配置方式。以常见的“按键一端接PTB0,另一端接GND”为例,引脚必须使能内部上拉,即PCR的PE=1、PS=1,方向设为输入。这样按键松开时读到1,按下时读到0,属于低电平有效。若按键接VCC,则应使能下拉(PE=1、PS=0),按下读到1。LED通常接PTD0,经限流电阻到LED再到GND,配置为输出、推挽(ODE=0),驱动强度DSE按LED电流需求选择,普通指示LED用低驱动即可。
// 以PTB0按键(上拉输入)、PTD0 LED(推挽输出)为例
PORTB->PCR[0] = PORT_PCR_MUX(1) | PORT_PCR_PE_MASK | PORT_PCR_PS_MASK;
PORTD->PCR[0] = PORT_PCR_MUX(1);
GPIOB->PDDR &= ~(1u << 0); // PTB0 输入
GPIOD->PDDR |= (1u << 0); // PTD0 输出
GPIOD->PSOR = (1u << 0); // 初始点亮
这里有一个容易踩的坑:如果只配置了GPIO方向而忘记写PCR的MUX,引脚仍停留在复位后的模拟或复用状态,读回值不会随按键变化。调试时先读PCR确认MUX=1,再读PDIR确认电平,是最快的定位路径。
二、按键中断与NVIC优先级配置
轮询按键会占用CPU并错过快速事件,工程上更常用PORT中断。PORT模块支持每个引脚独立配置中断:在PCR中设置IRQC字段选择触发边沿,例如IRQC=0b1010为下降沿触发,0b1001为上升沿触发,0b1011为双边沿触发。使能后,对应引脚的中断标志ISF会置位,并汇聚到NVIC的PORTB_IRQn等中断向量。
NVIC优先级用8位寄存器的高几位表示,MCX通常实现4位优先级,数值越小优先级越高。配置时先设优先级,再使能中断:
NVIC_SetPriority(PORTB_IRQn, 2); // 数值小=优先级高
NVIC_ClearPendingIRQ(PORTB_IRQn);
NVIC_EnableIRQ(PORTB_IRQn);
PORTB->PCR[0] |= PORT_PCR_IRQC(0b1010); // 下降沿
中断服务函数里必须先清标志再处理业务,否则会反复进入:
void PORTB_IRQHandler(void) {
uint32_t f = PORTB->ISFR;
if (f & (1u << 0)) {
PORTB->ISFR = (1u << 0); // 写1清标志
key_event_post(); // 只投递事件,不做重活
}
}
优先级配置要结合系统整体:如果按键中断需要快速响应,可给较高优先级;但如果它只是产生事件,真正耗时处理放在任务里,则优先级不必过高,避免打断通信或定时器中断。原则是“中断短、任务长”,ISR只做标志清除和事件投递。
三、去抖动处理
机械按键在按下和释放瞬间会产生数毫秒到十几毫秒的抖动,直接触发中断会得到多次误触发。常见做法有两类:硬件RC滤波加施密特触发,或软件去抖。软件去抖在ISR中启动一个定时器,延时10~20ms后再采样引脚,若电平仍为有效状态才确认事件。也可以采用“状态机+周期采样”的方式,在1ms定时任务里对按键连续采样,连续N次一致才更新状态。
下面给出一个基于时间戳的轻量去抖判断,适合放在周期任务中:
#define DEBOUNCE_MS 15
static uint32_t last_ms;
static uint8_t stable = 1;
void key_scan(uint32_t now_ms) {
uint8_t raw = (GPIOB->PDIR & (1u << 0)) ? 1 : 0;
if (raw != stable && (now_ms - last_ms) >= DEBOUNCE_MS) {
stable = raw;
last_ms = now_ms;
if (stable == 0) key_event_post(); // 确认按下
}
}
去抖时间不是越小越好:太短会漏掉真实抖动,太长会丢失快速连按。15~20ms对普通按键是较稳妥的折中,具体可结合按键规格书和实测波形调整。
四、LED状态机任务
把LED行为抽象成状态机,可以让主循环保持非阻塞。以“熄灭—常亮—慢闪—快闪”四态循环为例,按键每次确认按下就切换到下一状态。状态机保存当前状态和上次翻转时间,在周期任务中根据状态决定LED输出和翻转间隔。
typedef enum { LED_OFF, LED_ON, LED_SLOW, LED_FAST } led_state_t;
static led_state_t st = LED_OFF;
static uint32_t t_last;
void led_task(uint32_t now_ms) {
uint32_t period = (st == LED_SLOW) ? 500 :
(st == LED_FAST) ? 100 : 0;
switch (st) {
case LED_OFF: GPIOD->PCOR = (1u << 0); break;
case LED_ON: GPIOD->PSOR = (1u << 0); break;
default:
if (now_ms - t_last >= period) {
t_last = now_ms;
GPIOD->PTOR = (1u << 0); // 翻转
}
}
}
void key_event_post(void) {
st = (led_state_t)((st + 1) % 4);
t_last = systick_ms();
}
这样ISR只负责投递事件,去抖和状态切换在主循环完成,LED闪烁不占用阻塞延时,系统还能同时处理串口、ADC等任务。
五、工程检查要点
把上述内容落到工程上,建议按以下顺序核对:第一,确认每个引脚的PCR中MUX=1,输入引脚上拉或下拉与硬件接法一致;第二,确认PDDR方向与用途匹配,输出引脚初始电平明确;第三,中断引脚的IRQC触发边沿与按键有效电平一致,ISR中先清ISFR再投递事件;第四,NVIC优先级数值与系统其他中断协调,避免高优先级中断长时间占用;第五,去抖时间在15~20ms之间选取,并用示波器或逻辑分析仪观察按键波形验证;第六,LED状态机采用非阻塞结构,翻转间隔由系统时基驱动,不出现delay阻塞。
这套结构可以自然扩展到多按键、多LED和组合事件:每个按键对应一个事件位,状态机根据事件位切换,去抖参数集中定义,NVIC优先级按实时性要求分配。掌握这一节后,后续的定时器、串口和低功耗唤醒都可以沿用同样的“中断投递事件、任务处理业务”的分层思路。