← Firmware research← 韌體研究

The fpSup load chainfpSup 載入鏈

A card is two files. Between the firmware's AutoRun interpreter and a product doing its job there are four layers, each owning something different. This is all of them — the rules that hold them together, and what the camera's own clock says about where a boot actually goes.

一張卡只有兩個檔。從韌體的 AutoRun 直譯器,到產品真的開始工作, 中間有四層,每一層負責的東西都不一樣。這頁把四層攤開,列出撐住它們的規則, 並附上相機自己的時鐘對「開機時間花在哪」給的答案。

What this is這頁是什麼
A reference for anyone writing a card, and a yardstick to hold work against. Every rule below is enforced somewhere — a build check, a runtime guard, or a measurement — and where it is not yet, it says so.
寫卡的人要看的參考,也是拿來比對成果的尺。下面每一條規則背後都有東西撐著 —— 建置檢查、執行期守衛、或實測 —— 還沒有的,會寫明。
Where the numbers come from數字怎麼來的
The camera's own microsecond counter, stamped when the loader gets control and again when the load finishes. No host, no USB, no stopwatch. Ver.5.02.
相機自己的微秒計數器,在 loader 拿到控制權和載入完成時各寫一個字。 沒有主機、沒有 USB、沒有碼錶。Ver.5.02。
Nothing here is flashed這裡沒有任何東西寫進韌體
Every address on this page is RAM and is gone at power-off. The one exception is fast start, which writes the loader into a settings block — and that is opt-in, on the card-composer page, for exactly that reason.
這頁上的位址全是 RAM,關機就沒了。唯一的例外是快載入 —— 它會把 loader 寫進設定區,正因為如此,它是在合併器頁面上由使用者自己勾的。

The words名詞對照

The code and the notes use the English throughout. This is what each one means.

程式碼和筆記一律用英文,這裡是它們各自指什麼。

cave The injection cave. One 12 KB gap in firmware RAM, 0xC072D000..0xC0730000, that nothing else uses. Our code has to live here because it is the only RAM the firmware can branch to. 空洞區 / 注入區。韌體 RAM 裡一段沒人用的 12 KB 空隙, 0xC072D000..0xC0730000。我們的程式碼只能住這裡,因為韌體跳得到的只有它。
stage1 The loader, 284 bytes, in the cave. Only what has to exist before the file is in memory — so every word of it costs the AutoRun one mem set. loader,284 bytes,住空洞區。只放「檔案進記憶體之前非做不可」的事 —— 所以它每一個字都要 AutoRun 用一條 mem set 拼出來。
stage2 The loader's second half, carried as the first section of fpSup.BIN and run where it lands. However much it does, it costs the boot nothing. loader 的後半,是 fpSup.BIN 的第一個區段, 就地執行。做再多都不花開機時間。
VBIN The container: magic, section count, entry, payload length, then one (destination, length) record per section. Merging two cards is concatenation plus checks. 容器格式:magic、區段數、entry、資料長度,然後每個區段一筆 (目的位址, 長度)。合併兩張卡就是把它們串起來再檢查。
section Destination ≥0x40000000 is a firmware address; below it is an offset into a pool; zero means run where the file landed. 目的位址 ≥0x40000000 是韌體位址;小於它是池的位移; 等於 0 是就地執行。
entry The word at +0x08 of the header. stage2 calls it once — not branches — and it must return. 表頭 +0x08 那個字。stage2 呼叫它一次 (不是跳),而且它必須返回。
trampoline A merged card has several payloads and the header has one entry word, so the entry is a dispatcher that calls each product in turn. Boot only. 合併卡有好幾個 payload,但表頭只有一個 entry 字, 所以 entry 是一個分派器,逐張呼叫。只在開機跑一次。
veneer An 8-byte hop, for when a branch cannot reach its target. One per hook, no decisions. See below. 8 bytes 的跳板,用在「分支跳不夠遠」的時候。一個 hook 一個,不做判斷。見下。
pool A block from the firmware's allocator, around 0x45xxxxxx. Room to spare — but a firmware bl cannot reach it. 跟韌體配置器要來的一塊記憶體,大約在 0x45xxxxxx。 空間充裕 —— 但韌體的 bl 跳不到。
publish Freshly written words are still data to the caches. Clean the D-cache, invalidate the I-cache, and only then are they instructions. 剛寫進去的字對快取來說還是資料。清 D-cache、廢 I-cache, 它們才真的是指令。
store_boot Fast start's bootstrap, 104 bytes. Checks a hash, copies the loader out of the settings block, branches into it. 快載入的 bootstrap,104 bytes。比對雜湊、把 loader 從設定區複製出來、跳進去。
abort Stops the AutoRun interpreter early. It owes three things: close the script's file, pop the registration, clear the running byte. 提早停止 AutoRun 直譯器。它欠三件事:關腳本的檔、彈出註冊項、清 running byte。

Four layers四層

1  AutoRun.txt display × N · mem set × N · echo the only layer whose size costs time 唯一「長度會變成時間」的一層 72–83 ms per command 每條指令 2  store_boot — fast start only 2  store_boot —— 只有快載入卡有 magic == sha256(loader) ? copy it out of the settings block : return magic == sha256(loader) ? 從設定區複製出來 : 原樣返回 26 words instead of 64 — that is the whole saving 26 個字取代 64 個 —— 省的就是這個 3  loader — 284 bytes in the cave 3  loader —— 空洞區裡的 284 bytes ask for 64 KiB · open \fpSup.BIN · read · check "VBIN" · publish 要 64 KiB · 開 \fpSup.BIN · 讀 · 驗 "VBIN" · 發佈快取 fails safe: a missing file leaves the camera stock 失敗就收手:檔案不在,相機就是原廠相機 4  stage2 — carried in the file, runs where it lands 4  stage2 —— 在檔案裡,就地執行 place every firmware-address section → publish 放所有韌體位址的區段 → 發佈快取 call each product's entry — and it must come back 呼叫每個產品的 entry —— 而且它必須回來 provision the settings block · arm the stop 種設定區 · 武裝停止 free, however much it does: it travels in fpSup.BIN 做再多都免費:它跟著 fpSup.BIN 一起進來 3.5 ms all four boxes 四個框加起來 each product: allocate · publish · arm hooks last · return 每個產品:自己配記憶體 · 發佈 · 最後才 arm hook · 返回
The loader only promises two things: your bytes are at the address you named, and your entry is called once. Memory, pointers and hooks are each product's own business — which is why open gate allocates nothing and the gyro logger takes a megabyte. 載入器只保證兩件事:你的位元組在你說的位址上,而且你的 entry 會被呼叫一次。記憶體、指標、hook 全是各張卡自己的事 —— 所以 open gate 一個位元組都不配,而陀螺記錄器要一整個 MB。

stage2, opened upstage2 拆開來看

It is two passes with the entry in the middle, and the order is not arbitrary.

它是兩趟,中間夾著 entry,而這個順序不是隨便排的。

pass 1 Place every section whose destination is a firmware address. 放所有目的位址是韌體位址的區段。
publish Before anything placed is allowed to run. 在任何放進去的東西被執行之前。
entry Call each product. It allocates, publishes its pool address, writes its own pointer words, arms its firmware hooks last, and returns. 呼叫每個產品。它自己配記憶體、發佈池位址、寫自己的指標字、 最後才 arm 韌體 hook,然後返回。
pass 2 Place the sections whose destination is an offset into the memory the entry just published. They could not be placed before it — nobody owned a pool yet. 放那些目的位址是「剛剛發佈的那塊記憶體的位移」的區段。 在 entry 之前放不了 —— 那時還沒有人擁有一塊池。
provision Write the loader into the settings block, if what is there is not already this build. The magic goes last, so a store half-written by a power cut cannot look valid. 如果設定區裡不是這一版,就把 loader 寫進去。magic 最後寫 —— 斷電寫到一半的 store 不能看起來是有效的。
arm abort Point the echo handler at the routine that stops the script. The script's next line runs it. 把 echo handler 指向停止腳本的那段。腳本的下一行就會執行它。

If an entry does not return: its own firmware sections are in place, pass 2 never happens, the settings block is not seeded, the script is not stopped — and there is no diagnosis, because nobody is left alive to give one. Install yourself and return is the whole contract.

entry 如果不返回:它自己的韌體區段有了,pass 2 沒了, 設定區沒種,腳本沒停 —— 而且沒有任何診斷,因為沒人活著能講話。 裝好你自己,然後回來,契約就這一句。

Placing is also where a card is proved to be RAM-only: every destination is checked, and the NAND is not in the address space at all, so a section has no way to reach it.

放區段這一步也是「這張卡只碰 RAM」的證明:每個目的位址都被檢查, 而 NAND 根本不在位址空間裡,所以一個區段沒有辦法碰到它。

Where the boot actually goes開機時間到底花在哪

Two stamps split an 11.7-second boot into three parts. One of them is not what anybody expected.

兩個戳記把 11.7 秒的開機切成三段,其中一段跟所有人的預期都不一樣。

power-on 通電 worker running worker 開始跑 8,876 ms 2,813 ms the camera booting — not ours 相機自己開機 —— 不歸我們管 the AutoRun's 42 commands AutoRun 的 42 條指令 3.5 ms — the entire load 3.5 ms —— 整個載入 In those 3.5 ms: allocate 64 KiB, open and read a 32 KB file, close it, check the magic, 那 3.5 毫秒裡有:配 64 KiB、開檔讀 32 KB、關檔、驗 magic、 two whole-cache operations, place 10 sections, run the entry, write flash, arm the stop. 兩次整份快取、放 10 個區段、跑 entry、寫 flash、武裝停止。
What goes in fpSup.BIN is effectively free. Open gate places 80 sections from 3,307 writes; the shell places 10. The difference is milliseconds. Only the AutoRun's length is worth arguing about — which is why moving work out of it and into the file has been the right direction every single time. fpSup.BIN 裡放什麼,實質上免費。 open gate 放 80 個區段、3,307 筆寫入;shell 放 10 個。差別在毫秒。 唯一值得爭論的是 AutoRun 的長度 —— 所以「把工作從 AutoRun 搬進檔案」這個方向,每一次都是對的。

What a command costs一條指令多少錢

Everything the AutoRun does costs about the same, and the surprise is which half of a progress-bar frame is the expensive one.

AutoRun 做的每件事單價都差不多,而意外的是進度條一格裡貴的是哪一半。

command指令 cost單價 what it is是什麼
display osd76 msopens the OSD layer — and one follows every display text打開 OSD 圖層 —— 而每條 display text 後面都跟一條
display monitor83 msonce, at the top整份只有開頭一條
mem set72 msone 32-bit word; there is no batch form一個 32-bit 字;沒有批次寫法
display text38 msthe cheap half便宜的那一半

The layer runs three buffers, so a bar frame is sent three times — three texts and three layer-opens, 342 ms a frame, of which 228 ms is the opens. Cutting the intermediate frames saved 1.18 s; cutting the screen entirely saved 1.87 s.

圖層是三緩衝,所以一格要送三次 —— 三次文字加上三次開圖層, 一格 342 ms,其中 228 ms 是開圖層。砍掉中間幾格省 1.18 秒,畫面全關省 1.87 秒。

Ruled out on the camera: mem set takes exactly one value. Three arguments print the usage line and write nothing; a comma-separated pair writes only the first. Compressing the loader's 64 writes into a handful is not available.

實機排除:mem set 只吃一個值。給三個參數會印出用法而且 一個字都不寫;逗號形式只寫進第一個。「把 loader 的 64 條寫入壓成幾條」這條路不通。

Fast start快載入

Fast start keeps the loader in a flash-backed settings block, so the AutoRun writes a 26-word bootstrap instead of spelling out 64 words of loader.

快載入把 loader 放在 flash 支撐的設定區裡,所以 AutoRun 只要寫 26 個字的 bootstrap,不必拼出 64 個字的 loader。

saved, every boot每次開機省2.18 s
paid, once第一次多付4.35 s
breaks even回本2nd boot第 2 次開機

Obvious for development. Close to a wash for someone who switches the camera on twice a day — and the price is a settings block that survives a battery pull. So it is not the default and a release card must not carry it; the card-composer page asks instead, and release_card.py refuses a card that has it.

開發時明顯划算;一天只開一兩次的人幾乎打平,而代價是設定區被寫進東西、 撐得過拔電池。所以它不是預設值,發行版的卡也不准帶它 —— 由合併器頁面問使用者,而 release_card.py 會拒絕帶著它的卡。

The check is a hash of the loader's own bytes, not a string, so "the settings block holds a different build of the loader" is unreachable rather than unlikely — change the loader by one instruction and the magic changes with it. A store that does not match is not an error: the card falls back to spelling the loader out, and re-seeds the block on the way.

那個檢查是 loader 自己位元組的雜湊,不是字串, 所以「設定區裡是另一版 loader」這個狀態不可能發生,而不只是不太可能 —— loader 改一條指令,magic 就跟著變。對不上也不是錯誤:卡片退回把 loader 拼出來, 並且順手把設定區重新種好。

The cave, and what is left to do空洞區,以及還沒做的

Everything above runs in one 12 KB gap in firmware RAM — 0xC072D000..0xC0730000. The firmware's own data ends at 0xC072DE64; everything above that is zero in the image. The gap was the scarcest thing on the camera, and the reason was never size but addressing: a cave address written into a build is held whether the card runs or not, so products had to be hand-arranged around each other and a card you did not select still held its addresses. No product names one any more. The loader keeps the bottom 512 bytes, and everything above is an arena handed out at boot.

上面這一切都跑在韌體 RAM 裡的一段 12 KB 空隙裡 —— 0xC072D000..0xC0730000。韌體自己的資料到 0xC072DE64 為止, 以上在映像檔裡全是零。這段空隙曾經是這台相機上最稀缺的東西,而原因從來不是大小是定址: 寫進 build 的空洞區位址,不管那張卡跑不跑都佔著,所以產品之間要手工錯開, 而且沒被選到的卡照樣佔著它的位址。現在沒有任何產品指定位址。 loader 佔最下面 512 bytes,以上整片是開機時才配發的 arena。

the cave空洞區 bytes位元組
the gap整段空隙12,288
the firmware's own, to 0xC072DE64韌體本來就佔的,到 0xC072DE643,684
the loader's own blockloader 自己的區塊512
the arena, handed out at bootarena,開機時配發3,920
above the arena: park stub, shell state, host scratcharena 以上:park stub、shell 狀態、主機暫存4,172
addresses a build still namesbuild 還指定的位址0
bytes the gyro holds at runtime — four veneers陀螺執行期實際佔用 —— 四個 veneer32

It was never the bytes. 7,712 bytes of address space were held by constants against 708 bytes any one card wrote; the fix was to stop naming addresses, not to use fewer of them. What is left is 32 bytes of veneer, asked for at boot and given back by the next reboot — and the arena above it is one unbroken run, so the next payload does not have to be arranged around anybody.

問題從來不在位元組。被常數佔著的位址有 7,712 bytes, 而任何一張卡實際只寫 708 bytes;解法是不要再指定位址,不是少用幾個。 現在剩下的是 32 bytes 的 veneer,開機時要、下次重開就還回去 —— 而且上面那片 arena 是完整一段連續空間,下一個 payload 不必再跟誰錯開。

0xC072E064 0xC072EFB4 payload window — 3,920 B payload 視窗 —— 3,920 B before 搬之前 1,964 B · 684 B drain 324 space 784 now 現在 32 B · 3,888 B one free run — 3,888 B 單一連續空間 —— 3,888 B four veneers, allocated at boot 四個 veneer,開機時配置 hook landing point — the firmware branches here hook 落點 —— 韌體跳進來的地方 code that moved into the pool 已搬進池的程式碼 state words 狀態字
1,964 bytes down to 32, and the largest free run from 684 to 3,888. Moving the code back only took the window from 684 to 856: what fragmented it was the scattered state words, and they had to go too. They are fields of the writer's own blob in the pool now, reached through the pool pointer, so the window has nothing in it but four eight-byte landing points at the bottom — and those are asked for at boot, not written into a build. 1,964 bytes 降到 32,最大連續空間從 684 變 3,888。 只把程式碼搬走,視窗只從 684 變 856:切碎它的是散落的狀態字,所以那些也得走。 它們現在是 writer 自己那塊 blob 在池裡的欄位,透過池指標存取, 所以視窗裡只剩最下面四個 8 bytes 的落點 —— 而且那是開機時要的,不是寫進 build 的。

Why the landing point has to stay為什麼落點非留在空洞區不可

now — the whole stub sits in the cave 現在 —— 整段 stub 住在空洞區 0xC050D4C8 firmware 韌體 bl cave — stub body, 152 B 空洞區 —— stub 本體 152 B the scarcest memory on the camera 相機上最稀缺的記憶體 bx lr with a veneer — only the landing point stays 改用 veneer —— 只有落點留下 0xC050D4C8 unchanged 完全不變 bl veneer — 8 B ldr pc,[pc,#-4] .word pool — stub body, 152 B 池 —— stub 本體 152 B room to spare 空間充裕 bl reaches ±32 MB; the pool is 2 GB away — this branch cannot exist bl 只到 ±32 MB,池在 2 GB 外 —— 這條分支不可能存在
A veneer decides nothing. One instruction and one word, filled in at boot, one per hook. It touches no register and not lr, so the body still returns straight to the firmware and the caller cannot tell. Four hooks: 536 → 32 bytes. veneer 不做任何判斷。一條指令加一個字,開機時填,一個 hook 一個。 它不碰任何暫存器,也不碰 lr,所以本體照樣直接回到韌體, 呼叫方分不出中間有一層。四個 hook:536 → 32 bytes。

Invariants不變量

These are the rules that hold the chain together. Each one says what enforces it — a build error, a runtime guard, a policy, or nothing yet. A change that violates one is wrong even if the camera boots.

下面是撐住這條鏈的規則。每一條都寫明是什麼在執行它 —— 建置錯誤、執行期守衛、政策,或者目前還沒有。違反其中任何一條就是錯的, 即使相機開得起來。

rule規則 enforced by誰在執行
An entry is called and must return. entry 是被呼叫的,而且必須返回。 nothing — a card that hangs here gives no diagnosis沒有 —— 掛在這裡不會有任何診斷
A payload allocates its own memory and must not assume anyone else published a pool. payload 自己配記憶體,不可以假設別人已經發佈了一塊池。 nothing — convention沒有 —— 靠慣例
Firmware hooks are armed last, after the allocator has said yes. If it refuses, arm nothing. 韌體 hook 最後才 arm,在配置器點頭之後。配置失敗就一個都不 arm。 source convention; a half-armed card logs nothing and looks fine源碼慣例;半裝好的卡什麼都不記錄,而且看起來正常
No product names a cave address at build time. Ask the allocator at boot and keep only what the firmware has to branch to. 任何產品都不准在 build 時指定空洞區位址。 開機時跟配置器要,只留韌體非跳不可的落點。 build error — the release check lists every cave destination on the card and a release card must have none建置錯誤 —— 發行檢查會列出卡上每個空洞區目的地,發布卡必須一個都沒有
A host tool asks for its block too. `cave.claim(name, n)` off the same bump pointer, and a template is told where its block is. 主機端工具也要用要的。 用同一個 bump 指標 cave.claim(name, n),而且要告訴模板它的區塊在哪。 build error, mutation-tested — nothing may name an address inside the arena, and every assemble() of a template that takes a block must pass one建置錯誤,mutation 驗過 —— 任何地方都不准指定 arena 內的位址,而且會接收區塊的模板,每一次 assemble() 都必須傳給它
A state word carries its size. A displacement off a shared word lands inside its own field or exactly on another's start, and a field walked with writeback is wider than a word. 狀態字要帶著自己的尺寸。 對共用字做位移只能落在自己欄位內、或另一個欄位的起點; 被寫回指標走過的欄位不能只有一個字。 build error, mutation-tested — a cave .equ carried its size in the distance to the next address, and re-declaring the block lost it twice: once the camera froze after eight seconds, once immediately建置錯誤,mutation 驗過 —— 空洞區的 .equ 用「到下一個位址的距離」帶著尺寸,重新宣告時丟了兩次:一次相機錄八秒後凍結,一次立刻凍結
Written words are published to both caches before anything placed is allowed to run. 寫進去的字在任何東西被執行之前發佈到兩級快取。 build error — the emitted machine code is decoded and the two cache calls must be there, in order建置錯誤 —— 解碼產出的機器碼,兩個快取呼叫必須在、而且順序對
In the loader, no call before lr is saved. 在 loader 裡,任何呼叫都要排在存 lr 之後。 build error, mutation-tested建置錯誤,mutation 驗過
No caller-saved register carries sp across a call (r0–r3, ip). 不可以用 caller-saved 暫存器跨呼叫扛 sp (r0–r3、ip)。 build error, mutation-tested建置錯誤,mutation 驗過
The stack stays 8-byte aligned — an interrupt will use ours, and the firmware's LDRD requires it. 堆疊維持 8 位元組對齊 —— 中斷會用我們的堆疊, 而韌體的 LDRD 要求對齊。 nothing — source convention only沒有 —— 只有源碼慣例
A change to the loader is proven on a live camera before it goes near the boot path. loader 的改動要先在活著的相機上證明,才准靠近開機路徑。 process — write it into the cave, borrow the shell's echo handler, run it once, check it came back and the shell is alive程序 —— 寫進空洞區、借 shell 的 echo handler 跑一次、確認它回來了而且 shell 還活著
A release card must not write the settings block. 發行版的卡不准寫設定區。 policy, checked by the release script政策,發行腳本會擋
Whatever stops the AutoRun interpreter owes everything its teardown does: close the script's file, pop the registration, clear the running byte. 凡是提早停止 AutoRun 直譯器的,就欠它收尾會做的每一件事: 關腳本的檔、彈出註冊項、清 running byte。 source — learned by losing a day to a file nothing could open源碼 —— 用「任何東西都開不了那個檔」的一天換來的

Why the loader rules are build errors and not comments. Both were written as comments first, and both were then violated by the same person on consecutive attempts — a call above the push, then sp carried in r1. Each one froze the camera on every boot, and a loader that hangs leaves no shell to repair it with, so the card comes out of the slot. The checks decode the emitted machine code rather than reading the source, because a comment or a renamed macro can make a build look safe.

為什麼 loader 那兩條是建置錯誤而不是註解。 兩條一開始都只是註解,然後連續兩次被同一個人違反 —— 呼叫排在 push 之前,接著用 r1 扛 sp。 每一次都讓相機每次開機都凍,而 loader 一掛就沒有 shell 能救,卡只能從卡槽拔出來。 檢查讀的是產出的機器碼不是源碼,因為註解和改個巨集名字都能讓建置看起來安全。

The full measurements, the two failed attempts in detail, and what is still wrong with the upload path are in the project notes. The cards themselves come from the card composer, where fast start is a checkbox.

完整的量測數字、兩次失敗的細節,以及上傳路徑還有什麼問題, 都在專案筆記裡。卡片本身請用合併器產生, 快載入在那裡是一個勾選框。