ARMv7-A小系统实践(十一):启动CPU1,走通SCU与SGI核间通信

本章记录什么

这一章把单核可运行系统扩展为 Cortex-A9 双核启动底座。重点是共享状态和每核状态的边界:BSS、页表和 Distributor 只能由主核负责共享初始化;模式栈、GICC、PPI、CP15、TLB 和 L1 Cache 则必须由每个 CPU 分别建立。

在这一章之前做了什么

单核路径已经能够启动 MMU、Cache、GIC 和任务调度,但直接让 CPU1 执行同一入口会引入竞争:两个核可能同时清 BSS、重复初始化全局对象,或者共用一份 SVC/IRQ 栈。为此启动入口先读取 MPIDR 分流,CPU1 在 release flag 上等待,CPU0 完成发布后再用 cache clean、屏障和事件唤醒次核。

本章给出启动时序、SGI 往返验证和次核初始化代码,同时明确“第二个核在线”不等于“已经完成 SMP 调度器”。

系列目录 · 上一篇:Cache、SCU 与内存一致性

本章的阅读顺序

先读基础概念,确认每个术语解决什么问题;再看设计推导,理解实现为什么采用这种保存集合、初始化顺序或状态机;随后逐段阅读代码和执行轨迹;最后用验证方法与错误案例检查理解。文中保留寄存器名、指令名和标准缩写,叙述部分统一使用简体中文。

双核启动时序与资源归属

3. 启动与双核时序

1
2
3
4
5
6
7
8
9
QEMU/Boot loader 把 ELF 放到 0x00100000
→ 两核进入 _start,关 IRQ/FIQ,设置 VBAR
→ 根据 MPIDR 为每个模式选择 CPU0/CPU1 专属栈
→ CPU1 等待 secondary_release
→ CPU0 清 .bss,进入 kernel_main
→ CPU0 建 MMU,开 SCU/L1/L2,初始化 GIC Distributor/GICC
→ CPU0 发布 release flag,clean cache,写 Zynq kick address,SEV
→ CPU1 建自己的 TTBR/MMU/L1/ACTLR.SMP/GICC
→ 两核用 SGI0 往返确认,CPU1 随后 WFI

真实 Zynq-7000 的次核 kick address 为 0xfffffff0;QEMU 两核本来就从同一入口启动,
因此 CPU1 先在软件等待点停住。两种路径共享后续初始化,但真板仍需要由 FSBL/U-Boot
或 JTAG 保证 DDR、镜像地址、CPU1 状态和时钟满足交接合同。

从移植步骤理解 SMP 底座

十八、步骤 13:CPU1、per-CPU 栈与 SGI

两个核从同一 _start 进入时,只有 CPU0 能清 .bss、建立共享页表和初始化 GICD。
CPU1 读取 MPIDR 后立即去等待区,并使用自己的一组 banked mode stacks。

1
2
3
4
CPU0:共享初始化 → clean release → kick address → DSB → SEV
CPU1:等待 → 自己的MMU/L1/ACTLR.SMP/GICC → online → SEV
CPU0:SGI0发CPU1
CPU1:handler计数并SGI0回CPU0

GICD 是共享 Distributor;GICC、SGI/PPI banked 状态和 per-CPU IRQ stack 是每核资源。
SGI 往返只证明 IPI 底座,不证明完整 SMP scheduler。最终说明必须把“次核已经在线”和
“已经支持通用 SMP 调度”明确分开。

最终审计中的双核边界

F. Cortex-A9 双核底座(建议纳入最终轮)

这部分超过 x64 基线的真实完成度,但符合 Zynq-7020 平台事实:

  1. QEMU 改为 -smp 2
  2. _start 读取 MPIDR,CPU0/CPU1 走不同早期路径;
  3. 为每核分配 SVC/System/IRQ 等模式栈,禁止两个核共用 sp_irq
  4. CPU1 等待 CPU0 完成 .bss、页表和共享初始化;
  5. CPU0 通过 release flag + SEV 启动 CPU1;保留 Zynq 0xfffffff0 kick address 真板接口;
  6. 两核分别设置 VBAR、MMU、L1 Cache、ACTLR.SMP 和 GICC;
  7. Distributor 只由 CPU0 初始化;
  8. 注册 SGI0,完成 CPU0→CPU1→CPU0 的计数/确认;
  9. CPU1 完成验收后进入 WFI idle,CPU0 运行抢占调度。

这证明多核启动、per-CPU 栈、共享内存可见性和 IPI 通路;不等于两个核共享一个
完整调度器。真正 SMP 调度还需要 per-CPU scheduler、锁、原子操作、任务迁移和 TLB
shootdown,原 x64 mini 本身也没有这些实现。

CPU1 发布、次核初始化与 SGI 握手实现

下面给出文章对应的完整实现片段。即使没有配套仓库,也可以沿着注释、寄存器访问和调用关系逐行阅读。代码之后的分析只讨论本章主题,不要求预先掌握其他目录结构。

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include <stddef.h>
#include <stdint.h>

#include "cache.h"
#include "irq.h"
#include "mmu.h"
#include "platform.h"
#include "smp.h"
#include "smp_constants.h"

volatile uint32_t secondary_release = ZYNQ_SECONDARY_HOLD;

static volatile uint32_t cpu_online[2];
static volatile uint32_t sgi_count[2];
static volatile uint32_t secondary_replied;

extern void _start(void);

static void smp_sgi_handler(
uint32_t interrupt_id,
void *argument,
CPU_Interrupt_frame *frame
)
{
(void)interrupt_id;
(void)argument;
(void)frame;
const uint32_t cpu = arm_cpu_index();
if (cpu < 2u) {
++sgi_count[cpu];
}

if (cpu == 1u && secondary_replied == 0u) {
secondary_replied = 1u;
zynq_irq_send_sgi(ZYNQ_SGI_IRQ_ID, 1u << 0);
}
}

uint32_t zynq_smp_run_test(void)
{
cpu_online[0] = 1u;
if (zynq_irq_register(ZYNQ_SGI_IRQ_ID, smp_sgi_handler, NULL) != 0 ||
zynq_irq_enable(ZYNQ_SGI_IRQ_ID, 0x80u) != 0) {
return 0u;
}

secondary_release = ZYNQ_SECONDARY_GO;
zynq_cache_clean_range((const void *)&secondary_release, sizeof(secondary_release));
*(volatile uint32_t *)(uintptr_t)0xfffffff0u = (uint32_t)(uintptr_t)_start;
arm_data_sync_barrier();
arm_send_event();

uint32_t timeout = 20000000u;
while (cpu_online[1] == 0u && timeout-- != 0u) {
__asm__ volatile("nop");
}
if (cpu_online[1] == 0u) {
return 0u;
}

arm_enable_irq();
zynq_irq_send_sgi(ZYNQ_SGI_IRQ_ID, 1u << 1);
timeout = 20000000u;
while ((sgi_count[0] == 0u || sgi_count[1] == 0u) && timeout-- != 0u) {
arm_wait_for_event();
}
arm_disable_irq();
return sgi_count[0] != 0u && sgi_count[1] != 0u;
}

uint32_t zynq_smp_cpu_online(uint32_t cpu_index)
{
return cpu_index < 2u ? cpu_online[cpu_index] : 0u;
}

uint32_t zynq_smp_sgi_count(uint32_t cpu_index)
{
return cpu_index < 2u ? sgi_count[cpu_index] : 0u;
}

void secondary_kernel_main(void)
{
zynq_mmu_initialize_secondary();
zynq_cache_initialize_secondary();
zynq_irq_initialize_cpu_interface();
(void)zynq_irq_enable(ZYNQ_SGI_IRQ_ID, 0x80u);

cpu_online[1] = 1u;
zynq_cache_clean_range((const void *)&cpu_online[1], sizeof(cpu_online[1]));
arm_data_sync_barrier();
arm_send_event();
arm_enable_irq();

for (;;) {
arm_wait_for_interrupt();
}
}

从规则走到实际实现

下面回到本章对应的小系统实现。这里不再假定读者已经打开源码,而是把关键地址、数据结构、指令序列和返回路径直接放在文章中解释。

先把资源分成共享和每核两类

资源 归属 初始化原则
普通寄存器、CPSR、banked SP/LR 每核 每核独立设置
TTBR、TLB、SCTLR、ACTLR、L1 每核 CPU0 初始化不能替 CPU1 完成
模式栈与软件中断栈 每核内存区域 两核地址不同,不能重叠
页表内存 当前设计共享 CPU0 建表,CPU1 读取
SCU、L2C-310 共享组件 避免两核同时重复破坏性初始化
GIC Distributor 共享,部分寄存器 banked 共享部分由主核建立
GIC CPU Interface、PPI 状态 每核视图 每核设置
当前调度器数组与 current_task 目前全局单套 只供 CPU0 演示调度

最后一行很重要。两核启动成功后,当前 scheduler 仍不能被两个核同时当作独立调度器使用。全局 current_task 与 need_reschedule 没有提供 per-CPU 调度语义。

MPIDR 如何选自己的栈

_start 读取 MPIDR,再取低位作为这块双核平台的索引:

1
2
mrc p15, 0, r4, c0, c0, 5
and r4, r4, #0x3

之后每次进入一种模式,都按这个索引选择 CPU0/CPU1 的栈顶。这是针对当前平台的简化,不是可直接支持任意多簇 ARM 系统的拓扑解析。复杂系统应按 MPIDR 各级 affinity 建立映射。

两核虽然使用同一符号 _start,但应很早分流;只有 CPU0 清 BSS、创建页表和初始化共享设备。CPU1 先保持等待。

CPU1 等待循环为什么是“检查—等待—再检查”

来源:基础版 src/start.S

1
2
3
4
5
6
7
8
9
10
ldr r0, =secondary_release
ldr r2, =ZYNQ_SECONDARY_GO
.Lsecondary_wait_loop:
ldr r1, [r0]
cmp r1, r2
beq .Lsecondary_go
wfe
b .Lsecondary_wait_loop
.Lsecondary_go:
bl secondary_kernel_main

WFE 等待的是事件机制,不是“等待 flag 自动变成某个值”。真正的条件仍然是内存里的 release 值,因此唤醒后必须重新读取和检查。

SEV 可以使等待事件的 CPU 获得唤醒机会,但它不是一份携带共享数据的消息。先让共享数据可见,再发事件,才能建立有意义的协议。某些事件也可能使 WFE 提前返回,循环本身必须能处理这种情况。

CPU0 的发布顺序

src/smp.c 中的核心操作:

1
2
3
4
5
6
7
secondary_release = ZYNQ_SECONDARY_GO;
zynq_cache_clean_range((const void *)&secondary_release,
sizeof(secondary_release));
*(volatile uint32_t *)(uintptr_t)0xfffffff0u =
(uint32_t)(uintptr_t)_start;
arm_data_sync_barrier();
arm_send_event();

release flag 用于软件等待路径;0xfffffff0 是 Zynq 特定次核启动协议涉及的地址。二者不能被当成所有 ARM SoC 的通用启动方式。

当前 QEMU 的两个 CPU 进入共同软件入口,CPU1 在 release 循环停下。真实 Zynq 可能先在 BootROM 等待区,需要遵循 SoC 手册和前置固件的实际状态,再通过对应地址和事件交接。真板还要核对这一区域的映射属性、可见性以及 CPU1 是否仍处于复位。

这段代码展示了当前验证路径,但并未完成任意上游 MMU/Cache 状态下的通用次核启动。CPU1 在非缓存等待、CPU0 使用缓存的交接窗口尤其需要针对真实平台确认属性和外层可见点,不能只凭 QEMU 通过宣称所有组合可靠。

CPU1 需要重新做哪些工作

secondary_kernel_main 的顺序:

1
2
3
4
5
6
7
8
9
10
11
zynq_mmu_initialize_secondary();
zynq_cache_initialize_secondary();
zynq_irq_initialize_cpu_interface();
zynq_irq_enable(ZYNQ_SGI_IRQ_ID, 0x80u);

cpu_online[1] = 1u;
zynq_cache_clean_range((const void *)&cpu_online[1],
sizeof(cpu_online[1]));
arm_data_sync_barrier();
arm_send_event();
arm_enable_irq();

CPU1 使用已有共享页表,但独立写自己的 TTBR、DACR 和 SCTLR;设置本核 ACTLR.SMP/L1;初始化本核 GICC 与 SGI enable;最后发布 online。

online 应表示“后续允许依赖的基本条件已经成立”,不能在 CPU1 刚碰到第一条指令时就提前置位,否则 CPU0 可能过早发送中断。

共享页表和 Cache 一致性使两核能观察相同对象,但操作系统层面仍要有同步协议。当前 volatile/clean/事件组合服务于简单启动握手;它不构成一个可移植的通用锁或消息队列。

SGI0 往返怎样证明通路

CPU0 注册 SGI0 handler,等 CPU1 online 后,向目标掩码中的 CPU1 发 SGI:

1
zynq_irq_send_sgi(0u, 1u << 1);

驱动把目标位放进 GICD_SGIR 的 target list 字段。CPU1 handler 增加本核计数,并在首次收到时向 CPU0 发 SGI0:

1
2
3
4
if (cpu == 1u && secondary_replied == 0u) {
secondary_replied = 1u;
zynq_irq_send_sgi(0u, 1u << 0);
}

只回复一次是为了避免 CPU0 和 CPU1 永远互相应答。两边都观察到非零计数,证明了:

  • CPU1 已执行到自身初始化代码;
  • 每核向量/栈/GICC 基本可用;
  • Distributor 的目标路由和 SGI 投递可工作;
  • 两边 handler 都完成了分发与返回;
  • 当前验证环境中的共享状态至少在该流程上可观察。

它没有证明高并发共享结构安全,也没有证明跨核任务迁移正确。

WFI 和 WFE 不宜混着理解

CPU1 初始化结束后执行 WFI 循环,等待中断;release 循环用 WFE 配合 SEV。二者可以在更复杂电源管理中协同,但各自等待的体系结构条件不同。

还有一个调试陷阱:把计数 timeout 放在一个内部可能永久阻塞的 WFE 循环中,不一定形成有界的墙上时间超时。如果没有任何事件使 WFE 返回,计数也不会继续减。

当前 SGI 等待代码具有这种简化,不能把循环常量当成严格毫秒超时。可靠的诊断超时应由持续前进的时基控制,并确保等待机制本身能够被唤醒。

为什么不能直接让 CPU1 运行现有任务调度器

基础版 tasks/current_task/need_reschedule 都是单套全局状态;精简版还有单个 from/to 结果对象。如果 CPU0 正在切任务,CPU1 同时进入 scheduler_irq_tail,可能覆盖正在被 CPU0 汇编读取的结果。

当前流程通过用途与时序限制这个问题:SGI 启动测试在正式任务调度前完成,后续常驻任务在 CPU0 上运行,CPU1 主要等待。增加新的跨核中断或让 CPU1 参与任务执行前,必须重新划分调度状态。

完整 SMP 调度还需要:

1
2
3
4
5
6
7
per-CPU current/heir
任务是否已在另一个 CPU 执行的归属标记
就绪队列与锁
跨核重调度 IPI
任务迁移与栈所有权
TLB/地址空间更新协调
原子操作与正确的内存顺序

RTEMS 的普通 context switch 中出现 is_executing、LDREXB/STREXB、DMB 和 per-CPU 取值,就是为了覆盖其中一部分竞争条件。不能删掉这些后仍宣称保留相同的 SMP 能力。

独占访问还需要考虑切换边界

LDREX/STREX 用于构建原子更新,但独占预留并不是“跟着 C 局部变量自动保存”的状态。任务或中断切换时,要防止错误继承另一执行流的预留;RTEMS 的对应路径使用 CLREX 等机制处理。

当前小系统没有完整的原子同步子系统,IRQ 与普通切换中也没有同等覆盖。后续加入自旋锁时,必须把独占监视器、编译器约束与屏障一起纳入设计,不能只抄一个忙等循环。

双核调试时如何定位

在 GDB remote 会话中:

1
2
3
4
5
info threads
thread 1
info registers pc sp cpsr
thread 2
info registers pc sp cpsr

QEMU 通常把 CPU 显示为 remote threads。这里的 debugger thread 与小系统软件任务不是同一个概念:三个软件任务轮流在 CPU0 上执行,不会因此自动出现三个 GDB CPU thread。

若 CPU1 不 online,按下面的顺序检查:

1
2
3
4
5
6
CPU1 是否到 _start
→ 选中的模式栈地址是否属于 CPU1
→ release 是否达到预期值
→ 是否被 WFE 卡住
→ secondary_kernel_main 是否进入
→ 本核 MMU/Cache/GICC 初始化在哪里停下

若 online 但无 SGI,再检查 SGIR 目标、本核使能、CPSR.I、IAR/EOIR 与 handler 注册,不要重新把整个启动链当成未知。

多核机制的架构部分对照 Cortex-A9 MPCore TRM;Zynq 的 CPU1 启动约定对照 UG585Starting Code on CPU 1。一个讲处理器组合,另一个讲 SoC 实际交接,两者缺一不可。

下一篇:调试、验收与整体复盘

双核共用入口与每核模式栈

同一入口必须先读 MPIDR,再为每个 CPU 选择独立的 UND、ABT、IRQ、FIQ 和 SVC 栈。CPU1 随后进入等待点,不能穿过 CPU0 的共享清零路径。

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
.syntax unified
.cpu cortex-a9
.arm

#include "smp_constants.h"
#include "scheduler_offsets.h"

.section .vectors, "ax", %progbits
.align 5
.global _vectors
.type _vectors, %function
_vectors:
b _start
b undefined_handler
b svc_handler
b prefetch_abort_handler
b data_abort_handler
b reserved_handler
b irq_entry
b fiq_handler
.size _vectors, . - _vectors

.section .text.startup, "ax", %progbits
.align 4
.global _start
.type _start, %function
_start:
@ 启动期间先关 IRQ/FIQ,避免各模式栈尚未建立时进入异常。
cpsid if

@ VBAR 指向本镜像的异常向量表。
ldr r0, =_vectors
mcr p15, 0, r0, c12, c0, 0
dsb
isb

@ QEMU的两个核从同一入口启动;MPIDR.Aff0作为每核栈和路径索引。
mrc p15, 0, r4, c0, c0, 5
and r4, r4, #0x3

@ ARMv7-A 的异常模式拥有 banked SP,必须逐个切换模式后赋值。
cps #0x1b
cmp r4, #0
ldreq sp, =__stack_und_cpu0_top
ldrne sp, =__stack_und_cpu1_top

cps #0x17
cmp r4, #0
ldreq sp, =__stack_abt_cpu0_top
ldrne sp, =__stack_abt_cpu1_top

cps #0x12
cmp r4, #0
ldreq sp, =__stack_irq_cpu0_top
ldrne sp, =__stack_irq_cpu1_top

cps #0x11
cmp r4, #0
ldreq sp, =__stack_fiq_cpu0_top
ldrne sp, =__stack_fiq_cpu1_top

cps #0x13
cmp r4, #0
ldreq sp, =__stack_svc_cpu0_top
ldrne sp, =__stack_svc_cpu1_top

cmp r4, #0
bne .Lsecondary_wait

@ 裸机环境没有 C 运行库,启动代码负责清零 .bss。
ldr r0, =__bss_start
ldr r1, =__bss_end
mov r2, #0
.Lclear_bss:
cmp r0, r1
strlo r2, [r0], #4
blo .Lclear_bss

bl kernel_main

.Lhalt:
wfi
b .Lhalt

.Lsecondary_wait:
@ CPU1不能和CPU0同时清.bss或建页表,先在无Cache状态等待发布。
ldr r0, =secondary_release
ldr r2, =ZYNQ_SECONDARY_GO
.Lsecondary_wait_loop:
ldr r1, [r0]
cmp r1, r2
beq .Lsecondary_go
wfe
b .Lsecondary_wait_loop
.Lsecondary_go:
bl secondary_kernel_main
b .Lhalt
.size _start, . - _start

.macro FATAL_VECTOR name, number
.align 2
.type \name, %function
\name:
mov r1, lr
mrs r2, spsr
mov r0, #\number
b platform_exception_halt
.size \name, . - \name
.endm

FATAL_VECTOR undefined_handler, 1
FATAL_VECTOR svc_handler, 2
FATAL_VECTOR prefetch_abort_handler, 3
FATAL_VECTOR data_abort_handler, 4
FATAL_VECTOR reserved_handler, 5
FATAL_VECTOR fiq_handler, 7

.align 2
.global irq_entry
.type irq_entry, %function
irq_entry:
@ 修正PC,把PC/CPSR压入被打断任务的SVC栈
sub lr, lr, #4
srsdb sp!, #0x13 @ 把 LR_irq把 SPSR_irq压到 SP_svc 指向的栈
cps #0x13

@ 只保存caller-saved;r4-r11仅在实际任务切换时由IRQ尾部保存。
stmdb sp!, {r0-r3, r12, lr}
mov r0, sp

@ 切换到CPU专用软件IRQ栈;禁止嵌套,所以每次从栈顶开始
mrc p15, 0, r1, c0, c0, 5
and r1, r1, #0x3
cmp r1, #0
ldreq sp, =__stack_irq_cpu0_top
ldrne sp, =__stack_irq_cpu1_top
@ 把当前r0(svc下的sp) 保存到IRQ栈,返回NULL不切换
sub sp, sp, #8
str r0, [sp]
bl irq_dispatch
ldr r1, [sp]
add sp, sp, #8
cmp r0, #0
beq .Lirq_return_same

@ 任务切换:此时C调用已按AAPCS恢复当前任务的r4-r11,保存到旧任务控制块。
ldr r2, [r0, #SCHEDULER_IRQ_SWITCH_FROM]
stmia r2, {r4-r11}

@ 载入新任务的callee-saved寄存器与其caller-saved IRQ frame。
ldr r2, [r0, #SCHEDULER_IRQ_SWITCH_TO]
ldmia r2, {r4-r11}
ldr sp, [r2, #SCHEDULER_TASK_FRAME]
b .Lirq_restore_frame

.Lirq_return_same:
mov sp, r1
.Lirq_restore_frame:
ldmia sp!, {r0-r3, r12, lr}
rfefd sp!
.size irq_entry, . - irq_entry

.section .note.GNU-stack, "", %progbits

双核启动涉及的平台地址

发布次核时既要操作软件 release flag,也要理解 Zynq 的 kick address、SCU 和 GIC 地址归属。

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#ifndef ZYNQ7020_PLATFORM_H
#define ZYNQ7020_PLATFORM_H

#include <stdint.h>

#define ZYNQ_UART0_BASE 0xE0000000u
#define ZYNQ_A9_PRIVATE_PERIPHERAL_BASE 0xF8F00000u

static inline uint32_t arm_read_cpsr(void)
{
uint32_t value;
__asm__ volatile("mrs %0, cpsr" : "=r"(value));
return value;
}

static inline uint32_t arm_read_mpidr(void)
{
uint32_t value;
__asm__ volatile("mrc p15, 0, %0, c0, c0, 5" : "=r"(value));
return value;
}

static inline uint32_t arm_cpu_index(void)
{
return arm_read_mpidr() & 0x3u;
}

static inline uint32_t arm_read_sctlr(void)
{
uint32_t value;
__asm__ volatile("mrc p15, 0, %0, c1, c0, 0" : "=r"(value));
return value;
}

static inline uint32_t arm_read_dfsr(void)
{
uint32_t value;
__asm__ volatile("mrc p15, 0, %0, c5, c0, 0" : "=r"(value));
return value;
}

static inline uint32_t arm_read_ifsr(void)
{
uint32_t value;
__asm__ volatile("mrc p15, 0, %0, c5, c0, 1" : "=r"(value));
return value;
}

static inline uint32_t arm_read_dfar(void)
{
uint32_t value;
__asm__ volatile("mrc p15, 0, %0, c6, c0, 0" : "=r"(value));
return value;
}

static inline uint32_t arm_read_ifar(void)
{
uint32_t value;
__asm__ volatile("mrc p15, 0, %0, c6, c0, 2" : "=r"(value));
return value;
}

/* 读取当前模式实际可见的 SP;在 IRQ C 处理函数中读到的就是 sp_irq。 */
static inline uintptr_t arm_read_sp(void)
{
uintptr_t value;
__asm__ volatile("mov %0, sp" : "=r"(value));
return value;
}

static inline void arm_wait_for_interrupt(void)
{
__asm__ volatile("wfi" ::: "memory");
}

static inline void arm_wait_for_event(void)
{
__asm__ volatile("wfe" ::: "memory");
}

static inline void arm_send_event(void)
{
__asm__ volatile("sev" ::: "memory");
}

static inline void arm_enable_irq(void)
{
__asm__ volatile("cpsie i" ::: "memory");
}

static inline void arm_disable_irq(void)
{
__asm__ volatile("cpsid i" ::: "memory");
}

static inline void arm_data_sync_barrier(void)
{
__asm__ volatile("dsb" ::: "memory");
}

static inline void arm_instruction_sync_barrier(void)
{
__asm__ volatile("isb" ::: "memory");
}

#endif

SMP 最小接口

接口只覆盖次核发布、在线状态和 SGI 验证,因此不会给读者造成已经拥有完整 SMP 调度器的错觉。

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef ZYNQ7020_SMP_H
#define ZYNQ7020_SMP_H

#include <stdint.h>

extern volatile uint32_t secondary_release;

uint32_t zynq_smp_run_test(void);
uint32_t zynq_smp_cpu_online(uint32_t cpu_index);
uint32_t zynq_smp_sgi_count(uint32_t cpu_index);
void secondary_kernel_main(void) __attribute__((noreturn));

#endif

重点回看

双核启动的核心是区分共享初始化与每核初始化,并用明确的发布顺序交接状态。MPIDR 用于选择每核资源,release flag 配合 cache clean、DSB 和 SEV。CPU1 在线与 SGI 往返只能证明双核底座,完整 SMP 调度仍需锁、每核运行队列和迁移协议。

本章总结

双核启动的核心是区分共享初始化与每核初始化,并用明确的发布顺序交接状态。MPIDR 用于选择每核资源,release flag 配合 cache clean、DSB 和 SEV。CPU1 在线与 SGI 往返只能证明双核底座,完整 SMP 调度仍需锁、每核运行队列和迁移协议。

最后一章不再增加功能,而是整理复现、分层调试、证据和长期 Bug 经验。

ARMv7-A小系统实践(十一):启动CPU1,走通SCU与SGI核间通信

https://goko-son626.github.io/post/armv7a-10-smp.html

作者

GoKo Mell

发布于

2026-07-05

更新于

2026-07-05

许可协议

评论

:D 一言句子获取中...