移植既有韌體
手上已經有一份跑在 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.h | Keil 的儲存類別關鍵字(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 |
delay | delay_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 把步驟和每一步的驗收方式寫得很清楚,這裡只給概要:
- 複製
plugins\port_demo\並改名(例如my_fw),先不改任何東西,確認能編。 - 改名字:
Makefile的NAME、build.bat的NX_NAME、port_glue.c裡的 Plugin 名稱。 - 換韌體:把你的
.c放進fw\,每個韌體.c只 includenexus_port.h(標準函式庫標頭要放在它之前);新的.c加進Makefile的SRC與build.bat的NX_SRC。 - 宣告位址:改
port_glue.c的s_slaves[],每個位址一筆。有 bank 選擇暫存器的宣告 bank 模式;16-bit 位移的宣告NX_ADDR_MODE_16BIT; 執行期才決定、不一定在線上的候選位址全部宣告出來並標NX_SLAVE_F_OPTIONAL。 - 接進入點:把韌體初始化接到
probe、一輪工作接到自訂指令、長迴圈每輪查nx_port_should_abort()。 - 在假硬體上驗證,再上真硬體。
在 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
fwrunfwrun 是 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沒有 includenexus_plugin_api.h。 - 換頁(直接寫 bank 暫存器,或讀改寫)之後的下一筆存取落在正確的頁。
- 長迴圈可以被
plugin unload在 2 秒內取消。 - Log 裡沒有
delay_ms() called on UI Thread的錯誤。