Skip to Content
Plugin 開發讀寫硬體的三種方式

讀寫硬體的三種方式

Plugin 碰硬體只有三個入口,都掛在 NxHostApi(init 存下來的那張函式表)上。 你不能直接呼叫任何底層 I2C 函式,也沒有這個必要:三個入口已經包好了 Log、暫存器模型與 bank 管理。

方式會阻塞嗎哪些執行緒能呼叫用途
ExecSync會,等到硬體回應才返回只有 I2C Worker Thread 上的 callback需要「馬上知道結果」:probe、自訂指令、按鈕處理
SubmitJob + on_complete不會,入列就返回UI Thread 與 Worker Thread 都行背景輪詢,任何「送出去,晚點再處理結果」的存取
GetRegSnapshot不會,完全不碰硬體任何執行緒取得 Core 目前記著的暫存器內容,畫面用

哪個 callback 跑在哪條執行緒,見生命週期與執行緒。

下面三種方式的對照程式放在同一個範例檔,可以下載來看(它是閱讀輔助,不是完整的 Plugin; 三個函式要自己接進 IcPlugin):

hardware_access.c下載 hardware_access.c
/**
 * @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\ 裡)。