cswork / libraryOpen practice workspace

Articles

韌體工程師面試準備:C 語言、記憶體、中斷與除錯情境

韌體工程師面試準備指南:從 C 語言封包拷貝出發,確認長度、容量、指標生命週期與失敗不寫入的契約。用 ring buffer 與接收、解析計數定位漏資料,區分 volatile、同步與中斷限制。說明 host 測試、實機量測和未驗證風險的界線,不把模擬、編譯通過或特定 RTOS 文件當成所有裝置的保證。

韌體工程師面試準備,不是把 C 語言題庫背完,再補幾個中斷名詞。你需要能說清楚:這段記憶體屬於誰、可以讀寫到哪裡、哪個執行環境能操作它,以及一個不穩定症狀要用什麼證據定位。這些界線比「我很熟指標」更能承受技術追問。

先用 韌體的 byte order、ring buffer 與 stack 練習診斷弱項,再閱讀本文的兩個原創案例:有長度標頭的封包拷貝,以及從接收計數追到解析失敗的資料路徑。它們是能力練習,不是特定公司的已確認面試題。

從職缺辨認韌體工作的真正範圍

「韌體」可能指裸機 MCU、RTOS、boot firmware、裝置驅動、embedded Linux 或製造診斷工具。共同的 C/C++、記憶體與除錯知識,會落在不同的執行環境;不能把一套 RTOS API 或 MCU 的中斷規則套用所有職缺。

閱讀 JD 時先找三類訊號:執行平台、資料與時間限制、需要交付的結果。若是驅動與周邊介面,應練 register、資料手冊與 I/O 時序;若是 boot firmware,應補啟動階段與平台初始化;若是診斷工具,則要能描述測試可重現性與故障證據。

本文用一般 RAM 上的 C 函式、文件所描述的 Zephyr ISR,以及 GCC 對 volatile 的說明作為明確範圍。沒有指定 MCU、cache 架構或 RTOS port 的地方,就不能聲稱已驗證 DMA 一致性、中斷延遲或實機可靠性。

自己的準備清單也應分開「能解釋」、「能在 host 上測試」與「曾在板上量測」。如果只有模擬,誠實描述仍然可以是好的證據;把模擬說成實機經驗,才會在追問寄存器、探針與重現步驟時失去可信度。

C 語言練習:先定義長度、容量與失敗行為

原創題目:輸入是一段一般 RAM buffer,前兩個 bytes 表示 little-endian 的 payload 長度,其後接 payload。將 payload 拷貝到呼叫端提供的輸出 buffer,並回傳實際拷貝長度。標頭不足、資料不足或輸出容量不足時,失敗且不修改輸出內容與 written。

這裡假設整個 buffer 已到齊,允許封包後還有其他 bytes,但只處理第一個 frame。payload 可以是零長度。src、dst、written 都必須是有效且非空指標;輸出範圍不與輸入重疊,written 也不別名到被操作的 buffer。有效記憶體是呼叫端責任,函式不能靠指標值猜出實際配置大小。

不要一開始就寫 *(uint16_t *)src。那會引入 alignment、byte order 與型別存取等前提。這題只需讀兩個 byte 組合長度,不需要讓資料布局偷偷決定答案。

#include <stddef.h>
#include <stdint.h>
#include <string.h>

enum frame_status { FRAME_OK, FRAME_INVALID,
                    FRAME_SHORT, FRAME_NO_SPACE };

enum frame_status copy_frame(const uint8_t *src,
                             size_t n,
                             uint8_t *dst,
                             size_t cap,
                             size_t *written) {
    if (!src || !dst || !written) return FRAME_INVALID;
    if (n < 2) return FRAME_SHORT;
    size_t len = (size_t)src[0] | ((size_t)src[1] << 8);
    if (len > n - 2) return FRAME_SHORT;
    if (len > cap) return FRAME_NO_SPACE;
    memcpy(dst, src + 2, len);
    *written = len;
    return FRAME_OK;
}

關鍵是先確認 n >= 2,才算 n - 2。size_t 是 unsigned;若順序反過來,短輸入的減法可能繞回很大的數值,讓長度檢查失去意義。使用剩餘長度比較,也避免在更一般的格式中以「標頭加 payload」累加時產生溢位。

本函式的 len 最多來自 16-bit 無號長度,時間成本是 O(len),額外空間是常數;沒有動態配置。這仍不代表可以在任何 ISR 中直接呼叫:最大 payload 拷貝時間、標準函式庫實作與平台限制,都需要另外確認。

用失敗前後的記憶體檢查答案

只檢查回傳值不夠。先把輸出填成 sentinel,例如 0xA5,並把 written 設成 99;失敗後檢查兩者沒有改變。這讓「失敗不寫入」成為可觀測契約,而非註解裡的願望。

輸入與容量預期結果要檢查的界線
只有一個標頭 byteFRAME_SHORT不讀第二個 byte,輸出保留
長度 3,只有 2 個 payload bytesFRAME_SHORT不根據標頭盲目讀取
長度 3,容量 2FRAME_NO_SPACE不做部分拷貝
長度 3,容量 3FRAME_OK,寫入 3剛好容量與內容一致
長度 0,容量 0,有效指標FRAME_OK,寫入 0零長度不等於無效 frame
frame 後另有 trailing bytes只拷貝宣告的 payload不把下一筆資料一起複製

接著問介面追問:如果資料一次只收到一部分,這個函式不應把所有短輸入都視為不可恢復錯誤。串流解析器需要區分「等待更多 bytes」與「永久無效」,保存尚未完成的 frame,並設置最大長度與等待政策。這是新介面,不是加一個 retry 就完成。

若改成回傳 payload view 而不拷貝,記憶體與時間成本會下降,但 view 的生命週期必須短於來源 buffer。DMA 重用 buffer、接收佇列釋放節點,或函式回傳區域陣列的位址,都可能讓原本看似正確的指標失效。

Host 的編譯警告、AddressSanitizer、UndefinedBehaviorSanitizer 與枚舉短封包測試,能幫忙發現一般記憶體錯誤。它們不能取代裝置 MMIO、cache maintenance、bus fault 與中斷時序測試。面試時應主動說出這個驗證邊界。

ISR 到工作者:延後工作不會消除塞車

Zephyr 官方 ISR 文件說明中斷處理與一般 thread 的限制不同,並提供把耗時工作交給 thread 的方向。這是 Zephyr 的平台文件;採用其他 RTOS 或裸機設計時,需要查對自己的 API 與架構。

原創情境:接收 ISR 讀取必要資料,嘗試加入 ring buffer,工作者負責 frame 解析。使用四個儲存槽,並以 head == tail 表示空、下一個 head 等於 tail 表示滿;為了區分空滿,實際只能保存三筆。題目明確採用滿時丟棄最新資料的政策。

動作head / tail仍排隊的資料
初始0 / 0空
enqueue A、B、C3 / 0A、B、C
enqueue D3 / 0滿,拒絕 D,增加 drop 計數
dequeue A3 / 1B、C
enqueue E0 / 1B、C、E;head 繞回

先用這個順序模型驗證容量與 wraparound,再討論並行安全。它不是可直接貼到 ISR 的 lock-free 實作。真正的 producer、consumer 共享存取,還涉及索引原子性、資料先寫再發布、讀取完成再回收,以及編譯器與 CPU 的順序要求。

ISR 做哪些事也要明確:必要的裝置確認、固定成本的記錄、平台允許的通知;解析、阻塞等待、複雜 logging 與無界配置則應評估是否轉交工作者。並非所有「通知」API 都能在中斷中使用,也不能把某個平台的 zero-latency interrupt 當作一般 kernel-aware ISR。

volatile、atomic 與硬體資料不是同一件事

GCC 的 volatile 文件指出,volatile 存取不會因此替其他非 volatile 記憶體建立排序屏障。把 head 與 tail 都宣告為 volatile,並不能自動證明這個 queue 的同步、原子性或發布順序正確。

面試回答應先分類:MMIO register 存取、同一核心 ISR 與 task 的協調、多核心共享記憶體,以及 DMA 讀寫,是不同問題。語言層 atomic、RTOS primitive、短 critical section、平台 barrier 或 cache 操作,各自解決不同界線,必須按平台契約選擇。

「關中斷」也不是免費答案。需要說清楚關的是哪些中斷、在哪個核心、critical section 多長、能否巢狀,以及是否影響重要 deadline。如果選擇 C atomics,還要確認工具鏈與目標支援,不能把可能需要 runtime helper 的操作未經評估地放入 ISR。

物件生命週期同樣重要。工作者只能讀已完整發布的資料;producer 不能在 consumer 還持有 view 時重用該槽。C 標準的物件與存取規則是起點,裝置資料的可見性則還有額外平台要求。這比背誦「volatile 防止最佳化」更接近實際除錯。

漏資料的診斷:先找到哪一段少了

原創觀測例子:同一段已結束、queue 已清空的窗口,接收 10 筆、成功入列 8 筆、出列 8 筆、成功解析 7 筆。已知 queue drop 是 2,parse reject 是 1,這能把丟失分成兩個位置;不能全部歸因於串列線路。

先確認計數的單位與窗口相同:IRQ 次數不一定等於 frame 數,byte 數也不能直接減 frame 數。如果還有排隊中的 backlog,入列與出列的差距未必是遺失;若裝置 counter 在重置時清零,跨重啟比較也會誤導。

可以分三個假說設計實驗。若是 queue overload,觀察 occupancy 峰值、drop counter 與工作者處理耗時;若是 frame parser,保存非敏感的長度、狀態與錯誤位置;若是來源資料本來就沒收到,對照硬體接收狀態與外部量測。每個假說都應有能推翻它的證據。

增加 buffer 可以暫時吸收 burst,卻不能解決長期到達率大於處理率。移除大量 ISR logging 可能改善時序,但也不能因此宣稱真正根因是 logging,除非單變量對照與重現支持。一次同時換 compiler、clock、buffer 與排程,會讓結果難以解釋。

最後分開恢復與修正:重啟裝置可能恢復資料,但不會證明問題已消失。好的交付包括重現輸入、故障分類、選擇的修正、回歸結果、剩餘風險,以及尚需哪一種實機觀測。

記憶體預算:先列常駐與峰值,不只看剩餘數字

原創的簡化案例:裝置可用 RAM 是 48 KiB,常駐資料與固定 buffer 使用 18 KiB,task stack 預留 12 KiB,通訊工作區使用 8 KiB。帳面剩下 10 KiB,因為 48 - 18 - 12 - 8 = 10。這是規劃算式,不是已量測的某個 MCU 配置。

接著問這份帳是否重複計算、是否漏掉 ISR stack、allocator metadata、其他 task 或 DMA buffer。若 task stack 只是測試期間的 high-water mark,也不能直接當作最壞情況上限。巢狀呼叫、錯誤處理與中斷情境,可能出現尚未量到的峰值。

不一定要用 heap 才會失控。大型區域陣列可能讓 stack 超出預留;全域 buffer 雖然生命週期清楚,卻增加常駐成本;動態配置能按需求使用,但有碎片、失敗與執行時間的前提。面試答案應說明使用場景,而不是把其中一種方法稱為永遠正確。

回到封包案例,最大 payload 來自 16-bit 長度,不表示系統應接受接近 64 KiB 的封包。如果裝置資源只允許較小 frame,應在介面契約加上產品上限,並在配置或拷貝之前拒絕。協定可表示的最大值與產品能承受的最大值,是兩個不同限制。

若改為共享 pool,還要描述槽的狀態:空閒、producer 填寫、已發布、consumer 使用、可回收。錯誤處理或取消也必須把槽交還,而不是只在成功路徑釋放。測試重複失敗後的可用槽數,能揭露正常案例看不到的資源遺漏。

從症狀到實驗:避免把所有問題叫做 timing issue

假設資料偶爾遺失,而且加上 logging 後更頻繁。這個症狀能提出「處理耗時」假說,但不是根因證明。先量 logging 前後的 ISR 或工作者時間、queue occupancy 與 drop,而不是直接刪掉所有日誌後宣布修好。

另一個假說是來源 frame 不完整。保存長度與錯誤分類,檢查是否固定在某個邊界被截斷。如果 queue drop 沒有增加,parse reject 卻與輸入長度高度相關,應回頭看接收與 frame 組裝,而不是繼續擴大 queue。

也可能是 buffer 生命週期問題:consumer 讀取時,producer 已把同一槽覆寫。此時等待較久或加大 buffer 可能降低重現頻率,但不改變所有權錯誤。可以在 host 模型加入可控排程,讓發布、讀取與回收按指定順序交錯,找出最早違反契約的步驟。

DMA 與 cache 的問題需要平台證據。先確認 buffer 所在記憶體區域、是否 cacheable、DMA 完成事件與 CPU 讀取時點;查閱該架構的維護與 barrier 規則。不能把 host 上成功的 parser 當作 DMA coherence 證明,也不能到處加 volatile 當作修正。

實驗紀錄應至少有版本、輸入、環境、單一改變、預期、實際結果與下一個假說。無法重現時,保留可觀測性與收集策略,而不是挑一個最像的名詞填上。若只有特定 board revision 發生,也要保存板版差異,不能把所有板的測試混成一份成功率。

完成一次修正後,再練習向非韌體同事說明:影響是什麼、目前如何恢復、哪項證據支持原因、尚未確認什麼,以及需要誰提供哪個量測。這能展示你不只會操作 debugger,也能讓團隊知道下一步為何有價值。

中斷與 deadline:平均快,不等於最壞情況能完成

原創的簡化時間帳:某工作從事件發生到必須完成,有 5 ms 的 deadline。讀取與固定拷貝預估 1 ms,解析 2 ms,回報 1 ms,帳面留下 1 ms。這四毫秒是工作成本假設,不是已完成的排程可行性證明。

仍要加入其他中斷干擾、鎖等待、排程與 cache 影響,並確認數字來自平均量測、觀測最大值,還是可辯護的最壞上界。如果其中某個操作偶爾阻塞十毫秒,就不能用平均成本四毫秒宣稱滿足每次 deadline。

當面試官問要不要在 ISR 解析,先比較最大輸入與工作的上界,再看延後處理的等待成本。把工作交給 thread 可以縮短 ISR,但可能增加 queue 等待。選擇不是「ISR 永遠越短越好」一句話,而是讓每個重要事件的延遲與資源成本有明確範圍。

若事件來源可以控制,可以討論節流或合併;如果不能控制,就需要 overload policy。丟棄舊資料可能讓狀態更即時,丟棄新資料則保留先前順序;某些控制或安全資料兩者都不合適,應進入明確故障狀態。先說明資料用途,再選方便的政策。

量測也要減少擾動。高頻字串 logging 可能改變正在觀察的時間;可以使用固定成本的 counter 或平台允許的 trace,再驗證工具本身的 overhead。這些方法能改善可觀測性,仍需要實際平台支持,不是 host 範例已證明的能力。

五題把基礎知識連到工程判斷

以下是跨公司韌體、C/C++ 與系統相關題目;依自己的職缺補足平台知識,不代表每種韌體職位都會考 BIOS 或多核心同步。

完整題目練習重點
Reason About Endianness, Ring Buffers, and Stack Behavior in Firmware用資料路徑與狀態 trace 解釋規則
Explain C printf pointers and memory layout型別、指標與可解釋的輸出
Explain C++ alignment and ABI stabilitylayout 前提與跨介面限制
Explain BIOS and UEFI Firmware Mechanisms限定 boot firmware 路線的啟動知識
Reason About Deadlocks, Synchronization, False Sharing, and Privilege Transitions同步原語與執行環境的差異

下一次練習從 韌體 ring buffer 與記憶體題開始。寫下輸入契約、最小反例、所有權變化與能區分假說的實驗;若只留下正確結果,還少了韌體面試最重要的推理證據。

資料來源與延伸閱讀