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直接 includenexus_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就 includenexus_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'的錯誤(GNUld的實測訊息形狀),Plugin DLL 因此整個連結失敗,不是警告。 - 韌體
.c用了 Keil 關鍵字(xdata/idata/pdata/data/code/bit/reentrant/_at_)卻沒有 includenexus_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 才有各自獨立的符號表,不受影響。 - 韌體
.cinclude 了nexus_plugin_api.h(沒有任何編譯期或連結期症狀——實測nexus_port.h與nexus_plugin_api.h同時 include 進同一個編譯單元零警告零錯誤,因為nexus_plugin_api.h本身不 includenexus_port.h、也不 typedefBYTE/WORD/DWORD,兩者不會撞型別;注意這與上面「韌體 includenexus_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至少有nbytes 的容量,否則是一次沒有任何錯誤碼會攔下的緩衝區溢位。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。
執行緒
- 正確執行緒:同 nx_i2c_reg_read。
- 呼叫錯執行緒/尚未綁定:同 nx_i2c_reg_read。
- 如何確認:同 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。
執行緒
- 正確執行緒:同 nx_i2c_reg_read。
- 呼叫錯執行緒/尚未綁定:同 nx_i2c_reg_read。
- 如何確認:同 nx_i2c_reg_read。
使用時機:韌體對象使用雙位元組暫存器位移時的替換點(本 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_reg_read。
- 呼叫錯執行緒/尚未綁定:同 nx_i2c_reg_read。
- 如何確認:同 nx_i2c_reg_read。
使用時機:同 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 完成。
執行緒
- 正確執行緒:同 nx_i2c_reg_read。
- 呼叫錯執行緒/尚未綁定:同 nx_i2c_reg_read。
- 如何確認:同 nx_i2c_reg_read。
使用時機:韌體原始碼常見的 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。
執行緒
- 正確執行緒:同 nx_i2c_reg_read。
- 呼叫錯執行緒/尚未綁定:同 nx_i2c_reg_read。
- 如何確認:同 nx_i2c_reg_read。
使用時機:韌體對象沒有「暫存器位移」這個概念、或需要送出的不是單一位移位元組時(例如只送位址、不帶任何資料的 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)。
執行緒
- 正確執行緒:同 nx_i2c_reg_read。
- 呼叫錯執行緒/尚未綁定:同 nx_i2c_reg_read。
- 如何確認:同 nx_i2c_reg_read。
使用時機:韌體對象需要先送出命令碼(可能不只一個位元組)後立即讀回結果、中間不能被其他交易插隊的情形(例如某些 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至少有rnbytes。- 把它當成兩次獨立交易:不是——中間不釋放匯流排是本函式存在的理由,若只是想分開做兩件事,直接呼叫兩次 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 定義。
執行緒
- 正確執行緒:同 nx_i2c_reg_read——本函式只是原樣轉呼它。
- 呼叫錯執行緒/尚未綁定:同 nx_i2c_reg_read。
- 如何確認:同 nx_i2c_reg_read。
使用時機:韌體原始碼本來就呼叫 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沒有管道#includenexus_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真正沒有管道的是#includenexus_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 相同。
執行緒
- 正確執行緒:同 nx_i2c_reg_write。
- 呼叫錯執行緒/尚未綁定:同 nx_i2c_reg_write。
- 如何確認:同 nx_i2c_reg_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:行為等同呼叫 CRTmalloc(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()同一個指標:本函式的追蹤表只是簿記用途,不會攔下這個錯誤——第二次呼叫時該指標已不在表內(視為「不在追蹤表內」的情況),仍會原樣轉呼 CRTfree(),其後果與直接對 CRTmalloc/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_port_gpio_write(懶惰宣告失敗同樣被忽略)。
- 如何確認:同 nx_port_gpio_write。
使用時機:讀回這支腳位目前的電位,且只適合本來就是輸入腳、或還沒被本層宣告過的腳——見下方常見錯誤:對一支已經被 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的輸出裡 grepNX_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()。