嵌入式面试真题第 08 题:多中断源竞争下关键实时事件丢失的系统化诊断与中断架构设计

在这里插入图片描述

问题

在一个由 MCU、SoC 或实时处理器构成的嵌入式系统中,同时存在多类中断源:外部 GPIO 事件、定时器捕获、ADC/I2S/PWM 同步、DMA 半满/全满、CAN/Ethernet/USB、UART/SPI 高速数据、传感器告警、存储完成、看门狗和软件中断等。

系统在正常负载下运行稳定,但在通信突发、日志增加、Flash 擦写、协议栈高负载、任务临界区增多或多个外设同时活跃时,某个关键实时事件偶尔出现以下异常之一:

  • 外部边沿已经产生,但软件没有记录到;
  • 中断计数小于硬件事件计数;
  • 中断没有真正丢失,但响应延迟超过业务截止时间;
  • 多个事件被合并成一次 pending,导致事件次数丢失;
  • ISR 已执行,但后续队列、缓冲区或任务处理路径丢了事件;
  • 高频通信中断占满 CPU,关键同步、采样、控制或保护事件偶发失效;
  • RTOS 运行后才出现问题,裸机或低负载时无法复现。

你会如何建立一套通用的系统级排查方法,区分“硬件事件未被可靠捕获”“中断控制器未保留全部事件”“中断被屏蔽或长时间延迟”“ISR 自身处理错误”“ISR 到任务的数据路径丢失”这几类问题?又应如何重新设计中断优先级矩阵、ISR、DMA、事件队列、临界区和可观测性,使关键实时事件在峰值负载下仍然可证明地满足延迟、吞吐和不丢失要求?

回答

结论:不要把“漏中断”简单归因于优先级低,也不要只通过把某个 IRQ 调到最高优先级来掩盖问题。应先把完整链路拆成“物理事件产生、外设捕获、控制器 pending、CPU 进入 ISR、ISR 确认与清除、事件入队、任务消费”七个阶段,在每个阶段建立可计数、可时间戳、可对账的证据。只有确认事件究竟在哪一层消失,才能决定是改硬件捕获、触发方式、优先级、ISR、DMA、缓冲区还是任务调度。

通用的重构原则是:

  1. 关键事件尽可能由定时器输入捕获、硬件锁存、DMA 或外设 FIFO 保存,而不是依赖 CPU 恰好在边沿到来时及时响应。
  2. 最高关键级 ISR 只做读取硬件时间戳、保存必要状态、清除确定的标志和提交无阻塞事件,不做协议解析、日志、内存分配、长循环或可能阻塞的 RTOS 调用。
  3. 高频数据面使用 DMA、FIFO、批处理和环形缓冲,把“每字节中断”改成“每批数据中断”。
  4. 优先级按截止时间、最坏执行时间、事件保持能力和损失后果设计,而不是按外设名称或开发者主观重要程度排列。
  5. 对关中断区间、中断嵌套深度、ISR 执行时间、pending 时长、队列水位、FIFO overrun 和事件序号建立持续统计;没有这些数据,就无法证明系统没有漏中断。
  6. RTOS 下必须明确区分“可以调用内核 API 的 ISR”和“高于内核屏蔽阈值、不能调用内核 API 的超低延迟 ISR”。
  7. 系统验收应以峰值负载下的最坏延迟、最大抖动、零事件丢失、缓冲区不溢出和可恢复性为标准,而不是以平均 CPU 占用或短时间不复现为标准。

这类问题的本质不是单个 GPIO、串口或音频模块的问题,而是一个通用的实时事件传输与资源仲裁问题。音频同步、运动控制、功率保护、工业采样、通信时钟、传感器触发、网络时间戳和安全告警都可以套用同一套方法。

总体架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
flowchart LR
A[物理事件源\nGPIO/采样/同步/告警/通信] --> B[硬件捕获层\n锁存/输入捕获/FIFO/DMA]
B --> C[中断控制器\nPending/Mask/Priority/Route]
C --> D[极短 ISR\n读状态/取时间戳/清标志]
D --> E[无阻塞事件通道\nRing/Queue/Bitmap/Counter]
E --> F[实时任务或工作线程\n解析/计算/协议处理]
F --> G[业务输出\n控制/播放/传输/记录]

H[Trace & Health Monitor] -.计数/时间戳/水位.-> B
H -.Pending/Active/Mask.-> C
H -.入口/出口/执行时间.-> D
H -.丢弃/溢出/积压.-> E
H -.截止时间/处理延迟.-> F

I[高吞吐数据源] --> J[DMA + FIFO + 批处理]
J --> E

该架构把“事件可靠保存”和“事件复杂处理”分开。硬件捕获层负责把短脉冲、连续边沿或高速数据转换成可保持的寄存器、计数器、FIFO 或 DMA 缓冲;ISR 只负责把硬件状态安全地提交到软件事件通道;真正耗时的解析、算法、日志和协议处理放到任务上下文。

设计目标不是让 CPU 更快地响应所有中断,而是让关键事件即使在 CPU 暂时繁忙时也不会消失,并且让系统能够准确知道事件发生时间、响应时间、处理完成时间和是否出现积压。

可对应的开源实现与实现原理

结论:这套“硬件捕获、极短 ISR、无阻塞事件通道、任务化处理、批量搬运、延迟追踪”的架构并非纯理论设计。CMSIS-Core、FreeRTOS Kernel、Zephyr、RT-Thread 和 Linux 内核中都存在可以直接使用或可作为源码级参考的实现。

但需要先明确:这些开源实现处于不同层级。CMSIS-Core 解决 Cortex-M 核心寄存器访问;FreeRTOS、Zephyr 和 RT-Thread 解决 MCU/RTOS 的中断与任务协作;Linux Generic IRQ、threaded IRQ、NAPI、kfifo 和 ftrace 解决通用操作系统中的中断抽象、批处理和可观测性。不能把 Linux API 原样移植到 MCU,也不能把某个 RTOS 的临界区实现机械复制到另一种内核。真正值得复用的是其分层、所有权、状态提交、批处理和最坏延迟控制原理。

开源实现对应关系总表

本文机制 对应开源实现 主要源码或接口 能否直接使用 关键参考价值
NVIC 使能、Pending、Active、优先级读写 Arm CMSIS-Core NVIC NVIC_EnableIRQ()NVIC_GetPendingIRQ()NVIC_GetActive()NVIC_SetPriority() Cortex-M 项目可直接使用 统一寄存器映射、优先级位移、状态读回和启动自检
周期计数与 ISR 延迟测量 CMSIS-Core DWT DWT->CYCCNTDWT->CTRL 支持 DWT 的 Cortex-M 可直接使用 低开销记录 ISR 入口、出口、关中断区间和 WCET
仅屏蔽部分优先级的 RTOS 临界区 FreeRTOS Cortex-M Port BASEPRIconfigMAX_SYSCALL_INTERRUPT_PRIORITYportSET_INTERRUPT_MASK_FROM_ISR() 对应 FreeRTOS Cortex-M 端口可直接使用 内核临界区不必屏蔽最高紧急级 IRQ
ISR 唤醒高优先级任务 FreeRTOS FromISR API xTaskNotifyFromISR()xQueueSendFromISR()portYIELD_FROM_ISR() FreeRTOS 可直接使用 ISR 只提交事件,退出时通过 PendSV 完成调度切换
ISR 到任务的单生产者/单消费者字节流 FreeRTOS Stream Buffer xStreamBufferSendFromISR()xStreamBufferReceive() FreeRTOS 可直接使用 SPSC 环形缓冲、触发水位、等待任务唤醒
不受普通 IRQ lock 阻塞的极低延迟中断 Zephyr Zero-Latency Interrupts IRQ_DIRECT_CONNECT()ISR_DIRECT_DECLARE()IRQ_ZERO_LATENCY 支持该特性的 Zephyr/Cortex-M 可直接使用 将极关键 ISR 与内核普通临界区隔离,但禁止依赖内核 API
DMA 直接写入环形缓冲 Zephyr Ring Buffer ring_buf_put_claim()ring_buf_put_finish()ring_buf_get_claim()ring_buf_get_finish() Zephyr 可直接使用,其他系统可参考 claim/finish 两阶段提交、少拷贝、SPSC 所有权分离
中断嵌套统计和 ISR 进入/退出通知 RT-Thread Interrupt Management rt_interrupt_enter()rt_interrupt_leave()、进入/退出 hook RT-Thread BSP 可直接使用 内核感知中断嵌套,可挂接轻量级时延统计
固定内存环形缓冲 RT-Thread Ring Buffer rt_ringbuffer_put()rt_ringbuffer_get()rt_ringbuffer_get_direct() RT-Thread 可直接使用 读写索引加 mirror 位区分满/空,支持连续区间直接访问
IRQ 流程与芯片操作解耦 Linux Generic IRQ irq_desc、IRQ flow handler、irq_chiprequest_irq() Linux 驱动可直接使用,MCU 主要参考 把边沿、电平、EOI 流程与控制器寄存器操作解耦
顶半部与可调度处理分离 Linux Threaded IRQ request_threaded_irq()IRQ_WAKE_THREAD Linux 可直接使用 硬 IRQ 快速确认硬件,复杂处理在线程上下文执行
高频事件的中断抑制和预算轮询 Linux NAPI IRQ 触发 NAPI、poll budget、complete 后恢复中断 网络驱动可直接使用,其他系统参考 从“每事件中断”切换为“通知一次后有预算批处理”
单生产者/单消费者 FIFO 与 DMA 接口 Linux kfifo kfifo_in()kfifo_out()kfifo_dma_in_prepare_*()kfifo_dma_in_finish() Linux 内核可直接使用,RTOS 可参考 单读者/单写者无需额外锁,DMA prepare/finish 提交语义清晰
最长关中断区间追踪 Linux ftrace irqsoff tracer irqsoff tracer、tracing_max_latency Linux 可直接使用,MCU 可仿照 只保存刷新最大值的关键调用链,避免海量日志淹没证据

Arm CMSIS-Core:NVIC 状态读回和 DWT 时间戳

CMSIS-Core 是 Cortex-M 项目最直接的开源实现参考。它通过 NVIC_TypeSCB_TypeDWT_TypeCoreDebug_Type 等结构体,把核心外设的固定地址寄存器映射为统一的 C 接口;NVIC_SetPriority() 等内联函数再根据 __NVIC_PRIO_BITS 处理优先级字段在 8 位寄存器中的位置。

实现原理

  1. ISER/ICER 控制 IRQ 使能和禁止;
  2. ISPR/ICPR 读取或修改 pending;
  3. IABR 表示 IRQ 是否处于 active;
  4. IPR 保存外部中断优先级;
  5. SCB->AIRCR.PRIGROUP 决定抢占优先级与子优先级的位划分;
  6. DWT->CYCCNT 在启用后按核心周期递增,可作为低开销时间基准。
1
2
3
4
5
6
7
flowchart LR
A[CMSIS API] --> B[NVIC/SCB/DWT 结构体映射]
B --> C[核心私有外设寄存器]
C --> D[Enable/Pending/Active/Priority]
C --> E[CYCCNT 周期计数]
D --> F[配置读回与启动自检]
E --> G[ISR 延迟/WCET/irq-off 测量]

CMSIS 的价值不是替代芯片手册,而是消除不同厂商库在核心寄存器命名和优先级位移上的差异。外设中断标志如何清除、输入捕获如何配置、DMA 如何产生完成事件,仍然必须按具体芯片参考手册处理。

可落地的启动自检代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @brief Configure an external IRQ and verify the effective priority.
*
* @param irqn IRQ number to configure.
* @param priority Unshifted CMSIS logical priority value.
* @return true if the priority readback matches the requested value.
*/
static bool irq_configure_and_verify(IRQn_Type irqn, uint32_t priority)
{
NVIC_DisableIRQ(irqn);
NVIC_ClearPendingIRQ(irqn);
NVIC_SetPriority(irqn, priority);

if (NVIC_GetPriority(irqn) != priority) {
return false;
}

NVIC_EnableIRQ(irqn);
return NVIC_GetEnableIRQ(irqn) != 0U;
}

这段自检应在 RTOS 启动前或驱动初始化完成后执行。实际项目还应检查 priority grouping、向量表地址、目标安全域和中断路由。对只实现少量优先级位的芯片,读回结果才是最终有效值,不能仅相信写入参数。

DWT 测量的使用边界

DWT 周期计数适合测量微秒级甚至更短的路径,但有以下限制:

  • 并非所有 Cortex-M 内核或芯片配置都提供可用的 CYCCNT
  • 深睡、时钟门控或动态变频可能使计数停止或换算比例改变;
  • 32 位周期计数会回绕,计算差值时应使用无符号减法;
  • 测量代码本身也有固定开销,应先标定空测量成本;
  • 多核系统的各核周期计数不一定同相,不能直接当作全局时间。

因此,DWT 适合做单核局部延迟统计;需要跨设备、跨核或跨低功耗状态对时,应使用通用定时器、PTP/TSU 或外设输入捕获时间戳。

FreeRTOS Kernel:BASEPRI、FromISR 和 Stream Buffer

FreeRTOS 的 Cortex-M3/M4/M7 等端口提供了本文优先级分层的典型实现。内核临界区不是简单执行全局 CPSID I,而是在支持 BASEPRI 的端口中把 configMAX_SYSCALL_INTERRUPT_PRIORITY 写入 BASEPRI,只屏蔽逻辑优先级等于或低于阈值的 IRQ。优先级更高的极关键 IRQ 仍可响应,但这些 IRQ 不能调用 FreeRTOS 内核 API。

BASEPRI 选择性屏蔽原理

1
2
3
4
5
6
7
flowchart TD
A[进入 FreeRTOS 临界区] --> B[BASEPRI = API 阈值]
B --> C{IRQ 逻辑优先级}
C -->|高于阈值| D[仍可抢占内核]
C -->|等于或低于阈值| E[暂时屏蔽]
D --> F[禁止调用 FreeRTOS API]
E --> G[退出临界区后恢复]

这正对应本文的 P0/P1 分层:

  • P0:高于内核 API 阈值,延迟最低,但只能访问硬件、专用固定槽或独立 SPSC ring;
  • P1/P2:位于内核允许区间,可调用受支持的 ...FromISR() API,把事件交给任务;
  • 普通任务临界区只屏蔽会访问内核对象的那部分 IRQ,而不是粗暴屏蔽所有可屏蔽中断。

需要注意,Cortex-M0/M0+ 没有 BASEPRI,对应端口的临界区策略不同,不能照搬 Cortex-M4F 端口结论。

FromISR 到任务切换的实现链路

FreeRTOS 的 ISR 安全 API 通常通过 pxHigherPriorityTaskWoken 返回“本次操作是否使更高优先级任务就绪”。ISR 退出前调用 portYIELD_FROM_ISR(),端口层通过 PendSV 或等效机制请求上下文切换,从而避免在硬件 ISR 中直接执行完整调度器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sequenceDiagram
participant HW as 外设/DMA
participant ISR as ISR
participant OBJ as Queue/Stream/Notify
participant SCH as Scheduler/PendSV
participant TASK as 高优先级任务

HW->>ISR: IRQ
ISR->>ISR: 快照状态并清标志
ISR->>OBJ: ...FromISR() 提交事件
OBJ-->>ISR: pxHigherPriorityTaskWoken
ISR->>SCH: portYIELD_FROM_ISR()
ISR-->>SCH: 异常返回
SCH->>TASK: 切换到已就绪任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @brief Publish a completed DMA segment descriptor to a FreeRTOS queue.
*/
void transport_dma_irq_handler(void)
{
BaseType_t higher_priority_task_woken = pdFALSE;
BaseType_t sent;
transport_segment_t segment;

transport_dma_ack_irq();
transport_dma_get_completed_segment(&segment);

sent = xQueueSendFromISR(g_rx_segment_queue,
&segment,
&higher_priority_task_woken);
if (sent != pdPASS) {
g_transport_stats.segment_queue_overflow_count++;
transport_enter_degraded_mode_from_isr();
}

portYIELD_FROM_ISR(higher_priority_task_woken);
}

对 DMA 数据,ISR 更适合提交固定大小的 descriptor,而不是再次复制整块数据。DMA buffer 的 ownership 必须保持到任务处理完成并显式归还,不能在 descriptor 尚未消费时被 DMA 重用。

若使用 xStreamBufferSendFromISR() 传递小型、长度有严格上界的 FIFO 数据,也必须检查实际写入字节数;该接口可能只写入部分数据。对大块 DMA 数据在 ISR 中调用 Stream Buffer 会产生额外复制,可能破坏 ISR WCET,应改用 descriptor queue、索引通知或 DMA 直接写入专用 ring。

Stream Buffer、Queue 和 Direct-to-Task Notification 如何选

机制 数据形态 主要优点 主要限制 推荐用途
Direct-to-task notification 任务自带状态和 32 位值 开销小、唤醒路径短 不是通用多元素队列;按位通知可能合并重复事件 单一任务唤醒、计数、状态位
Queue 固定大小元素 支持多生产者、多消费者和逐条消息 每条消息有复制与队列管理开销 携带时间戳、序号、状态快照的事件记录
Stream Buffer 字节流 SPSC、实现紧凑、带触发水位 默认假设单写者和单读者;无消息边界 UART/DMA 字节流、连续采样数据
Message Buffer 变长消息 基于 Stream Buffer 保留消息边界 同样遵循单写者/单读者约束 变长协议帧和事件包

Direct-to-task notification 的“状态位”模式可能把多次相同事件合并,因此不能把它当作不可丢事件队列。若每个事件都必须保留时间戳和序号,应使用预分配 Queue 或专用 SPSC ring;若只需要表达“缓冲区已有数据”,通知只承担 doorbell 作用,实际事件数量由 ring 中的生产者索引保存。

FreeRTOS 实现可直接复用的边界

可以直接使用:

  • xTaskNotifyFromISR()xQueueSendFromISR()xStreamBufferSendFromISR()
  • 静态创建的 queue/stream buffer,避免运行期分配;
  • configASSERT() 对非法中断优先级和 API 使用进行检查;
  • portYIELD_FROM_ISR() 在 ISR 退出后立即调度实时任务。

不能直接套用:

  • 把高于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的 P0 ISR 直接调用队列、信号量或 stream buffer;
  • 多个 ISR 并发写同一个 Stream Buffer,却不增加外部串行化;
  • 用 task notification bit 代替事件序号和硬件计数器;
  • 认为提高任务优先级即可弥补 DMA ring 容量不足或长期吞吐不足。

Zephyr:Direct ISR、Zero-Latency IRQ 和 claim/finish Ring Buffer

Zephyr 把普通 ISR、direct ISR 和 zero-latency ISR 明确区分。普通 ISR 会经过内核通用入口和退出逻辑;direct ISR 减少参数传递、栈切换和调度检查等通用开销;zero-latency IRQ 则配置在普通 IRQ lock 无法屏蔽的优先级,并要求使用 direct ISR。

Zero-latency 实现原理

1
2
3
4
5
6
7
flowchart LR
A[普通 IRQ] --> B[Zephyr 通用 ISR 包装]
B --> C[内核记账/PM/调度检查]
D[Direct IRQ] --> E[直接 ISR 入口]
E --> F[可选择退出调度检查]
G[Zero-latency IRQ] --> H[Direct ISR + 特殊优先级]
H --> I[不调用内核 API]

这种设计与本文 P0 层完全对应:P0 ISR 必须自包含、固定时间、不能修改内核依赖的数据结构,也不能在内核尚未完成低功耗恢复时假设所有设备均已可用。Zephyr 官方还建议在 Flash 访问延迟成为问题时,把 ISR 及其依赖符号搬到 RAM。

需要注意:zero-latency 是架构相关能力,不能假设所有 Zephyr 平台都支持;即使支持,它也只解决“内核 IRQ lock 导致的屏蔽”,不能解决外设 pending 合并、GPIO 脉冲过窄、FIFO overrun 或总线长时间阻塞。

Zephyr ring_buf 的零拷贝原理

Zephyr ring_buf 提供两类接口:

  1. ring_buf_put() / ring_buf_get():由 API 完成复制;
  2. ring_buf_put_claim() / ring_buf_put_finish()ring_buf_get_claim() / ring_buf_get_finish():先取得内部连续区域,再由 DMA 或调用者直接访问,最后提交实际完成长度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sequenceDiagram
participant ISR as DMA/ISR
participant RB as ring_buf
participant DMA as DMA Engine
participant TASK as Consumer

ISR->>RB: put_claim(requested)
RB-->>ISR: ptr + contiguous_length
ISR->>DMA: DMA 写入 ptr
DMA-->>ISR: 完成 IRQ
ISR->>RB: put_finish(actual_length)
ISR->>TASK: semaphore/event
TASK->>RB: get_claim()
RB-->>TASK: ptr + readable_length
TASK->>TASK: 批量处理
TASK->>RB: get_finish(consumed)

该模型的核心是“数据写入完成”和“生产者索引发布”分离。DMA 写完之前不能推进可读索引,否则消费者可能读到尚未完成的数据;消费者处理完之前不能释放读索引,否则生产者可能提前覆盖。

Zephyr 官方文档明确说明,ring_buf 不提供通用内部并发保护,但单生产者和单消费者分别只修改 put/get 一侧索引时,可以在两个执行上下文中并发使用。多生产者、多消费者或 SMP 跨核场景需要额外串行化和内存可见性保证。

可借鉴到其他 RTOS 的关键点

  • 把 ring 分成 reserve/commit 两阶段,而不是“拿到指针就视为有效”;
  • claim 只能返回到缓冲区尾部的连续区间,跨回绕传输需要两个 descriptor;
  • 生产者只写生产索引,消费者只写消费索引;
  • 发布索引前保证数据写入可见,SMP 或非一致性 DMA 系统必须增加屏障/cache 维护;
  • ring 只负责保存数据,任务唤醒由 semaphore/event/notification 完成;
  • ring 满时必须有明确策略:拒绝、流控、丢弃新数据、覆盖旧数据或进入降级状态。

RT-Thread:中断嵌套记账、Hook 和 Ring Buffer

RT-Thread 的开源实现适合直接映射到 MCU 项目。BSP 的中断入口通常调用 rt_interrupt_enter(),退出时调用 rt_interrupt_leave();内核通过 rt_interrupt_nest 记录嵌套深度。源码还提供进入和退出 hook,但官方注释明确要求 hook 必须简单,不能阻塞或挂起。

中断进入/退出的实现原理

1
2
3
4
5
6
7
8
9
10
flowchart TD
A[架构中断入口] --> B[保存上下文]
B --> C[rt_interrupt_enter]
C --> D[irq_nest 加一 + enter hook]
D --> E[设备 ISR]
E --> F[rt_interrupt_leave]
F --> G[leave hook + irq_nest 减一]
G --> H{是否需要调度?}
H -->|是| I[中断退出时切换线程]
H -->|否| J[恢复原上下文]

在不修改每个设备驱动的前提下,可用 enter/leave hook 建立统一统计:

  • 当前嵌套深度和历史最大值;
  • ISR 总进入次数;
  • 从架构入口到退出的周期数;
  • 超过预算的 ISR 次数;
  • 故障发生时最近 IRQ 序列。

但 hook 本身不能做格式化日志、动态分配或复杂查表,否则测量机制会反过来放大 ISR 延迟。若要区分 IRQ 来源,应在 hook 中读取架构提供的当前异常号或由统一分发入口传入 source id。更适合的方式是写入固定大小 per-CPU trace record,后台线程再导出。

RT-Thread ringbuffer 的实现原理

RT-Thread rt_ringbuffer 使用:

  • read_index / write_index 表示当前位置;
  • read_mirror / write_mirror 区分索引相等时的空和满;
  • 回绕时翻转 mirror 位;
  • rt_ringbuffer_put() 在空间不足时只写入可容纳部分;
  • rt_ringbuffer_put_force() 在空间不足时覆盖旧数据;
  • rt_ringbuffer_get_direct() 可返回内部连续可读区间,减少一次复制。

mirror 位方案可以使用全部缓冲容量,不必像“预留一个空槽”方案那样损失一个元素。但对于不可丢关键事件,必须避免无条件使用 put_force():它会覆盖尚未消费的旧数据,使最新数据看似正常而历史 sequence 已经丢失。

1
2
3
关键事件 ring:put 失败 -> overflow 计数 -> 降级/告警,禁止静默覆盖
调试 trace ring:允许覆盖旧记录,但必须有 overwrite 计数
连续通信流:根据协议选择拒绝新数据、硬件流控或覆盖策略

从源码结构看,ringbuffer 本身并不替应用解决所有并发关系。推荐将其限定为一个 ISR/DMA 生产者和一个任务消费者;若多个 IRQ 共同写入,应先按源分 ring,或在更低一级软件中断中汇聚,避免在多个硬件 ISR 之间引入长临界区。

RT-Thread 场景下的特殊审计点

不同架构和移植层的 rt_hw_interrupt_disable() 可能采用全局屏蔽或架构相关实现。若关键捕获 IRQ 对关中断时间极为敏感,应实测 RT-Thread 临界区的最大持续时间,并检查当前 BSP 是否支持按优先级选择性屏蔽,不能仅依据其他 RTOS 的 BASEPRI 机制推断本系统行为。

Linux:Generic IRQ、Threaded IRQ、NAPI、kfifo 和 irqsoff

Linux 内核的实现规模远大于 MCU RTOS,但它把本文涉及的几种机制分得非常清楚,适合作为架构设计参考。

Generic IRQ:IRQ 流和 irq_chip 解耦

Linux Generic IRQ 将中断处理分成三层:

  1. 驱动 API:request_irq()request_threaded_irq()、enable/disable 等;
  2. 高层 flow handler:边沿、电平、fast EOI、per-CPU 等通用流程;
  3. irq_chip:具体控制器的 mask、unmask、ack、eoi、set_type 等操作。
1
2
3
4
5
6
7
8
9
10
11
flowchart TD
A[设备驱动 handler] --> B[Generic IRQ API]
B --> C[irq_desc]
C --> D{IRQ Flow Handler}
D -->|Edge| E[handle_edge_irq]
D -->|Level| F[handle_level_irq]
D -->|Fast EOI| G[handle_fasteoi_irq]
E --> H[irq_chip ack/mask/unmask]
F --> H
G --> H
H --> I[具体中断控制器]

其实现原理是把“事件语义”与“控制器寄存器细节”分离。对 MCU 通用驱动框架也可采用类似设计:设备驱动只声明边沿/电平、清除语义和 ISR;平台层负责 NVIC/PLIC/GIC 的 enable、priority、route 和 ack/eoi。

Threaded IRQ:硬 IRQ 与可调度处理分离

request_threaded_irq() 支持 hard handler 和 thread_fn 两部分。hard handler 只确认来源、保存状态、必要时屏蔽设备并返回 IRQ_WAKE_THREAD;复杂处理在线程上下文执行,可被调度器管理并允许使用睡眠锁或其他线程 API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* @brief Acknowledge a device IRQ and wake its threaded handler.
*/
static irqreturn_t device_irq_top(int irq, void *data)
{
struct device_context *ctx = data;
uint32_t status = device_read_status(ctx);

if ((status & DEVICE_RELEVANT_IRQ_MASK) == 0U) {
return IRQ_NONE;
}

device_ack_irq(ctx, status);
ctx->irq_status_snapshot = status;
return IRQ_WAKE_THREAD;
}

/**
* @brief Process the deferred device work in schedulable context.
*/
static irqreturn_t device_irq_thread(int irq, void *data)
{
struct device_context *ctx = data;

device_process_events(ctx, ctx->irq_status_snapshot);
return IRQ_HANDLED;
}

上面的单快照字段仅适用于设备 IRQ 在 threaded handler 完成前保持 masked,或使用 IRQF_ONESHOT 等机制防止同一来源并发覆盖快照的情况。若硬件允许新事件继续到达,应改用 FIFO、原子位图、计数器或事件 ring,而不是复用一个 irq_status_snapshot

在 PREEMPT_RT 中,IRQ 线程化被进一步用于缩短不可抢占路径,使高优先级实时线程能抢占普通中断处理。这个思路可映射为 MCU 中的“两级 ISR”:P0 硬件 ISR 写固定槽并触发 P1 软件中断,P1 再调用 RTOS API 或唤醒任务。

NAPI:从中断风暴切换为预算批处理

NAPI 是网络子系统机制,不能直接当成通用 MCU API,但其工作模型高度通用:

  1. 首个事件通过 IRQ 通知 CPU;
  2. 驱动调度 poll 实例,并抑制后续同类 IRQ;
  3. poll 每次最多处理 budget 个接收项;
  4. 若队列仍有数据,继续被调度;
  5. 队列清空后 complete 并重新打开 IRQ。
1
2
3
4
5
6
stateDiagram-v2
[*] --> IRQEnabled
IRQEnabled --> PollScheduled: 首次事件 IRQ
PollScheduled --> Polling: 关闭/抑制同类 IRQ
Polling --> Polling: 已处理 budget,仍有积压
Polling --> IRQEnabled: 队列清空,complete

这正适用于高速 UART、CAN RX FIFO、USB endpoint、ADC 批量采样等场景:IRQ 只是“队列非空”的 doorbell,任务或下半部按预算 drain。预算既限制单次占用,又减少每个元素一次中断的开销。

kfifo:SPSC 和 DMA prepare/finish

Linux kfifo 官方文档说明,在只有一个并发读者和一个并发写者时,kfifo_in() / kfifo_out() 等接口不需要额外锁。它还提供 DMA prepare/finish 接口:prepare 阶段生成可供 DMA 使用的连续或 scatterlist 区域,finish 阶段再推进生产或消费计数。

该实现与 Zephyr claim/finish 是同一类设计:

1
reserve/prepare -> DMA 或调用者写入 -> memory visibility -> commit/finish

可借鉴的重点不是 Linux 宏本身,而是明确“可写空间被预留”和“数据已对消费者可见”是两个不同状态。若 DMA 尚未完成就提交生产索引,所有后续队列逻辑都会建立在错误数据上。

irqsoff tracer:只保存最坏样本

Linux ftrace 的 irqsoff tracer 记录中断关闭持续时间;当出现新的最大延迟时,保存导致该最大值的调用链,并用新记录替换旧的较小最大值。

这比“每次关中断都打印”更适合嵌入式实时系统。MCU 可实现一个简化版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enter_critical:
if nesting == 0:
start_cycle = now
caller = return_address
nesting++

exit_critical:
nesting--
if nesting == 0:
duration = now - start_cycle
if duration > max_duration:
max_duration = duration
max_caller = caller
freeze_recent_trace_window()

实现时必须保存和恢复原始中断状态,正确处理嵌套,并避免在测量路径中调用日志和分配器。产品版本可以只保留最大值、调用点和超限次数,调试版本再保留短 trace window。

通用 SPSC 事件 Ring 的实现原理

FreeRTOS Stream Buffer、Zephyr ring_buf、RT-Thread rt_ringbuffer 和 Linux kfifo 的共同基础都是生产者索引与消费者索引分离。对“一个 ISR 生产、一个实时任务消费”的场景,可以实现固定大小 SPSC event ring。

1
2
3
4
5
flowchart LR
A[ISR Producer] -->|写 event slot| B[(SPSC Event Ring)]
A -->|release publish head| B
B -->|acquire read head| C[Task Consumer]
C -->|处理完成后更新 tail| B

核心规则如下:

  1. 生产者先写完整 event 数据,再发布新的 head;
  2. 消费者先读取已发布 head,再读取 event 数据;
  3. 生产者不能修改 tail,消费者不能修改 head;
  4. ring 满时不得静默覆盖关键事件;
  5. 每个 event 携带 sequence,便于检测硬件、ISR 或 ring 层跳号;
  6. 单核 MCU 上也要防止编译器重排;SMP 或 DMA 系统还需要架构内存屏障和 cache 一致性处理;
  7. head/tail 类型应保证单次访问原子,并正确处理回绕。

下面是结构示意,具体原子操作需替换为目标平台实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* @brief Fixed-size event stored in an ISR-to-task SPSC ring.
*/
typedef struct {
uint32_t sequence; /**< Monotonic source sequence number. */
uint32_t status; /**< Captured hardware status snapshot. */
uint64_t timestamp; /**< Hardware or core-cycle timestamp. */
} realtime_event_t;

/**
* @brief Single-producer/single-consumer event ring.
*/
typedef struct {
realtime_event_t *slots; /**< Preallocated event storage. */
uint32_t capacity; /**< Number of event slots. */
atomic_uint head; /**< Producer-owned published index. */
atomic_uint tail; /**< Consumer-owned released index. */
} realtime_event_ring_t;

/**
* @brief Publish one event from the single ISR producer.
*
* @param ring Event ring.
* @param event Event to publish.
* @return true on success, false when the ring is full.
*/
static bool realtime_event_ring_push_isr(realtime_event_ring_t *ring,
const realtime_event_t *event)
{
uint32_t head = atomic_load_explicit(&ring->head, memory_order_relaxed);
uint32_t next = (head + 1U) % ring->capacity;
uint32_t tail = atomic_load_explicit(&ring->tail, memory_order_acquire);

if (next == tail) {
return false;
}

ring->slots[head] = *event;
atomic_store_explicit(&ring->head, next, memory_order_release);
return true;
}

这段示意要求 capacity >= 2,并采用“预留一个槽位”区分满和空;实际项目也可使用单调递增计数器或 mirror bit 使用全部容量。重点是 release/acquire 发布关系,而不是具体取模写法。还必须确认所用原子类型在目标 MCU 上是 lock-free 且可安全用于 ISR;若目标编译器或架构不满足条件,应使用 RTOS/架构提供的原子 API 和内存屏障,不能简单以 volatile 替代。

开源实现的选型建议

系统环境 推荐直接采用 推荐参考但不要原样复制
Cortex-M 裸机 CMSIS NVIC/DWT、芯片输入捕获/DMA FreeRTOS BASEPRI 分层、Zephyr claim/finish、irqsoff 最大值追踪
Cortex-M + FreeRTOS CMSIS、FreeRTOS FromISR、Queue/Stream Buffer、静态对象 Zephyr zero-latency 和 Linux NAPI 的架构思想
Cortex-M + Zephyr Direct/zero-latency ISR、ring_buf、kernel offload FreeRTOS 阈值划分和 Linux threaded IRQ/NAPI
Cortex-M/RISC-V + RT-Thread rt_interrupt_enter/leave、hook、ringbuffer、设备 DMA 框架 FreeRTOS BASEPRI 是否适用需按 BSP 验证;参考 NAPI 预算批处理
Linux/PREEMPT_RT Generic IRQ、threaded IRQ、NAPI、kfifo、ftrace/irqsoff MCU 的 P0 固定槽模型可用于少数不可线程化路径
多核 SoC + RTOS 每核 ring、硬件 mailbox、原子和内存屏障 单核 SPSC 示例不能忽略 cache coherence 和跨核发布顺序

使用开源实现时必须避免的误区

  1. 看到 API 名称相似就认为语义相同。 FreeRTOS Stream Buffer、Zephyr ring_buf、RT-Thread ringbuffer 和 Linux kfifo 的并发保证、阻塞语义、DMA 接口和满缓冲策略并不相同。
  2. 只复制数据结构,不复制所有权协议。 Ring 是否可靠,取决于谁能写 head、谁能写 tail、何时 commit,以及失败时如何处理。
  3. 把零延迟中断理解为绝对零延迟。 它仍受当前更高优先级异常、硬件入口开销、总线等待、Flash wait state 和不可屏蔽异常影响。
  4. 把 threaded IRQ 当成硬件事件保存机制。 线程化只能让复杂处理可调度,不能恢复已经在单 bit pending 中合并的多次边沿。
  5. 把 DMA 当成自动不丢数据。 DMA descriptor 耗尽、ring 满、cache 未维护、ownership 错误和总线拥塞仍会造成数据损坏或 overrun。
  6. 使用覆盖式 ring 却不记录覆盖次数。 对关键事件必须拒绝并告警;只对允许保留“最近窗口”的 trace 数据使用覆盖策略。
  7. 把开源默认配置当作实时性证明。 最终仍需在目标芯片、编译选项、Flash/TCM 布局、总线负载和组合压力下测量 WCET 与最大延迟。

一句话概括:开源实现已经给出了可复用的构件,但可靠性来自“硬件保存事件、ISR 固定时间、数据 reserve/commit、单一所有权、任务及时消费、溢出显式化和最坏延迟可追踪”这一整套协议,而不是来自某一个 ring buffer 或某一个 RTOS API。

先定义“漏中断”到底是什么

工程现场常把多种现象都称为“漏中断”,但它们的根因和修复方式完全不同。排查前必须先统一术语。

现象 严格定义 典型根因 主要修复方向
物理事件未捕获 外部边沿或内部事件没有形成可见硬件状态 脉冲过窄、电平不达标、复用错误、滤波、时钟门控、低功耗 示波器、输入捕获、锁存、硬件整形
Pending 合并 多次事件在一个 pending 位中只保留一次 边沿发生频率高于服务速度,控制器/外设只有 1 bit 标志 计数器、FIFO、输入捕获 DMA、批处理
响应超时 中断最终执行,但入口延迟超过业务截止时间 长时间关中断、高优先级 ISR 过长、不可抢占、总线阻塞 优先级、缩短 ISR、减少临界区、硬件捕获
ISR 误清标志 ISR 进入后错误地清除了新到事件或其他共享源 读改写竞态、W1C 误用、清除顺序错误、共享 IRQ 扫描不全 按手册清标志、循环 drain、状态快照
外设溢出 CPU 或 DMA 未及时搬运数据,FIFO/接收寄存器 overrun 每字节中断、DMA 配置错误、总线拥塞、缓存维护错误 DMA、增大 FIFO/环形缓冲、背压
软件通道丢失 ISR 已记录,但队列满、覆盖、任务处理太慢 队列容量不足、无序号、错误覆盖策略、优先级反转 有界队列、水位、序号、背压、丢弃策略
业务层误判 中断和数据均存在,但状态机忽略或去重错误 时间窗错误、重复过滤、竞态、状态重置 端到端序号与状态机审计

因此,第一条原则是:**中断入口计数不等于物理事件计数,任务收到的消息数也不等于 ISR 执行次数。**必须建立多个独立计数器进行端到端对账。

从物理事件到业务处理的完整链路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
flowchart TD
A[事件发生] --> B{引脚/内部信号是否达到触发条件?}
B -->|否| B1[物理层或复用层丢失]
B -->|是| C{外设是否锁存/计数/FIFO 保存?}
C -->|否| C1[短脉冲或重复边沿可能不可恢复]
C -->|是| D{IRQ 是否使能且路由正确?}
D -->|否| D1[Enable/Route/Mask 配置问题]
D -->|是| E{控制器是否保留 pending?}
E -->|合并| E1[多事件压缩为一次 pending]
E -->|保留| F{CPU 何时进入 ISR?}
F -->|太晚| F1[屏蔽/抢占/长 ISR/总线延迟]
F -->|及时| G{ISR 是否完整处理所有源?}
G -->|否| G1[误清标志/共享 IRQ/读取顺序问题]
G -->|是| H{事件是否成功入队?}
H -->|否| H1[队列满/覆盖/并发错误]
H -->|是| I{任务是否在截止时间内消费?}
I -->|否| I1[任务调度/锁/优先级反转/算力不足]
I -->|是| J[业务成功]

这张图给出排查顺序。每次复现时,应尽可能收集从 A 到 I 的证据,而不是只看某个 ISR 是否被调用。

建立可量化的实时指标

“偶尔”和“很快”不能用于实时系统验收。至少要定义以下指标。

1
2
3
4
5
6
7
T_event      = 物理事件发生时间
T_latch = 外设锁存或硬件时间戳时间
T_isr_in = CPU 进入 ISR 的时间
T_isr_out = CPU 退出 ISR 的时间
T_enqueue = 事件成功进入软件队列的时间
T_task_start = 任务开始处理该事件的时间
T_done = 业务处理完成时间

由此得到:

1
2
3
4
5
6
7
8
L_irq      = T_isr_in - T_latch             # 中断响应延迟
C_isr = T_isr_out - T_isr_in # ISR 执行时间
L_queue = T_task_start - T_enqueue # 队列等待时间
L_end2end = T_done - T_event # 端到端延迟
J_irq = max(L_irq) - min(L_irq) # 中断响应抖动
Loss_hw = Count_event - Count_latch # 物理到捕获层丢失
Loss_irq = Count_latch - Count_isr_event # 捕获层到 ISR 丢失或合并
Loss_queue = Count_isr_event - Count_task # ISR 到任务通道丢失

对周期事件,还应统计周期误差:

1
Period_error[n] = (T_latch[n] - T_latch[n-1]) - Expected_period

对突发数据,还应统计最大到达率和服务率:

1
2
3
4
5
6
R_in_peak   = 峰值事件到达率
R_service = ISR/任务可持续服务率
B_queue = 事件队列或 DMA 缓冲容量
T_burst_max = 可持续承受的最长突发时间

B_queue >= (R_in_peak - R_service) * T_burst_max + B_margin

若长期平均到达率大于长期处理率,任何有限缓冲区最终都会溢出。此时提高中断优先级只能延后失败,不能解决系统容量不足。

已知客观机制与工程分析

已知客观机制

  1. 多数中断控制器至少提供使能、pending、active 和优先级概念,但具体优先级位数、数值方向、抢占规则和 pending 行为由架构及芯片实现决定。
  2. Arm Cortex-M 的 NVIC 允许设置 IRQ 优先级和优先级分组;在常见 Cortex-M 实现中,数值越小通常表示逻辑优先级越高,实际可用优先级位数由芯片实现并通过 __NVIC_PRIO_BITS 等信息体现。
  3. FreeRTOS 在支持 BASEPRI 的 Cortex-M 端口中,通常用 configMAX_SYSCALL_INTERRUPT_PRIORITY 划分可调用 RTOS API 的 ISR 与不能被内核临界区屏蔽的更高优先级 ISR。
  4. RISC-V PLIC 也提供中断源优先级、pending、enable 和每个 context 的阈值,但其优先级数值语义与 Cortex-M 不同,不能直接照搬数值配置。
  5. Linux、Zephyr 等系统都把低延迟顶半部与可延后处理分开;Linux 还提供 IRQ tracing、ftrace 等手段分析中断关闭区间和延迟。

基于上述机制的工程分析

  1. “把关键 IRQ 设成数字 0”不是通用答案。不同架构数值方向不同,某些 RTOS 对最高优先级 ISR 的 API 调用有严格限制,某些芯片还把不可屏蔽异常、系统异常和外设 IRQ 分开管理。
  2. 关键事件如果只由一个 pending 位表示,那么在 ISR 被延迟期间发生多次事件时,即使最高优先级也可能只看到一次。要保证事件次数,必须使用硬件计数器、输入捕获 FIFO、DMA 或外部锁存。
  3. 实时性应由“最坏情况”验证。平均 ISR 延迟很小不能证明峰值负载下不超时;应关注 P99.9、最大值和故障注入条件下的上界。
  4. 优先级只是资源仲裁的一部分。ISR 执行时间、关中断区间、总线访问、DMA 仲裁、缓存一致性、队列容量和任务锁竞争同样会决定结果。

第一阶段:确认硬件事件是否真实存在

同步观察事件源与 ISR 入口

最直接的方法是在示波器或逻辑分析仪上同时观察:

  1. 外部事件原始信号;
  2. MCU 实际引脚电平;
  3. ISR 入口翻转的调试 GPIO;
  4. 必要时再增加 ISR 出口 GPIO 或 DMA 完成 GPIO。
1
2
3
4
5
6
7
8
9
10
sequenceDiagram
participant SRC as 外部事件源
participant PIN as MCU 引脚/捕获单元
participant CPU as ISR 调试 GPIO
SRC->>PIN: 产生边沿
Note over SRC,PIN: 测量脉宽、电平、上升时间、抖动
PIN->>CPU: 中断请求
Note over PIN,CPU: Δt = 中断响应延迟
CPU-->>CPU: ISR 执行
Note over CPU: 脉宽 = ISR 执行时间近似值

如果原始信号存在,但 MCU 引脚处不存在,应检查电平转换、复用、上下拉、RC 滤波、施密特输入、串扰和引脚电压域。若 MCU 引脚存在但捕获寄存器没有变化,应检查外设时钟、输入复用、触发极性、数字滤波、低功耗门控和安全域权限。

检查脉冲宽度与输入采样条件

外部 GPIO 中断并不保证能检测任意窄的脉冲。输入同步器、去抖滤波、外设采样时钟和芯片手册规定的最小高低电平时间都会影响可靠性。对于脉冲宽度接近或小于系统时钟周期、低功耗采样周期或滤波窗口的事件,应优先使用:

  • 定时器输入捕获;
  • 异步事件/外部中断锁存;
  • 硬件计数器;
  • FPGA/CPLD 或外部触发锁存;
  • 脉冲展宽或单稳态电路;
  • 具备深度的捕获 FIFO;
  • 输入捕获 DMA。

关键原则是把“瞬时边沿”变成“可保持状态”。一旦硬件只给软件一个瞬时脉冲,而 CPU 又可能被屏蔽,软件无法事后恢复不存在的证据。

区分边沿触发和电平触发

类型 优点 风险 适用建议
边沿触发 对短事件敏感;无需保持电平 多个边沿可能合并;清标志竞态;脉冲过窄可能漏 事件次数重要时配合计数/FIFO/捕获
电平触发 只要条件保持,CPU 迟到仍能看到 源未清除会反复进入;共享线容易形成中断风暴 状态型告警、FIFO 非空、设备请求
双边沿 可记录高低转换 事件率翻倍;极性判断和清除更复杂 编码器、脉宽测量,优先用硬件捕获

如果事件是“数据已到达且 FIFO 非空”,电平触发通常比单次边沿更稳健;如果事件是“每个边沿都代表一个不可丢的采样时刻”,应由捕获计数器或 FIFO 记录每次边沿,而不是只依赖一个 IRQ pending 位。

第二阶段:检查中断控制器和屏蔽状态

读取 Enable、Pending、Active 和优先级

复现时应快照以下信息,具体寄存器名称由架构决定:

1
2
3
4
5
6
7
8
9
10
11
IRQ enable state
IRQ pending state
IRQ active state
IRQ priority
priority grouping / threshold
current exception number
global interrupt mask
selective interrupt mask
fault mask
vector table base
interrupt route / target CPU

在 Cortex-M 上通常会关注 NVIC 的 enable、pending、active、priority,以及 PRIMASKBASEPRIFAULTMASK、当前异常号和优先级分组;在 RISC-V PLIC 上则要关注 source priority、pending、context enable、threshold 和 claim/complete 流程。不要只打印某个库函数返回值,应尽量保留原始寄存器快照,避免抽象层隐藏配置错误。

审计所有关中断路径

常见的长时间屏蔽来源包括:

  • 全局关中断宏使用范围过大;
  • RTOS 临界区内进行内存拷贝、Flash 操作、复杂计算或日志;
  • 驱动在轮询硬件完成时保持中断关闭;
  • 中断处理程序中再次关中断并执行长循环;
  • 错误的嵌套临界区导致恢复掩码不正确;
  • Bootloader、升级、NVM 写入、时钟切换或低功耗切换代码;
  • 第三方协议栈或闭源库内部临界区;
  • SMP 自旋锁在 IRQ-off 状态下长时间竞争;
  • 安全域切换或 TrustZone 门调用路径。

建议对每次关中断和开中断记录时间戳、调用位置和持续时间,并统计:

1
2
3
4
5
irq_off_count
irq_off_max_cycles
irq_off_total_cycles
irq_off_over_deadline_count
irq_off_last_caller

对于 Cortex-M,可使用 DWT cycle counter、通用定时器或 GPIO 脉冲测量;对于 Linux,可使用 irqsoff/preemptoff tracer、ftrace 和 trace events。若系统没有现成工具,应在统一的临界区封装中加入轻量级统计,而不是在每个模块单独打印日志。

检查优先级分组和数值方向

常见错误不是“优先级不够高”,而是:

  1. 以为数值越大优先级越高,实际平台相反;
  2. 只设置了子优先级,没有获得预期的抢占能力;
  3. 库函数参数要求未移位值,但代码传入了已移位值,或反之;
  4. 芯片只实现部分优先级位,低位被忽略,多个看似不同的值实际相同;
  5. RTOS 启动或驱动初始化后重新配置了优先级分组;
  6. 默认优先级为最高,导致未配置的普通 IRQ 反而抢占关键 IRQ;
  7. 安全世界和非安全世界具有独立或受限的优先级配置;
  8. 多核系统把中断路由到了错误 CPU,或 CPU affinity 与实时任务不一致。

优先级配置完成后,应通过读回寄存器验证真实值,而不是相信初始化代码“已经执行”。

第三阶段:检查外设标志和 ISR 清除顺序

先读状态还是先清标志

不同外设的标志语义可能是:

  • 写 1 清除(W1C);
  • 写 0 清除(W0C);
  • 读状态后读数据清除;
  • 读某寄存器后写另一寄存器清除;
  • 读到空或完成 claim/complete 才清除;
  • 必须先关闭通道再清除;
  • 多个状态位共享一个清除寄存器。

因此不能用通用的 status &= ~FLAG 或随意的读改写清除所有外设标志。尤其是 W1C 寄存器,错误的读改写可能把并发到达的新标志一起清掉。

推荐模式是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void device_irq_handler(void)
{
uint32_t status;

for (;;) {
status = device_read_pending_status();
status &= DEVICE_IRQ_RELEVANT_MASK;
if (status == 0U) {
break;
}

device_capture_event_data(status);
device_clear_pending_status(status);
device_publish_events(status);
}
}

上述代码只是结构示意,具体顺序必须以芯片手册为准。有些外设应先清标志再读取数据,有些必须先读取锁存数据再清标志。关键是循环 drain 当前所有有效源,并在共享 IRQ 或边沿密集场景中避免只处理第一次快照。

共享中断线必须遍历全部来源

多个通道共享一个 IRQ 时,ISR 不能假设只有一个来源。应:

  1. 读取所有可能来源的状态;
  2. 只处理“状态有效且中断使能”的来源;
  3. 按硬件要求清除;
  4. 再次读取,直到没有待处理来源或达到防风暴上限;
  5. 对未知状态位计数并保留快照。

如果只处理第一个命中项就返回,其他来源可能保持 pending,造成中断风暴,也可能在清除公共标志时被误丢。

防止清除与新事件到达之间的竞态

典型竞态如下:

1
2
3
4
5
6
7
8
sequenceDiagram
participant HW as 外设
participant ISR as ISR
HW->>ISR: Event A 置位
ISR->>ISR: 读取 status=A
HW->>ISR: Event B 在清除前到达
ISR->>HW: 以错误读改写方式清除 A
Note over HW,ISR: B 可能被一起清除或与 A 合并

修复方法取决于硬件:使用 W1C 精确写入原始快照、读取事件计数器差值、使用 FIFO、循环读取 pending、在清除后再次检查、或改用电平条件和 drain-until-empty 模式。

第四阶段:审查 ISR 的最坏执行时间

ISR 里允许做什么

关键 ISR 推荐只做:

  • 读取外设状态、捕获寄存器或时间戳;
  • 复制固定长度的必要上下文;
  • 更新无锁计数器或固定大小统计;
  • 清除中断源;
  • 向预分配队列、环形缓冲或位图提交事件;
  • 必要时触发一次调度或软件中断。

不建议在关键 ISR 中做:

  • printf、串口同步日志或格式化字符串;
  • 动态内存分配和释放;
  • 文件系统、Flash 擦写或阻塞式总线访问;
  • 协议解析、校验大块数据、DSP/AI 算法;
  • 等待互斥锁、信号量或外设 ready;
  • 不确定次数的循环;
  • 大块 memcpy
  • 调用不明确支持 ISR 的驱动 API;
  • 修改大量共享状态;
  • 在高于 RTOS API 阈值的 ISR 中调用内核服务。

ISR 执行时间预算

设第 i 个中断的最坏执行时间为 C_i,最小到达间隔为 T_i,则其 CPU 利用率近似为:

1
2
U_i = C_i / T_i
U_irq_total = Σ U_i

这个估算还没有包含嵌套、进入退出开销、缓存未命中、总线等待和临界区,因此只能作为初筛。若高频 IRQ 的 U_i 已经很大,应优先减少中断频率,而不是继续微调优先级。

例如:

1
2
UART 每字节中断:3 Mbps,10 bit/byte,约 300000 次/s
单次 ISR 仅 2 us:CPU 时间约 600 ms/s,即 60%

即使每次 ISR 看起来很短,极高频率仍会吞噬大量 CPU,并增加所有其他中断的抢占和恢复开销。改为 DMA 每 256 字节一次中断后,中断频率约降至 1172 次/s,系统行为会完全不同。

限制一次 ISR 的处理量

对于 FIFO 或队列型外设,ISR 可以批量 drain,但不能无限处理到所有数据耗尽而饿死其他 IRQ。可采用预算机制:

1
2
3
MAX_ITEMS_PER_ISR
MAX_CYCLES_PER_ISR
MAX_BYTES_PER_ISR

达到预算后,保留中断条件或调度下半部继续处理。这样既减少中断频率,又限制单次占用上界。

第五阶段:高吞吐数据必须从“每事件中断”改为“批处理中断”

DMA + 环形缓冲 + IDLE/超时

UART、SPI、I2S、ADC、网络 MAC、USB 和高速传感器的通用数据面应尽量采用:

1
2
3
4
5
6
7
8
flowchart LR
A[高速外设] --> B[硬件 FIFO]
B --> C[DMA 环形/双缓冲]
C --> D[半满/全满/IDLE/描述符完成 IRQ]
D --> E[极短 ISR\n提交写指针与时间戳]
E --> F[无锁 Ring Buffer 或描述符队列]
F --> G[协议/算法任务批量处理]
G --> H[背压/流控/丢弃策略]

其中:

  • DMA 负责搬运数据;
  • ISR 只提交“新增数据区间”或完成描述符;
  • 任务批量处理,降低上下文切换;
  • 使用生产者/消费者索引而不是每字节消息;
  • 对缓存型 CPU 处理 DMA 一致性;
  • 通过 RTS/CTS、协议窗口、暂停发送或采样降级实现背压。

双缓冲和描述符环

连续流常用 ping-pong buffer:DMA 写 A 时任务处理 B,写 B 时处理 A。更高吞吐或长突发可用描述符环:

1
2
3
4
Descriptor 0 -> Buffer 0 -> OWN=DMA
Descriptor 1 -> Buffer 1 -> OWN=DMA
Descriptor 2 -> Buffer 2 -> OWN=CPU
...

需要明确 ownership 转移、内存屏障和缓存维护。否则可能出现“中断到了但 CPU 读到旧数据”或“CPU 已复用缓冲而 DMA 仍在写”的假性漏事件。

高频通信中断的优先级不应压过硬实时捕获

高吞吐不等于高实时。一个每秒几十万次的串口 RX 中断,如果每个字节都抢占关键同步 ISR,会把系统拖入高频上下文切换;反过来,如果将它设为最低优先级且没有 DMA/FIFO,又会导致 overrun。

正确做法是改变数据路径:让 DMA/FIFO吸收吞吐,IRQ 只在批次边界通知。然后再根据“允许的 FIFO 服务延迟”设置中等优先级,而不是靠最高优先级硬扛吞吐。

中断优先级矩阵如何设计

不按外设名称排序,而按实时属性排序

每个 IRQ 至少评估以下字段:

字段 含义 设计作用
Criticality 丢失或超时后果 决定保护级别和降级策略
Deadline 从事件到必须响应的最长期限 决定抢占优先级
WCET ISR 最坏执行时间 计算阻塞和 CPU 预算
Min Inter-arrival 最小到达间隔 计算峰值频率和嵌套风险
Hardware Retention pending/FIFO/计数器能保存多少事件 判断是否必须更高优先级或硬件缓存
Burst Size 最大突发事件数 计算缓冲容量
RTOS API Need ISR 是否必须调用内核 API 决定能否高于内核阈值
Shared Resource 是否访问锁、总线、缓存或共享寄存器 分析优先级反转和阻塞
Recovery 丢失后是否可重试、重建或降级 决定容错策略

可定义一个工程排序量:

1
Slack_i = Deadline_i - Blocking_i - WCET_i

Slack_i 越小,说明可用余量越小,通常越需要较高抢占优先级或硬件捕获。但这只是排序辅助,不能替代完整的响应时间分析。

通用五级矩阵

逻辑等级 典型来源 目标 ISR 约束 RTOS API 推荐数据保存方式
P0 极关键 安全保护、硬实时同步、输入捕获、过流/过温快速关断 最小延迟、即使内核临界区也可响应 极短、固定时间、禁止日志和阻塞 通常禁止 硬件锁存、捕获 FIFO、专用无锁槽
P1 硬实时 控制环定时器、ADC/I2S DMA、关键 PWM 更新、时间戳 满足周期和抖动上限 读取/提交/清除,不做复杂算法 视阈值而定 DMA 双缓冲、时间戳队列
P2 高吞吐 UART/SPI/USB/CAN/Ethernet DMA 完成、FIFO 水位 防止 FIFO 溢出并批量搬运 有预算地 drain,不逐字节解析 通常可以 FromISR DMA ring、描述符队列
P3 普通实时 普通传感器、系统 tick、非关键定时器、存储完成 有界延迟 可稍长但仍不阻塞 可以 普通队列/工作项
P4 后台 UI、日志发送、统计、调试、低速按键 不影响关键路径 尽量线程化 可以 任务/线程处理

P0 不应被大量使用。若很多 IRQ 都被设成 P0,P0 内部仍会互相阻塞,系统也失去分层意义。通常只保留极少数真正需要越过 RTOS 临界区的事件。

一个可落地的矩阵模板

1
2
3
4
5
6
7
8
9
10
11
+------+----------------------+----------+---------+----------+----------+-----------+----------------------+
| 等级 | IRQ | Deadline | WCET | 最小间隔 | 保持能力 | RTOS API | 处理策略 |
+------+----------------------+----------+---------+----------+----------+-----------+----------------------+
| P0 | safety_capture | 5 us | 0.8 us | 100 us | 4-entry | No | 捕获时间戳并置位 |
| P1 | control_timer | 20 us | 1.5 us | 1 ms | 1 pending| No/限定 | 更新寄存器并通知任务 |
| P1 | sample_dma_half | 40 us | 2 us | 500 us | 双缓冲 | Yes | 提交 buffer descriptor|
| P2 | uart_dma_idle | 1 ms | 3 us | 200 us | DMA ring | Yes | 提交新增数据范围 |
| P2 | can_rx_fifo | 500 us | 5 us | 50 us | 32 FIFO | Yes | 有预算批量 drain |
| P3 | sensor_data_ready | 5 ms | 4 us | 10 ms | 1 pending| Yes | 通知采集任务 |
| P4 | debug_uart_tx | 50 ms | 8 us | 可变 | FIFO | Yes | 后台发送或丢弃 |
+------+----------------------+----------+---------+----------+----------+-----------+----------------------+

表中的数字必须由实际芯片、业务截止时间和实测 WCET 替换,不能照抄。

Cortex-M + FreeRTOS 的常见规划方式

以下是通用原则,不代表某一颗 MCU 的固定数值。

  1. 先确认 __NVIC_PRIO_BITS 和实际可实现的优先级数量。
  2. 尽量把优先级位主要用于抢占,而不是把大量位分给子优先级。
  3. 明确 configKERNEL_INTERRUPT_PRIORITYconfigMAX_SYSCALL_INTERRUPT_PRIORITY 与 CMSIS NVIC_SetPriority() 参数之间的表示差异。
  4. 所有调用 xxxFromISR() 的 IRQ 都必须位于 RTOS 允许的逻辑优先级范围。
  5. 高于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的 IRQ 不能调用 FreeRTOS API;它们应写入无锁槽、硬件 mailbox、专用 ring 或触发一个较低优先级的软件中断。
  6. 不要让未初始化的 IRQ 保持默认最高优先级。
  7. 开发阶段启用 configASSERT(),对非法 ISR API 调用、优先级配置和队列使用进行早期检测。
1
2
3
4
5
6
7
flowchart TD
A[IRQ 发生] --> B{逻辑优先级高于 RTOS API 阈值?}
B -->|是| C[禁止调用 RTOS API]
C --> D[写无锁事件槽/计数器]
D --> E[触发低优先级软件 IRQ 或任务轮询]
B -->|否| F[可调用受支持的 FromISR API]
F --> G[发送队列/通知/信号量]

如果关键 ISR 必须与任务交换复杂数据,优先采用“P0 ISR 只写固定槽 + P1/P2 软件中断或任务完成提交”的两级处理,而不是强行在 P0 中调用内核服务。

RISC-V、GIC 和 Linux 场景的抽象对应

RISC-V PLIC

PLIC 通常通过 source priority、pending、context enable、threshold 和 claim/complete 协作。配置时应确认:

  • priority 0 是否代表禁用;
  • context threshold 是否把目标 IRQ 屏蔽;
  • claim 后是否总能 complete;
  • 同优先级 tie-break 规则;
  • 中断是否路由到正确 hart;
  • 本地中断与 PLIC 外部中断的优先关系。

不能把 Cortex-M“数字越小优先级越高”的经验直接套到 PLIC。

Arm GIC / 多核 SoC

在 GIC 或多核 SoC 中,还要考虑:

  • 中断 group、安全状态和 CPU target;
  • IRQ affinity 与实时任务 CPU 亲和性;
  • 是否被迁移到忙碌 CPU;
  • shared peripheral interrupt 与 per-CPU interrupt 的差异;
  • 中断线程化、软中断和调度延迟;
  • CPU idle、频率变化和中断唤醒延迟;
  • IOMMU、SMMU 或缓存一致性对 DMA 完成路径的影响。

Linux

Linux 驱动通常采用顶半部快速确认硬件和唤醒线程化处理;对允许睡眠或较长处理的部分使用 threaded IRQ、workqueue、NAPI 或专用 kthread。排查可使用:

  • /proc/interrupts 对比各 CPU IRQ 计数;
  • ftrace、trace-cmd、perf 和 irqsoff/preemptoff tracer;
  • IRQ affinity 和实时线程优先级;
  • request_threaded_irq()、NAPI poll budget、网络 ring 统计;
  • lockdep、RCU stall、softirq backlog;
  • 设备驱动的 overrun、drop、missed interrupt 计数器。

Linux 下“ISR 已进入”并不代表用户态及时收到数据;还要继续观察 threaded handler、softirq、调度、socket/字符设备缓冲和用户线程。

RTOS 下的任务优先级与中断优先级必须成对设计

只提高 IRQ 优先级而不提高事件消费任务的优先级,会把问题从“ISR 来不及”转移成“队列越来越满”。反之,任务优先级很高但硬件 IRQ 被长时间屏蔽,也无法恢复丢失的边沿。

1
2
3
4
5
6
7
8
flowchart LR
A[硬件 IRQ] --> B[ISR 提交事件]
B --> C[高优先级实时任务]
C --> D[计算/控制/协议处理]
D --> E[低优先级日志/存储]

F[普通任务持锁] -.可能阻塞.-> C
G[高频普通 IRQ] -.可能抢占.-> B

应同时审查:

  1. 事件消费者任务是否能在队列溢出前运行;
  2. 消费任务是否会等待低优先级任务持有的互斥锁;
  3. RTOS 是否启用优先级继承;
  4. 消费任务是否在处理过程中同步输出日志或访问慢设备;
  5. 高优先级任务是否因内存分配、文件系统或共享总线阻塞;
  6. ISR 唤醒任务后是否正确请求上下文切换;
  7. 同一事件是否被多个消费者竞争性读取。

避免优先级反转

典型场景:高优先级实时任务被低优先级日志任务持有的互斥锁阻塞,而中优先级任务持续运行,导致高优先级任务长期无法执行。应采用:

  • 具备优先级继承的互斥锁;
  • 缩短锁持有时间;
  • 不让实时任务依赖日志、文件系统或动态内存锁;
  • 用消息传递替代共享大对象;
  • 对关键数据使用双缓冲、RCU 风格快照或 lock-free SPSC ring;
  • 将慢操作转交后台任务。

事件队列和环形缓冲设计

事件记录应包含什么

一个通用实时事件至少可包含:

1
2
3
4
5
6
7
8
9
10
/**
* @brief Captured real-time event transferred from ISR to task context.
*/
typedef struct {
uint32_t source_id; /**< Logical interrupt or event source identifier. */
uint32_t sequence; /**< Monotonic source sequence number. */
uint64_t timestamp; /**< Hardware or low-overhead software timestamp. */
uint32_t status; /**< Captured peripheral status snapshot. */
uint32_t data; /**< Small fixed-size event payload or descriptor index. */
} realtime_event_t;

不要在 ISR 队列中复制大块数据。对于 DMA 数据,应只传递描述符索引、offset、length、generation 和 timestamp。

SPSC Ring 比通用队列更适合极短 ISR

若只有一个 ISR 生产者和一个任务消费者,可使用单生产者单消费者环形缓冲:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ISR producer:
next = (write + 1) % N
if next == read:
overflow++
execute_overflow_policy()
else:
ring[write] = event
memory_barrier_release()
write = next

Task consumer:
if read != write:
memory_barrier_acquire()
event = ring[read]
read = (read + 1) % N

实际实现要按 CPU 内存模型和编译器规则使用正确的原子操作或屏障。volatile 不能替代完整的并发同步。

队列满时必须有明确策略

策略 适用场景 风险
丢最新事件 旧事件必须按顺序处理 新事件延迟或缺失
丢最旧事件 只关心最新状态 历史连续性丢失
合并事件 状态型事件、重复通知 不适合每次边沿都有意义的计数事件
计数压缩 同类事件可用累计次数表达 无法保留每次时间戳
触发降级 安全保护、流控、降低采样率 业务性能下降
复位通道 已不可恢复的协议状态 有数据丢失和恢复时间

队列满不应悄悄覆盖。必须增加 overflow_count、最高水位、首次溢出时间和最近一次溢出上下文。

容量量化

1
2
3
4
5
6
7
R_peak       = 峰值生产速率
R_consume = 最低保证消费速率
T_block_max = 消费任务可能被阻塞的最长时间
N_inflight = 同时在途的 DMA/硬件事件数
N_margin = 安全余量

Queue_depth >= (R_peak - R_consume) * T_block_max + N_inflight + N_margin

对于周期性关键事件,队列容量还应覆盖最坏关中断时间和最坏调度延迟:

1
Queue_depth >= ceil((T_irq_off_max + T_sched_max) / T_event_min) + margin

若单个 pending 位在这段时间内只能保存一次事件,则再大的软件队列也没有意义,必须先改硬件捕获层。

时间戳和可观测性设计

时间戳优先级

从准确性高到低通常为:

  1. 外设输入捕获硬件时间戳;
  2. DMA 描述符或 MAC/定时器硬件时间戳;
  3. ISR 入口读取自由运行计数器;
  4. ISR 内软件 tick;
  5. 任务收到事件时的时间。

任务时间戳不能用于证明中断响应延迟,因为其中包含队列和调度延迟。

建议的统计结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @brief Runtime interrupt latency and loss statistics for one source.
*/
typedef struct {
uint64_t hardware_event_count; /**< Events observed by hardware capture logic. */
uint64_t isr_event_count; /**< Events decoded by the ISR. */
uint64_t task_event_count; /**< Events consumed by task context. */
uint64_t queue_overflow_count; /**< Events rejected or overwritten by software queue. */
uint64_t peripheral_overrun; /**< FIFO, DMA, or peripheral overrun count. */
uint32_t irq_latency_max; /**< Maximum observed latch-to-ISR latency in cycles. */
uint32_t isr_cycles_max; /**< Maximum observed ISR execution time in cycles. */
uint32_t queue_high_watermark; /**< Maximum queued event count. */
uint32_t deadline_miss_count; /**< End-to-end deadline violations. */
} irq_health_stats_t;

日志不能反过来破坏实时性

ISR 里不要直接打印。建议:

  • 使用固定大小二进制 trace record;
  • 写入独立 trace ring;
  • 后台批量导出;
  • 发生错误时冻结最近 N 条 trace;
  • 支持触发式采样,而不是持续输出所有事件;
  • 对 trace 自身设置丢弃计数,避免 trace 阻塞业务。
1
2
3
4
5
6
flowchart LR
A[ISR/关键路径] --> B[固定长度 Trace Ring]
B --> C[后台导出任务]
C --> D[UART/USB/文件/调试器]
E[异常触发条件] --> F[冻结前后窗口]
F --> B

总线、Flash、Cache 和 DMA 对中断延迟的影响

中断优先级高并不意味着 ISR 一定立即完成。CPU 进入 ISR 后仍可能等待:

  • Flash 指令取指和 wait state;
  • I-Cache/D-Cache miss;
  • 外设总线桥或 APB/AHB/AXI 仲裁;
  • DMA burst 占用总线;
  • 外部存储器访问;
  • TCM、SRAM 或共享内存竞争;
  • 写缓冲和内存屏障;
  • 多核缓存一致性事务。

对硬实时 ISR,可考虑:

  1. 将向量表和关键 ISR 放入低延迟 SRAM/TCM;
  2. 将关键数据结构放入确定性内存;
  3. 避免 ISR 访问大表、外部 RAM 或未预热代码;
  4. 限制 DMA 最大 burst,合理配置总线 QoS;
  5. 分离关键 DMA 与大吞吐 DMA 的通道和优先级;
  6. 对 cacheable DMA buffer 正确执行 clean/invalidate;
  7. 使用必要的 memory barrier 保证描述符 ownership 顺序;
  8. 测量而不是假设最坏总线延迟。

DMA 优先级也需要矩阵

DMA 等级 典型业务 策略
D0 控制采样、音频连续流、关键捕获 最短等待,较小 burst,确保周期性服务
D1 网络/CAN/高速串口 RX 保证不溢出,允许批量 burst
D2 屏幕、存储读写、大块内存搬运 限制 burst 或使用空闲带宽
D3 日志、后台校验、非关键复制 可暂停或降速

如果大块显示或存储 DMA 长时间垄断总线,CPU 即使进入高优先级 ISR,也可能在读取外设寄存器或 SRAM 时等待。此类问题必须从总线仲裁和 DMA 配置解决。

多核系统中的额外问题

IRQ affinity 与任务 affinity

关键 IRQ 和其消费任务最好位于同一实时 CPU,减少跨核唤醒和 cache line 迁移。对 Linux/RTOS SMP,应检查:

  • IRQ 是否在多个 CPU 之间自动迁移;
  • 实时任务是否被调度到另一个 CPU;
  • 共享队列是否造成 cache line 抖动;
  • 中断线程是否被普通任务抢占;
  • CPU 是否承担网络 softirq、存储中断和关键控制三类负载;
  • CPU 隔离和 affinity 配置是否真正生效。

跨核通信也可能丢事件

IPI、mailbox、共享内存门铃通常也只有有限 pending 能力。若多个事件用一个 doorbell 表示,应把事件内容放在可计数队列中,doorbell 只表示“队列非空”。否则多个门铃可能合并。

低功耗、时钟和动态频率的影响

漏中断只在低功耗或变频后出现时,应检查:

  • 该 IRQ 是否是合法唤醒源;
  • 进入睡眠前是否清除了 pending;
  • 外设时钟是否在睡眠中停止;
  • 唤醒后复用和触发极性是否重配;
  • CPU 唤醒延迟是否超过事件保持时间;
  • 动态频率变化后时间戳换算是否更新;
  • cache、TCM、DMA 和外设状态是否在恢复路径中同步;
  • 睡眠入口临界区是否过长;
  • 深睡时外部脉冲是否需要异步检测或锁存。

关键短脉冲若可能在深睡期间发生,必须确认硬件具备异步唤醒或事件锁存能力。单纯依赖睡眠中的 CPU 及时采样不可靠。

一个通用的系统化排查流程

第 1 步:定义症状和验收标准

明确:

  • 哪个事件丢失;
  • 事件最小间隔和最大突发;
  • 允许的最大响应延迟;
  • 允许的最大抖动;
  • 是否允许事件合并;
  • 丢失后果和恢复方式;
  • 复现负载、温度、电压和运行时长。

第 2 步:建立端到端计数

至少建立:

1
2
3
4
5
6
7
external_generator_count
hardware_capture_count
irq_entry_count
isr_decoded_event_count
queue_push_count
queue_pop_count
business_done_count

计数必须使用足够宽度,考虑并发和回绕,并在故障时同时快照。

第 3 步:测量物理信号和 ISR 延迟

用示波器同时捕获事件和 ISR GPIO。记录最小、最大、P99.9 和异常样本,不要只看平均值。

第 4 步:快照控制器与外设状态

故障时保存 enable、pending、active、priority、mask、外设状态、FIFO 水位、DMA 描述符和队列水位。

第 5 步:审计所有屏蔽区间

统计全局关中断、选择性屏蔽、RTOS 临界区、自旋锁和 Boot/Flash 操作的最长时间及调用者。

第 6 步:测量每个 ISR 的 WCET

在真实编译优化、真实存储位置、cache 开关和最坏总线负载下测量。加入嵌套和抢占测试。

第 7 步:检查清标志和共享 IRQ

按芯片手册逐项验证 W1C、读清、claim/complete、FIFO drain 和共享源遍历。

第 8 步:检查 ISR 到任务路径

统计队列满、覆盖、信号量饱和、任务延迟、互斥锁等待、内存分配失败和状态机丢弃。

第 9 步:构造峰值压力

同时打开高吞吐通信、存储、显示、日志、网络、传感器和算法任务,模拟最坏组合,而不是逐模块单测。

第 10 步:修改架构并回归

优先顺序通常是:

  1. 增强硬件捕获和 FIFO/DMA;
  2. 缩短 ISR;
  3. 消除长关中断区间;
  4. 重构高频中断为批处理;
  5. 重新规划优先级;
  6. 增加队列容量和背压;
  7. 调整任务优先级和锁;
  8. 优化总线、cache 和内存布局;
  9. 增加故障恢复和健康监控。

压力测试矩阵

维度 基线 压力条件 观测指标
关键事件频率 额定频率 1.2x、2x、最小间隔 捕获数、ISR 数、截止时间
通信负载 空闲 UART/SPI/CAN/网络满载突发 FIFO overrun、DMA ring 水位
存储负载 无写入 Flash 擦写、文件系统同步、大块读写 irq-off 最大值、总线延迟
日志负载 关闭 最大日志等级、异常重复输出 ISR 时间、队列积压
CPU 负载 算法满载、多个高优先级任务 调度延迟、任务截止时间
DMA 负载 单通道 多 DMA 同时大 burst 总线等待、关键 DMA 抖动
电源温度 常温标称 高低温、电压边界、时钟变化 信号质量、捕获稳定性
低功耗 不睡眠 高频睡眠/唤醒、深睡 唤醒事件、恢复状态
故障注入 队列接近满、延迟清标志、禁用消费者 溢出策略、恢复时间
长稳测试 分钟级 24h/72h/更长 最大值、计数对账、内存泄漏

必须覆盖的组合场景

单项压力通过不代表组合压力通过。至少测试:

  1. 关键事件 + 高频通信 RX;
  2. 关键事件 + Flash 擦写;
  3. 关键事件 + DMA 大 burst;
  4. 关键事件 + 最大日志;
  5. 关键事件 + RTOS 临界区最坏路径;
  6. 关键事件 + 高优先级任务持续运行;
  7. 关键事件 + 低功耗频繁进出;
  8. 所有高负载同时开启。

故障注入与证明性测试

为了证明架构有效,应主动制造失败条件:

  • 人为延长某个低优先级 ISR,验证关键 IRQ 是否仍满足截止时间;
  • 人为延长关中断区间,验证硬件捕获是否保存全部事件;
  • 限制任务消费速率,验证队列水位和背压是否生效;
  • 缩小 DMA ring,验证 overrun 计数和恢复路径;
  • 注入重复边沿和最小脉宽,验证捕获能力;
  • 强制共享 IRQ 多源同时置位,验证 ISR 是否完整 drain;
  • 模拟时间戳回绕、序号回绕和计数器溢出;
  • 故意触发 cache 维护遗漏,在测试版本中进行一致性校验;
  • 改变 IRQ priority/threshold,验证自检能否发现非法配置;
  • 暂停后台日志导出,验证 trace ring 不会阻塞关键路径。

证明性测试的目标不是“尽量不出错”,而是确认系统在达到设计边界时会以可预测方式降级、计数、告警和恢复。

运行时健康监控

建议周期性输出或远程读取以下指标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
irq[source].entry_count
irq[source].event_count
irq[source].latency_max
irq[source].isr_cycles_max
irq[source].nested_max
irq[source].unexpected_status
irq[source].deadline_miss
queue[source].depth
queue[source].high_watermark
queue[source].overflow
peripheral[source].overrun
peripheral[source].fifo_high_watermark
dma[channel].error
dma[channel].descriptor_starvation
system.irq_off_max
system.irq_off_caller
system.cpu_load_by_isr

健康状态机

1
2
3
4
5
6
7
8
9
stateDiagram-v2
[*] --> Normal
Normal --> Warning: 队列高水位/延迟接近上限
Warning --> Normal: 负载恢复且连续稳定
Warning --> Degraded: 多次 deadline miss 或 overrun
Degraded --> Recovering: 降采样/限流/重置通道
Recovering --> Normal: 对账通过且状态稳定
Recovering --> Fault: 无法恢复或关键事件丢失
Fault --> [*]

在 Warning 状态可以降低日志、暂停非关键存储、限制显示刷新或提高预读/消费服务能力;在 Degraded 状态可降低采样率、关闭非关键功能、启用流控或重启单个外设;只有无法局部恢复时才考虑系统级复位。

常见错误做法

只把关键 IRQ 调到最高

问题:可能违反 RTOS API 规则,导致普通未配置 IRQ 同样处于最高级;也无法解决 pending 合并、脉冲过窄、队列满和外设 overrun。

在 ISR 中打印调试日志

问题:串口日志、格式化和锁会显著扩大 ISR 时间,并改变复现概率。应使用 GPIO、cycle counter 和二进制 trace ring。

用平均延迟证明实时性

问题:漏中断通常发生在长尾。必须观察最大值、P99.9、最坏组合和长时间运行。

看到 pending 就认定硬件没问题

问题:一个 pending 位可能代表一次或多次事件;pending 存在不能证明事件次数完整。

只看 ISR 计数

问题:ISR 可能一次处理多个 FIFO 项,也可能多次进入只处理同一个未清电平;应比较硬件事件数、解码事件数和任务消费数。

通过加大队列掩盖处理能力不足

问题:如果长期消费率低于生产率,队列最终仍溢出。应降低中断频率、批处理、增加算力或实施背压。

在关键路径使用不可控第三方 API

问题:第三方库可能内部关中断、分配内存或持锁。应通过测量、封装和替代实现控制 WCET。

volatile 当成线程安全

问题:volatile 只约束部分编译器优化,不保证原子性、顺序性或多核可见性。应使用平台原子操作、临界区或内存屏障。

一个推荐的重构方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
flowchart TD
A[关键外部/内部事件] --> B[Timer Capture / Latch / FIFO]
B --> C[P0 ISR]
C --> D[读取硬件时间戳和序号]
D --> E[固定槽或 SPSC Ring]
E --> F[P1 实时任务]
F --> G[状态机/控制/同步处理]

H[高吞吐 UART/SPI/网络] --> I[DMA Descriptor Ring]
I --> J[P2 完成/IDLE IRQ]
J --> K[提交数据区间]
K --> L[P2/P3 协议任务]

M[日志/存储/UI] --> N[P4 后台任务]

O[Priority & Mask Self-check] -.启动自检.-> C
O -.启动自检.-> J
P[Latency/Overflow Monitor] -.运行时统计.-> E
P -.运行时统计.-> I
P -.降级控制.-> N

关键路径

  1. 硬件捕获每个关键事件并提供时间戳或计数;
  2. P0 ISR 固定时间完成,不调用 RTOS API;
  3. P0 ISR 写入 SPSC ring 或固定 mailbox;
  4. 通过低一级软件中断、事件标志或高优先级任务处理复杂逻辑;
  5. 每个事件携带 sequence,任务发现跳号立即记录;
  6. 队列高水位触发降级和流控;
  7. 所有非关键日志和存储从关键路径剥离。

高吞吐路径

  1. 外设 FIFO + DMA 环形缓冲吸收突发;
  2. 以半满、全满、IDLE、超时或描述符完成为中断边界;
  3. ISR 只提交新增范围;
  4. 任务批量解析;
  5. 当 ring 接近满时启用硬件流控、协议背压或有定义的丢弃策略。

自检路径

启动后读回并验证:

  • IRQ enable、priority、grouping、route;
  • RTOS API 阈值关系;
  • DMA 描述符地址、对齐和 ownership;
  • 向量表地址;
  • 关键队列容量;
  • 低功耗唤醒配置;
  • 时间戳频率和换算参数。

发现非法配置时应在开发版本立即 assert,在产品版本记录故障并进入安全降级,而不是继续运行到偶发失效。

示例伪代码

极短关键 ISR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @brief Handle a hard real-time capture interrupt.
*
* This handler must not call RTOS APIs when configured above the kernel
* interrupt masking threshold.
*/
void critical_capture_irq_handler(void)
{
uint32_t status;
uint32_t captured_count;
uint64_t timestamp;

status = capture_get_irq_status();
if ((status & CAPTURE_EVENT_FLAG) == 0U) {
g_capture_stats.unexpected_status++;
capture_clear_irq_status(status);
return;
}

timestamp = capture_read_timestamp();
captured_count = capture_read_event_count();
capture_clear_irq_status(CAPTURE_EVENT_FLAG);

if (!critical_event_ring_push_isr(timestamp, captured_count, status)) {
g_capture_stats.queue_overflow_count++;
critical_event_enter_degraded_mode();
}
}

DMA 完成 ISR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @brief Publish newly received DMA data without parsing it in interrupt context.
*/
void transport_dma_irq_handler(void)
{
uint32_t flags;
uint32_t producer_index;

flags = dma_get_and_clear_irq_flags();
producer_index = dma_get_producer_index();

transport_ring_publish_isr(producer_index, flags);
transport_schedule_worker_from_isr();
}

实时任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @brief Consume captured events and verify end-to-end sequence continuity.
*/
void critical_event_task(void *argument)
{
realtime_event_t event;
uint32_t expected_sequence = 0U;

(void)argument;

for (;;) {
wait_for_critical_event_notification();

while (critical_event_ring_pop(&event)) {
if (event.sequence != expected_sequence) {
report_event_gap(expected_sequence, event.sequence, event.timestamp);
expected_sequence = event.sequence;
}

expected_sequence++;
process_critical_event(&event);
check_event_deadline(&event);
}
}
}

这些代码只表达结构,寄存器清除顺序、原子操作、屏障和 RTOS API 必须按具体平台实现。

验收标准

一个可交付的中断架构至少应满足:

  1. 在规定的最小事件间隔和最大突发下,硬件捕获计数与事件发生计数一致;
  2. 关键事件 sequence 无跳号,或所有不可避免丢失都有明确计数和降级动作;
  3. 最大中断响应延迟小于规定上限,并包含最坏关中断、最坏嵌套和最坏总线负载;
  4. ISR WCET 有实测上界,且没有日志、动态分配和阻塞操作;
  5. 高频数据通道不使用不可承受的每字节中断;
  6. DMA/FIFO/软件队列在峰值组合负载下不溢出,或溢出策略经过验证;
  7. RTOS API 调用与中断优先级阈值匹配;
  8. 共享 IRQ、W1C、读清和 claim/complete 流程经过代码审计;
  9. 低功耗、Flash 擦写、日志满载和多 DMA 并发场景通过;
  10. 运行时能读取最大延迟、队列高水位、overrun、deadline miss 和最长 irq-off;
  11. 故障注入后系统能够告警、降级、局部恢复或安全复位;
  12. 配置读回自检能够发现优先级、路由、阈值和 DMA 描述符错误。

面试时的回答组织方式

回答这类题时,可以按以下顺序展开:

  1. 先澄清“漏中断”可能发生在物理输入、外设捕获、控制器 pending、ISR、队列或任务六个不同层次;
  2. 再说明证据链:示波器同时看事件与 ISR GPIO,增加硬件计数、ISR 计数、队列计数和业务计数;
  3. 然后审查触发方式、脉宽、pending 合并、标志清除、共享 IRQ 和 FIFO overrun;
  4. 接着检查全局关中断、RTOS 临界区、高优先级 ISR WCET、优先级分组和 API 阈值;
  5. 对高吞吐接口改成 DMA/FIFO/环形缓冲/批处理中断,避免每字节 IRQ;
  6. 按截止时间、WCET、最小到达间隔、事件保持能力和损失后果建立优先级矩阵;
  7. 将关键 ISR 缩到只做时间戳、状态快照、清标志和无阻塞提交;
  8. 最后给出峰值压力测试、故障注入、运行时健康监控和验收标准。

一句话概括:可靠的中断系统不是“某个 IRQ 优先级最高”,而是让关键事件由硬件可靠保存,让 ISR 有确定的最坏执行时间,让高吞吐数据批量搬运,让任务及时消费,并用端到端计数和时间戳证明事件没有在任何一层消失。

参考链接