feat(N34 阶段2): ch395f 五原语只加不接线 + Phase 15 套件 20/20

实施方案 docs/网络/N34-自愈审计/实施方案.md §4 五原语,形态按
E2/E3/E4 定案(实验方案.md):
1. 类型化状态回读 + 每 Socket 缓存(open=0 时 tcp_state 哨兵
   0xFF;sn>7 边界返 {00,FF} 计 fault);
2. 事件护栏 ch395f_sock_events:只读本 Socket SINT,全局寄存器
   唯一读者=net_poll(F-NET-7 契约);
3. ch395f_sock_reset_begin/poll 非阻塞状态机:读空 RX -> CLOSE
   -> 排空事件(quiescent 2 轮) -> 轮询 byte0=00H;GRACEFUL/
   QUARANTINE 最短等待 100/400ms(Trap 23);WAIT_CLOSE 内置
   LISTEN 态二段 CLOSE 补发(TC1502b 实证 ~180ms 落定);
4. ch395f_cmd_done 状态回读判据(E3:GET_CMD_STATUS 对
   OPEN/LISTEN/CLOSE 无判别力);
5. 链路态缓存 ch395f_glob_event_feed + link_up/变化计数
   (B9 判据 PHY!=DISCONN)。

Phase 15 板测(硬件层模式,Socket 7,无需 PC):20/20 PASS,
含二段关闭语义断言(一段 05/00、二段 byte0=00)。

RAM +56B < 64B(g_sock_state 16B + g_rst 32B + 链路/fault 8B);
产品 Code 与阶段 2 前逐字节同 209780(无调用者被链接器裁剪)。
@test PASS;relisten_stress 20/20、n30 3/3 零回归。
RAM 台账同步:RW_IRAM1 97952B 74.7% / RW_CCM 57376B 87.9%。
This commit is contained in:
2026-10-13 12:43:17 +08:00
parent 33405a57b5
commit f7f17931c9
8 changed files with 634 additions and 6 deletions
+367
View File
@@ -1408,3 +1408,370 @@ void ch395f_get_unreach_info(uint8_t *p_buf) {
}
ch395f_spi_end();
}
/* ============================================================================
* N34 阶段 2 驱动原语(实施方案 docs/网络/N34-自愈审计/实施方案.md §4;
* 形态定案 docs/网络/N34-自愈审计/实验方案.md E2/E3/E4,2026-10-13 台架实证)
* 只加不接线:net_poll 全量刷新 / 两级预清改调用 / 链路 feed 接线 = 阶段 3。
* ========================================================================== */
/* 每 Socket 状态缓存(刷新点:主动回读 ch395f_get_sock_state;
* 阶段 3 起 net_poll 每轮对在用 Socket 各刷一次——E2 实测 18.1us/次,
* 4 监听 ~72us 远小于 1ms 轮询周期) */
static ch395f_sock_state_t g_sock_state[8];
/* sock_reset 非阻塞状态机槽位(每 Socket 一个;t0 用 16 位 tick 差值,
* 各阶段超时 <=2s,远小于 65535ms 回绕窗) */
#define CH395F_RST_ST_IDLE 0U
#define CH395F_RST_ST_RX_DRAIN 1U
#define CH395F_RST_ST_CLOSE 2U
#define CH395F_RST_ST_MIN_WAIT 3U
#define CH395F_RST_ST_EVT_DRAIN 4U
#define CH395F_RST_ST_WAIT_CLOSE 5U
#define CH395F_RST_RX_MAX_ROUNDS 4U /* RX 缓冲 2KB,单读即空;4 为安全上限 */
#define CH395F_RST_QUIESCENT 2U /* 排空事件判据:连续 2 轮无新位(E8 M5 口径) */
#define CH395F_RST_RX_TMO_MS 500U
#define CH395F_RST_EVT_TMO_MS 500U
#define CH395F_RST_CLS_TMO_MS 2000U
typedef struct {
uint16_t t0; /* 当前阶段起始 tick */
uint8_t state; /* CH395F_RST_ST_* */
uint8_t flags; /* bit7=mode(1=QUARANTINE) bit6..4=quiescent 轮数 bit3..0=drain 轮数(饱和) */
} ch395f_rst_slot_t;
static ch395f_rst_slot_t g_rst[8];
/* 链路态缓存(F-DRV-13) */
static uint8_t g_link_up;
static uint8_t g_link_valid;
static uint16_t g_link_changes;
/* 边界 fault 计数(sn 越界等;日志只留痕一次,其余只计数) */
static uint16_t g_p2_faults;
static uint8_t g_p2_fault_logged;
/*
* 函数功能:阶段 2 原语边界异常留痕(首次 DBG_ERROR,之后只计数)
* 入口参数:tag - 出错点短标签 const char*
*/
static void ch395f_p2_fault_note(const char *tag) {
g_p2_faults++;
if (g_p2_fault_logged == 0U) {
g_p2_fault_logged = 1U;
DBG_ERROR("ch395f P2 boundary fault @%s (further ones counted only)", tag);
}
}
/*
* 函数功能:读空并丢弃 RX(排空契约形态=整缓冲单读,与 ch395f_read_recv_buf
* 同形但不拷出——复位残渣是垃圾,零新增 RAM)
* 入口参数:sn - Socket 索引 uint8_t
* len - 本次读出字节数(<=CH395F_SPI_DMA_MAX_PAYLOAD) uint16_t
*/
static void ch395f_rst_rx_discard(uint8_t sn, uint16_t len) {
s_spi2_dma_tx_buf[0] = CH395F_CMD_READ_RECV_BUF_SN;
s_spi2_dma_tx_buf[1] = sn;
s_spi2_dma_tx_buf[2] = len & 0xFFU;
s_spi2_dma_tx_buf[3] = (len >> 8) & 0xFFU;
memset(&s_spi2_dma_tx_buf[4], 0xFFU, len);
ch395f_spi_begin();
(void)ch395f_spi_dma_transfer(4U + len);
ch395f_spi_end();
}
/*
* 函数功能:主动回读本 Socket 状态并刷新驱动缓存(缓存刷新点之一,另一个
* 是阶段 3 的 net_poll 每轮全量刷新)
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
* 返回值:类型化状态;sn>7 返回 {CLOSED, 哨兵} 并计一次边界 fault
* 限定条件:芯片已初始化;netTask 上下文(与其它 CH395F 事务串行)
*/
ch395f_sock_state_t ch395f_get_sock_state(uint8_t sn) {
ch395f_sock_state_t st;
uint8_t raw[2];
if (sn >= 8U) {
ch395f_p2_fault_note("get_state sn");
st.open = CH395F_SOCKET_CLOSED;
st.tcp_state = CH395F_SOCK_STATE_NONE;
return st;
}
ch395f_get_socket_status(sn, raw);
st.open = raw[0];
st.tcp_state = (raw[0] == CH395F_SOCKET_OPEN) ? raw[1] : CH395F_SOCK_STATE_NONE;
g_sock_state[sn] = st;
return st;
}
/*
* 函数功能:读驱动缓存(零 SPI 成本;新鲜度取决于刷新点,见上)
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
* 返回值:最近一次刷新的状态;sn>7 返回 {CLOSED, 哨兵}
* 限定条件:芯片已初始化
*/
ch395f_sock_state_t ch395f_sock_state_cached(uint8_t sn) {
if (sn >= 8U) {
ch395f_sock_state_t st;
ch395f_p2_fault_note("cached sn");
st.open = CH395F_SOCKET_CLOSED;
st.tcp_state = CH395F_SOCK_STATE_NONE;
return st;
}
return g_sock_state[sn];
}
/*
* 函数功能:判"本 Socket 正在监听"(读缓存:open=OPEN 且 TCP=LISTEN)
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
* 返回值:1=在听 0=不在听 int
* 限定条件:缓存新鲜度同 ch395f_sock_state_cached
*/
int ch395f_sock_is_listening(uint8_t sn) {
ch395f_sock_state_t c = ch395f_sock_state_cached(sn);
return (c.open == CH395F_SOCKET_OPEN && c.tcp_state == CH395F_TCP_LISTEN) ? 1 : 0;
}
/*
* 函数功能:事件护栏——只读本 Socket SINT(读即清),永不触碰全局寄存器
* (F-NET-7 收窄:两级预清会清掉其它 Socket 的事件位)
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
* 返回值:SINT 位图;sn>7 返回 0 uint8_t
* 限定条件:芯片已初始化;全局寄存器唯一读者=net_poll(驱动侧契约)
*/
uint8_t ch395f_sock_events(uint8_t sn) {
if (sn >= 8U) {
ch395f_p2_fault_note("events sn");
return 0U;
}
/* 契约:只读本 Socket SINT(0x30 读即清),永不触碰全局寄存器 0x19——
* 全局寄存器读即清且含全部 Socket 事件位,唯一读者=net_poll(F-NET-7) */
return ch395f_get_sock_int_status(sn);
}
/*
* 函数功能:启动 Socket 复合复位(非阻塞状态机,每 net_poll 步进;E4 定案序列
* 读空 RX -> CLOSE -> 排空事件(quiescent) -> 轮询 byte0=00H)
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
* mode - 复位模式(GRACEFUL 100ms / QUARANTINE 400ms 最短等待)
* 返回值:0=已启动 -1=参数错或已在运行(调用方逻辑错误) int
* 限定条件:netTask 上下文;调用前本层已放弃该会话(复位丢弃 RX 残渣)
*/
int ch395f_sock_reset_begin(uint8_t sn, ch395f_reset_mode_t mode) {
if (sn >= 8U) {
ch395f_p2_fault_note("reset_begin sn");
return -1;
}
if (g_rst[sn].state != CH395F_RST_ST_IDLE) {
/* 已在运行:调用方逻辑错误(同一 Socket 不允许并发复位) */
return -1;
}
g_rst[sn].state = CH395F_RST_ST_RX_DRAIN;
g_rst[sn].flags = (mode == CH395F_RESET_QUARANTINE) ? 0x80U : 0x00U;
g_rst[sn].t0 = (uint16_t)HAL_GetTick();
return 0;
}
/*
* 函数功能:复位状态机步进(net_poll 或测试循环周期调用,禁复制忙等)
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
* 返回值:0=完成(含未启动,幂等) 1=进行中 -1=失败(超时/参数,槽位已复位可重开) int
* 限定条件:netTask 上下文
*/
int ch395f_sock_reset_poll(uint8_t sn) {
ch395f_rst_slot_t *p;
uint16_t now;
uint16_t el;
if (sn >= 8U) {
ch395f_p2_fault_note("reset_poll sn");
return -1;
}
p = &g_rst[sn];
if (p->state == CH395F_RST_ST_IDLE) {
return 0; /* 未启动/已完成:幂等 */
}
now = (uint16_t)HAL_GetTick();
el = (uint16_t)(now - p->t0);
switch (p->state) {
case CH395F_RST_ST_RX_DRAIN: {
/* E4 定案第 1 步:close 前 RX 必须为空(带脏缓冲关闭 -> 拆链停滞 ->
* 毒化下一会话且级联自持,C 变体 0/20 实证) */
uint16_t len = ch395f_get_recv_len(sn);
uint8_t rounds = (uint8_t)(p->flags & 0x0FU);
if (len == 0U) {
p->state = CH395F_RST_ST_CLOSE;
p->t0 = now;
break;
}
if (rounds < CH395F_RST_RX_MAX_ROUNDS) {
ch395f_rst_rx_discard(sn, (len > (uint16_t)CH395F_SPI_DMA_MAX_PAYLOAD)
? (uint16_t)CH395F_SPI_DMA_MAX_PAYLOAD : len);
p->flags = (uint8_t)((p->flags & 0xF0U) | (rounds + 1U));
} else {
/* 排不空(异常形态):不再恋战,继续 CLOSE,失败由 WAIT_CLOSE 超时兜底 */
ch395f_p2_fault_note("rx drain stall");
p->state = CH395F_RST_ST_CLOSE;
p->t0 = now;
}
if ((uint16_t)(now - p->t0) > CH395F_RST_RX_TMO_MS) {
p->state = CH395F_RST_ST_IDLE;
ch395f_p2_fault_note("rx drain tmo");
return -1;
}
break;
}
case CH395F_RST_ST_CLOSE:
/* E4 定案第 2 步:CLOSE(裸命令,不复制 poll_cmd_status 忙等——
* E3 实证其对 CLOSE 无判别力;完成判据=状态回读,E3 定案) */
ch395f_spi_begin();
ch395f_write_cmd(CH395F_CMD_CLOSE_SOCKET_SN);
ch395f_write_data(sn);
ch395f_spi_end();
p->state = CH395F_RST_ST_MIN_WAIT;
p->t0 = now;
break;
case CH395F_RST_ST_MIN_WAIT: {
uint16_t need = ((p->flags & 0x80U) != 0U) ? 400U : 100U;
if (el >= need) {
p->state = CH395F_RST_ST_EVT_DRAIN;
p->t0 = now;
p->flags = (uint8_t)(p->flags & 0x8FU); /* quiescent 计数清零 */
}
break;
}
case CH395F_RST_ST_EVT_DRAIN: {
/* E4 定案第 3 步:排空事件至 quiescent(陈旧粘滞位会误导事件驱动
* accept——E4 C 变体签名 0x04 陈旧 RECV;读即清,连续 2 轮无新位即静) */
uint8_t bits = ch395f_sock_events(sn);
if (bits == 0U) {
uint8_t q = (uint8_t)((p->flags >> 4) & 0x07U);
if ((q + 1U) >= CH395F_RST_QUIESCENT) {
p->state = CH395F_RST_ST_WAIT_CLOSE;
p->t0 = now;
} else {
p->flags = (uint8_t)((p->flags & 0x8FU) | ((q + 1U) << 4));
}
} else {
p->flags = (uint8_t)(p->flags & 0x8FU); /* 有新位:quiescent 计数清零 */
}
if (el > CH395F_RST_EVT_TMO_MS) {
p->state = CH395F_RST_ST_IDLE;
ch395f_p2_fault_note("evt drain tmo");
return -1;
}
break;
}
case CH395F_RST_ST_WAIT_CLOSE:
/* E4 定案第 4 步:轮询 byte0=00H(E4 实测 closed_ms=0——命令落定即空,
* 但为契约必需步) */
if (ch395f_cmd_done(sn, CH395F_DONE_CLOSE) == 1) {
p->state = CH395F_RST_ST_IDLE;
return 0;
}
/* 二段关闭实证(TC1501 台架 2026-10-13):LISTEN 态 Socket 的 CLOSE 返
* SUCCESS 但只拆监听(TCP 01->00),byte0 恒 05;需再发一次 CLOSE 才落
* 00(生产 net 层靠 Trap 09 open 重试吸收此行为)。复位状态机自行补发,
* 补发标记=drain 字段置 0xF(本阶段已用尽,复用安全)。 */
{
ch395f_sock_state_t c = ch395f_sock_state_cached(sn);
if (c.open == CH395F_SOCKET_OPEN && c.tcp_state == CH395F_TCP_CLOSED
&& (p->flags & 0x0FU) != 0x0FU) {
p->flags = (uint8_t)((p->flags & 0xF0U) | 0x0FU);
ch395f_spi_begin();
ch395f_write_cmd(CH395F_CMD_CLOSE_SOCKET_SN);
ch395f_write_data(sn);
ch395f_spi_end();
}
}
if (el > CH395F_RST_CLS_TMO_MS) {
p->state = CH395F_RST_ST_IDLE;
ch395f_p2_fault_note("wait closed tmo");
return -1;
}
break;
default:
p->state = CH395F_RST_ST_IDLE;
ch395f_p2_fault_note("bad rst state");
return -1;
}
return 1;
}
/*
* 函数功能:非阻塞命令完成查询(E3 定案=状态回读判据;单次回读 18.1us,E2 背书)
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
* what - 完成判据(OPEN=byte0 05H / LISTEN=+TCP 01H / CLOSE=byte0 00H)
* 返回值:1=已完成 0=未完成 -1=参数错 int
* 限定条件:netTask 上下文;勿复制 poll_cmd_status 忙等(E3 实证无判别力)
*/
int ch395f_cmd_done(uint8_t sn, ch395f_done_what_t what) {
ch395f_sock_state_t st;
if (sn >= 8U) {
return -1;
}
st = ch395f_get_sock_state(sn); /* 主动回读,单次 18.1us(E2) */
switch (what) {
case CH395F_DONE_OPEN:
return (st.open == CH395F_SOCKET_OPEN) ? 1 : 0;
case CH395F_DONE_LISTEN:
return (st.open == CH395F_SOCKET_OPEN && st.tcp_state == CH395F_TCP_LISTEN) ? 1 : 0;
case CH395F_DONE_CLOSE:
return (st.open == CH395F_SOCKET_CLOSED) ? 1 : 0;
default:
return -1;
}
}
/*
* 函数功能:喂全局中断位(阶段 3 接线:net_poll 读全局后喂 bit0~3;
* bit2 PHY_CHANGE 置位时重读 PHY 更新 link_up 缓存 + 变化计数)
* 入口参数:glob_bits - 全局中断状态低字节语义位图 uint16_t
* 返回值:无
* 限定条件:netTask 上下文(会发 SPI 读 PHY)
*/
void ch395f_glob_event_feed(uint16_t glob_bits) {
uint8_t up;
if ((glob_bits & CH395F_GINT_STAT_PHY_CHANGE) == 0U) {
return;
}
/* B9 判据(Trap 09 修正):PHY != DISCONN 即 link up */
up = (ch395f_get_phy_status() != CH395F_PHY_DISCONN) ? 1U : 0U;
if (g_link_valid == 0U) {
g_link_valid = 1U;
g_link_up = up;
} else if (up != g_link_up) {
g_link_up = up;
g_link_changes++;
}
}
/*
* 函数功能:读链路态缓存(首次查询惰性读 PHY 建档,不依赖 feed 已发生)
* 入口参数:无
* 返回值:1=link up 0=down uint8_t(B9 判据:PHY != DISCONN 即 up)
* 限定条件:芯片已初始化
*/
uint8_t ch395f_link_up_cached(void) {
if (g_link_valid == 0U) {
/* 惰性建档(首次查询):不依赖 feed 已发生过 */
g_link_up = (ch395f_get_phy_status() != CH395F_PHY_DISCONN) ? 1U : 0U;
g_link_valid = 1U;
}
return g_link_up;
}
/*
* 函数功能:读链路变化计数(net 层订阅:计数变化 -> 日志/可选重配)
* 入口参数:无
* 返回值:自建档以来的 PHY 连接状态翻转次数 uint16_t
* 限定条件:无
*/
uint16_t ch395f_link_change_count(void) {
return g_link_changes;
}
+107
View File
@@ -747,6 +747,113 @@ void ch395f_set_ttl(uint8_t sock, uint8_t ttl);
*/
void ch395f_get_unreach_info(uint8_t *p_buf);
/*
* 函数声明区 - N34 阶段 2 驱动原语(实施方案 docs/网络/N34-自愈审计/实施方案.md §4;
* 形态定案 docs/网络/N34-自愈审计/实验方案.md E2/E3/E4)
* ⚠ 阶段 2 = 只加不接线:net_poll 全量刷新、三处两级预清改调用、链路 feed 接线
* 均在阶段 3 收敛时进行。E2 实测:单次状态回读 18.1us;LISTEN 可见延迟 100% <=10ms。
*/
/* 类型化状态回读 + 每 Socket 缓存(F-DRV-1/3) */
#define CH395F_SOCK_STATE_NONE 0xFFU /* open=CLOSED 时 tcp_state 哨兵("无意义") */
typedef struct {
uint8_t open; /* byte0:CH395F_SOCKET_CLOSED / CH395F_SOCKET_OPEN */
uint8_t tcp_state; /* byte1:CH395F_TCP_*;open=CLOSED 时为 CH395F_SOCK_STATE_NONE */
} ch395f_sock_state_t;
/*
* 函数功能:主动回读本 Socket 状态并刷新驱动缓存(缓存刷新点之一,另一个
* 是阶段 3 的 net_poll 每轮全量刷新)
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
* 返回值:类型化状态;sn>7 返回 {CLOSED, 哨兵} 并计一次边界 fault uint8_t×2
* 限定条件:芯片已初始化;netTask 上下文(与其它 CH395F 事务串行)
*/
ch395f_sock_state_t ch395f_get_sock_state(uint8_t sn);
/*
* 函数功能:读驱动缓存(零 SPI 成本;数据新鲜度取决于刷新点,见上)
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
* 返回值:最近一次刷新的状态;sn>7 返回 {CLOSED, 哨兵}
*/
ch395f_sock_state_t ch395f_sock_state_cached(uint8_t sn);
/*
* 函数功能:判"本 Socket 正在监听"(读缓存:open=OPEN 且 TCP=LISTEN)
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
* 返回值:1=在听 0=不在 int
*/
int ch395f_sock_is_listening(uint8_t sn);
/*
* 函数功能:事件护栏——只读本 Socket SINT(0x30,读即清),永不触碰全局寄存器
* (F-NET-7 收窄:两级预清"先读全局再读本 socket"会清掉其它 Socket 的
* 事件位;全局寄存器唯一读者=net_poll 写成驱动侧契约)
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
* 返回值:SINT 位图;sn>7 返回 0 uint8_t
*/
uint8_t ch395f_sock_events(uint8_t sn);
/* 复合复位模式(F-DRV-4;两模式都必须排 RX——E4 定案,差异只剩最短等待) */
typedef enum {
CH395F_RESET_GRACEFUL = 0, /* 最短等待 100ms(Trap 23 二阶段实测值) */
CH395F_RESET_QUARANTINE /* 最短等待 400ms */
} ch395f_reset_mode_t;
/*
* 函数功能:启动 Socket 复合复位(非阻塞状态机,每 net_poll 步进;内部序列
* E4 定案:读空 RX -> CLOSE -> 排空事件(quiescent) -> 轮询 byte0=00H)
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
* mode - 复位模式 ch395f_reset_mode_t
* 返回值:0=已启动 -1=参数错或已在运行(调用方逻辑错误) int
* 限定条件:netTask 上下文;调用前本层已放弃该会话(复位会丢弃 RX 残渣)
*/
int ch395f_sock_reset_begin(uint8_t sn, ch395f_reset_mode_t mode);
/*
* 函数功能:复位状态机步进(在 net_poll 或测试循环里周期调用,禁忙等复制)
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
* 返回值:0=完成(含未启动,幂等) 1=进行中 -1=失败(超时/参数,槽位已复位可重开) int
*/
int ch395f_sock_reset_poll(uint8_t sn);
/* 命令完成判据(F-DRV-8;E3 定案=状态回读,GET_CMD_STATUS 对这三条无判别力) */
typedef enum {
CH395F_DONE_OPEN = 0, /* open 完成 = byte0 变 05H */
CH395F_DONE_LISTEN, /* listen 完成 = byte0=05H 且 TCP=01H(E2:<=10ms 可见) */
CH395F_DONE_CLOSE /* close 完成 = byte0=00H */
} ch395f_done_what_t;
/*
* 函数功能:非阻塞命令完成查询(单次主动回读 18.1us,E2 背书;勿复制
* poll_cmd_status 式忙等——E3 实证其对 OPEN/LISTEN/CLOSE 恒白等且无判别力)
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
* what - 完成判据 ch395f_done_what_t
* 返回值:1=已完成 0=未完成 -1=参数错 int
*/
int ch395f_cmd_done(uint8_t sn, ch395f_done_what_t what);
/* 链路态缓存(F-DRV-13;接线在阶段 3:net_poll 读全局后喂 bit0~3) */
/*
* 函数功能:喂全局中断位(net_poll 读 GET_GLOB_INT_STATUS_ALL 后调用;
* bit2 PHY_CHANGE 置位时驱动重读 PHY 更新 link_up 缓存 + 变化计数)
* 入口参数:glob_bits - 全局中断状态低字节语义位图 uint16_t
*/
void ch395f_glob_event_feed(uint16_t glob_bits);
/*
* 函数功能:读链路态缓存(首次查询惰性读 PHY 建档)
* 返回值:1=link up 0=down uint8_t(判据 B9:PHY != DISCONN 即 up)
*/
uint8_t ch395f_link_up_cached(void);
/*
* 函数功能:读链路变化计数(net 层订阅用:计数变化 -> 日志/可选重配)
* 返回值:自建档以来的 PHY 连接状态翻转次数 uint16_t
*/
uint16_t ch395f_link_change_count(void);
#ifdef __cplusplus
}
#endif