Skip to Content
Plugin 開發移植既有韌體

移植既有韌體

手上已經有一份跑在 8051/Keil 風格 MCU 上的 I2C 韌體嗎?不必從空白範本重寫。 PDK 有一個專門的範例 plugins\port_demo\ 與一組「移植層」(Port Shim),讓韌體原始碼幾乎不用改就能編成 Plugin。

想法

韌體平常自己碰 I2C(i2c_read/i2c_write)、印字(printf)、配記憶體(malloc)、延遲(delay)。 編成 Plugin 之後這些呼叫要改走 Core 的服務。移植層做的就是:讓韌體原始碼看到它本來就認得的名字,背後接到 Plugin 的 HostAPI。

三個檔的分工(都在 PDK 的 sdk\):

檔案做什麼
nexus_port.h給韌體原始碼 include:宣告 nx_i2c_* 系列函式,並把 printf/malloc/free 等常見名字用巨集換成移植層版本
nexus_port_plugin.h只有一個 .c(膠水檔)可以 include:這些函式的實作,底層用 ExecSync
nexus_port_keil.hKeil 的儲存類別關鍵字(xdata、code、bit……)降級成空的,讓一般編譯器吃得下

韌體的 .c 不要 include nexus_plugin_api.h,也不要知道 Plugin 的存在;是否接上 Plugin,全由膠水檔 port_glue.c 一手包辦。

對照表:韌體的東西在 Plugin 裡變成什麼

韌體裡的東西Plugin 裡改成
main() 開頭的初始化(會碰硬體的部分)probe(Worker Thread)
main() 開頭的初始化(不碰硬體,只是宣告)init(UI Thread)
無窮迴圈的一輪背景週期用 poll_fn;使用者觸發用自訂指令
中斷服務常式interrupt_handler
delaydelay_ms/delay_us(移植層提供)
printf自動導到 nx_port_printf(寫進 Log),或直接 LogWrite
malloc/free自動導到 nx_port_malloc/nx_port_free(cleanup 時會清算沒 free 的)
寫 bank 選擇暫存器換頁照寫;在 slaves[] 宣告 bank 模式,Core 會攔截並記住目前的頁
多條 I2C 匯流排SubmitJob 加 bus_name;nx_i2c_set_bus 只能選本掛載自己那條
可以被中止的長迴圈每一輪查 nx_port_should_abort()(對應 ShouldAbort)
EEPROM/flash 裡的設定值config.ini 的 [Plugin.<名稱>],見設定持久化
RTOS 的 vTaskSuspendAll()、看門狗、_nop_()移植層直接換成空操作

移植層提供的 I2C 函式有:8-bit/16-bit 暫存器讀寫(nx_i2c_reg_read/nx_i2c_reg_write/nx_i2c_reg16_read/nx_i2c_reg16_write)、 讀改寫某幾個位元(nx_i2c_reg_set)、不帶暫存器位移的原始寫入(nx_i2c_write_raw)、先寫後讀(nx_i2c_write_read)、選匯流排(nx_i2c_set_bus),另有 GPIO 輔助(nx_port_gpio_write、nx_port_gpio_pulse), 並附 i2c_read/i2c_write 這種簡短別名,讓你不必改韌體的呼叫端。 (廠牌自己有 int* 形狀的 i2c_read 簽章時,改用 nexus_port_adapter_ite_int.h。)

怎麼做

port_demo 目錄裡的 AGENT.md 把步驟和每一步的驗收方式寫得很清楚,這裡只給概要:

  1. 複製 plugins\port_demo\ 並改名(例如 my_fw),先不改任何東西,確認能編。
  2. 改名字:Makefile 的 NAME、build.bat 的 NX_NAME、port_glue.c 裡的 Plugin 名稱。
  3. 換韌體:把你的 .c 放進 fw\,每個韌體 .c 只 include nexus_port.h(標準函式庫標頭要放在它之前);新的 .c 加進 Makefile 的 SRC 與 build.bat 的 NX_SRC。
  4. 宣告位址:改 port_glue.c 的 s_slaves[],每個位址一筆。有 bank 選擇暫存器的宣告 bank 模式;16-bit 位移的宣告 NX_ADDR_MODE_16BIT; 執行期才決定、不一定在線上的候選位址全部宣告出來並標 NX_SLAVE_F_OPTIONAL。
  5. 接進入點:把韌體初始化接到 probe、一輪工作接到自訂指令、長迴圈每輪查 nx_port_should_abort()。
  6. 在假硬體上驗證,再上真硬體。

在 init 裡一定要原樣回傳 nx_port_bind() 的結果(return (NxLong)nx_port_bind(ctx, host);),不要改回 return ERR_OK;。 一顆移植 DLL 同時只能服務一個掛載,第二個掛載時它回 ERR_ALREADY_IN_USE,照原樣回傳才能讓 Core 拒絕第二個掛載、保住第一個。

驗證

不需要接實體 dongle,用模擬後端就能驗證整條路是通的:

nexus_tui.exe dongle mock p my_fw fwrun

fwrun 是 port_demo 的自訂指令,跑完應該在 Log 看到 fw: run ok。

⚠ port_demo 的 fwrun 與 isp 會直接對 0x50、0x58、0x48 這幾個範例用的固定位址讀寫,那是給模擬後端看的, 絕對不要對實體板子執行。對你自己移植好的 Plugin 做真硬體驗收時:接上 dongle、p my_fw、確認 probe 沒記錯誤、 逐一執行自訂指令並與韌體原本在 MCU 上的輸出比對、長迴圈中途 plugin unload 應在 2 秒內回到沒有 Plugin 的狀態。

自訂指令不能和內建指令同名:例如 run 是內建的 run <script.lua>,同名的 Plugin 指令永遠叫不到,載入時 Log 會出現一則 ... is shadowed by the built-in ... command and can never be invoked 的警告。所以範例的指令叫 fwrun。

已知限制

有幾類韌體行為不在 Plugin 模型的範圍內,或必須先在你的硬體上量測:

  • 同一顆移植 DLL 同時掛到兩個位置:移植層的綁定狀態與韌體的全域變數在一顆 DLL 裡都只有一份,第二個掛載在 init 被拒絕,第一個照常運作。要換位置先卸載原本那個。
  • 微秒級的 GPIO 波形(例如進入 ISP 模式前特殊的翻轉序列):USB dongle 每次 GPIO 翻轉的開銷是毫秒級,微秒級時序不保證做得到,要在你的 dongle 上實測,不能假設可行。
  • 1 ms 時槽紀律:Windows 的一般排程精度是十幾毫秒,Plugin 無法保證每 1 ms 準時做一次動作。需要這種即時性的邏輯要留在 MCU 上。
  • delay_ms 別亂用:在 init(UI Thread)呼叫 delay_ms 會記一則錯誤且不睡;單次超過 50 ms 會記一則警告。
  • 編成 DLL,不能編成靜態 Plugin:移植層定義的 delay_ms 會和主程式內部同名函式撞名。
  • 不能取代橋接:如果你要處理的是不能改的廠商程式、需要跟 Nexus 共用同一顆 dongle,那是另一條路(橋接),見 PDK 內的 docs\NEXUS_TUI_BRIDGE_PORTING.md,跟 Plugin 無關。

自己驗一遍

  • 用 clangmingw 建置,零警告;plugins\my_fw.dll 有產出。
  • 韌體 .c 沒有 include nexus_plugin_api.h。
  • 換頁(直接寫 bank 暫存器,或讀改寫)之後的下一筆存取落在正確的頁。
  • 長迴圈可以被 plugin unload 在 2 秒內取消。
  • Log 裡沒有 delay_ms() called on UI Thread 的錯誤。