# Linux 核心專題: 針對 Arm Cortex-M4 的移植和客製化
> 執行人: kk908676
> [專題解說影片](https://youtu.be/Jh63O0JPL6E)
### Reviwed by `jserv`
1. 請介紹從 aboot 到 Linux 系統啟動的過程中,需要做完哪些硬體設定,特別是藉由 Flash/eMMC 開機
2. 提供實際操作的展示影片,並提及對應的環境準備
3. 說好的 LTO 呢?
### Reviewed by `dingsen-Greenhorn`
「採用 vfork 的優勢在於省去頁表複製與寫時複製機制(copy-on-write, CoW)造成的延遲與 CPU 開銷,對於緊接著要執行 exec 的流程,效能提升尤為明顯。但 vfork 僅限於「子代行程執行完畢後立即呼叫 exec」的場景,且因共用位址空間,一旦子代行程出錯,也可能影響親代行程。」
「子代行程執行完畢後立即呼叫 exec」 建議改成「子代行程執行完畢前必須立即呼叫 exec」,比較符合原意,vfork限制是子程序必須在呼叫exec或exit前不做其他事。
### Reviewed by [`otischung`](https://github.com/otischung)
你的專題影片提到你目前使用的記憶卡是 16GB 的,有沒有嘗試使用 64GB 以上的進行測試呢?是否需要因為磁碟格式不同而有所調整?
## 任務簡述
將 Linux v6.14.y 移植到 STM32F429i-Discovery 平台並客製化相關的軟體組成,包含探討 Arm Cortex-M4 開機流程、記憶體佈局、中斷處理機制、輕量級 boot loader、Linux 核心組態、rootfs 建構,和降低佔用的空間等客製化。
> [待整理的筆記](https://hackmd.io/@yawu/HkZ2pm22yx) $\to$ 應適度展開於本頁面
## TODO: 闡述 Arm Cortex-M4 關鍵特性
> 至少涵蓋 Arm Cortex-M4 的開機流程、記憶體佈局 (含 XIP)、中斷處理等機制,以 STMF429i-Discovery 為例
> 參照: [嵌入式系統建構:開發運作於STM32的韌體程式](https://docs.google.com/document/d/1Ygl6cEGPXUffhTJE0K6B8zEtGmIuIdCjlZBkFlijUaE/edit?usp=sharing)
> 搭配閱讀下方網頁,以得知 Linux(-nommu) 如何移植到 Arm Cortex-M 處理器架構:
> * [Linux on Cortex-M3](http://www.linux-arm.org/LinuxKernel/LinuxM3)
> * [Practical Advice on Running uClinux on Cortex-M3/M4](https://www.electronicdesign.com/technologies/embedded/article/21795660/practical-advice-on-running-uclinux-on-cortex-m3-m4)
> * [More Linux for Less](https://www.kernel.org/doc/ols/2006/ols2006v1-pages-313-332.pdf)
[Arm Cortex-M](https://www.arm.com/architecture/cpu/m-profile) 是款 32 位元微控制器架構,設計目標包括低功耗、高效能與即時反應,廣泛應用於物聯網裝置、工業控制、感測器節點與可穿戴裝置;支援 [Thumb-2](https://developer.arm.com/documentation/ddi0344/latest/programmers-model/thumb-2-instruction-set) 混合指令長度設計、內建 NVIC (Nested Vectored Interrupt Controller) 、SysTick 定時器與可選用 MPU,並具備小封裝、低成本與較長的產品生命週期特性。
| 特性 | Cortex-M | Cortex-A |
| ---------- | ------------------------------------------- | ------------------------------------- |
| 主要目標 | 低功耗、即時且可預測 | 高效能、豐富作業系統支援 |
| 深度睡眠 | μA 等級 (極佳) | mA 等級 (要考慮 SoC 整理功耗) |
| 低活動 | 數百 μA 到若干 mA (如 Cortex-M0+) | 數萬 mA (閒置/輕量負載) |
| 高活動 | 數十 mA 到 ~100+ mA (如 Cortex-M7) | 數百 mA 到若干 A (高度負載) |
| 複雜度 | 簡單、較少晶片面積 | 較複雜、晶片面積更大,常整合 GPU |
| 作業系統 | 裸機、RTOS、Linux-noMMU | 完整的 Linux、Android、Windows 等 |
Linux-noMMU (過往稱為 [uClinux](https://en.wikipedia.org/wiki/%CE%9CClinux) 專案,針對 m68k 一類缺乏 MMU 的硬體) 是針對無 MMU 環境調整的 Linux 核心組態,其優勢包括:可重用龐大且成熟的驅動程式與應用生態,降低開發者學習成本;並藉由現有工具鏈、檔案系統與網路堆疊,快速建置複雜系統。此外,無 MMU 設計可簡化記憶體管理與啟動流程,適合資源有限但需整合 Linux 系統的專案。開發者需考量記憶體足跡、啟動時間、即時性需求與系統穩定性,並為行程隔離、中斷延遲與除錯提供替代機制,如利用 MPU、最佳化中斷優先權、採用精簡 rootfs 與串列除錯策略。
此外,單一定址空間設計消除頁表切換與快取資料不一致的風險,使行程切換與系統呼叫的延遲更可預測,對即時處理需求尤為有利;[vfork](https://man7.org/linux/man-pages/man2/vfork.2.html) 替代 fork 亦避免複製定址空間的高延遲開銷,有助於降低背景作業對即時性能的影響。在無 MMU 系統中,核心與應用共用同一實體定址空間,省去分頁表格管理、TLB 查詢與分頁異常處理,讓核心程式碼精簡、RAM 使用更有效率;應用程式直接以實體位址存取記憶體,除錯過程更直覺,跨行程共享記憶體也只需傳遞指標。所有記憶體存取皆繞過位址轉譯,消除因 TLB 未命中或分頁中斷帶來的非預期延遲,而行程切換時不必切換頁表或清除 TLB,也減少切換延遲,使中斷服務行為更可預測,對需要毫秒級甚至亞毫秒級反應的馬達控制或感測器擷取等嵌入式工作負載尤為關鍵。儘管此架構放棄 MMU 提供的行程隔離與記憶體保護,可能導致單一程式錯誤波及全域穩定性,但在 Cortex-M 這類 RAM/Flash 容量受限且追求極低功耗與高決定性的微控制器上,簡化的記憶體管理與更可預測的執行時間,往往遠超過虛擬記憶體帶來的好處;若硬體支援 MPU,則可對核心與特定行程或記憶區段給予存取權限,可彌補缺少 MMU 帶來的限制。
摘自〈[Context Switching and IPC Performance Comparison between uClinux and Linux on the ARM9 based Processor](https://www.ittc.ku.edu/~heechul/papers/context-samsung2005.pdf)〉對於 Linux + MMU (預設組態) 和 Linux no-MMU 在 [ARM920T](https://developer.arm.com/documentation/ddi0151/latest/introduction/about-the-arm920t) 硬體的測試表現。

> 基準測試結果顯示使用 [LMbench](https://lmbench.sourceforge.net/) 套件中的 `lat_ctx` 工具測得的情境切換延遲。評測涵蓋行程存取不同資料大小 (0 KB、1 KB、16 KB) 及不同並行的行程數量。在圖表或數據中,以實心點 (`•` 或 `x`) 表示具備 MMU 的標準 Linux 測量值,以空心圈 (o) 表示 Linux no-MMU 的測量值。

> 當資料大小設定為 0 KB 時,代表每個行程並未配置專屬的資料存取緩衝區,而僅靠暫存器與最基本的堆疊上下文切換即可完成。在具備 MMU 的標準 Linux 中,切換過程還包含頁表切換與 TLB 失效處理,而在無 MMU 的 Linux no-MMU 上,則省略這些步驟,使得在 0 KB 模式下的延遲更能反映最小化上下文保存與回復所需的時間。
對於追求毫秒級或更低切換延遲的即時系統,理解這種「無額外資料存取」的基準測試結果,有助於評估不同作業核心設計或排程策略對總體反應速度的影響。
所有行程建立機制(包括 [fork](https://man7.org/linux/man-pages/man2/fork.2.html), [vfork](https://man7.org/linux/man-pages/man2/vfork.2.html), [kthread_create](https://elixir.bootlin.com/linux/v6.14.7/source/include/linux/kthread.h#L16-L28))最終都會進入核心的 `copy_process`,由它根據參數為新行程分配所需資源。當使用 vfork 取代 fork 時,子代行程不會複製親代行程的記憶體定址空間,而是與親代行程共用同一實體定址空間,也就是說,子代行程對資料的任何修改,親代行程都能立即讀寫。為維護正確性,vfork 執行後會暫停親代行程,直至子代行程呼叫 exec 或自行結束。
採用 vfork 的優勢在於省去頁表複製與寫時複製機制(copy-on-write, CoW)造成的延遲與 CPU 開銷,對於緊接著要執行 exec 的流程,效能提升尤為明顯。但 vfork 僅限於「子代行程執行完畢後立即呼叫 exec」的場景,且因共用位址空間,一旦子代行程出錯,也可能影響親代行程。
POSIX 規範中另提供 [posix_spawn](https://www.man7.org/linux/man-pages/man3/posix_spawn.3.html) 作為行程建立的替代方案,原為不支援 fork 的平臺設計。posix_spawn 等同於 fork 加上 exec,但可擴充參數較少、語意更簡潔,其執行時間不受親代行程記憶體大小影響,適合對行程建立延遲要求嚴格的場景。
呼叫 fork 後,核心會複製親代行程的 `mm_struct`、區域結構與頁表,並將所有分頁標為只讀,當任一行程嘗試寫入時才觸發寫時複製機制,將需要修改的頁面真正複製到新的實體記憶體;親代與子代行程因此共享初始全域變數、暫存器與堆疊內容,並各自擁有獨立的定址空間。相較之下,vfork 不會在啟動時執行任何頁表複製或私有化,必須等子代行程呼叫 exec 或 exit 後,親代行程才會解除阻塞並繼續執行。行程內的多執行緒則始終共用同一地址空間與全域資料。
在 Arm Cortex-M 平台上執行 Linux-noMMU 需應當考慮以下底層議題:
1. 架構與指令集:Cortex-M 系列僅支援 Thumb‑2 指令集,未內建 MMU,必須以「單一實體定址空間」運作,並以 `CONFIG_MMU=n` 編譯核心。Thumb‑2 採用混合指令長度設計 (16 位元與 32 位元) ,其目標是提高指令編碼密度;需確保核心與裝置驅動均以 Thumb 模式編譯、連結,並在最小化指令集切換開銷與維持高效管線使用間取得平衡。
2. 記憶體佈局與保護:由於晶片上 SRAM (數百 KB) 不足,通常需外接 SDRAM (容量以 16 MB 起跳) 與外部快閃記憶體裝置 (如 NOR/NAND Flash) 儲存核心映像與 rootfs (root filesystem);建置時需規劃:核心載入位置、核心映像 XIP 區段、行程堆疊 (stack) 與初始程式碼段 (.text/.data) 分區。若 MCU 具備 MPU,可透過硬體分區隔離核心與使用者空間,或對關鍵行程進行細緻地保護,以降低「一個行程當機即全體當機」之風險;若不具備 MPU,則無此機制。
3. 中斷處理與上下文切換:Cortex-M 中斷使用 NVIC (Nested Vectored Interrupt Controller) ,需在核心中整合:中斷向量表重定向與優先權分組設定,並以 minimal context frame 保存暫存器 (R0–R12、LR、PC、xPSR)。移植時必須調整:ISR (Interrupt Service Routine) 與行程切換程式在 Privileged 與 Unprivileged 模式間的切換,以及 SysTick 定時器中斷與 PendSV 排程中斷的配合運作,以確保搶佔式多工與延遲上限符合預期且可預測。
4. 引導載入與裝置驅動程式支援:Bootloader (U-Boot 或簡易裸機程式) 須初始化時脈、SDRAM 控制器與序列埠,並將核心映像載入至預定位址;同時需設定 MPU 區段描述、重新定位中斷向量表。裝置驅動程式需針對具備相關功能的 Cortex-M MCU 特殊週邊 (如 UART、SPI、I2C;部分高階型號亦支援 SDIO 與 Ethernet MAC) 自行撰寫或修改,並在核心設定中精選驅動程式,降低映像檔的容量與記憶體佔用。
5. 工具鏈與建置流程:採用支援 Thumb‑2 + noMMU 的交叉編譯器 (arm-none-eabi-gcc) 並配置 `--specs=nosys.specs` 或 `--specs=nano.specs` 以縮減函式庫大小;運用 [buildroot](https://buildroot.org/) 和 [Yocto](https://www.yoctoproject.org/) 建構精簡的 rootfs (BusyBox + uClibc-ng/musl),搭配 initramfs 或外部 Flash (JFFS2/UBIFS)。BSP 應包含 Cortex-M 特定組態 (啟用 `CONFIG_ARM_THUMB` 選項、設定 NVIC、驅動 MPU),以加速移植並確保與 Linux 上游開發相容。
## TODO: 針對 Linux no-MMU 的執行檔格式調整
> 參照 [uClinux flat file format](https://myembeddeddev.blogspot.com/2010/02/uclinux-flat-file-format.html) 和 [MMU-less systems and FDPIC](https://maskray.me/blog/2024-02-20-mmu-less-systems-and-fdpic),探討 Flat File Format 和 FDPIC 等議題,應涵蓋動態連結的處理機制。
> 注意: 若干網頁的資訊已過時,斟酌閱讀並謹慎求證
在無 Linux no-MMU 環境裡,傳統 [ELF 可執行檔](https://hackmd.io/@sysprog/c-linker-loader)(Executable and Linkable Format)面臨虛擬位址與實體位址映射缺失的挑戰:ELF 的程式區段(`.text`, `.data`, `.bss`)通常依賴 `p_vaddr` 欄位指定的虛擬定址,再由 MMU 於載入時建立對應到實體記憶體的映射,缺少這層映射便無法直接將 ELF 匯入至任意記憶體位置執行。雖然 ELF 本身並不強制要有虛擬記憶,但現有的載入器(loader)與核心 binfmt_elf 模組都假設底層具備 MMU 支援,因此在無 MMU 架構下只能倚靠額外機制才能運作。
對此,Linux no-MMU 系統多採用 bFLT (Binary Flat Format) 作為可執行檔格式。bFLT 以扁平化記憶體模型為主,亦即所有區段既無虛擬定址也無分頁映射,而是由核心載入器根據檔頭 (`flat_hdr`) 中的 `p_mem_start`, `p_mem_end`, `p_bss_end` 與堆疊大小等欄位,在一塊連續的實體 RAM 範圍中依序佈置 `.text`, `.data` 與 `.bss`,再依靠重定位條目 (reloc entries) 修正程式碼或資料裡的絕對位址。elf2flt 工具會自動解析 ELF 檔案,提取這些必要欄位並生成相應的重定位表,使得在載入階段,只要把 bFLT 標頭、程式區段與重定位列表逐項讀入並依序處理,就能在任意實體位址執行原先以 ELF 編譯的程式。
在 Linux 核心原始程式碼,bFLT 載入器位於 [fs/binfmt_flat.c](https://github.com/torvalds/linux/blob/master/fs/binfmt_flat.c),部分廠商也為 bFLT 增補對位置獨立程式碼 (PIC) 與共享函式庫的支援。若要在 Linux no-MMU 直接使用 ELF,可改採編譯器與連結器支援的簡易 flat-ELF 方案:將所有 `PT_LOAD` 程式標頭 (program header) 的 `p_paddr` 欄位設定為實體定址,並在核心以相同邏輯載入,省去 bFLT 格式轉換。
bFLT 的實務操作流程為:核心讀取 `flat_hdr` 標頭、建立夠大的連續 RAM 區塊,將 `.text` 與已初始化的 `.data` 複製到該區,再將 `.bss` 全部歸零,接著依序走訪重定位條目,將程式內所有絕對位址修正至實際載入基址,並配置行程堆疊。最後核心將程式參數 (`argc`, `argv`) 及環境變數放到堆疊上,跳至 header 中的入口函式地址開始執行。由於此流程不涉及 MMU,適合 RAM/Flash 容量有限、卻仍需多行程支援的 Cortex-M 或其他缺乏 MMU 的微控制器。
至於如何產生 bFLT,常見做法是先用交叉編譯器(如 arm-none-eabi-gcc)搭配 `-fPIE` 或 `-fPIC` 產生位置獨立的 ELF,再經由 [elf2flt](https://github.com/uclinux-dev/elf2flt) 工具轉換。buildroot 和 Yocto 已整合 elf2flt,可在連結階段藉由 `-Wl,-elf2flt` 旗標自動指定,直接輸出 bFLT 而無需手動執行。
## TODO: 客製化 buildroot
> 使用 [buildroot 2025.02](https://buildroot.org/download.html) 或之後的版本,建構 Linux 核心和 rootfs 映像檔
### Buildroot
Buildroot 是一個用於建立嵌入式 Linux 系統的建構框架,它可以用來自動化建構包含 `root filesystem` 、`toolchain` 、 `bootloader` 、 `Linux kernel` 及應用程式等元件的整個系統映像。整個 Buildroot 系統是透過 `Kconfig` 和 `Makefile` 進行配置,使用方式與編譯 Linux kernel 類似,可以透過 `menuconfig` 等工具進行設定。雖然 Buildroot 通常會搭配 Linux kernel 使用,但它也可以在沒有 kernel 的情況下建立檔案系統,例如用於 `bare-metal` 環境
#### 為什麼需要Buildroot ?
當我們編譯 `Linux` 系統時,往往需要自行準備交叉編譯器(cross-compiler)、解決套件編譯相依性、手動建構 kernel、rootfs 和 boot loader,過程繁瑣易出錯。
`Buildroot` 正是為了簡化這一流程而設計的工具。它提供一個整合式的建構系統,讓你可以:
- 選擇目標平台與 kernel 版本
- 自動下載與編譯 BusyBox 、 toolchain 、 bootloader 等元件
- 建立最小可運作的 root filesystem(initramfs 或獨立掛載)
- 透過 `menuconfig` 介面進行自定義設定
- 透過幾個命令自動化建構整個可開機的 Linux 映像
適合嵌入式開發,能大幅減少設定錯誤與整合時間
下載 Buildroot 原始碼 :
```bash
git clone https://github.com/buildroot/buildroot
```
使用預設的 config
```
make stm32f429_disco_xip_defconfig
```
選擇我們需要的特定 Linux Kernel 版本
```
make menuconfig
```
位置: Kernel $\to$ Kernel version

需要做的更動
```
6.1.133 -> 6.14.5
```
以及 `In-tree Device Tree Source file names` 位置
```
stm32f429-disco -> st/stm32f429-disco
```

接著更改用來建構 C 標頭檔( kernel headers ) 的來源版本
位置 : Toolchain $\to$ Custom kernel headers series
```
6.1.x -> 6.13.x or later
```

另外我們在編譯時, Buildroot 為了安全性會去強制驗證下載的 `linux-6.14.5.tar.xz` 的 SHA256 雜湊值是否與 Buildroot 官方所設定的一致
為避免因為沒有對應的雜湊, 所以先將強制驗證關閉
位置 : Build options $\to$ Advanced $\to$ Force all downloads to have a valid hash

將設定保存後, 緊接著就是編譯
```
make -j12
```
完成後會在 `/buildroot/output/images/` 看到 `xipImage`
```
$ ls -lh xipImage
-rw-r--r-- 1 an an 2.6M May 8 16:26 xipImage
```
## TODO: 將 u-boot 換為 afboot-stm32
> 好處是精簡、可追蹤開機流程,可斟酌 fork
> 搭配「闡述 Arm Cortex-M4 關鍵特性」,解說在 STM32F4 如何初始化硬體並載入 Linux 核心
下載 afboot-stm32
```bash
git clone https://github.com/mcoquelin-stm32/afboot-stm32.git
```
直接編譯後會發現這個問題
```
$ make stm32f429i-disco
arm-none-eabi-gcc -c -mthumb -mcpu=cortex-m4 -ffunction-sections -fdata-sections -Os -std=gnu99 -Wall -DKERNEL_ADDR=0x08008000 -DDTB_ADDR=0x08004000 stm32f429i-disco.c -o stm32f429i-disco.o
arm-none-eabi-gcc -c -mthumb -mcpu=cortex-m4 -ffunction-sections -fdata-sections -Os -std=gnu99 -Wall -DKERNEL_ADDR=0x08008000 -DDTB_ADDR=0x08004000 gpio.c -o gpio.o
arm-none-eabi-gcc -c -mthumb -mcpu=cortex-m4 -ffunction-sections -fdata-sections -Os -std=gnu99 -Wall -DKERNEL_ADDR=0x08008000 -DDTB_ADDR=0x08004000 mpu.c -o mpu.o
arm-none-eabi-gcc -c -mthumb -mcpu=cortex-m4 -ffunction-sections -fdata-sections -Os -std=gnu99 -Wall -DKERNEL_ADDR=0x08008000 -DDTB_ADDR=0x08004000 qspi.c -o qspi.o
arm-none-eabi-gcc -c -mthumb -mcpu=cortex-m4 -ffunction-sections -fdata-sections -Os -std=gnu99 -Wall -DKERNEL_ADDR=0x08008000 -DDTB_ADDR=0x08004000 start_kernel.c -o start_kernel.o
arm-none-eabi-gcc -c -mthumb -mcpu=cortex-m4 -ffunction-sections -fdata-sections -Os -std=gnu99 -Wall -DKERNEL_ADDR=0x08008000 -DDTB_ADDR=0x08004000 usart-f4.c -o usart-f4.o
arm-none-eabi-ld -T stm32f429.lds -nostartfiles --gc-sections -o stm32f429i-disco.elf stm32f429i-disco.o gpio.o mpu.o qspi.o start_kernel.o usart-f4.o
arm-none-eabi-ld: Error: unable to disambiguate: -nostartfiles (did you mean --nostartfiles ?)
make: *** [Makefile:28: stm32f429i-disco] Error 1
```
這是因為鏈接器從 `gcc` 改為直接使用 `ld`,但 `ld` 的新版本不再接受 `-nostartfiles` 選項,導致編譯錯誤( `Error: unable to disambiguate: -nostartfiles` )
修改 Makefile
```diff
- LINKERFLAGS := -nostartfiles --gc-sections
+ LINKERFLAGS := --gc-sections
```
修改完後又出現了另一個問題
```
$ make stm32f429i-disco
arm-none-eabi-ld -T stm32f429.lds --gc-sections -o stm32f429i-disco.elf stm32f429i-disco.o gpio.o mpu.o qspi.o start_kernel.o usart-f4.o
arm-none-eabi-ld: stm32f429i-disco.o: in function `reset':
stm32f429i-disco.c:(.text.reset+0x1a): undefined reference to `memcpy'
arm-none-eabi-ld: stm32f429i-disco.c:(.text.reset+0x34): undefined reference to `memset'
make: *** [Makefile:28: stm32f429i-disco] Error 1
```
因為在 `gcc` 編譯時,編譯器會將某些 `hand-written code` (例如看起來像 `memcpy` 或 `memset` 的迴圈)最佳化為對 C 標準庫函數的呼叫。但是, 在 `bare-metal` 環境中, 這些標準庫函數不可用,導致鏈接錯誤( `undefined reference to memcpy` 和 `memset` )
再對 Makefile 做以下修改
```diff
CFLAGS := -mthumb -mcpu=cortex-m4
CFLAGS += -ffunction-sections -fdata-sections
CFLAGS += -Os -std=gnu99 -Wall
+CFLAGS += -fno-builtin
LINKERFLAGS := -nostartfiles --gc-sections
```
修改完之後執行
```
$ make clean
$ make stm32f429i-disco
arm-none-eabi-gcc -c -mthumb -mcpu=cortex-m4 -ffunction-sections -fdata-sections -Os -std=gnu99 -Wall -fno-builtin -DKERNEL_ADDR=0x08008000 -DDTB_ADDR=0x08004000 stm32f429i-disco.c -o stm32f429i-disco.o
arm-none-eabi-gcc -c -mthumb -mcpu=cortex-m4 -ffunction-sections -fdata-sections -Os -std=gnu99 -Wall -fno-builtin -DKERNEL_ADDR=0x08008000 -DDTB_ADDR=0x08004000 gpio.c -o gpio.o
arm-none-eabi-gcc -c -mthumb -mcpu=cortex-m4 -ffunction-sections -fdata-sections -Os -std=gnu99 -Wall -fno-builtin -DKERNEL_ADDR=0x08008000 -DDTB_ADDR=0x08004000 mpu.c -o mpu.o
arm-none-eabi-gcc -c -mthumb -mcpu=cortex-m4 -ffunction-sections -fdata-sections -Os -std=gnu99 -Wall -fno-builtin -DKERNEL_ADDR=0x08008000 -DDTB_ADDR=0x08004000 qspi.c -o qspi.o
arm-none-eabi-gcc -c -mthumb -mcpu=cortex-m4 -ffunction-sections -fdata-sections -Os -std=gnu99 -Wall -fno-builtin -DKERNEL_ADDR=0x08008000 -DDTB_ADDR=0x08004000 start_kernel.c -o start_kernel.o
arm-none-eabi-gcc -c -mthumb -mcpu=cortex-m4 -ffunction-sections -fdata-sections -Os -std=gnu99 -Wall -fno-builtin -DKERNEL_ADDR=0x08008000 -DDTB_ADDR=0x08004000 usart-f4.c -o usart-f4.o
arm-none-eabi-ld -T stm32f429.lds --gc-sections -o stm32f429i-disco.elf stm32f429i-disco.o gpio.o mpu.o qspi.o start_kernel.o usart-f4.o
arm-none-eabi-objcopy -Obinary stm32f429i-disco.elf stm32f429i-disco.bin
arm-none-eabi-size stm32f429i-disco.elf
text data bss dec hex filename
1708 0 0 1708 6ac stm32f429i-disco.elf
```
就會看到目錄中多了 `stm32f429i-disco.bin` ,之後將它燒入開發板中
```
$ st-flash write stm32f429i-disco.bin 0x08000000
st-flash 1.8.0-99-gb149e24
2025-05-04T19:43:05 INFO common_legacy.c: STM32F42x_F43x: 256 KiB SRAM, 2048 KiB flash in at least 16 KiB pages.
file stm32f429i-disco.bin md5 checksum: b27515311742bd4736eec8ee740f828, stlink checksum: 0x00033fd4
2025-05-04T19:43:05 INFO common_flash.c: Attempting to write 2258 (0x8d2) bytes to stm32 address: 134217728 (0x8000000)
EraseFlash - Sector:0x0 Size:0x4000 -> Flash page at 0x8000000 erased (size: 0x4000)
2025-05-04T19:43:05 INFO flash_loader.c: Starting Flash write for F2/F4/F7/L4
2025-05-04T19:43:05 INFO flash_loader.c: Successfully loaded flash loader in sram
2025-05-04T19:43:05 INFO flash_loader.c: Clear DFSR
2025-05-04T19:43:05 INFO flash_loader.c: Clear CFSR
2025-05-04T19:43:05 INFO flash_loader.c: Clear HFSR
2025-05-04T19:43:05 INFO flash_loader.c: enabling 32-bit flash writes
2025-05-04T19:43:05 INFO common_flash.c: Starting verification of write complete
2025-05-04T19:43:05 INFO common_flash.c: Flash written and verified! jolly good!
```
## TODO: 使用 SD card reader 並調整相關核心設定
> 確定 Linux 核心可從 SD 卡讀取並正確啟動,包含 rootfs 也比照處理
##### <font color="#f00">目前使用的 Linux 版本為 6.1.133 測試 SD card功能</font>
為了避免在實務過程中反覆的燒錄韌體, 所以將必要的 Linux Kernel image 以及 Device Tree 燒錄在 SD card 並且透過 SPI 傳輸資料
| STM32 PIN | VCP |
| --------- | ---- |
| PE2 | SPI4_SCK |
| PE4 | SPI4_NSS |
| PE5 | SPI4_MISO |
| PE6 | SPI4_MOSI |
USB to TTL 接線:
- PE2 $\to$ SCK
- PE4 $\to$ CS
- PE5 $\to$ MISO
- PE6 $\to$ MOSI
- GND $\to$ GND
- 5V $\to$ VCC

下方是 Linux Kernel image 以及 Device Tree 擺放位置
```
# FLASH
# -----------------------------------
# AFBOOT 0x08000000 - max 2MiB
#
# SDRAM
# -----------------------------------
# KERNEL (XIP) 0x90000000 - 3MiB-32KiB = 3064KiB
# DTB 0x902F8000 - 32KiB
# FREE SDRAM 0x90300000 5MiB
#
# SDCARD
# -----------------------------------
# Master Boot Record (MBR)
# 1. Partition - Linux Kernel image (RAW data)
# 2. Partition - Device Tree Blob (RAW data)
# 3. Partition - Reserved
```
之後就藉由 afboot 做 :
- 初始化 SPI 與 SD 卡
- 從 SD 卡讀取 MBR 分割區資訊
- 分別將 kernel 與 device tree 從對應分割區載入至指定 SDRAM 位址
下載 afboot patch 來修改原本的程式碼
```bash
git clone https://gist.github.com/kk908676/d9d659cb64f60318505ff70d5b19bafa
```
執行
```
cd ~/Desktop/afboot-stm32/ && git apply ~/Desktop/d9d659cb64f60318505ff70d5b19bafa/afboot.patch
```
燒錄進板子內
```
cd afboot-stm32
make flash_stm32f429i-disco
```
有了 bootloader 之後我們就只需要將 Linux Kernel image 以及 Device Tree 燒錄在 SD card ,這樣 bootloader 就可以到指定的地點透過 SPI 抓取這些資料以此來啟動 kernel
在準備 image 和 device tree 時依據上方的記憶體配置圖修改 SDRAM 大小以及位置、 xipImage 執行位置
```
cd buildroot/
make linux-menuconfig
```
位置 : System Type $\to$ Set flash/sdram size and base addr

更改 (S)DRAM Base Address 為 `0x90300000`
更改 (S)DRAM SIZE 為 `0x00500000`
位置 : Boot options $\to$ Kernel Execute-In-Place from ROM

更改 XIP Kernel Physical Location 為 `0x90000000`
改完之後就能直接編譯產生我們要的 Linux Kernel image 以及 Device Tree
```
make -j12
```
會在 `buildroot/output/images` 看到 xipImage 以及 stm32f429-disco.dtb
接著就要將這兩個檔案燒錄進 SD card ,下載 :
```bash
sudo apt install genimage
```
然後還需要一個 SD card 的映像檔用來定義 SD card 的分割區結構
```
$vi sdcard.config
image sdcard.img {
hdimage {
}
partition xipImage {
image = "output/images/xipImage"
}
partition dtb {
image = "output/images/stm32f429-disco.dtb"
}
}
```
執行
```
genimage --config sdcard.config --inputpath . --outputpath .
```
就可以看到 `sdcard.img` 的映像檔了
接下來就把 SD card 插入電腦
```
$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
loop0 7:0 0 73.9M 1 loop /snap/core22/1748
loop1 7:1 0 4K 1 loop /snap/bare/5
loop2 7:2 0 258M 1 loop /snap/firefox/5751
loop3 7:3 0 73.9M 1 loop /snap/core22/1963
loop4 7:4 0 11.1M 1 loop /snap/firmware-updater/167
loop5 7:5 0 241.9M 1 loop /snap/firefox/6159
loop6 7:6 0 516M 1 loop /snap/gnome-42-2204/202
loop7 7:7 0 91.7M 1 loop /snap/gtk-common-themes/1535
loop8 7:8 0 10.8M 1 loop /snap/snap-store/1248
loop9 7:9 0 10.8M 1 loop /snap/snap-store/1270
loop10 7:10 0 44.4M 1 loop /snap/snapd/23545
loop11 7:11 0 50.9M 1 loop /snap/snapd/24505
loop12 7:12 0 568K 1 loop /snap/snapd-desktop-integration/253
loop13 7:13 0 210.4M 1 loop /snap/thunderbird/644
loop14 7:14 0 210.2M 1 loop /snap/thunderbird/721
loop15 7:15 0 9.8M 1 loop
├─loop15p1 259:5 0 1.8M 1 part
├─loop15p2 259:6 0 19.5K 1 part
└─loop15p3 259:7 0 8M 1 part
sda 8:0 0 931.5G 0 disk
├─sda1 8:1 0 16M 0 part
└─sda2 8:2 0 931.5G 0 part
sdb 8:16 0 1.8T 0 disk
├─sdb1 8:17 0 409.9G 0 part /
├─sdb2 8:18 0 1G 0 part /boot/efi
├─sdb3 8:19 0 30G 0 part [SWAP]
└─sdb4 8:20 0 956.5G 0 part /home
sdc 8:32 1 2M 0 disk /media/an/DIS_F429ZI
sdd 8:48 1 15G 0 disk
└─sdd1 8:49 1 15G 0 part
nvme0n1 259:0 0 465.8G 0 disk
├─nvme0n1p1 259:1 0 529M 0 part
├─nvme0n1p2 259:2 0 99M 0 part
├─nvme0n1p3 259:3 0 16M 0 part
└─nvme0n1p4 259:4 0 465.1G 0 part
```
可以看到 `sdd` 就是剛剛插入大小為 15G 的 SD card
```
sdd 8:48 1 15G 0 disk
└─sdd1 8:49 1 15G 0 part
```
透過以下命令將 `sdcard.img` 燒錄進 SD card
```
sudo dd if=sdcard.img of=/dev/sdd bs=4M status=progress conv=fsync
```
便能看到 `Linux Kernel image` 和 `Device Tree`
```
sdd 8:48 1 15G 0 disk
├─sdd1 8:49 1 1.6M 0 part
└─sdd2 8:50 1 18.5K 0 part
```
隨後將 SD card 插入 microsd card adapter
待 bootloader 成功讀抓取到這些資料後便能啟動 Linux kernel
```
Booting ...
Reading MBR ...
Loading kernel ...
..........................
Loading device tree blob ...
...................
Executing kernel ...
** 7 printk messages dropped **
[ 0.000000] Normal [mem 0x0000000090300000-0x00000000907fffff]
[ 0.000000] Movable zone start for each node
[ 0.000000] Early memory node ranges
[ 0.000000] node 0: [mem 0x0000000090300000-0x00000000907fffff]
[ 0.000000] Initmem setup node 0 [mem 0x0000000090300000-0x00000000907fffff]
[ 0.000000] On node 0, zone Normal: 768 pages in unavailable ranges
[ 0.000000] pcpu-alloc: s0 r0 d32768 u32768 alloc=1*32768
[ 0.000000] pcpu-alloc: [0] 0
[ 0.000000] Built 1 zonelists, mobility grouping off. Total pages: 1270
[ 0.000000] Kernel command line: root=/dev/ram
[ 0.000000] Dentry cache hash table entries: 1024 (order: 0, 4096 bytes, linear)
[ 0.000000] Inode-cache hash table entries: 1024 (order: 0, 4096 bytes, linear)
[ 0.000000] mem auto-init: stack:all(zero), heap alloc:off, heap free:off
[ 0.000000] Memory: 4804K/5120K available (951K kernel code, 69K rwdata, 216K rodata, 72K init, 45K bss, 316K reserved, 0K cma-reserved)
[ 0.000000] rcu: Preemptible hierarchical RCU implementation.
[ 0.000000] rcu: RCU calculated value of scheduler-enlistment delay is 100 jiffies.
[ 0.000000] NR_IRQS: 16, nr_irqs: 16, preallocated irqs: 16
[ 0.000000] /soc/interrupt-controller@40013c00: bank0
[ 0.000000] rcu: srcu_init: Setting srcu_struct sizes based on contention.
[ 0.000000] clocksource: arm_system_timer: mask: 0xffffff max_cycles: 0xffffff, max_idle_ns: 355517175 ns
[ 0.000000] ARM System timer initialized as clocksource
[ 0.000026] sched_clock: 32 bits at 84MHz, resolution 11ns, wraps every 25565281274ns
[ 0.000490] timers@40000c00: STM32 sched_clock registered
[ 0.000894] Switching to timer-based delay loop, resolution 11ns
[ 0.001128] timers@40000c00: STM32 delay timer registered
[ 0.001478] clocksource: timers@40000c00: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 22753100554 ns
[ 0.002093] /soc/timers@40000c00: STM32 clockevent driver initialized (32 bits)
[ 0.009570] Calibrating delay loop (skipped), value calculated using timer frequency.. 168.00 BogoMIPS (lpj=84000)
[ 0.010091] pid_max: default: 4096 minimum: 301
[ 0.011757] Mount-cache hash table entries: 1024 (order: 0, 4096 bytes, linear)
[ 0.012352] Mountpoint-cache hash table entries: 1024 (order: 0, 4096 bytes, linear)
[ 0.046812] rcu: Hierarchical SRCU implementation.
[ 0.046973] rcu: Max phase no-delay instances is 400.
[ 0.059041] devtmpfs: initialized
[ 0.748431] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 1911260446275000 ns
[ 0.749793] pinctrl core: initialized pinctrl subsystem
[ 1.108871] /soc/spi@40015000/display@1: Fixed dependency cycle(s) with /soc/display-controller@40016800
[ 1.110786] /soc/display-controller@40016800: Fixed dependency cycle(s) with /soc/spi@40015000/display@1
[ 1.173585] /soc/spi@40015000/display@1: Fixed dependency cycle(s) with /soc/display-controller@40016800
[ 1.193676] /soc/spi@40015000/display@1: Fixed dependency cycle(s) with /soc/display-controller@40016800
[ 1.207708] /soc/display-controller@40016800: Fixed dependency cycle(s) with /soc/spi@40015000/display@1
[ 1.370683] stm32f429-pinctrl soc:pinctrl@40020000: GPIOA bank added
[ 1.393696] stm32f429-pinctrl soc:pinctrl@40020000: GPIOB bank added
[ 1.418110] stm32f429-pinctrl soc:pinctrl@40020000: GPIOC bank added
[ 1.454932] stm32f429-pinctrl soc:pinctrl@40020000: GPIOD bank added
[ 1.479779] stm32f429-pinctrl soc:pinctrl@40020000: GPIOE bank added
[ 1.506033] stm32f429-pinctrl soc:pinctrl@40020000: GPIOF bank added
[ 1.533503] stm32f429-pinctrl soc:pinctrl@40020000: GPIOG bank added
[ 1.569453] stm32f429-pinctrl soc:pinctrl@40020000: GPIOH bank added
[ 1.606354] stm32f429-pinctrl soc:pinctrl@40020000: GPIOI bank added
[ 1.643034] stm32f429-pinctrl soc:pinctrl@40020000: GPIOJ bank added
[ 1.670745] stm32f429-pinctrl soc:pinctrl@40020000: GPIOK bank added
[ 1.671841] stm32f429-pinctrl soc:pinctrl@40020000: Pinctrl STM32 initialized
[ 2.188840] stm32-dma 40026000.dma-controller: STM32 DMA driver registered
[ 2.316773] stm32-dma 40026400.dma-controller: STM32 DMA driver registered
[ 2.358368] clocksource: Switched to clocksource timers@40000c00
[ 2.493684] workingset: timestamp_bits=30 max_order=11 bucket_order=0
[ 3.620731] STM32 USART driver initialized
[ 3.669996] 40011000.serial: ttySTM0 at MMIO 0x40011000 (irq = 47, base_baud = 5250000) is a stm32-usart
[ 4.486832] printk: console [ttySTM0] enabled
[ 4.538656] random: crng init done
[ 4.590596] stm32_rtc 40002800.rtc: registered as rtc0
[ 4.597101] stm32_rtc 40002800.rtc: setting system clock to 2000-01-01T00:00:35 UTC (946684835)
[ 4.611273] stm32_rtc 40002800.rtc: Date/Time must be initialized
[ 5.589741] clk: Disabling unused clocks
[ 5.613033] Freeing unused kernel image (initmem) memory: 24K
[ 5.620087] This architecture does not have kernel memory protection.
[ 5.627857] Run /init as init process
[ 5.632266] with arguments:
[ 5.636088] /init
[ 5.639232] with environment:
[ 5.643288] HOME=/
[ 5.646647] TERM=linux
[ 7.643876] S01seedrng: page allocation failure: order:7, mode:0xcc0(GFP_KERNEL), nodemask=(null)
[ 7.655967] CPU: 0 PID: 33 Comm: S01seedrng Not tainted 6.1.133 #4
[ 7.663247] Hardware name: STM32 (Device Tree Support)
[ 7.669164] Function entered at [<90002b52>] from [<90001f9b>]
[ 7.675925] Function entered at [<90001f9b>] from [<900d9b45>]
[ 7.682691] Function entered at [<900d9b45>] from [<90046edb>]
[ 7.689470] Function entered at [<90046edb>] from [<90047259>]
[ 7.696234] Function entered at [<90047259>] from [<90047677>]
[ 7.703006] Function entered at [<90047677>] from [<90047889>]
[ 7.709659] Function entered at [<90047889>] from [<90044765>]
[ 7.716546] Function entered at [<90044765>] from [<9003f9dd>]
[ 7.723321] Function entered at [<9003f9dd>] from [<9006a9e9>]
[ 7.730086] Function entered at [<9006a9e9>] from [<9004f3f3>]
[ 7.736974] Function entered at [<9004f3f3>] from [<9004f6cb>]
[ 7.743634] Function entered at [<9004f6cb>] from [<900500ad>]
[ 7.750511] Function entered at [<900500ad>] from [<90000061>]
[ 7.757233] Exception stack(0x9041ffa8 to 0x9041fff0)
[ 7.763459] ffa0: 9031f698 9031f658 9031f698 9031f658 9055f648 0000000b
[ 7.773023] ffc0: 9031f698 9031f658 905dca70 0000000b 9031f698 9031f368 0000000d 00000000
[ 7.782384] ffe0: 00000000 905dca70 9058f6b3 90596964
[ 7.788897] Mem-Info:
[ 7.791830] active_anon:0 inactive_anon:0 isolated_anon:0
[ 7.791830] active_file:0 inactive_file:0 isolated_file:0
[ 7.791830] unevictable:270 dirty:0 writeback:0
[ 7.791830] slab_reclaimable:0 slab_unreclaimable:397
[ 7.791830] mapped:0 shmem:0 pagetables:0
[ 7.791830] sec_pagetables:0 bounce:0
[ 7.791830] kernel_misc_reclaimable:0
[ 7.791830] free:283 free_pcp:0 free_cma:0
[ 7.833706] Node 0 active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:1080kB isolated(anon):0kB isolated(file):0kB mapped:0kB dirty:0kB writeback:0kB shmem:0kB writeback_tmp:0kB kernel_stack:176kB pagetables:0kB sec_pagetables:0kB all_unreclaimable? no
[ 7.862644] Normal free:1132kB boost:0kB min:276kB low:344kB high:412kB reserved_highatomic:0KB active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:1080kB writepending:0kB present:5120kB managed:4828kB mlocked:0kB bounce:0kB free_pcp:0kB local_pcp:0kB free_cma:0kB
[ 7.892809] lowmem_reserve[]: 0 0
[ 7.897660] Normal: 1*4kB (U) 1*8kB (U) 0*16kB 1*32kB (U) 1*64kB (U) 4*128kB (U) 2*256kB (U) 0*512kB 0*1024kB 0*2048kB 0*4096kB = 1132kB
[ 7.917635] 276 total pagecache pages
[ 7.922090] 1280 pages RAM
[ 7.925692] 0 pages HighMem/MovableOnly
[ 7.930547] 73 pages reserved
[ 7.934253] nommu: Allocation of length 380928 from process 33 (S01seedrng) failed
[ 7.943652] active_anon:0 inactive_anon:0 isolated_anon:0
[ 7.943652] active_file:0 inactive_file:0 isolated_file:0
[ 7.943652] unevictable:270 dirty:0 writeback:0
[ 7.943652] slab_reclaimable:0 slab_unreclaimable:397
[ 7.943652] mapped:0 shmem:0 pagetables:0
[ 7.943652] sec_pagetables:0 bounce:0
[ 7.943652] kernel_misc_reclaimable:0
[ 7.943652] free:283 free_pcp:0 free_cma:0
[ 7.984977] Node 0 active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:1080kB isolated(anon):0kB isolated(file):0kB mapped:0kB dirty:0kB writeback:0kB shmem:0kB writeback_tmp:0kB kernel_stack:176kB pagetables:0kB sec_pagetables:0kB all_unreclaimable? no
[ 8.013762] Normal free:1132kB boost:0kB min:276kB low:344kB high:412kB reserved_highatomic:0KB active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:1080kB writepending:0kB present:5120kB managed:4828kB mlocked:0kB bounce:0kB free_pcp:0kB local_pcp:0kB free_cma:0kB
[ 8.043815] lowmem_reserve[]: 0 0
[ 8.048685] Normal: 1*4kB (U) 1*8kB (U) 0*16kB 1*32kB (U) 1*64kB (U) 4*128kB (U) 2*256kB (U) 0*512kB 0*1024kB 0*2048kB 0*4096kB = 1132kB
[ 8.068712] 276 total pagecache pages
[ 8.073247] binfmt_flat: Unable to allocate RAM for process text/data, errno -12
Welcome to Buildroot
(none) login:
```
## TODO: 運用 LTO 縮減核心映像檔空間
> 參照 [Shrinking the kernel with link-time optimization](https://lwn.net/Articles/744507/)
## TODO: 客製化 Linux 核心
> 開啟 fbdev, touchscreen, rng 等裝置,見 [AdrianHuang/uclinux-robutest](https://github.com/AdrianHuang/uclinux-robutest/tree/MovingTux)
> 整合 [Linux Tiny Patches for Linux 6.9.1
](https://github.com/snacsnoc/linux-tiny-updated-patches) 和 [OpenWrt 的修改](https://git.openwrt.org/?p=openwrt/openwrt.git;a=blob;f=target/linux/generic/hack-6.6/902-debloat_proc.patch;hb=HEAD)