在 Embedded 開發筆記 - 第一次從頭規劃嵌入式產品,以 FreeRTOS 為例 提到了計劃的開始,下一步就是燒到板子上跑。動手前先卡在一個很基本的問題:燒錄到底是寫到哪裡?開機後程式又是在哪裡跑?

在 server 的世界,這件事幾乎不用想:程式在硬碟,執行時載進記憶體,CPU 有 cache。嵌入式其實也是同一套邏輯,只是每一層都要自己顧。

先用 server 對照

嵌入式是什麼server 上的對應
ROM(boot ROM)晶片內建、出廠就寫死的一小段程式主機板上的 BIOS / UEFI
Flash(常見是 SPI NOR)斷電不會消失的儲存,放 bootloader、韌體、設定硬碟 / SSD
RAM(外接的 DDR)斷電就消失,程式實際執行的地方記憶體
SRAM / cache晶片內部,小但極快CPU 的 L1 / L2 cache

容量感大概是:Flash 幾 MB 到幾十 MB,DDR 幾十 MB 到幾百 MB,cache 只有 KB 等級。

ROM:永遠在、永遠救得回來

ROM 是晶片裡出廠就固定的程式,插電後 CPU 第一個執行的就是它。它通常只做幾件事:

  • 檢查是不是要進燒錄模式(例如按著某個鍵開機),是的話就開一條 USB 或 UART 等電腦傳韌體過來
  • 不是的話,就去 Flash 開頭讀 bootloader 來執行

因為 ROM 改不掉,所以就算 Flash 裡的 bootloader、韌體全燒壞了,按著燒錄鍵開機還是救得回來。第一次燒自己的韌體之前,先確認這條救援路徑真的走得通,這是最重要的保險。

Flash:燒錄寫的就是它

燒錄就是把一份映像寫進 Flash。小型產品常見的是 SPI NOR Flash,特性:

  • 讀:可以隨機讀,而且不慢
  • 寫:不能直接覆寫,要先「抹除」一整個扇區(常見 4 KB),抹完再寫
  • 壽命:每個扇區能抹寫的次數有限(常見規格約 10 萬次)

一份典型的 Flash 內容大概長這樣:

Flash
├── bootloader          (開機第二段)
├── 韌體映像            (你的 RTOS / kernel)
└── 資料區              (設定、配對記錄...,固定扇區或小型檔案系統)

NOR Flash 其實可以直接在上面執行程式(XIP,execute in place),但速度比 DDR 慢很多,所以多數系統開機時會先整份複製到 DDR 再跑。

DDR:程式真正跑的地方

開機時 bootloader 把韌體映像從 Flash 複製到 DDR 的某個位址,再跳過去執行。之後所有的程式碼、變數、heap、各種緩衝區,都在 DDR 上。

實際量過一次:一份約 1 MB 的 RTOS 映像,從 Flash 讀進 DDR 大約只要零點幾秒。開機慢通常不是慢在這裡,而是慢在等外部硬體(例如 USB、WiFi 晶片初始化)和等網路。

DDR 裡要怎麼切,就是自己的事了。以影音類產品為例,大致會分成:

DDR
├── 程式碼、資料、heap
├── 影像擷取的畫面緩衝區(一張 1080p 畫面就要好幾 MB)
├── 音訊緩衝區
└── 編碼器用的記憶體

容量通常不是問題,真正要擔心的是頻寬:擷取寫入、編碼讀出,都在搶同一條 DDR。

cache:最容易踩的雷

CPU 讀寫 DDR 時,資料會先經過 cache。只有 CPU 自己用的話一切正常,麻煩出在硬體也直接讀寫 DDR(DMA,例如影像擷取、音訊、USB):

  • 硬體剛把資料寫進 DDR,CPU 去讀,讀到的卻是 cache 裡的舊資料
  • CPU 寫了資料要給硬體用,資料還留在 cache 裡沒寫回 DDR,硬體讀到的是舊的

以 MIPS 為例,同一塊實體記憶體有兩種位址可以存取:

位址區段名稱會不會經過 cache
0x80000000 起kseg0會(一般程式用這段,快)
0xA0000000 起kseg1不會(跟硬體共用的資料可以用這段)

兩段都對應到實體位址 0 開始的同一塊記憶體。所以處理 DMA 的規則是:

  • CPU 要讀硬體寫的資料:先讓那段 cache 失效(invalidate),或改用不走 cache 的位址
  • CPU 寫給硬體的資料:先把 cache 寫回 DDR(flush)
  • 交給硬體的位址要給實體位址,不是 CPU 看到的 0x8xxxxxxx

另外,要撐過熱重開機的資料(例如記錄「上次為什麼重開」),一定要用不走 cache 的方式寫進 DDR,不然值還留在 cache 裡,一重開就沒了。

把整條線串起來

燒錄:電腦 ──USB(ROM 的燒錄模式)──▶ Flash

開機:ROM ──讀──▶ Flash 裡的 bootloader ──複製韌體──▶ DDR ──跳過去──▶ 你的 main()
                                                         │
                                                     cache 夾在 CPU 和 DDR 之間

搞懂這張圖之後,下一個問題就是:bootloader 到底怎麼知道韌體要放到 DDR 的哪裡、從哪裡開始跑?這就是下一篇要談的開機流程。