問題
這顆晶片的程式存在一塊叫 APROM 的記憶體裡,正常開機就是從那裡跑。 要換掉它,得有人從外面寫進去——那個「外面」一直是專用燒錄器。
但晶片還有第二塊小記憶體叫 LDROM,只有 4 KB, 而且可以設定成開機先跑那一塊。 如果把一支「會透過序列埠接收韌體、寫進 APROM」的小程式放進 LDROM, 那塊板子就自己成了自己的燒錄器。
這是很多現成開發板「插上 USB 就能更新」的原理。 差別只在那些板子出廠就有,而這塊沒有——所以要自己放一支進去。
為什麼不直接寫,要拆成三階
直接動手的話,會同時有三個不知道答案的問題:
- 程式連結到 LDROM 那個位址,還能不能正常跑?
- 晶片能不能自己抹寫自己的記憶體?
- 要改哪個設定才會從 LDROM 開機?
三個綁在一起,失敗時分不出是哪一個。 更麻煩的是其中一個的失敗方式是「板子開不了機」—— 那時你連問它「哪裡錯了」的管道都沒有。
所以拆成三階,風險最大的放最後, 而且輪到它時前面兩個都已經是已知的。
| 階 | 要回答什麼 | 結果 |
|---|---|---|
| A | 設定值現在是多少?晶片能自己抹寫嗎? | 能。設定值是未寫過的初始狀態 |
| B | 程式連結到 LDROM 還能成立嗎? | 能。1.3 KB,燒進去回讀比對通過 |
| C | 能不能真的從 LDROM 開機? | 能,而且沒有動到任何永久設定 |
C 階原本被排成「高風險」,結果不必
原訂做法是改晶片的永久設定。那是寫下去就每次上電都生效的東西, 萬一 LDROM 裡的程式是壞的,板子就每次都開進壞掉的程式。
後來在晶片手冊裡找到另一個開關:它一樣能決定下次從哪裡開機, 但只要斷電再上電就會自動復原。
「斷電就復原」把一個永久操作換成了臨時操作,而拿到的資訊一模一樣。 這是整條軌跡裡最划算的一個決定, 而它來自把手冊多讀了一段——不是來自更小心。
本體:不是把舊程式減肥,是換掉底下那一層
專案裡本來就有一支「用一塊板去燒另一塊板」的程式,6 KB 出頭。 直覺是把它壓進 4 KB。那個方向是錯的。
那支程式的體積有一大半,是用來透過除錯線去控制另一顆晶片的, 外加板子上的按鍵與螢幕操作介面。 而 bootloader 燒的是自己——那整層根本不存在, 也沒有人站在板子旁邊按鍵。
| 原程式的組成 | bootloader 要不要 |
|---|---|
| 序列埠收送、封包校驗、位址檢查、逐字回讀驗證 | 要 |
| 控制另一顆晶片的除錯協定 | 不要 |
| 按鍵與螢幕介面 | 不要 |
所以做的是「保留對外的溝通協定、換掉底下的執行層」。 最後 2 KB,一半的空間都沒用到。
框架錯了,省下來的力氣會花在錯的地方。 真去壓那層除錯協定的程式碼是白做的——它整個不該在裡面。
三道保險,其中一道自己是壞的
一、不可能寫壞自己
bootloader 住在 LDROM、寫的是 APROM。晶片硬體層面就禁止 一塊記憶體寫自己——所以這支程式不可能把自己弄壞。 這不是靠小心,是靠結構。
二、寫完一定讀回來比對
每寫一個字就讀回來對一次。「寫入沒有回報錯誤」和「寫進去的是對的」是兩件事—— 這個專案先前就吃過這個虧:一條看起來完全正常的傳輸,實際上會偶爾少掉一個位元。
三、APROM 是空的就不要跳過去
等待期間沒人要燒錄時,bootloader 會讓路給使用者程式。 但如果 APROM 是空的(例如剛抹掉、還沒寫新的),跳過去只會當掉, 而那時序列埠已經不在服務了——等於把自己鎖在門外。 所以跳之前先確認那邊真的有程式。
這道保險第一版是壞的。 從 LDROM 開機時,「位址 0」指到的是 LDROM 自己, 所以它每次都讀到自己、每次都判定「有程式」。 實測把 APROM 整個抹掉後叫它讓路,它照樣跳了過去,板子當場沒東西可跑—— 正是這道保險存在的理由,而它自己是壞的。
改成用記憶體控制器去問,才問得到真正的 APROM。 教訓是一句話:在 bootloader 裡,「位址 0」不是一個明確的概念—— 要問「哪一塊記憶體」,就得用管那塊記憶體的硬體去問,不能用程式的視角去看。
驗收:先把自己逼進最糟的狀態,再看它能不能爬出來
| 做了什麼 | 結果 |
|---|---|
| 上電後敲一下序列埠 | bootloader 回應了 |
| 送一支韌體進去(936 byte) | 0.3 秒寫完,每一塊都通過回讀驗證 |
| 叫它跳去跑新韌體 | 燈開始閃 |
| 用另一套獨立工具回讀比對 | 內容正確 |
| 把整個 APROM 抹掉,再叫它讓路 | 拒絕跳過去,留在原地繼續服務 |
| 在那個全空的狀態下重新送一次韌體 | 燈又會閃了,全程沒有用到燒錄器 |
最後兩列才是這東西真正的價值。會動不稀奇,抹壞了還能自己爬回來才是。
最後一步:讓它每次上電都這樣
前面都還是臨時的。最後改掉晶片的永久設定,讓上電就先進 bootloader。 那個設定值不是猜的——是從晶片廠商自己的工具設定檔裡查到的, 而且只動其中一個位元。
驗證方式是真的拔掉電源再插上:只有真正斷電,那個設定才會被載入。 用軟體重置去測是測不出來的——那條路會被強制指回原本的開機來源。
量到的數字
| 項目 | 數值 |
|---|---|
| bootloader 大小 | 約 2 KB(上限 4 KB) |
| 上電後等待燒錄的時間 | 2.26 秒,之後讓路給使用者程式 |
| 送一支 936 byte 的韌體 | 0.3 秒,含逐字回讀驗證 |
那 2.26 秒原本在註解裡寫「大約 1.5 秒」,是猜的。 量過才知道差 1.5 倍——不算離譜,但猜的東西不該用肯定句寫。
最花時間的不是程式的錯,是觀測方法的錯
這條軌跡真正燒掉的時間,有三次是耗在自己設計壞掉的實驗上, 而不是耗在程式的錯誤上。三次都是同一個形狀: 我以為我在測 A,其實我在測 B。
- 訊號只在開機那一瞬間送一次,而我每次都在那之後才開始監聽。 「收不到」於是同時包含了「沒送出去」和「送了但沒人在聽」,我只當成前者查。
- 兩塊記憶體裡燒了長得一模一樣的程式, 畫面上分不出正在跑的是哪一份。後來讓程式自己報出「我在哪一塊」才看清楚。
- 燒錄工具的重置會把開機來源設回原本那塊, 所以「燒完馬上測」測到的一直是另一份程式。
還有一次是時間量錯了原點:記錄上寫著第 18.7 秒收到訊號, 那個數字是真的,但它是從監聽程式啟動算起,而不是從插上電算起—— 中間那段是人去插電的時間。 我卻據此推論「開機要等快 20 秒、體驗很差、出借前必須修」,整串都不成立。 實際上是 2.26 秒。
時間戳的原點,跟時間戳本身一樣重要。 三個假線索都不是硬體在說謊——硬體從頭到尾都照實回答, 只是我問錯了問題。
老實說的限制
- bootloader 自己不能透過這條路更新。 硬體禁止一塊記憶體寫自己——那道保險的另一面,就是它一旦出門就是凍結的。 要改它還是得接燒錄器。所以它裡面的錯誤,代價比一般程式高得多, 這也是為什麼上面那三道保險要驗到「抹掉再救回來」為止。
- 目前只在三塊板子中的一塊做過。 另外兩塊要照同樣流程各做一次,三套才能同時借出去。
- 「把整個程式讀回來備份」這條路還沒測過。 協定裡有這個指令,但這次只驗了寫入方向。
- 程式裡那個延時函式的單位是錯的,實際約為標稱的 13 倍。 但它有四十幾個使用者,而且全都是照這個錯誤行為調校出來的—— 改「對」會讓四十幾個地方同時壞掉。 所以這次只把真相寫進註解,沒有動它。
最後一條是個值得記住的情況:單位錯了,但所有使用者都已經照錯的單位校準過。 這時候改單位不是修 bug,是同時製造四十幾個 bug。