與在線升級實踐)
FPGA上跑Linux當年我第一次看到這個說法時確實挺好奇的。等自己動手折騰了快一個月才明白CPU核只是冰山一角真正花時間的地方全在總線、DDR、啟動鏈路和系統(tǒng)裁剪上。這篇文章記錄一下我完成的一套方案一個核心代碼量僅5000行左右的RISC-V軟核在FPGA上跑起輕量級Linux并在此基礎上做了現場可用的在線升級機制。如果你是FPGA工程師想往RISC-V和嵌入式Linux方向擴展或者你本身是做嵌入式Linux的想搞明白FPGA側的門道這篇應該都能幫上忙。我不會只貼“成功日志”更多是講清楚每一步為什么這么做、有哪些坑可以提前避開。1. 為什么是RISC-V軟核選型與設計思路1.1 方案選型能跑Linux的軟核不是“隨便寫個CPU”FPGA上做軟核CPU通常想到的是MicroBlaze或者NiosII。這兩個確實是成熟方案問題是商業(yè)授權、封閉生態(tài)、定制成本都比較高。換成RISC-V之后開源、指令集精簡而且相關工具鏈已經非常完整很適合自己做軟核。但“能在FPGA上跑Linux”有一個硬性門檻處理器必須帶MMU也就是內存管理單元。沒有MMU的處理器Linux內核根本不愿意配合因為它的進程管理、虛擬內存、fork、exec、用戶態(tài)與內核態(tài)切換全都建立在頁表映射和地址保護之上。我之前用過PicoRV32這類只有一兩千行代碼的小核跑裸機或者RTOS很輕松但要跑Linux就不行了——它壓根沒有S模式也沒有MMU硬上Linux只能跑uClinux很多應用和驅動都會出問題。所以選型時我把重點放在“有MMU、有S特權級、有足夠穩(wěn)定的總線接口”這幾個條件上。最終采用的方案是圍繞VexRiscv框架擴展出來的一個RV32軟核支持Sv32頁表、支持M/S/U三級特權模式核心RTL代碼量大概5000行。這個規(guī)模不算大但足夠把Linux內核啟動起來。有一點要提前說清楚5000行指的是CPU核心也就是流水線、寄存器堆、MMU、中斷和總線接口這些關鍵部分整個SoC還包括DDR控制器、Flash控制器、UART等外設這些往往通過LiteX這類框架自動生成。不要指望一個文件就撐起整個系統(tǒng)。1.2 架構拆解處理器內部是怎么設計的這個軟核采用的是單發(fā)射、順序執(zhí)行的5級流水線結構。對比那些支持亂序執(zhí)行、多發(fā)射的高性能核它的優(yōu)勢是時序容易收斂面積小邏輯簡單出問題好排查。在FPGA上流水線深度和分支預測策略都會直接影響最高時鐘頻率順序發(fā)射的代價是IPC低一點但換來的是更簡單的實現和更穩(wěn)定的時序這個取舍對FPGA上的軟核來說非常合理。為了跑Linux軟核必須包含三個關鍵機制特權級切換M模式運行OpenSBI或BootROMS模式運行Linux內核U模式運行用戶進程。這樣內核和用戶程序可以被硬件隔離。中斷和異常使用RISC-V標準的PLIC和CLINT中斷控制器提供外部中斷和定時器中斷。Linux的進程調度依賴定時器中斷沒有這部分系統(tǒng)啟動到一半就會卡住。MMU這個核實現了Sv32頁表格式能處理32位虛擬地址到物理地址的轉換。沒有它Linux就只能退化成uClinux方案。我實際跑起來的時鐘頻率在90MHz左右性能當然比不上硬核CPU但跑輕量級Linux、跑基礎網絡服務、跑業(yè)務邏輯都夠用。很多應用場景其實根本不缺那點性能缺的是能自由改代碼的CPU。1.3 為什么代碼量能控制在5000行左右“5000行Verilog跑Linux”聽起來像魔術其實是設計上做了大量取舍。RISC-V指令集本身就是典型RISC設計指令數量少編碼規(guī)則整齊這是先天優(yōu)勢。其次這個核沒有做浮點單元浮點運算交給軟件模擬硬要算浮點會慢但對輕量級Linux來說完全可以接受。也沒有做復雜的亂序執(zhí)行和深度緩存cache用簡單直連映射大小也就2KB到8KB夠用就行。最關鍵的一點是大量“處理器外圍”工作被外包給了Bus Matrix和現成IP。處理器只需要實現取指、譯碼、執(zhí)行、訪存、寫回這幾個核心環(huán)節(jié)然后通過AXI接口掛到總線上剩下的DDR控制器、SPI Flash控制器、UART控制器都可以用現成方案。這樣核心代碼自然就壓到5000行左右不會膨脹成一個幾萬行的怪物。2. 硬件平臺搭建從RTL到可運行SoC2.1 FPGA板卡選型與存儲資源規(guī)劃跑Linux對資源的要求比裸機高不少首要瓶頸不是邏輯單元而是存儲。我用的是Artix-7級別的板卡帶有128MB DDR3和16MB SPI NOR Flash。35T這個量級的邏輯資源已經足夠放軟核加各種外設LUT利用率一般也就60%左右布線壓力不大。如果板卡沒有DDR只在FPGA片上BRAM里跑Linux那會很痛苦。Linux內核解壓后要占用內存initramfs要解壓到內存進程棧、堆、頁表全都要內存效應期的片上BRAM只有1.8Mbit左右遠遠不夠。所以選板卡時必須確認有DDR/SDRAM最好是DDR3容量至少64MB才比較好跑一個稍微完整的文件系統(tǒng)。Flash容量也要提前規(guī)劃。一個FPGA bitstream文件大約幾MBOpenSBI大約幾十到一百多KBLinux內核壓縮鏡像幾MBrootfs取決于你放多少工具BusyBox initramfs可以壓到幾MB如果放完整工具鏈就會超過十幾MB。16MB Flash能裝但比較緊湊有條件建議直接上32MB或64MB給后續(xù)在線升級留出空間。2.2 外設與總線搭建CPU之外的另一半工作CPU核心本身不產生價值它必須能訪問DDR、能從Flash啟動、能和外界通信這就要靠總線把它們串起來。這個SoC采用AXI4總線作為主干CPU作為主設備DDR、Flash、UART、Timer、中斷控制器都掛在總線上作為從設備??偩€矩陣選擇了LiteX自動生成省去了大量手寫狀態(tài)機的工作。地址映射是整個系統(tǒng)最需要提前想清楚的事。我用的映射大約是外設地址范圍說明DDR30x40000000 - 0x47FFFFFF主內存Linux運行于此SPI Flash0x20000000 - 0x20FFFFFF存放bitstream、固件、文件系統(tǒng)UART0xF0000000串口控制臺與升級通道CLINT/PLIC0xF0010000附近定時器與中斷控制器DDR控制器是系統(tǒng)里最大的坑。自己用Verilog寫一個DDR3控制器從PHY層到Bank調度工作量不是一兩個月能穩(wěn)定收尾的。實際做法是使用現成方案比如Xilinx MIG IP或者LiteDRAM生成的可綜合控制器。這些方案經過大量驗證時序和訓練邏輯相對可靠你只需要在約束文件里填對時鐘頻率、引腳位置和DDR型號即可。UART模塊在方案里相對簡單但也不能掉以輕心。串口控制臺是聯(lián)調階段最核心的觀測手段如果UART都有問題后面全盲調很難受。2.3 啟動方式與Flash布局FPGA上電后首先加載的是bitstream。bitstream存在SPI Flash里FPGA廠商的配置引擎會自動從Flash地址0讀取配置。配置完成后軟核CPU開始復位取指。這時候就會出現一個關鍵問題CPU從哪里開始執(zhí)行第一條指令我是把OpenSBI放在Flash里的固定偏移CPU復位后通過BootROM把OpenSBI加載到內存然后OpenSBI初始化S模式環(huán)境再跳轉到Linux內核入口。Flash布局大致是這樣區(qū)域偏移內容FPGA配置區(qū)0x00000000bitstreamOpenSBI區(qū)0x00500000左右引導固件內核A區(qū)0x00600000左右Linux內核鏡像DTB A區(qū)0x00A00000左右設備樹內核B區(qū)0x00B00000備用內核鏡像DTB B區(qū)0x00F00000備用設備樹rootfs區(qū)0x01000000BusyBox文件系統(tǒng)這個分區(qū)表在項目一開始就要定好因為后面在線升級全靠它。我最初沒重視后面改分區(qū)偏移差點把整個系統(tǒng)又折騰了一遍。3. 輕量級Linux系統(tǒng)的構建與啟動3.1 軟件棧OpenSBI Linux內核 initramfs跑Linux需要在FPGA里跑一套完整的軟件棧這里用的是OpenSBI、Linux內核、BusyBox initramfs三件套。OpenSBI運行在M模式下負責最底層的硬件初始化、中斷轉發(fā)和SBI調用服務相當于RISC-V世界里的“固件”。OpenSBI跑完后會跳到S模式啟動Linux內核。選擇OpenSBI而不是傳統(tǒng)的BBL原因是它維護更活躍、功能更完整社區(qū)主流基本都轉向它了。Linux內核直接使用主線內核源碼配置成RISC-V架構。我這里因為是RV32所以工具鏈前綴是riscv32-unknown-linux-gnu-如果是RV64則用riscv64-linux-gnu-。工具鏈可以直接下載預編譯版本也可以讓Buildroot自己編后者更省心。initramfs是最省事的根文件系統(tǒng)方案。它把BusyBox和必要的腳本打包進一個cpio歸檔再和內核一起編譯成鏡像。啟動時內核會把它解壓到內存當作根文件系統(tǒng)。好處是不用管SD卡、不用寫Flash根文件系統(tǒng)驅動非常適合軟核起步階段。用Buildroot構建的過程很簡單配置目標架構為RISC-V選擇BusyBox指定內核源碼路徑和內核defconfig然后build。一次構建會同時生成工具鏈、內核鏡像和rootfs歸檔省去很多手工步驟。3.2 內核裁剪的關鍵配置Linux內核默認配置打開的功能特別多覆蓋了各種硬件平臺在FPGA這種資源受限環(huán)境下必須裁剪。以下幾個配置我認為非常關鍵CONFIG_MMUy CONFIG_SERIAL_8250y CONFIG_SERIAL_8250_CONSOLEy CONFIG_RD_GZIPy CONFIG_INITRAMFS_SOURCErootfs.cpio.gz CONFIG_CMDLINEconsolettyS0,115200 root/dev/ram0CONFIG_MMU必須開啟沒有MMU就別想跑標準Linux。CONFIG_SERIAL_8250提供串口驅動console輸出全靠它。CONFIG_INITRAMFS_SOURCE指向打包好的rootfs內核會把它內嵌進鏡像。CONFIG_CMDLINE用于指定啟動參數。如果設備樹里也有chosen/bootargs兩者可能會沖突調試時要留意必要時用CONFIG_CMDLINE_FORCE強制覆蓋設備樹里的參數。編譯前盡量關閉不用的驅動大量的網絡協(xié)議、聲卡驅動、USB主機驅動、GPU驅動等統(tǒng)統(tǒng)不要。裁剪不是靠猜而是先編一版查看鏡像大小再啟動看內核輸出的設備注冊信息把用不到的模塊關掉。這樣一輪輪下來內核鏡像可以從幾MB壓到2-3MB甚至更小。3.3 啟動流程與日志解析正常啟動時串口應該能看到這樣的輸出順序OpenSBI v1.2 ____ _____ ____ _____ / __ \ / ____| _ \_ _| ... [ 0.000000] Linux version 6.1.0-riscv [ 0.000000] Kernel command line: consolettyS0,115200 root/dev/ram0 ... [ 1.200000] Run /init as init process看到“Run /init as init process”意味著內核已經成功掛載rootfs并啟動了用戶態(tài)第一個進程。如果到這里接著出現BusyBox的啟動日志和命令行提示符整條鏈路就通了。我踩過一次很典型的坑日志卡在內核解壓完、剛跳轉到內核入口就沒輸出。這種問題通常不是內核本身而是MMU初始化或內存映射參數有問題。定位方法是先把earlycon打開讓它盡早輸出再就是檢查DDR地址是否和鏈接地址匹配。4. FPGA上的在線升級實現4.1 在線升級到底在更新什么既然系統(tǒng)能在現場跑起來那么“在線升級”這個概念就需要拆開看。在FPGALinux這種組合里升級的對象至少分三層FPGA邏輯升級更新bitstream即CPU、總線、外設的硬件邏輯。固件升級更新OpenSBI、Linux內核鏡像、設備樹DTB。應用升級更新rootfs里的業(yè)務程序、配置腳本等。傳統(tǒng)做法是接JTAG線把電腦拿到設備旁邊燒Flash這在研發(fā)調試階段沒問題但真正的工業(yè)現場不可能這么干。在線升級的意義在于系統(tǒng)運行狀態(tài)下通過網絡或串口把新版本傳到設備由系統(tǒng)自身完成Flash更新然后重啟生效。需要注意在線升級不等于“熱切換”。Linux內核本身不能在家里運行、原地把自己替換成新內核操作系統(tǒng)啟動過程必須是完整重來一次。所以在線升級的本質是“更新持久化鏡像 重啟加載新鏡像”。4.2 Flash分區(qū)設計與A/B備份策略在線升級里最怕的不是升級失敗而是升級失敗后沒有退路。為此我采用了A/B雙分區(qū)機制。所謂A/B就是把內核和rootfs同時準備兩份分別放在不同Flash區(qū)域。系統(tǒng)正常從A區(qū)啟動升級時只寫B(tài)區(qū)。寫完B區(qū)后校驗文件頭和數據校驗和再修改啟動標記告訴引導程序下一次從B區(qū)啟動。如果B區(qū)啟動失敗引導程序自動回退到A區(qū)。MTD分區(qū)用起來大概是這樣的概念mtdpartsspi0.0:1M(openSBI),4M(kernel_a),4M(kernel_b),1M(dtb_a),1M(dtb_b),8M(rootfs_a),8M(rootfs_b)Linux起來后用cat /proc/mtd查看實際分區(qū)# cat /proc/mtd dev: size erasesize name mtd0: 00100000 00010000 openSBI mtd1: 00400000 00010000 kernel_a mtd2: 00400000 00010000 kernel_b mtd3: 00100000 00010000 dtb_a mtd4: 00100000 00010000 dtb_b mtd5: 00800000 00010000 rootfs_a mtd6: 00800000 00010000 rootfs_bA/B分區(qū)會多占用一倍Flash容量這是最直接的代價。如果Flash實在緊張至少要對內核分區(qū)做雙備份因為這是升級中最常改、最容易寫壞的部分。OpenSBI和bitstream可以只保留一個黃金版本正常使用中很少更新。4.3 升級流程實戰(zhàn)串口、網絡與腳本當前系統(tǒng)正常啟動時在線升級最直接的做法就是在Linux里寫Flash分區(qū)。假設備份內核已經通過scp或者串口傳到了/tmp/Image執(zhí)行flashcp -v /tmp/Image /dev/mtd2flashcp會先擦除mtd2分區(qū)再把數據寫進去比直接dd到/dev/mtd2更安全因為它不會讓你誤擦超范圍。寫完內核后設備樹也要一起更新flashcp -v /tmp/system.dtb /dev/mtd4然后修改啟動標記讓引導程序下次從B區(qū)啟動。如果是U-Boot可以用fw_setenvfw_setenv bootpart b sync reboot如果系統(tǒng)已經完全起不來只能進引導程序恢復那么串口就是最后一條命。在U-Boot或自研bootloader里用Ymodem協(xié)議接收文件再寫入Flashloady 0x40000000 erase 0x0B000000 0x400000 cp.b 0x40000000 0x0B000000 0x400000這里的地址要格外小心一定是引導程序視角的Flash地址而不是Linux視角的地址?;煜刂肥俏以谡{試中犯過的錯誤一次寫錯就可能導致系統(tǒng)無法啟動。如果沒有把握先把舊的Flash內容備份到內存或者用md命令讀幾個字節(jié)確認分區(qū)首部特征再動手。整個升級過程最好腳本化腳本里強制做校驗md5sum /tmp/Image md5sum /tmp/Image.remote # 不一致則退出沒有校驗的升級腳本就是在拿現場設備做盲測。4.4 掉電保護與回滾機制在線升級最大的敵人是寫著寫著斷電。如果正在擦寫Flash的過程中斷電該分區(qū)內容既不是舊版本也不是新版本系統(tǒng)可能直接變磚。所以升級策略的底線是永遠不要先擦當前啟動分區(qū)。先把新鏡像寫到B區(qū)校驗成功后再切換啟動標記。這樣就算升級過程斷電A區(qū)還是好的下次上電還是從A區(qū)啟動。再搭配一個bootcount機制每次引導程序啟動一個分區(qū)時在Flash里記錄啟動次數加1。用戶程序正常運行后通過命令把計數清零。如果新分區(qū)啟動時反復重啟計數會持續(xù)累積達到閾值后引導程序自動切換回舊分區(qū)。這是嵌入式領域通用的回滾方案。FPGA側還有個特殊的坑bitstream在Flash里如果被寫壞FPGA會配置失敗整塊板子直接起不來。我的辦法是做一個極簡的“golden”bitstream放在Flash最前面現場需要更新邏輯時只更新后面主bitstream區(qū)域如果主bitstream校驗失敗FPGA硬件引擎會回退到golden配置。golden版本只做基礎功能保證系統(tǒng)能啟動、能繼續(xù)做恢復操作這相當于給硬件上了一道保險。5. 踩坑實錄常見問題與排查技巧5.1 串口完全沒有任何輸出遇到“上電后串口死寂”的情況第一反應不要怪內核先確認硬件和RTL。按這個順序排查會更高效確認時鐘和復位。用邏輯分析儀或ILA抓一下CPU時鐘是否在跑復位釋放時間是否正常。確認UART波特率。串口終端必須和RTL里的波特率分頻一致。RTL里如果寫成9600終端卻設成115200自然沒輸出。確認UART引腳綁定。很多板卡USB轉串口芯片的TX/RX方向容易搞反或者共地沒接好。確認內核命令行真的傳到了。很多時候不是沒輸出而是consolettyS0,115200沒生效內核輸出沒走串口。其中“硬件方向接反”這種低級錯誤最容易讓人白折騰半天。遇到串口沒輸出我會先用萬用表或者示波器看USB轉串口芯片的TX引腳確認有沒有波形再考慮軟件問題。5.2 內核啟動到一半卡住或重啟如果串口日志能打出來但卡在“Uncompressing Linux... done, booting the kernel”之后通常是MMU和內存的問題。這里有個典型的連鎖反應MMU初始化時頁表設置不正確內核第一條訪問虛擬內存的指令就觸發(fā)異常異常處理又沒準備好于是掛死。我遇到過幾次這樣的卡頓最后定位都是DDR訓練參數不匹配。解決思路是確認DDR控制器的時序參數和真實內存顆粒匹配特別是tRCD、tRP、tCL這些值。在OpenSBI里加一個簡單的內存讀寫測試啟動時就對DDR地址范圍做一遍踏步讀寫提前暴露問題。確認內核鏈接地址和DDR物理地址對應。如果CPU從0x40000000取指內核鏡像在編譯時也必須指定運行地址在該區(qū)域兩者不一致時表現就是各種莫名卡死。5.3 Flash寫入失敗或者升級后無法啟動升級腳本執(zhí)行完文件也顯示寫入成功重啟后系統(tǒng)卻起不來。這種情況大概率是分區(qū)偏移錯了。Flash廠商的擦除塊大小不統(tǒng)一有的256KB有的64KB。如果分區(qū)定義和Flash實際擦除塊沒對齊寫入時可能把別的分區(qū)擦掉。查看/proc/mtd里的erasesize再對一下分區(qū)偏移就能發(fā)現很多問題。還有一次是升級后忘記sync。Linux有緩存機制flashcp寫完后數據還在緩存立刻斷電可能丟失。升級腳本里必須加sync最好再延時等緩存落盤。這是在線升級最容易被忽略的細節(jié)。為了救磚我強烈建議在Flash最前面留一塊完全不被業(yè)務升級觸碰的“急救區(qū)”放一個能通過串口下載文件并寫Flash的最小引導程序。配置好這個后再折騰在線升級心里會踏實很多——最壞情況下頂多回退或者串口重刷不需要開蓋拆Flash用編程器。5.4 地址映射導致的“靈異現象”調試過程中我遇到過一個很隱蔽的現象內核跑起來了命令行也能執(zhí)行但偶爾訪問某個外設時系統(tǒng)重啟。后來發(fā)現是總線地址映射有重疊區(qū)CPU發(fā)出去的外設訪問地址同時命中了Flash和UART兩個從設備??偩€仲裁器看到多個設備同時應答直接掛死。這類問題排查起來非常煩因為現象隨機。建議在SoC搭建階段就要一份清晰的總線地址分配表檢查是否有重疊區(qū)域。另外在RTL里給總線加一個默認slave一旦地址沒匹配到任何設備就返回一個錯誤響應而不是讓總線掛在未知狀態(tài)。這個默認slave設計雖小卻能幫我省下大量的摸黑調試時間。最后再分享一個小技巧在線升級這套機制不要等整個系統(tǒng)全部調通了才做最好在第一次Linux能啟動時就順手把Flash分區(qū)和升級腳本寫好。早期分區(qū)改動成本低等業(yè)務代碼和上層驅動都依賴固定地址后再改分區(qū)才是真的傷筋動骨。先把升級通道打通后面不管調內核還是調應用都能省很多事。