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 直譯器,到產品真的開始工作, 中間有四層,每一層負責的東西都不一樣。這頁把四層攤開,列出撐住它們的規則, 並附上相機自己的時鐘對「開機時間花在哪」給的答案。
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。 |
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 根本不在位址空間裡,所以一個區段沒有辦法碰到它。
Two stamps split an 11.7-second boot into three parts. One of them is not what anybody expected.
兩個戳記把 11.7 秒的開機切成三段,其中一段跟所有人的預期都不一樣。
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 搬進檔案」這個方向,每一次都是對的。
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 osd | 76 ms | opens the OSD layer — and one follows every display text打開 OSD 圖層 —— 而每條 display text 後面都跟一條 |
display monitor | 83 ms | once, at the top整份只有開頭一條 |
mem set | 72 ms | one 32-bit word; there is no batch form一個 32-bit 字;沒有批次寫法 |
display text | 38 ms | the 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 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 拼出來, 並且順手把設定區重新種好。
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韌體本來就佔的,到 0xC072DE64 | 3,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陀螺執行期實際佔用 —— 四個 veneer | 32 |
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 不必再跟誰錯開。
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。
bl inside the blob now — so they never needed a fixed address.cave 從 1,736 降到 612 bytes。沒有任何東西跳進它們(現在 hook 在同一塊 blob 裡用普通的 bl 呼叫),所以它們本來就不需要固定位址。
bl reaches ±32 MB, and the pool is 2 GB away — so the landing point must stay in the cave, but only the landing point. 536 → 32 bytes.韌體的 bl 只到 ±32 MB,而池在 2 GB 外 —— 所以落點非留在 cave 不可,但只有落點。536 → 32 bytes。
LOADER_END..0xC072EFB4, 3,920 bytes in one run.真正重要的那一條,已經做完:發布卡一個空洞區位址都不指定,建置檢查也是這樣寫的。不需要目錄字 —— 主機工具直接問 blob 的符號表,跟相機用的是同一張表。arena 是 LOADER_END..0xC072EFB4,3,920 bytes 單一連續。
.bss. Measured after a boot and a take, all 972 unclaimed words read zero — that covers the paths we walk, not the ones we do not. Holding 32 bytes instead of 1,736 is the answer to that, not a claim that the gap is ours.這段空隙在載入的韌體映像裡面,不是堆積,所以沒有分配器會把它發出去 —— 重開機會還原是因為映像重新載入,不是因為有人回收。但映像檔裡是零只代表載入時是零:某個少用的功能可能以 .bss 擁有它。開機加一趟錄影之後量過,未配置的 972 個字全是零 —— 那涵蓋我們走過的路徑,不涵蓋沒走過的。答案是只佔 32 bytes 而不是 1,736,不是宣稱這段空隙是我們的。
0xC072F700兩個東西共用 0xC072F700
The fast-start bootstrap and the host file tools' parameter block — and, it turned out, oneshot.S and four probes as well. The host moved out rather than the bootstrap: fp_usb_shell/cave.py asks the same bump pointer stage2 resets, so a host tool's blocks come from the arena and the bootstrap word has one owner. The host names no cave address now either.快載入的 bootstrap 和主機檔案工具的參數區 —— 後來發現 oneshot.S 和四個探針也在同一個位址。搬走的是主機不是 bootstrap:fp_usb_shell/cave.py 跟 stage2 重設的同一個 bump 指標要空間,所以主機工具的區塊來自 arena,那個 bootstrap 字只剩一個主人。主機端現在也一個空洞區位址都不指定。
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.
完整的量測數字、兩次失敗的細節,以及上傳路徑還有什麼問題, 都在專案筆記裡。卡片本身請用合併器產生, 快載入在那裡是一個勾選框。