ESP8266 HW Timer1-MultiTimers Lib ver.0.5

No Comments

這是 MultiTimers(ESP8266 HW Timer1 硬體計時器多工函式庫)在〈系列大一統總結之後的第一個實際改版(ver.0.5),由 Claude-code 依那篇整理出的問題清單動手修。先講最重要的一點:這次的修正只經過純邏輯審閱與軟體端隨機化測試,還沒有在真正的 ESP8266 硬體上跑過,Ken 之後會準備一個可遠端存取的硬體測試平台,屆時驗證過才會轉成正式發布,目前先以草稿保存。

這次修了什麼

大一統那篇整理出的 6 個問題裡,#3(main_isr/Sync 互斥)與 #4(arm() 保護)是作者已經自覺、刻意取捨的設計決定(安全與即時性的權衡),這次維持原樣不動。#5(char loc)已經在 ver.0.4 修過(改成 int)。真正動手的是下面三項:

#1 Sync() / ForceHaltForSync() 的 m、n 未初始化

這是六版全紀錄裡唯一「從主篇到最終版完全沒被碰過」的問題,甚至被複製貼上到後來新增的 ForceHaltForSync() 裡。原始寫法:

unsigned m, n, z;
while (i){
    z=OP_GET_ID(timer_obj.current[i]);
    if (z==ref.loc) m=i;
    else if (z==loc) n=i;
    --i;
}
if ((z=OP_GET_CNT(timer_obj.current[m])+k)<=UP_BOUND_CNT){
    min_heap_deterioration(OP_MERGE_CNTID(z, loc), n, timer_obj.current);
}

如果 ref(或自己)當下剛好不在 heap 陣列裡(例如已經 Stop 過),迴圈永遠不會賦值給 mn,接下來就拿著垃圾值當陣列索引去讀寫記憶體。修法很直接:mn 預設為 0,兩者都真的找到才繼續,否則視為同步失敗(本來 Sync() 的註解就寫著「this func might fail, if so, affect nothing」,這才是符合原意的失敗方式,而不是未定義行為)。

#2 min_heap_deterioration():大一統那篇判定「沿用已修」,但實測發現其實還是壞的

大一統的版本比對表裡,#2 從 ver.0.6 開始標記「已修:新增 min_heap_deterioration()」,一路沿用到最終版。但那次的核對是「有沒有呼叫重排函式」這個層級的檢查,沒有驗證那支函式本身邏輯正不正確。這次把 min_heap_insertionmin_heap_removalmin_heap_deterioration 三個函式逐字抽出寫成跟硬體無關的純 C,在本機用 gcc 寫了一個隨機化壓力測試(每次隨機挑 insert/removal/deterioration 其中一種操作,每步之後都驗證 min-heap 性質與元素集合是否正確),結果跑不到 20 次就抓到 heap 性質被破壞

操作前 t = [_, 1331861(id5), 1553037(id13), 1537044(id4)]
DETERIORATION target_id=13, idx_target=2, new_key=474461(比原值 1553037 小很多)
操作後 t = [_, 1331861(id5), 474461(id13), 1537044(id4)]
→ 父節點 t[1]=1331861 大於子節點 t[2]=474461,違反 min-heap 性質

根因:原本的寫法固定「先從 idx_target 往下沉」(只對『新值變大』的情況正確),最後往上比對父節點那個迴圈又被 j>=idx_target 這個條件卡死,導致新值即使比更上層的祖先都還小,也永遠沒有機會往上冒泡超過 idx_target 自己的位置。換句話說,這支函式只處理了「deterioration(惡化/變慢)」這一種方向,遇到「新值反而比較小」(在 Sync()ForceHaltForSync() 的實際用法裡,兩種方向都可能發生,取決於 ref 跟自己原本在佇列裡的相對位置)就會出包。

修法改成教科書寫法:先比較新值跟舊值,新值比較小就只往上浮,新值比較大(或相等)就只往下沉,兩個方向各自獨立處理,不再混在一起猜測方向。修好後用 6 組不同亂數種子、每組 200 萬次隨機操作(合計 1200 萬次),heap 性質與元素集合全部驗證通過,沒有再出現任何一次違反。

#6 「2^9-1 顆計時器」的宣稱,其實只是註解沒寫清楚,不是程式碼 bug

核對後發現這不是功能性問題:目前巨集設定 ID_MASK_BIT=4TIMER_NUM=15(4 個 bit 剛好對應 15,數字本身是自洽的),原本的註解卻只提到「保守建議 3 bit(6 顆)」跟「理論上限 9 bit(511 顆,有 watchdog 重置風險)」兩種情境,唯獨漏了目前實際配置用的這個折衷值(4 bit/15 顆)沒講。這次照 Ken 的要求,連最輕微的問題也一併處理——不只寫進改版紀錄,也直接把程式碼裡的註解本身改正確,明講三種情境(保守值/目前預設值/理論上限)分別是什麼。

怎麼驗證的,以及還沒驗證的部分

這台伺服器沒有 ESP8266/Arduino 的編譯工具鏈,也沒有實體硬體,沒辦法像過去 Python 那幾篇一樣「先在本地跑過整套流程再發佈」。這次做得到的驗證,是把跟硬體無關的純演算法部分(三個 heap 函式)抽出來,用一般的 gcc 在這台機器上做隨機化壓力測試,這部分是真正跑過、有數據佐證的。但跟中斷(ISR)、硬體計時器暫存器直接相關的修正(m/n 那個 fix 雖然邏輯簡單,但用到的地方是在真正的硬體中斷情境下),只能算是程式碼審閱層級的把關,還沒有實機驗證過。

所以這篇先以草稿形式保存,附上目前的 ver.0.5 原始碼供先睹為快或自行核對,等 Ken 準備好的硬體測試平台跑過驗證,才會正式發布。

Categories: Arduino

Tags: ,

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

PHP Code Snippets Powered By : XYZScripts.com