Nuclei UX900FD RISC-V IP 裸机启动流程深度解析

Nuclei UX900FD RISC-V IP 裸机启动流程

最近公司在调研芯来IP,借此机会,分析一下RISC-V的裸机启动流程

基于 Nuclei SDK + evalsoc 平台 + ILM 下载模式,从复位到 main() 流程分析

1. 概述:启动流程全景图

上电/复位后,CPU 从 _startmain() 的完整流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
上电/复位


┌──────────────────────────────────────────────────┐
│ _start(汇编,复位向量) │
│ ├─ Stage 1: 关中断、核识别、GP/TP/栈初始化 │
│ ├─ Stage 2: 使能 FPU、Vector、Cache、计数器 │
│ └─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ __init_common(汇编) │
│ ├─ 搬 .text 段 │
│ ├─ 搬 .data 段 │
│ └─ 清 .bss 段 │
│ └─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ _start_premain(汇编→C 的过渡) │
│ ├─ call SystemInit() ← 芯片级初始化 │
│ └─ call __libc_init_array() ← C++全局构造 │
│ └─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ __skip_init(汇编) │
│ ├─ call __sync_harts() ← 多核同步 │
│ ├─ call _premain_init() ← 板级全面初始化 │
│ ├─ 使能 BPU │
│ └─ call main() ← 用户代码入口 │
│ └─ call _postmain_fini() ← 仿真退出 │
└──────────────────────────────────────────────────┘

2. 内存布局:ILM 链接脚本分析

2.1 物理内存映射

来自 evalsoc.memory

区域 基址 大小 用途
FLASH 0x20000000 8 MB 非易失存储,存放烧录镜像
ILM 0x80000000 64 KB Instruction Local Memory——指令紧耦合存储器
DLM 0x90000000 64 KB Data Local Memory——数据紧耦合存储器
SRAM/DDR 0xA0000000 128 KB / 256 MB 外部 RAM,视配置选择

ILM 和 DLM 是 Nuclei 核内部的紧耦合存储器,通过专用总线直连 CPU,访问延迟远低于外部 DDR/SRAM,且与 I-Cache/D-Cache 独立——这是实现确定实时性的关键硬件机制。

2.2 链接脚本 ILM 模式的内存分区

1
2
3
4
5
6
/* gcc_evalsoc_ilm.ld */
MEMORY
{
ilm (rxa!w) : ORIGIN = 0x80000000, LENGTH = 64K /* 只读+执行 */
ram (wxa!r) : ORIGIN = 0x90000000, LENGTH = 64K /* 读写+执行 */
}
链接器区域 物理对应 权限 存放内容
ROM 别名 = ILM ILM (0x80000000) R+X .init(向量表+启动代码)、.text(程序代码)、.riscv.jvt(Zcmt 跳转表)
RAM 别名 = DLM DLM (0x90000000) R+W+X .data(初始化数据+只读常数)、.tdata(TLS 数据)、.tbss/.bss(未初始化数据)、.heap.stack

2.3 关键符号定义

链接脚本定义了一系列符号,这些符号在启动汇编中被直接引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* .text 段符号——用于代码搬运和 GP 初始化 */
PROVIDE( _text_lma = LOADADDR(.text) ); /* .text 在加载映像中的地址 */
PROVIDE( _text = ADDR(.text) ); /* .text 在运行时的地址(ILM 中) */
PROVIDE( _etext = . ); /* .text 结束地址 */

/* .data 段符号——用于数据搬运 */
PROVIDE( _data_lma = LOADADDR(.data) ); /* .data 在加载映像中的地址 */
PROVIDE( _data = ADDR(.data) ); /* .data 在运行时的地址(DLM 中) */
PROVIDE( _edata = . ); /* .data 结束地址 */

/* .bss 段符号——用于清零 */
PROVIDE( __bss_start = . ); /* .bss 起始地址 */
PROVIDE( _end = . ); /* .bss 结束地址 */

/* GP/TLS/栈/堆符号 */
PROVIDE( __global_pointer$ = . + 0x800 ); /* GP 位于 .sdata 开头后 2KB 处 */
PROVIDE( __tls_base = . ); /* TLS 基址 */
PROVIDE( _sp = . ); /* 初始栈指针(DLM 顶部) */
PROVIDE( __heap_start = . ); /* 堆起始 */

__global_pointer$ 被放置在 .sdata(短数据段)起始位置偏移 0x800 处,这使得编译器能以 gp ± 2KB 范围覆盖整个 .sdata + .sbss 区域。

2.4 ILM 下载模式 vs 其他模式

模式 .text 加载地址(LMA) .text 运行地址(VMA) 是否需要搬运
ILM ILM (0x80000000) ILM (0x80000000) 不需要(LMA = VMA)
DDR DDR (0xA0000000) DDR (0xA0000000) 不需要
Flash Flash (0x20000000) ILM (0x80000000) 需要——启动时从 Flash 拷贝到 ILM
FlashXIP Flash (0x20000000) Flash (0x20000000) 不需要(直接就地执行)
SRAM SRAM (0xA0000000) SRAM (0xA0000000) 不需要

ILM 模式:烧录器直接将可执行文件下载到 ILM 和 DLM 中__init_common 检测到 _text_lma == _text,就会跳过 .text 搬运步骤。但 .data 段仍然需要从加载地址搬运到运行地址(DLM)。


3. Stage 1:复位向量 _start——CPU 的第一条指令

3.1 入口点

1
2
3
4
5
.section .text.init          /* 放入 .init 段——链接脚本中排在第一 */
.globl _start
.type _start, @function

_start:

链接脚本通过 ENTRY(_start) 指定入口点,CPU 复位后第一条指令就是 _start 的第一条汇编指令

3.2 关中断

1
csrc CSR_MSTATUS, MSTATUS_MIE    /* 清零 mstatus.MIE 位,全局关中断 */

RISC-V 的 mstatus.MIE(Machine Interrupt Enable,bit 3)是机器模式中断总开关。启动阶段中断系统尚未初始化,任何中断到达都会导致不可预测行为。

3.3 启动核(Boot Hart)选择

1
2
3
4
5
6
#ifndef SMP_CPU_CNT                /* 单核模式下进入此分支 */
csrr a0, CSR_MHARTID /* 读当前核的 HartID */
andi a0, a0, 0xFF /* 取低 8 位(集群内核编号) */
li a1, BOOT_HARTID /* 默认 BOOT_HARTID = 0 */
bne a0, a1, __amp_wait /* 非启动核 → 跳转到 WFI 死循环 */
#endif

在多核(SMP)SoC 中,所有核同时从 _start 开始执行。但只需一个核(通常为 Hart 0)负责初始化——其他核应立即进入低功耗等待状态:

1
2
3
__amp_wait:
wfi /* Wait For Interrupt——进入低功耗暂停 */
j __amp_wait /* 被唤醒后继续等待(不死不休) */

从核被 __amp_wait 卡住,直到主核完成初始化后在 __sync_harts() 中通过写 MSIP 寄存器唤醒它们。

3.4 初始化 GP、TP 和 Zcmt 跳转表

1
2
3
4
5
6
7
8
9
.option push
.option norelax
la gp, __global_pointer$ /* 初始化全局指针寄存器 x3 */
la tp, __tls_base /* 初始化线程指针寄存器 x4 */
#if defined(__riscv_zcmt)
la t0, __jvt_base$
csrw CSR_JVT, t0 /* 设置 Zcmt 跳转向量表 CSR */
#endif
.option pop

GP(Global Pointer,x3)

__global_pointer$ 位于 .sdata 段中间偏移 0x800 处。对在 gp ± 2KB 范围内的变量,编译器生成单条 ld rd, offset(gp) 指令代替常规的 lui + addi + ld 三指令序列。这是一种代码尺寸 + 性能的双重优化,在嵌入式领域非常关键。

对比:

1
2
3
4
5
6
; 普通寻址(2条指令 + 1条访存)
lui t0, %hi(system_clock) ; 加载高 20 位
lw a0, %lo(system_clock)(t0) ; 加上低 12 位后读取

; GP-relative 寻址(1条访存指令)
lw a0, %gp_rel(system_clock)(gp)

.option norelax 防止汇编器将 la 伪指令优化为 GP-relative 形式——因为此时 GP 本身尚未初始化,必须先做绝对加载。

TP(Thread Pointer,x4)

__tls_base 指向 TLS(Thread Local Storage)段基址。TLS 用于支持 RTOS 下的线程私有变量(如 errno)。在裸机单任务场景下 TP 虽暂时不用,但初始化它保证进入 RTOS 后上下文切换机制正确——这是一种前瞻性设计

Zcmt(Compressed Jump Table for Multi-call)

Zcmt 是 RISC-V Zc* 代码密度扩展族的一员。它引入 cm.jt / cm.jalt 两条 16 位指令,通过查询 CSR_JVT 指向的跳转表完成间接函数调用。

1
2
3
4
5
; 无 Zcmt:间接调用需要 ~4 字节(c.jalr)或 8 字节(jalr)
jalr ra, t0, 0

; 有 Zcmt:1 条 16 位指令
cm.jalt 5 ; 调用跳转表中第 5 号函数

3.5 初始化栈指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 单核:直接设置 sp = _sp */
la sp, _sp

/* 多核:为每个核分配独立的栈区间(每个核 __STACK_SIZE 字节) */
#if defined(SMP_CPU_CNT) && (SMP_CPU_CNT > 1)
lui t0, %hi(__STACK_SIZE)
addi t0, t0, %lo(__STACK_SIZE) /* t0 = 每个核的栈大小(2KB) */
la sp, _sp /* sp = 栈顶 */
csrr a0, CSR_MHARTID
andi a0, a0, 0xFF
li a1, 0
1: beq a0, a1, 2f /* 找到自己的核编号→退出 */
sub sp, sp, t0 /* sp 下移一个栈区间 */
addi a1, a1, 1
j 1b
2:

_sp 在链接脚本中被定义为 DLM 顶部地址。

3.6 配置 NMI 共享与 Zc 硬件使能

1
2
3
4
5
6
7
8
9
10
11
/* 配置 NMI 基址与 mtvec 共享 */
li t0, MMISC_CTL_NMI_CAUSE_FFF /* bit 9 */
csrs CSR_MMISC_CTL, t0

/* 使能或禁用 Zc 硬件 */
li t0, MMISC_CTL_ZC
#if defined(__riscv_zcmp) || defined(__riscv_zcmt)
csrs CSR_MMISC_CTL, t0 /* 有 Zc 指令→打开硬件 */
#else
csrc CSR_MMISC_CTL, t0 /* 无 Zc 指令→关掉省电 */
#endif

CSR_MMISC_CTL 是 Nuclei 自定义的机器模式杂项控制寄存器。NMI 共享设计让 mnvecmtvec 共享同一入口值,启动阶段不需要维护两套入口。后续 _premain_init() 中的 Exception_Init() 会重新设置完整的异常处理体系。

3.7 设置早期异常入口

1
2
la t0, early_exc_entry
csrw CSR_MTVEC, t0
1
2
3
4
5
.align 6
.global early_exc_entry
early_exc_entry:
wfi
j early_exc_entry

early_exc_entry 是一个占位异常处理器——任何启动阶段的异常都会导致 CPU 进入 WFI 暂停 + 自旋死循环。正常启动过程不应触发任何异常,如果停在这里说明有严重的早期初始化错误(如 FPU 未使能就执行浮点指令)。


4. Stage 2:FPU、Vector、Cache 与计数器使能

4.1 使能浮点单元(FPU)

1
2
3
4
5
6
#if defined(__riscv_flen) && __riscv_flen > 0
li t0, MSTATUS_FS /* FS 域掩码:mstatus bits[14:13] */
csrc mstatus, t0 /* FS 清零 → Off */
li t0, MSTATUS_FS_INITIAL /* FS = 0b01 → Initial */
csrs mstatus, t0
#endif

RISC-V mstatus.FS 域控制浮点单元的状态,有四种取值:

FS 值 名称 含义
0b00 Off FPU 关闭——执行浮点指令触发非法指令异常
0b01 Initial FPU 使能,但寄存器内容为初始值(未被修改)
0b10 Clean 使能,寄存器内容与内存保存副本一致
0b11 Dirty 使能,且寄存器已被修改

先清后设的安全序列确保无论复位值是什么,FS 最终精确等于 0b01(Initial)。使用 Initial 而非 Dirty 有一个关键优势:在 RTOS 上下文切换时,处于 Initial 状态的线程不需要保存/恢复 32 个浮点寄存器(256 字节),减少了切换开销。

4.2 使能向量扩展(V 扩展)

1
2
3
4
5
6
#if defined(__riscv_vector)
li t0, MSTATUS_VS /* VS 域掩码:mstatus bits[16:15] */
csrc mstatus, t0 /* VS 清零 */
li t0, MSTATUS_VS_INITIAL /* VS = 0b01 */
csrs mstatus, t0
#endif

完全对称于 FPU 使能逻辑,控制的是向量单元。

4.3 使能 I/D Cache

1
2
3
4
5
6
7
#ifdef CFG_HAS_ICACHE
csrsi CSR_MCACHE_CTL, MCACHE_CTL_IC_EN /* 使能 I-Cache */
#endif
#ifdef CFG_HAS_DCACHE
li t0, MCACHE_CTL_DC_EN
csrs CSR_MCACHE_CTL, t0 /* 使能 D-Cache */
#endif

Cache 使能通过 Nuclei 自定义 CSR CSR_MCACHE_CTL 控制。Stage 2 的 Cache 使能是早期粗粒度使能,仅限单核模式。SMP 模式下的 Cache 使能被推迟到 _premain_init() 中。

4.4 启用性能计数器

1
csrci CSR_MCOUNTINHIBIT, 0x5      /* 清除 MHPM_CTL bit 0 和 bit 2 */

清零 bit 0 使能 cycle 计数,清零 bit 2 使能指令计数——这是后续 get_cpu_freq() 测量 CPU 频率的基础。


5. Stage 3:__init_common——搬 .text、搬 .data、清 .bss

这个函数做的是所有 C 程序运行前最基本的内存初始化。它不是通过 call 调用的,而是 _start 执行完毕后通过代码顺序执行自然落入的(_start__init_common 处于同一个 .text.init 段中)。

搬 .text(代码段)

1
2
3
4
5
6
7
8
9
10
11
12
13
__init_common:
/* ===== Step 1: 搬 .text(代码段) ===== */
la a0, _text_lma /* a0 = LMA(加载地址) */
la a1, _text /* a1 = VMA(运行地址) */
beq a0, a1, 2f /* LMA == VMA → 不需要搬运 */
la a2, _etext /* a2 = 结束地址 */
bgeu a1, a2, 2f /* 长度为零 → 跳过 */
1: lw t0, (a0) /* 从 LMA 读 4 字节 */
sw t0, (a1) /* 写入 VMA */
addi a0, a0, 4
addi a1, a1, 4
bltu a1, a2, 1b
fence.i /* 指令屏障:刷 I-Cache */

RISC-V 的 I-Cache 和 D-Cache 是分离的。sw 指令通过数据通路写入了内存,但 CPU 的取指单元可能仍然在 I-Cache 中缓存了旧数据。fence.i 保证:所有之前的数据写入对指令取指可见,且 I-Cache 被刷新。

不执行 fence.i 的后果:CPU 可能从旧地址取指,执行到垃圾代码。

使用 4 字节传输而非 8 字节,是为了同时兼容 RV32 和 RV64——当 __riscv_xlen == 32 时,ld/sd 指令非法。

搬 .data(已初始化数据段)

1
2
3
4
5
6
7
8
9
10
11
/* ===== Step 2: 搬 .data(已初始化数据段) ===== */
2: la a0, _data_lma
la a1, _data
beq a0, a1, 2f
la a2, _edata
bgeu a1, a2, 2f
1: lw t0, (a0)
sw t0, (a1)
addi a0, a0, 4
addi a1, a1, 4
bltu a1, a2, 1b

搬运 .data 段是因为 C 语言要求在 main() 之前所有已初始化全局变量的值必须可用。在嵌入式系统中,这些初始值存储在非易失介质(Flash)中,变量本身位于可写 RAM 中:

1
2
3
4
5
Flash 0x20000000:  [代码][.data 初始值: x=42, y=100, ...]

__init_common 搬运

DLM 0x90000000: [.data: x=42, y=100, ...]

清 .bss(未初始化数据段)

1
2
3
4
5
6
7
8
/* ===== Step 3: 清 .bss(未初始化数据段) ===== */
2: la a0, __bss_start
la a1, _end
bgeu a0, a1, 2f
1: sw zero, (a0)
addi a0, a0, 4
bltu a0, a1, 1b
2:

C 标准要求所有未显式初始化的全局变量在 main() 前被清零。

内存操作全景

以 ILM 下载模式为例,三个阶段完成后的内存状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ILM (0x80000000):
[.vtable] ← 中断向量表
[.text.init] ← _start, __init_common
[.text] ← 所有程序代码
[.riscv.jvt] ← Zcmt 跳转表

DLM (0x90000000):
[.data] ← 初始化的全局变量 (从 LMA 搬运来)
[.sdata] ← 短数据全局变量
[.rodata] ← 只读常量 (被放置在 RAM 中以加速访问)
[.tdata] ← TLS 初始数据
[.tbss/.bss] ← 清零后的未初始化变量
[.heap] ← 堆空间 (至少 2KB)
[.stack] ← 栈空间 (至少 2KB)

6. Stage 4:_start_premain——SystemInit 与 C++ 构造

1
2
3
_start_premain:
call SystemInit /* 芯片级早期初始化 */
call __libc_init_array /* C++ 全局构造 + _init */

6.1 SystemInit()

1
2
3
4
5
6
7
void SystemInit(void)
{
/* ToDo: add code to initialize the system
* Warn: do not use global variables because this function is called before
* reaching pre-main. RW section maybe overwritten afterwards.
*/
}

当前是一个空壳实现,应在此填充 PLL 配置、外部时钟源初始化、Flash 控制器时序配置等。由于 __libc_init_array 可能在内部重写 .data/.bss 段,SystemInit() 中只能做纯寄存器操作

6.2 __libc_init_array()

此函数来自 GCC 工具链的 libc_nano.a。它依次执行:_init() → 遍历 .init_array 段(C++ 全局对象构造发生在这里) → 设置 atexit 处理链。


7. Stage 5:__skip_init——多核同步与 _premain_init

1
2
3
4
5
6
7
8
9
10
11
__skip_init:
call __sync_harts /* 多核同步屏障 */
call _premain_init /* 板级全面初始化 */
/* 使能 BPU(分支预测单元) */
li t0, MMISC_CTL_BPU
csrs CSR_MMISC_CTL, t0
/* 调用 main */
li a0, 0 /* argc = 0 */
li a1, 0 /* argv = NULL */
call main
call _postmain_fini

7.1 __sync_harts()——多核同步屏障

通过 CLINT 的 MSIP(Machine Software Interrupt Pending)寄存器实现核间同步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
时间线:

主核:搬运 .text → 搬运 .data → 清 .bss → __libc_init_array


__sync_harts
│ 使能 SMP/L2
│ 等待 MSIP[1..n]=1
│ ↑
│ │
从核1:__amp_wait(WFI) → 被 MSIP 唤醒 → __sync_harts
│ 置位 MSIP[1]=1
│ 等待 MSIP[1]=0
│ ↓
主核: MSIP[1]=1 收到 ←───────┘
│ 清除所有 MSIP
│ 继续执行 _premain_init → 从核被释放,继续执行

主核负责所有的内存初始化,从核在这些共享资源准备好之前不能进入 _premain_init

7.2 _premain_init()——真正的板级初始化核心

主核执行路径

  • 配置 ILM/DLM(使能/禁用 + ECC 开关)
  • 配置 I-Cache / D-Cache(运行时检测 + ECC + 使能)
  • 探测 CPU IRegion 基址,使能全局预取
  • 配置 L2 Cache / BPU
  • get_system_clock() 测量/读取 CPU 频率
  • uart_init() 串口初始化
  • Exception_Init() 初始化异常处理表
  • Interrupt_Init() 初始化中断控制器(ECLIC/PLIC/CLINT)

从核执行路径

  • 自旋等待主核设置好 CpuIRegionBase
  • 仅调用 Interrupt_Init()

get_system_clock()——频率测量

1
2
3
4
5
6
7
static uint32_t get_system_clock(void)
{
if (__RV_CSR_READ(CSR_MCYCLE) == 0) {
return SYSTEM_CLOCK; // 默认 16 MHz
}
return get_cpu_freq(); // 实际测量
}

Exception_Init()——异常处理框架:初始化长度为 27 的异常处理函数指针表,全部指向 system_default_exception_handler。同时设置 CSR_MSCRATCH 为栈顶地址。

Interrupt_Init()——中断控制器初始化:根据 SoC 配置选择 ECLIC/PLIC/CLINT。默认使用 ECLIC(Enhanced Core Local Interrupt Controller):

1
2
3
4
5
6
7
ECLIC 模式的异常/中断入口切换:

启动早期: MTVEC = early_exc_entry ← 简单 WFI 死循环
_premain_init 后:
MTVEC = exc_entry | 0x3 ← 完整异常入口(CLIC 模式)
MTVT = vector_base ← 向量中断表
MTVT2 = irq_entry ← 非向量中断入口

exc_entry | 0x3 中最低两位表示使用 CLIC 向量中断模式——CPU 硬件会根据中断号自动从 mtvt 指向的向量表中加载中断服务例程,无需软件分发。


8. Stage 6:main——用户应用程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(void)
{
printf("Core Running at 0x%lx, Current Hart ID: %d\r\n",
__RV_CSR_READ(CSR_MARCHID), __get_hart_id());
printf("CPU Feature Probe Start\r\n");

/* 读取关键 CSR */
unsigned long marchid = __RV_CSR_READ(CSR_MARCHID);
unsigned long mimpid = __RV_CSR_READ(CSR_MIMPID);
unsigned long misa = __RV_CSR_READ(CSR_MISA);
unsigned long mhartid = __RV_CSR_READ(CSR_MHARTID);

/* 探测 CPU 特性并打印 */
show_cpuinfo();
...
}

main() 执行时,以下环境已经就绪:

资源 状态
栈指针 SP 指向 DLM 顶部,可用
全局指针 GP .sdata GP-relative 寻址可用
线程指针 TP TLS 基址已设(RTOS 准备)
I-Cache / D-Cache 已使能
FPU / Vector 已使能,状态为 Initial
.text / .data / .bss 已搬运初始化
C++ 全局对象 已构造
异常处理表 所有异常有默认 handler
中断控制器 ECLIC/PLIC/CLINT 已配置
UART 串口 已初始化(115200 bps)
系统时钟 SystemCoreClock 已设置
L2 Cache / BPU 已使能
多核同步 所有核已到达同步点

9. Stage 7:_postmain_fini——善后与仿真退出

1
2
3
4
5
6
7
8
9
void _postmain_fini(int status)
{
#if defined(CODESIZE) && (CODESIZE == 1)
SIMULATION_EXIT(status);
#else
extern void simulation_exit(int status);
simulation_exit(status);
#endif
}

main() 返回时执行 _postmain_fini()。在仿真场景中通过 UART 写特定值通知仿真器终止;在真实硬件上,此函数应做适当的系统关断处理。


10. 完整调用链

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
_start (startup_evalsoc.S:157)
│ 关中断、核识别
│ GP/TP/Zcmt 初始化
│ SP 初始化
│ NMI 共享 / Zc 硬件开关
│ MTVEC = early_exc_entry
│ FPU 使能、Vector 使能
│ Cache 使能(单核模式)
│ mcycle/minstret 使能

├─→ __init_common (startup_evalsoc.S:289)
│ ├─ 搬 .text (fence.i)
│ ├─ 搬 .data
│ └─ 清 .bss

├─→ _start_premain (startup_evalsoc.S:342)
│ ├─ call SystemInit() ← system_evalsoc.c:217
│ └─ call __libc_init_array() ← GCC libc_nano.a

├─→ __skip_init (startup_evalsoc.S:363)
│ ├─ call __sync_harts() ← system_evalsoc.c:1316
│ ├─ call _premain_init() ← system_evalsoc.c:1411
│ │ ├─ ILM/DLM 配置
│ │ ├─ I/D Cache 配置/使能
│ │ ├─ 读 CpuIRegionBase
│ │ ├─ L2 Cache / BPU 使能
│ │ ├─ [主核] get_system_clock()
│ │ ├─ [主核] uart_init()
│ │ ├─ [主核] SystemBannerPrint()
│ │ ├─ [主核] Exception_Init()
│ │ └─ [主核] Interrupt_Init()
│ │ └─ ECLIC_Interrupt_Init()
│ │ ├─ MTVT = vector_base
│ │ ├─ MTVT2 = irq_entry
│ │ ├─ MTVEC = exc_entry | 3
│ │ └─ ECLIC 配置
│ ├─ BPU 使能
│ └─ call main() ← application/main.c

└─→ call _postmain_fini() ← system_evalsoc.c:1623

如果觉得本文有帮助,欢迎关注公众号「菠菜的碎碎念」,获取更多技术干货。

公众号二维码