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);
|
||||
|
||||
|
||||
@@ -239,6 +239,20 @@ if (n > 0) {
|
||||
2. **最小帧长判据统一**:`remo_handle` 以 `n < 5` 拦截(最短合法帧 5 字节),两端一致——旧 MCU `n < 6` 会丢 5 字节 KEEPLIVE/INIT 帧的隐患已随统一消除。
|
||||
3. **断线重连统一**:`ui_net_sink_poll` 在客户端断开后用 `net_get_sock(g_listen) == NULL` 判监听是否失效(MCU 单连接模型下监听随客户端一起失效),失效即 `bind_listen()` 重建——两端一致,并修复了 MCU 单连接模型断线后无法重连的隐患。
|
||||
4. **协议兼容**:线格式与命令语义不变,任一端都能与 DTU-RemoteLCD 参考客户端互通;双端行为一致已由 e2e 回归(`simulator/tests/check_remo_sink.py`,0/25600 位差异)与双端构建(Keil + CMake 均 0 错误 0 警告)验证。
|
||||
5. **驱动者是宿主,不是协议自己**:`ui_net_sink_poll()` 由宿主周期调用——MCU 端目前 = **uiTask 主循环 10 ms**,模拟器 = `main.c` 主循环。协议本身不含节拍、不阻塞。
|
||||
|
||||
> ✅ **门控已落地(2026-10-10)**:`ui_net_sink_poll()` 现在**先查后动**——`net_can_accept()` /
|
||||
> `net_can_read()` 纯 RAM 读控制块判"有没有活",无活直接返回,不再每 10 ms 发一次
|
||||
> `net_accept`(原 100 msg/s 队列压力,与 FTP 抢深度 8 的队列)。门控判"无数据"时仍
|
||||
> **每 32 轮无条件 recv 一次**兜底(`readable` 只由 RECV_OK 中断置位,中断丢失时纯门控
|
||||
> 会把数据永久藏住)。模拟器后端恒返回"就绪",行为逐位不变。
|
||||
>
|
||||
> ⚠ **残留暴露(已知,刻意不处理)**:门控只消除"无谓的往返",消除不了"往返本身无上界"——
|
||||
> 客户端在连且 netTask 拥塞时,3204 B 的 LCDMEM 应答仍可能让宿主任务等最长 ≈4 s。
|
||||
> "把推流搬出 uiTask(独立任务 + 帧 seqlock)"已完整评估并**决定不做**:实测刷新最坏周期
|
||||
> (`uip` ≈61 ms)是 **LVGL 帧周期地板**、与网络无关,搬任务不会让刷新变快。
|
||||
> 否决理由、残留暴露的观测手段(`[PROF]` 的 `uis`/`uisn`)见
|
||||
> `docs/问题记录/UI任务看门狗停摆分析.md` §9.4。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -456,6 +456,7 @@ snapshot == required → miss_streak=0、证据门闩复位、喂狗
|
||||
| R4 | **NAND 真实卡死时 FTL 直接喂狗** | 设备存活但存储功能冻结(与 ORE 自愈同类边界)。若要求"NAND 死也必须复位",需给 `nand_ftl_wd_feed()` 加次数/时限上限 |
|
||||
| R5 | **栈溢出与断言在复位原因上不可区分**(都是 `SFTRSTF`) | 可接受;串口分别打 `[ERR] STACK OVERFLOW` / `[ERR] ASSERT FAILED`,`selftest.log` 侧另有记录 |
|
||||
| R6 | **`RCC_CSR` 读后即清** | 已在 `main.c` 统一处理;漏清会让下次软复位读回上次标志 |
|
||||
| R7 | **UI 推流的网络往返卡死不占心跳位**(产品掩码仍 `0x77`) | 刻意:往返卡死的根因在 netTask,netTask 已在掩码内、由它暴露;为"远端显示卡了"新增一位只会把观感问题升级成整机复位。UI 心跳已由 P1-4a 自产、不受推流影响;推流耗时看 `[PROF]` 的 `uis`/`uisn`。详见 `docs/问题记录/UI任务看门狗停摆分析.md` §9 |
|
||||
|
||||
### 9.2 未办项(登记簿同步)
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ int net_close_sock(net_sock_t *p_sock);
|
||||
- **`netTask` 不可向自身发消息**:`netTask` 中直接调 `net_send()` 会死锁(等待自己处理消息),必须在 `netTask` 中使用 `net_send_sock()` 等内部版本
|
||||
- **阻塞 recv 不再独占 netTask(L3 pending 化,2026-09-22)**:无数据时公共 `net_recv()` / `net_recv_to()` **登记挂起请求后返回**(每 fd 至多一个,禁止覆盖——覆盖即丢失前调用方通知 = 永久挂死);完成由 `net_pending_tick()` 按主循环节拍检查(①数据 ②到点 ③断连,看门狗 1s)并经任务通知唤醒调用方——等待期间调用方在自己上下文睡通知,netTask 零占用(消除 E-02 头阻塞)。副作用:`buf` 指针须保持有效至返回;close / 网络重建 / 陈旧重同步会**显式取消并通知**(`NET_ERR_CONNRESET` 等),调用方永不挂死
|
||||
- **队列阻塞语义**:只有 `osMessageQueuePut` 失败(8 长度固定队列,极端情形)时公共 API 阻塞;recv 挂起等待**不计入**队列占用(完成前队列中无消息滞留)
|
||||
- **哪些操作要求 netTask 已运行**:`net_listen()` / `net_accept()` / `net_connect()` / `net_send()` / `net_recv()` / `net_close()` / `net_relisten()` / `net_resync_listen()` 全经消息队列 → 只能在就绪屏障后的任务调用;`net_socket()` / `net_bind()` 为直接调用,但同样建议屏障后
|
||||
- **哪些操作要求 netTask 已运行**:`net_listen()` / `net_accept()` / `net_connect()` / `net_send()` / `net_recv()` / `net_close()` / `net_relisten()` / `net_resync_listen()` 全经消息队列 → 只能在就绪屏障后的任务调用;`net_socket()` / `net_bind()` 为直接调用,但同样建议屏障后;**`net_can_accept()` / `net_can_read()` 是纯 RAM 读控制块**(§10.1),不入队、不等回执,**任意任务可安全周期调用**(用途 = 门控,把"无谓的 RPC"降为 0)
|
||||
|
||||
### 3.6 芯片/本层状态对账与重监听自愈(Trap 09 / 22 / 23)
|
||||
|
||||
@@ -873,6 +873,23 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen);
|
||||
|
||||
> 单连接模式:监听 Socket 在 CONNECT 后**自身即数据通道**(§5.1)。`net_accept` 失败时若状态为 CLOSED 且 `auto_relisten` 开启,内部触发**非阻塞**重监听(失败交重试轮询,§3.6)。中断丢失也不会让监听死亡——对账在 200ms 内修正"本层 LISTENING 而芯片监听消失"等失同步。
|
||||
|
||||
#### net_can_accept / net_can_read - 就绪门控(**纯 RAM 读,不走消息队列**)
|
||||
|
||||
```c
|
||||
int net_can_accept(int sockfd); /* 1 = 有连接可接受;0 = 无 */
|
||||
int net_can_read(int sockfd); /* 1 = 应发一次 recv;0 = 可跳过本轮 */
|
||||
```
|
||||
|
||||
| 项 | 说明 |
|
||||
|------|------|
|
||||
| 代价 | **零 RPC**:只读 Socket 控制块(`net_get_sock()`),不入队、不等 netTask 回执 → 任意任务上下文可安全周期调用,**不被 netTask 忙碌程度拖住** |
|
||||
| `net_can_accept` 判据 | `state == ESTABLISHED`,与 `net_accept_locked()` 严格等价(单连接模型:监听 Socket 自身在 CONNECT 后转数据通道) |
|
||||
| `net_can_read` 判据 | `ESTABLISHED` → 看 `readable`;**非 `ESTABLISHED` 一律返 1**(必须让上层 recv 一次拿到 EOF/错误并复位自身状态,门掉它会把"连接已消失"永久藏住);描述符失效也返 1(让 recv 报 BADF 走既有错误分支) |
|
||||
| 用途 | **门控**:无活时跳过 `net_accept()`/`net_recv()`——两者内部都是 `osMessageQueuePut(osWaitForever)` + `xTaskNotifyWait(portMAX_DELAY)`,**均无上界**(队列深 8 且与 FTP/timeSync 共享) |
|
||||
| ⚠ 不可作为唯一判据 | `readable` 只由 RECV_OK 中断置位,而 §3.6 的三类对账**不含**"芯片 RX 非空但 `readable=0`" → 中断丢失时 `net_can_read()` 恒返 0。**调用方必须保留低频兜底 recv**(`ui_net_sink.c` 每 32 轮无条件收一次,见 `docs/问题记录/UI任务看门狗停摆分析.md` §9.3.1) |
|
||||
| 竞态 | netTask 是唯一写者,单字读;最坏"晚一轮发现"(netTask 每 1 ms 就 `net_poll()`)或"扑空"(RPC 返回 `NET_ERR_WOULDBLOCK`),均无副作用 |
|
||||
| 模拟器端 | `net_socket_sim.c` 同名实现**恒返回 1**——Winsock 本就非阻塞、无 RPC 成本,恒 1 使门控在模拟器上逐位等价于改前行为 |
|
||||
|
||||
#### net_connect - 连接服务器
|
||||
|
||||
```c
|
||||
@@ -1395,6 +1412,7 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po
|
||||
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| 2026-10-10 | 新增 **`net_can_accept()` / `net_can_read()` 就绪门控 API**(§10.1):纯 RAM 读控制块、**不走消息队列、不等 netTask 回执**,供实时性敏感任务在发 RPC 前判断"有没有活"(动因 = `ui_net_sink_poll()` 每 10 ms 一次 `net_accept` 把 uiTask 拖成最坏 ≈50 ms;原拟登记 N-19 / C7,当日结案不入登记簿;设计与实测见 `docs/问题记录/UI任务看门狗停摆分析.md` §9);§3.5 补"netTask 运行要求"例外条;⚠ 记录两条使用约束——**不可作为唯一判据**(`readable` 只由 RECV_OK 中断置位,须保留低频兜底 recv)与**模拟器后端恒返 1**(`net_socket_sim.c`,Winsock 无 RPC 成本) |
|
||||
| 2026-09-23 | 同步 L3 pending 化、D10 非阻塞重监听、Trap 22 对账、Trap 23 排渣隔离后的现状:新增 `net_recv_to` / `net_sock_state` / `net_resync_listen` / `net_relisten`(§10.1)及 `net_getsockname`;§3 新增 3.6(对账与自愈四机制)、3.3/3.5 改写(recv 挂起模型、队列语义、netTask 运行要求);§4/§5 示例更新(监听由就绪屏障后服务任务建立、`net_sock_state` 轮询);§10.2 改述 `net_process_messages()` 每轮节奏(重监听重试 → 挂起 recv 完成 → 200ms 对账 → 队列),补 `timeout_ms`/新消息类型说明;新增 §11.2 分片阻塞读(`net_recv_to`);§12.1/12.3/12.4/12.9 更新,新增 §12.10(自愈行为数据与排障判据,含 Trap 21 结案) |
|
||||
| 2026-09-21 | **修正公开 API 的错误码回传**:`net_msg_t` 按值拷入队列 → netTask 对消息的写入调用方不可见,改用 `xTaskNotify()` 通知值回传具体错误码(负值),公开 API 恢复 errno 后统一返回 -1(此前被覆盖成 `NET_ERR`,使 `net_get_errno() != NET_ERR_WOULDBLOCK` 的"空轮询"判断恒成立 → `ui_net_sink` 连上即 `net_close`);§12.4 补单连接模型断开处理(EOF 时不得 `net_close` 监听 socket);§12.9 补错误码回传约束 |
|
||||
| 2026-09-15 | 同步配置引擎属主变更(P1-3):`net_init` 调用方说明改为"netTask 只读 RAM 副本"(网口配置由 defaultTask 在就绪屏障前加载);模拟器端启动序列补 `net_cfg_save_run()`(主循环 = 模拟器侧配置引擎属主,执行 UI 保存请求) |
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
> **状态**:**分析完成,P0/P1/P2 处置已落地并上板验证**(2026-10-10)。P0(adc 压环条件 +
|
||||
> 截止期重同步)修复后累计 22 次启动 0/22 复现;P1-4a 已解耦 UI 心跳与 netTask 往返;P2 常开
|
||||
> 探针已上线。详见 §8「处置落地」。
|
||||
> **遗留 → §9**:P1-4a 只解耦了**心跳**,**刷新仍被 `ui_net_sink_poll()` 拖**(用户可观测的
|
||||
> "刷新不稳")。§9 已**结案**:A 门控 + 兜底**已落地并 10/10 上板验证**;"把推流搬出 uiTask"
|
||||
> 的 B 方案**已评估并决定不做**。⚠ 实测修正了当初的归因——刷新最坏周期(`uip` ≈61 ms)是
|
||||
> **LVGL 帧周期地板,与网络无关**。详见 §9.3.2 / §9.4。
|
||||
> **关联**:
|
||||
> [`../待处理内容.md`](../待处理内容.md)(**N-17** 心跳缺口登记)、
|
||||
> [`../子系统设计/看门狗设计说明.md`](../子系统设计/看门狗设计说明.md)(`required_mask` / 抽样窗 / 长耗时白名单)、
|
||||
@@ -304,8 +308,166 @@ next += ADC_TASK_BEAT_MS; /* 固定 +1ms,不随实际进度修正 */
|
||||
|
||||
---
|
||||
|
||||
## 9. 彻底解耦(2026-10-10 结案:**A 已落地,B 决定不做**)
|
||||
|
||||
> **结案状态**:**§9.3 方案 A(门控 + 兜底)已落地并验证**(`net_can_accept()`/`net_can_read()`
|
||||
> + `ui_net_sink_poll()` 先门控再 RPC;`@build` 0 错 0 警、`@test` 10/10 PASS、e2e 0/25600 位差异);
|
||||
> **§9.4 方案 B(把推流搬出 uiTask)已评估并决定不做**,理由与残留暴露写在 §9.4。
|
||||
> **动因**:P1-4a 之后"心跳"不再被推流拖,但**刷新仍被推流拖**——现象从"偶发复位"变成
|
||||
> "画面周期性卡顿 / 刷新间隔不稳"。**实测结论修正了这个动因**:刷新的最坏周期其实是
|
||||
> LVGL 帧周期地板,与网络无关(§9.3.2)。
|
||||
> **登记**:N-19 / C7 **不入登记簿**——当日登记当日结案,结论只沉淀在本节。
|
||||
|
||||
### 9.1 先把"poll 运行时间不可预测"量化:3 个**没有上界**的等待点
|
||||
|
||||
`ui_net_sink_poll()` 自己不产生耗时——它只是**向 netTask 发 RPC 并原地等回执**
|
||||
(`net_*` 公开 API 的固定模式:`osMessageQueuePut` → `net_wait_result`,见
|
||||
`Drivers/BSP/NET/net_socket.c:851`)。
|
||||
|
||||
| poll 内调用 | 触发频率 | 实际代价 | 上界 |
|
||||
|---|---|---|---|
|
||||
| `osMessageQueuePut(..., osWaitForever)` | 每次 RPC | 队列深 **8**,与 FTP / timeSync 共享(登记簿 **N-10**) | **无**(满则永久阻塞) |
|
||||
| `net_wait_result()` → `xTaskNotifyWait(..., portMAX_DELAY)` | 每次 RPC | 等 netTask 排到本条(它还要跑 `net_poll` + 3 个自愈 tick + FTP 大文件) | **无** |
|
||||
| `net_accept()` | **无客户端时每 10 ms 一次**(台架常态)= **100 msg/s** | 一次往返 + `net_accept_locked()` 内再跑一遍 `net_poll()`(整轮 SPI 中断读) | 实测最坏 **≈50 ms**(§8 `uis≈uiw≈50ms`) |
|
||||
| `net_recv(NET_MSG_DONTWAIT)` | 有客户端时每轮 | 同上(`net_recv_locked()` 亦先 `net_poll()`) | 同上 |
|
||||
| `net_send()` LCDMEM 应答 **3204 B** | 客户端每请求一帧(peer ≈10 次/s) | `net_send_locked` 按 `NET_SEND_CHUNK_MAX = min(CH395F_SEND_BUF_SIZE_BYTES=2×512, NET_DMA_MAX_PAYLOAD=2KB) = 1024 B` 拆 **4 块**,每块写完等 `SENBUF_FREE`,`NET_SEND_TIMEOUT_MS=1000` | 最坏 **≈4 s**,调用方全程等 |
|
||||
|
||||
**结论**:poll 的耗时 = **"netTask 什么时候有空"**,不是"UI 算得慢"。netTask 是全部 socket
|
||||
操作的**唯一执行者**,这个等待天然不可预测——E-02 实测一次空闲 FTP 登录把远端显示冻结
|
||||
**17.7 s**(`docs/问题记录/错误码传播审计.md`);帧周期真机 **61.6 ms** vs 模拟器 **6.8 ms**
|
||||
(`docs/测试说明/链路延迟测试说明.md` L8)同源。**任何"把心跳挪个位置"的改法都改变不了这一点。**
|
||||
|
||||
### 9.2 "彻底解决"的三条判据
|
||||
|
||||
| # | 判据 | 现状 |
|
||||
|---|---|---|
|
||||
| **J1** | uiTask 主循环里 **0 次** net 往返 → 刷新周期只由 LVGL + 调度决定 | ❌ 每轮 1~2 次 |
|
||||
| **J2** | 心跳自产,不依赖任何其他任务 | ✅ P1-4a |
|
||||
| **J3** | 推流的阻塞只影响推流自己(故障域隔离)+ 有观测 | ❌ 与刷新同域 |
|
||||
|
||||
### 9.3 方案 A:门控——让"没事的时候"poll 真的不做事
|
||||
|
||||
**做法**:发 RPC 之前先用**控制块 RAM 状态**判断"有没有活要干",没有就直接返回。
|
||||
|
||||
| 需要的信息 | 现成 RAM 判据(`net_get_sock()` 是公开 API、纯 RAM 读) | 语义依据 |
|
||||
|---|---|---|
|
||||
| 有连接可接受 | `p->state == NET_SOCK_STATE_ESTABLISHED` | 与 `net_accept_locked()` 同判据(单连接模型:监听 Socket 自身在 CONNECT 后转数据通道) |
|
||||
| 有数据可读 | `p->readable != 0` | `net_poll` 在 RECV_OK 置位、`net_recv_locked` 读空清零(`net_select.c` 的 `check_socket_readable` 用的就是它) |
|
||||
| 对端已关闭 | `p->state == NET_SOCK_STATE_CLOSED` | 需发一次 recv 拿 EOF(0) 并触发 `auto_relisten` |
|
||||
|
||||
**落点(关键,别踩坑)**:判据**不能写进共享的 `ui_net_sink.c`**,必须放**平台后端**——
|
||||
模拟器 `simulator/sim/net_socket_sim.c` 的监听块在 `accept()` 成功**之前**不会是
|
||||
`ESTABLISHED`,直接门控会让**模拟器永远连不上**。定稿取 **net 层公开 API**(不是 UI backend):
|
||||
|
||||
```c
|
||||
/* Drivers/BSP/NET/net_socket.h(新增,双端各一份实现) */
|
||||
int net_can_accept(int sockfd); /* 1 = 有连接可接受;0 = 无(纯 RAM 读,无 RPC) */
|
||||
int net_can_read(int sockfd); /* 1 = 有数据可读或已 EOF;0 = 无 */
|
||||
```
|
||||
|
||||
选 net 层而非 UI backend 的三条理由:① 语义属于 socket,不属于 UI;② 模拟器后端本就是
|
||||
`net_*` 的镜像实现,加在那里比在 UI backend 里再开一条"绕过 net API"的旁路更收敛;
|
||||
③ timeSync / FTP 后续同样能用(N-4 pending 二期落地前先给它们减负)。
|
||||
共享的 `ui_net_sink.c` **只调这两个 API,不直接读控制块**。
|
||||
|
||||
**竞态论证**:单字读、netTask 单写者;最坏两种结果都可接受——
|
||||
① **晚一轮发现**(≤1 ms:netTask 自己每 1 ms 就 `net_poll()` 一次,`readable`/`state` 早已刷新);
|
||||
② **扑空**(状态刚变,RPC 返回 `WOULDBLOCK`,现有分支已处理)。
|
||||
|
||||
**收益**:台架常态(无客户端)poll 从"一次往返(最坏 50 ms)"降到**两次 RAM 读(< 2 µs)**;
|
||||
顺带销项 **N-10**(100 msg/s 队列压力,与 FTP 抢深度 8 的队列)。
|
||||
⚠ **A 不改变"有客户端时"的行为**——那正是 B 的靶子。
|
||||
|
||||
#### 9.3.1 落地补充(2026-10-10 实现时新增的两条,设计阶段未预见)
|
||||
|
||||
| # | 落地时发现的坑 | 处置 |
|
||||
|---|---|---|
|
||||
| **I-2 兜底收帧** | `readable` **只由 RECV_OK 中断置位**(`handle_recv_event`),而 Trap 22 的 `net_reconcile_tick()` 三类对账**不含**"芯片 RX 非空但 `readable=0`" → 该中断一旦丢失,纯门控会把已到达的数据**永久藏住**(改前靠每轮 `net_recv_locked()` 里的 `ch395f_get_recv_len()` 顺带发现) | `ui_net_sink.c` 加 `RM_LIVENESS_DIV=32`:门控判"无数据"时仍**每 32 轮无条件 recv 一次**(10 ms 节拍 → 最坏额外延迟 ≈320 ms;RPC 频率仍从 100/s 降到 ≈3/s)。**教训:门控类优化必须保留低频兜底,否则是拿"自愈能力"换性能** |
|
||||
| **I-3 监听失效兜底** | 加了 accept 门控后,若监听控制块消失(网络重建 / 异常释放),`net_can_accept()` 恒返 0 → 改前"每轮 accept 撞 BADF"这条**暴露路径随之消失** → 端口永久不再接受新客户端 | 门控返回 0 时补一次 `net_get_sock(g_listen) == NULL` 判定,命中即 `g_listen = -1` 交回既有重建分支 |
|
||||
|
||||
**双端语义**:模拟器 `net_socket_sim.c` 的两个同名实现**恒返回 1**——Winsock 本就非阻塞、
|
||||
没有 RPC 成本,恒 1 使门控在模拟器上**逐位等价于改前行为**(含"每轮都 recv"的自愈特性),
|
||||
`simulator/tests/check_remo_sink.py` 的协议回归因此仍有效。
|
||||
|
||||
#### 9.3.2 A 落地实测(2026-10-10,`@test` PASS,33 个 `[PROF]` 样本 / 开机 29 s / 台架无客户端)
|
||||
|
||||
| 字段 | 改前(§8 基线) | **改后实测** | 判读 |
|
||||
|---|---|---|---|
|
||||
| `uis` 推流往返最坏 | **≈50 ms** | **1 µs**(33 样本 max,`uisn` 全程 0) | 门控生效,**降 5 个数量级**;§9.6 目标 ≤50 µs 达成 |
|
||||
| `uiw` UI 单轮工作最坏 | ≈50 ms(且 `uiw≈uis` → 单轮几乎全是推流) | **11.4 ms** | 剩下的**只含 LVGL**——网络已不在刷新路径上 |
|
||||
| `uil` 周期最近值 | 10 ms | 9999 µs | 标称节拍未变 |
|
||||
| `uip` 周期最坏 | ≈61 ms | **60.9 ms** | ⚠ **未达标**(§9.6 原写 ≤15 ms),但**已与 `uis` 脱钩** → 见下 |
|
||||
| `gl` 丢样 | 0 | 0 | DWT 数据可信 |
|
||||
|
||||
⚠ **`uip` 未达标不是 A 失败,是判据当初归因错了**:`uis` 已到 1 µs 而 `uip` 仍 60.9 ms,
|
||||
说明 uiTask 循环周期的最坏值由 **LVGL 整帧渲染**决定(`docs/测试说明/链路延迟测试说明.md` L8
|
||||
实测帧周期真机 **61.6 ms**,与实测 60.877 ms 吻合),推流从来只是**叠加**在这条地板上的
|
||||
**额外**抖动。**这条地板不是网络问题**——正是这条实测结论让 §9.4 的方案 B 被否决。
|
||||
**若目标是把刷新周期本身压下来,那是另一个议题(LVGL 帧周期 / 渲染模式),不在本案范围。**
|
||||
|
||||
### 9.4 方案 B(把推流搬出 uiTask,独立 `uiSinkTask`)——**已评估,决定不做(2026-10-10)**
|
||||
|
||||
**当初的动机**:客户端一连上,几乎每轮都要 `net_send` 3204 B,这个等待由芯片 FIFO / 链路 /
|
||||
netTask 决定,UI 侧无法设上界(半途截断 = 客户端 CRC 失败 = 黑屏,E-04 已定死不能碰)。
|
||||
按"uiTask 里一次 net 调用都不留"的思路完整设计过:新任务(256 words / BelowNormal /
|
||||
不进 `required_mask`)+ 帧 seqlock 取帧 + 栈与堆两条预算路线 + 观测面迁移。
|
||||
|
||||
**否决理由(全部来自 §9.3.2 的实测)**:
|
||||
|
||||
1. **治不到病**:A 之后 `uis` = 1 µs,而 `uip` 最坏仍是 60.9 ms = **LVGL 整帧渲染地板**
|
||||
(文档实测帧周期 61.6 ms)。搬走推流**不会让刷新变快**,只是把同一段等待换个任务背。
|
||||
2. **收益只剩极端并发下的"故障域隔离"**:即"客户端在连 + netTask 被 FTP 挤住"这一格
|
||||
(E-02 的 17.7 s 是**改前**、FTP 空闲登录场景)。那是**远端显示的观感**问题,
|
||||
设备本体的复位风险已由 P1-4a 解除。
|
||||
3. **成本不成比例**:新增任务 + 帧 seqlock + `ui_port`/backend 双端改动 + **必改 `.ioc`**
|
||||
(栈或堆两条路线都要 CubeMX 重生成 + 存活审计)+ 十余处改动与全套回归;
|
||||
其中"不动 uiTask 栈"那条路线还会把堆 `min_ever` 压到 S11 告警线(3072 B)以下。
|
||||
|
||||
**不做 B 的残留暴露(如实记录)**:客户端在连且 netTask 拥塞时,`ui_net_sink_poll()`
|
||||
仍可能在 uiTask 里等最长 ≈4 s(LCDMEM 应答 4 块 × `NET_SEND_TIMEOUT_MS`),
|
||||
表现为**本地画面与远端同时卡住**。观测面已常驻,无需新增埋点:`[PROF]` 的 `uis`
|
||||
(推流往返最坏)与 `uisn`(此刻仍在推流段内多久)——**`uisn` 持续非 0 就是这一格真发生的铁证**。
|
||||
若日后它成为实际问题,**正确方向也不是 B**,而是先治 netTask 的排队公平性
|
||||
(登记簿 N-4 pending 二期),那才是同一条根因的上游。
|
||||
|
||||
### 9.5 明确不做的四条(及理由)
|
||||
|
||||
| 备选 | 为什么不做 |
|
||||
|---|---|
|
||||
| 只调心跳顺序(=现状 P1-4a) | 心跳不再被拖,**刷新仍然被拖**——不治本次的病 |
|
||||
| 给 `net_*` 加"有界等待"版 | 回执通道是"每任务一个 32 位通知值",超时后**迟到的回执会被下一次调用的零等待拾取当成新结果**(`net_wait_result` 注释里 L7 挂死 / 整段错位的同款根因)。要做对必须引入请求序号,代价高于搬家 |
|
||||
| 把推流塞进 netTask | 零往返、零新增 RAM,最"彻底";但共享源码 `ui_net_sink.c` 要改成走 backend 的 socket 端口(双端同改)、netTask 这个唯一执行者更胖、且一个 UI 侧 bug 直接变成整机复位。**同样不做**;若 netTask 排队公平性真成问题,从 N-4 二期治上游,而不是把 UI 搬过去 |
|
||||
| 提高 uiTask 优先级 | §8 已明确警告它会**掩盖** §4.2/§4.4 两个真实缺陷;而且它根本不解决"UI 在等 netTask" |
|
||||
|
||||
### 9.6 落地与验收判据
|
||||
|
||||
```
|
||||
A(门控 + 兜底)→ @build + @test + e2e → 看 [PROF]:uis 掉到 µs 级 → 本案结案
|
||||
(B 已评估否决,见 §9.4)
|
||||
```
|
||||
|
||||
| 判据 | 目标值 | 状态(2026-10-10) |
|
||||
|---|---|---|
|
||||
| `uis`(无客户端) | ≤ **50 µs**(门控生效的直接证据;改前 ≈50 ms) | ✅ **A 已达成:实测 1 µs**(§9.3.2) |
|
||||
| `uiw` 与 `uis` **不再同值** | 改前 `uiw≈uis≈50 ms` | ✅ **A 已达成:11.4 ms vs 1 µs** |
|
||||
| `uip`(uiTask 循环周期最坏) | ~~≤15 ms~~ → **修订**:`uip` 地板 = LVGL 帧周期 ≈61 ms,**与网络无关,本案不治它**;只要求它**不随网络活动抬升**(`uis` 到 µs 级即为达成) | ⚠ 判据已按实测修订(§9.3.2) |
|
||||
| 回归 | `@test` 连续 **10 次** 0 复现;`@build` 0 错误 0 警告;模拟器 `ui-verify` 五步 + `simulator/tests/check_remo_sink.py` e2e **0/25600 位差异** | ✅ **A 已达成(2026-10-10)**:`@build` 0 错 0 警;**`@test` 10/10 PASS**(每轮 `uis` ≤ **1 µs**、`uisn` ≡ 0、`task stall` 0 次、`[ERR]` 0 条、`uiw` ≤ 11.36 ms、`uip` ≤ 61.35 ms);e2e `check_remo_sink.py` **0/25600 位差异**;`check_menu_rows.py` PASS;内容页文本断言 50/50(像素比对需本机 `simulator/tests/ref/`,该目录不入库)。⚠ 10 连跑中 `net init: OK` 的 `[+Nms]` 由 ≈589 ms 变到 ≈9.1 s,**是采集零点亮盘/校验/复位耗时推移,非固件变慢**:同一日志内 `reset cause` 在 +8517 ms、`net init: OK` 在 +9141 ms → **固件自身仍 ≈624 ms**;17:31 的旧镜像日志(本次改动之前)已有同形态 |
|
||||
|
||||
### 9.7 风险与回退
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|---|---|
|
||||
| 门控误判(芯片丢中断 → `state`/`readable` 长期不同步) | netTask 已有 `net_reconcile_tick()` 200 ms 对账 + 去抖自愈(Trap 22);门控只是"少发 RPC",**不新增自愈责任**;`readable` 侧另有 **I-2** 每 32 轮无条件 recv 兜底 |
|
||||
| 监听控制块消失 → 门控恒返 0,端口不再接受新客户端 | **I-3**:门控返 0 时查 `net_get_sock(g_listen)`,消失即 `g_listen = -1` 交回既有重建分支 |
|
||||
| 回退 | 门控是纯函数级改动,只决定"要不要发 RPC",**单独可回退**(去掉两处 `net_can_*()` 判断即恢复改前行为),无数据结构 / 协议 / 任务变更 |
|
||||
|
||||
---
|
||||
|
||||
## 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
|---|---|
|
||||
| 2026-10-10 | 创建。基于唯一留存失败日志(`serial.log` @17:36)+ 代码走查完成归因:否掉"CPU 饱和"(实测占空比 10%~20%,`ring` 速率反证无空转),定位为**优先级饥饿(uiTask 唯一 BelowNormal)+ UI 心跳依赖 netTask 往返**;另发现"无新帧仍压环"的独立正确性缺陷;给出 P0/P1/P2 分层处置与"先测量后修"的推荐顺序 |
|
||||
| 2026-10-10 | 新增 **§9 彻底解耦方案(待落地)**:量化 `ui_net_sink_poll()` 的 3 个**无上界**等待点(队列 `osWaitForever` / `xTaskNotifyWait(portMAX_DELAY)` / LCDMEM 应答 3204 B 按 1024 B 拆 4 块等 `SENBUF_FREE`,最坏 ≈4 s),指出"poll 耗时 = netTask 什么时候有空";给出 **A 门控**(`net_can_accept()`/`net_can_read()` 纯 RAM 判据,落点必须在平台后端——模拟器监听块 accept 前不是 `ESTABLISHED`)+ 一套"把推流搬出 uiTask"的方案(**同日评估后否决并删除,见下面两轮**);记录四条**明确不做**的备选与理由、验收判据、风险与回退。**代码未改动**,登记 **N-19 / C7** |
|
||||
| 2026-10-10 | **§9.3 方案 A 已落地**:`net_socket.c/.h` 新增 `net_can_accept()` / `net_can_read()`(纯 RAM 读控制块、零 RPC),`net_socket_sim.c` 同名实现恒返 1(模拟器行为逐位不变),`ui_net_sink_poll()` 改"先门控再 RPC",并补 **I-2 兜底**(每 32 轮无条件 recv,防 RECV_OK 中断丢失把数据永久藏住)与 **I-3 监听失效兜底**(控制块消失 → `g_listen=-1` 重建);`ui_net_sink.h/.c` 注释订正(原"非阻塞轮询 / 不阻塞 LVGL 刷新"表述不准确 → 改为"门控只消除无谓往返,真有活时往返仍无上界")。验证:Keil **0 错 0 警**(Code 256276→256368,+92 B)、模拟器构建 OK、`check_menu_rows.py` PASS、`check_coding_standard.py` PASS、**`@test` PASS**。**实测(§9.3.2):`uis` 50 ms → 1 µs、`uiw` 50 ms → 11.4 ms(只剩 LVGL)、`gl=0`**;`uip` 仍 60.9 ms = LVGL 帧周期地板,§9.6 判据已据此修订 |
|
||||
| 2026-10-10 | **A 收口 + 方案 B 否决并删除**(用户定案"B 不做")。① **A 收口**:`@test` **10/10 PASS**(每轮 `uis` ≤1 µs、`uisn` ≡ 0、0 `task stall`、0 `[ERR]`)、e2e `check_remo_sink.py` **0/25600 位差异**、内容页文本断言 50/50。② **B 删除**:§9.4 由完整设计(新任务 256 words / BelowNormal / 不进 `required_mask`、帧 seqlock、栈与堆两条预算路线、观测面迁移、12 处改动清单)改写为**否决记录**——实测 `uis`=1 µs 而 `uip` 仍 60.9 ms(LVGL 帧周期地板)证明"搬出 uiTask"**不会让刷新变快**,收益只剩极端并发下的观感隔离,而成本含**必改 `.ioc`** 的 CubeMX 重生成;残留暴露与观测手段(`uis`/`uisn`)如实保留。③ **同步清理**:§9.5/§9.6/§9.7 去掉 B 条目;**N-19 / C7 不入登记簿**(当日结案,`docs/待处理内容.md` 保持无该编号);`子系统设计/看门狗设计说明.md` §9.2 的 N-19 行移除;`RemoDispBus协议与接收设计说明.md` 去掉"规划中改独立任务"段;`ui_net_sink.c/.h` 注释去掉"应搬出 uiTask"的指向 |
|
||||
|
||||
@@ -320,7 +320,7 @@ D-2/D-3 的共同根因:**没有任何机械检查**保证"文档说的 = 代
|
||||
|------|------|------|------|
|
||||
| **N-02** | **FTP 无鉴权**:`cmd_pass` 无条件 `send_simple_response(socket, 230, STATUS_230)` | `lftpd.c:367-371`(`cmd_user :539` 亦不校验) | 最小可用方案:`config.ini` 增 `[ftp] user/pwd_hash`,`cmd_pass` 用**定长比较 + 失败计数锁定**;生产固件默认关闭 FTP 写权限(`FF_FS_READONLY` 式的只读模式) |
|
||||
| N-09 | `cmd_pasv` 每次新建监听 `lftpd_inet_listen(0)`(随机端口) | `lftpd.c:373` | 在**单连接 socket** 上与 UI `:7003`、对时 `:8888` 争用同一物理 socket;建议限定监听资源池 + 显式"FTP 会话独占"策略,并在文档写明"一次只允许一个 TCP 服务活动" |
|
||||
| N-10 | uiTask 以 **10 ms** 周期向 netMsgQueue 投递 accept 轮询 → ≈100 msg/s,与 FTP 共享深度 8 的队列 | `ui_net_sink.c` 轮询逻辑 | 队列满时 `osErrorResource` → UI 丢帧/重投。建议:把"服务就绪"改为事件驱动(netTask 在 LISTEN 状态变化时置位),UI 侧改为等待事件;或给队列扩容并在 UI 侧背压 |
|
||||
| N-10 | uiTask 以 **10 ms** 周期向 netMsgQueue 投递 accept 轮询 → ≈100 msg/s,与 FTP 共享深度 8 的队列 | `ui_net_sink.c` 轮询逻辑 | 队列满时 `osErrorResource` → UI 丢帧/重投。建议:把"服务就绪"改为事件驱动(netTask 在 LISTEN 状态变化时置位),UI 侧改为等待事件;或给队列扩容并在 UI 侧背压。**→ 已销项(2026-10-10)**:未走"事件驱动",改用**门控**——`net_can_accept()`/`net_can_read()` 纯 RAM 读控制块,无活时**一次 RPC 都不发**(100 msg/s → 0);实测 `uis` 50 ms → **1 µs**。方案与实测见 `UI任务看门狗停摆分析.md` §9.3 / §9.3.2 |
|
||||
| P2-11 | `net_task.c` 对 `osEventFlagsGet` + Clear 非原子 | 台账 | 用 `osEventFlagsAcquire`/单次 Wait 消 TOCTOU |
|
||||
| P2-8 | `net_recv` 阻塞超时 30 s | 台账 | 缩短并让上层可取消 |
|
||||
| — | PHY 强制 100M 全双工,忽略自协商 | AGENTS/architecture | 与某些交换机会联不通;至少加"链路 up 失败 3 次后回退自协商" |
|
||||
@@ -679,7 +679,7 @@ const ui_page_desc_t ui_page_deadzone = UI_FORM_PAGE("遥测死区", s_items_dea
|
||||
| F-1/F-2/F-3 | `lv_conf.h` 与 LVGL 9.3 对齐 + 主题/控件/flex 裁剪 | Flash −55 KB |
|
||||
| N-12/N-13 | `dhara_nand_is_free` 全页校验;`dhara_nand_copy` 处理 `DHARA_NO_PAGE` | 掉电注入测试通过 |
|
||||
| N-16 | 日志常驻 `FIL` + 批量 flush;UI 记录读缓存升级 | 单条日志 NAND 操作次数下降 ≥50% |
|
||||
| N-10 | UI accept 改事件驱动(脱离 100 msg/s 轮询) | 队列占用可测量下降 |
|
||||
| N-10 | ~~UI accept 改事件驱动(脱离 100 msg/s 轮询)~~ **已落地(2026-10-10,改走"门控"而非事件驱动)** | ✅ 无客户端时队列占用 **100 msg/s → 0**;实测 `uis` 50 ms → **1 µs**(`UI任务看门狗停摆分析.md` §9.3 / §9.3.2) |
|
||||
| D-1 | 芯片手册图片降采样 / 拆 submodule | clone 40 MB → ~8 MB |
|
||||
| D-3 | `check_doc_links.py` 入门禁 + 修 18 条断链 | 零断链 |
|
||||
| U1–U4 | UI 令牌化 + 原型库 + 生成器 + golden | 新增一页 ≤30 行 |
|
||||
|
||||
@@ -347,6 +347,39 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen)
|
||||
return nidx;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:查询监听 Socket 是否有可接受的连接(与 MCU 端同名 API)
|
||||
* 入口参数:sockfd - 监听 socket 描述符
|
||||
* 返回值:1 - 可尝试 accept;0 - 描述符无效
|
||||
* 限定条件:socket 已创建
|
||||
* 函数说明:Winsock 后端无"零成本预知"手段(要嘛真调 accept、要嘛 select()),
|
||||
* 而 accept 本就是非阻塞的、**没有 MCU 端的 RPC 成本** → 恒返回 1,
|
||||
* 使上层门控在模拟器上等价于"改前行为"(逐位一致,不引入新语义)
|
||||
*/
|
||||
int net_can_accept(int sockfd)
|
||||
{
|
||||
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:查询 Socket 是否"值得发一次 recv"(与 MCU 端同名 API)
|
||||
* 入口参数:sockfd - socket 描述符
|
||||
* 返回值:1 - 可尝试 recv;0 - 描述符无效
|
||||
* 限定条件:socket 已创建
|
||||
* 函数说明:同 net_can_accept——Winsock recv 非阻塞、无 RPC 成本 → 恒返回 1,
|
||||
* 上层门控在模拟器上不改变既有行为(含"每轮都 recv"的自愈特性)
|
||||
*/
|
||||
int net_can_read(int sockfd)
|
||||
{
|
||||
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:TCP Client 发起连接
|
||||
* 入口参数:sockfd - socket 描述符
|
||||
|
||||
Reference in New Issue
Block a user