Nuttx - Interrupt

从 GIC 硬件中断触发到 ISR 执行再到中断级上下文切换,完整解析 NuttX 如何在 ARMv7-A 上管理中断的注册、分发、响应与任务调度联动。

本文回答以下问题:硬件中断触发后 CPU 经历了哪些步骤才到达你注册的 ISR?GIC 如何决定哪个中断优先?为什么 ISR 中调用 sem_post() 不会立即切换任务而要等中断返回?enter_critical_section() 到底做了什么?读完后,你将能够理解中断从硬件到调度器的完整链路,并具备独立编写和调试 NuttX 中断驱动的能力。


1. 开篇:中断处理框架解决什么问题?

中断是嵌入式系统的”神经系统”——外设通过中断通知 CPU”我有数据了”或”我完成了”。但硬件只负责”打断 CPU 并跳转到固定地址”,其余所有问题都需要软件框架来解决:

  1. 识别:哪个设备产生了中断?(几十上百个中断源共享少量 IRQ 线)
  2. 分发:找到该中断对应的处理函数(ISR)
  3. 保护:ISR 执行期间如何防止数据竞争?
  4. 调度联动:ISR 唤醒了高优先级任务后,如何最快切换过去?

NuttX 的中断处理框架围绕 ARMv7-A 的 GIC(Generic Interrupt Controller)硬件构建,形成了一条从硬件触发到任务切换的完整链路。NuttX 官方文档(Documentation/implementation/interrupt_controls.rst)描述了中断控制的设计意图:中断处理应当尽可能短,复杂逻辑应交给任务上下文完成。

接下来先了解 GIC 硬件的基本架构——它是整个中断框架的物理基础。


2. GIC 硬件基础

ARMv7-A 处理器只有两条中断输入线:IRQ 和 FIQ。但一个 SoC 可能有上百个中断源(UART、SPI、DMA、定时器…)。GIC 负责将这些中断源汇聚、排优先级、路由到正确的 CPU 核心,最终拉高 CPU 的 IRQ 线。

2.1 GIC 的两个组件

1
2
3
4
5
6
7
8
9
+------------------+          +------------------+
| Distributor | | CPU Interface | (per-core)
| (system-wide) | | (MPCORE_ICC) |
| | | |
| - SPI enable | IRQ | - Priority mask | IRQ/FIQ
| - SPI disable | -------> | - ACK (ICCIAR) | -------> CPU
| - Priority set | | - EOI (ICCEOIR) |
| - CPU routing | | |
+------------------+ +------------------+

Distributor 管理所有 SPI 中断的使能、优先级和目标 CPU 路由;CPU Interface 负责优先级过滤、中断确认(ACK)和结束信号(EOI)。

  • Distributor(分发器):管理所有中断源的使能、优先级、目标 CPU 路由。系统全局唯一。
  • CPU Interface(CPU 接口):每个核心一个。负责优先级屏蔽、中断确认(ACK)、中断结束(EOI)信号。

这两个组件是真实的硬件模块,不是软件抽象。 GIC 是 ARM 公司定义的硬件 IP(规范见 ARM Generic Interrupt Controller Architecture Specification v2.0),SoC 厂商在集成 Cortex-A7/A9/A15 等 ARMv7-A 核心时会将 GIC 作为片上外设一并流片。Distributor 和 CPU Interface 各自拥有独立的 memory-mapped 寄存器空间:Distributor 寄存器组(GICD_*)映射在 MPCORE_ICD_BASE,CPU Interface 寄存器组(GICC_*)映射在 MPCORE_ICC_BASE。NuttX 代码中的 putreg32(val, GIC_ICDDCR) / getreg32(GIC_ICCIAR) 就是通过 MMIO 直接访问这些硬件寄存器。

2.2 三类中断

文件:arch/arm/src/armv7-a/gic.h:598-651

类型 ID 范围 特征 用途
SGI (Software Generated) 0–15 软件触发,per-CPU 私有 SMP 跨核通知(启动、调度、函数调用)
PPI (Private Peripheral) 16–31 硬件触发,per-CPU 私有 每核私有定时器、watchdog
SPI (Shared Peripheral) 32+ 硬件触发,可路由到任意 CPU UART、SPI、DMA 等外设中断

实例:qemu-armv7a 上的典型中断分配

1
2
3
4
5
6
ID 27: Global Timer (PPI)
ID 29: Secure Physical Timer (PPI)
ID 30: Non-secure Physical Timer (PPI) ← 系统 tick 定时器
ID 32: UART0 (SPI, first external interrupt)
ID 33: UART1 (SPI)
...

2.3 关键寄存器

后续所有代码都是对 GIC 寄存器的读写操作。这里先建立一张”寄存器速查表”,在阅读第 3-5 章时可随时回查。

文件:arch/arm/src/armv7-a/gic.h:107-277

Distributor 寄存器(基址 MPCORE_ICD_VBASE,系统唯一)

NuttX 宏 偏移 全称 作用 后文使用场景
GIC_ICDDCR 0x000 Distributor Control Register bit[0]=1 使能 Group0 转发,bit[1]=1 使能 Group1 转发。写 0 则所有中断被 Distributor 截停 第 3 章初始化最后一步
GIC_ICDISER(n) 0x100+ Interrupt Set-Enable Register 写 1 使能对应中断(每 bit 对应一个 IRQ ID)。32 个中断一组 up_enable_irq()
GIC_ICDICER(n) 0x180+ Interrupt Clear-Enable Register 写 1 禁用对应中断。与 ICDISER 镜像,分开设计避免读-改-写竞争 第 3 章初始化时禁用所有 SPI
GIC_ICDIPR(n) 0x400+ Interrupt Priority Register 每个中断 8-bit 优先级(0=最高,255=最低)。4 个中断打包在一个 32-bit 寄存器中 第 3 章设置默认优先级 128
GIC_ICDIPTR(n) 0x800+ Interrupt Processor Targets Register 每个中断 8-bit CPU 掩码(bit0=CPU0, bit1=CPU1…)。决定中断路由到哪个核 第 3 章路由所有 SPI 到 CPU0
GIC_ICDICFR(n) 0xC00+ Interrupt Configuration Register 每中断 2-bit:01=电平触发 1-N 模型,11=边沿触发 第 3 章设置 SPI 为电平触发

Group 0 与 Group 1

上表中 GIC_ICDDCR 提到了 Group0 和 Group1——这是 GIC 对中断的一种安全分组机制。每个中断 ID 通过 Distributor 的 GIC_ICDGRPR(n) 寄存器被分配到其中一组:

分组 信号路径 典型用途
Group 0 FIQ(快速中断) Secure 世界的中断(TrustZone 安全侧),如安全定时器、安全看门狗
Group 1 IRQ(普通中断) Non-secure 世界的中断,即操作系统和应用使用的常规中断

ARM TrustZone 通过这种分组实现安全隔离:Secure 世界的中断走 FIQ,Non-secure 走 IRQ,两者互不干扰。GIC_ICDDCR 的两个 bit 分别控制这两组的总开关。

NuttX 的处理: NuttX 不使用 TrustZone 安全扩展,初始化时同时使能两组(GIC_ICDDCR_ENABLEGRP0 | GIC_ICDDCR_ENABLEGRP1),并通过 GIC_ICCICR 中的 FIQEN 位控制 Group 0 是否走 FIQ。在 qemu-armv7a 上,NuttX 将所有中断统一作为 IRQ 处理,Group 分组对功能没有实际影响——但寄存器中这两个 bit 必须都置 1,否则会有中断无法到达 CPU。

CPU Interface 寄存器(基址 MPCORE_ICC_VBASE,每核一份)

NuttX 宏 偏移 全称 作用 后文使用场景
GIC_ICCICR 0x00 CPU Interface Control Register 使能/禁用该核的中断接收。Group0/Group1 分别控制 第 3 章使能 CPU Interface
GIC_ICCPMR 0x04 Priority Mask Register 优先级阈值。只有优先级数值小于此值的中断才能通过(值越小优先级越高)。写 0xFF = 放行所有 第 3 章设置为 0xFF(全放行)
GIC_ICCBPR 0x08 Binary Point Register 决定优先级的哪些位参与”抢占比较”。值 = 7 表示没有任何位用于抢占——即禁用中断嵌套 第 3 章禁用嵌套
GIC_ICCIAR 0x0C Interrupt Acknowledge Register 读取此寄存器有两个副作用:(1) 返回当前最高优先级 Pending 中断的 ID [9:0];(2) 将该中断状态从 Pending → Active 第 5 章 arm_decodeirq()
GIC_ICCEOIR 0x10 End of Interrupt Register 写入中断 ID,通知 GIC 该中断处理完毕。中断状态从 Active → Inactive,GIC 可以再次触发 第 5 章中断处理完成后

寄存器地址计算

NuttX 中这些寄存器的实际地址 = 基址 + 偏移。以 GIC_ICCIAR 为例:

1
2
3
4
5
/* gic.h:110 */
#define GIC_ICCIAR_OFFSET 0x000c

/* gic.h:244 */
#define GIC_ICCIAR (MPCORE_ICC_VBASE + GIC_ICCIAR_OFFSET)

MPCORE_ICC_VBASE 由板级硬件决定(qemu-armv7a 上为 GIC 外设映射的虚拟地址)。代码中 getreg32(GIC_ICCIAR) 就是一次 32-bit MMIO 读操作。

ICCIAR 寄存器位域

1
2
3
4
 31            13  12    10  9                 0
+----------------+--------+-------------------+
| Reserved |CPUSRC | Interrupt ID |
+----------------+--------+-------------------+
  • **Interrupt ID [9:0]**:触发中断的 IRQ 编号(0-1019)。值 1023(0x3FF)表示”伪中断”(Spurious),即没有有效的 Pending 中断。
  • **CPUSRC [12:10]**:对于 SGI,指示是哪个 CPU 发送的。对于 SPI/PPI 此字段无意义。

了解了 GIC 硬件结构和寄存器含义,下面看 NuttX 如何在启动时配置它们。


3. GIC 初始化

GIC 上电后所有中断默认禁用、优先级未设置、路由未配置。不初始化就收不到任何中断。NuttX 在启动早期(nx_start()up_irqinitialize()arm_gic_initialize())完成 GIC 配置。

3.1 CPU0 专属初始化:arm_gic0_initialize()

文件:arch/arm/src/armv7-a/arm_gicv2.c:165-225(关键摘录)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void arm_gic0_initialize(void)
{
unsigned int nlines = arm_gic_nlines();

/* 禁用所有 SPI 中断 */
for (irq = GIC_IRQ_SPI; irq < nlines; irq += 32)
{
putreg32(0xffffffff, GIC_ICDICER(irq));
}

/* 设置所有 SPI 为电平触发、1-N 模型 */
for (irq = GIC_IRQ_SPI; irq < nlines; irq += 16)
{
putreg32(0x55555555, GIC_ICDICFR(irq));
}

/* 设置优先级 = 128(中等)和目标 CPU = CPU0 */
for (irq = GIC_IRQ_SPI; irq < nlines; irq += 4)
{
putreg32(0x80808080, GIC_ICDIPR(irq));
putreg32(0x01010101, GIC_ICDIPTR(irq));
}
}

只由 CPU0 在启动时执行一次,配置系统级的 SPI 中断。

3.2 每 CPU 初始化:arm_gic_initialize()

文件:arch/arm/src/armv7-a/arm_gicv2.c:241-378(关键摘录)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void arm_gic_initialize(void)
{
/* 设置 SGI 和 PPI 优先级 */
putreg32(0x80808080, GIC_ICDIPR(0)); /* SGI[0:3] */
/* ... 其他 SGI/PPI 优先级 ... */

/* 设置 Binary Point(不允许中断嵌套)*/
putreg32(GIC_ICCBPR_NOPREMPT, GIC_ICCBPR);

/* 设置优先级掩码(允许所有优先级的中断通过)*/
putreg32(GIC_ICCPMR_MASK, GIC_ICCPMR);

/* 使能 CPU Interface */
iccicr = GIC_ICCICR_ENABLEGRP0 | GIC_ICCICR_ENABLEGRP1;
putreg32(iccicr, GIC_ICCICR);

/* 使能 Distributor */
putreg32(GIC_ICDDCR_ENABLEGRP0 | GIC_ICDDCR_ENABLEGRP1, GIC_ICDDCR);
}

关键配置:中断嵌套默认禁用GIC_ICCBPR_NOPREMPT 设置 Binary Point = 7,意味着 GIC 不会用优先级的任何位来做抢占比较——当一个中断正在处理时,即使更高优先级的中断到来也不会嵌套进入。

对比 FreeRTOS:FreeRTOS 在 Cortex-M 上通过 BASEPRI 寄存器允许高优先级中断嵌套。NuttX 在 ARMv7-A 上默认不嵌套——简化了 ISR 编写(不用考虑重入),代价是高优先级中断可能被低优先级中断延迟。

GIC 初始化完成后,中断可以触发了。下面看 CPU 收到 IRQ 后的第一步:汇编向量入口。


4. IRQ 向量入口:arm_vectorirq

当 IRQ 触发时,CPU 硬件只做三件事:保存 CPSR 到 SPSR_irq、保存返回地址到 LR_irq、跳转到 IRQ 向量地址。所有寄存器保存、栈切换、C 函数调用都必须由汇编代码完成。

4.1 完整汇编流程

文件:arch/arm/src/armv7-a/arm_vectors.S:171-278(关键摘录,完整函数约 107 行)

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
arm_vectorirq:
/* Step 1: 修正返回地址(ARM IRQ 返回地址多了 4 字节)*/
sub lr, lr, #4

/* Step 2: 将 LR_irq 和 SPSR_irq 保存到 SYS 模式栈 */
srsdb sp!, #PSR_MODE_SYS

/* Step 3: 切换到 SYS 模式,禁用中断 */
cpsid if, #PSR_MODE_SYS

/* Step 4: 保存通用寄存器到 SYS 栈 */
stmdb sp!, {r0-r12}

/* Step 5: 保存修正后的用户 SP 和 LR */
add r1, sp, #(XCPTCONTEXT_SIZE-4*REG_R0)
stmdb sp!, {r1, r14}

/* Step 6: 保存 FPU 寄存器(如果启用)*/
savefpu sp, r1

/* Step 7: 设置参数并调用 C 函数 */
mov r0, sp /* R0 = 寄存器帧指针 */
mov r4, sp /* R4 = 备份 SP(跨函数调用保留)*/

/* Step 8: 切换到中断栈(如果配置了独立中断栈)*/
setirqstack r1, r3 /* SP = g_intstacktop */

bic sp, sp, #7 /* 8 字节对齐 */
bl arm_decodeirq /* 调用 C 分发函数 */

/* Step 9: 恢复 SP 并切回 IRQ 模式 */
mov sp, r4
restorefpu r0, r2
cps #PSR_MODE_IRQ

/* Step 10: 从返回的 regs 指针恢复寄存器 */
ldmia r0, {r13, r14}^ /* 恢复用户 SP 和 LR */
add r14, r0, #8
ldmia r14!, {r0-r12} /* 恢复 r0-r12 */
rfeia r14 /* 异常返回(恢复 PC + CPSR)*/

下图展示了压栈阶段(Step 1-6)构建的完整寄存器帧,以及出栈阶段(Step 9-10)的恢复过程。底部还展示了上下文切换场景——arm_decodeirq() 返回不同任务的 regs 指针时,Step 10 会恢复另一个任务的寄存器,实现透明的任务切换:

中断压栈

与 SVC 入口(arm_vectorsvc)的对比

特征 arm_vectorirq (IRQ) arm_vectorsvc (SVC)
返回地址修正 sub lr, #4(必须) 不需要
中断栈 切换到独立中断栈 不切换(用 SYS 栈)
返回值含义 可能指向不同任务的帧 同上
触发来源 硬件外设 软件 SVC #0 指令

关键洞察arm_decodeirq() 的返回值 r0 可能指向一个不同任务的寄存器帧。如果 ISR 唤醒了更高优先级的任务,arm_doirq() 会返回新任务的 xcp.regs——汇编代码从这个新帧恢复寄存器,于是 CPU “返回”到了新任务,完成了中断级上下文切换。

4.2 关键观察

回顾整个 arm_vectorirq 流程,有两点核心设计值得特别注意:

观察 1:模式切换路径是 IRQ → SYS → IRQ

中断入口在 IRQ 模式下只停留数条指令(修正 LR、保存 banked 寄存器到内存),随即切到 SYS 模式完成所有实质工作(压栈、执行 C 分发函数),最后切回 IRQ 模式做异常返回。这样设计的原因:

  • SYS 模式的 SP 就是当前任务的栈指针——上下文帧保存在任务私有栈上,天然支持上下文切换(切换任务 = 切换帧指针)。
  • 提前离开 IRQ 模式保护了 LR_irq 和 SPSR_irq 这两个 banked 寄存器——它们只有一份,停留在 IRQ 模式期间如果发生异常会被覆盖。
  • 返回时必须切回 IRQ 模式,因为 ldmia {r13,r14}^^ 后缀写入 USR 寄存器)和 rfeia(异常返回)都要求在特权异常模式下执行。

观察 2:C 分发函数运行在公共中断栈 g_intstacktop

压栈完成后,SP 被 setirqstack 切换到全局中断栈(大小由 CONFIG_ARCH_INTERRUPTSTACK 决定,per-core 一份)。arm_decodeirq() 及其调用的所有 ISR 都在这个公共栈上执行,不消耗任何任务栈空间。任务栈只承载上下文帧本身(固定大小),不随 ISR 复杂度增长。这意味着:

  • 任务栈规划时无需考虑”最坏情况下 ISR 嵌套深度”——ISR 的栈开销被隔离到中断栈。
  • 所有核共享中断栈(per-core),100 个任务也只有 1 份中断栈开销。
  • 代价:中断栈大小必须覆盖最深的 ISR 调用链,如果设置不当会栈溢出(但影响范围是全系统而非单个任务)。

下面看 arm_decodeirq() 如何从 GIC 读取中断号并分发。


5. arm_decodeirq():从 GIC 读取中断号

CPU 只知道”有 IRQ 发生了”,但不知道是哪个设备产生的。必须读 GIC 的 ICCIAR 寄存器才能获取中断 ID。

5.1 实现

文件:arch/arm/src/armv7-a/arm_gicv2.c:395-429

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
uint32_t *arm_decodeirq(uint32_t *regs)
{
uint32_t regval;
int irq;

/* 读 GIC CPU Interface 的 Interrupt Acknowledge Register */
regval = getreg32(GIC_ICCIAR);
irq = (regval & GIC_ICCIAR_INTID_MASK); /* 提取中断 ID [9:0] */

/* 忽略伪中断(ID = 1022 或 1023)*/
if (irq < NR_IRQS)
{
regs = arm_doirq(irq, regs); /* 分发到处理逻辑 */
}

/* 写 EOI 寄存器,通知 GIC 中断处理完成 */
putreg32(regval, GIC_ICCEOIR);

return regs;
}

GIC_ICCIAR 的副作用:这不仅仅是”读取”——GIC 会将该中断标记为 Active 状态(从 Pending → Active),并且如果有更低优先级的中断排队,它们会被阻塞直到 EOI。

GIC_ICCEOIR 的作用:告诉 GIC”我处理完了”。中断状态从 Active → Inactive,GIC 可以再次触发该中断(如果仍有 Pending)。

arm_decodeirq() 拿到中断号后调用 arm_doirq()——这是中断处理的核心 C 函数。


6. arm_doirq():中断处理与上下文切换检测

ISR 运行在中断上下文中——它使用的是中断栈(或被中断任务的栈),不在任何任务的正常执行流中。如果 ISR 中直接做上下文切换,中断返回的寄存器帧会混乱。NuttX 的策略是:ISR 只修改调度器状态(就绪队列),实际切换延迟到中断返回时

6.1 实现

文件:arch/arm/src/armv7-a/arm_doirq.c:56-129(关键摘录)

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
uint32_t *arm_doirq(int irq, uint32_t *regs)
{
struct tcb_s *tcb = this_task();

/* 保存当前任务的寄存器帧指针 */
tcb->xcp.regs = regs;

/* 标记进入中断上下文 */
up_set_interrupt_context(true);

/* 分发中断到注册的 ISR */
irq_dispatch(irq, regs);

/* ISR 可能唤醒了更高优先级的任务,重新获取当前最高优先级任务 */
tcb = this_task();

/* 关键:检测是否需要上下文切换 */
if (regs != tcb->xcp.regs)
{
/* regs 不等 → 调度器已经切换了 g_readytorun 头部! */
struct tcb_s **running_task = &g_running_tasks[this_cpu()];

/* 切换地址环境(KERNEL 模式)*/
addrenv_switch(tcb);

/* 更新调度器状态 */
nxsched_switch_context(*running_task, tcb);

*running_task = tcb;
regs = tcb->xcp.regs; /* 使用新任务的寄存器帧! */
}

/* 清除中断上下文标志 */
up_set_interrupt_context(false);

return regs; /* 返回给汇编——可能是不同任务的帧 */
}

6.2 上下文切换检测的工作原理(选读)

这是 NuttX 中断处理最精妙的设计。让我用一个场景走一遍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
时刻 T0: TaskA(prio=100) 正在运行,定时器中断触发
arm_doirq(30, regs_of_TaskA):
tcb->xcp.regs = regs_of_TaskA // 保存 TaskA 帧

时刻 T1: irq_dispatch() → 调用定时器 ISR
定时器 ISR 内部调用 sem_post() 唤醒 TaskB(prio=200)
→ nxsched_add_readytorun(TaskB)
→ TaskB 优先级更高,成为 g_readytorun 头部
→ TaskB->task_state = RUNNING
→ TaskA->task_state = READYTORUN

时刻 T2: irq_dispatch() 返回
tcb = this_task() // = TaskB (g_readytorun 头部变了!)
regs != tcb->xcp.regs ? // regs = TaskA 的帧, tcb->xcp.regs = TaskB 的帧
→ YES! 需要上下文切换!
regs = tcb->xcp.regs // 返回 TaskB 的帧

时刻 T3: arm_vectorirq 恢复路径
ldmia r0, {r13, r14}^ // 从 TaskB 帧恢复 SP, LR
ldmia r14!, {r0-r12} // 从 TaskB 帧恢复寄存器
rfeia r14 // "返回"到 TaskB(不是 TaskA!)

一句话总结:ISR 只管修改调度器状态(把 TaskB 放到链表头),arm_doirq() 通过比较寄存器帧指针检测变化,汇编代码根据返回的帧指针”返回”到不同的任务——无需显式调用任何切换函数。

上面展示了 ISR 直接修改调度器状态的场景。但复杂驱动(如网卡、SPI 设备)的中断处理不适合全部在中断上下文完成——NuttX 提供了 work_queue 机制来延迟处理。


7. 延迟处理:work_queue(NuttX 的”下半部”)

ISR 运行在中断上下文中,有多个限制:

  • 中断栈有限(通常 2-4KB),不能有深层调用
  • 不能阻塞(不能调用 sem_wait()sleep() 等)
  • 会延迟其他中断(NuttX 默认不嵌套,ISR 期间其他 IRQ 被屏蔽)

对于需要做 SPI/I2C 通信、大量数据处理或网络协议栈调用的中断,应在 ISR 中仅做最小工作(ACK 硬件 + 禁用中断),然后将实际处理交给内核工作线程

7.1 work_queue API

文件:include/nuttx/wqueue.h:406

1
2
int work_queue(int qid, FAR struct work_s *work, worker_t worker,
FAR void *arg, clock_t delay);
  • qidHPWORK(高优先级工作线程)或 LPWORK(低优先级工作线程)
  • work:工作项结构体(通常嵌入在驱动私有数据中)
  • worker:延迟执行的函数(签名:void worker(FAR void *arg)
  • arg:传递给 worker 的参数
  • delay:延迟多少 tick 后执行(0 = 立即排队)

7.2 HPWORK 与 LPWORK

工作队列 配置项 默认优先级 特征
HPWORK CONFIG_SCHED_HPWORK 224(接近最高) 不应做阻塞操作,用于 ISR 延迟处理
LPWORK CONFIG_SCHED_LPWORK 50(较低) 可做较长时间操作,用于非紧急后台任务

两者都是普通的内核线程——它们在任务上下文中运行,有自己的栈,可以做大多数内核操作(但 HPWORK 优先级高,不应长时间阻塞)。

7.3 实例:ICJx IO Expander 驱动的中断处理

文件:drivers/ioexpander/icjx.c:1081-1094(ISR)

1
2
3
4
5
6
7
8
9
10
11
12
13
static int icjx_interrupt(int irq, FAR void *context, FAR void *arg)
{
FAR struct icjx_dev_s *priv = (FAR struct icjx_dev_s *)arg;

/* 中断上下文中只做一件事:调度工作到 HPWORK
* 注释原文:"We do not want to do this in the interrupt handler
* because SPI communication speed."
*/

DEBUGVERIFY(work_queue(HPWORK, &priv->work, icjx_interrupt_worker,
(FAR void *)priv, 0));
return OK;
}

文件:drivers/ioexpander/icjx.c:1013-1038(Worker,任务上下文)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void icjx_interrupt_worker(void *arg)
{
FAR struct icjx_dev_s *priv = (FAR struct icjx_dev_s *)arg;
uint16_t isr;

/* 禁用中断(防止 worker 执行期间重复触发)*/
priv->config->enable(priv->config, false);

/* 通过 SPI 读取中断状态寄存器(这在 ISR 中不能做——SPI 可能阻塞!)*/
icjx_read(priv, ICJX_INT_STATUS_A, &isr, ICJX_NOB2);

while (isr != 0)
{
/* 处理变化的 IO 引脚,调用用户注册的回调 */
/* ... */
icjx_read(priv, ICJX_INT_STATUS_A, &isr, ICJX_NOB2);
}

/* 重新使能中断 */
priv->config->enable(priv->config, true);
}

模式总结

1
2
3
4
5
ISR (interrupt context):          Worker (task context, HPWORK):
- 极短(几行代码) - 可以做 SPI/I2C 通信
- 禁用设备中断 - 可以做数据处理
- work_queue(HPWORK, ...) - 完成后重新使能中断
- return OK

7.4 与 Linux 下半部机制的对比

机制 Linux NuttX 等价物
softirq 内核软中断,不可阻塞,per-CPU
tasklet 基于 softirq,不可阻塞
workqueue 内核线程,可阻塞 work_queue(HPWORK/LPWORK, ...) ← 唯一选项
threaded IRQ 中断线程化,可阻塞 HPWORK 工作线程(等效)

NuttX 用一个统一的 work_queue 机制覆盖了 Linux 需要多种机制处理的场景。设计哲学:嵌入式系统中断源少、处理逻辑相对简单,一个高优先级工作线程足够应对。

work_queue 解决了”ISR 太长”的问题。下面回到 ISR 注册本身——看 irq_attach() 如何将 ISR 绑定到中断号。


8. irq_attach() 与 irq_dispatch():ISR 注册与分发

8.0 为什么需要软件分发?——ARMv7-A 的向量表限制

在理解 irq_attach() / irq_dispatch() 之前,需要先了解一个硬件事实:ARMv7-A 的异常向量表只有 8 个条目,所有外设中断共用其中一个入口。

1
2
3
4
5
6
7
8
9
10
11
12
13
ARMv7-A Exception Vector Table (仅 8 项):

Offset Exception
+--------+---------------------------+
| 0x00 | Reset |
| 0x04 | Undefined Instruction |
| 0x08 | SVC (Supervisor Call) |
| 0x0C | Prefetch Abort |
| 0x10 | Data Abort |
| 0x14 | (Reserved) |
| 0x18 | IRQ ← 所有外设中断共用! |
| 0x1C | FIQ |
+--------+---------------------------+

无论是 UART、定时器、DMA 还是 SPI 触发中断,CPU 都跳到偏移 0x18同一个地址(即 arm_vectorirq)。硬件不会告诉你”是哪个设备”——必须读 GIC_ICCIAR 获取中断号,再通过软件查表找到对应的处理函数。

对比 Cortex-M(NVIC): Cortex-M 的向量表有 16 + N 个条目(N = 外设中断数,可达数百个),每个外设中断有独立的入口地址。IRQ 33 触发时,CPU 硬件直接跳到向量表第 33 项指向的函数——不需要软件分发。这就是为什么 FreeRTOS 在 Cortex-M 上可以直接用硬件向量表注册 ISR,而 NuttX 在 ARMv7-A 上必须维护一张软件分发表。

1
2
3
4
5
6
7
8
Cortex-M (NVIC):                 ARMv7-A (GIC):
┌────────────────────┐ ┌───────────────────────┐
│ Vector[16] → UART │ │ 0x18 → arm_vectorirq │ (唯一入口)
│ Vector[17] → Timer │ │ read GIC_ICCIAR → ID│
│ Vector[18] → DMA │ │ g_irqvector[ID]() │ (软件查表)
│ ... │ └───────────────────────┘
└────────────────────┘
硬件直接跳转 软件分发

理解了这个硬件背景,就能明白为什么 NuttX 需要 g_irqvector[] 数组 + irq_attach() 注册机制——它们本质上是用软件实现了 ARMv7-A 硬件缺失的”per-interrupt 向量表”。

8.1 处理函数表

文件:sched/irq/irq.h:50-59

1
2
3
4
5
6
7
struct irq_info_s
{
xcpt_t handler; /* ISR 函数指针 */
FAR void *arg; /* 传递给 ISR 的参数 */
clock_t time; /* 最大执行时间(监控用)*/
uint32_t count; /* 触发次数 */
};

文件:sched/irq/irq_initialize.c:53-57

1
struct irq_info_s g_irqvector[NR_IRQS];

启动时所有条目初始化为 irq_unexpected_isr(未注册中断的 panic 处理)。

8.2 irq_attach():注册 ISR

文件:sched/irq/irq_attach.c:114-185

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int irq_attach(int irq, xcpt_t isr, FAR void *arg)
{
int ndx = IRQ_TO_NDX(irq);

irqstate_t flags = spin_lock_irqsave(&g_irqlock);

if (isr == NULL)
{
/* 注销:禁用中断,指向 panic 处理函数 */
up_disable_irq(irq);
isr = irq_unexpected_isr;
}

g_irqvector[ndx].handler = isr;
g_irqvector[ndx].arg = arg;

spin_unlock_irqrestore(&g_irqlock, flags);
return OK;
}

ISR 函数签名int handler(int irq, FAR void *context, FAR void *arg)

  • irq:中断号
  • context:寄存器帧指针(通常不用)
  • argirq_attach() 时传入的自定义参数

8.3 irq_dispatch():查表调用

文件:sched/irq/irq_dispatch.c:98-165

1
2
3
4
5
6
7
8
9
void irq_dispatch(int irq, FAR void *context)
{
int ndx = IRQ_TO_NDX(irq);
xcpt_t vector = g_irqvector[ndx].handler;
FAR void *arg = g_irqvector[ndx].arg;

/* 调用注册的 ISR */
vector(irq, context, arg);
}

整个分发过程就是一次数组索引 + 函数指针调用——O(1) 复杂度。


9. enter_critical_section():临界区保护

多任务系统中,共享数据可能被”任务 + 中断”或”多个 CPU”同时访问。enter_critical_section() 通过禁用中断(+ SMP 下的 spinlock)保证代码段的原子性。

9.1 ARMv7-A 实现(非 SMP)

文件:arch/arm/include/armv7-a/irq.h:367-380

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static inline irqstate_t up_irq_save(void)
{
unsigned int cpsr;
__asm__ __volatile__ (
"mrs %0, cpsr\n" /* 读当前 CPSR */
"cpsid i\n" /* 禁用 IRQ(设置 CPSR.I 位)*/
: "=r" (cpsr) : : "memory"
);
return cpsr; /* 返回修改前的 CPSR(用于恢复)*/
}

static inline void up_irq_restore(irqstate_t flags)
{
__asm__ __volatile__ (
"msr cpsr_c, %0" /* 写回 CPSR 控制位(包含 I 位)*/
: : "r" (flags) : "memory"
);
}

enter_critical_section() = up_irq_save()(禁中断)+ 记录嵌套计数。
leave_critical_section() = 减计数 + up_irq_restore()(恢复中断状态)。

9.2 与 sched_lock() 的区别

特性 enter_critical_section() sched_lock()
机制 禁用 CPU 中断(CPSR.I) 递增 lockcount 计数器
中断响应 被屏蔽 正常响应
保护范围 防止中断 + 防止切换 仅防止切换
SMP 行为 + 全局 spinlock 仅本 CPU 有效
延迟影响 增加中断响应延迟 不影响中断响应
适用场景 极短临界区(几条指令) 较长临界区(允许中断响应)

原则:能用 sched_lock() 就不用 enter_critical_section()——前者对实时性的影响更小。


10. 中断栈

如果中断使用被中断任务的栈,每个任务都必须预留足够空间给中断处理(ISR + 可能的嵌套)。这浪费内存。独立中断栈让所有中断共享一块固定大小的栈空间,任务栈只需考虑自身需求。

10.1 配置与分配

文件:arch/arm/src/armv7-a/arm_vectors.S:796-828

1
2
3
4
5
6
#if !defined(CONFIG_SMP) && CONFIG_ARCH_INTERRUPTSTACK > 7
.bss
.balign 8
g_intstackalloc:
.skip ((CONFIG_ARCH_INTERRUPTSTACK + 4) & ~7)
g_intstacktop:

在 BSS 段分配固定大小的中断栈。arm_vectorirq 入口处通过 setirqstack 宏将 SP 切换到 g_intstacktop

配置值的含义

  • CONFIG_ARCH_INTERRUPTSTACK = 0:不使用独立中断栈(中断直接用任务栈)
  • CONFIG_ARCH_INTERRUPTSTACK = 4096:分配 4KB 中断栈

实践建议:对于 ARMv7-A KERNEL 模式,推荐设置 CONFIG_ARCH_INTERRUPTSTACK >= 2048——因为 ISR 中可能调用较深的内核函数(如 sem_post()nxsched_add_readytorun())。


11. 完整中断处理流程总览

下图展示了从硬件中断触发到上下文切换完成的完整时序:

中断处理完整流程

将上面所有环节串起来:

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
42
43
44
45
Hardware IRQ asserted
|
v
[CPU] 保存 CPSR→SPSR_irq, PC→LR_irq, 切换到 IRQ 模式, 跳转向量
|
v
arm_vectorirq (assembly) arch/arm/src/armv7-a/arm_vectors.S:175
├─ sub lr, #4 修正返回地址
├─ srsdb sp!, #PSR_MODE_SYS 保存 LR+SPSR 到 SYS 栈
├─ cpsid if, #PSR_MODE_SYS 切到 SYS 模式,禁中断
├─ stmdb sp!, {r0-r12} 保存通用寄存器
├─ stmdb sp!, {r1, r14} 保存修正 SP 和 LR
├─ setirqstack 切换到中断栈
└─ bl arm_decodeirq 调用 C 函数
|
v
arm_decodeirq() arch/arm/src/armv7-a/arm_gicv2.c:395
├─ getreg32(GIC_ICCIAR) 读 GIC,获取中断 ID + ACK
├─ arm_doirq(irq, regs)
│ |
│ v
│ arm_doirq() arch/arm/src/armv7-a/arm_doirq.c:56
│ ├─ tcb->xcp.regs = regs 保存当前帧到 TCB
│ ├─ up_set_interrupt_context(true)
│ ├─ irq_dispatch(irq, regs) 查 g_irqvector[] 表,调用 ISR
│ │ └─ handler(irq, context, arg) ← 用户注册的 ISR 执行
│ │ (ISR 中可能 sem_post/信号 → 修改就绪队列)
│ ├─ tcb = this_task() ISR 返回后重新获取最高优先级任务
│ ├─ if (regs != tcb->xcp.regs) 检测上下文切换
│ │ ├─ addrenv_switch(tcb) 切换 MMU 页表(KERNEL 模式)
│ │ ├─ nxsched_switch_context() 更新调度状态
│ │ └─ regs = tcb->xcp.regs 使用新任务的帧
│ └─ return regs 返回(可能是不同任务的帧)

└─ putreg32(regval, GIC_ICCEOIR) 写 EOI,通知 GIC 处理完成
return regs
|
v
arm_vectorirq (restore path)
├─ ldmia r0, {r13, r14}^ 从返回的帧恢复 SP+LR
├─ ldmia r14!, {r0-r12} 恢复 r0-r12
└─ rfeia r14 异常返回(PC+CPSR 从帧中加载)
|
v
[CPU] 以恢复的 CPSR 运行 → 可能是原任务,也可能是新任务

12. 对比分析

特性 NuttX (ARMv7-A GICv2) Linux (ARMv7-A GICv2) FreeRTOS (Cortex-M NVIC)
中断控制器 GIC,软件配置 同,+ irqdomain 抽象 NVIC,硬件优先级
中断嵌套 默认禁用 支持(threaded IRQ) 硬件自动嵌套
ISR 注册 irq_attach(irq, handler, arg) request_irq(irq, handler, flags, name, dev) 直接写向量表
ISR 上下文 中断栈或任务栈 专用内核栈 任务栈(MSP 或 PSP)
上下文切换 中断返回时比较 regs 指针 schedule() 在 softirq/preempt 点 PendSV(延迟切换)
下半部机制 work_queue (HPWORK/LPWORK) softirq / tasklet / workqueue 无(ISR 直接处理)
EOI 时机 ISR 返回后写 EOI 硬件自动(NVIC)
ISR 函数签名 int f(int irq, void *ctx, void *arg) irqreturn_t f(int irq, void *dev_id) void f(void)

NuttX 的设计取舍

  • 无下半部——ISR 中直接完成所有工作,简单但 ISR 不能太长
  • 默认不嵌套——避免了复杂的重入问题,但可能增加高优先级中断的延迟
  • 中断级上下文切换——比 FreeRTOS 的 PendSV 更直接(不需要额外的软中断),但实现复杂度在汇编层

13. 关键要点

  1. **GIC 是 ARMv7-A 中断的”总管”**——管理几百个中断源的使能、优先级和 CPU 路由。NuttX 默认禁用中断嵌套。

  2. arm_vectorirq 汇编入口做三件事:保存寄存器到 SYS 栈帧 → 切换到中断栈 → 调用 C 函数 arm_decodeirq()

  3. 中断号从 GIC_ICCIAR 寄存器读取arm_decodeirq()),读操作本身就是 ACK;处理完后写 GIC_ICCEOIR 表示 EOI。

  4. **arm_doirq() 的核心逻辑是”比较帧指针”**——ISR 运行前保存 regs,运行后检查 this_task()->xcp.regs 是否变了。如果变了,说明调度器切换了任务,返回新帧给汇编代码。

  5. ISR 中不直接切换任务——只修改调度器状态(nxsched_add_readytorun() 等)。实际切换延迟到 arm_doirq() 返回、汇编代码恢复寄存器时生效。

  6. enter_critical_section() = cpsid i(禁 IRQ)+ SMP 下的 spinlock。比 sched_lock() 更重量级,应尽量缩短使用时间。

  7. 独立中断栈避免任务栈浪费——所有中断共享一块固定栈空间,推荐 >= 2048 字节。


14. 参考文件索引

文件路径 关键内容 引用行号
arch/arm/src/armv7-a/arm_vectors.S arm_vectorirq 汇编入口、中断栈分配 171-278, 796-828
arch/arm/src/armv7-a/arm_gicv2.c arm_decodeirq(), arm_gic0_initialize(), arm_gic_initialize() 165-429
arch/arm/src/armv7-a/arm_doirq.c arm_doirq() 上下文切换检测 56-129
arch/arm/src/armv7-a/gic.h GIC 寄存器定义、SPI/PPI/SGI ID 103-651
arch/arm/include/armv7-a/irq.h up_irq_save/restore, up_interrupt_context, 帧布局 367-494
sched/irq/irq.h struct irq_info_s, g_irqvector 声明 50-81
sched/irq/irq_initialize.c g_irqvector[] 定义和初始化 53-90
sched/irq/irq_attach.c irq_attach() ISR 注册 114-185
sched/irq/irq_dispatch.c irq_dispatch() ISR 分发 98-165
sched/irq/irq_csection.c enter/leave_critical_section() 258-463
arch/arm/src/common/arm_internal.h g_intstackalloc/g_intstacktop 声明 201-204
include/nuttx/wqueue.h work_queue() API 定义 406
sched/wqueue/kwork_queue.c work_queue() 实现 70-157
drivers/ioexpander/icjx.c ISR + worker 模式实例 1013-1094
Documentation/implementation/interrupt_controls.rst NuttX 官方中断控制文档 全文

外部参考文档:

文档 说明
ARM Architecture Reference Manual (ARMv7-A/R) 异常模型、IRQ/FIQ 模式、CPSR 位定义
ARM Generic Interrupt Controller Architecture Specification (GIC v2.0) ICCIAR/ICCEOIR 寄存器、优先级模型、Binary Point
NuttX Documentation: https://nuttx.apache.org/docs/latest/ 官方中断配置和驱动开发指南