到u-boot:嵌入式Linux啟動(dòng)流程與實(shí)戰(zhàn)指南)
1. 從點(diǎn)燈到跑系統(tǒng)為什么我勸你盡早啃下 u-boot剛?cè)胄心菚?huì)兒我和大多數(shù)人一樣抱著51單片機(jī)、STM32開(kāi)發(fā)板寫(xiě)個(gè)GPIO點(diǎn)燈、調(diào)個(gè)DHT11溫濕度、LCD1602顯示字符就覺(jué)得自己已經(jīng)入門(mén)嵌入式了。后來(lái)去面試面試官問(wèn)我一句你板子上的Linux是怎么啟動(dòng)起來(lái)的我當(dāng)場(chǎng)卡殼。那一刻我才意識(shí)到單片機(jī)那套上電就跑main函數(shù)的世界和真正的嵌入式Linux系統(tǒng)之間隔著一道很深的溝。而這道溝上最關(guān)鍵的一座橋就是u-boot。這篇文章不是又一篇u-boot命令大全的搬運(yùn)而是我想以一個(gè)踩過(guò)坑的從業(yè)者身份把為什么從單片機(jī)轉(zhuǎn)向u-boot、u-boot到底在系統(tǒng)里扮演什么角色、怎么從零把它跑起來(lái)、遇到問(wèn)題怎么排查這幾件事講透。核心關(guān)鍵詞就幾個(gè)u-boot、嵌入式、單片機(jī)、Linux內(nèi)核、ARM64。如果你現(xiàn)在還在糾結(jié)應(yīng)用層開(kāi)發(fā)是不是嵌入式這種問(wèn)題或者剛學(xué)完51單片機(jī)、STM32想往上走一層那這篇內(nèi)容就是寫(xiě)給你的。我會(huì)盡量用生活化的類(lèi)比把啟動(dòng)流程講清楚也會(huì)給出可以直接抄作業(yè)的編譯、燒錄、調(diào)試步驟讓你看完能真正動(dòng)手而不是看完只會(huì)點(diǎn)頭。先說(shuō)結(jié)論單片機(jī)讓你理解硬件怎么被軟件控制而u-boot讓你理解一個(gè)完整的操作系統(tǒng)是怎么被一步步喚醒的。前者是手藝后者是體系。兩者都重要但如果你想在嵌入式這條路上走得更遠(yuǎn)u-boot是繞不過(guò)去的一課。2. 先搞清楚 u-boot 到底是個(gè)什么東西2.1 用搬家類(lèi)比理解 Bootloader 的定位你可以把一塊嵌入式開(kāi)發(fā)板想象成一間剛交房、什么都沒(méi)裝的毛坯房。CPU上電那一刻內(nèi)存是空的硬盤(pán)Flash/eMMC里躺著操作系統(tǒng)鏡像但CPU自己不知道怎么把它搬進(jìn)來(lái)、怎么布置家具、怎么通水通電。這時(shí)候就需要一個(gè)搬家隊(duì)長(zhǎng)——它先把自己安頓好然后負(fù)責(zé)把真正的住戶(hù)Linux內(nèi)核請(qǐng)進(jìn)來(lái)再把各種家具設(shè)備樹(shù)、根文件系統(tǒng)擺到位最后把控制權(quán)交給住戶(hù)。這個(gè)搬家隊(duì)長(zhǎng)就是Bootloader而u-boot是目前嵌入式領(lǐng)域用得最廣、生態(tài)最成熟的那一個(gè)。u-boot的全稱(chēng)是Universal Boot Loader從名字就能看出它的野心——通用。它支持ARM、ARM64、MIPS、RISC-V、x86等多種架構(gòu)支持從NAND、NOR、eMMC、SD卡、網(wǎng)絡(luò)、USB等多種介質(zhì)加載系統(tǒng)。你在熱搜里看到的qemu模擬arm64、arm64和x64有什么區(qū)別其實(shí)都和u-boot的跨架構(gòu)能力直接相關(guān)。x64是我們?nèi)粘C的架構(gòu)而arm64也叫AArch64是當(dāng)前主流嵌入式Linux和移動(dòng)設(shè)備的核心架構(gòu)u-boot對(duì)這兩者的支持邏輯是相通的但啟動(dòng)細(xì)節(jié)差別很大。2.2 u-boot 在啟動(dòng)鏈條里的精確位置一個(gè)典型的ARM64嵌入式Linux啟動(dòng)鏈條是這樣的芯片內(nèi)部固化的BootROMCPU一上電先執(zhí)行芯片廠商燒死在ROM里的一小段代碼它負(fù)責(zé)從固定位置比如SD卡、SPI Flash加載第一級(jí)引導(dǎo)程序。SPL / TPL可選如果內(nèi)存還沒(méi)初始化u-boot會(huì)先跑一個(gè)精簡(jiǎn)版Secondary Program Loader把DDR初始化好。u-boot proper完整版這才是我們平時(shí)說(shuō)的u-boot它有了完整的內(nèi)存和驅(qū)動(dòng)能跑命令行、能加載內(nèi)核。Linux內(nèi)核u-boot把內(nèi)核鏡像Image/zImage和設(shè)備樹(shù)dtb加載到內(nèi)存指定地址跳轉(zhuǎn)執(zhí)行。根文件系統(tǒng)內(nèi)核掛載rootfs啟動(dòng)init進(jìn)程系統(tǒng)正式跑起來(lái)。提示很多人以為u-boot只是引導(dǎo)一下其實(shí)它還承擔(dān)了硬件初始化、環(huán)境變量管理、固件升級(jí)、快速啟動(dòng)優(yōu)化等大量工作。在量產(chǎn)設(shè)備里u-boot的啟動(dòng)時(shí)間往往是被反復(fù)優(yōu)化的對(duì)象。2.3 單片機(jī)和 u-boot 世界的本質(zhì)差異對(duì)比維度單片機(jī)51/STM32u-boot Linux啟動(dòng)方式上電直接跑main多級(jí)引導(dǎo)BootROM→SPL→u-boot→內(nèi)核內(nèi)存模型直接操作物理地址無(wú)MMU開(kāi)啟MMU虛擬內(nèi)存分頁(yè)管理程序規(guī)模幾KB到幾百KBu-boot本身幾百KB內(nèi)核幾MB起開(kāi)發(fā)語(yǔ)言C 寄存器操作C 匯編 設(shè)備樹(shù) 腳本調(diào)試手段仿真器、串口打印串口、JTAG、網(wǎng)絡(luò)tftp、gdb典型場(chǎng)景家電、傳感器節(jié)點(diǎn)網(wǎng)關(guān)、工控機(jī)、車(chē)載、服務(wù)器看懂這張表你就明白為什么我說(shuō)單片機(jī)入門(mén)和u-boot進(jìn)階是兩個(gè)世界。單片機(jī)里你寫(xiě)GPIO_SetBits()就完事而在u-boot里你要理解時(shí)鐘樹(shù)、DDR訓(xùn)練、設(shè)備樹(shù)節(jié)點(diǎn)、加載地址這些概念。但好消息是這些概念一旦打通你看任何一款A(yù)RM64芯片的啟動(dòng)流程都會(huì)豁然開(kāi)朗。3. 動(dòng)手前的準(zhǔn)備環(huán)境、工具與選型思路3.1 硬件與軟件環(huán)境怎么選如果你是新手我不建議一上來(lái)就買(mǎi)最貴的開(kāi)發(fā)板。最省心的入門(mén)路徑是一塊支持主線(xiàn)u-boot的ARM64開(kāi)發(fā)板 一張SD卡 一根USB轉(zhuǎn)串口線(xiàn)。像樹(shù)莓派、瑞芯微、全志、NXP i.MX系列都有大量社區(qū)資料。如果你想零成本先感受流程用QEMU模擬arm64是絕佳選擇熱搜里的qemu模擬arm64就是這個(gè)思路。軟件環(huán)境我推薦在Linux下開(kāi)發(fā)Ubuntu 20.04/22.04都行因?yàn)榻徊婢幾g工具鏈、設(shè)備樹(shù)編譯器、燒錄工具在Linux下最順手。如果你只有Windows可以用WSL2或者裝個(gè)虛擬機(jī)。熱搜里提到的銀河麒麟v10系統(tǒng)桌面版更換linux內(nèi)核版本4.19這類(lèi)操作本質(zhì)上也是在Linux環(huán)境下折騰內(nèi)核和引導(dǎo)思路是相通的。需要準(zhǔn)備的核心工具交叉編譯工具鏈ARM64用aarch64-linux-gnu-ARM32用arm-linux-gnueabihf-。設(shè)備樹(shù)編譯器dtc把.dts編譯成.dtb。串口工具minicom、picocom或screen用來(lái)接開(kāi)發(fā)板的調(diào)試串口。燒錄工具dd命令、fastboot、廠商專(zhuān)用工具如瑞芯微的upgrade_tool。QEMUqemu-system-aarch64用于無(wú)硬件模擬。3.2 為什么優(yōu)先選主線(xiàn) u-boot 而不是廠商版這是很多新手會(huì)踩的坑。廠商提供的u-boot往往改得面目全非加了一堆私有驅(qū)動(dòng)和腳本你照著學(xué)很容易被帶偏。而主線(xiàn)u-boot從官方git倉(cāng)庫(kù)拉取代碼結(jié)構(gòu)清晰、提交記錄規(guī)范、社區(qū)活躍遇到問(wèn)題搜得到答案。我的建議是先用主線(xiàn)u-boot在QEMU或開(kāi)發(fā)板上跑通一次完整啟動(dòng)理解標(biāo)準(zhǔn)流程再去啃廠商版就輕松了。注意廠商版u-boot雖然亂但量產(chǎn)項(xiàng)目里你往往不得不用它因?yàn)樗薉DR初始化參數(shù)、PMIC配置這些芯片原廠才知道的細(xì)節(jié)。學(xué)習(xí)用主線(xiàn)干活用廠商版這個(gè)策略最穩(wěn)。3.3 獲取源碼與目錄結(jié)構(gòu)速覽從官方倉(cāng)庫(kù)拉取源碼git clone https://source.denx.de/u-boot/u-boot.git cd u-boot git checkout v2024.01 # 選一個(gè)穩(wěn)定tag別直接用master拉下來(lái)后先別急著編譯花十分鐘看看目錄結(jié)構(gòu)這對(duì)后面定位問(wèn)題極其重要arch/arm/ARM架構(gòu)相關(guān)arch/arm/cpu/armv8/是ARM64的啟動(dòng)匯編。board/各廠商開(kāi)發(fā)板配置。configs/各板子的默認(rèn)配置xxx_defconfig。drivers/驅(qū)動(dòng)串口、網(wǎng)卡、MMC、USB都在這。dts/設(shè)備樹(shù)源文件ARM64的放在arch/arm/dts/。common/通用邏輯包括命令行、環(huán)境變量。cmd/各種u-boot命令的實(shí)現(xiàn)??炊@個(gè)結(jié)構(gòu)你就知道改一個(gè)板子的啟動(dòng)行為該去哪個(gè)目錄找代碼了。4. 從零編譯并跑通一次 u-boot 啟動(dòng)4.1 選擇目標(biāo)板與 defconfig假設(shè)我們用QEMU的ARM64虛擬板qemu_arm64_defconfig這是最干凈的練手環(huán)境。先確認(rèn)配置make qemu_arm64_defconfig這一步會(huì)把configs/qemu_arm64_defconfig里的默認(rèn)配置展開(kāi)成.config。你可以用make menuconfig進(jìn)去看看重點(diǎn)看幾個(gè)選項(xiàng)串口波特率、環(huán)境變量存儲(chǔ)位置、是否開(kāi)啟命令行。新手最容易忽略的是環(huán)境變量存儲(chǔ)介質(zhì)如果配錯(cuò)u-boot每次啟動(dòng)都會(huì)報(bào)bad CRC。4.2 交叉編譯與產(chǎn)物解讀設(shè)置好工具鏈前綴后編譯export CROSS_COMPILEaarch64-linux-gnu- make -j$(nproc)編譯完成后重點(diǎn)看這幾個(gè)產(chǎn)物u-bootELF格式帶符號(hào)用于調(diào)試。u-boot.bin純二進(jìn)制用于燒錄。u-boot.map內(nèi)存映射文件排查地址問(wèn)題必備。u-boot.srec另一種格式某些燒錄器用。實(shí)操心得編譯報(bào)錯(cuò)時(shí)先看是不是工具鏈版本不匹配。u-boot對(duì)gcc版本比較敏感太新或太舊都可能出問(wèn)題。我一般用Linaro或ARM官方發(fā)布的工具鏈穩(wěn)定。4.3 用 QEMU 跑起來(lái)并進(jìn)入命令行QEMU啟動(dòng)命令大致如下qemu-system-aarch64 -M virt -cpu cortex-a57 -nographic \ -bios u-boot.bin跑起來(lái)后你會(huì)看到串口輸出一大段啟動(dòng)日志最后停在提示符這就是u-boot的命令行。到這里恭喜你你已經(jīng)完成了從單片機(jī)點(diǎn)燈到跑起一個(gè)Bootloader的跨越。接下來(lái)可以試試幾個(gè)基礎(chǔ)命令bdinfo查看板級(jí)信息包括內(nèi)存起始地址、大小。printenv打印所有環(huán)境變量。mmc list列出MMC設(shè)備。help查看所有可用命令。4.4 加載內(nèi)核與設(shè)備樹(shù)的完整流程在真實(shí)板子上u-boot加載內(nèi)核一般分幾步。以從SD卡加載為例# 把內(nèi)核和設(shè)備樹(shù)加載到內(nèi)存 fatload mmc 0:1 0x40080000 Image fatload mmc 0:1 0x48000000 board.dtb # 設(shè)置啟動(dòng)參數(shù) setenv bootargs consolettyAMA0,115200 root/dev/mmcblk0p2 rw # 跳轉(zhuǎn)啟動(dòng) booti 0x40080000 - 0x48000000這里的地址不是隨便填的。0x40080000是ARM64 Linux內(nèi)核約定的加載地址內(nèi)核入口偏移0x80000booti是專(zhuān)門(mén)啟動(dòng)ARM64內(nèi)核的命令A(yù)RM32用bootz。設(shè)備樹(shù)地址要避開(kāi)內(nèi)核占用的區(qū)域具體看你的內(nèi)存布局。注意bootargs里的console參數(shù)必須和實(shí)際串口一致否則你啟動(dòng)后看不到任何內(nèi)核日志會(huì)誤以為卡死了。這個(gè)坑我踩過(guò)不止一次。5. 深入核心u-boot 啟動(dòng)流程與關(guān)鍵機(jī)制拆解5.1 從匯編入口到 board_init 的完整鏈路u-boot在ARM64上的啟動(dòng)入口在arch/arm/cpu/armv8/start.S。這段匯編干的事非常關(guān)鍵設(shè)置異常向量表、關(guān)閉MMU和緩存、初始化棧指針、然后跳到C語(yǔ)言的board_init_f。為什么要先關(guān)MMU因?yàn)榇藭r(shí)內(nèi)存還沒(méi)初始化虛擬地址映射還沒(méi)建立只能跑物理地址。board_init_f階段會(huì)做一系列初始化串口讓你能看到打印、定時(shí)器、內(nèi)存控制器。然后進(jìn)入board_init_r這時(shí)內(nèi)存已經(jīng)可用u-boot會(huì)重定位自己到內(nèi)存高端接著初始化各種驅(qū)動(dòng)最后進(jìn)入主循環(huán)等待命令或自動(dòng)啟動(dòng)。理解這條鏈路的意義在于當(dāng)你的板子卡在某個(gè)階段沒(méi)有任何輸出時(shí)你能根據(jù)卡在哪一步快速定位問(wèn)題。比如完全沒(méi)有串口輸出多半是時(shí)鐘或串口引腳配置問(wèn)題有輸出但卡在DDR初始化那就是內(nèi)存參數(shù)問(wèn)題。5.2 設(shè)備樹(shù)在 u-boot 里的作用設(shè)備樹(shù)Device Tree是ARM Linux體系里描述硬件的方式。u-boot自己也用設(shè)備樹(shù)叫控制用設(shè)備樹(shù)u-boot.dtb它告訴u-boot這塊板子有哪些外設(shè)、地址是多少、用什么驅(qū)動(dòng)。這和內(nèi)核用的設(shè)備樹(shù)是兩份但通常同源。熱搜里深入解析omap-l137 dsp內(nèi)存映射與c674x緩存架構(gòu)這類(lèi)內(nèi)容本質(zhì)也是在講硬件描述和內(nèi)存布局。設(shè)備樹(shù)的核心節(jié)點(diǎn)包括chosen啟動(dòng)參數(shù)比如bootargs。memory內(nèi)存起始地址和大小。soc各種外設(shè)控制器。aliases設(shè)備別名方便引用。改設(shè)備樹(shù)最常見(jiàn)的場(chǎng)景是換了一塊屏、改了一個(gè)串口、調(diào)整了內(nèi)存大小。這時(shí)候你改.dts重新編譯成.dtb再讓u-boot加載新的dtb即可。5.3 環(huán)境變量的存儲(chǔ)與讀寫(xiě)機(jī)制環(huán)境變量是u-boot的靈魂。bootcmd決定默認(rèn)啟動(dòng)什么bootargs決定內(nèi)核怎么啟動(dòng)ipaddr、serverip決定網(wǎng)絡(luò)下載。它們存在哪通常存在Flash或eMMC的一個(gè)固定分區(qū)里帶CRC校驗(yàn)。如果CRC校驗(yàn)失敗u-boot會(huì)加載一套默認(rèn)環(huán)境變量并提示bad CRC, using default environment。這時(shí)候你saveenv一下就能修復(fù)。但如果存儲(chǔ)介質(zhì)本身有問(wèn)題就會(huì)反復(fù)報(bào)錯(cuò)需要檢查分區(qū)偏移和大小配置。實(shí)操心得調(diào)試階段我習(xí)慣把環(huán)境變量存在內(nèi)存里配置CONFIG_ENV_IS_NOWHERE這樣每次重啟都是干凈的默認(rèn)值避免被上一次的錯(cuò)誤配置干擾。等調(diào)試穩(wěn)定了再改回Flash存儲(chǔ)。5.4 網(wǎng)絡(luò)啟動(dòng)tftp 與 nfs 的高效調(diào)試組合量產(chǎn)前調(diào)試內(nèi)核最爽的方式是網(wǎng)絡(luò)啟動(dòng)內(nèi)核和設(shè)備樹(shù)放服務(wù)器上用tftp下載根文件系統(tǒng)用nfs掛載。這樣改一次內(nèi)核不用重新燒錄重啟板子就行效率提升十倍。配置步驟大致是setenv ipaddr 192.168.1.100 setenv serverip 192.168.1.10 setenv bootcmd tftp 0x40080000 Image; tftp 0x48000000 board.dtb; booti 0x40080000 - 0x48000000 setenv bootargs consolettyAMA0,115200 root/dev/nfs nfsroot192.168.1.10:/nfsroot ip192.168.1.100 saveenv這套組合是嵌入式Linux開(kāi)發(fā)的標(biāo)配熱搜里嵌入式linux項(xiàng)目、嵌入式內(nèi)核源碼相關(guān)的實(shí)戰(zhàn)幾乎都離不開(kāi)它。6. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄6.1 啟動(dòng)無(wú)輸出、卡死、反復(fù)重啟怎么查這是新手最常遇到的三大類(lèi)問(wèn)題我整理成速查表現(xiàn)象可能原因排查方向串口完全無(wú)輸出串口引腳/時(shí)鐘配置錯(cuò)、波特率不對(duì)查設(shè)備樹(shù)串口節(jié)點(diǎn)、確認(rèn)波特率115200有輸出但卡在DDRDDR初始化參數(shù)錯(cuò)對(duì)比廠商提供的DDR配置反復(fù)重啟看門(mén)狗未喂、電源不穩(wěn)關(guān)閉看門(mén)狗、測(cè)電源紋波bad CRC環(huán)境變量存儲(chǔ)介質(zhì)配置錯(cuò)檢查分區(qū)偏移、執(zhí)行saveenv加載內(nèi)核后無(wú)日志bootargs的console錯(cuò)核對(duì)串口設(shè)備名6.2 編譯與鏈接階段的典型報(bào)錯(cuò)編譯u-boot時(shí)常見(jiàn)的坑工具鏈找不到確認(rèn)CROSS_COMPILE前綴和PATH。undefined reference多半是defconfig里沒(méi)開(kāi)某個(gè)驅(qū)動(dòng)去menuconfig里勾上。dtc版本太舊設(shè)備樹(shù)語(yǔ)法不兼容升級(jí)dtc。鏡像太大超出分區(qū)精簡(jiǎn)配置或調(diào)整分區(qū)。6.3 獨(dú)家避坑經(jīng)驗(yàn)分享第一永遠(yuǎn)保留一份能啟動(dòng)的備份。改u-boot之前先把當(dāng)前能用的鏡像備份出來(lái)改崩了能回滾。第二串口是你的眼睛任何調(diào)試都從串口日志開(kāi)始別指望JTAG能解決一切。第三地址別亂填內(nèi)核加載地址、設(shè)備樹(shù)地址、根文件系統(tǒng)地址都有約定亂填會(huì)導(dǎo)致內(nèi)核解壓失敗或覆蓋數(shù)據(jù)。第四善用bdinfo和printenv這兩個(gè)命令能告訴你板子的真實(shí)狀態(tài)比猜靠譜得多。7. 從 u-boot 出發(fā)嵌入式這條路還能怎么走把u-boot跑通只是起點(diǎn)。往深了走你可以研究SPL的DDR訓(xùn)練理解內(nèi)存為什么需要訓(xùn)練才能穩(wěn)定工作可以研究安全啟動(dòng)理解鏡像簽名和校驗(yàn)的機(jī)制可以研究快速啟動(dòng)優(yōu)化把啟動(dòng)時(shí)間從幾秒壓到幾百毫秒這在車(chē)載和工控場(chǎng)景里是硬需求。熱搜里linux內(nèi)核虛擬化、嵌入式qt包含wayland這些方向也都是建立在扎實(shí)的啟動(dòng)流程理解之上的。我個(gè)人在實(shí)際操作中的體會(huì)是u-boot這東西看十遍文檔不如親手跑通一遍。你會(huì)在一次次卡死—查日志—改配置—重啟的循環(huán)里慢慢建立起對(duì)整個(gè)系統(tǒng)的直覺(jué)。這種直覺(jué)是任何教程都給不了的。等你哪天看到一塊陌生板子能憑串口日志判斷出它卡在哪一級(jí)引導(dǎo)你就真正入門(mén)了。到那時(shí)候再回頭看單片機(jī)你會(huì)發(fā)現(xiàn)它不是被拋棄了而是成了你理解更復(fù)雜系統(tǒng)的地基。