05_启动流程_从Reset到Main
系列说明:本文是阿栈的《 GHS MULTI 实战系列》第 05 期,也是全系列信息密度最高的一篇。
上期回顾:《04_链接脚本(下)_SECTIONS与自定义段》
摘要
"上电就跑飞"是嵌入式最经典、也最难查的问题——因为你写的 main() 一行都还没执行,所有 printf、所有断点、所有日志都不存在。
要查这类问题,必须知道从复位向量到 main 之间那段代码干了什么。它不是编译器"自动帮你搞定"的魔法,它有明确的名字、位置和执行顺序:
RESET → 取入口 → crt0(汇编)→ __lowinit / __ghs_board_memory_init
→ __ghs_ind_crt0(C)→ __ghs_ind_crt1(C)→ main → exit

本篇把这条链逐段拆到源码级,重点讲清六件事:
- 入口点到底是谁——
_start?_start_T?还是零地址?有个优先级表; crt0顶部那堆.weak是钩子——可以接管,但接管失败不会报错;.bss清零和.data拷贝不是硬编码循环,是查表,而且不是一张表,是三对表;PIC/PID/PIR三个基址是干什么的,以及baseptrs怎么从汇编传到 C;ind_crt1.c是一张弱函数清单——你可以在main之前插代码,不用改官方文件;- 启动要多久、跑飞了怎么五段定位——把前面所有原理变成可执行动作。
关键词:GHS 启动文件、crt0、ind_crt0、ind_crt1、__lowinit、__ghs_board_memory_init、BSS 清零、ROM 到 RAM 拷贝、弱函数、-e 入口、PIC base。
一、先建立分层:两层,各自能改到什么程度
| 层 | 文件 | 位置 | 特点 | 能不能改 |
|---|---|---|---|---|
| 架构相关层 | startup*.850(V850/RH850)、crt0.arm、crt0.ppc | libstartup.a | 汇编,设默认寄存器、算 PIC/PID 指针、装向量表 | 基本不改。Renesas 明确要求 RESET 之后直接用他们提供的启动代码 |
| 架构无关层 | ind_crt0.c、ind_crt1.c | libstartup.a / libsys.a | C,按段初始化内存、搭 C 运行环境 | 可以改,而且扩展时应该改这一层 |
一句很实在的评语:
由于
_start是用汇编写的,没有太多优化代码大小或执行速度的余地。
换句话说:别指望优化启动代码来省 Flash,要省就往别处省。
💡 第 02 期讲过:建 Program 时勾了
startup/system,这两个库的源码会落到你工程目录里,可以读可以改;不勾就只能用二进制库。想跟着本篇读源码,先去确认你当初勾了没有。
顺便说清一个边界:ind_crt0.c 在 libstartup.a,ind_crt1.c 在 libsys.a。跨库的——它决定了"你改哪个库能生效"。
二、RESET 之后,硬件先做了什么
在讨论 _start 之前,有一层更靠前:复位那一刻,CPU 是怎么找到第一行代码的?
答案是"硬件固定取址",你改不了:
| 架构 | 复位后从哪取入口 | 说明 |
|---|---|---|
| RH850 / V850 | 地址 0x00000000 | 该地址存放复位处理函数的入口地址。链接脚本 flash_rsvd1 : ORIGIN = 0x00000000 就是这条线的证据 |
| Cortex-M(ARM) | 地址 0x00000000 取初始 SP,0x00000004 取复位向量 | 第一项是栈指针不是代码,这是 Cortex-M 与其它架构最大的差异 |
| PowerPC(MPC57xx) | Boot Header(RCHW)指定 | 多了"选引导核"这一步,第 12 节展开 |
【RH850 / V850】只需一次取址
0x00000000 ┌────────────────────────┐
│ 复位入口地址 │ ← 硬件取出来,直接跳过去
0x00000004 ├────────────────────────┤
│ (向量区继续) │
└────────────────────────┘
【Cortex-M】需要两次:先装栈,再跳
0x00000000 ┌────────────────────────┐
│ 初始 SP 值 │ ← 硬件取出来装进 sp
0x00000004 ├────────────────────────┤
│ 复位向量 │ ← 硬件取出来跳过去
└────────────────────────┘
两种模型有个实际后果:Cortex-M 复位后栈指针是硬件装好的(从地址 0 取),所以它的 crt0 里有"没找到 MULTI 就自己设栈"的分支;RH850 则由 startup*.850 显式装 sp。这也解释了为什么两份启动代码的"设栈"位置不一样——不是风格差异,是硬件差异。
⚠️ 本篇后面讲"入口点优先级"时说的"零地址兜底",和这张表是同一件事的两面:链接器把 ELF 的
e_entry设成 0,调试器按e_entry启动,于是又跳回了复位向量——你看到的现象是"程序一启动就回到最开始",像反复重启,而不是停在某一行。
三、入口点:第一条指令到底从哪来
一张优先级表,非常好用。前提是"没有特别指定 entry point":
| 优先级 | 符号 | 说明 |
|---|---|---|
| 1 | _start | GHS 标准入口 |
| 2 | start | 老式写法 |
| 3 | _main | — |
| 4 | main | 没有启动文件时直接进 main(几乎只用于特殊实验) |
| 5 | 零地址 | 前面都没有时,去地址 0 |
两个特殊情况:
- 只支持 Thumb 指令的架构(Cortex-M)默认入口是
_start_T,不是_start——因为它们复位后不在 ARM 态; - 要自定义入口:
Set Build Options → All Options → linker → **start and end files**,或在链接脚本里写OPTION("-e Reset_Handler")。两者只能选一个,同时写会冲突。
图 1|入口点选择顺序。"零地址"是最后的兜底,也是"程序跑飞"的常见结局——结合第二节,它意味着回到复位向量。
怎么查当前工程用的是哪个入口(比猜快得多):看 map 文件里是否有 _start 这一行符号、或者用 gdump/nm 类工具看 ELF 入口。符号在、但没被用作入口,说明有人用 -e 覆盖了。
四、crt0 逐段解剖(以 ARM 版为例)
下面这段 crt0.arm,我按执行顺序切成七段。RH850 的 crt0.850 结构同源:弱符号清单、检测 MULTI、__ghs_board_memory_init、跳到 ind_crt0 这几步一样,只是寄存器名换成 r6/r7,开 FPU 换成设 FPCSR。
4.1 第一段:一堆 .weak——这就是预留的钩子
.globl _start_T
_start_T:
.need exit
.need __dotsyscall
文件头部还有一长串:
.weak __ghsbegin_picbase
.weak __ghsbegin_robase
.weak __ghsbegin_pidbase
.weak __ghs_rambootcodestart
.weak __ghs_rambootcodeend
.weak __ghs_rombootcodestart
.weak __ghs_rombootcodeend
.weak __ghsbegin_tracebuf
.weak __ghs_do_fp_init
.weak __ghs_board_memory_init
.weak __ghs_initial_dotsyscall
这一堆 .weak 就是启动代码的可插拔接口。 这些符号在链接脚本里可能不存在(比如你没有 .tracebuf 段),弱声明让汇编能编过;你在自己的 C/汇编里定义同名强符号,就自动接管。
顺带解释 .need 指令:它的意思是"即使看起来没用到,也必须把这两个符号链进来"。exit 和 __dotsyscall 是启动代码的硬依赖,.need 保证了这一点。
💡 阿栈点评:这解释了第 04 期那个
__ghs_rombootcodestart为什么"建议不要删"。它在这里被.weak引用,链接脚本里彻底没有这个符号时会取到 0——而启动代码用它算 boot code 范围。取到 0 不报错,只是算错。
4.2 第二段:保存现场——argc/argv/envp 的两条命运
; Save argc, argv, envp (if not under Multi, these are meaningless)
mov r6, r0
mov r7, r1
mov r8, r2
注释里那句括号很关键:“if not under Multi, these are meaningless”。
R0/R1/R2 里的东西是调试器传进来的(MULTI 能把命令行参数传给目标)。脱机运行时这三个寄存器里是垃圾。所以启动代码把它们先存到 r6/r7/r8,等确认"上面是不是 MULTI"之后再决定用不用:
| 情况 | 后续动作 | main 收到的参数 |
|---|---|---|
| 在 MULTI 下运行 | 把 r6/r7/r8 还原到 r1/r2/r3 | 调试器传的真实参数 |
| 脱机运行 | 走 setupregs,把 r1/r2/r3 清 0 | argc = 0 |
然后这个 argc = 0 会被 ind_crt1 接手修掉——第八节 8.2 讲。这条链跨了三个文件(crt0.arm → ind_crt0.c → ind_crt1.c),是"阅读启动代码必须连着看"的最好例子。
4.3 第三段:__ghs_board_memory_init——第一个"静默跳过"
; initialize SDRAM a chance to initialize if we're running from flash
; NOTE: elxr will jump to the next instruction if no __ghs_board_memory_init
bl __ghs_board_memory_init
注释里那句 NOTE 是全篇最该划重点的一行:
elxr will jump to the next instruction if no
__ghs_board_memory_init
翻译:如果你没定义这个函数,链接器(elxr)就让这条 bl 变成"跳到下一条指令"——不报错、不警告、无事发生。
说明:
__ghs_board_memory_init是一个弱定义函数,用户可以根据需要自己完成该函数,建议使用汇编。推荐该函数实现设置中断向量表和启动前设置操作。如果用户没有在外部定义该函数,此句为空将跳过。
这和 RH850 上的 __lowinit 是完全同一类坑:一个"可选钩子",没实现就静默跳过,而它负责的往往是"时钟 / 向量表 / SDRAM / ECC"这种没配也能跑、但跑得不对的东西。
| 钩子 | 架构 | 该干的事 | 没实现的表现 |
|---|---|---|---|
__lowinit | RH850 / V850 | 时钟树、关键 SFR | 跑在默认时钟上,波特率/定时全错 |
__ghs_board_memory_init | ARM / PowerPC | 装向量表(VTOR)、SDRAM/ECC 初始化 | 向量表指向默认位置;从 RAM 镜像启动时中断全飞 |
__ghs_board_devices_init | 通用(C) | 关键寄存器、默认运行时钟 | 外设时钟没开 |
__ghs_do_fp_init | 部分架构 | 浮点单元初始化 | 浮点指令异常或结果错 |
4.4 第四段:SYSCALL_HELLO——为什么 printf 只在调试器里有输出
mov r0, SYSCALL_HELLO ; Set call type
mov r1, 1 ; Another argument
bl __ghs_initial_dotsyscall
...
cmp r0, 0 ; Did we find Multi?
bne setupregs ; 没找到 Multi → 走 setupregs(自己设栈)
解读:
此段代码是设置了系统调用(
bl __ghs_initial_dotsyscall)并检查是否运行了 MULTI 的调试器,如果使用第三方调试器,将进入到setupregs代码段,否则继续执行,还原最开始的保存现场的值。
这就是"调试器里 printf 有输出、脱机一点没有"的机制级解释:SYSCALL_HELLO 在问"上面是不是 MULTI?"。是 → 走调试器的 syscall 通道(半主机);不是 → 自己设栈、清参数,printf 的输出没有去处。
要注意这个判断的返回值还决定了另一件事:栈指针要不要自己装。也就是说"MULTI 在不在"这一个标志,同时决定了"参数从哪来"和"栈从哪来"。
⚠️ 裸机上
printf想真正输出,必须重定向到串口(通常实现__gh_iob_init或改写底层write)。靠调试器通道只是开发期便利,不是产品能力。
4.5 第五段:picbase = 实际地址 − 链接时地址
; picbias = actualaddress - linktime address
; 4 is added to the linktime address, to account for the extra 4 added
; to the PC (r15) when read, due to the pipeline
ldr r5, pool_myaddr
sub r5, r5, 1 ; unset the low order bit
.Lpicbase:
myaddr:
mov r2, pc
sub r5, r2, r5 ; Now r5 contains picbase
三个细节:
| 细节 | 含义 |
|---|---|
sub r5, r5, 1 注释 “unset the low order bit” | Thumb 地址的最低位是模式标志(1 = Thumb),清掉它才是真实地址 |
mov r2, pc | 读当前 PC,得到运行时的真实地址 |
sub r5, r2, r5 | 真实地址 − 链接时地址 = 偏移量(picbase) |
为什么需要这个偏移:如果你的程序"链接在 A 地址、实际跑在 B 地址"(Bootloader 跳到 App、RAM 镜像、重定位加载都是这种情况),那么所有"链接时算好的绝对地址"全都不对,必须整体加一个偏移。这个偏移就是 picbase。
💡 阿栈点评:这段是 Bootloader 开发者最该读懂的一段。"App 链接在 0x00010000,Bootloader 把它搬到 0x00020000 跑"这种设计,能不能跑取决于 picbase 算得对不对。 算错了的表现是:代码能执行(相对跳转没问题),但一读全局变量就错(绝对地址没修正)。
4.6 第六段:设栈、开 FPU、初始化全局寄存器
setupregs:
ldr r0, pool_stack
mov sp, r0
mov r1, 0 ; Clear argc
mov r2, 0 ; Clear argv
mov r3, 0 ; Clear envp
regsokay:
movw r0, 0xED88 ; CPACR at 0xE000ED88
movt r0, 0xE000
ldr r4, [r0]
orr r4, r4, #(15<<20) ; 置位 20-23,使能 CP10/CP11
str r4, [r0]
三个动作:
| 动作 | 意义 | RH850 对应 |
|---|---|---|
ldr r0, pool_stack; mov sp, r0 | 从链接脚本给的 __ghsend_stack 装 SP | startup*.850 里装 sp |
CPACR 置位 | 使能协处理器 CP10/CP11(浮点) | 改 FPCSR / 使能 FPU |
mov r1/r2/r3, 0 | 脱机时把参数清零 | 同理 |
对 FPU 一句直白的说明:“以上代码为设置 FPU,用户可以按照需求进行删改。” 也就是说它不是不可动的骨架——不用浮点的工程,这段可以删;用了浮点但想自己控制的,可以改。
关于栈,有一个常见误解要澄清:启动代码不"初始化"栈,只是把 SP 指向 __ghsend_stack。栈里的内容是上电残留值,不清零。所以"栈上未初始化的局部变量是随机值"这件事,是设计如此,不是 bug。第 04 期说 .stack 用 PAD 不用 CLEAR,这里是它的执行侧印证。
4.7 第七段:把 baseptrs 交给 C(这一段最容易被漏掉)
/* VFP && __THUMB2_AWARE */
ldr r0, pool_baseptrs ; Could use add r0, pc, offset if assembler would take it
add r0, r0, r5 ; Add in picbase
; Initialize global regs
ldr r7, pool_regtab
add r7, r7, r5
ldr r4, [r7]
ldr r5, [r7, 4]
ldr r6, [r7, 8]
; This is where control merges back together.
bl __ghs_ind_crt0 ; Go to C land
这一段是全篇的接缝,容易漏掉:
ldr r0, pool_baseptrs+add r0, r0, r5→ 把baseptrs的地址加上 picbase,放进 r0。r0 就是__ghs_ind_crt0的第一个参数(char *baseptrs[]);regtab里连取三个值到 r4/r5/r6 → 初始化全局寄存器(GHS 的"global regs"优化,把常用值常驻寄存器);bl __ghs_ind_crt0→ 进第一个 C 函数。
所以两个函数的签名能对上:
/* 汇编这边:r0 = baseptrs, r1 = argc, r2 = argv, r3 = envp */
void __ghs_ind_crt0(char *baseptrs[], int argc, char *argv[], char *envp[]);
baseptrs 里装的就是 PIC/PID/PIR 那几个基址,由 crt0 算好、通过这个数组交给 C 层。这样 ind_crt0.c 里才有 BASE[4] 和 picbase/pidbase 可用——下一节展开。
💡 顺带说一句
regtab:它不是一个 C 变量,从名字和用法看是全局寄存器的初值表(GHS 的 global regs 特性:把常用值常驻在某些寄存器里)。你在 C 里看不到它,但它会改变寄存器分配。这也是为什么"启动代码里改一行,全工程性能可能变"——它动的是全局寄存器的初始状态。
五、__lowinit:RH850/V850 上最危险的一个函数
在 RH850/V850 上,这个前置钩子叫 __lowinit,而不是 __ghs_board_memory_init。
5.1 它的定位
Renesas 教程原文很明确:
- RESET 会调用一个名为
__lowinit()的子过程; - 如果
__lowinit不存在,不会发出警告,执行直接继续到_start; - 用途是初始化时钟树或其他重要的 SFR;
__lowinit例程可以用 C 实现。
5.2 为什么说它危险
你写了一版
__lowinit想配 PLL,但由于签名不匹配 / 编译选项把它优化掉了 / 名字拼错,它根本没被链接进来。程序不会报错,会若无其事地继续跑——跑在一个你完全没预期的时钟频率上。
接下来发生的事:
| 症状 | 机理 |
|---|---|
| 串口波特率全错 | 时钟不对,分频算出来的波特率就是错的 |
| 定时器周期不对 | 所有周期性任务的时间基准都错 |
| Flash 等待周期与实际频率不匹配 | 读 Flash 读到错数据 |
| 看门狗超时时间不对 | 可能一直在复位,表现为"程序起不来" |
而这些问题在 main 里打断点看变量是看不出来的——变量都是对的,只是时间不对。
5.3 怎么确认它真的被执行了
| 方法 | 操作 | 判据 |
|---|---|---|
| GPIO 打点(最可靠) | 在 __lowinit 第一行翻转一个空闲脚 | 示波器看到翻转 = 进来了 |
| 断点 | 在调试器里对它下断点 | 命不中 = 没链接进来 |
| 写魔数 | 往一个固定 RAM 地址写 0xA5A5A5A5 | 调试器里读到 = 执行过 |
| 反查符号 | map 里搜 __lowinit | 有符号、且地址在 .text 里 |
💡 阿栈点评:我见过不止一个项目,
__lowinit的链接属性不对,导致时钟始终工作在内部 RC 而不是外部晶振,性能只有设计值的几分之一。因为"功能都正常",谁都没发现,直到 EMC 测试指标全线不合格才回头查到这里。上电第一件事,先用示波器确认系统时钟频率。
六、ind_crt0.c:六张表、三对指针
这是本篇的技术核心。很多人以为启动代码是"两个 for 循环",实际不是——它是查表,而且是三对表。
6.1 函数签名
void __ghs_ind_crt0(char *baseptrs[], int argc, char *argv[], char *envp[])
{
size_t BASE[4];
caddr_t picbase, robase, pidbase;
caddr_t rambootcodestart, rambootcodeend;
caddr_t rombootcodestart, rombootcodeend;
...
}
一个容易踩的点:
注意此处的
rambootcodestart等变量没有下划线(_)开头,这几个变量实际是一个(size_t)类型。
链接脚本里是 __ghs_rambootcodestart(有下划线),C 函数里是局部变量 rambootcodestart(无下划线),两者不是一回事。看源码时别搞混。
还有 #if !defined(MINIMAL_STARTUP) —— 这行意味着存在一个"最小启动"变体,它砍掉了一部分表处理。如果你在工程里定义了 MINIMAL_STARTUP,本篇讲的很多步骤是不执行的。这是个隐形的开关,排查启动问题时值得先搜一下有没有人定义它。
6.2 完整表清单:三对,不是一张
两段代码,但把符号名连起来看,一共是六张表头符号、三对:
| 表 | 起点 | 分界点 | 终点 | 干什么 |
|---|---|---|---|---|
| 清 BSS | __ghsbinfo_clear | __ghsbinfo_aclear | __ghseinfo_clear | 该清零的段 |
| ROM→RAM 拷贝 | __ghsbinfo_copy | __ghsbinfo_tcopy | __ghseinfo_copy | .data、ROM() 段 |
| CROM 解压拷贝 | __ghsbinfo_comcopy | __ghsbinfo_comtcopy | __ghseinfo_comcopy | CROM() 段 |
三对表结构完全一样,都是"起点 → 分界 → 终点"三段式。 这个模式一旦看出来,后面就不用逐字读了:一张表在内存里是连续的,中间插一个指针把它切成两批。
| 段 | 从哪到哪 | 地址怎么算 |
|---|---|---|
| 前一批 | info_xxx → 分界指针 | 要加上基址修正(PIDBASE / PICBASE) |
| 后一批 | 分界指针 → einfo_xxx | 用绝对地址,不加修正 |
info / einfo 是 begin / end 的缩写,t 是 text(代码)的缩写,a 是 additional/absolute 的缩写。分界指针的作用就是:同一张表切成两批,前一批要修正,后一批不用。
6.3 清 BSS:记录是 (目标, 填充值, 长度)
/* Clear BSS */
{
void **b = (void **)((char *)__ghs_funcadjust(__ghsbinfo_clear) + SCIFIX);
void **e = (void **)((char *)__ghs_funcadjust(__ghseinfo_clear) + SCIFIX);
void **a = __ghs_funcadjust(__ghsbinfo_aclear)
? ((void **)((char *)__ghs_funcadjust(__ghsbinfo_aclear) + SCIFIX))
: e;
/* Warning: This code assumes
__ghsbinfo_clear <= __ghsbinfo_aclear <= __ghseinfo_clear
Which is currently enforced with elxr OR
__ghsbinfo_aclear == 0 (i.e.: undefined) */
int OFFSET = PIDBASE;
while (b != e) {
void *t; ptrdiff_t v; size_t n;
while (b != a) {
t = OFFSET + (char *)(*b++);
v = *((ptrdiff_t *)b); b++;
n = *((size_t *)b); b++;
(*memsetp)(t, v, n);
}
OFFSET = 0;
a = e;
}
}
读法:
| 概念 | 含义 |
|---|---|
表里每条是三元组 (目标地址, 填充值, 长度) | 所以循环里连续取三次指针 |
while (b != a) → OFFSET = 0 → a = e | 两遍循环:第一遍带 PIDBASE 偏移,第二遍绝对地址 |
a 指向不存在时(== 0) | 直接令 a = e,等于跳过第一遍 |
__ghs_funcadjust + SCIFIX | PIC 情况下把"链接时地址"换算成"运行时地址" |
两个值得注意的点:
- 记录里带"填充值",不只是清零。代码调的是
memset(t, v, n),v来自表。理论上这套机制能填任意值;不过链接器对CLEAR段填的是 0,看到这个v你应该理解成"机制上更通用",而不是"我能随便填"。 - 那张注释里的"Warning"是给链接器作者看的约束:
clear <= aclear <= einfo_clear。也就是说两张表在内存里是连续的,靠一个指针切分。如果你手工改链接脚本把这两张表分开了(比如把某批段挪到别的内存块),这个假设就破了。
关键结论(第 04 期的正面印证):
SECTIONS里一个段带不带CLEAR,决定了它上电后是不是 0。 你用#pragma ghs section bss=".mypool"造了个自定义段,但脚本里没给它CLEAR,它就不会被清零——那里面全是上电残留值。
6.4 拷贝:记录是 (目标, 源, 长度)
/* Copy from ROM to RAM */
{
void **b = (void **)((char *)__ghs_funcadjust(__ghsbinfo_copy) + SCIFIX);
void **m = (void **)((char *)__ghs_funcadjust(__ghsbinfo_tcopy) + SCIFIX);
void **e = (void **)((char *)__ghs_funcadjust(__ghseinfo_copy) + SCIFIX);
while (b != e) {
void *t; void *s; size_t n;
t = ((b < m) ? PIDBASE : PICBASE) + (char *)(*b); b++;
s = PIRBASE + (char *)(*b++);
n = *((size_t *)b); b++;
...
}
}
和清 BSS 的差别只有一个字:第三元是 n(长度),不是 v(填充值);同时多了 s(源地址)。
(b < m) 那次判断决定了"目标地址加哪个基址":前半张表是数据(PIDBASE),后半张是代码(PICBASE)。这就是为什么 __ghsbinfo_tcopy 名字里有 t——t = text。
这就是第 04 期 CROM(.data) / ROM(.mydata) 的实际执行者:链接脚本负责"登记",这里负责"执行"。 你写的那两行 SECTIONS,最终变成这张表里的一条记录。
6.5 CROM 解压:弱符号提升机制
if (__ghs_weak_func_exists(__ghs_decompress)) {
...
}
If no CROM sections are present, the linker will not promote the weak symbol
__ghs_decompressto strong and__ghs_decompresswill not be linked into the program.
用了 CROM 才有解压代码——不用就一点空间都不占。这也是为什么 04 期说"不用 CROM 是零成本的":它的成本不在体积,在启动时间(解压要花时间,下一节算)。
6.6 最后一跳
(*crt1p)(argc, argv, envp);
注明:crt1p 指向 __ghs_ind_crt1,已不属于 libstartup.a,而是 libsys.a(ind_crt1.c)。跨库了,注意这个边界。
七、PIC / PID / PIR:三个基址到底是什么
上一节反复出现 PIDBASE、PICBASE、PIRBASE,这里一次说清。这是启动代码里最容易"看着眼熟但不知道在干嘛"的三个词。
| 基址 | 管什么 | 用在哪 | 不加会怎样 |
|---|---|---|---|
PICBASE | 代码的运行基址 | 代码段重定位 | 函数调用跳到链接时地址(错的) |
PIDBASE | 数据的运行基址 | 可写数据重定位 | 全局变量读写落在链接时地址 |
PIRBASE | **ROM 侧(源)**的基址 | 拷贝源地址 | 从错误的 ROM 地址取初值,拷进去的是垃圾 |
为什么代码和数据要分开两个基址?因为它们在运行时可以落在不同区域:代码可能还在 Flash(XIP),而数据一定在 RAM。两者相对链接时地址的偏移量不同,所以必须分开算。
⚠️ 三个缩写这里按用途命名(PIC/PID 分别对应 code / data,PIR 对应拷贝源),目的是让你记的时候有抓手。如果要在评审里引用,请以你版本编译器的
build_xxx.pdf中 “PIC” 相关章节的原文定义为准,别引用这个中文解释。
| 区域 | 链接时 | 运行时 | 用哪个基址 |
|---|---|---|---|
代码 .text | 链接地址 | 可能整体被搬到别处(重定位、RAM 镜像) | PICBASE |
数据 .data | 跟在代码后面 | 一定在 RAM,与链接地址完全不同 | PIDBASE |
| 拷贝源(Flash 里的初值) | 在 Flash 内 | 仍在 Flash 内 | PIRBASE |
表格里不填具体数字,是因为偏移量取决于你这次运行到底把它搬到了哪——这正是"基址"这个概念存在的原因。
另外两个符号:
| 符号 | 作用 | 说明 |
|---|---|---|
__ghs_funcadjust(p) | 把"链接时的函数/数据地址"换算成"运行时地址" | 表里的指针存的是链接时地址,用之前都要过一遍它 |
SCIFIX | 加在信息表指针上的修正量 | 从用法看是 PIC 场景下的表内偏移修正 |
💡 阿栈点评:这一节和 04 期的
.sdabase是同一个思路的两端——"基址"这件事在 GHS 里出现了三次:small data 有基址(.sdabase)、代码有基址(PICBASE)、数据有基址(PIDBASE)。理解"为什么需要基址",比记住这三个名字有用:因为链接时的地址和运行时的地址可以不一样。
八、ind_crt1.c:一张弱函数清单
这是全篇最实用的一节。ind_crt1 干的事可以一句话概括:把所有"可选初始化"挨个问一遍"你实现了吗",实现了就调用,没实现就跳过。
void __ghs_ind_crt1(int argc, char *argv[], char *envp[])
{
#if defined (__CRT_TRACE_INIT)
{ __ghs_trace_init(); }
#endif
{ if (__ghs_weak_func_exists(__ghs_board_devices_init)) __ghs_board_devices_init(); }
{ if (__ghs_weak_func_exists(__gh_lock_init)) __gh_lock_init(); }
{ if (__ghs_weak_func_exists(__ghs_malloc_init)) __ghs_malloc_init(); }
{ if (__ghs_weak_func_exists(__gh_iob_init)) __gh_iob_init(); }
...
if (!argc) {
noname[0] = 0; noname[1] = 0;
arg[0] = noname; arg[1] = 0;
env[0] = noname+1; env[1] = 0;
envp = env; argv = arg; argc = 1;
}
environ = envp;
exit(main(argc, argv, envp));
_Exit(-1);
}
8.1 钩子全表
| 钩子函数 | 何时被调用 | 干什么 | 能不能自己实现 |
|---|---|---|---|
__ghs_trace_init | 定义了 __CRT_TRACE_INIT | 初始化 trace | 一般不用 |
__ghs_board_devices_init | 总是(若存在) | 关键寄存器、默认运行时钟 | ✅ 推荐 |
__gh_lock_init | 总是(若存在) | 锁机制初始化 | 慎改 |
__ghs_malloc_init | 总是(若存在) | malloc 库初始化 | 用了动态内存再看 |
__gh_iob_init | 总是(若存在) | 标准输出(printf)通道初始化 | ⚠️ 不熟悉勿改 |
__ghs_cpp_exception_init | C++ 且非 __OSE | C++ 异常 | C++ 项目注意 |
__ghs_manprf_init | 总是(若存在) | 手动 profiling 初始化 | ⚠️ 不熟悉勿改 |
__ghs_coverage_init | 总是(若存在) | 覆盖率初始化 | ⚠️ 不熟悉勿改 |
__ghs_set_stack_chk_guard | 非 __OSE | 栈保护哨兵 | 用 -stack_protector 时关注 |
__ghs_validate_ghtws | __arm__ | 校验 .ghtws 段信息 | ARM 专用 |
直白的提醒:
__gh_iob_init()为系统调用标准输出函数初始化函数,如果不熟悉,请勿自定义。
__ghs_manprf_init()和__ghs_coverage_init()两个函数默认使用 MULTI 已经写好的函数,如果对该功能不熟悉的情况下,请勿自定义。此为测试代码覆盖率和运行性能部分。……在发布版本中,请勿使用该功能。
💡 阿栈点评:这张表的实战价值在于——"我想在
main之前跑一段自己的代码"这件事,正确做法不是改ind_crt1.c,而是实现__ghs_board_devices_init()。 前者会在你升级编译器时全盘重来,后者是官方预留接口,跨版本稳定。选哪个钩子也有讲究:**要动"必须在 C 之前发生的事"(向量表基址、SDRAM)→
__ghs_board_memory_init(汇编);要动"普通外设"→__ghs_board_devices_init(C,不用碰汇编)。
8.2 argc == 0 那段是干嘛的(补上 4.2 断掉的链)
if (!argc) 是在调试器没有传参数(脱机从 Flash 执行)时,构造一个合法的空 argv/envp。
接上 4.2 的因果链,完整故事是这样的:
crt0 发现"没有 MULTI" → r1/r2/r3 清 0 → ind_crt0 收到 argc=0
→ 原样传给 ind_crt1
→ ind_crt1 发现 argc==0 → 造一个空字符串当 argv[0] → argc=1
→ main(1, argv, envp)
所以裸机程序里 argv[0] 不是 NULL,而是一个空字符串——这就是 noname 干的。三个文件接力,最终目的是让 main 的签名始终成立。
⚠️ 一个实际后果:如果你的代码依赖
argc == 0来判断"是不是脱机",那是错的——它一定是 1。要判断调试器在不在,得看别的东西(比如SYSCALL_HELLO的返回值,或者干脆自己打个标志)。
8.3 main 返回之后去哪了
注意这里不是 main(...),而是 exit(main(...))。
exit 会做三件事:跑 atexit 注册的函数、flush 标准输出缓冲、最后调 _Exit。所以:
| 场景 | 结果 |
|---|---|
main 里 return 0 | 走 exit → 刷缓冲 → _Exit(0) → 通常死循环或触发复位 |
main 里 while(1) | 永远不返回,exit 不会被调用 |
main 返回后还有代码 | exit(...) 之后的 _Exit(-1) 是兜底,正常不会执行 |
这意味着 main 里写了 return 的裸机程序,"看起来正常"其实是在跑 _Exit 的死循环。如果你的工程靠 main 返回后的行为做复位(比如某些看门狗策略),要确认 _Exit 到底做了什么——这跟你用的 C 库实现有关。
这也是为什么"裸机 main 最后要写 while(1)"不是风格问题——不写的话,控制权交给 exit,而 exit 的行为不是你能预期的。
九、自定义启动脚本:三种做法,官方只推荐一种
| 方式 | 做法 | 评价 |
|---|---|---|
| ① 完全自定义 | 不用任何默认脚本,自己从零写 | 最自由,但要把 .data 拷贝、.bss 清零、CROM 解压、PIC 处理全部自己实现 |
| ② 直接改官方脚本 | 在 GHS 提供的脚本里改 | 升级编译器时改动会丢 |
| ③ 重定义弱函数 / 自定义脚本里调默认脚本 | 不动官方文件 | ✅ GHS 推荐:最大限度利用默认脚本,升级不痛 |
GHS 推荐使用第三种方式,这样可以最大限度利用默认脚本实现的功能,帮助用户获取更多便利性。
9.1 做法 A:只重定义弱函数(最常见)
文件 1:init_isr_vector.arm —— 单独放向量表
.file "init_isr_vector.arm"
.import _start_T
.import __ghsend_stack
.section .isr_vector, "a" ; naming the vector table
; __ghsautoimport_isr_vector causes it to be pulled in by the linker,
; even when part of a library, along with any dependencies.
.globl __ghsautoimport_isr_vector
__ghsautoimport_isr_vector::
.long __ghsend_stack ; Stack
.long _start_T ; POR Reset Entry
.long NMI_Handler ; Non Maskable handler
.long HardFault_Handler ; SV Hard Fault handler
; ... 省略 ...
.long SCM_CacheErr_IRQHandler ; irq 204
.end
注意那个 .globl __ghsautoimport_isr_vector——这是 __ghsautoimport 关键字的实际用法:即使向量表被编在库里,链接器也会把它拉进来。没有它,向量表可能整段不被链接,表现为"所有中断都不响应"。
文件 2:initboard.arm —— 装向量表基址
.file "initboard.arm"
.section .reset_handler, "ax"
.align 4
.weak __ghsautoimport_isr_vector
.set VTOR, 0xE000ED08
__ghs_board_memory_init::
cpsid fi ; 先关中断
ldr r0, =VTOR
ldr r1, =__ghsautoimport_isr_vector
str r1, [r0] ; 装向量表基址
cpsie fi ; 恢复中断
bx lr
.scall __ghs_board_memory_init, __leaf__
.maxstack __ghs_board_memory_init, 0
.end
__ghsautoimport_board_init::
.weak __ghs_board_memory_init
说明:
用户只需要增改
__ghs_board_memory_init()内的指令,此函数仅用来初始化关键信息;如果需要设置一般寄存器或者其他外设,请实现 C 函数__ghs_board_devices_init()。
两个汇编细节值得单独看:
| 指令 | 作用 | 忘了会怎样 |
|---|---|---|
.scall ... __leaf__ | 告诉编译器"这是叶子函数,没有栈帧" | 栈用量统计偏大,-stack_check 误报 |
.maxstack ... 0 | 声明该函数不占栈 | 同上 |
这组声明在功能安全的栈分析里很重要——它直接影响工具算出来的最大栈深。手写汇编钩子时漏了它们,栈分析的结果会虚高,你可能会为此白白砍功能。
9.2 做法 B:自定义启动符号 + 清 SRAM ECC
有些芯片要求启动时先清 RAM 的 ECC,才能开中断/用 RAM。这种情况要自己定义一个 Reset_Handler,做完再跳回默认的 _start_T:
.file "initboard.arm"
.section .reset_handler, "ax"
.align 4
.weak Reset_Handler
.set VTOR, 0xE000ED08
Reset_Handler::
; Clean SRAM ECC which is NEEDED to avoid ECC errors.
ldr r0, =__ghs_ramstart
ldr r1, =__ghs_ramend
movs r2, 0
movs r3, 0
1: strd r2, r3, [r0], 8 ; 每次写 8 字节(两个寄存器 = 全总线宽度)
cmp r0, r1
blo 1b
ldr r0, =_start_T
bx r0
然后二选一指定入口:
编译选项:-e Reset_Handler
或 链接脚本:OPTION("-e Reset_Handler")
⚠️ 特别标注:两种方式只能选其一。同时写会冲突。
💡 阿栈点评:
strd r2, r3, [r0], 8这个"写满总线宽度"的细节,是 ECC 初始化的通用要求——ECC 是对整个总线字计算的,只写一半字节会留下未定义的校验位,于是你清完 ECC 反而制造了 ECC 错误。第 10 期讲 PowerPC 多核时同样的坑会再出现一次(那边是用"跳转到自身"的指令填充)。另外注意这里用的是
__ghs_ramstart/__ghs_ramend——正是第 04 期链接脚本末尾那四个"不要删"的符号。删了它们,这段汇编的ldr r0, =__ghs_ramstart就取到 0,然后你把 0 地址往后写 256KB,后果自己想象。
9.3 选择建议
| 你的需求 | 选哪种 |
|---|---|
| 只想加个向量表、改个时钟 | 做法 A(重定义 __ghs_board_memory_init) |
| 只想初始化外设、跑点 C 代码 | 实现 __ghs_board_devices_init()(连汇编都不用) |
| RAM 要清 ECC、要特殊上电序列 | 做法 B(自定义 Reset_Handler + -e) |
| 用 AUTOSAR OS 接管启动 | 属于"方式①",必须自己补齐 .data 拷贝和 .bss 清零,否则全局变量初始化全废 |
| 要做功能安全、需要可追溯 | 做法 A(官方脚本不动,只加钩子,升级证据链干净) |
十、启动耗时:从 RESET 到 main 要多久
这一节回答一个工程上很实际、但资料从不给数字的问题:启动到底花了多少时间? 车载 ECU 的"快速启动"要求动辄 50ms、100ms,而 .bss 清零往往是里面最大的一块。
10.1 三个耗时段
| 阶段 | 耗时来源 | 量级 |
|---|---|---|
__lowinit / board_memory_init | PLL 锁定等待(几十~几百微秒)、SDRAM 初始化 | 百微秒级 |
.bss 清零 | 纯内存写,量 = .bss 大小 | 毫秒级(大头) |
.data 拷贝 | 从 Flash 读 + 往 RAM 写 | 毫秒级(量大时) |
| CROM 解压 | 比拷贝多一步解压运算 | 比 ROM() 慢 2~5 倍 |
10.2 算一下
假设:内核 100MHz,32 位总线,memset 每写 4 字节约 3 个周期(含循环开销):
.bss = 256KB = 262144 字节 = 65536 个 32 位字
周期数 ≈ 65536 × 3 = 196608
时间 ≈ 196608 / 100MHz ≈ 1.97ms
再算 .data 拷贝 16KB(每字"读 + 写"约 5 周期):
4096 字 × 5 = 20480 周期 ≈ 0.2ms
合计约 2.2ms。 看起来不多,但:
| 如果 | 那就会 |
|---|---|
.bss 是 1MB | 约 8ms |
| 主频只有 20MHz | 约 11ms(.bss 1MB 时 40ms+) |
| 看门狗超时设成 1ms | 每 1ms 复位一次,永远进不了 main |
最后一行是真实事故:“程序起不来、一直在复位”,根因是看门狗超时比 .bss 清零时间还短。 现象和"启动代码坏了"一模一样,实际是时间参数配错了。
10.3 想快就动这四个地方
| 手段 | 效果 | 代价 |
|---|---|---|
减小 .bss(大缓冲改成不清零的段) | 最有效,线性收益 | 那块内存内容不确定,必须自己初始化 |
用 -O2 以上让 memset 展开成字写 | 通常快 2~4 倍 | 代码略大 |
不用 CROM() 装大表 | 省掉解压时间 | Flash 占用变大 |
| 别在这条链上开 profiling/coverage | 少一大堆初始化 | 失去调试能力(发布版本来也不该有) |
关于"减小 .bss"要说得更具体:char big_buf[64*1024]; 这样的全局缓冲,默认进 .bss,每次上电都要清 64KB。如果它其实是个"用完即弃"的暂存区(比如通信帧缓冲,每次用之前都会覆盖),那清零纯属浪费——把它放进一个脚本里没给 CLEAR 的段,启动就能省下这块时间。
⚠️ 但这一步有前提:你得保证没有代码依赖"它是 0"。很多"偶发"bug 就是这么来的——昨天还在清零,今天为了快 2ms 不清了,某个漏初始化的标志位突然变成 1。改之前先全局搜这个缓冲的读操作。
十一、上电就跑飞:五段定位法
前面十节讲的是原理,这一节把它变成动作。核心思路:在链路的五个位置各放一个"我到过这里"的标记,用二分法缩小范围。
图 3|五段定位。哪一段没到,问题就在上一段到这一段之间——不是"某处坏了",而是"区间缩小了"。
打点用什么?三个可选,按侵入性排序:
| 手段 | 怎么做 | 优点 | 缺点 |
|---|---|---|---|
| 写魔数(推荐第一步) | 每个点往固定 RAM 地址写 0xA1/0xA2… | 不影响时序、不需要外设 | 需要调试器看内存 |
| 翻 GPIO | 每个点翻转一个空闲脚 | 用示波器能看时序和耗时 | 需要空闲脚、需要初始化 |
| 死循环 | 每个点后加 while(1) | 最原始但最确定 | 一次只能测一个点,要重编 5 次 |
写魔数为什么最好用:它不改控制流、不依赖任何外设初始化(GPIO 可能正是坏的那部分),也不会因为"翻转太快示波器抓不到"而误判。选一个全程不会被覆盖的 RAM 地址(比如 RAM 最后 4 字节,但要确认不在栈范围内),在五个点各写不同值,上电后读一次就知道走到哪了。
读值 → 结论对照表:
| 读到的最后一个值 | 卡在哪 | 下一步查什么 |
|---|---|---|
| 什么都没写 | _start 之前 | 入口点是否设错(第三节)、向量表/启动文件是否被链接 |
0xA1 | _start 到钩子返回之间 | __lowinit / board_memory_init 内部(时钟、SDRAM、ECC) |
0xA2 | ind_crt0 入口之前 | 设栈、FPU、picbase 计算 |
0xA3 | ind_crt0 内部 | 段表(04 期的 SECTIONS)、拷贝源地址、CROM 解压 |
0xA4 | ind_crt1 内部 | 某个弱钩子(__gh_iob_init 最常见) |
0xA5 | main 第一行之前 | argc 处理、exit 之前 |
这张表的价值在于它把"跑飞"变成了"区间"。没有它,你只有"不跑"和"跑"两个信息;有了它,第一次上电就能砍掉 80% 的可能。
💡 阿栈点评:我建议每个新板子第一次 bring-up 都先放这五个点,哪怕程序是好的。因为它是"基线"——以后出问题时,你可以立刻回答"上次到这里是好的",而不是从零开始怀疑。这五行的成本,比任何调试器功能都低。
十二、跨架构对照:PowerPC 多核的启动多了什么
如果你在做 MPC57xx 这类多核 PowerPC,链路会长很多,因为多了三个裸机没有的问题:谁来启动、SRAM 的 ECC 怎么初始化、其他核怎么唤醒。
| 事项 | 说明 |
|---|---|
| Boot Header / RCHW | 从 Flash 起始特定地址读取,含 Boot Identifier = 0x5A。复位后决定复位配置、选择启动核、指定启动核的向量入口 |
| SRAM ECC 初始化 | SRAM 使用前必须初始化,否则产生 ECC 错误。往所有 SRAM 位置写入任意值,必须写满整个总线宽度(64 位宽就做 64 位写) |
| MMU 初始化 | 部分驱动需要 |
| 核寄存器初始化 | 使用一致性(coherency)时,上电复位后核寄存器未初始化 |
| 其他初始化 | Mode Entry 模块、PLL、看门狗服务或关闭、Cache |
以 MPC5746M 工程为例:
mpc5746m.ppc 中的 .boot_header 位于 0xF8C000(16KB Flash block 0)
→ 只使能一个核(CPU2)做初始引导
→ 引导核向量 __ghs_rombootcodestart = .text 起始
→ _start:
bl __ghs_board_memory_init ECC 初始化共享 SRAM
bl __ghs_e200z4204_core_init MSR[ME]=1、IVPR=0x40000000、
初始化本地数据内存、初始化 I/D Cache
GHS 内务:使能调试器 syscall(printf 靠它)、
初始化 SP/SDA/SDA2 到共享内存、PIC 相关处理
→ 跳转 __ghs_ind_crt0
→ 初始化静态和全局数据
→ PIC/PIR/PID 时重定位指针
→ 调用 __ghs_board_cache_sync(刷数据 cache、失效指令 cache)
→ 可选的已初始化数据解压缩、可选校验和
对照着看,你会发现 _start 的位置完全一样、__ghs_ind_crt0 的位置完全一样——多出来的只是前面"选核 + 清 ECC"和中间"核初始化"。这印证了本篇的核心观点:GHS 的启动链路是一套跨架构的骨架,架构差异被收敛在 crt0 一个文件里。
Boot Header 结构(MPC5746M):
| 字段 | 值 | 描述 |
|---|---|---|
.boot_header | 0x005A0001 | Boot header id + enable CPU2 |
_ghs_rombootcodestart | — | CPU2 复位地址 |
| 保留 ×2 | 0x0 | — |
| — | 0x0 | CPU0 / CPU1 / CPUC 复位地址 |
| 保留 | 0x0 | — |
⚠️ GHS 工程的 Boot Header 在
mpc5746m.ppc文件里,不在 C 代码里。 这解释了高频困惑:“我明明给 CPU0 写了代码,为什么只有 CPU2 在跑?”——引导核是 Boot Header 选的,不是你在main里选的。 第 10 期展开。
十三、完整链路总图
图 2|完整启动链路。四个可以插手的点:
crt0的弱符号钩子、__lowinit(RH850)、ind_crt0的段处理(靠链接脚本控制)、ind_crt1的一串弱函数。
十四、避坑清单
| 现象 | 原因 | 处理 |
|---|---|---|
| 串口波特率全错、定时器周期不对 | __lowinit 没执行,时钟仍在默认源 | 在 __lowinit 里翻 GPIO 验证;确认符号被链接 |
| 从 RAM 镜像启动,中断全飞 | __ghs_board_memory_init 未实现,VTOR 没设 | 实现该钩子,str 向量表地址到 VTOR |
| 全局变量初值随机 | 自定义段没被清/拷(脚本缺 CLEAR 或没登记进表) | 检查 SECTIONS 是否给了正确属性 |
| 用了 CROM 但变量没初始化 | 启动脚本不是 GHS 默认的,拷贝逻辑丢了 | 保留 __ghs_ind_crt0 调用,或自己实现 |
| 所有中断都不响应 | 向量表没被链进来 | 用 __ghsautoimport_isr_vector 强制 import |
调试器里 printf 有输出,脱机没有 | printf 依赖调试器 syscall 通道 | 重定向到串口;发布版移除 |
| PowerPC 上电就进异常 | SRAM 未做 ECC 初始化 | 确认 __ghs_board_memory_init 被调用,且按总线宽度写满 |
| 多核只有 CPU2 在跑 | Boot Header 只使能了引导核 | 检查 Boot Header 配置与从核启动代码 |
| 程序从地址 0 开始跑 | 既无 _start 也无 start,走了兜底 | 检查启动文件是否被链接、入口符号名 |
-e Reset_Handler 报冲突 | 编译选项和 OPTION("-e ...") 同时写了 | 二选一 |
| 程序一直在复位,进不了 main | 看门狗超时 < .bss 清零时间 | 先放宽看门狗,再优化启动耗时(第十节) |
| 清完 ECC 反而报 ECC 错误 | 没按总线宽度写满 | 用 strd/64 位写,别用 strb 循环 |
靠 argc == 0 判断"是不是脱机" | ind_crt1 会把它改成 1 | 改用其他判据 |
main 里写 return 后行为诡异 | 走的是 exit → _Exit 那条路 | 裸机 main 用 while(1) 收尾 |
| 栈分析算出的最大栈深虚高 | 手写汇编钩子漏了 .scall __leaf__ / .maxstack | 补上声明 |
MINIMAL_STARTUP 被定义,部分初始化不执行 | 有人为了省空间开了这个开关 | 全局搜该宏,确认是否该开 |
十五、小结
- 硬件先取址:RH850/V850 从
0x00000000取复位入口;Cortex-M 是"地址 0 取 SP、地址 4 取复位向量",所以两份启动代码的"设栈"位置不同——是硬件差异不是风格差异; - 启动代码分两层:架构相关(汇编,基本不改)+ 架构无关(C,通过钩子扩展);
ind_crt0.c在libstartup.a,ind_crt1.c在libsys.a; - 入口点有优先级:
_start>start>_main>main> 零地址;Thumb-only 用_start_T;-e与OPTION("-e ...")二选一; crt0顶部那堆.weak是官方预留的钩子,__lowinit/__ghs_board_memory_init都属于这类——没实现就静默跳过,这是"时钟不对但功能看似正常"的根因;SYSCALL_HELLO同时决定了"参数从哪来"和"栈从哪来",也是printf只在调试器下有输出的原因;ind_crt0是表驱动的,而且是三对表(clear/aclear、copy/tcopy、comcopy/comtcopy),结构都是"起点→分界→终点";记录是三元组,清 BSS 是(目标, 值, 长),拷贝是(目标, 源, 长);- PIC/PID/PIR 是三个基址:代码、数据、ROM 源。搞清"为什么需要基址",比记住名字有用;
ind_crt1是一张弱函数清单,__ghs_board_devices_init()是你"在main之前插代码"的正规入口;汇编钩子管"必须在 C 之前的事",C 钩子管普通外设;argc从 0 被改成 1 这条链跨了三个文件,别用argc == 0判断脱机;main是被exit(main(...))调用的,裸机main要用while(1)收尾;- 自定义启动三种做法,官方只推荐第三种(重定义弱函数 / 调默认脚本),升级编译器不痛;
- 启动耗时的大头是
.bss清零,256KB 约 2ms(100MHz);看门狗超时比它短就会无限复位——这是"程序起不来"的一个非代码原因; - 跑飞用五段定位:写魔数、读值、按表查区间。成本极低,建议每个新板子 bring-up 都先做一遍。
下篇预告
下一篇讲中断。这是工程问题最集中的地方,也是 GHS 与 Keil/IAR 差异最大的地方——GHS 有三条完全不同的装表路径,选错一条,你会花一整天找不到中断为什么进不去。 而且本篇的 __ghsautoimport_isr_vector 和 __ghs_board_memory_init 会在那里再出现一次。
更多推荐

所有评论(0)