Skip to Content
Plugin 開發參考文件API 參考:Port Shim

API 參考:Port Shim

本頁內容與 PDK 內的 docs\NEXUS_TUI_PLUGIN_API_REFERENCE.md 相同(依章節分頁,連結已改成站內連結)。

6.0 include 規則

檔案誰 include規則
nexus_port.h每個韌體 .c每檔一次;標準函式庫標頭必須在它之前
nexus_port_keil.h需要 Keil 關鍵字的韌體 .c在 nexus_port.h 之後
nexus_port_plugin.h只有膠水檔(如 port_glue.c)整顆 Plugin 恰好一次,且先 #define NX_PORT_IMPL;韌體檔絕不 include

違反這張表在下列情境中會有確定的編譯期或連結期後果;每一項各自標注其依據——多數已用 gcc -std=c89 -pedantic 實測重現(訊息逐條列在描述裡;未在其餘工具鏈上覆核,不代表文字或嚴重度相同),少數未實測或本來就沒有可觀察症狀的另外標明:

  • 標準函式庫標頭排在 nexus_port.h 之後:nexus_port.h 尾端的巨集區塊(見下方 NX_PORT_IMPL 守衛)已把裸的 malloc/free/printf 改寫成 nx_port_malloc/nx_port_free/nx_port_printf;之後才 include 的 <stdlib.h>/<stdio.h> 內建原型會被這批巨集展開,導致宣告與本層原型(nx_port_malloc(unsigned long) 等)簽章不一致。實測輸出形如 nexus_port.h:380:27: error: conflicting types for 'nx_port_malloc'(外加一則指到 nexus_port.h 自己宣告 nx_port_malloc() 那一行的 note:)——錯誤指到的是 nexus_port.h 自己 #define malloc 那一行的巨集展開位置,不是 <stdlib.h> 內的宣告行,也不是任何一行韌體程式碼,容易誤判成本層標頭本身壞掉。
  • 韌體 .c 直接 include nexus_port_plugin.h:該檔開頭 #ifndef NX_PORT_IMPL 守衛的 #error "firmware sources must include nexus_port.h only" 立即中止編譯,訊息逐字就是這一行。韌體 .c 從不會自己定義 NX_PORT_IMPL(那是膠水檔的責任),故這個 #error 對韌體 .c 而言必定觸發。
  • 膠水檔忘記先 #define NX_PORT_IMPL 就 include nexus_port_plugin.h:同一個 #error,訊息相同——本檔無法分辨「這是韌體檔誤用」還是「膠水檔漏了一行 #define」,兩者共用同一道守門。
  • 膠水檔額外 include 了 nexus_port_keil.h:nexus_port_keil.h 開頭 #ifdef NX_PORT_IMPL 守衛的 #error "nexus_port_keil.h must not be included by port_glue.c" 立即中止編譯——膠水檔一定已經 #define NX_PORT_IMPL(見上一條的前提),故這個 #error 對膠水檔而言必定觸發。
  • 超過一個編譯單元 include nexus_port_plugin.h:該檔含函式本體,不只是宣告;連結期對 nx_i2c_reg_read、nx_port_bind、delay_ms 等每一個符號都各自出現一次形如 multiple definition of 'nx_i2c_reg_read' 的錯誤(GNU ld 的實測訊息形狀),Plugin DLL 因此整個連結失敗,不是警告。
  • 韌體 .c 用了 Keil 關鍵字(xdata/idata/pdata/data/code/bit/reentrant/_at_)卻沒有 include nexus_port_keil.h:這些不是 ANSI C89 關鍵字,第一個錯誤的確切文字依宣告寫法而定(兩者皆實測):把記憶體區域關鍵字寫在型別之前(例如 static xdata BYTE g_x;,本檔 plugins/port_demo 韌體範例的寫法)第一個錯誤是 error: unknown type name 'xdata';寫在型別之後(Keil 教科書慣用的 unsigned char xdata g_buf[4];)則是 error: expected '=', ',', ';', 'asm' or '__attribute__' before 'g_buf'——這種寫法的第一則訊息點名的是變數本身(g_buf,一個合法存在的名字),不是造成問題的 xdata,比前一種寫法更容易誤判成別的原因。兩種寫法後續每個函式各自使用到該變數的地方,都只在該函式內第一次用到時報一次「'g_buf' undeclared (first use in this function)」(gcc 自己會補一句 note: each undeclared identifier is reported only once for each function it appears in),不是每次使用都各報一次。
  • 把移植出來的 Plugin 編成靜態 Plugin,而非 DLL Plugin(未實測——本條依據 nexus_port_plugin.h 檔頭 Doxygen 的記載,不是本節其餘各條那樣的 gcc 實測結果):nexus_port_plugin.h 定義的 delay_ms(WORD) 與主程式內部另一個同名但簽章不同的函式,在同一個執行檔的連結期會撞名(C 沒有依簽章的名稱修飾),形如 multiple definition of 'delay_ms' 的連結期錯誤;只有編成 DLL Plugin 才有各自獨立的符號表,不受影響。
  • 韌體 .c include 了 nexus_plugin_api.h(沒有任何編譯期或連結期症狀——實測 nexus_port.h 與 nexus_plugin_api.h 同時 include 進同一個編譯單元零警告零錯誤,因為 nexus_plugin_api.h 本身不 include nexus_port.h、也不 typedef BYTE/WORD/DWORD,兩者不會撞型別;注意這與上面「韌體 include nexus_port_plugin.h」是兩支不同的標頭,後者才有 #error 守門):這是一條沒有編譯期守門的純架構規則,違反了只會讓韌體原始碼摻進它本來不該看到的 Plugin ABI 型別,是否連上 Plugin 這件事必須繼續由膠水檔一手包辦,韌體維持對 Plugin ABI 無感知。

NX_PORT_NO_SHORTHAND 與 nexus_port_adapter_ite_int.h 只有兩種可搭配的設定,不是三種——C89 不支援函式多載,i2c_read/i2c_write 這兩個識別字在同一個編譯單元裡永遠只有一種形狀:

  • (a) 預設:不定義 NX_PORT_NO_SHORTHAND,i2c_read/i2c_write 就是 nexus_port_plugin.h 提供的 BYTE*-shaped 簡短別名(見 i2c_read/i2c_write)。
  • (b) #define NX_PORT_NO_SHORTHAND 在 #include "nexus_port_plugin.h" 之前,讓該檔跳過它的 BYTE* 簡短別名宣告,接著 #include "nexus_port_adapter_ite_int.h"(其 #error 守門要求 nexus_port_plugin.h 必須已經 include 過,且要求已定義 NX_PORT_IMPL_PLUGIN)——此時 i2c_read/i2c_write 改指 IT6510D0 家族慣用的 int*-shaped 版本。兩個標頭可以同時 include 進同一個編譯單元,但 i2c_read/i2c_write 只會是其中一種形狀;沒有「兩者並存、依參數型別自動選一個」這種第三選項——一份真正的移植 glue 檔案本來就只會用到兩者之一。這個切換設定是給膠水檔用的——nexus_port_adapter_ite_int.h 的 #error 要求 NX_PORT_IMPL_PLUGIN 已定義,那只可能發生在已經 #define NX_PORT_IMPL 並 include 過 nexus_port_plugin.h 的編譯單元裡,韌體 .c 沒有管道 include 它——韌體 .c 自己若也定義 NX_PORT_NO_SHORTHAND,只會讓 nexus_port.h 不再替它宣告 BYTE*-shaped 原型,呼叫 i2c_read/i2c_write 時只有加 -Wall 才會看到 warning: implicit declaration of function 'i2c_read'(單獨 -std=c89 -pedantic 不會有任何提示,PDK 的 Makefile 有帶 -Wall);這不中止編譯,韌體 .c 在自己的原始碼裡本來就該有廠商自己提供、與 int*-shaped 簽章相符的原型,這正是 IT6510D0 家族韌體原本的寫法。真正決定連結結果的是膠水檔(唯一能定義這兩個符號的地方)的設定,不是韌體 .c 的設定;三種組合皆已實測:膠水檔用預設 (a)(不定義 NX_PORT_NO_SHORTHAND)時,它恆會匯出 BYTE*-shaped 定義——韌體 .c 不論自己有沒有定義 NX_PORT_NO_SHORTHAND,對 i2c_read/i2c_write 的呼叫都會連結成功,解析到這個 BYTE*-shaped 定義;若韌體 .c 那時其實是照 int*-shaped 簽章在呼叫(例如照抄 IT6510D0 家族韌體卻忘記膠水檔要對應改設定),這是簽章不符但連結器完全不會報錯的組合,是三者中最危險的一種。膠水檔定義 NX_PORT_NO_SHORTHAND 且 #include 了 nexus_port_adapter_ite_int.h 時,它匯出 int*-shaped 定義,韌體 .c 的呼叫連結成功並解析到正確的形狀——這才是 IT6510D0 家族要走的路徑。膠水檔定義了 NX_PORT_NO_SHORTHAND 卻沒有 #include 這個 adapter 時,整顆 Plugin 裡沒有任何一個編譯單元定義這兩個符號,連結期是 undefined reference to 'i2c_read'。結論:NX_PORT_NO_SHORTHAND 必須在韌體與膠水檔兩邊一致地設定,且膠水檔一旦定義它就必須同時接上 adapter——只有韌體 .c 一側定義它、膠水檔仍用預設 (a),是連結器抓不到的簽章錯位。

6.1 韌體側 nexus_port.h

nx_i2c_reg_read

簽章

int nx_i2c_reg_read(BYTE addr8, BYTE reg, BYTE *buf, WORD n);

用途:以 8-bit 暫存器位移讀取 I2C 從機的多個位元組;是 NxHostApi::ExecSync 的韌體側包裝——組一支 NxJobRequest::op 為 NX_JOB_READ 的請求並呼叫它,讓既有韌體原始碼原封不動地呼叫這個名字就能存取硬體。

執行緒

  • 正確執行緒:與 NxHostApi::ExecSync 相同——本掛載所綁後端自己的 I2C Worker Thread 上的 Plugin callback(probe/poll_fn/interrupt_handler/自訂指令 handler/on_widget_event/on_tick)。
  • 呼叫錯執行緒/尚未綁定:底層 ExecSync() 判定為錯執行緒時回 ERR_WRONG_THREAD(-85),本函式原樣傳回;nx_port_bind 從未在本次掛載被呼叫過(init() 還沒執行、或 nx_port_unbind 已經執行過)時,本函式直接回 ERR_INVALID_CONTEXT(-84),連 ExecSync() 都不會被呼叫。
  • 如何確認:查 PLUGIN_GUIDE 第 4 章執行緒歸屬表;本函式不自己判斷執行緒,限制完全繼承自 NxHostApi::ExecSync。

使用時機:既有韌體原始碼本來就呼叫類似名字的讀取函式時,最少改動的替換點——韌體檔只需要保留這個名字,不需要知道 NxJobRequest/ExecSync 的存在。等價的簡短別名見 i2c_read。

最小範例

/* 放在韌體的 xxx.c 內,某個由 probe() 轉呼的初始化函式裡 */ #define DEV_ADDR8 0xA0 /* 7-bit 0x50 左移一位後的 8-bit 寫入形式 */ #define REG_ID 0x00 BYTE id_val; int rc; rc = nx_i2c_reg_read(DEV_ADDR8, REG_ID, &id_val, 1); if (rc != 0) { return rc; /* 讓呼叫端(韌體的初始化進入點)原樣回報失敗 */ }

常見錯誤與症狀

  • ERR_WRONG_THREAD(-85):韌體函式被從 init() 這類 UI Thread callback 呼叫到(例如誤把硬體探測寫在 init() 而不是 probe())。
  • ERR_INVALID_CONTEXT(-84):nx_port_bind 尚未執行過,或已經 nx_port_unbind 過(多半是在 cleanup() 之後又被某個殘留的非同步呼叫到)。
  • ERR_HW_OFFLINE(-40)/ERR_INVALID_PARAM(-2):與 NxHostApi::ExecSync 的「常見錯誤與症狀」逐條相同;n 同時是 NxJobRequest::length ——0 或大於 256 一律 ERR_INVALID_PARAM(一個 Bank 的上限)。
  • ERR_BUFFER_TOO_SMALL:只在 buf 為 NULL 時發生——本函式呼叫 ExecSync() 時 out_cap 恆等於 n(即 length),故「容量不足」那一半條件永遠不成立,唯一能觸發這個碼的路徑是 buf == NULL。這代表 buf 容量不足但非 NULL 時本函式偵測不到:呼叫端必須自行保證 buf 至少有 n bytes 的容量,否則是一次沒有任何錯誤碼會攔下的緩衝區溢位。
  • addr8 忘記左移:本層的 addr8 是韌體慣用的「7-bit 位址左移一位」表示(內部先 addr8 >> 1 還原成 7-bit 再填 slave_addr),直接填 7-bit 值會存取到錯誤的裝置且不報錯(右移後恰好對到另一個合法位址時尤其難以察覺)。
  • 期待成功時拿到「實際讀到的位元組數」:不會——本函式恆 return (rc < 0) ? (int)rc : 0;,成功時永遠回 0,讀到的資料只存在 buf 裡。

nx_i2c_reg_write

簽章

int nx_i2c_reg_write(BYTE addr8, BYTE reg, const BYTE *buf, WORD n);

用途:以 8-bit 暫存器位移寫入 I2C 從機的多個位元組;組一支 NxJobRequest::op 為 NX_JOB_WRITE 的請求並呼叫 NxHostApi::ExecSync,其餘同 nx_i2c_reg_read。

執行緒

使用時機:既有韌體原始碼本來就呼叫類似名字的寫入函式時的替換點。等價的簡短別名見 i2c_write。

最小範例

/* 放在韌體的 xxx.c 內,某個由自訂指令 handler 轉呼的函式裡 */ #define DEV_ADDR8 0xA0 #define REG_CTRL 0x02 BYTE pattern; int rc; pattern = 0x01; rc = nx_i2c_reg_write(DEV_ADDR8, REG_CTRL, &pattern, 1); if (rc != 0) { return rc; }

常見錯誤與症狀

  • ERR_WRONG_THREAD(-85)/ERR_INVALID_CONTEXT(-84):同 nx_i2c_reg_read。
  • ERR_HW_OFFLINE(-40)/ERR_INVALID_PARAM(-2,含 n 為 0 或大於 256,以及 buf 為 NULL——NxJobRequest::data 對 NX_JOB_WRITE 不得為 NULL):同 nx_i2c_reg_read 的對應錯誤碼。不會出現 ERR_BUFFER_TOO_SMALL:本函式呼叫 ExecSync() 時恆傳 NULL/0 作為 out/out_cap(寫入不讀回任何資料),底層對 NX_JOB_WRITE 也從不檢查這兩個參數。
  • 誤以為 ExecSync() 會保留 buf 供之後非同步使用:不會——buf 內容在本次呼叫返回前就已經被複製走,返回後即可重複使用同一塊緩衝區寫下一筆資料。
  • addr8 忘記左移:同 nx_i2c_reg_read。

nx_i2c_reg16_read

簽章

int nx_i2c_reg16_read(BYTE addr8, WORD reg, BYTE *buf, WORD n);

用途:以 16-bit 暫存器位移讀取 I2C 從機的多個位元組(部分感測器/驅動 IC 用雙位元組暫存器定址);組一支 NX_JOB_READ(offset 填 16-bit 的 reg)並呼叫 NxHostApi::ExecSync。

執行緒

使用時機:韌體對象使用雙位元組暫存器位移時的替換點(本 SDK 沒有 8-bit/16-bit 兩種簽章共用一個名字的設計,韌體原始碼需要哪一種就直接呼叫哪一支)。

最小範例

/* 放在韌體的 xxx.c 內,一個讀 ISP 狀態暫存器的函式裡 */ #define ISP_ADDR8 0x90 #define ISP_BASE16 0x01C0 BYTE status; int rc; rc = nx_i2c_reg16_read(ISP_ADDR8, (WORD)(ISP_BASE16 | 0x02), &status, 1); if (rc != 0) { return rc; }

常見錯誤與症狀

  • ERR_WRONG_THREAD(-85)/ERR_INVALID_CONTEXT(-84)/ERR_HW_OFFLINE(-40):同 nx_i2c_reg_read。ERR_BUFFER_TOO_SMALL 同樣只在 buf 為 NULL 時發生,理由同 nx_i2c_reg_read——buf 容量不足但非 NULL 時本函式偵測不到。
  • 16-bit 定址是否真的發生在匯流排上,取決於本掛載的位址寬度設定,不是呼叫了哪一支函式:若本掛載仍是預設的 8-bit 定址,reg <= 0xFF 的呼叫會靜默以 8-bit 線路交易完成,不報任何錯誤或警告;reg > 0xFF 時,見 NxJobRequest::offset——已掛載的 8-bit 裝置回 ERR_INVALID_PARAM,不會把 reg 截斷後送去硬體。本函式無法查詢也無法覆寫掛載的位址寬度,呼叫端必須自行確保掛載組態正確(宣告 slaves[] 時填對 addr_mode,見 NxSlaveDecl::addr_mode)——但填對也不保證生效:若使用者在載入本 Plugin 之前已經對這個掛載槽設過位址(addr/mount),slaves[0].addr_mode 會被忽略,掛載沿用既有的定址寬度(見 NxSlaveDecl::addr_mode 的已知陷阱),同樣不報錯。

nx_i2c_reg16_write

簽章

int nx_i2c_reg16_write(BYTE addr8, WORD reg, const BYTE *buf, WORD n);

用途:以 16-bit 暫存器位移寫入 I2C 從機的多個位元組;組一支 NX_JOB_WRITE(offset 填 16-bit 的 reg)並呼叫 NxHostApi::ExecSync。

執行緒

使用時機:同 nx_i2c_reg16_read,方向為寫入。

最小範例

/* 放在韌體的 xxx.c 內 */ #define ISP_ADDR8 0x90 #define ISP_BASE16 0x01C0 BYTE cmd; int rc; cmd = 0x5A; rc = nx_i2c_reg16_write(ISP_ADDR8, (WORD)(ISP_BASE16 | 0x00), &cmd, 1); if (rc != 0) { return rc; }

常見錯誤與症狀

  • 同 nx_i2c_reg16_read 的 16-bit/8-bit 定址靜默截斷警示,方向為寫入——寫錯暫存器同樣不報錯。
  • buf 為 NULL:ERR_INVALID_PARAM(-2),同 nx_i2c_reg_write。

nx_i2c_reg_set

簽章

int nx_i2c_reg_set(BYTE addr8, BYTE reg, BYTE mask, BYTE val);

用途:讀改寫單一暫存器的部分位元;組一支 NxJobRequest::op 為 NX_JOB_RMW 的請求(length 固定填 1,and_mask = ~mask、or_mask = val & mask,即 new = (old & ~mask) | (val & mask))並呼叫 NxHostApi::ExecSync,保證底層以單一原子 Job 完成。

執行緒

使用時機:韌體原始碼常見的 set/clr/換頁寫法(例如先讀改寫 bank select 暫存器再繼續存取);比自己「讀一次、改一個位元、再寫回去」更安全,因為 Core 保證中間不會被其他 Job 插隊。

最小範例

/* 放在韌體的 xxx.c 內,一個換頁函式裡(IT6520 dprxset 形狀) */ #define DEV_ADDR8 0xA0 #define BANK_REG 0x05 #define BANK_MASK 0x0F int rc; rc = nx_i2c_reg_set(DEV_ADDR8, BANK_REG, BANK_MASK, (BYTE)1); if (rc != 0) { return rc; }

常見錯誤與症狀

  • ERR_WRONG_THREAD(-85)/ERR_INVALID_CONTEXT(-84)/ERR_HW_OFFLINE(-40):同 nx_i2c_reg_read。不會出現「表外位址帶非 0 的 bank」這種 NxJobRequest::bank 相關的 ERR_INVALID_PARAM:本函式建構的請求 bank 欄位恆是 memset 後留下的 0(沿用本掛載目前的頁),從不主動覆寫。
  • 期待拿到讀改寫後的實際結果值:不會——ExecSync() 讀回的新值存在本函式內部的區域變數,不會回傳給呼叫端;需要之後的值請再呼叫一次 nx_i2c_reg_read(plugins/port_demo 的 fw_run_all() 就是這樣示範的)。
  • 把 mask/val 搞反:mask 決定「哪些位元受影響」,val 是套用在這些位元上的新值(val 落在 mask 之外的位元一律被忽略,不會意外污染其他位元)——不是「先寫 val 再用 mask 過濾」。

nx_i2c_write_raw

簽章

int nx_i2c_write_raw(BYTE addr8, const BYTE *buf, WORD n);

用途:不含暫存器位移前綴、直接對 I2C 從機寫入一段原始位元組序列;組一支 NxJobRequest::op 為 NX_JOB_XFER 的請求(data/length 填 buf/n,rx_len 留 0)並呼叫 NxHostApi::ExecSync。

執行緒

使用時機:韌體對象沒有「暫存器位移」這個概念、或需要送出的不是單一位移位元組時(例如只送位址、不帶任何資料的 address-only 探測循環)。n == 0 合法,代表只送位址;此時 buf 可以是 NULL。

最小範例

/* 放在韌體的 xxx.c 內,一個只探測位址是否存在的函式裡(IT6227 DBGR 形狀) */ #define MPTX_ADDR8 0xB0 int rc; rc = nx_i2c_write_raw(MPTX_ADDR8, (const BYTE *)0, 0); if (rc != 0) { return rc; }

常見錯誤與症狀

  • ERR_WRONG_THREAD(-85)/ERR_INVALID_CONTEXT(-84)/ERR_HW_OFFLINE(-40):同 nx_i2c_reg_read。
  • 連線後端不支援 NX_JOB_XFER:ERR_NOT_SUPPORTED(-4)——這不是呼叫端的錯,見 NxJobRequest::op:是否支援由後端決定,Plugin 只能以實際呼叫後的回傳碼判定,無法事先查詢。
  • n > 0 但 buf 為 NULL:ERR_INVALID_PARAM(-2);n == 0 時 buf 允許為 NULL,不要把兩種情境混為一談。
  • 期待本函式帶一個暫存器位移:不會——本函式不讀取暫存器位移的概念,需要「先寫位移再讀回」請改用 nx_i2c_write_read。

nx_i2c_write_read

簽章

int nx_i2c_write_read(BYTE addr8, const BYTE *wbuf, WORD wn, BYTE *rbuf, WORD rn);

用途:組合式「先寫後讀」(write-then-repeated-start-read)交易,中間不釋放匯流排;組一支 NX_JOB_XFER(data/length 填 wbuf/wn,rx_len 填 rn)並呼叫 NxHostApi::ExecSync(out/out_cap 填 rbuf/rn)。

執行緒

使用時機:韌體對象需要先送出命令碼(可能不只一個位元組)後立即讀回結果、中間不能被其他交易插隊的情形(例如某些 IC 的 PIO/DMA 讀取形狀)。

最小範例

/* 放在韌體的 xxx.c 內,一個先寫前綴再讀回的函式裡(IT6520 pio 形狀) */ #define DPRX_ADDR8 0xA0 BYTE prefix; BYTE val; int rc; prefix = 0x30; rc = nx_i2c_write_read(DPRX_ADDR8, &prefix, 1, &val, 1); if (rc != 0) { return rc; }

常見錯誤與症狀

  • ERR_WRONG_THREAD(-85)/ERR_INVALID_CONTEXT(-84)/ERR_HW_OFFLINE(-40)/ERR_NOT_SUPPORTED(-4,後端不支援 NX_JOB_XFER):同 nx_i2c_write_raw。
  • rn 超過 NX_JOB_MAX_PAYLOAD(512):ERR_INVALID_PARAM(-2),見 NxJobRequest::rx_len——拒絕,不截斷,不會回一段比要求短的資料。
  • wn > 0 但 wbuf 為 NULL:ERR_INVALID_PARAM(-2)。
  • ERR_BUFFER_TOO_SMALL:只在 rn > 0 且 rbuf 為 NULL 時發生——本函式呼叫 ExecSync() 時 out_cap 恆等於 rn,「容量不足」那一半條件永遠不成立。rbuf 容量不足但非 NULL 時本函式偵測不到,呼叫端必須自行保證 rbuf 至少有 rn bytes。
  • 把它當成兩次獨立交易:不是——中間不釋放匯流排是本函式存在的理由,若只是想分開做兩件事,直接呼叫兩次 nx_i2c_write_raw/nx_i2c_reg_read 即可,不需要本函式。

nx_i2c_set_bus

簽章

int nx_i2c_set_bus(const char *bus_name);

用途:指定接下來的 nx_i2c_* 存取所綁定的具名匯流排(多後端拓撲下的 bus 選擇)。本函式本身不建構 Job、不呼叫 NxHostApi::ExecSync——它只是把名字抄進本檔案的內部狀態,之後 nx_i2c_reg_read/_write/nx_i2c_reg16_read/_write/nx_i2c_reg_set/nx_i2c_write_raw/nx_i2c_write_read 這七支函式(不含本函式自己,是共第八支 nx_i2c_* 符號)都會把它填進各自 NxJobRequest 的 NxJobRequest::bus_name。

執行緒

  • 正確執行緒:不做執行緒檢查,但不是執行緒安全的——本函式的實作只是把 bus_name 複製進一份本檔案內部的共用狀態並恆回 ERR_OK,完全不檢查目前是哪條執行緒、也不檢查是否已經 nx_port_bind 過;正確用法是只從韌體自己那唯一一條 I2C Worker Thread 呼叫(同 nx_i2c_reg_read),不是「隨便哪條執行緒都安全」。
  • 呼叫錯執行緒:不會回任何錯誤碼——即使在 UI Thread 或尚未綁定的狀態下呼叫也會成功記下名字;但若同時有另一條執行緒也在呼叫本檔其餘 nx_i2c_* 函式(它們會讀這份共用狀態),兩者之間沒有任何互斥,是未定義的競態,不是「呼叫錯執行緒直接失敗」的形狀。
  • 如何確認:sdk/nexus_port.h 對它的 @note 宣稱「執行緒限制同 nx_i2c_reg_read()」,但實作完全沒有做那項檢查——以下方「常見錯誤與症狀」的實測行為為準。

使用時機:在同一次呼叫其中一支 nx_i2c_* 函式之前呼叫,用來選定(或確認)目前這個掛載所屬、韌體自己這條 I2C Worker Thread 本來就擁有的那一條具名匯流排——不是用來跨到一條真正不同的匯流排:見下方「常見錯誤與症狀」,名字只要解不回本掛載自己所屬的匯流排就會失敗。完成後應呼叫 nx_i2c_set_bus((const char *)0) 切回沿用預設,避免這條匯流排名稱一路殘留影響到後面完全不相干的存取。

最小範例

/* 放在韌體的 xxx.c 內,讀完一次就切回預設(IT6565 VGA DDC edid_read 形狀) */ #define DPRX_ADDR8 0xA0 #define EDID_REG 0x20 BYTE val; int rc; rc = nx_i2c_set_bus("mock"); if (rc == 0) { rc = nx_i2c_reg_read(DPRX_ADDR8, EDID_REG, &val, 1); } (void)nx_i2c_set_bus((const char *)0); /* 切回沿用本掛載預設 */

常見錯誤與症狀

  • 恆回 ERR_OK(0):名字是否真的解析得到,要看下一筆 nx_i2c_* 存取的回傳碼,不是本函式的回傳碼——本函式從不驗證這個名字。
  • 名稱不在後端註冊表裡:下一筆存取回 ERR_HW_OFFLINE(-40);名稱存在但不是本掛載自己所屬的匯流排:下一筆存取回 ERR_WRONG_THREAD(-85)——這條同步捷徑只能解回本掛載自己所屬的匯流排,要真的存取另一條不同的匯流排,得改由膠水檔經 NxHostApi::SubmitJob 帶 bus_name 送出。
  • 忘記切回預設:本函式記下的名字沒有生命週期限制,會一路套用到後面每一筆 nx_i2c_* 存取,直到再被呼叫一次(含傳 NULL/空字串)為止——容易讓後面完全不相干的存取莫名其妙指到錯的匯流排。
  • 誤以為它只影響「下一筆」存取:不是「一次性」,是「持續生效直到下一次呼叫本函式」——見上一條。
  • 同一顆 Plugin DLL 若被多個 Tab 掛載:本函式操作的內部緩衝是整顆 DLL 共用的檔案靜態狀態,不是逐掛載各自獨立——一顆移植 DLL 同時只能服務一個掛載,第二個掛載會在 init() 被 nx_port_bind 拒絕,見該節的「回傳值」與「常見錯誤與症狀」。

i2c_read

簽章

int i2c_read(BYTE addr8, BYTE reg, BYTE *buf, WORD n);

用途:nx_i2c_reg_read 的簡短別名,對齊部分韌體既有的 i2c_read(addr, reg, buf, n) 呼叫慣例,讓移植時不需要改呼叫端。受 6.0 include 規則 的 NX_PORT_NO_SHORTHAND 守門:定義該巨集會讓 nexus_port.h 這一份 BYTE*-shaped 宣告消失——但這只影響定義該巨集的那個編譯單元自己看不看得到原型,不代表 i2c_read 這個符號在整顆 Plugin 連結出來時也跟著消失:C 的連結按符號名稱比對,不比對簽章,而真正決定這個符號連結到哪個定義的是膠水檔的設定,不是韌體 .c 的設定——見 6.0 include 規則 三種組合(膠水檔預設 (a)/膠水檔設定 (b) 含 adapter/膠水檔定義 NX_PORT_NO_SHORTHAND 卻沒接 adapter)各自的連結結果,其中「韌體 .c 自己定義了 NX_PORT_NO_SHORTHAND,但膠水檔仍是預設 (a)」是最危險的一種:不會有任何編譯或連結期錯誤,卻會把韌體以為的 int*-shaped 呼叫靜默連到 BYTE*-shaped 定義。

執行緒

使用時機:韌體原始碼本來就呼叫 i2c_read() 這個名字時,不需要改動呼叫端。新寫的移植 glue 若沒有既有呼叫慣例的包袱,直接用 nx_i2c_reg_read 更明確。

最小範例

/* 放在韌體的 xxx.c 內,直接沿用既有呼叫慣例 */ #define DEV_ADDR8 0xA0 #define REG_STATUS 0x03 BYTE status; int rc; rc = i2c_read(DEV_ADDR8, REG_STATUS, &status, 1); if (rc != 0) { return rc; }

常見錯誤與症狀

  • 全部同 nx_i2c_reg_read——本函式的函式體恆為 return nx_i2c_reg_read(addr8, reg, buf, n);,逐位元組相同的行為。
  • 韌體 .c 自己定義 NX_PORT_NO_SHORTHAND 期待「原型也跟著改成 int*-shaped」:不會——韌體 .c 沒有管道 #include nexus_port_adapter_ite_int.h(它只服務膠水檔),故韌體 .c 自己的編譯單元裡 i2c_read 就是沒有原型;呼叫它只有加 -Wall 才會看到 warning: implicit declaration of function 'i2c_read'(警告,不中止編譯;單獨 -std=c89 -pedantic 不會有任何提示,plugins/port_demo 的 Makefile 有帶 -Wall)。
  • 誤以為「韌體 .c 定義了 NX_PORT_NO_SHORTHAND」本身就足以讓呼叫連到 int*-shaped 定義,或誤以為連結一定失敗:兩者都錯——連結結果完全由膠水檔的設定決定,與韌體 .c 自己是否定義這個巨集無關。韌體 .c 不是不能自己定義 int i2c_read(...)/int i2c_write(...) 這兩個符號——語法上完全合法,只是一旦膠水檔那份編譯單元也定義了同名符號(預設 (a) 的情況),連結期會是 multiple definition of 'i2c_read',直接中止建置,不是靜默問題;韌體 .c 真正沒有管道的是 #include nexus_port_adapter_ite_int.h(它只服務膠水檔),所以韌體 .c 自己的編譯單元裡看不到 int*-shaped 原型。三種膠水檔設定皆已實測:膠水檔用預設 (a) 時,它照常匯出 BYTE*-shaped 定義,韌體那個未宣告呼叫照樣連結成功,但靜默解析到形狀不符的 BYTE*-shaped 定義——這是最危險的組合,沒有任何編譯或連結期錯誤能抓到簽章不符;膠水檔定義 NX_PORT_NO_SHORTHAND 並 #include 了 nexus_port_adapter_ite_int.h 時,韌體那個呼叫連結成功並正確解析到 int*-shaped 定義——這才是 IT6510D0 家族要走的路徑;膠水檔定義了 NX_PORT_NO_SHORTHAND 卻沒有接上 adapter 時,整顆 Plugin 裡沒有任何定義,連結期是 undefined reference to 'i2c_read'。反方向同樣危險且不對稱:韌體 .c 維持預設 (a)(看到的是 nexus_port.h 的 BYTE*-shaped 原型,以 BYTE*-shaped 呼叫慣例呼叫),但膠水檔卻定義了 NX_PORT_NO_SHORTHAND 並接上 int*-shaped adapter——連結一樣成功(C 的連結只比對符號名稱、不比對簽章),韌體那個依 BYTE*-shaped 慣例組出的呼叫會靜默解析到 int*-shaped 定義,同樣是編譯期與連結期都抓不到的簽章錯位。結論:NX_PORT_NO_SHORTHAND 必須在韌體與膠水檔兩邊一致設定,膠水檔一旦定義它就必須同時接上 adapter,見 6.0 include 規則。

i2c_write

簽章

int i2c_write(BYTE addr8, BYTE reg, const BYTE *buf, WORD n);

用途:nx_i2c_reg_write 的簡短別名,理由與 i2c_read 相同。

執行緒

使用時機:同 i2c_read,方向為寫入。

最小範例

/* 放在韌體的 xxx.c 內 */ #define DEV_ADDR8 0xA0 #define REG_CTRL 0x02 BYTE v; int rc; v = 0x01; rc = i2c_write(DEV_ADDR8, REG_CTRL, &v, 1); if (rc != 0) { return rc; }

常見錯誤與症狀

  • 全部同 nx_i2c_reg_write——本函式的函式體恆為 return nx_i2c_reg_write(addr8, reg, buf, n);。
  • 與 i2c_read 相同的 NX_PORT_NO_SHORTHAND 提醒——定義該巨集後,韌體 .c 自己的編譯單元裡沒有原型(只有加 -Wall 才會看到 implicit declaration 警告),但連結期實際解析到哪個定義(BYTE*-shaped、int*-shaped、或完全解析不到)完全由膠水檔的設定決定,與韌體 .c 自己的設定無關——三種組合見 i2c_read 的「常見錯誤與症狀」與 6.0 include 規則,其中膠水檔仍用預設 (a) 的組合會靜默連結成功但簽章不符,是三者中最危險的一種。

delay_ms

簽章

void delay_ms(WORD ms);

用途:延遲毫秒等級的時間,對應韌體常見的 delay_ms();不經任何 NxHostApi,直接呼叫 Windows 的 Sleep()。

執行緒

  • 正確執行緒:I2C Worker Thread 上的 Plugin callback(同 nx_i2c_reg_read 列出的那幾個)。
  • 呼叫錯執行緒:本函式自己偵測是否被 UI Thread 呼叫(依 nx_port_bind 於 init() 當下捕捉到的執行緒 id 判定,不是靠 ExecSync() 那種硬體側判定)——是則記一次 NX_LOG_ERROR(訊息含 delay_ms)並直接返回,不睡眠;否則正常 Sleep(ms)。尚未 nx_port_bind() 就呼叫:捕捉到的執行緒 id 初始值是 0,任何真實的 Windows 執行緒 id 都不會是 0,故這道保護必定判定為「不是 UI Thread」而直接睡眠——這與其餘 nx_i2c_* 系列「尚未綁定一律回 ERR_INVALID_CONTEXT」的行為不同,因為本函式回傳型別是 void,沒有錯誤碼可回。
  • 如何確認:查 PLUGIN_GUIDE 第 4 章執行緒歸屬表;本函式的 UI Thread 誤用偵測是移植層自己做的,不是 ExecSync() 那條路徑。

使用時機:韌體原始碼常見的 delay_ms(N) 呼叫點,原封不動保留即可。單次延遲超過 nx_port_delay_warn_ms(見 Port Shim 相容別名巨集)時另記一次 NX_LOG_WARN,提醒可能遺留了會悄悄卡住 I2C Worker Thread 的長延遲迴圈。

最小範例

/* 放在韌體的 xxx.c 內,一段命令送出後的等待(IT6520 韌體常見形狀) */ delay_ms(500);

常見錯誤與症狀

  • 在 UI Thread 上呼叫(例如誤把 delay_ms() 寫進 init()):Log 出現一則含 delay_ms 字樣的 NX_LOG_ERROR,且完全不睡眠——不是慢一點,是這次呼叫的等待時間直接消失,若韌體邏輯依賴這段等待生效,行為會與原本在真實 MCU 上不同。
  • 單次呼叫的 ms 超過 nx_port_delay_warn_ms(預設 50):多記一則 NX_LOG_WARN,不影響實際睡眠時間,只是提示。
  • 誤以為 delay_us 有同樣的 UI Thread 保護:沒有,見 delay_us 的「常見錯誤與症狀」。
  • 在極熱的迴圈內大量呼叫小 ms:每次呼叫都是一次真正的 Sleep() 系統呼叫,標準 Windows 排程精度約 15.6ms,ms 很小時實際睡眠時間可能被無條件放大到這個量級。

delay_us

簽章

void delay_us(WORD us);

用途:延遲微秒等級的時間,對應韌體常見的 delay_us();不經任何 NxHostApi,以 QueryPerformanceCounter() 忙等實作。

執行緒

  • 正確執行緒:I2C Worker Thread 上的 Plugin callback(同 nx_i2c_reg_read)——這是設計上的限制,實作沒有強制它。
  • 呼叫錯執行緒:本函式完全不檢查呼叫執行緒——與 delay_ms 不同,它不比對 init() 當下捕捉到的執行緒 id,在 UI Thread 上呼叫一樣會原樣忙等(最多鉗制到 1ms),不記任何 Log、不返回任何錯誤碼,是一次會實際阻塞呼叫執行緒的忙等迴圈。
  • 如何確認:sdk/nexus_port.h 對 delay_us 的 @warning 宣稱「執行緒限制同 delay_ms()」——但那只是標頭的宣稱,nexus_port_plugin.h 的實際函式體並不落實它:函式體裡沒有任何執行緒判斷,在任何執行緒呼叫都會原樣忙等,不會拒絕、不會記錄、也不會跳過。

使用時機:韌體原始碼常見的微秒級忙等(delay_us(N)),us 內部鉗制在 1000(1ms)——超過這個量級的等待請改用 delay_ms,忙等太久會不必要地佔滿呼叫它的那條執行緒的 CPU;在 UI Thread 呼叫時同樣會忙等(最多 1ms),本函式本身不會替你擋下這個誤用。

最小範例

/* 放在韌體的 xxx.c 內,一段訊號建立時間的等待 */ delay_us(200);

常見錯誤與症狀

  • 在 UI Thread(如 init())呼叫:不會記任何 ERROR、不會被跳過——會真的忙等最多 1ms 並阻塞 UI Thread;雖然鉗制在 1ms 以內、數字上沒有超過,但這是一段真正佔用 CPU 的忙等迴圈,不是可中斷的等待,移植層本身既不偵測也不擋下這個呼叫。
  • 標準 Windows 排程精度約 15.6ms,微秒等級延遲在此環境下不承諾實體時間精度,只承諾「至少延遲這麼久」——us 一旦超過 1000 會被靜默鉗制到 1000,不是報錯。
  • 期待超過門檻會像 delay_ms 一樣記一次 NX_LOG_WARN:不會——本函式的忙等時間已經鉗制在 1ms 以內,實作刻意不對它套用 nx_port_delay_warn_ms 門檻的邏輯。

nx_port_tick_ms

簽章

DWORD nx_port_tick_ms(void);

用途:取得本層的相對時間戳(毫秒),供韌體移植碼量測經過時間、實作逾時判斷;不經任何 NxHostApi,直接轉呼 Windows 的 GetTickCount()。

執行緒

  • 正確執行緒:任一執行緒皆可呼叫(UI Thread 或 I2C Worker Thread),本函式不涉及硬體存取,也不檢查是否已 nx_port_bind。
  • 呼叫錯執行緒:不適用——沒有執行緒限制。
  • 如何確認:sdk/nexus_port.h 對 nx_port_tick_ms 的 @note「可在 UI Thread 或 I2C Worker Thread 呼叫」。

使用時機:韌體原始碼需要量測「經過了多久」時(例如自己手寫的逾時迴圈,不透過 nx_port_should_abort 判斷取消)。

最小範例

/* 放在韌體的 xxx.c 內,一個自訂逾時迴圈 */ DWORD start; DWORD now; start = nx_port_tick_ms(); for (;;) { now = nx_port_tick_ms(); if ((DWORD)(now - start) > (DWORD)2000) { break; } /* 差值比較,見下方常見錯誤 */ /* [範本] 這裡放一次輪詢 */ }

常見錯誤與症狀

  • 用大小比較而不是差值比較:本函式的回傳值會在約 49.7 天後 wrap,必須寫成 (DWORD)(now - start) > threshold,直接寫 now > start + threshold 在 wrap 附近會得到錯誤結果。
  • 期待它反映真正的牆上時鐘(wall clock):不是,它只是一個持續遞增(並在 49.7 天後 wrap)的相對計時,不能拿來算現在幾點幾分。

nx_port_should_abort

簽章

int nx_port_should_abort(void);

用途:查詢目前是否應中止長時間執行的迴圈(例如韌體常見的 polling/wait-for-ready 迴圈),讓移植碼能在 Plugin 下線或 Host 要求關閉時及早跳出;轉呼 NxHostApi::ShouldAbort。

執行緒

  • 正確執行緒:與 NxHostApi::ShouldAbort 相同——任一執行緒皆可呼叫,沒有執行緒限制。
  • 呼叫錯執行緒:不適用——沒有執行緒限制;ctx/host 尚未 nx_port_bind 時本函式直接回 0(可以繼續),不是錯誤碼,因為回傳型別是 int 語意上的布林值,不是 nexus_errors.h 的錯誤碼。
  • 如何確認:查 NxHostApi::ShouldAbort 的「執行緒」段;本函式不自己新增任何限制,只是多了一層「尚未綁定時回 0」的哨兵。

使用時機:分鐘級的長迴圈(如 ISP 燒錄、fw_isp_loop() 這類)應在每一輪查一次本函式並在非零時儘快返回——Host 不會替 Plugin 中斷任何東西,理由與回傳非零的四個來源,見 NxHostApi::ShouldAbort。

最小範例

/* 放在韌體的 xxx.c 內,長效迴圈(由自訂指令 isp 轉呼) */ WORD n; BYTE v; int rc; n = 0; while (!nx_port_should_abort()) { rc = nx_i2c_reg16_read(0x90, (WORD)(0x01C0 | (n & 0x3F)), &v, 1); if (rc == 0) { /* [範本] 這裡處理讀到的 v */ } delay_ms(10); n++; }

常見錯誤與症狀

  • 全部同 NxHostApi::ShouldAbort 的「常見錯誤與症狀」——本函式只是原樣轉呼並把 NxBool 轉成 int(0/1)。
  • 尚未綁定時忽略回傳值 0 就繼續執行大量工作:不算誤用——0 就是「可以繼續」的正常語意,只是尚未綁定狀態下不會有任何硬體存取能成功,繼續執行多半只是在空轉。

nx_port_printf

簽章

int nx_port_printf(const char *fmt, ...);

用途:移植層的 printf 替代實作,把格式化字串逐行組裝後透過 NxHostApi::LogWrite(等級恆為 NX_LOG_INFO)送出;預設會把裸的 printf 呼叫透過巨集改寫為本函式(見 Port Shim 相容別名巨集)。

執行緒

  • 正確執行緒:I2C Worker Thread(同 nx_i2c_reg_read)——雖然底層 NxHostApi::LogWrite 本身任一執行緒皆可呼叫,但本函式內部用一個檔案靜態緩衝區逐字元累積待輸出內容,在多執行緒下不具重入安全性,故僅供單一 I2C Worker Thread 呼叫。
  • 呼叫錯執行緒:不會回任何錯誤碼(LogWrite() 任一執行緒皆可呼叫),但若同時有兩條執行緒呼叫本函式,兩邊的輸出字元會交錯寫進同一塊緩衝區,Log 內容會混雜、不可預期——這不是「呼叫錯執行緒直接失敗」的形狀,是靜默的資料損毀。
  • 如何確認:nexus_port_plugin.h 對 nx_port_printf 的 @warning「靜態緩衝區在多執行緒下不具重入安全性」。

使用時機:既有韌體原始碼呼叫 printf()(會被巨集自動改寫成本函式)、或韌體慣用的雙括號除錯巨集(例如 SB_Msg(("fmt", args)),展開後仍是 printf(...));需要精確控制輸出格式時本函式不夠用,見下方常見錯誤。

最小範例

/* 放在韌體的 xxx.c 內,一次示範跑完後印一行摘要 */ nx_port_printf("fw: x=%bx\n", (BYTE)0x42);

常見錯誤與症狀

  • 期待標準 printf 的補零/欄寬對齊/precision 截斷語意:不會套用——本函式只剖析(跳過)%[flags][width][.prec],讓後面的轉換字元被正確辨識,但不會依這些修飾做任何補零或截斷;%08d 與 %d 的輸出逐位元組相同。%s 一律截斷在 31 個字元,與是否指定 .prec 無關。
  • 期待回傳值等於輸出的字元數:不會——本函式恆回 0,不承諾標準 printf 的「輸出字元數」語意,只承諾訊息會被送進 LogWrite()。
  • 用了不支援的轉換字元(不在 {d,i,u,x,X,c,s,%} 之列):該次轉換輸出空字串,不報錯、不中止剩餘字元的處理——但本函式不會替這個轉換字元消耗一個可變參數(不呼叫 va_arg())。若呼叫端在 fmt 裡確實為它傳了一個參數,該參數會被跳過不讀,後面每一個轉換字元讀到的可變參數就此整體錯位一個位置,讀出來的值全部是別的參數,且不會有任何錯誤提示——不要在同一次呼叫的 fmt 裡混用不支援的轉換字元與後續仍要正確讀值的轉換字元。
  • 尚未 nx_port_bind:訊息被靜默丟棄(內部緩衝仍會累積並在換行時嘗試呼叫 LogWrite(),但 host 為 NULL 時直接跳過),不會有任何提示告訴你訊息沒送出去。

nx_port_malloc

簽章

void *nx_port_malloc(unsigned long n);

用途:移植層的 malloc 替代實作,委派給 CRT malloc() 並登記進一張固定容量的追蹤表,供 nx_port_unbind 於掛載卸除時清算未釋放的配置。

執行緒

  • 正確執行緒:不做執行緒檢查,但不是執行緒安全的——本函式不觸碰硬體,只操作一份本檔案內部的共用追蹤表與 CRT malloc(),沒有 nx_i2c_reg_read 那種「尚未綁定即回 ERR_INVALID_CONTEXT」的限制:未 nx_port_bind 時一樣配置成功;但那份追蹤表沒有任何互斥保護,正確用法是只從韌體自己那唯一一條 Worker 執行緒呼叫。
  • 呼叫錯執行緒:不會回任何錯誤碼;但若有兩條執行緒同時呼叫本函式(或本函式與 nx_port_free),共用的追蹤表會在無保護下被同時讀寫,是未定義的競態,不是「呼叫錯執行緒直接失敗」的形狀。渲染路徑(ui_render_fn)另外禁止呼叫(渲染路徑禁止動態記憶體配置),這是特定 callback 的限制,不是「特定執行緒」的限制——ui_render_fn 跑在 render 執行緒,而 init() 並不受這條限制。
  • 如何確認:sdk/nexus_port.h 對 nx_port_malloc 的 @warning;ui_render_fn 的限制見 IcPlugin::ui_render_fn。

使用時機:既有韌體原始碼呼叫 malloc()(被巨集自動改寫)、或 mymalloc/pvPortMalloc 等韌體慣用的配置函式名稱(見 Port Shim 相容別名巨集)。

最小範例

/* 放在韌體的 xxx.c 內,配置一塊暫存緩衝 */ BYTE *p; p = (BYTE *)malloc(32); /* 未定義 NX_PORT_IMPL 時,malloc 已被巨集改寫成本函式 */ if (p != (BYTE *)0) { p[0] = 0; free(p); /* 見 nx_port_free */ }

常見錯誤與症狀

  • 追蹤表(固定 256 筆)滿載:本函式仍會配置成功(退回純 CRT malloc()),只是失去追蹤能力,並在 host 非 NULL 時記一次 NX_LOG_WARN——設計成「退化但不失敗」,拒絕配置對移植碼是更嚴重的行為改變。
  • 配置後從不 free():不會立即被發現——只有在 nx_port_unbind 執行時才會對追蹤表內仍非 NULL 的項目記一次洩漏 WARN 並真正 free();plugins/port_demo 的 fw_leak_once() 就是刻意示範這個形狀。
  • 在 ui_render_fn 內呼叫:違反渲染路徑禁止動態記憶體配置的規則,本函式本身不會擋下這個呼叫(它自己並不知道現在跑在哪個 callback 裡),純粹是設計規則要求呼叫端自律。
  • n 為 0:行為等同呼叫 CRT malloc(0),是否回傳 NULL 由 CRT 實作決定,本層不另外處理。

nx_port_free

簽章

void nx_port_free(void *p);

用途:移植層的 free 替代實作,對應 nx_port_malloc 配置的記憶體:從追蹤表除籍後轉呼 CRT free()。

執行緒

  • 正確執行緒:不做執行緒檢查,但不是執行緒安全的,理由同 nx_port_malloc——操作的是同一份共用追蹤表,正確用法同樣是只從韌體自己那唯一一條 Worker 執行緒呼叫。
  • 呼叫錯執行緒:不會回任何錯誤碼;但與 nx_port_malloc 同時被不同執行緒呼叫時,共用追蹤表的讀寫沒有任何互斥保護,是未定義的競態。
  • 如何確認:sdk/nexus_port.h 對 nx_port_free 的 @note。

使用時機:對應每一次 nx_port_malloc(或被巨集改寫的 free/myfree/vPortFree)。

最小範例

/* 放在韌體的 xxx.c 內,釋放稍早配置的緩衝 */ free(p); /* 未定義 NX_PORT_IMPL 時,free 已被巨集改寫成本函式;p 為 NULL 也安全 */

常見錯誤與症狀

  • p 為 NULL:合法的無操作,不報錯、不記 Log。
  • p 不在追蹤表內(例如追蹤表滿載時退化配置出來的指標):仍會被正常 free()——除籍只是簿記,不是釋放的必要條件。
  • 重複 free() 同一個指標:本函式的追蹤表只是簿記用途,不會攔下這個錯誤——第二次呼叫時該指標已不在表內(視為「不在追蹤表內」的情況),仍會原樣轉呼 CRT free(),其後果與直接對 CRT malloc/free 重複釋放相同(未定義行為)。

nx_port_gpio_write

簽章

int nx_port_gpio_write(BYTE pin, BYTE v);

用途:寫入本掛載已宣告持有的 GPIO 輸出腳位;懶惰宣告該腳位為 NX_PIN_FUNC_GPIO_OUT(若尚未宣告,或先前宣告為別的 func),再轉呼 NxHostApi::GpioWrite。

執行緒

  • 正確執行緒:與 NxHostApi::GpioWrite/NxHostApi::GpioConfigure 相同——本掛載所綁後端自己的 I2C Worker Thread 上的 Plugin callback。
  • 呼叫錯執行緒/尚未綁定:懶惰宣告那一步會先呼叫 NxHostApi::GpioConfigure,但其回傳值被本函式忽略——即使那次宣告因為執行緒錯誤而失敗,本函式仍會把該腳位在本地標記為「已宣告」,接著才呼叫 GpioWrite();最終回傳碼來自這次 GpioWrite() 呼叫,同樣是 ERR_WRONG_THREAD(-85)。尚未 nx_port_bind 時直接回 ERR_INVALID_CONTEXT(-84),連懶惰宣告都不會嘗試。
  • 如何確認:sdk/nexus_port.h 對 nx_port_gpio_write 的 @note「執行緒限制同 nx_i2c_reg_read()」(比照 NxHostApi::GpioWrite 的執行緒限制)。

使用時機:韌體原始碼常見的 reset/strobe/使能腳位操作;不需要自己先呼叫 NxHostApi::GpioConfigure——本函式的懶惰宣告會在第一次存取某腳位、或該腳位先前被宣告成別的功能時自動補上。

最小範例

/* 放在韌體的 xxx.c 內,拉低一支 reset 腳 */ int rc; rc = nx_port_gpio_write((BYTE)0, (BYTE)0); if (rc != 0) { return rc; }

常見錯誤與症狀

  • ERR_WRONG_THREAD(-85)/ERR_INVALID_CONTEXT(-84):同上「執行緒」段。
  • 懶惰宣告失敗被忽略、之後每次呼叫都卡在同一個錯誤:GpioConfigure() 失敗的原因(例如錯的執行緒、腳位已被其他掛載持有、硬體離線)不會被回報,也不會在下次呼叫時重試——本地快取仍記成「已宣告」,之後每次呼叫都直接跳去 GpioWrite(),得到的錯誤碼(多半是 ERR_INVALID_PARAM,因為底層其實從未真正持有這支腳)與真正的失敗原因(宣告失敗)對不上,容易誤判成別的問題。典型觸發順序:第一次在錯的執行緒上呼叫本函式(懶惰宣告與 GpioWrite() 都因為錯執行緒而失敗,腳位確實沒有動);之後即使改在正確的執行緒上再呼叫本函式,本地快取已經記成「已宣告為 GPIO_OUT」,懶惰宣告被跳過,底層其實從未真正持有這支腳——GpioWrite() 因此持續回 ERR_INVALID_PARAM,且沒有任何線索指向「第一次呼叫時執行緒錯了」這個根因。這個卡住狀態不是永久的:中間如果對同一支腳呼叫過一次 nx_port_gpio_read,它要求的 want_func(GPIO_IN)與快取不同,會讓懶惰宣告重新真正嘗試一次(進而視當下執行緒是否正確而可能真正成功宣告、改變快取),下一次 nx_port_gpio_write() 才會跟著再試一次。
  • 該腳已被其他掛載持有:ERR_INVALID_PARAM(-2),見 NxHostApi::GpioWrite 的常見錯誤。
  • 硬體離線:ERR_HW_OFFLINE(-40)。

nx_port_gpio_read

簽章

int nx_port_gpio_read(BYTE pin, BYTE *v);

用途:讀取本掛載已宣告持有的 GPIO 腳位電位;懶惰宣告該腳位為 NX_PIN_FUNC_GPIO_IN,再轉呼 NxHostApi::GpioRead。

執行緒

使用時機:讀回這支腳位目前的電位,且只適合本來就是輸入腳、或還沒被本層宣告過的腳——見下方常見錯誤:對一支已經被 nx_port_gpio_write 宣告成輸出的腳呼叫本函式,會把它改設成輸入腳再讀,不是「順便看一眼輸出腳目前電位」。NX_PIN_FUNC_INT 腳的中斷邊緣需另外由 interrupt_handler 得知,本函式讀到的只是即時電位。

最小範例

/* 放在韌體的 xxx.c 內,輪詢一支本來就是輸入的 ready 腳 */ BYTE ready; int rc; rc = nx_port_gpio_read((BYTE)1, &ready); if (rc == 0 && ready != 0) { /* [範本] 裝置已就緒 */ }

常見錯誤與症狀

  • 對已宣告為輸出的腳呼叫本函式,期待讀到「輸出腳目前電位」:不會單純讀值——懶惰宣告一律把目標腳重新宣告成 NX_PIN_FUNC_GPIO_IN(只要目前快取的功能不是 GPIO_IN 就會重新宣告,不分原本是 GPIO_OUT 還是別的),該支腳因此被靜默改設成輸入腳,原本的輸出功能就此消失,之後再用 nx_port_gpio_write 寫它會被懶惰宣告改回輸出(又一次重新宣告)。不要在同一支腳交替呼叫 nx_port_gpio_write/nx_port_gpio_read,除非確實要它在讀寫之間切換功能。
  • 對已經直接用 NxHostApi::GpioConfigure()(不經本層)宣告成 NX_PIN_FUNC_INT 的腳呼叫本函式:本層自己的懶惰宣告快取不知道這支腳被宣告過(它的快取只在經過本層的呼叫時更新),會判定「目前功能不是 GPIO_IN」而觸發重新宣告嘗試。後果視這支腳是否也在該後端的 GPIO 能力遮罩內而定:若這支腳同時支援一般 GPIO(NxGpioCaps::gpio_pin_mask 含這支腳),重新宣告會成功,把它從 NX_PIN_FUNC_INT 真的改設成 NX_PIN_FUNC_GPIO_IN——中斷偵測功能因此遺失,且沒有任何錯誤碼提示;若這支腳只支援中斷、不在 gpio_pin_mask 內,重新宣告會被拒絕(ERR_INVALID_PARAM,且此回傳值同樣被本層忽略),腳位在 Core 端仍保持 NX_PIN_FUNC_INT、中斷不受影響——但本層自己的快取仍會誤記成已經是 GPIO_IN(懶惰宣告不論成功或失敗都會無條件更新快取),之後對同一支腳的讀取仍會成功(讀值不檢查功能別),而且因為快取已經記成 GPIO_IN,之後每次呼叫都不會再重試這個重新宣告——不是「每次都再試一次」,是懶惰宣告從此被永久跳過,Core 端與本層快取自此就這樣不一致下去。不要在混用本層與直接呼叫 NxHostApi::GpioConfigure() 的同一支腳上呼叫本函式。
  • 懶惰宣告本身失敗(例如執行緒錯誤)被忽略:同 nx_port_gpio_write。
  • v 傳 NULL:不會被擋下也不會回錯誤碼——底層 NxHostApi::GpioRead 一律先讀進自己的區域變數,只有 v 非 NULL 時才寫回去;成功時的讀值會被靜默丟棄,呼叫端拿不到任何結果也看不到任何症狀。

nx_port_gpio_pulse

簽章

int nx_port_gpio_pulse(BYTE pin, BYTE level, WORD hold_ms);

用途:對輸出腳送出一次脈波:寫入 level、以 delay_ms 維持 hold_ms,再寫回 level 的相反電位(不是呼叫前的原始電位),對應韌體常見的 reset/strobe 腳位操作。

執行緒

  • 正確執行緒:同 nx_port_gpio_write(本函式內部呼叫的兩次 nx_port_gpio_write 與一次 delay_ms 皆有相同的執行緒要求)。
  • 呼叫錯執行緒:懶惰宣告與緊接著的 GpioWrite() 用的是同一個執行緒判定,錯執行緒時兩者皆在碰觸硬體前就失敗——第一次 nx_port_gpio_write() 因此提早返回錯誤碼,delay_ms() 與第二次寫入都不會被執行,腳位確實沒有被動到。但這只是「錯執行緒」這一種失敗原因才成立:見下方「常見錯誤與症狀」,其他原因造成的第一次寫入失敗不能同樣假設腳位沒有變化。
  • 如何確認:同 nx_port_gpio_write;中間的等待另外受 nx_port_delay_warn_ms 門檻管轄,見 delay_ms。

使用時機:韌體原始碼常見的 reset 脈波(拉低保持一段時間再放開),一次呼叫取代「寫、延遲、再寫」三行。

最小範例

/* 放在韌體的 xxx.c 內,送一次 10ms 低電位 reset 脈波 */ int rc; rc = nx_port_gpio_pulse((BYTE)0, (BYTE)0, (WORD)10); if (rc != 0) { return rc; }

常見錯誤與症狀

  • 第一次 nx_port_gpio_write() 失敗:本函式提早返回該錯誤碼,delay_ms()/第二次寫入都不會執行——但腳位不一定停在呼叫前的原始電位:若這是該腳第一次被本層存取(或先前宣告成別的 func),懶惰宣告會先呼叫 GpioConfigure(pin, GPIO_OUT, level);只要那一步成功,腳位在硬體上已經被驅動到 level,之後才失敗的是緊接著那次獨立的 GpioWrite() 呼叫——回傳的是它的錯誤碼,不代表硬體真的沒有變化。這支腳早已被宣告為 GPIO_OUT(懶惰宣告被跳過)時,失敗的 GpioWrite() 才真的沒有動到腳位;懶惰宣告與 GpioWrite() 因為執行緒錯誤而一起失敗時(兩者用的是同一個執行緒判定,皆在碰觸硬體前失敗,見上方「執行緒」段)腳位同樣沒有被動到——這兩種情況才是腳位真的維持原狀的例外,其餘造成懶惰宣告成功、但緊接著的 GpioWrite() 才失敗的原因(例如懶惰宣告成功後硬體才離線)都不能假設腳位沒有變化。
  • 第二次(拉回)失敗:本函式回傳第二次的錯誤碼,不是第一次的——即使第一次成功、脈波確實已經發生過,回傳值仍可能是非零,呼叫端不能只憑回傳值判斷「脈波有沒有送出去」。
  • 誤以為會拉回「呼叫前的原始電位」:不會——一律拉回 level 的相反電位(level 為高則拉回低,反之亦然),與呼叫前那支腳實際是什麼電位無關。
  • hold_ms 超過 nx_port_delay_warn_ms 門檻:中間的 delay_ms 一樣會記一次 NX_LOG_WARN。

NX_PORT_PIN

簽章

#define NX_PORT_PIN(n) (n)

用途:腳位編號的恆等轉換巨集,供移植碼以統一寫法引用腳位常數(例如把原韌體的 P1_0 定義成 NX_PORT_PIN(0x10) 之類),本層不做任何腳位重新映射,僅原樣傳遞數值——存在的理由是讓韌體原始碼裡引用腳位常數的寫法統一,而不是提供任何轉換能力。

執行緒

  • 正確執行緒:任一執行緒皆可使用——這是一個純編譯期的恆等值替換巨集,不涉及任何執行期呼叫,沒有執行緒的概念。
  • 呼叫錯執行緒:不適用。
  • 如何確認:sdk/nexus_port.h 對 NX_PORT_PIN 的 Doxygen;巨集展開後就是原樣的 n——注意巨集名稱本身在預處理後不會出現在輸出裡(那正是巨集展開的定義:呼叫點被替換成展開結果)。P1_0 這類引用它的常數同樣是巨集,一樣會在預處理後消失,故不能在 gcc -E 的輸出裡 grep NX_PORT_PIN 或 P1_0 這兩個名字本身——要看的是呼叫端(例如 nx_port_gpio_write(P1_0, ...) 那一行)展開後變成什麼:找到那一行呼叫在預處理輸出裡的位置,第一個參數應該原樣是 (0x10),不是別的數值。

使用時機:宣告腳位常數時,讓韌體原始碼看起來與原本 MCU SDK 的寫法一致;本層實際使用的腳位編號就是傳入的數值本身,直接寫裸數字也完全等價。

最小範例

/* 放在韌體的 xxx.c 或其標頭內,宣告一個腳位常數 */ #define P1_0 NX_PORT_PIN(0x10) int rc; rc = nx_port_gpio_write(P1_0, (BYTE)1); if (rc != 0) { return rc; }

常見錯誤與症狀

  • 期待它做腳位重新映射(例如把 MCU 的實體腳位編號轉成本層的 pin 編號):不會——本層的 pin 就是 NxHostApi::GpioConfigure 等函式直接使用的編號,映射(如果需要)必須由移植者自己在宣告腳位常數時就決定好數值。
  • 誤以為不用本巨集就無法使用 GPIO:不是——本巨集純粹是命名習慣,直接傳裸數字給 nx_port_gpio_write 等函式效果完全相同。

Port Shim 相容別名巨集

簽章

#define mymalloc(pool, n) nx_port_malloc(n) #define myfree(pool, p) nx_port_free(p) #define osDelay(ms) delay_ms((WORD)(ms)) #define vTaskSuspendAll() ((void)0) #define xTaskResumeAll() (1) #define _CLEAR_WATCH_DOG() ((void)0) #define _STOP_WATCH_DOG() ((void)0) #define _nop_() ((void)0)

用途:對齊 8051/Keil/FreeRTOS 韌體常見的符號名稱,讓韌體原始碼不必手動改名即可編譯。這批巨集只在未定義 NX_PORT_IMPL 時注入(見 6.0 include 規則 的 NX_PORT_IMPL 守衛)——nexus_port_plugin.h 自己需要呼叫真正的 CRT 函式,不能被自己的巨集改寫。

巨集展開成備註
mymalloc(pool, n)nx_port_malloc(n)pool 參數在本層被忽略
myfree(pool, p)nx_port_free(p)pool 參數在本層被忽略
osDelay(ms)delay_ms((WORD)(ms))FreeRTOS 風格的工作延遲別名
vTaskSuspendAll()((void)0)本層單執行緒模型下為無操作
xTaskResumeAll()(1)本層單執行緒模型下恆回真值
_CLEAR_WATCH_DOG()((void)0)本層無實體看門狗,恆無操作
_STOP_WATCH_DOG()((void)0)理由同 _CLEAR_WATCH_DOG()
_nop_()((void)0)8051 組合語言 NOP,本層為無操作

另有五個物件式(不帶參數列表的裸識別字)重導向,不各自成節:printf → nx_port_printf、malloc → nx_port_malloc、free → nx_port_free、pvPortMalloc → nx_port_malloc、vPortFree → nx_port_free。

nx_port_delay_warn_ms(型別 WORD,宣告在 nexus_port.h、定義在 nexus_port_plugin.h):delay_ms/delay_us 的告警門檻,預設 50(毫秒)。單次延遲超過此值時記一次 NX_LOG_WARN(delay_us 例外,見該節);這是一個獨立於 nx_port_bind/nx_port_unbind 綁定狀態機之外的一般全域變數(nx_port_bind() 不會重置它),呼叫端可以用一般的指定敘述在任何時機覆寫它的值——不需要等 nx_port_bind() 執行過,實務上通常寫在 init() 內。

執行緒

  • 正確執行緒:與各巨集展開後轉呼的目標函式相同(mymalloc/myfree/osDelay 見對應的 nx_port_malloc/nx_port_free/delay_ms);其餘五個無操作巨集與五個物件式重導向不涉及任何執行緒判斷——無操作巨集在任一執行緒展開都是同一句 ((void)0) 或常數,物件式重導向的限制同它們各自轉呼的函式。
  • 呼叫錯執行緒:不適用——本節本身不是單一函式,個別巨集的執行緒限制見它們各自展開後轉呼的目標。
  • 如何確認:查各巨集展開目標各自的「執行緒」段。

使用時機:韌體原始碼原封不動搬過來時,這批巨集讓它不需要事先改名;新寫的移植 glue 若沒有既有呼叫慣例的包袱,直接呼叫 nx_port_malloc/delay_ms 等本名更明確。

最小範例

/* 放在韌體的 xxx.c 內,示範 FreeRTOS 與 8051 風格符號原封不動編譯 */ BYTE *p; vTaskSuspendAll(); _nop_(); (void)xTaskResumeAll(); _CLEAR_WATCH_DOG(); p = (BYTE *)mymalloc(0, 16); if (p != (BYTE *)0) { myfree(0, p); } osDelay(5);

常見錯誤與症狀

  • 期待 vTaskSuspendAll()/xTaskResumeAll() 真的暫停/恢復排程:不會——本層是單執行緒模型,這兩個巨集恆為無操作/恆回真值,韌體原本依賴「暫停排程期間不會被切走」語意的臨界區保護在移植後不再成立,若韌體邏輯真的需要互斥,必須改用別的手段。
  • 期待 _CLEAR_WATCH_DOG()/_STOP_WATCH_DOG() 真的操作看門狗:不會——本層沒有實體看門狗,兩者恆無操作。
  • 在已定義 NX_PORT_IMPL 的編譯單元(即 port_glue.c 自己)內期待這批巨集存在:不存在——這批巨集只在未定義 NX_PORT_IMPL 時才會被 nexus_port.h 注入,port_glue.c 必須呼叫真正的 CRT/本名函式。
  • 期待每次掛載都自動重置回預設值:不會——nx_port_bind 不會重置 nx_port_delay_warn_ms,覆寫過的值會一直沿用到下次自己再覆寫,與綁定/解綁次數無關。
  • 誤以為它需要一個「檔案作用域的靜態初始化式」才能覆寫:不需要——它就是一個一般的全域變數,用一般的指定敘述(nx_port_delay_warn_ms = 100;)在任何函式內、任何時機都能覆寫。

6.2 Plugin 側 nexus_port_plugin.h

nx_port_bind

簽章

int nx_port_bind(NxPluginCtx ctx, const NxHostApi *host);

用途:於 Plugin 的 IcPlugin::init 呼叫,記錄本次掛載的 ctx/host,並重置本層全部內部狀態(nx_i2c_set_bus 記下的匯流排名稱歸零、nx_port_malloc 的追蹤表整個清空、GPIO 懶惰宣告快取整個重置、捕捉目前執行緒 id 供 delay_ms 判斷 UI Thread 誤用)——本層全部 nx_i2c_*/nx_port_* 函式在被呼叫到之前都需要先有這一次呼叫。

回傳值

  • ERR_OK(0):本 DLL 尚未綁定而完成綁定;或以同一個 ctx 再呼叫一次(照舊重置全部內部狀態,見「常見錯誤與症狀」)。
  • ERR_ALREADY_IN_USE(-22):本 DLL 已綁定另一個掛載的 ctx。本函式不改動任何狀態(第一個掛載的 ctx/host/追蹤表/GPIO 快取原封不動),並以這次傳入的 ctx 記一筆 NX_LOG_WARN(訊息以 nx_port_bind: 開頭;host 為 NULL 時不記)。

init() 必須原樣回傳這個值——Core 見到 init() 失敗會回捲這次掛載、不呼叫它的 IcPlugin::cleanup,所以第一個掛載的綁定不會被第二個掛載的 nx_port_unbind 清掉。

執行緒

  • 正確執行緒:UI Thread(與 IcPlugin::init 相同——本函式的呼叫點就在 init() 內)。
  • 呼叫錯執行緒:本函式自己不做任何執行緒判斷——它只是把參數存進檔案靜態變數、把幾張表清空,沒有 ERR_WRONG_THREAD 這種概念。Core 保證 IcPlugin::init 本身恆在 UI Thread 上被呼叫,故照 ABI 正常掛載時不會發生「本函式被呼叫錯執行緒」;但若移植者自己在 init() 之外(例如某個 I2C Worker Thread 上的 callback)又呼叫了一次本函式,它會把那條 Worker 的執行緒 id 當成 UI Thread id 存下——之後在同一條執行緒上呼叫 delay_ms 會被誤判成「從 UI Thread 呼叫」,記一次 NX_LOG_ERROR 並跳過睡眠,見下方「常見錯誤與症狀」。
  • 如何確認:查 IcPlugin::init 的「執行緒」段;plugins/port_demo 的 port_glue.c(pd_init())是本函式唯一的呼叫點示範,函式體只有一行 return (NxLong)nx_port_bind(ctx, host);。

使用時機:只能在 IcPlugin::init 內呼叫一次——這是本層唯一取得 ctx/host 的時機,init() 之外沒有其他管道能拿到這兩個值。呼叫之後才能使用本檔其餘的 nx_i2c_*/nx_port_* 函式(nx_i2c_set_bus 例外,它不檢查是否已綁定)。

最小範例

/* 放在 IcPlugin::init 具名賦值的函式裡(port_glue.c 的 pd_init() 形狀) */ static NxLong NX_CALL my_init(NxPluginCtx ctx, const NxHostApi *host) { return (NxLong)nx_port_bind(ctx, host); /* 失敗時讓 Core 回捲這次掛載 */ }

常見錯誤與症狀

  • 忘記呼叫:本檔其餘 nx_i2c_* 函式(以及 nx_port_gpio_write/_read)此後每次呼叫都回 ERR_INVALID_CONTEXT(-84),delay_ms 的 UI Thread 誤用偵測永遠判定為「不是 UI Thread」(見該節),nx_port_should_abort 永遠回 0——症狀集中在「所有硬體存取都在同一個錯誤碼上失敗」,容易第一時間誤判成硬體或掛載設定的問題,而不是漏了這一行。例外:nx_i2c_set_bus 不檢查是否已綁定,忘記呼叫本函式不影響它——它仍然會恆回 ERR_OK。
  • 在 init() 之外的任何時機呼叫(例如自訂指令 handler 或 probe() 裡「保險起見」再呼叫一次):一次呼叫兩種症狀一起發生——① GPIO 懶惰宣告快取與 malloc 追蹤表整個歸零,讓 Core 端仍持有的腳位與尚未釋放的配置從此對不上本層記錄的狀態,不是安全的「重新初始化」;② 若呼叫發生在某個 I2C Worker Thread 上的 callback 裡,本函式會把那條 Worker 執行緒的 id 當成 UI Thread id 記下——之後同一條執行緒上每一次 delay_ms 呼叫都會被誤判成「從 UI Thread 呼叫」,記一次含 delay_ms 字樣的 NX_LOG_ERROR 並直接跳過睡眠、不真的等待,且不會有任何訊息指出成因是「nx_port_bind() 被呼叫了兩次」。
  • 同一顆 Plugin DLL 被多個 Tab 同時掛載:本檔整份狀態(ctx/host/匯流排名稱/malloc 追蹤表/GPIO 快取/UI Thread id)都是檔案靜態,一份 DLL 只有一份;nx_i2c_* 沒有 ctx 參數,被移植韌體自己的檔案全域也只有一份——一顆移植 DLL 同時只能服務一個掛載,這是物理限制。第二個掛載的 init() 呼叫本函式時回 ERR_ALREADY_IN_USE、不覆寫第一個掛載的任何狀態,並在第二個掛載所在的 Tab 記一筆 NX_LOG_WARN;init() 照範例原樣回傳它,第二個掛載就以 init() 失敗收場(plugin switch 回報該錯誤碼、該位置維持未掛載),第一個掛載照常運作。要換到另一個 Tab,先卸載第一個掛載再掛。
  • init() 忽略本函式的回傳值、自己回 ERR_OK:第二個掛載會被 Core 視為掛載成功,但本層仍綁在第一個掛載上——第二個掛載的每一筆 nx_i2c_* 其實打到第一個掛載的位址/匯流排;更糟的是它卸載時的 nx_port_unbind 會把第一個掛載的綁定清掉,此後第一個掛載的存取全部回 ERR_INVALID_CONTEXT(-84)。唯一的線索是第二個掛載所在 Tab 的那筆 WARN。

nx_port_unbind

簽章

void nx_port_unbind(void);

用途:於 Plugin 的 IcPlugin::cleanup 呼叫,清算 nx_port_malloc 追蹤表內仍未釋放的配置(逐筆記一次 NX_LOG_WARN 後才真正 free()),並把 ctx/host 清回 NULL。

執行緒

  • 正確執行緒:I2C Worker Thread(與 IcPlugin::cleanup 相同——依 ABI,cleanup() 保證在該掛載所綁後端自己的 I2C Worker Thread 上被呼叫)。
  • 呼叫錯執行緒:不適用——本函式自己不做執行緒判斷,執行緒保證來自呼叫它的 IcPlugin::cleanup。
  • 如何確認:查 IcPlugin::cleanup 的「執行緒」段;plugins/port_demo 的 port_glue.c(pd_cleanup())是本函式唯一的呼叫點示範。

使用時機:只能在 IcPlugin::cleanup 內呼叫,且是該 callback 應該做的唯一一件事——IcPlugin::cleanup 一節已說明,呼叫期間掛載槽已經被清空,但 LogWrite() 是例外、照常寫進 Log,故本函式對追蹤表每一筆洩漏項目記的 NX_LOG_WARN(nx_port: leaked alloc ptr=... size=...)會出現在 Log 面板,歸屬卸載前的 Tab;記完之後照常 free()。

最小範例

/* 放在 IcPlugin::cleanup 具名賦值的函式裡(port_glue.c 的 pd_cleanup() 形狀) */ static void NX_CALL my_cleanup(NxPluginCtx ctx) { NX_UNUSED_ARG(ctx); nx_port_unbind(); }

常見錯誤與症狀

  • 忘記呼叫:追蹤表內尚未釋放的配置既不會被記一次 WARN,也不會被 free()——不是「洩漏但至少有紀錄」,是完全靜默的洩漏。更嚴重的是下一次同一顆 DLL 被重新掛載時,nx_port_bind 會把整張追蹤表直接歸零(不會走清算路徑),先前的指標從此在本層任何地方都查不到,連事後補救的機會都沒有。
  • 在 IcPlugin::cleanup 內呼叫本函式之外,還呼叫了 Log 以外、以 ctx 反查掛載的 HostAPI(例如想在下線前用 nx_i2c_* 再寫一次暫存器):失敗,見 IcPlugin::cleanup 的「常見錯誤與症狀」——cleanup() 內除了 Log,不要依賴任何以 ctx 解析掛載的呼叫。
  • 呼叫兩次(例如 cleanup() 重入,或誤放進某個會被呼叫多次的清理路徑):第二次呼叫時追蹤表已經是空的(第一次已經清空並把 host 設回 NULL),不會重複 free() 已釋放的指標,但 host 已是 NULL 意味著任何期待 WARN 被記錄的假設從第一次呼叫起就不成立。
  • 誤以為呼叫後 nx_i2c_* 系列會回到「尚未綁定」以外的其他狀態:不會——nx_port_unbind() 之後與從未呼叫過 nx_port_bind 是同一種狀態,之後任何 nx_i2c_* 呼叫一律回 ERR_INVALID_CONTEXT(-84),直到下一次 init()/nx_port_bind()。