ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

三星uboot移植实战:从编译到内核引导的完整流程

三星uboot移植实战:从编译到内核引导的完整流程 1. 从零开始的移植思路为什么不能直接编译官方uboot拿到一份芯片原厂发布的uboot源码很多人的第一反应是找到配置文件直接make然后烧录看能不能跑。这个思路在部分场景下确实能跑通但放到三星平台的uboot移植上大概率会在编译阶段就卡住或者编译通过了但板子毫无反应。原因在于原厂uboot的代码结构是面向自家参考板设计的它默认你用的DDR型号、时钟配置、串口引脚、存储介质都和参考板一模一样。一旦你的硬件和参考板有差异就必须在源码层面做适配。移植的本质是什么说白了就是让一份通用的引导程序代码认识你这块特定板子的硬件。uboot在启动阶段要做的事情非常固定初始化时钟、初始化DDR、初始化串口、初始化存储设备然后把内核从存储介质搬到DDR里最后跳转执行。这四件事里时钟和DDR的初始化是芯片内部的事情通常原厂代码已经写好了你只需要根据自己板子的晶振频率和DDR型号做参数调整。串口和存储设备则和板级硬件强相关引脚定义、控制器编号、供电引脚都可能不同这些是移植过程中改动最多的地方。我个人的习惯是在动手改代码之前先把整个移植流程拆成三个阶段编译阶段、启动阶段、功能验证阶段。编译阶段的目标是让代码能编译出可烧录的镜像启动阶段的目标是让串口能打印出uboot的启动信息功能验证阶段的目标是让uboot能识别存储设备、能读写、能引导内核。这三个阶段是递进关系前一个阶段没搞定不要急着进入下一个阶段否则问题会叠加在一起排查起来非常痛苦。还有一个容易被忽略的点三星平台的uboot源码通常包含多个板级配置文件分布在include/configs/目录下。这些配置文件的命名规则一般是芯片型号_板子名称.h。你需要先确认自己用的是哪颗芯片然后找到对应的配置文件作为基础复制一份改成自己的板子名称。这个操作看起来简单但配置文件里的宏定义数量非常多有些宏是芯片级别的有些是板级级别的改错了会导致编译报错或者运行时行为异常。所以复制之后不要急着大改先编译一次原始配置确认工具链和源码本身没有问题再逐步修改。2. 编译环境的搭建与工具链选择2.1 交叉编译器的版本匹配问题三星平台的uboot通常比较老很多版本是基于ARMv7架构的对应的交叉编译器是arm-linux-gcc系列。这里有一个非常关键的坑编译器版本不能太新。我试过用较新的GCC去编译老版本的uboot结果在链接阶段报出一堆关于-march和-mtune参数不兼容的错误或者编译出来的二进制文件在板子上跑飞。原因是新版本编译器默认使用的ABI和指令集可能和老代码不匹配。比较稳妥的做法是使用源码包中README或者Makefile里推荐的编译器版本。如果文档没有写可以看Makefile里CROSS_COMPILE变量的默认值通常能推断出大致的工具链前缀。比如看到CROSS_COMPILE ? arm-linux-那就去找对应的arm-linux-gcc工具链版本控制在4.x到6.x之间比较安全。安装工具链的时候建议把它解压到一个固定路径比如/opt/toolchain/然后把bin目录加到PATH环境变量里。不要用系统包管理器安装的编译器因为系统仓库里的版本往往太新而且可能缺少一些老uboot需要的库文件。安装完成后用arm-linux-gcc -v确认版本信息确保能正常输出版本号。2.2 源码目录结构的快速梳理三星uboot的源码目录结构和其他厂商的uboot大同小异但有几个目录需要特别关注arch/arm/cpu/芯片级别的初始化代码包括时钟、DDR、串口等底层驱动。这个目录下的代码通常不需要大改但需要理解它的初始化流程。board/samsung/板级代码这是移植过程中改动最多的目录。里面通常有多个子目录每个子目录对应一块参考板。include/configs/板级配置文件定义了各种宏开关决定了哪些功能会被编译进uboot。drivers/各种外设驱动包括串口、MMC、网络等。移植时可能需要根据硬件调整驱动中的引脚配置。我一般会先花半小时把这几个目录浏览一遍重点看board/samsung/下有哪些参考板以及include/configs/下对应的配置文件。这样能快速建立起对源码结构的整体认知后面改代码的时候知道该去哪个目录找。2.3 编译命令与常见报错处理编译uboot的标准流程是两步先make board_name_config生成配置文件再make开始编译。但三星平台有时候需要先执行make distclean清理之前的编译产物否则残留的.o文件可能导致链接错误。常见的编译报错有几类一是头文件找不到通常是CROSS_COMPILE路径没设置对二是宏定义冲突比如两个配置文件里定义了同一个宏但值不同三是链接脚本里的地址重叠这种错误比较隐蔽需要检查board/samsung/board/u-boot.lds文件里的内存布局。遇到报错不要慌先看错误信息的第一行通常那里会指出具体是哪个文件哪一行出了问题。如果是宏定义冲突可以用grep -r 宏名称 include/来搜索所有定义位置然后逐一排查。如果是链接错误重点检查u-boot.lds和config.mk里的TEXT_BASE地址是否和DDR的起始地址匹配。3. 板级配置文件的裁剪与适配3.1 从参考板配置文件复制起步假设你用的芯片是某款三星ARMv7芯片参考板叫smdkv210你的板子叫myboard。第一步是复制配置文件cp include/configs/smdkv210.h include/configs/myboard.h然后在boards.cfg或者Makefile里添加myboard的编译目标。不同版本的uboot板级目标的定义方式不同老版本用boards.cfg新版本直接在Makefile里加规则。添加完成后执行make myboard_config如果能看到Configuring for myboard board...的输出说明配置文件已经被正确识别。接下来是修改myboard.h里的关键宏。以下几类宏是必须检查的时钟频率相关CONFIG_SYS_CLK_FREQ、CONFIG_SYS_HZ等要和板子的晶振频率一致。串口相关CONFIG_SERIALn、CONFIG_SYS_NS16550_COMx等要和实际使用的串口控制器编号匹配。DDR相关CONFIG_SYS_SDRAM_BASE、CONFIG_SYS_TEXT_BASE等要和DDR的物理地址映射一致。存储设备相关CONFIG_MMC、CONFIG_NAND等根据板子实际使用的存储介质来开关。3.2 串口配置的细节调整串口是移植过程中最重要的调试手段没有串口输出后面所有工作都是盲人摸象。三星平台的串口驱动通常在drivers/serial/serial_s5p.c或者类似文件里。你需要确认三件事串口控制器的基地址、波特率分频系数、引脚复用配置。基地址一般在芯片手册的存储器映射章节能找到。波特率分频系数取决于串口的时钟源频率计算公式通常是分频值 时钟源频率 / (波特率 × 16)。比如时钟源是100MHz波特率是115200那么分频值大约是54。这个值需要写入串口控制器的波特率寄存器。引脚复用配置是最容易出问题的地方。三星芯片的引脚通常有多种功能串口功能需要把对应的GPIO配置成串口模式。这个配置在board/samsung/myboard/myboard.c里的board_early_init_f或者gpio_init函数中完成。如果引脚没配对串口就不会有任何输出。注意有些三星芯片的串口引脚默认是输入模式需要显式设置为复用功能。配置的时候要同时设置功能选择寄存器和上下拉电阻寄存器只设一个可能不稳定。3.3 DDR参数的适配逻辑DDR初始化是uboot启动过程中最关键的步骤之一。如果DDR没初始化好代码连内存都用不了更别提加载内核了。三星平台的DDR初始化代码通常在arch/arm/cpu/芯片型号/目录下里面有一个spl.c或者ddr_init.c文件。DDR参数主要包括行地址位数、列地址位数、Bank位数、时序参数tRCD、tRP、tRAS等、刷新周期。这些参数必须和板子上实际焊接的DDR芯片手册一致。如果参数不对轻则DDR容量识别错误重则系统直接跑飞。我的经验是先找到参考板的DDR参数然后对照自己板子的DDR芯片手册逐项核对。重点看时序参数因为不同品牌的DDR芯片即使容量相同时序也可能有差异。如果实在找不到完全匹配的参数可以先用参考板的参数试一下能启动再慢慢调优。4. 启动阶段的调试与问题定位4.1 串口无输出的排查链路编译通过、烧录完成、上电之后串口没有任何输出这是移植过程中最常见的问题。排查这个问题的思路是从电源开始逐级往上查。第一步确认板子供电正常。用万用表量一下核心电压和IO电压确保在芯片手册要求的范围内。供电不正常的话芯片根本不会启动。第二步确认启动模式设置正确。三星芯片通常有多个启动模式引脚不同的电平组合决定了芯片从哪里启动NAND、MMC、UART等。如果启动模式设错了芯片会去错误的地方找引导代码自然不会有输出。第三步确认uboot镜像烧录到了正确的位置。有些存储介质有多个分区uboot必须烧录到引导分区。烧录地址错了芯片读不到有效的引导代码。第四步确认串口线连接正确。TX和RX不能接反波特率要匹配。我遇到过好几次是因为串口线的TX/RX接反了折腾了半天才发现。第五步如果以上都没问题那可能是uboot代码本身的问题。可以在board_early_init_f函数的最开始加一句GPIO翻转的代码用示波器或者LED来判断代码有没有执行到那里。如果GPIO有翻转说明代码在跑问题出在串口初始化如果没有翻转说明代码在更早的阶段就挂了。4.2 时钟初始化失败的典型表现时钟初始化失败的表现比较隐蔽有时候串口能输出几个乱码字符然后就没了有时候完全没输出。判断时钟是否正常的一个方法是测量芯片的时钟输出引脚看有没有波形。如果没有波形说明时钟初始化代码没有正确执行。三星平台的时钟初始化通常在arch/arm/cpu/芯片型号/clock.c里。重点检查PLL的配置参数包括倍频系数、分频系数、锁定时间。这些参数的计算公式在芯片手册的时钟章节有详细说明。一个常见的错误是PLL的输入时钟频率设错了导致输出频率偏离预期串口的波特率也跟着错打印出来就是乱码。4.3 内存初始化后的自检方法DDR初始化完成后uboot会进行内存自检。如果自检失败串口通常会打印类似SDRAM test failed的信息。这时候需要检查DDR的时序参数和容量配置。一个实用的调试技巧是在DDR初始化代码之后加一段简单的内存读写测试往某个地址写入一个已知值然后读回来比较。如果读回来的值不对说明DDR没有正常工作。可以逐步调整时序参数每次只改一个观察结果变化。提示DDR时序参数的调整需要耐心建议用表格记录每次修改的参数和对应的测试结果方便回溯。5. 存储设备与外设的驱动适配5.1 MMC/SD控制器的移植要点如果板子使用MMC或者SD卡作为启动介质需要确保uboot里的MMC驱动能正确识别设备。三星平台的MMC驱动通常在drivers/mmc/s5p_mmc.c或者类似文件里。移植时需要确认控制器的基地址、时钟频率、引脚配置。MMC控制器的时钟频率通常来自一个专用的分频器需要根据芯片手册设置分频值。引脚配置包括CLK、CMD、DATA0-3等信号线这些引脚需要配置成MMC功能模式。如果引脚配置不对MMC设备无法被识别。一个常见的坑是MMC控制器的电压设置。有些SD卡需要3.3V有些需要1.8V如果电压设置不对设备可能无法正常工作。uboot里通常有mmc_set_voltage之类的函数来设置电压需要根据实际使用的存储卡类型来调用。5.2 网络驱动的移植思路网络功能在uboot里主要用于TFTP下载内核和文件系统。三星平台的网络驱动通常是drivers/net/dm9000.c或者designware.c之类的通用驱动。移植时需要确认PHY芯片的地址、接口类型RMII或者MII、复位引脚。PHY地址通常由硬件设计决定可以通过PHY芯片的地址引脚电平来确认。接口类型取决于MAC控制器和PHY之间的连接方式RMII使用2根数据线MII使用4根。复位引脚需要在uboot里显式拉高再拉低完成PHY的复位。网络调试的时候先用ping命令测试连通性。如果ping不通检查网线、PHY地址、接口类型。可以用mii info命令查看PHY寄存器的值判断PHY是否被正确识别。5.3 环境变量的保存位置选择uboot的环境变量需要保存在非易失存储介质里通常是MMC的一个分区或者NAND的一个块。保存位置通过CONFIG_ENV_OFFSET和CONFIG_ENV_SIZE来定义。如果保存位置和uboot镜像本身重叠会导致uboot被覆盖系统无法启动。选择保存位置的时候要确保它不和uboot镜像、内核镜像、文件系统重叠。一般会在存储介质的开头预留一段空间给uboot和环境变量具体布局可以参考参考板的设计。6. 从能启动到能引导内核的最后一公里6.1 内核镜像的加载地址与入口点uboot的最终目标是加载内核并跳转执行。内核镜像通常是一个压缩的zImage或者uImage需要被加载到DDR的某个地址然后跳转到入口点执行。加载地址通过CONFIG_SYS_LOAD_ADDR定义入口点通常是加载地址加上一个固定偏移。三星平台的内核入口点通常是加载地址 0x40这个偏移量是为了跳过uboot传递的参数头。如果偏移量设错了内核会从错误的位置开始执行导致启动失败。6.2 启动参数的传递与校验uboot通过bootargs环境变量向内核传递启动参数包括控制台设备、根文件系统位置、内存大小等。这些参数必须和实际硬件配置一致否则内核可能找不到根文件系统或者控制台。一个实用的调试方法是在uboot命令行里手动设置bootargs然后用bootm命令启动内核观察内核的打印信息。如果内核打印出Kernel panic - not syncing: VFS: Unable to mount root fs说明根文件系统参数不对。如果打印出Uncompressing Linux...之后就没了可能是加载地址或者入口点不对。6.3 常见启动失败案例的复盘我遇到过几次比较典型的启动失败一次是内核加载地址和DDR的保留区域重叠导致内核被覆盖一次是bootargs里的控制台设备名写错了内核找不到串口还有一次是MMC分区的偏移量算错了内核读到了错误的数据。这些问题的共同点是uboot本身能正常启动串口有输出但内核起不来。排查的时候要充分利用uboot的命令行功能用md命令查看内存内容用mmc read命令读取存储介质逐步缩小问题范围。经验每次修改bootargs或者加载地址之后先用printenv确认环境变量已经生效再执行启动命令。不要假设修改会自动保存有些环境变量需要saveenv才会写入存储介质。7. 移植完成后的功能验证清单移植完成后不要急着宣布成功先按照以下清单逐项验证验证项验证方法预期结果串口输出上电观察串口终端打印uboot版本和启动信息DDR容量执行bdinfo命令显示的DRAM容量和实际一致MMC识别执行mmc list命令列出所有MMC设备MMC读写执行mmc read和mmc write读写操作成功无报错网络连通执行ping命令能ping通同网段设备环境变量保存修改后执行saveenv重启修改后的值仍然有效内核加载执行bootm命令内核正常启动进入控制台这份清单里的每一项都验证通过之后移植工作才算真正完成。如果某一项失败回到对应的章节排查不要跳过。8. 移植过程中积累的几条实战经验第一条经验是关于代码改动的粒度。每次只改一个地方改完立刻编译烧录验证。我见过有人一次性改了十几个文件结果板子起不来根本不知道是哪个改动导致的。移植是一个逐步逼近的过程不是一蹴而就的。第二条经验是关于参考文档的使用。芯片手册是必须看的但不要指望手册能告诉你所有答案。很多细节需要结合源码和实际测试来确认。比如串口的波特率分频系数手册里可能只给了公式但时钟源的实际频率需要从时钟初始化代码里反推。第三条经验是关于调试手段的储备。串口是最基本的但有时候串口本身就有问题这时候就需要备用的调试手段比如GPIO翻转加LED、JTAG调试器。多一种手段就多一条排查路径。第四条经验是关于版本管理。移植过程中会反复修改代码建议用Git管理源码每次修改前先提交一个版本这样改错了可以随时回退。我一般会在关键节点打Tag比如“串口能输出”“DDR自检通过”“内核能启动”方便后续回溯。第五条经验是关于心态。移植过程中遇到问题是常态不要指望一次成功。遇到问题的时候先冷静分析把问题拆解成小问题逐个击破。最怕的是心态崩了开始乱改代码那样只会让问题越来越复杂。最后分享一个我常用的技巧在uboot的board_init_f函数里加一个简单的进度打印每完成一个初始化步骤就打印一个字符。这样即使串口输出不完整也能通过打印的字符数量判断代码执行到了哪一步。这个技巧在排查早期启动问题时特别有用。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进