深入解析U-Boot TPL代码:嵌入式启动的“第一棒”背后的秘密

电子说

1.4w人已加入

描述

 

 

在嵌入式系统启动过程中,从按下电源键到操作系统开始运行,中间藏着一系列精密的初始化步骤。今天我们就来拆解 Rockchip 平台 U-Boot 中的 TPLTiny Program Loader)阶段核心代码tpl.c,看看这个 "启动第一棒是如何为整个系统保驾护航的。

嵌入式系统

什么是 TPL?为什么它如此重要?

 

TPL 是 U-Boot 启动流程中最早执行的阶段之一,全称 Tiny Program Loader(小型程序加载器)。不同于后续的 SPLSecondary Program Loader)和 U-Boot 主体,TPL 运行在系统资源极其有限的环境中(通常依赖片内 SRAM),却要完成最关键的硬件初始化工作。

 

 

可以把嵌入式启动比作一场接力赛:

 

 

BootROM(芯片内置的启动程序)是 "发令员"

 

 

TPL 是 "第一棒选手",负责启动最基础的硬件

 

 

SPL 是 "第二棒",完成更多初始化

 

 

U-Boot 主体则是 "最后一棒",最终引导操作系统

 

 

tpl.c正是 "第一棒选手的核心操作手册。

 

 

代码解析:TPL 都做了些什么?

 

我们通过代码结构,一步步揭开 TPL 的工作内容:

 

 

1. 最小化的 "输出系统":串口调试的基石

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
#ifndef CONFIG_TPL_LIBCOMMON_SUPPORT#define CONFIG_SYS_NS16550_COM1 CONFIG_DEBUG_UART_BASEvoid puts(const char *str){    while (*str)        putc(*str++);}void putc(char c){    if (c == 'n')        NS16550_putc((NS16550_t)(CONFIG_SYS_NS16550_COM1), 'r');    NS16550_putc((NS16550_t)(CONFIG_SYS_NS16550_COM1), c);}#endif

这段代码实现了最基础的字符串输出功能。在 TPL 阶段,系统还没有加载完整的函数库,因此需要直接操作 NS16550 串口控制器实现putc(输出字符)和puts(输出字符串)函数。

 

 

特别注意到对换行符n的处理—— 自动转换为rn(回车 + 换行),这是为了保证在串口终端上能正确换行,细节处体现了调试友好性。

 

 

2. 时间管理:系统的 "心跳初始化

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
#ifndef CONFIG_TPL_LIBGENERIC_SUPPORTint __weak timer_init(void) return 0; }#ifdef CONFIG_ARM64void __weak __udelay(unsigned long usec){    u64 i, j, count;    asm volatile ("MRS %0, CNTPCT_EL0" : "=r"(count));    i = count;    j = usec * 24;  // 24MHz计数器    i += j;    while (1) {        asm volatile ("MRS %0, CNTPCT_EL0" : "=r"(count));        if (count > i) break;    }}#else// 非ARM64架构的延迟实现#endifvoid udelay(unsigned long usec) { __udelay(usec); }#endif

时间延迟是硬件初始化的关键需求(比如等待外设上电稳定)。这段代码通过直接操作 CPU 计数器寄存器:

 

 

 ARM64 架构读取CNTPCT_EL0寄存器

 

 

为其他架构使用mrrc p15指令

 

 

基于 24MHz 的基准频率计算延迟时间

 

 

这种底层实现确保了在没有操作系统支持的情况下,系统仍能获得精确的微秒级延迟能力。

 

 

3. 核心初始化流程:board_init_f的使命

 

作为 TPL 阶段的主入口函数,board_init_f串起了整个初始化流程:

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
void board_init_f(ulong dummy){    rockchip_stimer_init();  // 系统定时器初始化    arch_cpu_init();         // CPU架构初始化#ifdef EARLY_DEBUG    debug_uart_init();       // 调试串口初始化    printascii("U-Boot TPL " PLAIN_VERSION "...n");  // 版本信息打印#endif    timer_init();            // 定时器初始化    // DRAM初始化(根据配置选择设备模型或直接调用)#if defined(CONFIG_SPL_FRAMEWORK) && !CONFIG_IS_ENABLED(TINY_FRAMEWORK)    ret = uclass_get_device(UCLASS_RAM, 0, &dev);#else    sdram_init();#endif    // 条件允许时返回BootROM执行下一阶段#if defined(CONFIG_TPL_ROCKCHIP_BACK_TO_BROM) && !defined(CONFIG_TPL_BOARD_INIT)    back_to_bootrom(BROM_BOOT_NEXTSTAGE);#endif}

这个函数的执行逻辑清晰展现了 TPL 的核心任务:

 

 

1.初始化系统定时器,建立时间基准

 

 

2.完成 CPU 架构相关初始化

 

 

3.启动调试串口,输出版本信息(关键的调试点)

 

 

4.初始化 DRAM(内存),为后续阶段提供运行空间

 

 

5.交接控制权到下一阶段

 

 

对启动流程的关键作用

 

TPL 作为启动的 "第一棒",其工作质量直接决定了系统能否正常启动:

 

 

1.硬件最小化初始化:在资源受限的环境下,优先初始化串口、定时器、内存等关键硬件,为后续阶段铺路

 

 

2.启动流程衔接:通过back_to_bootrom函数与 BootROM 协作,确保启动流程按顺序推进

 

 

3.兼容性适配:通过条件编译(如CONFIG_ARM64CONFIG_TPL_LIBCOMMON_SUPPORT)支持不同架构和配置,提高代码复用性

 

 

4.故障隔离:如果 TPL 阶段失败(如 DRAM 初始化出错),系统会在早期挂起,避免错误扩散

 

 

对调试工作的特殊意义

 

对于嵌入式开发者来说,TPL 阶段的调试支持堪称 "救命稻草"

 

 

1.早期日志输出:通过debug_uart_initprintascii实现的早期打印,能帮助定位启动失败的第一现场。想象一下,如果连 "U-Boot TPL 版本信息都打印不出来,排查问题会有多困难!

 

 

2.时序问题排查:精确的udelay函数确保硬件初始化时序正确,减少因时序错误导致的 "偶发故障"

 

 

3.版本追溯:启动时打印的版本号(PLAIN_VERSION)和编译时间,能快速确认是否使用了正确的代码版本

 

 

4.错误定位hang函数在出错时进入死循环,配合 JTAG 等工具可捕获现场状态,避免系统 "crash 后无痕迹"

 

 

总结:TPL—— 启动流程的 "基石"

 

tpl.c虽然代码量不大,却承载了嵌入式系统启动最基础也最关键的工作。它像一位严谨的 "系统初始化工程师",在资源极度有限的环境下,有条不紊地完成硬件唤醒、环境准备和流程交接。

 

 

对于开发者而言,理解 TPL 代码不仅能帮助我们快速定位启动问题,更能深入理解嵌入式系统从 "断电到 "运行的整个唤醒过程。下次调试启动故障时,不妨先看看 TPL 阶段的日志输出 —— 答案往往就藏在这些最早期的信息里。

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分