net/ui: UI 推流加就绪门控,刷新脱离 netTask 往返(销项 N-10)
新增 net_can_accept() / net_can_read():纯 RAM 读 Socket 控制块,不入队、 不等 netTask 回执,供实时性敏感任务在发 RPC 前判断"有没有活"。 ui_net_sink_poll() 改为先门控再 RPC;无客户端时 RPC 从 100 msg/s 降到 0。 两条兜底(门控引入的新卡死路径,设计阶段未预见): - I-2 每 32 轮无条件 recv:readable 仅由 RECV_OK 中断置位,Trap 22 三类 对账不含"RX 非空但 readable=0",纯门控会把已到达的数据永久藏住; - I-3 门控返 0 时查监听控制块,消失即 g_listen=-1 重建(改前靠每轮 accept 撞 BADF 暴露,门控会连这条暴露路径一起消掉)。 net_can_read() 对非 ESTABLISHED / 非法 fd 故意返 1,让上层必得 EOF/BADF 走既有错误分支——返 0 会把死连接永久藏住。 模拟器后端恒返 1(Winsock 无 RPC 成本,行为逐位不变)。 实测:uis 50ms→1µs、uiw 50ms→11.4ms(只剩 LVGL);@build 0 错 0 警、 @test 10/10 PASS(uis≤1µs、uisn≡0、0 task stall、0 [ERR])、 check_remo_sink.py e2e 0/25600 位差异、内容页文本断言 50/50。 uip 最坏 61ms 是 LVGL 帧周期地板、与网络无关,判据已按实测修订。 方案 B(把推流搬出 uiTask + 帧 seqlock)已评估并决定不做:搬任务不会 让刷新变快,收益只剩极端并发下的观感隔离,成本含必改 .ioc 的 CubeMX 重生成。残留暴露与观测手段(uis/uisn)如实记录在 §9.4。 N-19 / C7 不入登记簿(当日结案),结论沉淀进问题记录 §9。
This commit is contained in:
@@ -1271,6 +1271,54 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) {
|
||||
return msg.result;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:查询监听 Socket 是否有可接受的连接(纯 RAM 读,不发起 RPC)
|
||||
* 入口参数:sockfd - 监听 Socket 描述符 int
|
||||
* 出口参数:无
|
||||
* 返回值:1 - 有连接可接受;0 - 无(含描述符无效) int
|
||||
* 限定条件:任意任务上下文;net_init() 已调用
|
||||
* 函数说明:1. 判据与 net_accept_locked() 一致(单连接模型:监听 Socket 自身在
|
||||
* CONNECT 后转 ESTABLISHED),故"门控放行"与"accept 会成功"严格等价
|
||||
* 2. 只读控制块,不入队、不等通知 → 调用方(uiTask 等)不被 netTask 拖住
|
||||
* 3. 丢 CONNECT 中断由 net_reconcile_tick() 对账自愈,本函数不做自愈
|
||||
*/
|
||||
int net_can_accept(int sockfd) {
|
||||
/* Socket 控制块(纯 RAM 读,可能为 NULL = 描述符越界或未分配) */
|
||||
const net_sock_t *p_sock = net_get_sock(sockfd);
|
||||
|
||||
if (p_sock == NULL) {
|
||||
return 0;
|
||||
}
|
||||
return (p_sock->state == NET_SOCK_STATE_ESTABLISHED) ? 1 : 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:查询 Socket 是否"值得发一次 recv"(纯 RAM 读,不发起 RPC)
|
||||
* 入口参数:sockfd - Socket 描述符 int
|
||||
* 出口参数:无
|
||||
* 返回值:1 - 应发 recv;0 - 当前无数据可读,可跳过本轮 int
|
||||
* 限定条件:任意任务上下文;net_init() 已调用
|
||||
* 函数说明:1. ESTABLISHED → 看 readable(net_poll 在 RECV_OK 置位、读空清零)
|
||||
* 2. 非 ESTABLISHED / 描述符失效一律返回 1:必须让上层真的 recv 一次
|
||||
* 拿到 EOF/错误并复位自身状态,门掉它会把"连接已不存在"永久藏住
|
||||
* 3. ⚠ readable 仅由 RECV_OK 中断置位,Trap 22 对账不含"RX 非空但
|
||||
* readable=0" → 中断丢失时本函数恒返 0,调用方须保留低频兜底 recv
|
||||
*/
|
||||
int net_can_read(int sockfd) {
|
||||
/* Socket 控制块(纯 RAM 读,可能为 NULL = 描述符越界或未分配) */
|
||||
const net_sock_t *p_sock = net_get_sock(sockfd);
|
||||
|
||||
/* 描述符已失效:让 recv 去报 BADF,走调用方既有错误分支重建,勿静默卡死 */
|
||||
if (p_sock == NULL) {
|
||||
return 1;
|
||||
}
|
||||
/* 连接已非 ESTABLISHED(CLOSED/LISTENING/…):需要一次 recv 了结 EOF/错误 */
|
||||
if (p_sock->state != NET_SOCK_STATE_ESTABLISHED) {
|
||||
return 1;
|
||||
}
|
||||
return (p_sock->readable != 0U) ? 1 : 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:TCP Client 发起与服务器的连接(线程安全版本,通过消息队列委托给 netTask)
|
||||
* 入口参数:sockfd - Socket 描述符 (0~7),必须为已创建的 TCP Socket int
|
||||
|
||||
@@ -150,6 +150,47 @@ int net_listen(int sockfd);
|
||||
*/
|
||||
int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen);
|
||||
|
||||
/*
|
||||
* 函数功能:查询监听 Socket 当前是否有可接受的连接(**纯 RAM 读,不发起 RPC**)
|
||||
* 入口参数:sockfd - 监听 Socket 描述符 int
|
||||
* 出口参数:无
|
||||
* 返回值:1 - 有连接可接受;0 - 无(含描述符无效) int
|
||||
* 限定条件:net_init() 已调用;任意任务上下文(含 uiTask 等实时性敏感任务)
|
||||
* 函数说明:1. 只读 Socket 控制块状态,**不经消息队列、不等 netTask 回执** →
|
||||
* 调用方不会被 netTask 的忙碌程度拖住(对比 net_accept:
|
||||
* osMessageQueuePut(osWaitForever) + xTaskNotifyWait(portMAX_DELAY),
|
||||
* 两者均**无上界**)
|
||||
* 2. 判据与 net_accept_locked() 完全一致:单连接模型下监听 Socket 自身
|
||||
* 在 CONNECT 事件后转 ESTABLISHED,即"有连接可接受"
|
||||
* 3. 用途 = **门控**:无连接时直接跳过 net_accept(),把"每轮一次无谓
|
||||
* RPC"降为 0(UI 推流 10 ms 轮询 = 100 msg/s 队列压力的根因)
|
||||
* 4. 跨任务读单字:netTask 是唯一写者,最坏两种结果都无害——
|
||||
* 晚一轮发现(netTask 每 1 ms 就 net_poll 一次),或扑空
|
||||
* (net_accept 返回 -1 + NET_ERR_WOULDBLOCK)
|
||||
* 5. 丢 CONNECT 中断由 net_reconcile_tick()(200 ms 对账 + 去抖)自愈,
|
||||
* 本函数**不承担**自愈责任
|
||||
*/
|
||||
int net_can_accept(int sockfd);
|
||||
|
||||
/*
|
||||
* 函数功能:查询 Socket 是否"值得发一次 recv"(**纯 RAM 读,不发起 RPC**)
|
||||
* 入口参数:sockfd - Socket 描述符 int
|
||||
* 出口参数:无
|
||||
* 返回值:1 - 应发 recv(有数据可读,或连接已非 ESTABLISHED 需了结);0 - 不必发 int
|
||||
* 限定条件:net_init() 已调用;任意任务上下文
|
||||
* 函数说明:1. ESTABLISHED → 看 readable(net_poll 在 RECV_OK 置位、读空清零)
|
||||
* 2. **非 ESTABLISHED 一律返回 1**:连接已消失时必须让上层真的 recv 一次
|
||||
* 拿到 EOF/错误并自行复位;若把它门掉,"连接已不存在"会被永久藏住
|
||||
* (推流侧 g_client 永不复位 → 再也不接受新客户端)
|
||||
* 3. 描述符失效(net_get_sock 返回 NULL)同样返回 1 → 让 recv 报 BADF,
|
||||
* 走调用方既有的错误分支重建,而不是静默卡死
|
||||
* 4. ⚠ **不可作为唯一判据**:readable 只由 RECV_OK 中断置位,而
|
||||
* net_reconcile_tick 的三类对账**不含**"芯片 RX 非空但 readable=0"
|
||||
* → 中断丢失时本函数会恒返 0。调用方须保留**低频兜底 recv**
|
||||
* (ui_net_sink 每 32 轮无条件收一次,最坏延迟 ≈320 ms)
|
||||
*/
|
||||
int net_can_read(int sockfd);
|
||||
|
||||
/*
|
||||
* 函数功能:TCP Client 发起连接
|
||||
* 入口参数:sockfd - socket 描述符
|
||||
|
||||
@@ -15,6 +15,14 @@
|
||||
* 2026-10-02 王建锋 E-04:remo_send 校验 net_send 返回值(原丢弃)——短写/失败
|
||||
* 告警,连续 3 次失败阻断推送,新连接/网络重建解除阻断
|
||||
* (MCU 单连接模型不得 net_close 数据 fd,会连监听一起毁掉)
|
||||
* 2026-10-10 王建锋 I-1 门控 + I-2 兜底(Task #32 §9.3,销项 N-10):accept/recv
|
||||
* 前先用 net_can_accept()/net_can_read()(纯 RAM 读控制块)判
|
||||
* "有没有活",无活直接返回——台架常态(无客户端)从每 10 ms
|
||||
* 一次 RPC(100 msg/s,与 FTP 抢深度 8 的队列)降到 0 次。
|
||||
* 兜底:readable 仅由 RECV_OK 中断置位且 Trap 22 对账不含
|
||||
* "RX 非空但 readable=0",故每 32 轮无条件 recv 一次(≈320 ms);
|
||||
* 门控放行时若监听控制块已消失则置 g_listen=-1 触发重建,
|
||||
* 避免"门控把端口永久不再接受新客户端"这一新卡死路径
|
||||
*/
|
||||
#include "ui_net_sink.h"
|
||||
|
||||
@@ -57,6 +65,18 @@ static uint32_t g_gen; /* 建立监听时的网络代次号(变化
|
||||
static uint8_t s_send_fail_run; /* 连续发送失败计数(E-04:0 = 正常) */
|
||||
static uint8_t s_sink_blocked; /* 推送阻断标志(1 = 停发帧,等客户端重连解除) */
|
||||
|
||||
/*
|
||||
* 兜底收帧节拍(门控失效保险,2026-10-10 I-2):
|
||||
* net_can_read() 的 readable 只由 RECV_OK 中断置位,而 Trap 22 的
|
||||
* net_reconcile_tick() 三类对账**不含**"芯片 RX 非空但 readable=0" →
|
||||
* 该中断一旦丢失,纯门控会把已到达的数据永久藏住(改前靠每轮 recv 内的
|
||||
* ch395f_get_recv_len() 顺带发现)。故每 RM_LIVENESS_DIV 轮**无条件**收一次:
|
||||
* 10 ms 节拍 → 最坏额外延迟 ≈320 ms,而 RPC 频率仍从 100/s 降到 ≈3/s。
|
||||
* 取值必须是 2 的幂(用 & 取模,免除法)。
|
||||
*/
|
||||
#define RM_LIVENESS_DIV 32U
|
||||
static uint8_t s_liveness_ctr; /* 兜底计数器(单写者 = 推流宿主任务) */
|
||||
|
||||
/*
|
||||
* 函数功能:计算 RemoDispBus 帧 payload 的 CRC 校验值
|
||||
* 入口参数:d - 待校验数据指针(len 为 0 时不访问)
|
||||
@@ -449,10 +469,16 @@ int ui_net_sink_start(uint16_t port) {
|
||||
* 限定条件:ui_net_sink_start() 已成功调用
|
||||
* 函数说明: 1. 网络代次变化(重建)时关闭旧监听/客户端并重建
|
||||
* 2. 无监听(启动失败)时重试启动
|
||||
* 3. 无客户端时尝试 accept,成功则复位状态机
|
||||
* 4. 有客户端时非阻塞 recv 逐字节喂入状态机
|
||||
* 3. 无客户端时**先门控**(net_can_accept,纯 RAM 读)再 accept,成功则复位状态机
|
||||
* 4. 有客户端时**先门控**(net_can_read)再非阻塞 recv 逐字节喂入状态机;
|
||||
* 门控判为"无数据"时仍每 RM_LIVENESS_DIV 轮无条件收一次(兜底)
|
||||
* 5. 对端关闭/真错误时关闭客户端;若监听 Socket 随客户端失效
|
||||
* (MCU 单连接模型)则重建监听,等待重连
|
||||
* ⚠ **门控只消除"无谓的往返",不消除"往返本身无上界"**:一旦真有活要干
|
||||
* (accept / recv / 3204 B 的 LCDMEM 应答),仍是"入队 + 等 netTask 回执",
|
||||
* 最坏 ≈4 s。宿主 = uiTask 主循环;"搬出 uiTask"已完整评估并**决定不做**
|
||||
* (实测刷新最坏周期 = LVGL 帧周期地板,与网络无关),理由与残留暴露
|
||||
* 见 docs/问题记录/UI任务看门狗停摆分析.md §9.4
|
||||
*/
|
||||
void ui_net_sink_poll(void) {
|
||||
int n;
|
||||
@@ -470,6 +496,7 @@ void ui_net_sink_poll(void) {
|
||||
rm_parser_reset();
|
||||
s_send_fail_run = 0U; /* 网络重建:失败累计清零、解除推送阻断(E-04) */
|
||||
s_sink_blocked = 0U;
|
||||
s_liveness_ctr = 0U; /* 网络重建:兜底节拍重新起算 */
|
||||
(void)ui_net_sink_start(g_port);
|
||||
return;
|
||||
}
|
||||
@@ -480,19 +507,39 @@ void ui_net_sink_poll(void) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* 无客户端时尝试接受连接 */
|
||||
/* 无客户端时尝试接受连接(I-1 门控:无连接可接受 → 0 次 RPC) */
|
||||
if (g_client < 0) {
|
||||
if (net_can_accept(g_listen) == 0) {
|
||||
/* 监听控制块已消失(网络重建 / 异常释放)→ 置 -1 交回上方
|
||||
* g_listen < 0 分支重建监听。改前靠"每轮都 accept"撞出 BADF 来暴露,
|
||||
* 加了门控就必须显式兜这一条,否则端口永久不再接受新客户端 */
|
||||
if (net_get_sock(g_listen) == NULL) {
|
||||
g_listen = -1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
int c = net_accept(g_listen, NULL, NULL);
|
||||
if (c >= 0) {
|
||||
g_client = c;
|
||||
rm_parser_reset();
|
||||
s_send_fail_run = 0U; /* 新连接:失败累计清零、解除推送阻断(E-04) */
|
||||
s_sink_blocked = 0U;
|
||||
s_liveness_ctr = 0U; /* 新连接:兜底节拍重新起算 */
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* 有客户端时非阻塞收帧,逐字节喂入解析状态机 */
|
||||
/* 有客户端时非阻塞收帧(I-1 门控 + I-2 兜底):
|
||||
* 无数据可读 → 多数轮次直接返回,省掉一次 net_recv 往返(该往返内部
|
||||
* osMessageQueuePut(osWaitForever) + xTaskNotifyWait(portMAX_DELAY),无上界);
|
||||
* 但每 RM_LIVENESS_DIV 轮无条件收一次,防 RECV_OK 中断丢失把数据永久藏住 */
|
||||
if (net_can_read(g_client) == 0) {
|
||||
if (((++s_liveness_ctr) & (RM_LIVENESS_DIV - 1U)) != 0U) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
s_liveness_ctr = 0U;
|
||||
}
|
||||
n = net_recv(g_client, g_rx, (int)sizeof(g_rx), NET_MSG_DONTWAIT);
|
||||
if (n > 0) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
|
||||
@@ -40,13 +40,20 @@ extern "C" {
|
||||
int ui_net_sink_start(uint16_t port);
|
||||
|
||||
/*
|
||||
* 函数功能:非阻塞轮询 RemoDispBus 协议
|
||||
* 函数功能:轮询 RemoDispBus 协议(接受连接、收帧、逐字节解析)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:ui_net_sink_start() 已成功调用
|
||||
* 函数说明:1. 无客户端时尝试接受连接,成功即上报 INIT
|
||||
* 2. 有客户端时用 DONTWAIT 收帧并解析(KEEPLIVE/INIT/KEY/LCDMEM)
|
||||
* 3. 对端关闭或异常时复位客户端,等待下次重监听,不阻塞 LVGL 刷新
|
||||
* 函数说明:1. 无客户端时先门控(net_can_accept,纯 RAM 读)再尝试接受连接
|
||||
* 2. 有客户端时先门控(net_can_read)再用 DONTWAIT 收帧并解析
|
||||
* (KEEPLIVE/INIT/KEY/LCDMEM);门控判为无数据时仍每 32 轮兜底收一次
|
||||
* 3. 对端关闭或异常时复位客户端,等待下次重监听
|
||||
* ⚠ **不是"零成本非阻塞"**:门控只消除无谓往返;真有活时仍是
|
||||
* "入队 + 等 netTask 回执"(osMessageQueuePut(osWaitForever) +
|
||||
* xTaskNotifyWait(portMAX_DELAY),均无上界;LCDMEM 应答 3204 B 最坏 ≈4 s)
|
||||
* → 会拖住宿主(uiTask)的刷新;"搬出 uiTask"已完整评估并**决定不做**
|
||||
* (实测刷新最坏周期 = LVGL 帧周期地板,与网络无关),见
|
||||
* docs/问题记录/UI任务看门狗停摆分析.md §9.4
|
||||
*/
|
||||
void ui_net_sink_poll(void);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user