第一個 Plugin 3:讀暫存器
現在讓 Plugin 真的跟 IC 講話。這一步把 hello 旁邊再加一個 read 指令:讀 IC 的
暫存器 0x00,把值寫進 Log。這是整條路第一次走通:Plugin → Core → dongle → IC。
完整程式碼
用它取代你的 src\hello_ic.c(別忘了改回你的 HELLO_IC_ADDR):
/**
* @file hello_ic.c
* @brief hello_ic 教學步驟 3:在步驟 2 之上加一個 `read` 指令,真的讀一次 IC。
*
* 新概念只有一個:**`ExecSync()` 同步讀寫**,以及它只能在 I2C Worker Thread 上用。
* 自訂指令的處理函式恰好就跑在 Worker Thread,所以硬體存取寫在這裡最安全。
*
* @see docs/NEXUS_TUI_PLUGIN_GUIDE.md 第 4 章(執行緒)、第 5 章 5.1(ExecSync)
*/
#include <stdio.h>
#include <string.h>
#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 遮罩、旗標、保留、顯示名稱。
* @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 Core 提供的服務函式表,`init()` 時存下來,之後的 callback 才用得到。
* @note 生命期涵蓋整個行程;不得修改其內容、不得釋放。
*/
static const NxHostApi *s_host = (const NxHostApi *)0;
/**
* @brief Plugin 掛載時的初始化,Core 在 **UI Thread** 上呼叫。
*
* @param[in] ctx 本次掛載的上下文
* @param[in] host HostAPI 函式表
* @return `ERR_OK` 表示成功;非 `ERR_OK` 會使這次掛載中止。
*
* @warning 在 UI Thread 上執行,不得做 I2C 存取或任何會阻塞的事。
* @note 把 `host` 存進 `s_host`:自訂指令的處理函式只拿得到 `ctx`,
* 要呼叫 HostAPI 就得靠這個全域指標。
*/
static NxLong NX_CALL hello_ic_init(NxPluginCtx ctx, const NxHostApi *host)
{
s_host = host;
host->LogWrite(ctx, NX_LOG_INFO, "hello_ic: loaded");
return ERR_OK;
}
/**
* @brief Plugin 卸載時的清理。**強制實作,不得為 `NULL`。**
*
* @param[in] ctx 本次掛載的上下文
* @return 無
*
* @warning 在 **I2C Worker Thread** 上呼叫,返回後 `ctx` 立即失效。
* @note 把 `s_host` 清掉:卸載之後任何遲到的呼叫都不該再碰 HostAPI。
*/
static void NX_CALL hello_ic_cleanup(NxPluginCtx ctx)
{
NX_UNUSED_ARG(ctx);
s_host = (const NxHostApi *)0;
}
/**
* @brief 自訂指令 `hello` 的處理函式:寫一行 Log,回 `ERR_OK`。
*
* @param[in] ctx 本次掛載的上下文
* @param[in] argc 參數個數(含指令本身)
* @param[in] argv 參數陣列;只在本次呼叫期間有效
* @return `ERR_OK`。指令處理函式自己的回傳值就是這次指令的結果:
* 回 `ERR_OK` 或錯誤碼;非 `ERR_OK` 時主程式會顯示錯誤。
* (batch 模式下,非 ERR_OK 的回傳值會成為 Exit Code 的絕對值。)
*
* @warning 在該掛載所綁定後端的 **I2C Worker Thread** 上呼叫,不是 UI Thread。
*/
static NxLong NX_CALL hello_ic_cmd_hello(NxPluginCtx ctx, int argc, const char *const *argv)
{
NX_UNUSED_ARG(argc);
NX_UNUSED_ARG(argv);
s_host->LogWrite(ctx, NX_LOG_INFO, "hello_ic: hello!");
return ERR_OK;
}
/**
* @brief 自訂指令 `read` 的處理函式:讀 IC 的暫存器 0x00,把值寫進 Log。
*
* @param[in] ctx 本次掛載的上下文
* @param[in] argc 參數個數(含指令本身)
* @param[in] argv 參數陣列;只在本次呼叫期間有效
* @return `ERR_OK`,或 `ExecSync()` 失敗時的錯誤碼;非 `ERR_OK` 時主程式會顯示錯誤。
* (batch 模式下,非 ERR_OK 的回傳值會成為 Exit Code 的絕對值。)
*
* @warning 在 **I2C Worker Thread** 上呼叫,所以可以直接用 `ExecSync()`。
* **同一段程式若放進 `init()`(UI Thread),`ExecSync()` 會回
* `ERR_WRONG_THREAD`(-85)**,而且不會碰硬體。
* @note `memset` 清成全 0 就是全部的設定:位址 0 代表「用這次掛載當下的位址」
* (使用者可能用 `addr` 指令改過,別自己填 `HELLO_IC_ADDR`),
* bank 與後端也同樣沿用掛載的設定。
*/
static NxLong NX_CALL hello_ic_cmd_read(NxPluginCtx ctx, int argc, const char *const *argv)
{
NxJobRequest req;
NxByte value;
NxLong rc;
char line[64];
NX_UNUSED_ARG(argc);
NX_UNUSED_ARG(argv);
memset(&req, 0, sizeof(req));
req.size_of_struct = (NxDword)sizeof(req);
req.op = NX_JOB_READ;
req.offset = 0x00;
req.length = 1;
rc = s_host->ExecSync(ctx, &req, &value, (NxWord)sizeof(value));
if (rc != ERR_OK)
{
sprintf(line, "hello_ic: read 0x00 failed, rc=%ld", (long)rc);
s_host->LogWrite(ctx, NX_LOG_WARN, line);
return rc;
}
sprintf(line, "hello_ic: reg 0x00 = 0x%02X", (unsigned)value);
s_host->LogWrite(ctx, NX_LOG_INFO, line);
return ERR_OK;
}
/**
* @brief 本 Plugin 的自訂指令表:名稱、`help` 說明、用法、處理函式。
*
* @note 靜態生命期,Core 只讀不釋放。
* @warning 說明字串請用純 ASCII 收尾(VC6 以 CP950 掃描原始碼,中文結尾的
* 字串會讓引號無法關閉);這裡整段用英文最保險。
*/
static const NxCommandDef s_commands[] =
{
{ "hello", "say hello in the log", "hello", hello_ic_cmd_hello },
{ "read", "read register 0x00 and log it", "read", hello_ic_cmd_read }
};
/**
* @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.commands = s_commands;
s_plugin.command_count = (NxWord)(sizeof(s_commands) / sizeof(s_commands[0]));
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;
}新增了什麼
只有一個新的處理函式 hello_ic_cmd_read()、指令表多一列,以及開頭多兩個標準標頭
(<stdio.h> 給 sprintf、<string.h> 給 memset)。核心是這一小段:
NxJobRequest req;
NxByte value;
memset(&req, 0, sizeof(req));
req.size_of_struct = (NxDword)sizeof(req);
req.op = NX_JOB_READ;
req.offset = 0x00;
req.length = 1;
rc = s_host->ExecSync(ctx, &req, &value, (NxWord)sizeof(value));ExecSync() 是 Plugin 同步存取硬體的唯一入口:你交一張「請求單」,它直接在目前這條
I2C Worker 執行緒上同步執行,不排進佇列,硬體回應之後把結果放進你給的緩衝區才返回。逐行看:
memset(&req, 0, ...):整張單先清成 0。全 0 就是預設值:位址 0 代表「用這次掛載當下的位址」 (使用者可能用addr指令改過,所以不要自己填HELLO_IC_ADDR),bank 與使用的後端也同樣沿用掛載的設定。req.size_of_struct:填這張單自己的大小。API 以後可能在結構結尾加欄位, Core 靠這個值知道你用的是哪一版。每次都要填。req.op = NX_JOB_READ:這是一次讀取。還有寫入、讀改寫等。req.offset = 0x00、req.length = 1:從暫存器0x00開始讀 1 個位元組。ExecSync(ctx, &req, &value, sizeof(value)):最後兩個參數是接收資料的緩衝區與它的大小。 回傳ERR_OK是成功,否則是負的錯誤碼。
失敗時把錯誤碼寫進 Log(NX_LOG_WARN)並原樣回傳,主程式會顯示給使用者。
最重要的規則:為什麼放在指令裡
ExecSync() 只能在 I2C Worker Thread 上呼叫。 如果你把同一段程式放進 init()
(UI Thread),它會回傳 ERR_WRONG_THREAD(-85),而且不會碰硬體。
自訂指令的處理函式剛好跑在 Worker Thread,所以在這裡讀最安全,畫面也不會被卡住。
| 你看到 | 原因與處置 |
|---|---|
read 0x00 failed, rc=-85 | 這段程式放錯 callback,寫進了 init() 或其他 UI Thread callback。硬體存取只能在 Worker Thread 上的 callback |
read 0x00 failed, rc=-40 | 沒接 dongle 或後端開不起來。沒有硬體就用 --dongle mock |
建置、載入、執行
-
重新建置,把新的
hello_ic.dll複製進plugins\,關掉主程式重開。 -
啟動主程式(沒有硬體就加
--dongle mock):plugin switch hello_ic read
Log 出現讀到的值:


這兩張圖是接著實體 IC 拍的:0x58 的暫存器 0x00 讀回 0x54(ASCII 的 T)。你的數字取決於你的 IC;
拿去跟資料手冊對,值對得上,就代表位址、位移、接線都正確。
下一步
步驟 4:Widget 面板:不用打指令,按一顆按鈕就讀。