十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32G071外部中断EPR request解析:挂起标志与清除陷阱

STM32G071外部中断EPR request解析:挂起标志与清除陷阱 先说个结论你在 STM32G071 上搜“EPR request”RTFM 手册里其实找不到一个叫 EPR 的官方寄存器。我翻遍了 RM0444 里 EXTI 外设的寄存器列表IMR、EMR、RTSR、FTSR、SWIER、RPR、FPR 都在唯独没有 EPR。结合这些年调试 G0 系列的经验这个说法大概率是外部中断/事件的挂起请求Pending Request也就是 EXTI 中断标志那一套东西。很多从 F1 系列转过来的人会在 G071 上被“挂起标志为什么清不掉”“中断为什么反复进”这类问题卡住本质都是对 G0 这条 EXTI 请求链路的理解偏差。这篇文章就把这条链路从寄存器到底层原理拆开讲透顺便把我踩过的坑一并交代清楚。1. EPR这个缩写在G071上到底指什么1.1 先从寄存器表切入看EXTI外设到底有哪些“请求”在 STM32G071 这类 G0 系列芯片里外部中断/事件控制器EXTI负责把外部引脚边沿、内部外设事件转成中断请求或事件请求。RM0444 里 EXTI 相关寄存器大致有这些EXTI_EXTICR1~4外部引脚映射决定 EXTI0~15 具体连到 PA/PB/PC 哪个端口。EXTI_IMR1/IMR2中断屏蔽寄存器对应位写 1 表示允许该线产生中断。EXTI_EMR1/EMR2事件屏蔽寄存器对应位写 1 表示允许该线产生事件。EXTI_RTSR1/RTSR2上升沿触发选择寄存器。EXTI_FTSR1/FTSR2下降沿触发选择寄存器。EXTI_SWIER1/SWIER2软件中断事件寄存器用来软件触发请求。EXTI_RPR1/RPR2上升沿挂起寄存器Rising edge Pending。EXTI_FPR1/FPR2下降沿挂起寄存器Falling edge Pending。EXTI_CFGR全局配置比如 VDDIO 连接、事件输出极性等。我在项目里遇到有人问“EPR request”时基本都是卡在 RPR/FPR 这两个挂起寄存器的读写上。你可以把 RPR 和 FPR 理解成 EXT 请求的“账本”当某个配置过的边沿到达时硬件就在对应位置记一笔账置 1CPU 处理完需要自己把这笔账销掉写 1 清零。如果不清账硬件会认为这个请求一直没被处理于是反复往 NVIC 扔中断请求。1.2 “EPR request”大概率是指挂起请求这条链路既然手册里没有 EPR我倾向于认为这是早期一些博客、论坛帖子里的误写把 EXTI 的“Pending Request”简写成了 EPR。很多经验贴讲 F1 系列时会写“EXTI_PR 挂起寄存器”到了 G0 系列PR 被拆成 RPR 和 FPR写帖子的人再一缩写就成了“EPR request”。所以你在搜索引擎里看到这个词十有八九是在问外部中断挂起标志的查询和清除问题。这类问题最常见的两种场景配置了外部中断但程序进不了中断服务函数于是想确认“请求到底有没有置位”。中断能进但进去之后出不来或者明明只按了一下按键中断却连续触发好几次怀疑是挂起标志没清。这两个场景我在后面的排查章节会详细展开先记住关键结论G071 上清挂起标志是对 RPR/FPR 写 1不是写 0也不是读一下自动清除。1.3 中断请求和事件请求是两个东西EXTI 支持两种请求模式用 IMR 和 EMR 分别控制很多人会把它们混为一谈。中断模式IMR 置位边沿到来后硬件置位 RPR/FPR同时向 NVIC 发出中断请求CPU 跳进对应的 ISR。ISR 里需要手动清挂起标志否则中断会一直触发。事件模式EMR 置位边沿到来后硬件同样会置位 RPR/FPR但不会向 NVIC 发请求而是产生一个事件脉冲可用来唤醒 CPU配合 WFE或触发其他外设比如 TIM、ADC 的硬件触发。事件模式下你也可以不理会 RPR/FPR但如果后续要复用同一根线做中断唤醒就需要清掉旧标志。用生活类比中断模式像电话铃响你接了电话还得在电话里确认“收到”事件模式像门铃闪灯你看到荧光灯亮了就知道有人来过但是否要“确认”取决于你要不要记录这次到访。这个区别在低功耗唤醒场景里特别重要。2. 挂起请求RPR/FPR的寄存器级行为与F1的PR差在哪2.1 为什么G0要把挂起寄存器拆成上升沿和下降沿两份老一代 STM32 系列比如 F1只有一个 EXTI_PR 挂起寄存器不管上升沿还是下降沿触发只要发生边沿对应位就置 1。处理时也是写 1 清零区分不了具体是哪种边沿引起的请求。G0 系列把它们拆成了 RPR1/RPR2 和 FPR1/FPR2。上升沿触发的请求记在 RPR下降沿触发的请求记在 FPR。这样做的直接好处是软件能精确知道这次请求是哪类边沿尤其适合双边沿触发的场景你可以在 ISR 里通过读 RPR 和 FPR 来判断引脚是从低到高还是从高到低跳变不用再去读引脚电平做二次判断。但这也有个代价清除挂起标志时你得清楚自己清的是哪份账。如果配置了双边沿触发一次动作可能同时或先后置位 RPR 和 FPR只清其中一个另一个会继续触发中断。我第一次在 G071 上做旋转编码器时就吃过这个亏后来养成习惯只要用了双边沿就强制两个寄存器都清一遍。2.2 挂起标志的置位、读取和清除规则挂起有关的核心规则我用几句话总结触发条件满足时RTSR/FTSR 配置的边沿到来且 IMR 或 EMR 已使能对应线RPR 或 FPR 对应位置 1。读取直接读 RPR1/FPR1或 RPR2/FPR2寄存器对应位是 1 就说明有挂起请求。清除对置位的那一位写 1硬件清零。写 0 没有任何效果。如果该线没有被使能IMR 和 EMR 都为 0即使外部引脚有边沿RPR/FPR 也不会置位。注意是“不会置位”不是“置位但不响应”。代码上寄存器操作非常简单// 查询 EXTI0 是否上升沿挂起 if (EXTI-RPR1 (1UL 0)) { // 确实有上升沿请求 } // 清除 EXTI0 的上升沿挂起标志 EXTI-RPR1 (1UL 0); // 清除 EXTI0 的下降沿挂起标志 EXTI-FPR1 (1UL 0); // 双边沿场景下建议两个都清 EXTI-RPR1 (1UL 0); EXTI-FPR1 (1UL 0);这里有个新手容易犯的错用EXTI-RPR1 ~(1UL 0)去清标志结果发现怎么都清不掉。原因就是清除动作要求“写 1”位带操作里那是读改写写回去的是 0硬件当然无动于衷。2.3 不清标志会怎样一个死循环式的经典故障先描述一个很典型的故障现场按键接在 PA0配置成上升沿触发的外部中断。按下按键后程序能进中断在 ISR 里翻转一个 LED但松开按键时发现 LED 状态又翻转回去了或者打断点看中断服务函数被连续调用了两次。原因是这样的机械按键在按下和松开的瞬间会产生抖动引脚在几毫秒内有好几次边沿变化。你配置的是上升沿触发如果抖动期间恰好有多个上升沿EXTI 会多次置位 RPR。第一次进入 ISR 时你只是翻转了 LED 就退出没有清 RPR于是刚出中断同一个挂起位还在 1NVIC 判定中断没有被处理完立刻再次进入 ISR。这跟按键去抖还不完全是一回事——即使按键稳定只要你不清标志这个“无穷重入”就会发生。解决办法就一句话进入 ISR 后第一时间清除对应的 RPR/FPR再做业务处理。HAL 库里HAL_GPIO_EXTI_IRQHandler会先清标志再回调就是为了避免这个问题。自己手写寄存器时务必保持同样的顺序。2.4 寄存器操作与HAL库操作对照G0 的 HAL 驱动把清标志逻辑封装好了。使用中断模式时你在stm32g0xx_it.c里写void EXTI0_1_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0); }然后在用户代码里实现回调void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin GPIO_PIN_0) { // 按键事件处理 } }HAL 内部HAL_GPIO_EXTI_IRQHandler的实现大概是这样void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin) { if (EXTI_GetPendingBit(GPIO_Pin) ! RESET) { EXTI_ClearPendingBit(GPIO_Pin); HAL_GPIO_EXTI_Callback(GPIO_Pin); } }也就是说HAL 是在回调之前清标志的所以你在回调里一般不需要再清一次。但如果你在回调里直接操作寄存器比如手动读取 RPR/FPR 做边沿判断读完之后也要注意不要让旧标志残留到下个周期。纯寄存器方案则简单直接效率也更高void EXTI0_1_IRQHandler(void) { uint32_t pending EXTI-RPR1 | EXTI-FPR1; if (pending (1UL 0)) { EXTI-RPR1 (1UL 0); EXTI-FPR1 (1UL 0); // 业务处理 } }3. 在G071上完整配置一个EXTI请求从GPIO到中断服务这一节把从零到一的过程完整跑一遍。为什么要按这个顺序因为 EXTI 请求链路是“GPIO 输入 → SYSCFG 引脚映射 → EXTI 边沿检测 → 挂起标志 → NVIC 中断”。任何一环断了请求都到不了 CPU。3.1 第1步使能外设时钟配置GPIO模式先使能 GPIO 端口时钟再使能 SYSCFG 时钟。G071 的 SYSCFG 时钟使能位在 RCC_APBENR2 里__HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SYSCFG_CLK_ENABLE();GPIO 要配置成输入模式。以 PA0 为例用 HAL 的写法是GPIO_InitTypeDef gpio {0}; gpio.Pin GPIO_PIN_0; gpio.Mode GPIO_MODE_IT_RISING; // 上升沿中断 gpio.Pull GPIO_PULLDOWN; // 外部按键接地默认拉低按下接高 HAL_GPIO_Init(GPIOA, gpio);很多刚接触的人会在这里直接把 GPIO 配成输出然后又开外部中断结果发现中断根本进不去。原因是输出模式下引脚是芯片自己驱动的外部电平变化不一定能通过输入通路送到 EXTI 检测器。记住EXTI 检测的是引脚输入状态所以必须配置为输入模式。3.2 第2步选择EXTI线与引脚映射SYSCFG_EXTICREXTI0~15 并不是固定对应 PA0~PA15 的。G0 系列允许通过 SYSCFG-EXTICR 寄存器把同一号线映射到不同端口的同编号引脚。比如 EXTI0 可以选 PA0也可以选 PB0、PC0具体看封装上有哪些引脚。手动配置时需要设置 SYSCFG-EXTICR1 的低 4 位为 0表示 EXTI0 连接到 PA0SYSCFG-EXTICR[0] ~SYSCFG_EXTICR1_EXTI0;用 HAL 的时候HAL_GPIO_Init内部会根据 GPIO 端口和引脚号自动写 SYSCFG-EXTICR所以大部分项目不用手动碰这个寄存器。但调试时如果发现中断完全不触发一定要回去确认 EXTI 线到底映射到了哪个端口。默认复位值是 0PA 或备用功能如果你用的是 PB 引脚这里漏配就是“看起来初始化了实际上根本没接到那根线上”。3.3 第3步配置触发极性和使能通道触发极性由 RTSR 和 FTSR 控制。这两个寄存器每一位对应一条 EXTI 线规则是RTSR 位FTSR 位触发方式00该线不触发10上升沿触发01下降沿触发11双边沿触发寄存器操作// 上升沿触发 EXTI-RTSR1 | (1UL 0); EXTI-FTSR1 ~(1UL 0); // 下降沿触发 EXTI-RTSR1 ~(1UL 0); EXTI-FTSR1 | (1UL 0); // 双边沿触发 EXTI-RTSR1 | (1UL 0); EXTI-FTSR1 | (1UL 0);然后使能该线的中断通道IMR或事件通道EMR// 中断模式 EXTI-IMR1 | (1UL 0); // 事件模式 EXTI-EMR1 | (1UL 0);HAL 的GPIO_MODE_IT_RISING等宏会同时完成极性和 IMR 设置但理解底层对应关系对排查问题还是有帮助的。3.4 第4步NVIC使能与中断服务函数写法G0 系列是 Cortex-M0 内核NVIC 中断通道资源比 F1 少所以 EXTI 线被分成了三组NVIC 中断通道包含的 EXTI 线EXTI0_1_IRQnEXTI0、EXTI1EXTI2_3_IRQnEXTI2、EXTI3EXTI4_15_IRQnEXTI4~EXTI15这个分法很容易被老代码坑到。F1 上 EXTI0 和 EXTI1 各自有独立中断号G0 上却共用EXTI0_1_IRQn。所以 ISR 里必须判断到底是哪条线来的请求void EXTI0_1_IRQHandler(void) { if (EXTI-RPR1 (1UL 0)) { EXTI-RPR1 (1UL 0); // 处理 EXTI0 } if (EXTI-RPR1 (1UL 1)) { EXTI-RPR1 (1UL 1); // 处理 EXTI1 } }NVIC 使能用 HALHAL_NVIC_SetPriority(EXTI0_1_IRQn, 2, 0); HAL_NVIC_EnableIRQ(EXTI0_1_IRQn);注意Cortex-M0 没有中断优先级分组只有 0~3 四个优先级这里优先级数值越小越高。3.5 一个建议用事件模式模拟按钮扫描如果你只是想让 CPU 在按键按下时醒过来但之后还是想做一遍软件扫描和去抖建议用事件模式而不是中断模式。事件模式不会进 ISR配合 WFE 指令可以让 CPU 进入睡眠按键边沿来临时自动唤醒醒过来之后再扫描按键状态。这样能省掉中断上下文切换代码也更简单。配置方式就是把上面的 IMR 改成 EMREXTI-RTSR1 | (1UL 0); EXTI-EMR1 | (1UL 0);然后在主循环里while (1) { __WFI(); // 等待事件 // 被事件唤醒后扫描按键做消抖处理 }唤醒后你会发现 RPR1 对应位置 1 了。你不主动清它也不影响下次事件但如果你之后要切换成中断模式最好先把它清掉避免一使能中断就立刻误触发。4. 调试“EPR请求不生效/反复触发”的完整排查链路这一节回到文章开头说的那两种常见故障完整走一遍排查过程。我调试时习惯按现象分三类完全不触发、反复触发、清了还是触发。4.1 现象A中断完全不触发先别急着怀疑芯片坏了。按以下顺序查第一步确认 GPIO 确实是输入模式并且外部电平能到引脚。用调试器把 GPIOA-IDR 读出来手动短路引脚到高/低电平看 IDR 对应位有没有变化。如果电平都不变大概率是硬件连接或引脚复用问题。第二步确认 SYSCFG-EXTICR 里 EXTI 线映射到了正确的端口。用 CubeMX 初始化一般没问题手动初始化时就容易漏。检查方法uint32_t map (SYSCFG-EXTICR[0] 0) 0xF; // 0 表示 PA1 表示 PB2 表示 PC第三步确认 RTSR/FTSR 配置正确并且 IMR1/IMR2 对应位是 1。如果用的是事件模式要检查是不是 IMR 写了 0、EMR 写了 1导致根本没有中断请求产生。第四步确认 NVIC 已使能。G0 上外部中断被合并成三个通道别在EXTI4_15_IRQn里等 EXTI2 的中断。第五步用软件触发验证整条链路。先把 SWIER1 对应位置 1人为产生一个挂起请求看能不能进 ISREXTI-SWIER1 | (1UL 0);如果软件触发能进中断说明从挂起到 NVIC 的链路没问题问题就出在外部边沿没有到达 EXTI 检测器如果软件触发也不能进中断就得回头查 NVIC 和 IMR 配置。4.2 现象B中断触发一次后卡死或反复进入先看代码里有没有清 RPR/FPR。没有清的话加上EXTI-RPR1 (1UL 0); EXTI-FPR1 (1UL 0);加上之后如果还是反复触发考虑是不是边沿配置带来的问题。比如按键电路是低电平有效按键按下时引脚从高变低配置成下降沿触发没问题但抖动期间如果有上升沿返回你又配置了双边沿就会触发两次。这种情况先改成单边沿再用软件消抖。还有一种情况是中断优先级和主循环里的延时逻辑冲突。比如 ISR 里调用了HAL_Delay()但 SysTick 中断优先级低于当前 EXTI 中断导致HAL_Delay()时间到不了函数卡死在等待里。表面上看起来是中断反复触发实际上是优先级嵌套导致的软死锁。解决方法是不要在 ISR 里用阻塞延时或者把 SysTick 优先级调得比外部中断高。4.3 现象C清除标志位后仍立刻重新置位这个现象最迷惑人。你明明在 ISR 里清掉了 RPR退出中断后马上又看到 RPR 是 1像是“清不掉”。先说最容易踩的坑清标志时用了 ~等于写 0根本没清掉。我在 2.2 节已经强调过这里再重复一次。第二个原因是外部引脚电平本身在抖动。比如按键没有在内部或外部加上拉/下拉引脚悬空电平在阈值附近来回跳变。边沿每跳一次RPR 就会重新置 1。你清掉一次它立刻又置位一次。处理方式要么在硬件上加电容要么在软件里做去抖要么用事件模式配合定时器采样。第三个原因是共享中断线。G0 的 EXTI0~15 分组共享 NVIC 通道比如 EXTI0 和 EXTI1 共用EXTI0_1_IRQn。你在 ISR 里只清了 EXTI0 的挂起但 EXTI1 也有挂起于是退出 ISR 后 NVIC 又因为 EXTI1 的挂起重新进入中断。排查方法是把 ISR 里所有组的挂起状态都读一遍uint32_t rpr_all EXTI-RPR1; uint32_t fpr_all EXTI-FPR1; // 逐个处理并清除 EXTI-RPR1 rpr_all; EXTI-FPR1 fpr_all;注意这里用“先读全部再写全部”的方式避免在处理过程中有新的请求进来被遗漏也方便你调试时看看到底是哪个引脚在闹事。4.4 调试技巧用寄存器窗口看RPR/FPR和EXTICR遇到这类问题我建议直接在调试器里打开 EXTI 寄存器组盯着 RPR1、FPR1、IMR1、EMR1、RTSR1、FTSR1 和 SYSCFG-EXTICR。用手触碰引脚观察 RPR1/FPR1 有没有变化。这个办法能快速区分问题在哪一环RPR 变化但中断不触发查 IMR 和 NVIC。RPR 不变查 EXTICR 映射、GPIO 复用、外部硬件电平。RPR 变化且中断触发但清不掉检查清除方式是不是写 1以及是否有多个线共享中断。还有一个技巧在 ISR 入口和出口各读一次 RPR1 | FPR1输出到串口。这样能看到“进入时挂起有哪些、退出时挂起有哪些”本质就是给中断系统做一个行为日志。比光靠眼睛盯调试器更高效。4.5 这个坑背后的“为什么”硬件边沿锁存与电平检测差异为什么 G0 的挂起标志必须写 1 清除为什么边沿请求可以作为“请求”被锁存EXTI 的挂起标志本质上是一个硬件锁存器。外部引脚的电平变化经过边沿检测器后如果匹配 RTSR/FTSR 配置就把对应的锁存位置 1。这个锁存一旦置位即使外部引脚电平已经恢复原状锁存器也保持 1直到软件写 1 清除。这种设计的好处是ISR 不会因为引脚电平恢复而丢失请求——即使你处理时外部信号已经消失挂起标志还在代表“这个边沿发生过”。坏处就是软件必须承担“确认收到”的责任不然请求会一直挂起。写 1 清除这种反直觉操作其实在很多外设里都能看到事件标志位、DMA 传输完成标志、UART 接收超时标志等。这是硬件设计的通用约定写 0 可能造成读到旧值、写 1 清零更利于并发安全。适配过几个外设之后你会慢慢习惯这种风格。5. 进阶让EXTI挂起请求在低功耗场景下可靠工作5.1 STOP模式下的唤醒链路G071 进入 Stop 模式后EXTI 仍然在工作这是低功耗唤醒的主要途径之一。你可以在 Stop 模式下把按键、通信接口的唤醒线接到 EXTI边沿到来时把芯片唤醒。关键坑点在于唤醒后系统时钟会回到复位后的默认状态通常是 HSI16而不是你之前配置的 PLL 主频。所以唤醒后第一件事是重新配置时钟然后再处理业务。如果你只配置了 EXTI 中断忘了恢复系统时钟外设跑起来时序全乱表现就是“偶尔正常偶尔不正常”。另外一个与挂起相关的坑进入 Stop 模式之前如果 RPR/FPR 上残留有旧的挂起请求芯片可能一进 Stop 就被立刻唤醒根本停不下来。所以在执行HAL_PWR_EnterSTOPMode之前最好把将要使用的唤醒线对应的 RPR/FPR 清一遍EXTI-RPR1 (1UL 0); EXTI-FPR1 (1UL 0); HAL_PWR_EnterSTOPMode(HAL_POWERREGULATOR_LOWPOWER, HAL_PWR_STOPENTRY_WFI);5.2 唤醒后第一件事清挂起而不是先处理数据如果你是用事件模式唤醒CPU 从__WFI()后面继续执行此时 RPR/FPR 可能还挂着。如果你在唤醒后想转成中断模式来处理后续请求一定要先清掉这个旧挂起否则中断一使能就立刻进 ISR看起来就像“刚唤醒就死循环”。如果你是用中断模式唤醒ISR 里先清标志再读取外设数据。这个顺序很关键如果先读数据再清标志读取过程中新来的边沿又会置位清标志时把新请求也一并清了容易丢事件。更好的做法是先把需要的硬件状态记录下来再清标志再处理业务void EXTI0_1_IRQHandler(void) { uint32_t val GPIOA-IDR (1UL 0); // 先记录当前引脚电平 EXTI-RPR1 (1UL 0); // 清挂起 EXTI-FPR1 (1UL 0); // 再根据 val 做业务判断 }5.3 多个引脚共享一条EXTI线G0给出的答案G0 系列里同一个 EXTI 线号在同一时间只能映射到一个端口引脚。换句话说PA0 和 PB0 不能同时用作 EXTI0它们只能二选一。如果两个引脚都要支持外部中断就必须用不同的线号比如 PA0 用 EXTI0PB1 用 EXTI1或者 PB2 用 EXTI2。这个限制在电路设计阶段就要想清楚。我见过一块板子上把两个按键分别接到了 PA0 和 PB0软件里想当然配成两个 EXTI0结果只有一个能触发。反复查了半天才想起去翻 EXTICR 映射。如果你必须使用不同端口的同号引脚唯一的办法是分时复用先映射 A 端口处理完再映射 B 端口但实时性会受影响一般不建议这么干。设计阶段就规划好哪个引脚对应哪条 EXTI 线能省掉后面很多麻烦。一般建议优先使用同一端口的连续引脚比如 PA0、PA1、PA2这样中断分组也可能更集中处理逻辑更清晰。5.4 实战配置速查表最后给一张我在 G071 项目里常用的配置速查表适合直接抄配置项推荐值说明GPIO 模式输入模式上拉或下拉禁止配置成输出复用SYSCFG 时钟RCC_APBENR2 里使能 SYSCFGEN手动配置 EXTICR 时必开触发方式按键按下沿避免双边沿增加无谓中断挂起清除RPR/FPR 对应位写 1不是写 0也不是读清除ISR 顺序先清挂起再处理业务防重入、防丢事件NVIC 分组EXTI0_1 / EXTI2_3 / EXTI4_15M0 没有优先级分组低功耗前清残留挂起防止一睡就被唤醒唤醒后先恢复时钟再处理外设数据这些配置我通常会在工程模板里固定下来。每次新项目用到 EXTI 时直接拿模板改引脚号和回调逻辑省得反复踩相同的坑。我个人在实际操作中的一点体会是G071 的 EXTI 整体设计比老 F1 清晰但寄存器拆分带来的变化很容易被惯性思维掩盖。遇到任何莫名其妙的“中断反复进”问题先别急着怀疑代码逻辑打开调试器把 RPR/FPR 和 EXTICR 读一遍基本能定位八成问题。挂起请求这条链路说白了就是“硬件置位、软件确认、写 1 清除”的三步循环把这三个动作想通EPR request 就不再是玄学了。
返回列表