第一個 Plugin 1:載入
我們要做一顆叫 hello_ic 的 Plugin,分四步長大。這一步只求一件事:讓它被主程式載入,
在 plugin list 看得到它。它什麼都不做,只在載入時寫一行 Log。
開始之前,你需要:解壓好的 PDK、一個編譯器
(下面用 llvm-mingw 示範,其他車道只差在 build.bat 後面那個名字)、以及主程式。
1. 從範本複製出你的專案
PDK 的 plugins\ 裡有 template_plugin,是最小的起點。不要直接改它,複製一份:
cd <PDK 資料夾>\plugins
xcopy /e /i template_plugin hello_ic得到 plugins\hello_ic\,裡面是一顆完全獨立的子專案:
plugins\hello_ic\
build.bat 建置腳本(本教學用它)
Makefile GNU make 版的建置腳本
toolchain.local.bat.sample 告訴 build.bat 你的編譯器在哪(多半用不到)
src\
template_plugin.c 範本的原始碼
template_plugin.rc 版本資源
plugin_version.h Plugin 的版本號,單一來源
template_panel.h/.nxlay 範本的面板宣告(步驟 4 才會用到)只能 include 兩個地方:./src(你自己的)與 ../../sdk(PDK 的標頭)。
build.bat 的 include 路徑只設這兩處,這是刻意的邊界。
2. 改四處名字
複製出來的專案還叫 template。有四處要一起改,改漏一處不會編譯失敗,只會出現
「兩顆 Plugin 同名」或「DLL 檔名跟內容對不上」這類很難查的症狀:
build.bat:把set "NX_NAME=template"改成set "NX_NAME=hello_ic",set "NX_SRC=src\template_plugin.c"改成set "NX_SRC=src\hello_ic.c"。NX_NAME決定輸出的檔名hello_ic.dll。Makefile:NAME = hello_ic、SRC = src/hello_ic.c。用build.bat建置時不會讀它, 但要改,免得以後用make時編出叫template.dll的東西。- 原始碼開頭的 Plugin 名稱與位址:使用者在指令列打的
plugin switch <名稱>就是這個名字。 NexusPluginGetInterface()裡的s_plugin.name與s_plugin.description。
接著把 src\template_plugin.c 改名成 src\hello_ic.c,然後用下面下載的檔案整個取代它的內容——
第 3、4 處已經在那份檔案裡改好了,不用再手動改。
template_plugin.rc裡的檔案說明還寫著template。它只影響「檔案總管 → 內容 → 詳細資料」 顯示的文字,對載入沒有影響;要發佈 Plugin 時再改成你自己的名稱即可。 版本號則來自plugin_version.h,.rc會自己讀它。
3. 步驟 1 的完整程式碼
/**
* @file hello_ic.c
* @brief hello_ic 教學步驟 1:最小的 Plugin,只在載入時寫一行 Log。
*
* 這一步只需要記住三件事:
* 1. Plugin 是一顆 DLL,Core 只認一個進入點 `NexusPluginGetInterface()`。
* 2. 進入點回傳一張填好的 `IcPlugin` 表,表裡放名字、位址與 callback。
* 3. `init()` 是 Plugin 掛載時 Core 呼叫的第一個 callback。
*
* @see docs/NEXUS_TUI_PLUGIN_GUIDE.md 第 1 章 1b、第 2 章
*/
#include "nexus_plugin_api.h"
#include "plugin_version.h"
/**
* @brief 本 Plugin 對外的名稱。
* @note 使用者在指令列打 `plugin list` 會看到它,打 `p hello_ic` 就掛載它。
*/
#define HELLO_IC_NAME "hello_ic"
/**
* @brief 你的 IC 的 7-bit I2C 位址。
* @note **請改成你自己的 IC 的位址。** 不知道位址時,在主程式指令列打 `scan`,
* 它會列出匯流排上有回應的所有 7-bit 位址。本教學預設 0x58
* (模擬後端 mock 預設就有一顆 0x58)。
*/
#define HELLO_IC_ADDR 0x58
/**
* @brief 本 Plugin 對外宣告的 slave 位址表(至少要有一列,否則 Core 拒載)。
*
* @note 欄位依序是:位址、位址模式(8-bit 暫存器位移)、bank 模式(無 bank)、
* bank 暫存器、bank 遮罩、旗標、保留、顯示名稱。這一列的意思是
* 「一顆用 8-bit 暫存器位移定址、沒有 bank 的 IC」,多數 IC 都是這樣。
* @warning 必須是靜態儲存期:Core 只讀不釋放,所以不能是函式內的區域變數。
*/
static const NxSlaveDecl s_slaves[] =
{
{ HELLO_IC_ADDR, NX_ADDR_MODE_8BIT, NX_BANK_MODE_NONE, 0, 0, 0, { 0, 0 }, "hello_ic" }
};
/**
* @brief Plugin 掛載時的初始化,Core 在 **UI Thread** 上呼叫。
*
* @param[in] ctx 本次掛載的上下文;之後每一次呼叫 HostAPI 都要把它當第一個參數傳回去
* @param[in] host HostAPI 函式表,生命期涵蓋整個行程;不得修改、不得釋放
* @return `ERR_OK` 表示成功;非 `ERR_OK` 會使這次掛載中止。
*
* @warning 在 UI Thread 上執行,不得做 I2C 存取或任何會阻塞的事。
* @note `LogWrite()` 是最簡單的「證明我活著」:這行字會出現在主程式的 Log 面板。
*/
static NxLong NX_CALL hello_ic_init(NxPluginCtx ctx, const NxHostApi *host)
{
host->LogWrite(ctx, NX_LOG_INFO, "hello_ic: loaded");
return ERR_OK;
}
/**
* @brief Plugin 卸載時的清理。**強制實作,不得為 `NULL`。**
*
* @param[in] ctx 本次掛載的上下文
* @return 無
*
* @warning 在 **I2C Worker Thread** 上呼叫(不是 UI Thread),返回後 `ctx` 立即失效。
* @note 步驟 1 沒有任何資源要釋放,所以什麼都不做。
*/
static void NX_CALL hello_ic_cleanup(NxPluginCtx ctx)
{
NX_UNUSED_ARG(ctx);
}
/**
* @brief 本 Plugin 的能力宣告,Core 只讀不釋放。
*
* @note 必須是靜態儲存期(不能是函式內的區域變數):靜態變數一開始全是 0,
* 而 0 對每一個選用 callback 而言正是「我不提供」。
*/
static IcPlugin s_plugin;
/**
* @brief Plugin DLL 的進入點:Core 載入 DLL 之後第一個呼叫的函式。
*
* @param[in] host_abi_major Host 端的 `NX_PLUGIN_ABI_MAJOR`
* @param[in] host_abi_minor Host 端的 `NX_PLUGIN_ABI_MINOR`(本函式刻意不比較:minor 不同不是拒載的理由)
* @return 指向靜態生命期 `IcPlugin` 的指標;ABI major 不相容時回傳 `NULL`。
*
* @warning 在 UI Thread 上呼叫,不得做 I2C 存取或阻塞操作。
* @note 欄位一律用**具名賦值**填寫,沒填的欄位維持 0(=不提供該 callback)。
*/
NX_PLUGIN_EXPORT struct IcPlugin * NX_CALL NexusPluginGetInterface(NxDword host_abi_major,
NxDword host_abi_minor)
{
NX_UNUSED_ARG(host_abi_minor);
if (host_abi_major != NX_PLUGIN_ABI_MAJOR)
{
return (struct IcPlugin *)0;
}
s_plugin.abi_major = NX_PLUGIN_ABI_MAJOR;
s_plugin.abi_minor = NX_PLUGIN_ABI_MINOR;
s_plugin.size_of_struct = sizeof(IcPlugin);
s_plugin.name = HELLO_IC_NAME;
s_plugin.description = "hello_ic tutorial plugin";
s_plugin.slaves = s_slaves;
s_plugin.slave_count = (NxByte)(sizeof(s_slaves) / sizeof(s_slaves[0]));
s_plugin.slave_decl_size = (NxDword)sizeof(NxSlaveDecl);
s_plugin.panel_mode = NX_PANEL_MODE_NONE;
s_plugin.init = hello_ic_init;
s_plugin.cleanup = hello_ic_cleanup;
s_plugin.version_major = (NxWord)PLUGIN_VER_MAJOR;
s_plugin.version_minor = (NxWord)PLUGIN_VER_MINOR;
s_plugin.version_patch = (NxWord)PLUGIN_VER_PATCH;
return &s_plugin;
}檔案有點長,因為每個函式都有詳細的說明註解。真正的內容只有這些,由上往下:
#include "nexus_plugin_api.h":Plugin API 的標頭,來自sdk\。plugin_version.h是src\裡那個版本號檔案。HELLO_IC_NAME:這顆 Plugin 的名字,使用者會用它掛載。HELLO_IC_ADDR:你的 IC 的 7 位元 I2C 位址,預設0x58,請改成你自己的。 怎麼找見下面第 6 節。s_slaves[]:宣告「這顆 Plugin 要跟哪些 I2C 位址講話」。至少要有一列,否則 Core 拒載。 這一列的意思是「一顆用 8 位元暫存器位移定址、沒有 bank 的 IC」,多數 IC 都是這樣。 必須是static:Core 只讀不釋放,放在函式裡的區域變數載入後就失效。hello_ic_init():Plugin 掛載時 Core 呼叫的第一個 callback,在 UI Thread 上。 這裡只做一件事:host->LogWrite(...)寫一行 Log。host就是上一頁提到的 HostAPI 函式表。hello_ic_cleanup():卸載時的清理,強制要有、不可為NULL,而且在 I2C Worker Thread 上 (跟init不同條執行緒)。這一步沒有資源要釋放,所以是空的。s_plugin:那張IcPlugin表。也是static,一開始全是 0, 而 0 對每個選用的 callback 都是「我不提供」,所以你只需要填你要的。NexusPluginGetInterface():DLL 唯一的進入點,Core 載入 DLL 後第一個呼叫它, 你把s_plugin填好、把指標還回去。第一件事是檢查 ABI 主版本,不符就回NULL拒絕載入。 欄位一律具名賦值(s_plugin.name = ...)。
這一整套骨架,之後三步都不變;接下來每一步只是多填幾個欄位。 規則細節見生命週期與執行緒。
4. 建置
在 plugins\hello_ic\ 開命令提示字元:
build.bat clangmingw x64 release看到 [ OK ] plugin built: plugins\hello_ic.dll 就成功了。其他車道:
gcc、Visual Studio、
clang-cl、VC6。
主程式是 32 位元還是 64 位元,就要編哪一種,見架構要對。
不確定的話,先看你用的 nexus_tui.exe 是哪一種(該頁有檢查方法)。
5. 放進主程式並載入
-
產物在
plugins\hello_ic.dll(plugins\hello_ic\的上一層)。 -
把它複製到主程式旁邊的
plugins\資料夾——也就是跟nexus_tui.exe放在同一層的plugins\。免安裝版沒有這個資料夾的話自己建一個。 -
啟動主程式。程式啟動時掃描一次
plugins\。若主程式已經開著:視窗版用 ☰ → Plugin → Rescan plugins/ folder;終端機版沒有對應的指令,關掉重開。 -
看有沒有載進來:
plugin list看到
hello_ic(1.0.0)就成功了。


再掛載它,看它的 init() 有沒有被呼叫:
plugin switch hello_ic回應會是 PLUGIN: switched to hello_ic (probe in progress)。步驟 1 到 3 的 Plugin 宣告了「沒有面板」,這種掛載叫 headless(帶不帶 @p<N> 都一樣)
(沒有面板,見架構總覽)。要看到 Log 那一行,需要有一個面板顯示 I2C Bus Log
(沿用預設的即可);init() 寫的 hello_ic: loaded 會出現在裡面,前面的 [hello_ic] 是主程式自動加上的來源標籤:
![終端機版的 I2C Bus Log 面板,一行 INFO:[hello_ic] hello_ic: loaded](/img/plugin-step1-loaded-tui-detail.png)
![視窗版的 I2C Bus Log 面板,一行 INFO:[hello_ic] hello_ic: loaded](/img/plugin-step1-loaded-gui.png)
沒有掃到、plugin list 是空的:先看 Log 有沒有一行
plugin dll skipped: hello_ic.dll could not be loaded (win32 error 193)——193 就是 DLL 與主程式的
架構不同(訊息本身沒這樣說),見架構要對。其次是 DLL 放錯資料夾(要在 plugins\ 而不是
plugins\hello_ic\)、程式開著時沒有重新掃描。
6. 找出你的 IC 的位址
Plugin 裡的 HELLO_IC_ADDR 要填你的 IC 的位址。不知道的話,讓主程式幫你找:接好你的板子,
在指令列打
scan它會掃描匯流排,把有回應的所有 7 位元位址寫進 Log:


(這張圖的匯流排上剛好有三顆裝置,你的結果會不同。)找到你的 IC 那一個,例如 0x2A,
把 hello_ic.c 裡的 #define HELLO_IC_ADDR 0x58 改成 0x2A,重新編譯、重新複製 DLL、
重新載入。
注意 7 位元與 8 位元的差別:有些資料手冊寫的是 8 位元位址(已經把讀寫位元併進去,
例如 0xB0/0xB1)。這裡要的是7 位元位址,也就是 8 位元位址右移一位(0xB0 >> 1 = 0x58)。
不確定就用 scan,它印出來的就是對的格式。
沒有硬體:用模擬後端
沒有接任何 I2C 轉接器也可以做完整個教學。主程式內建模擬後端 mock,它假裝有兩顆裝置:
0x58(所以 HELLO_IC_ADDR 預設就是它)與 0x50,讀寫都是真的,只是資料留在記憶體裡。
終端機版用 --dongle mock 啟動:
nexus_tui.exe --dongle mock(或啟動後在指令列打 dongle mock 切過去。)
目前觀察到:有實體硬體接著的時候,視窗版會忽略
--dongle mock,連執行期的dongle mock指令也一樣, 繼續用你的硬體,而且沒有任何提示。要用模擬後端,請先把硬體拔掉,或改用終端機版 (終端機版會照做)。 本教學的畫面圖是接著實體 IC 拍的,不是模擬後端。