ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

U-Boot移植实战:从Kconfig到串口出字的完整流程与排坑指南

U-Boot移植实战:从Kconfig到串口出字的完整流程与排坑指南 1. 移植U-Boot先弄明白你手里到底有什么1.1 一块裸板怎么会走到Linux内核这一步拿到一块全新板子第一件事不是写应用、不是调系统而是得让一把“钥匙”把CPU从出厂状态里接进来。这把钥匙在绝大多数ARM Linux方案里就是U-Boot。你可以把它理解成一个“二级中转站”芯片内置的BootROM先把U-Boot拉起来U-Boot完成硬件初始化和内核加载再把你真正的操作系统交给CPU。很多刚接触嵌入式的人以为移植U-Boot是“从零写一个引导程序”其实完全不是这么回事。U-Boot在主流架构上已经支持了全球几千块板子芯片级的东西CPU初始化、Cache、MMU、架构相关代码、常见外设驱动你基本不用碰。你需要做的是把“你的板子”的信息准确地告诉U-Boot现有的框架用哪颗DDR、跑多少频率、串口挂在哪个引脚上、SD卡挂在哪个控制器上、启动介质和内核加载地址是多少。说得再直白一些移植U-Boot更像是在给一套标准流程填写一套半结构化的硬件档案而不是重写流程本身。这也解释了为什么现代U-Boot移植特别依赖Kbuild和Kconfig硬件差异被尽可能收敛到“配置项设备树”里代码只能维持一份。你今天看到configs/目录下一堆xxx_defconfig看到board/目录下的板级文件看到arch/arm/dts/里成千上万个dts文件这些都是同一套框架对“不同硬件档案”的实例化。理解了这个心智模型后面所有细节都好办了。1.2 先分清你属于哪一类移植SoC级移植。这是最硬核的一类工作集中在arch/arch/mach-xxx、arch/arch/cpu这样的位置要新增整个芯片家族的支持代码比如管理寄存器、时钟树、Cache和TLB、厂商ID识别等等。这类工作通常只有芯片方案公司和少数顶级BSP工程师在做个人玩家极少遇到。如果你不是要给一款完全没被U-Boot支持过的处理器写支持这段可以跳过。板级移植。这是最常见的场景SoC已经有人支持了你只是把同一颗或同系列SoC做在一块全新的板子上需要为这块板建配置、建设备树、写板级初始化代码。参考板往往就在官方支持列表里你的绝大部分工作就是从最近的参考板“分叉”出去改成你自己的硬件。我后面整篇文章主要讲的就是这个。功能级移植。板子本身能启动但你要加一个新功能从SPI NOR启动、加一个网络引导协议、把存储从eMMC换成SD卡、支持一个新的显示面板、加上fastboot或者AB分区等等。这类工作通常只是在既有板级支持上叠加配置和驱动风险最低也是很多人在做完板级移植之后会遇到的第一个“舒适区外任务”。热点里那一堆“freertos移植lvgl”“lvgl移植stm32”“easylogger移植stm32”其实就是同一套思路在不同项目上的体现代码模块本身大概率是好的你真正要做的是让它跑在你的硬件上下文里。1.3 和Kbuild打交道之前先把引导链分层画清楚移植U-Boot的时候脑子里一定要有这条完整的链芯片BootROM - SPL或TPLSPL - U-Boot proper - Linux内核BootROM是芯片出厂写死的你动不了。它根据启动引脚状态从SD卡、eMMC、SPI NOR、USB等介质里按固定偏移读取下一级镜像放进芯片内部的SRAM里执行。SRAM容量小往往只有几十到几百KB所以上一级镜像必须足够精简这就是SPL存在的意义。SPL完成DDR初始化之后再把完整的U-Boot主程序从存储介质读入DDR然后跳转执行。主程序跑起来你才能看到经典串口输出U-Boot 2024.04 (Feb 02 2024 - 16:00:00 0800) CPU: STM32MP157CAC Board: MYBOARD DRAM: 1 GiB MMC: sdmmc058005000: 0, sdmmc158005000: 1在这条链路里移植工作最关心的就三件事第一上一层怎么把自己加载出来第二DDR能不能被正确初始化第三串口能不能出字。后面两个直接决定你是否有能力调试所以几乎所有U-Boot移植教程都会让你先“让串口出字”。串口就是你在黑盒世界里的眼睛没有它后面全是盲人摸象。Kbuild这套构建系统要解决的就是如何把这根链条上的每一个环节根据你的配置编译、链接、打包成正确的镜像。所以你看先搞清楚引导链再看Kbuild一切才不突兀。2. Kbuild移植基础一次make的全过程2.1 一份defconfig是怎么变成u-boot.bin的我见过太多人第一次接触U-Boot构建时被那一串make搞得晕头转向其实拆开看就三步export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- make stm32mp157_myboard_defconfig make -j8ARCH告诉构建系统你用的是哪个架构CROSS_COMPILE告诉它编译器前缀。第一行make xxx_defconfig做的事是读取configs/xxx_defconfig文件把它定义的CONFIG_*项灌进.config再通过一系列Kconfig规则把依赖项补齐。第二行make才开始真正的编译和链接。Kbuild在这里的完整工作流大致是这样的scripts/kconfig/conf解析Kconfig树和defconfig生成.config然后由conf继续生成三套中间产物——include/config/auto.conf给Makefile用的配置、include/generated/autoconf.mk也是给Makefile用的但偏符号级、include/generated/autoconf.h给C源码用的头文件。往下构建系统开始递归进入各个子目录按Kconfig和Makefile里的规则决定哪些.c文件要编、编成.o之后怎么归并、最后在顶层把全部目标文件链接成u-bootELF再用objcopy产出u-boot.bin。如果你是第一次看这个流程一个常见的困惑是“我改了defconfig里的一个CONFIG为什么有时候重新make不生效”因为make xxx_defconfig会重新生成.config但如果你直接改.configKbuild不一定会检测到所有依赖变化。规范做法是改defconfig源文件然后重新跑一次make xxx_defconfig再加make -j。踩过的人都知道这一步省不得。2.2 obj-y、Kconfig和Makefile谁指挥谁Kbuild这套体系的核心是三个角色各司其职Kconfig负责“有哪些选项可选”defconfig和menuconfig负责“选了什么”Makefile里的obj列表负责“选中之后编什么”。obj-y意思是“无条件编译”比如drivers/serial/Makefile里最常见的写法obj-$(CONFIG_DEBUG_UART) debug_uart.o obj-$(CONFIG_SYS_NS16550) ns16550.o当Kconfig配置了CONFIG_DEBUG_UARTyobj-$(CONFIG_DEBUG_UART)就是obj-y这个源文件被编译进U-Boot如果不选这条语句就变成obj-什么都没有。你可以把obj-$(CONFIG_XXX)理解成一个“开关控制的菜篮子”Kbuild编译时只装篮子里有的菜。Kconfig本身是一种带依赖关系的描述语言常用关键字就那几个bool定义开关型选项int/hex定义数字型选项default给默认值depends on限定“只有满足某个前提才显示这个选项”select表示“选了我就必须连同把某个其他选项也选了”imply表示“选了我默认建议选某个选项但不强制”。config TARGET_MYBOARD bool Support MyBoard STM32MP157 board select STM32MP157 select DM_SERIAL select SYS_NS16550select在板级Kconfig里特别常见因为很多底层依赖你不想让用户选择比如“你是块STM32MP157的板子就必须允许STM32MP157这个SoC选项被选中”。这些依赖关系处理不好最常见的报错就是Kconfig向你抱怨“aborted dependency”或者直接给你的配置里多出/少掉一堆莫名其妙的符号。2.3 那些藏在构建过程里的关键生成物写Makefile和Kconfig文件时心里最好始终有这三样东西include/config/auto.conf、include/generated/autoconf.h、以及链接脚本。auto.conf是所有Makefile共享的真相来源。你在子目录Makefile里写的obj-$(CONFIG_...)展开时读取的就是这份文件。autoconf.h则是C代码的真相来源你源码里写#ifdef CONFIG_DEBUG_UART编译时其实就是在查这份生成头文件。注意这里不是直接查.configCONFIG_DEBUG_UARTy和#define CONFIG_DEBUG_UART 1是Kbuild替你完成的一次“翻译”。链接脚本也是生成的。U-Boot在链接阶段会从arch/arch/cpu/u-boot.lds这个模板展开出真正的u-boot.lds其中最关键的是CONFIG_SYS_TEXT_BASE——U-Boot主程序觉得自己应该被加载到哪个地址。SPL也有自己的链接脚本对应CONFIG_SPL_TEXT_BASE。这块我在后面“常见问题”里还会讲因为TEXT_BASE配错是新手翻车率最高的点之一。构建完成后你会在根目录看到一堆产物常用的这几个要认识u-bootELF带调试信息、u-boot.bin纯二进制、u-boot.img用mkimage打包过的镜像带header、spl/u-boot-spl.binSPL阶段二进制、u-boot.dtb设备树二进制。SPL镜像烧到存储介质的位置、U-Boot主程序镜像烧到存储介质的位置都由你板卡启动方案决定通常芯片参考手册的“BootROM”章节写得清清楚楚。3. 从头移植一块新板从零到串口出字3.1 先花时间找到参考板而不是急着抄作业移植U-Boot最忌讳的事情就是拿到板子直接复制一份别人的defconfig就开干。参考板的意义在于“和你足够接近但又足够简单”它决定了你后面所有调试工作的起点。举例来说假设你手里的板子用的是STM32MP157这颗异构双核Cortex-A7芯片2GB DDR3、SD卡启动、串口UART4。我的选择顺序是这样先看arch/arm/mach-stm32mp/Kconfig里有哪些TARGET_XXX找到同样是STM32MP157、同样是DDR3、同样从SD/eMMC启动的官方评估板比如stm32mp157d-dk1或stm32mp157c-ev1用它的configs/stm32mp157d-dk1_defconfig作为种子。然后对比你的原理图看差异点在哪里DDR容量和颗粒型号、串口引脚、以太网PHY、LED/按键、PMIC型号。参考板不是“不用看原理图也能用”而是“有了它你至少可以拿着差异清单去查资料而不是拿着空白板子拍脑袋”。这一步还牵扯到一个被很多人忽略的问题选一个你能轻松找到BSP参考实现的分支。如果你选的板子是某个SoC家族里冷门的衍生型号而参考板又是官方做出来的整板方案那中间隔着的大量寄存器差异会让你非常痛苦。宁可参考板稍微旧一点也别选一个和你板子只有半毛钱关系的“近亲”。3.2 建立板级目录Kconfig、MAINTAINERS、Makefile车子准备好了开始动工。以我的myboard为例需要建立的目录结构通常是board/mycompany/myboard/ ├── Kconfig ├── MAINTAINERS ├── Makefile └── myboard.cKconfig负责把这块板注册进U-Boot选择系统。最核心的是定义TARGET_MYBOARD以及告诉构建系统板名、厂商名、配置头文件名if TARGET_MYBOARD config SYS_BOARD default myboard config SYS_VENDOR default mycompany config SYS_CONFIG_NAME default myboard endifSYS_BOARD决定board/mycompany/myboard这个路径怎么被引用SYS_VENDOR决定board/mycompany这一层SYS_CONFIG_NAME决定后续include/configs/myboard.h这个名字。这三个符号不是写给人看的是Kbuild用来拼路径的一旦拼不上后面构建系统根本找不到你的板级文件。Makefile是板目录的编译入口obj-y myboard.omyboard.c则是板级初始化函数的家常见的成员你一定认识int board_init(void) { gd-bd-bi_boot_params 0xc0000000; return 0; } int board_late_init(void) { return 0; } int dram_init(void) { gd-ram_size PHYS_SDRAM_SIZE; return 0; }这些函数是U-Boot在引导不同阶段调用的每个函数缺了或者返回错误启动流程就会在对应位置提前中断。对于量产板子board_init里往往还有IO扩展器、PMIC、板载LED之类的外设初始化和检测逻辑。3.3 defconfig和设备树双管齐下板级目录建好后要注册到全局Kconfig入口。不同架构入口位置不同ARM平台一般要去arch/arm/Kconfig找config ARCH_STM32MP这种SoC入口它下面有if ARCH_STM32MP ... source board/mycompany/myboard/Kconfig ... endif这样的结构。你可以理解为每个board/xxx/yyy/Kconfig都要被某个source语句“挂”到Kconfig树上否则make menuconfig根本看不到你这块板。接着创建configs/myboard_defconfig。以STM32MP157为例最小可启动的defconfig应该有下面这些关键项CONFIG_ARMy CONFIG_ARCH_STM32MPy CONFIG_SYS_MALLOC_LEN0x2000000 CONFIG_TARGET_MYBOARDy CONFIG_DEFAULT_DEVICE_TREEstm32mp157-myboard CONFIG_DEBUG_UARTy CONFIG_DEBUG_UART_BASE0x40010000 CONFIG_DEBUG_UART_CLOCK24000000CONFIG_DEFAULT_DEVICE_TREE指定了默认设备树源文件的名字CONFIG_DEBUG_UART开启早期调试串口——这个功能能在极其早期把字符打出来是串口出字最快的一条路。不同SoC的DEBUG_UART_BASE和DEBUG_UART_CLOCK不一样查参考手册的UART内存映射和时钟章节即可。设备树做出来的是arch/arm/dts/stm32mp157-myboard.dts。最省事的做法是从参考板的stm32mp157d-dk1.dts复制然后修改model、compatible、内存节点memoryc0000000reg要改容量、串口别名、SD卡mmc别名等。改完别忘了把新dtb加进arch/arm/dts/Makefiledtb-$(CONFIG_ARCH_STM32MP) stm32mp157-dk1.dtb dtb-$(CONFIG_ARCH_STM32MP) stm32mp157-myboard.dtb到这里实际上你已经完成了一大半。很多人会问“老式Board头文件还要不要”我的建议是跟随你参考板的风格。如果参考板已经在走“Kconfig设备树”的纯新式路线你就别写include/configs/myboard.h如果参考板还在用一堆#define CONFIG_SYS_*你也别硬扛着新式写法另起炉灶会让调试时很难对照参考实现。适配移植不是炫技优先保证可对比性。3.4 让串口先出字最小化的验证路径一切就绪之后构建export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- make stm32mp157_myboard_defconfig make -j8看到产物之后烧录方式因板而异。STM32MP157这类芯片一般是从SD卡启动SPL放在SD卡特定偏移通常是1KB处U-Boot主程序放在后续偏移。很多SoC的文档里干脆提供了tools/mkimage的打包参数照着用就行。把SD卡插入板子串口接好设置波特率常见115200上电看串口有没有输出。如果运气够好你会直接看到从SPL到U-Boot的完整日志。如果运气不好串口一片死寂那就进入下一章——排错。严格来说串口没有输出才是移植的“新手村”所有老手都从这里摔过。4. 移植中一定会踩的坑编译、链接、运行4.1 编译期的坑头文件、配置同步和旧产物编译期问题通常不是语法错误而是“配置没同步”。我见过最多的场景改了defconfig里的一个选项然后直接make -j8结果发现行为没有任何变化。原因是Kbuild的依赖跟踪没有覆盖到你改的那条路径正确的流程我刚才已经强调过改defconfig之后务必重新执行make xxx_defconfig让Kconfig重新解析并刷新auto.conf、autoconf.h和autoconf.mk。第二个坑是“改了头文件但编译没生效”。如果你在include/configs/下面改了东西或者改了某些头文件Kbuild不像make直呼OK那样即时响应所有细粒度依赖。保险做法是一次干净的重新构建make mrproper make xxx_defconfig make -j8mrproper会把所有生成物清掉。别嫌慢移植阶段多花两分钟全量重编远比在一个脏树上反复猜“这个改动到底生没生效”来得值。第三个坑是目录Makefile写错变量名。子目录里常见的是obj-y但你有时也会看到obj-m、lib-y、extra-y。obj-m在U-Boot里不常用lib-y用于生成静态库extra-y用于生成非直接链接的中间文件。新手如果从Linux内核驱动教程里复制了一个obj-m过来编译确实能过但链接时模块根本没进U-Boot于是出现“函数找不到”的诡异链接错误。4.2 链接期的坑段重叠、TEXT_BASE和SPL大小链接期一等一的坑是“段重叠”报错最常见的形式是Error: section .text overlaps section .rodata或者链接脚本抱怨“地址回归”。这事九成和CONFIG_SYS_TEXT_BASE有关。U-Boot主程序的镜像在链接时被安排在CONFIG_SYS_TEXT_BASE这个地址它必须落在DDR实际存在的物理地址范围里还要避开自己在DDR里的数据堆和栈区域。如果参考板用的是0xc0000000你换成DDR从0x80000000开始的板子却忘了改链接器把.text放在了一片不存在的内存上结果要么链接报错要么起来就挂。SPL的大小问题同样让人头疼。SPL跑在芯片的SRAM里SRAM就那么大超了就是超了。报错往往长这样SPL image has exceeded size limit: 0x20000处理方法有三板斧一是看CONFIG_SPL_MAX_SIZE或CONFIG_SPL_TEXT_BASE相关限制确认它和芯片SRAM容量匹配二是裁剪不需要的驱动和功能把不用的CONFIG_SPL_*关掉三是如果SPL本身不需要支持某些功能比如SPL阶段不可能接显示器就别开那些驱动。压SPL大小其实是一个很好的练习它逼你理解哪些代码被编进了SPL哪些只编进了主U-Boot这是Kbuild“同一套代码不同配置编译出不同产品”的精髓所在。4.3 运行期的坑DDR、时钟和板级initcall跨过编译和链接启动过程里的问题更折磨人。串口完全没有输出时优先排查三件事Debug UART配置是否正确、SPL有没有被正确加载、DDR初始化是否成功。CONFIG_DEBUG_UART_BASE和时钟如果不对U-Boot的早期打印就出不来但这并不代表它没在跑。你可以量一下UART TX引脚看有没有电平翻转或者用JTAG去挂一下CPU的PC指针看它到底卡在哪个地址。如果你的SoC支持建议一开始就只用CONFIG_DEBUG_UART不要依赖完整的串口驱动初始化这样可以快速区分“UART驱动还没起来”和“卡死在DDR初始化”两种场景。DDR初始化失败是板级移植里最经典的“无声死法”。U-Boot在SPL阶段通过dram_init和厂商DDR训练代码配置内存控制器任何一项参数错了时序、引脚阻抗、bank数量都会导致CPU在访问DDR时挂死。这时候你往往连串口都看不到只能用JTAG、逻辑分析仪、甚至是看复位引脚状态变化来推断。我自己的经验是先找官方DDR测试工具很多厂商提供在没有任何bootloader的情况下先把DDR调通再回过来移植U-Boot。DDR调通移植就算成功了70%。initcall阶段的坑也别小看。U-Boot主程序启动时会按顺序调用一堆init_*函数任何一个返回-EPERM都可能中断启动。常见现象是“串口打印到某一行就停住”比如board_late_init里你加了I2C访问PMIC的代码而I2C总线又没有上拉电阻函数一直等总线应答启动就永远卡在这里。这时候可以把函数里面的操作临时注释掉逐步缩小范围不要指望一次全通。把这些坑踩完你基本已经能把U-Boot“带活”了。剩下的是大量细节打磨环境变量默认值、启动参数、bootcmd、分区表、显示、网络、fastboot等等。但主轴已经立住了。5. 可以让你少走很多弯路的几条经验5.1 用git和diff管理每一次验证移植最怕的不是改错而是“不知道自己改了什么导致现在好了/坏了”。我的习惯是拿到参考板的源码先原地git init并提交一版“干净的官方代码”此后每一次尝试改动小到调一个CONFIG_SYS_TEXT_BASE都单独提交。每完成一个里程碑串口出字、DDR完成、SD卡识别、进内核就在commit message里写清楚现象。这样一旦某次改动把之前的成果搞坏了git diff能立刻告诉你嫌疑位置更重要的是这种记录最终会变成你面向同事或甲方交付时的移植报告价值远超你想象。5.2 参考BootROM和SDK的初始化序列很多人闷头在U-Boot里改半天其实芯片厂商的SDK和参考代码已经把标准答案写好了。BootROM章节会画出启动介质内部的镜像布局这是你烧录SPL时偏移量的依据厂商的裸机或RTOS例程里有完整的DDR初始化序列、PLL频率表、电源启动时序这些可以直接对照U-Boot里的arch/arch/mach-xxx代码看厂商驱动和U-Boot的差异在哪。不要觉得“用厂商的东西是老路”恰恰相反移植的本质就是理解别人的实现再把它搬进新框架里。官方BSP、参考板代码、U-Boot主线代码三份对照着看是最快的路径。5.3 从mainline到vendor fork的取舍另一个现实问题是你到底用主线U-Boot还是用SoC厂商的fork我的建议是如果你的SoC已经进了主线U-Boot优先基于主线做移植。主线的好处是持续维护、工具链现代、调试手段跟得上。厂商fork往往带着完整但臃肿的BSP有时候还锁定老版本编译器。但厂商fork里往往有主线还没有的新驱动代码比如新DDR颗粒的支持、新显示控制器的驱动这时候正确姿势是“从厂商fork里摘驱动移植回主线”而不是反过来。这条道理同样适合同一SoC家族的新板。很多时候你需要的不是整棵树的移植而是把参考板相关的一小块代码板级目录、defconfig、dts、部分驱动摘出来嵌进一个新基线的U-Boot里。学会“摘代码”比学会“整树搬运”重要得多。5.4 Kbuild的能力不止适用于U-Boot最后说点题外话。今天热点里大量“xxx移植stm32”这类工作比如freertos移植、lvgl移植、easylogger移植stm32、nanomodbus移植裸机表面看跟U-Boot毫无关系但底层思维高度一致先看懂模块的构建和配置方式再把它嵌入目标工程。U-Boot里的Kconfig/Kbuild经验放到Linux内核、Zephyr乃至一些大型C/C项目里都能直接复用因为这套“Kconfig选项—Makefile变量—构建规则—生成头文件”的体系已经被行业验证了几十年。我自己这几年的体会是移植U-Boot最值钱的回报不是“板子能启动”那一刻的成就感而是你被迫把芯片手册、Boot流程、构建系统、设备树、驱动模型这几座孤岛串起来。这个串的过程是任何教程都给不了你的。下次再把一块新板从死寂的串口带到Linux shell前你会发现自己不再慌因为你知道每一层在干什么也知道出了问题该去查哪里。
返回列表