讀寫硬體的三種方式
Plugin 碰硬體只有三個入口,都掛在 NxHostApi(init 存下來的那張函式表)上。
你不能直接呼叫任何底層 I2C 函式,也沒有這個必要:三個入口已經包好了 Log、暫存器模型與 bank 管理。
| 方式 | 會阻塞嗎 | 哪些執行緒能呼叫 | 用途 |
|---|---|---|---|
ExecSync | 會,等到硬體回應才返回 | 只有 I2C Worker Thread 上的 callback | 需要「馬上知道結果」:probe、自訂指令、按鈕處理 |
SubmitJob + on_complete | 不會,入列就返回 | UI Thread 與 Worker Thread 都行 | 背景輪詢,任何「送出去,晚點再處理結果」的存取 |
GetRegSnapshot | 不會,完全不碰硬體 | 任何執行緒 | 取得 Core 目前記著的暫存器內容,畫面用 |
哪個 callback 跑在哪條執行緒,見生命週期與執行緒。
下面三種方式的對照程式放在同一個範例檔,可以下載來看(它是閱讀輔助,不是完整的 Plugin;
三個函式要自己接進 IcPlugin):
/**
* @file hardware_access.c
* @brief Three ways a plugin reaches the hardware: ExecSync, SubmitJob with
* on_complete, and GetRegSnapshot. A reading aid for the tutorial
* page "hardware-access", not a complete plugin: the three functions
* below would be wired into an IcPlugin by name (probe, poll_fn).
*
* @note Kept ASCII-only on purpose (the PDK samples must survive VC6 and
* CP950), and C89 so it also passes the syntax-only guard.
*/
#include <string.h>
#include "nexus_plugin_api.h"
/** @brief The HostAPI table, saved by init() (not shown here). */
static const NxHostApi *s_host = (const NxHostApi *)0;
/** @brief Last bytes read by the background poll (written on the Worker). */
static NxByte s_last[4];
/**
* @brief Way 1 - ExecSync: blocking read, worker thread only.
* @param[in] ctx This mount's context.
* @return ERR_OK, or the error the hardware access returned.
* @note Call this from probe(), poll_fn(), a custom command handler or
* on_widget_event(). From init() (UI Thread) it returns
* ERR_WRONG_THREAD and touches nothing.
*/
NxLong hwa_read_id(NxPluginCtx ctx)
{
NxJobRequest req;
NxByte id;
memset(&req, 0, sizeof(req)); /* zeros = this mount's address, bank and backend */
req.size_of_struct = (NxDword)sizeof(req);
req.op = NX_JOB_READ;
req.offset = 0x00;
req.length = 1;
return s_host->ExecSync(ctx, &req, &id, (NxWord)sizeof(id));
}
/**
* @brief Way 2 (completion): runs on the Worker after the read finished.
* @param[in] ctx This mount's context.
* @param[in] rc ERR_OK or the error of the job.
* @param[in] data Bytes read; valid only during this call.
* @param[in] len Number of bytes in data.
* @param[in] user_data Whatever the request carried (unused here).
*/
static void NX_CALL hwa_on_done(NxPluginCtx ctx, NxLong rc, const NxByte *data,
NxWord len, void *user_data)
{
NX_UNUSED_ARG(user_data);
if (rc != ERR_OK || data == (const NxByte *)0)
{
return;
}
if (len > (NxWord)sizeof(s_last))
{
len = (NxWord)sizeof(s_last);
}
memcpy(s_last, data, (size_t)len); /* copy: data is gone after we return */
s_host->NotifyUiRefresh(ctx); /* the only way a Worker asks the UI to redraw */
}
/**
* @brief Way 2 - SubmitJob: queue a read and return at once.
* @param[in] ctx This mount's context.
* @return ERR_OK when queued; ERR_POLL_DISABLED when the user typed
* "poll off" (a low priority job is refused, which is correct).
* @note Legal on the UI Thread as well as the Worker: it never blocks.
*/
NxLong NX_CALL hwa_poll_fn(NxPluginCtx ctx)
{
NxJobRequest req;
memset(&req, 0, sizeof(req));
req.size_of_struct = (NxDword)sizeof(req);
req.op = NX_JOB_READ;
req.priority = NX_JOB_PRI_LOW; /* background work: obeys "poll off" */
req.offset = 0x00;
req.length = (NxWord)sizeof(s_last);
req.on_complete = hwa_on_done;
return s_host->SubmitJob(ctx, &req);
}
/**
* @brief Way 3 - GetRegSnapshot: no hardware access, any thread.
* @param[in] ctx This mount's context.
* @param[out] out Receives 256 register bytes.
* @param[out] valid Receives the 256-bit mask: bit set = that byte was really read.
* @return ERR_OK, or an error code with out/valid left alone.
* @note Never treat a byte whose valid bit is clear as 0x00: it was
* simply never read.
*/
NxLong hwa_snapshot(NxPluginCtx ctx, NxByte out[256], NxByte valid[32])
{
return s_host->GetRegSnapshot(ctx, 0, 0, out, 256, valid, 32);
}一、ExecSync:同步讀寫
第三步你已經用過。重點只有這幾條:
- 只能在 Worker 那一列的 callback 裡用。 在 UI Thread 上呼叫(例如放進
init、on_key_press、on_config_save)一律回ERR_WRONG_THREAD(-85),而且不會做任何硬體存取。這是刻意的:在 UI Thread 上等硬體,畫面會卡到整筆 I2C 交易結束。 - 它不進佇列。 它在你呼叫的當下、在這條 Worker 上直接執行,所以會阻塞到硬體回應為止。
memset的那些 0 就是全部設定。slave_addr == 0是「這次掛載目前綁定的位址」;bus_name留空是「這次掛載綁定的後端」。 不要改成填你自己slaves[]裡宣告的位址:使用者用addr/mount指令改過位址之後,你填的舊值會一直打到錯的位址。- 它忽略
priority、on_complete、user_data:同步呼叫沒有事後回呼。 - 單次讀寫最多 256 位元組(一個 bank);超過回
ERR_INVALID_PARAM,請分段。 - 寫入的形狀相同:
req.op = NX_JOB_WRITE; req.data = buf;,輸出緩衝傳(NxByte *)0, 0。
常見的回傳值:ERR_OK、ERR_HW_OFFLINE(沒接 dongle 或後端開不起來)、ERR_I2C_NACK(IC 沒有回應)。
沒有實體 dongle 時可以用模擬後端驗證 Plugin 本身,見第一個 Plugin 1。
分鐘級的長迴圈(燒錄、整片清除)要自己在每一輪問 ShouldAbort(ctx):Plugin 被標記待移除、後端離線、程式關閉時它回非零,
這時請自己收尾返回。Core 不會替你中斷任何東西。
二、SubmitJob:非同步
SubmitJob 把一筆工作放進佇列就返回,結果晚點才到,由你填的 on_complete 收。要點:
on_complete在這次掛載綁定的後端的 Worker Thread 上被呼叫,不在 UI Thread,也不在你呼叫SubmitJob的那條執行緒。 裡面不能畫圖;資料memcpy到自己的緩衝之後呼叫NotifyUiRefresh(ctx)請畫面重畫(data只在這次呼叫期間有效)。 同一個更新週期內多次呼叫NotifyUiRefresh會被合併成一次重繪。priority決定它受不受poll off管。 背景輪詢請用NX_JOB_PRI_LOW:使用者打了poll off, 低優先級的工作在入列的當下就被拒絕(回ERR_POLL_DISABLED,-61),這是正確行為,不是壞了。 你的poll_fn在poll off期間根本不會被呼叫;on_tick則仍會被呼叫,所以它裡面送的低優先級工作會得到ERR_POLL_DISABLED。memset之後priority是 0,也就是高優先級(NX_JOB_PRI_HIGH),使用者手動觸發的動作用它,不受poll off影響。- 佇列滿了會立刻回
ERR_QUEUE_FULL(-60),絕不阻塞。 - 合併: 為了不讓高頻刷新塞爆佇列,Core 只在五項全部成立時才合併:低優先級、
NX_JOB_READ、沒有on_complete、沒有NX_JOB_F_NO_COALESCE旗標、匯流排參數與所屬分頁(tab_id)相同。 另外,不同掛載之間(發出者與目標不是同一個掛載)的工作絕不合併。你的輪詢一旦填了on_complete,就不會被合併,每一筆都會執行、都會回呼。 data是在入列時就被複製的,SubmitJob返回後你可以立刻重用自己的緩衝。- 送到別的匯流排(
bus_name填另一個後端的名字)也用SubmitJob:它排進那個後端自己的 Worker 佇列。 名字不存在回ERR_HW_OFFLINE。(用ExecSync打到別條匯流排會回ERR_WRONG_THREAD,因為同步存取不可能跨執行緒。)
三、GetRegSnapshot:不碰硬體的快照
Core 記著每個掛載最近讀到的暫存器內容。GetRegSnapshot 把它複製到你的緩衝,不觸發任何硬體存取,所以任何執行緒都能叫。
它是 Canvas 模式在 ui_render_fn 之前準備繪圖資料的方式(ui_render_fn 內禁止碰硬體,見Canvas 模式)。
兩個容易漏的地方:
- 一定要附有效遮罩,
valid不接受NULL。 那是 256 個位元(32 位元組)的逐位元組旗標:某位元組真的被讀過,對應位元才是 1。 少了它,「從來沒讀過」的位置會被當成00,而00正好是最常見的合法值,畫錯了完全看不出來。 遮罩為 0 的位置,就是「不知道」,請顯示成--之類,不要顯示00。 slave_index是slaves[]的索引。 宣告了幾個位址就有幾個索引(0到slave_count - 1), 各自對應自己那顆 IC 的快照;超出範圍回ERR_INVALID_PARAM,不會偷偷退回第 0 顆的資料。
快照與遮罩要在同一次呼叫裡取得,不能分兩次呼叫:兩次中間如果剛好有一筆讀取完成,遮罩會說「有效」,快照拿到的卻是讀取前的舊值。
三種怎麼選
- 要在
probe裡確認 IC 在不在、或按一顆按鈕就要立刻讀一個值回報 →ExecSync(你就在 Worker 上)。 - 要定時更新畫面上的數值、或必須從 UI Thread 送出存取 →
SubmitJob(on_complete收結果,NotifyUiRefresh通知畫面)。 - 要畫暫存器熱圖,或只是想看 Core 目前記得的內容 →
GetRegSnapshot。
bank 的基本觀念
有些 IC 的暫存器分頁(bank):先寫一個「bank 選擇暫存器」再讀寫別的暫存器。你在 slaves[] 裡宣告這件事(bank_mode、bank 暫存器位址),
之後 Core 幫你處理:
- 是否帶 bank,由掛載宣告的
bank_mode決定,不是由req.bank的值反推。對「有 bank 暫存器」的掛載,req.bank == 0不是「不帶 bank」, 而是「用 Core 記得的那一頁」;填非 0 是只影響這一筆的單次覆寫。對「沒有 bank」的掛載,填非 0 的bank一律回ERR_INVALID_PARAM。 - 直接寫 bank 選擇暫存器就好。 寫入範圍恰好是那一個位元組時,Core 會自動轉成真正的換頁,並記住新的頁;
範圍涵蓋它但不是恰好一個位元組(例如一次寫 4 個位元組其中一個剛好是它)一律回
ERR_INVALID_PARAM,Core 不猜你的意圖。 - 換頁或讀寫失敗不會自動重試:Core 立刻把這顆 IC 標成離線並記一筆錯誤。
ExecSync 與 SubmitJob 對這些規則的處理逐字相同。完整規則(讀改寫 NX_JOB_RMW、NX_JOB_XFER、表外位址等)見
開發指南第 5 章與
API 參考的硬體存取一節(兩份文件也隨 PDK 附在 docs\ 裡)。