3. 开发环境搭建

芯片上电后,到底发生了什么?从复位向量到C语言的main(),中间这条链路,我称之为“嵌入式开发的暗黑三分钟”。很多新手上来就找main(),结果发现程序跑飞了,根本不知道问题出在哪。说白了,启动流程就是给芯片“搭骨架”,骨架没搭好,肉(业务代码)往哪长?

3.1 上电复位:第一行代码在哪?

芯片一上电,PC指针会指向一个固定地址——复位向量。这个地址通常放在Flash的最开头,也就是0x00000000或0x08000000(取决于芯片)。

以ARM Cortex-M为例,向量表的前两个word分别是:

  • 0x00000000:栈顶地址(MSP初始值)
  • 0x00000004:复位中断服务函数地址

芯片硬件自动完成两件事:

  1. 从0x00000000加载栈指针到MSP
  2. 从0x00000004加载PC值,跳转到复位函数
我个人的习惯:在链接脚本里把向量表放在第一个section,并且用汇编的.word伪指令显式定义前两个入口。这样即使芯片型号不同,启动逻辑也一目了然。

3.2 汇编启动代码:硬件的“第一口奶”

复位函数通常用汇编写,因为这时候C环境还没准备好——全局变量没初始化,BSS段没清零,堆栈可能都不够用。你想想看,这时候要是调个printf(),那不得直接HardFault?

LiteOS-M的汇编启动代码,我拆解成四个关键步骤:

步骤 操作 说明
1 设置主栈指针 从链接脚本获取__stack_top
2 清零BSS段 从__bss_start到__bss_end
3 拷贝数据段 从Flash加载到SRAM(__data_load → __data_start)
4 跳转C入口 调用main()或LOS_Start()

下面是一段典型的Cortex-M启动汇编(简化版):

    .section .vectors, "a"
    .word   __stack_top
    .word   Reset_Handler

    .section .text.Reset_Handler
    .weak   Reset_Handler
    .type   Reset_Handler, %function
Reset_Handler:
    /* 1. 设置栈指针 */
    ldr     sp, =__stack_top

    /* 2. 清零BSS段 */
    ldr     r0, =__bss_start
    ldr     r1, =__bss_end
    mov     r2, #0
    .Lbss_loop:
    cmp     r0, r1
    itt     lo
    strlo   r2, [r0], #4
    blo     .Lbss_loop

    /* 3. 拷贝数据段 */
    ldr     r0, =__data_start
    ldr     r1, =__data_end
    ldr     r2, =__data_load
    .Ldata_loop:
    cmp     r0, r1
    itt     lo
    ldrlo   r3, [r2], #4
    strlo   r3, [r0], #4
    blo     .Ldata_loop

    /* 4. 跳转到C语言初始化 */
    bl      SystemInit
    bl      main
    b       .
我曾经踩过一个坑:在某个低功耗项目里,BSS段清零后,系统时钟配置还没做,导致外设寄存器访问异常。后来我养成了习惯——在清零BSS之前,先把必要的时钟和电源域打开。顺序很重要,别省。

3.3 C语言初始化阶段:从汇编到世界的桥梁

跳进main()之前,其实还有一层“隐形初始化”。LiteOS-M的启动流程里,SystemInit()负责硬件层面的基础配置:

  • 时钟树初始化:配置PLL、分频器,让CPU跑在目标频率
  • 中断向量表重定位:把向量表从Flash拷贝到SRAM(部分芯片需要)
  • FPU使能:如果芯片带浮点单元,得在CPACR寄存器里打开
  • MPU/Cache配置:内存保护单元和缓存,按需初始化

之后才进入main()。但LiteOS-M的main()不是让你写业务逻辑的——它是个“调度器启动函数”。真正的流程是:

  1. main()里调用LOS_KernelInit(),初始化任务管理、内存管理、中断管理
  2. 创建第一个任务(Idle任务)和软件定时器任务
  3. 调用LOS_Start(),启动调度器

说白了,从芯片上电到系统跑起来,就是一条“硬件→汇编→C→操作系统”的链条。每一层都在为下一层铺路。

3.4 完整启动链路图

下面这张图,我画的是LiteOS-M从复位到任务调度的完整链路。你把它打印出来贴在工位上,调试的时候对着看,能省不少时间。

LiteOS-M 内核启动完整链路 芯片上电复位 硬件加载向量表(MSP+PC) 汇编启动代码(Reset_Handler) C语言初始化(SystemInit) LOS_KernelInit() 内核初始化 LOS_Start() 启动任务调度 关键点: • 汇编阶段不能调C函数 • BSS清零必须在数据拷贝前 • SystemInit 后全局变量才可用 避坑提示: • 检查链接脚本符号是否对齐 • 确认栈大小是否足够

3.5 避坑指南:启动阶段常见问题

做移植的时候,启动阶段最容易出问题。我列几个高频坑,你遇到了可以直接翻回来查:

现象 可能原因 排查方法
上电后直接跑飞 向量表地址不对或栈指针未正确加载 检查链接脚本中__stack_top的值
全局变量值不对 数据段拷贝未执行或拷贝地址错误 对比__data_load和__data_start的地址
BSS变量非零 BSS段清零循环未覆盖完整区域 检查__bss_end是否对齐到4字节
进入HardFault 中断使能过早或栈溢出 在SystemInit中先关全局中断

我的经验:启动代码调试,最笨也最有效的方法——在汇编里每个关键步骤后加一个GPIO翻转。用示波器看波形,一眼就能看出代码卡在哪一步。比加打印靠谱多了,毕竟这时候串口可能还没初始化。

嗯,启动流程这块,说白了就是“先活下来,再活好”。汇编阶段保证芯片能跑,C阶段保证环境能用,内核阶段保证任务能调度。每一步都踩实了,后面的开发才稳当。