1. GPIO寄存器体系:从硬件接口到软件控制的桥梁
在嵌入式开发领域,通用输入输出(GPIO)接口是连接微控制器与外部物理世界的基石。无论是点亮一个LED,读取一个按键状态,还是与传感器进行通信,都离不开对GPIO引脚的精确控制。然而,很多开发者在使用高级库函数或驱动框架时,往往忽略了其底层运作的核心——寄存器。理解GPIO的寄存器,特别是输入数据寄存器(IN_DATAn)和中断状态寄存器(INTSTATn),是深入掌握嵌入式系统硬件交互、实现高效可靠代码的关键。这不仅仅是知道如何调用一个digitalRead()函数,而是理解当你调用这个函数时,处理器内部究竟发生了什么,时钟如何同步,数据如何流动,以及中断如何被精准地捕获和响应。本文将带你深入TI微控制器(以SPRUH90D文档描述的体系为例)的GPIO寄存器世界,拆解其设计原理、操作细节,并分享在实际项目中应用这些知识来优化性能、解决棘手问题的实战经验。
2. GPIO输入数据寄存器(IN_DATAn)深度解析
2.1 寄存器功能与访问机制
GPIO输入数据寄存器,通常命名为IN_DATAn(其中n代表特定的组或Bank编号),其核心功能非常直接:读取指定GPIO引脚当前的逻辑电平状态。这是一个只读(Read-Only, R)寄存器,意味着软件只能从中读取值,而不能通过写入来改变引脚状态。这一点至关重要,它从硬件上防止了软件误操作导致输出冲突。
根据技术手册描述,IN_DATAn寄存器的行为会根据GPIO引脚的配置模式(输入或输出)而略有不同:
- 当引脚配置为输入时:读取
IN_DATAn返回的是经过GPIO外设时钟同步后的输入信号状态。这个“同步”过程是数字电路可靠性的关键,它通过触发器对异步的外部信号进行采样,将其与系统时钟对齐,从而避免亚稳态问题,确保读取的值是稳定和确定的。 - 当引脚配置为输出时:读取
IN_DATAn返回的并不是外部电路反馈回来的电平(虽然有时物理上可能是相同的),而是设备当前正在驱动的输出值。这让你可以确认你之前写入输出数据寄存器的值是否已被成功锁存并驱动到引脚上。
以IN_DATA01寄存器为例(对应Bank 0和Bank 1),它是一个32位寄存器,其位域分配清晰:
- 位[15:0]:对应GPIO Bank 0的16个引脚(GP0P0 到 GP0P15)。
- 位[31:16]:对应GPIO Bank 1的16个引脚(GP1P0 到 GP1P15)。 每一位(GPkPj)直接映射到特定Bank(k)的特定引脚(j)。读取该位,0表示逻辑低电平,1表示逻辑高电平。
2.2 同步采样原理与抗抖动设计
“同步到GPIO外设时钟”这句话背后是硬件设计上的深思熟虑。外部世界的信号变化是异步于微控制器内部系统时钟的。如果直接用内部逻辑去读取一个异步变化的信号,极有可能在时钟边沿附近采样到正在变化的不稳定信号,导致系统内部产生亚稳态,进而引发不可预测的逻辑错误甚至系统崩溃。
IN_DATAn寄存器之前的同步器通常由两级或多级D触发器构成。外部引脚信号首先进入第一个触发器,由GPIO时钟进行第一次采样。这个采样值可能仍处于亚稳态。经过一个时钟周期后,第二个触发器对第一个触发器的输出进行再次采样,此时得到的信号稳定概率就大大增加,成为一个与内部时钟域同步的、干净的数字信号,然后才被送入IN_DATAn寄存器供CPU读取。
注意:这个同步过程会引入1到2个时钟周期的延迟。在编写对时序要求极其苛刻的代码时(例如软件模拟高速通信协议),必须将这个延迟考虑在内。你不能假设读取到的电平变化与外部信号变化是瞬间同步的。
在实际应用中,仅仅同步还不够。机械开关、长导线感应等都可能在引脚上引入毛刺(Glitch)。因此,许多微控制器的GPIO模块在同步器之后,还会包含一个可编程的数字滤波器(噪声滤波器)。它可以配置为对采样信号进行多次采样,只有连续几次采样值一致时才认为信号有效,从而滤除窄脉冲噪声。虽然IN_DATAn本身不直接体现滤波功能,但它是滤波后结果的呈现窗口。在配置GPIO时,如果发现读取的值有随机跳变,除了检查硬件电路,也应查阅手册确认是否启用了输入滤波功能及其参数是否合适。
2.3 实战应用:可靠的按键检测与传感器读取
理解了IN_DATAn的原理,我们就能写出更可靠的底层代码。以下是一个基于寄存器直接操作、包含去抖的按键检测示例(假设使用Bank0的Pin5):
// 假设 GPIO Bank0 基地址为 0x4804C000, IN_DATA01 偏移为 0x138 #define GPIO_BANK0_BASE 0x4804C000 #define GPIO_IN_DATA01 (*(volatile unsigned int *)(GPIO_BANK0_BASE + 0x138)) #define BUTTON_PIN_MASK (1 << 5) // GP0P5 // 简单的软件去抖函数 int is_button_pressed_debounced() { static int stable_state = 1; // 假设初始为高(上拉) static int last_raw_state = 1; int raw_state; int new_state = stable_state; // 读取IN_DATA01寄存器中Bank0部分(低16位),并检查特定引脚 raw_state = (GPIO_IN_DATA01 & BUTTON_PIN_MASK) ? 1 : 0; // 简易去抖逻辑:只有连续两次读取状态一致,才认为状态可能改变 if (raw_state == last_raw_state && raw_state != stable_state) { // 状态稳定了一段时间,可以更新 new_state = raw_state; } last_raw_state = raw_state; if (new_state != stable_state) { stable_state = new_state; // 返回1表示按下(假设低电平有效),0表示释放 return (stable_state == 0); } return 0; // 状态未改变 }对于传感器读取(如数字温湿度传感器),流程类似,但更关注读取的时机(根据传感器通信协议)而非连续防抖。关键在于,你通过直接读取IN_DATAn,获得了对硬件状态最直接、延迟可控的访问能力,这是使用高级抽象API所无法比拟的。
3. GPIO中断状态寄存器(INTSTATn)与边沿触发控制
3.1 中断状态寄存器(INTSTATn)的核心作用
如果说IN_DATAn是GPIO的“眼睛”,用于观察状态,那么中断状态寄存器INTSTATn就是GPIO的“警报器”,用于通知事件。它是一个读/写-1清除(Read/Write 1 to Clear, R/W1C)寄存器。这个属性非常特殊且重要:
- 读取:可以获取所有GPIO引脚的中断 pending(挂起)状态。某一位为1,表示对应引脚上发生了使能的中断触发事件(如上升沿),且该中断尚未被处理(清除)。
- 写入:向某一位写入1,会清除该引脚的中断挂起标志位。写入0则没有任何效果。这种“写1清0”的机制是硬件中断状态管理的常见模式,可以确保软件能原子性地、精确地清除已处理的中断标志,避免在清除操作期间错过新的中断事件。
INTSTATn寄存器的位域布局与IN_DATAn类似,例如INTSTAT01同样管理Bank 0和Bank 1的32个引脚。当中断服务程序(ISR)被调用时,第一步通常就是读取INTSTATn(或结合中断线映射)来确定是哪个或哪些引脚触发了中断。
3.2 边沿触发寄存器的配对逻辑:SET与CLR
在配置GPIO中断时,我们不仅需要知道状态,更需要控制在何种条件下产生中断。这就是边沿触发寄存器的作用。技术手册中详细描述了四组寄存器:
SET_RIS_TRIGn/CLR_RIS_TRIGn:用于控制���升沿(低电平到高电平)触发中断的使能与禁用。SET_FAL_TRIGn/CLR_FAL_TRIGn:用于控制下降沿(高电平到低电平)触发中断的使能与禁用。
这里的设计非常巧妙:使能(SET)和禁用(CLR)操作使用两个独立的寄存器地址,但读取任何一个寄存器,得到的都是该中断触发功能的当前使能状态。
手册中明确写道:“Reading the GPkPj bit in either SET_RIS_TRIGn or CLR_RIS_TRIGn always returns an indication of whether the rising edge interrupt generation function is enabled for pin GPk[j].”
这意味着:
- 使能上升沿中断:向
SET_RIS_TRIGn寄存器的对应位写1。 - 禁用上升沿中断:向
CLR_RIS_TRIGn寄存器的对应位写1。 - 查询上升沿中断是否使能:读取
SET_RIS_TRIGn或CLR_RIS_TRIGn的对应位,结果一致。如果使能,两者都返回1;如果禁用,两者都返回0。
这种“SET/CLR”寄存器对的设计,是嵌入式系统中一种常见且优秀的硬件设计模式。它的核心优势在于原子性和安全性。软件不需要执行“读-修改-写”操作(该操作在多任务或中断环境下可能被打断,导致数据竞争)。你只需要向一个明确的地址写入一个明确的值(1),即可完成单一比特位的设置或清除,无需担心影响寄存器中的其他位。这极大地简化了驱动程序的编写,并提高了可靠性。
3.3 中断处理流程全景图
结合这些寄存器,一个完整的GPIO中断处理流程如下:
初始化配置:
- 配置引脚为输入模式(通过方向寄存器
DIRn)。 - 配置引脚复用功能为GPIO(通过引脚控制寄存器)。
- 配置边沿触发类型:根据需求,向
SET_RIS_TRIGn或SET_FAL_TRIGn写入1,使能特定边沿检测。可以同时使能上升沿和下降沿,用于捕获信号变化。 - 在中断控制器(如NVIC)中使能该GPIO Bank对应的系统中断。
- 配置引脚为输入模式(通过方向寄存器
中断发生:
- 引脚上出现使能的边沿事件。
- 硬件自动将
INTSTATn寄存器中对应位置1,表示有中断挂起。 - 如果系统中断已使能,CPU会跳转到对应的中断服务程序(ISR)。
中断服务程序(ISR)内:
- 识别中断源:读取
INTSTATn寄存器,确定是哪个引脚触发的中断。对于多个引脚共享一个中断线的情况,这一步是必需的。 - 执行处理逻辑:执行与该中断相关的业务代码(如置标志、发送消息、启动定时器等)。
- 清除中断标志:向
INTSTATn寄存器中刚才识别到的位置写入1,以清除挂起标志。这是至关重要的一步,如果忘记清除,CPU退出ISR后会立即再次进入,导致系统卡死。 - (可选)如果需要动态改变中断触发条件,可以操作
SET_RIS_TRIGn/CLR_RIS_TRIGn等寄存器。
- 识别中断源:读取
中断禁用/重新配置:
- 当不再需要某个引脚的中断时,应向对应的
CLR_RIS_TRIGn或CLR_FAL_TRIGn寄存器写1来禁用,而不是简单地屏蔽系统中断。这可以防止中断事件在硬件内部不断累积。
- 当不再需要某个引脚的中断时,应向对应的
4. 寄存器级编程实战与代码剖析
理解了原理,我们通过一个完整的例子,看看如何不依赖任何库,直接操作这些寄存器来实现一个带消抖的边沿触发按键中断。
4.1 硬件定义与宏
首先,我们需要根据芯片数据手册定义寄存器的内存映射地址。以下是一个示例:
/* 假设 GPIO Bank0 控制寄存器组基地址 */ #define GPIO0_BASE 0x4804C000 /* 寄存器偏移量定义 (基于常见TI AM335x系列,具体需查手册) */ #define GPIO_OE 0x134 // 输出使能寄存器 (Output Enable) #define GPIO_DATAIN 0x138 // 输入数据寄存器 (IN_DATA01) #define GPIO_SETDATAOUT 0x194 // 输出数据置位寄存器 #define GPIO_CLEARDATAOUT 0x190 // 输出数据清除寄存器 #define GPIO_IRQSTATUS_0 0x2C // 中断状态寄存器 (INTSTAT01) #define GPIO_IRQSTATUS_SET_0 0x34 // 中断状态置位寄存器(写1置位,用于软件触发测试) #define GPIO_IRQSTATUS_CLR_0 0x3C // 中断状态清除寄存器(写1清除) #define GPIO_RISINGDETECT 0x148 // 上升沿检测使能寄存器 (SET_RIS_TRIG) #define GPIO_FALLINGDETECT 0x14C // 下降沿检测使能寄存器 (SET_FAL_TRIG) /* 注意:实际SET/CLR对可能使用不同的偏移,这里简化表示原理 */ /* 便捷的寄存器访问宏 */ #define GPIO_REG(offset) (*(volatile unsigned int *)(GPIO0_BASE + (offset))) /* 引脚定义:Bank0, Pin5 作为按键输入(低电平有效) */ #define BUTTON_PIN (1 << 5)4.2 初始化函数:配置引脚与中断
void gpio_button_interrupt_init(void) { // 1. 确保GPIO模块时钟已使能(此处省略系统时钟配置代码) // 2. 配置引脚为输入模式:清除OE寄存器的对应位 GPIO_REG(GPIO_OE) &= ~BUTTON_PIN; // 3. 配置下降沿触发中断(因为按键按下通常为下降沿) // 向上升沿检测寄存器写0(不清除),向下降沿检测寄存器写1使能 // 假设GPIO_FALLINGDETECT是SET_FAL_TRIG类型的寄存器 GPIO_REG(GPIO_FALLINGDETECT) |= BUTTON_PIN; // 如果需要禁用上升沿触发,确保对应寄存器位为0 GPIO_REG(GPIO_RISINGDETECT) &= ~BUTTON_PIN; // 4. 清除可能存在的旧中断标志(写1清除) GPIO_REG(GPIO_IRQSTATUS_CLR_0) = BUTTON_PIN; // 5. 在系统中断控制器(如ARM Cortex-A的GIC或Cortex-M的NVIC)中 // 使能GPIO0的中断。这里以伪代码表示,具体取决于CPU架构。 // enable_irq(GPIO0_IRQ_NUM); }4.3 中断服务程序(ISR)实现
// GPIO0的中断服务程序 void GPIO0_IRQ_Handler(void) { uint32_t irq_status; // 1. 读取中断状态寄存器,确定是哪个引脚产生的中断 irq_status = GPIO_REG(GPIO_IRQSTATUS_0); // 2. 检查是否是我们关心的按键引脚触发 if (irq_status & BUTTON_PIN) { // 3. 执行中断处理逻辑 // 注意:此时按键可能仍在抖动,ISR中应尽快处理,将耗时操作放到主循环 // 例如,设置一个标志位,或者启动一个去抖定时器 button_pressed_flag = 1; // 4. 清除该引脚的中断挂起标志(写1清除) // 这是最关键的一步,防止中断重入! GPIO_REG(GPIO_IRQSTATUS_CLR_0) = BUTTON_PIN; // 5. (可选)如果需要,可以在这里临时禁用该引脚的中断,防止抖动期间多次触发 // GPIO_REG(GPIO_FALLINGDETECT) &= ~BUTTON_PIN; // 然后在主循环或定时器中,经过一段消抖时间后再重新使能。 } // 理论上,还应检查其他引脚的中断,但此例中我们只关心一个引脚 }4.4 主循环中的去抖与状态处理
由于机械按键抖动可能在毫秒级,在ISR中直接进行状态判定是不可靠的。更优的做法是结合软件定时器:
volatile int button_pressed_flag = 0; uint32_t button_last_press_time = 0; #define DEBOUNCE_DELAY_MS 20 void main_loop(void) { while(1) { // 其他任务... // 检查按键中断标志 if (button_pressed_flag) { uint32_t current_time = get_system_tick_ms(); // 简单的时间戳去抖:距离上次有效按下至少间隔去抖时间 if ((current_time - button_last_press_time) > DEBOUNCE_DELAY_MS) { // 再次确认引脚电平(可选,增加可靠性) if ((GPIO_REG(GPIO_DATAIN) & BUTTON_PIN) == 0) { // 低电平表示按下 // 执行真正的按键处理逻辑 handle_button_press(); button_last_press_time = current_time; } } // 清除中断标志,准备接收下一次中断 button_pressed_flag = 0; // 如果在ISR中禁用了中断,这里需要重新使能 // GPIO_REG(GPIO_FALLINGDETECT) |= BUTTON_PIN; } } }5. 高级应用场景与性能优化技巧
5.1 多引脚中断管理与状态查询
当一个GPIO Bank的多个引脚都使能了中断,并且共享同一个中断向量时,ISR必须高效地识别所有触发源。INTSTATn寄存器在这里发挥了核心作用。
void GPIO_Bank_IRQ_Handler(void) { uint32_t pending_irqs = GPIO_REG(GPIO_IRQSTATUS_0); uint32_t handled_irqs = 0; // 方法1:顺序检查每一位(适合引脚数少或触发不频繁) if (pending_irqs & PIN1_MASK) { handle_pin1_event(); handled_irqs |= PIN1_MASK; } if (pending_irqs & PIN2_MASK) { handle_pin2_event(); handled_irqs |= PIN2_MASK; } // ... 检查其他引脚 // 方法2:使用循环和位操作(适合引脚多) while (pending_irqs) { // 找到最低有效位为1的引脚编号 int pin_index = __builtin_ctz(pending_irqs); // 编译器内置函数,计算尾随零的个数 uint32_t pin_mask = 1U << pin_index; handle_pin_event(pin_index); handled_irqs |= pin_mask; pending_irqs &= ~pin_mask; // 清除已处理的位 } // 一次性清除所有已处理的中断标志 GPIO_REG(GPIO_IRQSTATUS_CLR_0) = handled_irqs; }性能提示:
__builtin_ctz是GCC/Clang等编译器的内置函数,用于高效计算整数二进制表示中末尾0的个数,非常适合用于从位图中快速找到第一个置位的位置。在IAR或Keil中,可能有类似的内部函数,如__CLZ(计算前导零)的变通使用。
5.2 模拟电平触发中断
标准的GPIO中断通常是边沿触发。但有时我们需要电平触发(如检测一个持续的低电平警报信号)。这可以通过结合边沿中断和IN_DATAn寄存器轮询来模拟:
- 配置为下降沿触发中断。
- ISR被触发后,在ISR或关联的任务中,循环读取
IN_DATAn,只要引脚仍为低电平,就持续执行处理逻辑或标记警报状态。 - 当读取到引脚变为高电平时,退出电平处理状态。
- 为了在电平持续期间不重复进入ISR,可以在第一次进入ISR后,暂时禁用该引脚的中断,或在ISR末尾不立即清除标志(需谨慎,可能导致中断被屏蔽)。更好的方法是,在ISR中启动一个周期性定时器,在定时器中断中读取
IN_DATAn检查电平,直到电平恢复。
5.3 低功耗场景下的GPIO中断配置
在电池供电的物联网设备中,GPIO中断是唤醒系统从睡眠模式的关键。此时,对寄存器的操作有特殊要求:
- 引脚配置保持:在进入低功耗模式前,GPIO的上下拉电阻、方向、中断触发边沿等配置必须保持。确保这些配置在唤醒后依然有效。
- 中断使能:
SET_RIS_TRIGn/SET_FAL_TRIGn必须已正确使能。 - 系统级中断使能:CPU的中断必须全局使能,并且该GPIO中断在中断控制器中必须使能。
- 状态标志:进入睡眠前,确保
INTSTATn中相关标志已被清除,否则可能无法进入深度睡眠,或立即被唤醒。 - 唤醒后处理:唤醒后的第一件事就是在ISR中读取
INTSTATn确定唤醒源,并立即清除标志。然后根据唤醒源执行相应操作,再决定是继续工作还是再次进入睡眠。
6. 常见问题排查与调试心得
6.1 中断无法触发或触发异常
这是最常遇到的问题,可以按照以下清单排查:
| 问题现象 | 可能原因 | 排查步骤与解决方法 |
|---|---|---|
| 中断根本不被触发 | 1. 引脚未配置为GPIO功能。 2. 引脚方向未配置为输入。 3. 边沿触发寄存器(SET_RIS/FAL_TRIG)未正确使能。 4. 系统中断控制器(NVIC/GIC)未使能该GPIO中断。 5. CPU全局中断未开启。 | 1. 检查引脚复用控制寄存器,确保选择GPIO模式。 2. 确认方向寄存器(OE/DIR)对应位为1(输入)。 3. 读取SET_RIS/FAL_TRIG寄存器,确认对应位为1。 4. 检查中断控制器配置,确认中断号、优先级正确并使能。 5. 在main函数初始化中启用全局中断(如ARM的 cpsie i)。 |
| 中断触发一次后不再触发 | 1.中断状态标志(INTSTATn)未清除。 2. 在ISR中错误地禁用了中断(如修改了IER寄存器)。 3. 外部信号问题(如按键松开后未恢复高电平)。 | 1.这是最常见原因!确保ISR中向INTSTATn的对应位写1以清除标志。 2. 检查ISR代码,确认没有误操作中断使能寄存器。 3. 用示波器或逻辑分析仪观察引脚实际波形。 |
| 中断频繁触发(抖动) | 1. 机械开关抖动。 2. 电路噪声或干扰。 3. 错误配置了双边沿触发。 | 1. 增加硬件RC滤波或软件去抖逻辑(如本节示例)。 2. 检查PCB布局、电源滤波,启用GPIO内部数字滤波器(如果支持)。 3. 确认只使能了需要的边沿(上升沿或下降沿)。 |
| 读取的中断状态与预期不符 | 1. 未正确屏蔽其他引脚位。 2. 中断标志清除操作有误。 3. 寄存器地址或位偏移计算错误。 | 1. 在ISR中,用irq_status & YOUR_PIN_MASK来精确判断。2. 确保清除操作是 REG = pin_mask(写1清除),而不是REG &= ~pin_mask。3. 仔细核对数据手册中的寄存器映射表和位定义。 |
6.2 输入数据读取不稳定
- 问题:即使外部信号稳定,连续读取
IN_DATAn寄存器时值也会跳动。 - 排查:
- 硬件层面:首先用示波器检查引脚实际波形,排除信号完整性问题(过冲、振铃)、电源噪声或接地不良。
- 配置层面:检查是否启用了GPIO内部的上拉/下拉电阻?不合适的上下拉可能导致引脚浮空。确认引脚配置为输入模式。
- 软件层面:如果读取操作非常快(如在单周期循环中),可能读到了同步器尚未稳定的值。虽然不常见,但可以在关键读取操作间增加一个小的空指令延迟(
__nop()),或连续读取两次以第二次为准。 - 滤波功能:查阅手册,看该GPIO模块是否支持可配置的输入数字滤波器。如果支持,尝试启用并适当调整滤波采样周期,可以有效滤除毛刺。
6.3 多任务/中断环境下的寄存器访问安全
在RTOS或复杂中断应用中,多个任务或中断可能竞争访问同一个GPIO Bank的寄存器。虽然对IN_DATAn(只读)和INTSTATn(通常按位清除)的访问冲突风险较低,但对配置寄存器(如方向寄存器、SET/CLR触发寄存器)的“读-修改-写”操作是非原子的。
解决方案:
- 使用硬件提供的SET/CLR寄存器对:这是最佳实践。例如,要设置某个引脚为输出,应使用“输出方向设置寄存器”(如果存在)直接写1,而不是先读取整个方向寄存器,修改一位后再写回。
- 如果必须进行“读-修改-写”:在访问前后使用关中断或互斥锁(如RTOS中的信号量)来保护这段临界区代码。
- 对于输出数据寄存器:如果存在独立的
SET_DATA和CLR_DATA寄存器,就用它们来单独控制引脚高低电平,避免直接读写整个数据��存器。
7. 从寄存器到驱动:理解软件抽象层
我们深入研究了寄存器,但在实际项目中,直接操作寄存器并非总是最佳选择。芯片厂商通常会提供硬件抽象层(HAL)库或驱动程序库(如TI的DriverLib,STM32的HAL/LL库)。这些库的本质,就是对寄存器操作的封装。
例如,一个GPIO_setInterruptType()函数内部,很可能就是对我们上面分析的SET_RIS_TRIGn和SET_FAL_TRIGn寄存器进行写入操作。理解寄存器后,你再去看这些库函数的源码,就会豁然开朗,甚至能在库函数出现bug或无法满足特定性能需求时,自己动手进行底层修改或绕过。
更重要的是,当你在没有现成库的新平台或小众芯片上开发时,直接操作寄存器的能力就变得无可替代。你能够根据数据手册,从零开始构建最精简、最高效的驱动程序,完全掌控系统的时序和资源。
我个人在多年的嵌入式开发中有一个深刻体会:寄存器是硬件的语言,数据手册是语法书。花时间读懂它,你就能与硬件直接对话。当你调试一个棘手的硬件问题时,逻辑分析仪抓到的波形和寄存器窗口里的值,是你定位问题的终极坐标。这份从底层理解系统运作的能力,是区分普通程序员和资深嵌入式工程师的重要标志。下次当你配置一个GPIO中断时,不妨想一想背后的SET_RIS_TRIG和INTSTAT寄存器,它们正在硬件层面为你默默工作,而你现在已经知道了它们的全部秘密。