diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index 8a0870cf..b06aee38 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -38,7 +38,6 @@ * 全局变量定义区 - 尽量避免使用全局变量 */ static net_sock_t s_net_socks[NET_MAX_SOCKETS]; /* Socket 控制块数组 */ -static int s_net_errno = 0; /* 最后错误码 */ static uint8_t s_net_initialized = 0; /* 初始化标志 */ static uint8_t s_local_ip[4]; /* 本机 IP 缓存(网络序,net_getsockname 用) */ static uint16_t s_dynamic_port = 49152; /* 动态端口分配起始值 */ @@ -52,6 +51,101 @@ static volatile uint32_t s_net_generation = 0; /* 网络代次号:网络 */ extern osMessageQueueId_t netMsgQueueHandle; /* CubeMX 生成的消息队列句柄 */ +/* ================= 错误码存储(E-03 修复:按任务隔离,消除跨任务覆写竞态) ================= + * + * 旧实现是单个全局 `static int s_net_errno`:netTask 在处理消息时写入本层的 + * 「最后错误码」,调用方包装器随后在**自己的上下文**再写一次。两条写路径分属不同 + * 任务且无互斥,A 任务刚被置好的码会被 B 任务的通知处理抢先覆盖 → A 读到 B 的码。 + * + * 新实现把错误码存进「每个任务自己的槽位」:任务读写自己的槽位,天然无竞态, + * 且保留 net_get_errno() 原语义(本任务最后一次失败的精确码、成功不清零)。 + * + * 容量:NET_ERRNO_TASK_SLOTS 个任务句柄槽位(应用任务 + netTask + 测试任务够用)。 + * 超出时**不新增共享槽位**(那会把竞态带回来),而是统一落到 [0] 槽—— + * 语义退化为全局共享,仅影响排障可读性,不影响任何功能判定。 + */ +#define NET_ERRNO_TASK_SLOTS 8U + +/* 每任务错误码槽位:slot[i].owner == 当前任务句柄 时命中 */ +static struct { + void *owner; /* 槽主任务句柄(NULL = 空槽) */ + int err; /* 该任务最后一次失败的精确错误码 */ +} s_net_errno_slots[NET_ERRNO_TASK_SLOTS]; + +/* + * 函数功能:取本任务的错误码槽位(命中/分配),无 RTOS 时回退 [0] + * 入口参数:无 + * 出口参数:无 + * 返回值:槽位下标(0 ~ NET_ERRNO_TASK_SLOTS-1) + * 限定条件:内核未启动或中断上下文返回 0(不做槽位分配,避免在 ISR 里改槽位表) + * 函数说明:1. 已存在的槽位(owner 命中)直接复用;空槽(owner == NULL)首次占用 + * 2. 快路径**只读不写**:并发读者绝不会看到"owner 已写、err 未写"的中间态 + * 3. 槽位与任务不强绑定释放:任务句柄被回收复用时,新任务沿用该槽位的 + * err 初值无碍(语义本就允许残留码,与旧全局变量行为一致) + */ +static uint32_t net_errno_slot(void) { + /* 任务句柄(未启动内核/中断上下文时为 NULL) */ + void *me; + /* 空槽候选与循环变量 */ + uint32_t i; + uint32_t empty = NET_ERRNO_TASK_SLOTS; + + /* 内核未启动或中断上下文:统一用 [0] 槽(语义退化为共享,仅排障用) */ + if (osKernelGetState() != osKernelRunning) { + return 0U; + } + me = (void *)xTaskGetCurrentTaskHandle(); + if (me == NULL) { + return 0U; + } + + /* 无锁快路径:找自己的槽位,顺带记下第一个空槽,全程**绝不写**任何槽 + * (写会让并发读者看到半成品槽位) */ + for (i = 0U; i < NET_ERRNO_TASK_SLOTS; i++) { + const void *owner = s_net_errno_slots[i].owner; + if (owner == me) { + return i; /* 命中:本任务已有槽位 */ + } + if (owner == NULL && empty == NET_ERRNO_TASK_SLOTS) { + empty = i; /* 记录第一个空槽(尚未占用) */ + } + } + + /* 无自己的槽位:尝试占用空槽。本函数只在任务上下文走到这里(中断/内核未启动 + * 已在上方返回 0),故用任务上下文临界区(与 App/log/console_log.c 同一写法) */ + if (empty < NET_ERRNO_TASK_SLOTS) { + taskENTER_CRITICAL(); + if (s_net_errno_slots[empty].owner == NULL) { + s_net_errno_slots[empty].err = NET_OK; + s_net_errno_slots[empty].owner = me; + taskEXIT_CRITICAL(); + return empty; + } + /* 复查失败(被并发占用):不重试,回退 [0] 槽(这是**特意避免**再次循环 + * 的取舍——重试会与另一任务形成写入竞争,那就把要修的竞态又带回来了) */ + taskEXIT_CRITICAL(); + } + + /* 槽位用尽:回退 [0] 槽(共享语义,仅排障可读性下降) */ + return 0U; +} + +/* + * 函数功能:记录**本任务**最后一次失败的精确错误码(取代原 `s_net_errno = X` 赋值) + * 入口参数:err - 错误码(NET_ERR_xxx,负值) + * 出口参数:无 + * 返回值:err 原值(便于写成 `return net_set_errno(NET_ERR_XXX);`) + * 限定条件:无 + * 函数说明:只写本任务的槽位,其他任务读不到、也改不了 → 消除跨任务覆写竞态 + */ +static int net_set_errno(int err) { + s_net_errno_slots[net_errno_slot()].err = err; + return err; +} + +/* 「记本任务错误码 + 统一失败返回 -1」的紧凑写法,取代原 `s_net_errno = X; return -1;` */ +#define NET_ERR_RETURN_1(err) (net_set_errno(err), -1) + /* @@ -149,15 +243,14 @@ static bool s_mac_is_zero(const uint8_t *p_mac) { /* 清零 Socket 控制块 */ memset(s_net_socks, 0, sizeof(s_net_socks)); - s_net_errno = 0; + net_set_errno(NET_OK); s_net_initialized = 0; s_net_task_handle = xTaskGetCurrentTaskHandle(); /* 执行任务 = net_init 调用者(NET 层 = netTask / 硬件层 = testTask) */ /* 检查 CH395F 芯片 */ if (ch395f_check_exist() != CH395F_STATUS_OK) { DBG_ERROR("net_init: ch395f_check_exist FAILED"); - s_net_errno = NET_ERR; - return -1; + return NET_ERR_RETURN_1(NET_ERR); } /* 复位芯片 */ ch395f_reset(); @@ -211,8 +304,7 @@ static bool s_mac_is_zero(const uint8_t *p_mac) { /* 初始化 CH395F 协议栈 */ ret = ch395f_init(); if (ret != CH395F_STATUS_OK) { - s_net_errno = NET_ERR; - return -1; + return NET_ERR_RETURN_1(NET_ERR); } /* 设置 MAC(须在协议栈初始化之后,限定条件"芯片已初始化";写芯片 EEPROM, @@ -580,11 +672,11 @@ void net_process_messages(void) { net_sock_t *p_sock = net_get_sock(msg.sockfd); if (p_sock == NULL) { /* 描述符无效 → BADF,结果 -1 */ - s_net_errno = NET_ERR_BADF; + net_set_errno(NET_ERR_BADF); msg.result = -1; } else if (p_sock->state != NET_SOCK_STATE_ESTABLISHED) { /* 未建立连接(或已断开)→ NOTCONN */ - s_net_errno = NET_ERR_NOTCONN; + net_set_errno(NET_ERR_NOTCONN); msg.result = -1; } else { /* 真正写入芯片:1KB/块等 SEND_OK 中断,单块停滞 1s 超时, @@ -598,7 +690,7 @@ void net_process_messages(void) { net_sock_t *p_sock = net_get_sock(msg.sockfd); if (p_sock == NULL) { /* 描述符无效 → BADF,结果 -1 */ - s_net_errno = NET_ERR_BADF; + net_set_errno(NET_ERR_BADF); msg.result = -1; } else { /* 三条出口:数据就绪 → 即时 SPI 读出;DONTWAIT 无数据 → 0(对端关闭) @@ -617,7 +709,7 @@ void net_process_messages(void) { net_sock_t *p_sock = net_get_sock(msg.sockfd); if (p_sock == NULL) { /* 描述符无效 → BADF,结果 -1 */ - s_net_errno = NET_ERR_BADF; + net_set_errno(NET_ERR_BADF); msg.result = -1; } else { /* 先了结本 fd 挂起 recv(排空或取消,必通知),再关芯片侧 Socket; @@ -658,7 +750,7 @@ void net_process_messages(void) { net_sock_t *p_sock = net_get_sock(msg.sockfd); if (p_sock == NULL) { /* 描述符无效 → BADF,结果 -1 */ - s_net_errno = NET_ERR_BADF; + net_set_errno(NET_ERR_BADF); msg.result = -1; } else if (p_sock->state != NET_SOCK_STATE_ESTABLISHED) { /* 非 ESTABLISHED(LISTENING/CLOSED 等):两侧已一致,无需动作 */ @@ -681,7 +773,7 @@ void net_process_messages(void) { net_sock_t *p_sock = net_get_sock(msg.sockfd); if (p_sock == NULL) { /* 描述符无效 → BADF,结果 -1 */ - s_net_errno = NET_ERR_BADF; + net_set_errno(NET_ERR_BADF); msg.result = -1; } else { /* 无条件芯片侧重监听:单次尝试,失败登记非阻塞重试 @@ -693,7 +785,7 @@ void net_process_messages(void) { default: /* 未知消息类型(枚举闭合,正常不应触发):置通用错误码与结果 -1, * 保证通知值恒为本条消息的确定结果(无上一条残留) */ - s_net_errno = NET_ERR; + net_set_errno(NET_ERR); msg.result = -1; break; } @@ -719,9 +811,12 @@ void net_process_messages(void) { /* 成功:结果的 32 位原值(字节数 / 描述符) */ uint32_t notify_val = (uint32_t)msg.result; - /* 失败:携带具体错误码(如 NET_ERR_WOULDBLOCK;errno 未置位时回退通用 NET_ERR) */ + /* 失败:回传本条消息**自身**的精确错误码(NET_ERR_WOULDBLOCK 等)。 + * E-03 后取的是 netTask 自己的错误码槽位——锁定实现(*_locked)在 + * 失败返回前记录的码,不受其他任务影响;未置位时回退通用 NET_ERR。 */ if (msg.result < 0) { - notify_val = (uint32_t)((s_net_errno != 0) ? s_net_errno : NET_ERR); + int te = s_net_errno_slots[net_errno_slot()].err; + notify_val = (uint32_t)((te != 0) ? te : NET_ERR); } /* 覆盖式写入通知值:调用方零等待拾取即得最新回执 */ xTaskNotify(msg.caller, notify_val, eSetValueWithOverwrite); @@ -783,16 +878,14 @@ int net_socket(int domain, int type) { /* 参数检查:地址族仅支持 NET_AF_INET */ if (domain != NET_AF_INET) { /* 不支持的地址族 → INVAL */ - s_net_errno = NET_ERR_INVAL; - return -1; + return NET_ERR_RETURN_1(NET_ERR_INVAL); } /* 分配空闲控制块(in_use=0 的槽位) */ sockfd = alloc_socket(); if (sockfd < 0) { /* 无空闲控制块 → NOMEM */ - s_net_errno = NET_ERR_NOMEM; - return -1; + return NET_ERR_RETURN_1(NET_ERR_NOMEM); } /* 初始化控制块:芯片侧 Socket 编号与描述符一一对应 */ @@ -841,22 +934,19 @@ int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) { /* 参数检查:描述符范围 */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { /* 越界 → BADF */ - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 取控制块 */ p_sock = &s_net_socks[sockfd]; /* 控制块未在用(未创建/已关闭)→ BADF */ if (!p_sock->in_use) { - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 地址指针/长度无效 → INVAL */ if (addr == NULL || addrlen < (int)sizeof(struct net_sockaddr_in)) { - s_net_errno = NET_ERR_INVAL; - return -1; + return NET_ERR_RETURN_1(NET_ERR_INVAL); } /* 地址强转为 IPv4 结构体 */ @@ -917,8 +1007,7 @@ int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) { if (status != CH395F_ERR_SUCCESS) { /* open 失败:留痕并返回 BUSY(芯片忙/复位冲突) */ DBG_ERROR("sock%d open_socket FAIL: status=0x%02X (expected 0x00)", sockfd, status); - s_net_errno = NET_ERR_BUSY; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BUSY); } /* UDP 打开即可用(无监听态) */ @@ -950,15 +1039,13 @@ int net_listen_locked(int sockfd) { /* 合法性检查:Socket 必须已创建且为 TCP 流类型 */ if (!p_sock->in_use || p_sock->type != NET_SOCK_STREAM) { - s_net_errno = NET_ERR_INVAL; - return -1; + return NET_ERR_RETURN_1(NET_ERR_INVAL); } /* 状态检查:仅允许在 BOUND(已 bind)或 CREATED(未 bind 直接用随机端口)状态下启动监听 */ if (p_sock->state != NET_SOCK_STATE_BOUND && p_sock->state != NET_SOCK_STATE_CREATED) { - s_net_errno = NET_ERR_INVAL; - return -1; + return NET_ERR_RETURN_1(NET_ERR_INVAL); } /* 单连接:监听 Socket 自身即数据通道,复用 CH395F 默认收发缓冲, @@ -983,15 +1070,13 @@ int net_listen_locked(int sockfd) { } /* 3 次仍失败,返回错误 */ if (status != CH395F_ERR_SUCCESS) { - s_net_errno = NET_ERR_BUSY; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BUSY); } /* 下发 TCP LISTEN 命令,进入监听状态 */ status = ch395f_tcp_listen(sockfd); /* LISTEN 失败 → BUSY */ if (status != CH395F_ERR_SUCCESS) { - s_net_errno = NET_ERR_BUSY; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BUSY); } /* 监听 Socket 进入 LISTENING 态 */ @@ -1026,14 +1111,12 @@ int net_listen(int sockfd) { /* 描述符越界 → BADF */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 消息队列未创建(网络层未初始化)→ 通用错误 */ if (netMsgQueueHandle == NULL) { - s_net_errno = NET_ERR; - return -1; + return NET_ERR_RETURN_1(NET_ERR); } /* 消息类型:启动 TCP 监听 */ @@ -1050,8 +1133,7 @@ int net_listen(int sockfd) { /* 入队:队列满时阻塞调用方直至有空位 */ if (osMessageQueuePut(netMsgQueueHandle, &msg, 0, osWaitForever) != osOK) { /* 入队失败 → BUSY */ - s_net_errno = NET_ERR_BUSY; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BUSY); } /* 阻塞等待 netTask 回执(两段式拾取,防丢失通知) */ @@ -1060,8 +1142,7 @@ int net_listen(int sockfd) { if (msg.result < 0) { /* netTask 把具体错误码(如 NET_ERR_WOULDBLOCK)以负值搭通知值回传,此处恢复为 * errno;对外仍保持 BSD 语义——失败一律返回 -1 */ - s_net_errno = msg.result; - return -1; + return NET_ERR_RETURN_1(msg.result); } /* 返回结果(成功 = 描述符/字节数) */ return msg.result; @@ -1084,8 +1165,7 @@ static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen p_listen_sock = &s_net_socks[sockfd]; /* 控制块未在用 → INVAL */ if (!p_listen_sock->in_use) { - s_net_errno = NET_ERR_INVAL; - return -1; + return NET_ERR_RETURN_1(NET_ERR_INVAL); } /* 先轮询一次:把状态机刷新到芯片侧最新状态 */ @@ -1102,8 +1182,7 @@ static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen (void)net_relisten_try(p_listen_sock); } /* 当前无连接 → WOULDBLOCK */ - s_net_errno = NET_ERR_WOULDBLOCK; - return -1; + return NET_ERR_RETURN_1(NET_ERR_WOULDBLOCK); } /* 填充对端地址(若调用方需要) */ @@ -1139,14 +1218,12 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) { /* 描述符越界 → BADF */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 消息队列未创建(网络层未初始化)→ 通用错误 */ if (netMsgQueueHandle == NULL) { - s_net_errno = NET_ERR; - return -1; + return NET_ERR_RETURN_1(NET_ERR); } /* 消息类型:接受新连接 */ @@ -1167,8 +1244,7 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) { /* 入队:队列满时阻塞调用方直至有空位 */ if (osMessageQueuePut(netMsgQueueHandle, &msg, 0, osWaitForever) != osOK) { /* 入队失败 → BUSY */ - s_net_errno = NET_ERR_BUSY; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BUSY); } /* 阻塞等待 netTask 回执(两段式拾取,防丢失通知) */ @@ -1182,8 +1258,7 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) { if (msg.result < 0) { /* netTask 把具体错误码(如 NET_ERR_WOULDBLOCK)以负值搭通知值回传,此处恢复为 * errno;对外仍保持 BSD 语义——失败一律返回 -1 */ - s_net_errno = msg.result; - return -1; + return NET_ERR_RETURN_1(msg.result); } /* 返回结果(成功 = 新连接描述符) */ return msg.result; @@ -1208,20 +1283,17 @@ int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen) { /* 描述符越界 → BADF */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 消息队列未创建(网络层未初始化)→ 通用错误 */ if (netMsgQueueHandle == NULL) { - s_net_errno = NET_ERR; - return -1; + return NET_ERR_RETURN_1(NET_ERR); } /* 服务器地址无效 → INVAL */ if (addr == NULL || addrlen < (int)sizeof(struct net_sockaddr_in)) { - s_net_errno = NET_ERR_INVAL; - return -1; + return NET_ERR_RETURN_1(NET_ERR_INVAL); } /* 消息类型:发起 TCP 连接 */ @@ -1246,8 +1318,7 @@ int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen) { /* 入队:队列满时阻塞调用方直至有空位 */ if (osMessageQueuePut(netMsgQueueHandle, &msg, 0, osWaitForever) != osOK) { /* 入队失败 → BUSY */ - s_net_errno = NET_ERR_BUSY; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BUSY); } /* 阻塞等待 netTask 回执(两段式拾取,防丢失通知) */ @@ -1256,8 +1327,7 @@ int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen) { if (msg.result < 0) { /* netTask 把具体错误码(如 NET_ERR_WOULDBLOCK)以负值搭通知值回传,此处恢复为 * errno;对外仍保持 BSD 语义——失败一律返回 -1 */ - s_net_errno = msg.result; - return -1; + return NET_ERR_RETURN_1(msg.result); } /* 返回结果(0 = 连接命令已下发,ESTABLISHED 须以状态轮询等待) */ return msg.result; @@ -1287,22 +1357,19 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a /* 参数检查:描述符范围 */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { /* 越界 → BADF */ - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 取控制块 */ p_sock = &s_net_socks[sockfd]; /* 未在用或非 TCP 流类型 → INVAL */ if (!p_sock->in_use || p_sock->type != NET_SOCK_STREAM) { - s_net_errno = NET_ERR_INVAL; - return -1; + return NET_ERR_RETURN_1(NET_ERR_INVAL); } /* 服务器地址无效 → INVAL */ if (addr == NULL || addrlen < (int)sizeof(struct net_sockaddr_in)) { - s_net_errno = NET_ERR_INVAL; - return -1; + return NET_ERR_RETURN_1(NET_ERR_INVAL); } /* 地址强转为 IPv4 结构体 */ @@ -1341,8 +1408,7 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a /* 3 次仍失败 → BUSY */ if (status != CH395F_ERR_SUCCESS) { - s_net_errno = NET_ERR_BUSY; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BUSY); } /* 清掉本 Socket 可能残留的中断状态,丢弃上一条连接遗留的 DISCONNECT */ @@ -1429,14 +1495,12 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len) { /* 参数检查:缓冲区无效 → INVAL */ if (buf == NULL || len <= 0) { - s_net_errno = NET_ERR_INVAL; - return -1; + return NET_ERR_RETURN_1(NET_ERR_INVAL); } /* 未建立连接 → NOTCONN */ if (p_sock->state != NET_SOCK_STATE_ESTABLISHED) { - s_net_errno = NET_ERR_NOTCONN; - return -1; + return NET_ERR_RETURN_1(NET_ERR_NOTCONN); } /* 记录起始时刻(用于停滞超时判定) */ @@ -1456,14 +1520,14 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len) { /* 发送中途断连(DISCONNECT/TIMEOUT 已由 net_poll 处理并置状态非 ESTABLISHED): * 立即失败,不再空等满超时(旧行为:白等 1s 后报 TIMEDOUT) */ if (p_sock->state != NET_SOCK_STATE_ESTABLISHED) { - s_net_errno = NET_ERR_CONNRESET; + (void)net_set_errno(NET_ERR_CONNRESET); break; } /* 停滞超时:NET_SEND_TIMEOUT_MS 后 send_ready 仍未置位 */ if (NET_SEND_TIMEOUT_MS > 0 && (HAL_GetTick() - tick_start) >= NET_SEND_TIMEOUT_MS) { DBG_ERROR("sock%d send stall: send_ready not set (%d/%d)", sockfd, sent, len); - s_net_errno = NET_ERR_TIMEDOUT; + (void)net_set_errno(NET_ERR_TIMEDOUT); break; } /* 轮询一次(更新 send_ready/状态),下一轮再试 */ @@ -1492,7 +1556,7 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len) { while (!p_sock->send_ready) { /* 发送中途断连:立即失败(同外层,避免白等超时) */ if (p_sock->state != NET_SOCK_STATE_ESTABLISHED) { - s_net_errno = NET_ERR_CONNRESET; + (void)net_set_errno(NET_ERR_CONNRESET); break; } /* 轮询:处理 SEND_OK/SENBUF_FREE 中断 */ @@ -1515,7 +1579,8 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len) { if (sent < len) { /* 短写告警:调用方必须检查返回值。静默截断会让对端永久等待剩余字节 * (2026-09-21 实证:UI 推流应答被截断 → 客户端反复判定"全程无字节"失联)。 */ - DBG_ERROR("sock%d short send %d/%d (errno=%d)", sockfd, sent, len, s_net_errno); + DBG_ERROR("sock%d short send %d/%d (errno=%d)", sockfd, sent, len, + s_net_errno_slots[net_errno_slot()].err); } /* 返回实际发送字节数(调用方必须检查,勿只看非负) */ return sent; @@ -1547,8 +1612,7 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, /* 参数检查:缓冲区无效 → INVAL */ if (buf == NULL || len <= 0) { - s_net_errno = NET_ERR_INVAL; - return -1; + return NET_ERR_RETURN_1(NET_ERR_INVAL); } /* 超时来源:调用方指定(单次预算)优先,否则用默认值(均作为挂起截止) */ @@ -1588,8 +1652,7 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, return 0; } /* 暂时无数据 → WOULDBLOCK */ - s_net_errno = NET_ERR_WOULDBLOCK; - return -1; + return NET_ERR_RETURN_1(NET_ERR_WOULDBLOCK); } /* 无数据 + 阻塞等待:登记挂起请求,立即返回(netTask 任何时刻都不被占用)。 @@ -1729,7 +1792,7 @@ static int net_relisten_locked(net_sock_t *p_sock) { DBG_INFO("sock%d relisten %s (sock=0x%02X tcp=0x%02X)", sockfd, tag, st[0], st[1]); /* 置 BUSY(结果回传调用方) */ - s_net_errno = NET_ERR_BUSY; + (void)net_set_errno(NET_ERR_BUSY); /* 交重试轮询(30ms 后复检) */ return -1; } @@ -1759,7 +1822,7 @@ static int net_relisten_locked(net_sock_t *p_sock) { /* 先关闭,交重试轮询(30ms 后复检) */ ch395f_close_socket((uint8_t)sockfd); /* 置 BUSY(结果回传调用方) */ - s_net_errno = NET_ERR_BUSY; + (void)net_set_errno(NET_ERR_BUSY); /* 交重试轮询(30ms 后复检) */ return -1; } @@ -1768,8 +1831,7 @@ static int net_relisten_locked(net_sock_t *p_sock) { /* LISTEN 失败 → BUSY(同样交重试) */ if (status != CH395F_ERR_SUCCESS) { /* 置 BUSY(结果回传调用方) */ - s_net_errno = NET_ERR_BUSY; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BUSY); } /* 状态置回 LISTENING(fd 保留) */ @@ -1881,12 +1943,11 @@ static int net_relisten_try(net_sock_t *p_sock) { /* 描述符越界 → BADF */ if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 已在重试队列:不重复尝试、不重置时刻(防重试饥饿) */ if (s_relisten_wait[sockfd] != 0U) { - s_net_errno = NET_ERR_BUSY; + (void)net_set_errno(NET_ERR_BUSY); /* 已在重试队列:由 tick 负责 */ return -1; } @@ -1994,9 +2055,9 @@ static void net_pending_complete(int sockfd, int result) { if (p->active == 0U) { return; } - /* 失败结果:同步 errno(保持 netTask 上下文一致性) */ + /* 失败结果:记入 netTask 自己的错误码槽位(E-03;不再写入跨任务共享全局) */ if (result < 0) { - s_net_errno = result; + (void)net_set_errno(result); } /* 覆盖式写入通知值 = 结果本身(32 位原值) */ xTaskNotify(p->caller, (uint32_t)result, eSetValueWithOverwrite); @@ -2028,21 +2089,18 @@ static int net_pending_register(int sockfd, void *buf, int len, /* 描述符越界 → BADF */ if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 取挂起请求 */ p = &s_net_pending[sockfd]; /* 同 fd 已有挂起:拒绝(覆盖将丢失前通知 = 该任务永久挂死) */ if (p->active != 0U) { - s_net_errno = NET_ERR_BUSY; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BUSY); } /* netTask 自身发起:无他人可完成(tick 跑在 netTask 内)→ 拒绝(自等死锁) */ if ((caller == NULL) || (caller == s_net_task_handle)) { - s_net_errno = NET_ERR_BUSY; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BUSY); } /* 当前 tick(截止/看门狗基准) */ @@ -2124,9 +2182,7 @@ static void net_pending_tick(void) { /* ② 到点(tick 差值按有符号比较,抗回绕) */ if ((int32_t)(now - p->deadline) >= 0) { - /* 置 TIMEDOUT(同步 errno) */ - s_net_errno = NET_ERR_TIMEDOUT; - /* 了结:超时 */ + /* 了结:超时(错误码由 net_pending_complete 记入本任务槽位) */ net_pending_complete(i, NET_ERR_TIMEDOUT); continue; } @@ -2158,8 +2214,7 @@ static void net_pending_tick(void) { * 正常 FIN(DISCONNECT)仍回 0(在途 FIN 与正常完成不可区分——协议边界,见 Trap 23 记录) */ /* 异常释放:以连接重置了结(回 5xx,不误报 226) */ if (p_sock->released_abnormal != 0U) { - /* 置 CONNRESET(同步 errno) */ - s_net_errno = NET_ERR_CONNRESET; + /* 异常释放(错误码由 net_pending_complete 记入本任务槽位) */ net_pending_complete(i, NET_ERR_CONNRESET); } else { /* 正常 EOF:以 0 了结 */ @@ -2176,9 +2231,7 @@ static void net_pending_tick(void) { /* 强制了结留痕(超截止时刻的毫秒数) */ DBG_ERROR("sock%d pending recv stuck %ums past deadline (force settle)", i, (unsigned)((int32_t)now - (int32_t)p->deadline)); - /* 置 TIMEDOUT(同步 errno) */ - s_net_errno = NET_ERR_TIMEDOUT; - /* 强制了结:超时 */ + /* 强制了结:超时(错误码由 net_pending_complete 记入本任务槽位) */ net_pending_complete(i, NET_ERR_TIMEDOUT); } } @@ -2360,15 +2413,13 @@ int net_send(int sockfd, const void *buf, int len, int flags) { /* ---- 1. 参数合法性检查 ---- */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { /* 描述符越界 */ - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* ---- 2. 运行环境检查:消息队列须已创建(网络层已初始化) ---- */ if (netMsgQueueHandle == NULL) { /* 网络层未初始化 */ - s_net_errno = NET_ERR; - return -1; + return NET_ERR_RETURN_1(NET_ERR); } /* ---- 3. 组装消息:把本次 send 的全部上下文填入 msg ---- */ @@ -2392,8 +2443,7 @@ int net_send(int sockfd, const void *buf, int len, int flags) { /* ---- 4. 入队:将消息投递给 netTask,osWaitForever 表示队列满时阻塞调用方 ---- */ if (osMessageQueuePut(netMsgQueueHandle, &msg, 0, osWaitForever) != osOK) { /* 入队失败(队列异常等极端情况) */ - s_net_errno = NET_ERR_BUSY; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BUSY); } /* ---- 5. 阻塞等待 netTask 处理完毕:netTask 调 net_send_locked 后通过 @@ -2405,8 +2455,7 @@ int net_send(int sockfd, const void *buf, int len, int flags) { if (msg.result < 0) { /* netTask 把具体错误码(如 NET_ERR_WOULDBLOCK)以负值搭通知值回传,此处恢复为 * errno;对外仍保持 BSD 语义——失败一律返回 -1 */ - s_net_errno = msg.result; - return -1; + return NET_ERR_RETURN_1(msg.result); } /* 返回实际发送字节数 */ return msg.result; @@ -2431,14 +2480,12 @@ int net_recv(int sockfd, void *buf, int len, int flags) { /* 描述符越界 → BADF */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 消息队列未创建(网络层未初始化)→ 通用错误 */ if (netMsgQueueHandle == NULL) { - s_net_errno = NET_ERR; - return -1; + return NET_ERR_RETURN_1(NET_ERR); } /* 消息类型:接收数据 */ @@ -2461,8 +2508,7 @@ int net_recv(int sockfd, void *buf, int len, int flags) { /* 入队:队列满时阻塞调用方直至有空位 */ if (osMessageQueuePut(netMsgQueueHandle, &msg, 0, osWaitForever) != osOK) { /* 入队失败 → BUSY */ - s_net_errno = NET_ERR_BUSY; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BUSY); } /* 阻塞等待 netTask 回执(两段式拾取,防丢失通知) */ @@ -2471,8 +2517,7 @@ int net_recv(int sockfd, void *buf, int len, int flags) { if (msg.result < 0) { /* netTask 把具体错误码(如 NET_ERR_WOULDBLOCK)以负值搭通知值回传,此处恢复为 * errno;对外仍保持 BSD 语义——失败一律返回 -1 */ - s_net_errno = msg.result; - return -1; + return NET_ERR_RETURN_1(msg.result); } /* 返回结果(接收字节数;0 = EOF) */ return msg.result; @@ -2499,23 +2544,20 @@ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) { /* ---- 1. 参数合法性检查 ---- */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { /* 描述符越界 */ - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* ---- 2. 运行环境检查:消息队列须已创建(网络层已初始化) ---- */ if (netMsgQueueHandle == NULL) { /* 网络层未初始化 */ - s_net_errno = NET_ERR; - return -1; + return NET_ERR_RETURN_1(NET_ERR); } /* ---- 3. 分片长度检查:timeout_ms = 0 在 netTask 侧意味着"改用默认 * NET_RECV_TIMEOUT_MS(30 秒)",恰是本接口要规避的长阻塞,直接拒绝 ---- */ if (timeout_ms == 0U) { /* 分片长度必须 > 0 */ - s_net_errno = NET_ERR_INVAL; - return -1; + return NET_ERR_RETURN_1(NET_ERR_INVAL); } /* ---- 4. 组装消息:把本次分片读的全部上下文填入 msg ---- */ @@ -2539,8 +2581,7 @@ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) { /* ---- 5. 入队:将消息投递给 netTask,osWaitForever 表示队列满时阻塞调用方 ---- */ if (osMessageQueuePut(netMsgQueueHandle, &msg, 0, osWaitForever) != osOK) { /* 入队失败(队列异常等极端情况) */ - s_net_errno = NET_ERR_BUSY; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BUSY); } /* ---- 6. 阻塞等待 netTask 处理完毕:netTask 执行 net_recv_locked 后经 xTaskNotify() @@ -2552,8 +2593,8 @@ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) { /* ---- 7. 回传结果(三分支:分片超时 / 真错误 / 数据或 EOF) ---- */ if (msg.result == NET_ERR_TIMEDOUT) { /* 本片超时:timeout_ms 内未等到数据,非致命 → 返回 -2 */ - /* 超时只凭返回码 -2 判定、不得依赖 errno:s_net_errno 是跨任务共享全局量,netTask 写入的值随时可能被其他任务的 net 调用覆盖 */ - /* 返回码是 netTask 经任务通知值专为本任务回传的,天然无竞态(本分支也不再改写 s_net_errno) */ + /* 超时只凭返回码 -2 判定、不得依赖 errno:返回码是 netTask 经任务通知值 + * 专为本任务回传的,天然无竞态(本分支也不改写错误码) */ /* 调用方见到 -2 后累计自身总超时预算,决定是否发起下一片 */ /* 本判断必须先于下方通用 <0 分支:NET_ERR_TIMEDOUT = -9 同样满足 <0 */ return -2; @@ -2562,8 +2603,7 @@ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) { /* 真错误:netTask 把具体错误码(如 NET_ERR_WOULDBLOCK)以负值回传, * 此处恢复为线程级错误码(可经 net_get_errno() 读取); * 对外保持 BSD 语义——失败一律返回 -1 */ - s_net_errno = msg.result; - return -1; + return NET_ERR_RETURN_1(msg.result); } /* >= 0:0 = 对端关闭(EOF,net 层尽力自动重监听,失败由重试轮询接管); @@ -2588,14 +2628,12 @@ int net_close(int sockfd) { /* 描述符越界 → BADF */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 消息队列未创建(网络层未初始化)→ 通用错误 */ if (netMsgQueueHandle == NULL) { - s_net_errno = NET_ERR; - return -1; + return NET_ERR_RETURN_1(NET_ERR); } /* 消息类型:关闭 Socket */ @@ -2612,8 +2650,7 @@ int net_close(int sockfd) { /* 入队:队列满时阻塞调用方直至有空位 */ if (osMessageQueuePut(netMsgQueueHandle, &msg, 0, osWaitForever) != osOK) { /* 入队失败 → BUSY */ - s_net_errno = NET_ERR_BUSY; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BUSY); } /* 阻塞等待 netTask 回执(两段式拾取,防丢失通知) */ @@ -2622,8 +2659,7 @@ int net_close(int sockfd) { if (msg.result < 0) { /* netTask 把具体错误码(如 NET_ERR_WOULDBLOCK)以负值搭通知值回传,此处恢复为 * errno;对外仍保持 BSD 语义——失败一律返回 -1 */ - s_net_errno = msg.result; - return -1; + return NET_ERR_RETURN_1(msg.result); } /* 关闭成功(0) */ return msg.result; @@ -2658,22 +2694,19 @@ int net_sendto(int sockfd, const void *buf, int len, int flags, /* 参数检查 */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { /* 描述符越界 → BADF */ - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 取控制块 */ p_sock = &s_net_socks[sockfd]; /* 控制块未在用 → BADF */ if (!p_sock->in_use) { - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 缓冲区无效 → INVAL */ if (buf == NULL || len <= 0) { - s_net_errno = NET_ERR_INVAL; - return -1; + return NET_ERR_RETURN_1(NET_ERR_INVAL); } /* UDP 模式:设置目标地址 */ @@ -2717,7 +2750,7 @@ int net_sendto(int sockfd, const void *buf, int len, int flags, if (NET_SEND_TIMEOUT_MS > 0 && (HAL_GetTick() - tick_start) >= NET_SEND_TIMEOUT_MS) { DBG_ERROR("sock%d send stall: send_ready not set (%d/%d)", sockfd, sent, len); - s_net_errno = NET_ERR_TIMEDOUT; + (void)net_set_errno(NET_ERR_TIMEDOUT); break; } /* 轮询一次,下一轮再试 */ @@ -2753,7 +2786,8 @@ int net_sendto(int sockfd, const void *buf, int len, int flags, if (sent < len) { /* 短写告警:调用方必须检查返回值。静默截断会让对端永久等待剩余字节 * (2026-09-21 实证:UI 推流应答被截断 → 客户端反复判定"全程无字节"失联)。 */ - DBG_ERROR("sock%d short send %d/%d (errno=%d)", sockfd, sent, len, s_net_errno); + DBG_ERROR("sock%d short send %d/%d (errno=%d)", sockfd, sent, len, + s_net_errno_slots[net_errno_slot()].err); } /* 返回实际发送字节数(调用方必须检查) */ return sent; @@ -2783,22 +2817,19 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags, /* 参数检查 */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { /* 描述符越界 → BADF */ - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 取控制块 */ p_sock = &s_net_socks[sockfd]; /* 控制块未在用 → BADF */ if (!p_sock->in_use) { - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 缓冲区无效 → INVAL */ if (buf == NULL || len <= 0) { - s_net_errno = NET_ERR_INVAL; - return -1; + return NET_ERR_RETURN_1(NET_ERR_INVAL); } /* 记录起始时刻(超时基准) */ @@ -2859,16 +2890,14 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags, /* 非阻塞模式:无数据立即返回 */ if (flags & NET_MSG_DONTWAIT) { /* 置 WOULDBLOCK */ - s_net_errno = NET_ERR_WOULDBLOCK; - return -1; + return NET_ERR_RETURN_1(NET_ERR_WOULDBLOCK); } /* 超时检查 */ if (NET_RECV_TIMEOUT_MS > 0 && (HAL_GetTick() - tick_start) >= NET_RECV_TIMEOUT_MS) { /* 置 TIMEDOUT */ - s_net_errno = NET_ERR_TIMEDOUT; - return -1; + return NET_ERR_RETURN_1(NET_ERR_TIMEDOUT); } } } @@ -2886,8 +2915,7 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags, int net_send_sock(net_sock_t *p_sock, const void *buf, int len) { /* 控制块指针为空 → BADF */ if (p_sock == NULL) { - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 直接委托锁定实现(netTask 上下文,无队列往返) */ return net_send_locked(p_sock, buf, len); @@ -2910,8 +2938,7 @@ int net_send_sock(net_sock_t *p_sock, const void *buf, int len) { int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags) { /* 控制块指针为空 → BADF */ if (p_sock == NULL) { - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 直接委托锁定实现(netTask 上下文,无队列往返); * 0 = 默认 NET_RECV_TIMEOUT_MS;caller = netTask 自身(自调用) */ @@ -2930,8 +2957,7 @@ int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags) { int net_close_sock(net_sock_t *p_sock) { /* 控制块指针为空 → BADF */ if (p_sock == NULL) { - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 直接委托锁定实现(netTask 上下文,无队列往返) */ return net_close_locked(p_sock); @@ -2956,16 +2982,14 @@ int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg) { /* 参数检查 */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { /* 描述符越界 → BADF */ - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 取控制块 */ p_sock = &s_net_socks[sockfd]; /* 控制块未在用 → BADF */ if (!p_sock->in_use) { - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 注册回调函数(NULL = 取消注册) */ @@ -2977,17 +3001,19 @@ int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg) { return 0; } /* - * 函数功能:获取 Socket 最后一次操作返回的错误码 + * 函数功能:获取 **本任务** 最后一次 net_*() 调用失败时的精确错误码 * 入口参数:无 * 出口参数:无 - * 返回值:当前错误码值(NET_OK=0, NET_ERR_xxx=-N) int - * 限定条件:无 - * 函数说明:每次 net_*() API 调用失败后,错误码被设置。调用成功时不自动清零 + * 返回值:错误码(NET_OK=0, NET_ERR_xxx=-N);本任务无失败记录时为 NET_OK + * 限定条件:无(任何上下文可调用;中断上下文读 [0] 槽) + * 函数说明:1. E-03 修复:错误码**按任务隔离**——只读本任务自己的槽位, + * 其他任务的 net 调用再也覆盖不了它(旧实现为单一全局量,跨任务竞态) + * 2. 调用成功时**不清零**(保持历史语义,向后兼容既有调用方) + * 3. 超时场景(net_recv_to)请直接用返回码 -2 判定,不要依赖本接口 */ - int net_get_errno(void) { - /* 返回当前错误码(只读快照,成功时不自动清零) */ - return s_net_errno; + /* 返回本任务槽位的错误码(只读快照;无槽位时回退 [0] 槽) */ + return s_net_errno_slots[net_errno_slot()].err; } /* @@ -3029,14 +3055,12 @@ int net_resync_listen(int sockfd) { /* 描述符越界 → BADF */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 消息队列未创建(网络层未初始化)→ 通用错误 */ if (netMsgQueueHandle == NULL) { - s_net_errno = NET_ERR; - return -1; + return NET_ERR_RETURN_1(NET_ERR); } /* 消息类型:断连重同步(MCU 侧收回 LISTENING 并重建监听) */ @@ -3053,8 +3077,7 @@ int net_resync_listen(int sockfd) { /* 入队:队列满时阻塞调用方直至有空位 */ if (osMessageQueuePut(netMsgQueueHandle, &msg, 0, osWaitForever) != osOK) { /* 入队失败 → BUSY */ - s_net_errno = NET_ERR_BUSY; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BUSY); } /* 阻塞等待 netTask 回执(两段式拾取,防丢失通知) */ @@ -3062,8 +3085,7 @@ int net_resync_listen(int sockfd) { /* 失败:恢复 errno(对外统一 -1) */ if (msg.result < 0) { - s_net_errno = msg.result; - return -1; + return NET_ERR_RETURN_1(msg.result); } /* 重同步成功(0) */ return 0; @@ -3087,14 +3109,12 @@ int net_relisten(int sockfd) { /* 描述符越界 → BADF */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 消息队列未创建(网络层未初始化)→ 通用错误 */ if (netMsgQueueHandle == NULL) { - s_net_errno = NET_ERR; - return -1; + return NET_ERR_RETURN_1(NET_ERR); } /* 消息类型:无条件重新监听(含 Trap 09 非阻塞重试) */ @@ -3111,8 +3131,7 @@ int net_relisten(int sockfd) { /* 入队:队列满时阻塞调用方直至有空位 */ if (osMessageQueuePut(netMsgQueueHandle, &msg, 0, osWaitForever) != osOK) { /* 入队失败 → BUSY */ - s_net_errno = NET_ERR_BUSY; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BUSY); } /* 阻塞等待 netTask 回执(两段式拾取,防丢失通知) */ @@ -3120,8 +3139,7 @@ int net_relisten(int sockfd) { /* 失败:恢复 errno(对外统一 -1) */ if (msg.result < 0) { - s_net_errno = msg.result; - return -1; + return NET_ERR_RETURN_1(msg.result); } /* 重监听成功(0) */ return 0; @@ -3608,22 +3626,19 @@ net_sock_t *net_get_sock(int sockfd) { int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen) { /* 描述符越界 → BADF */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 控制块(const:本函数只读 Socket 信息) */ const net_sock_t *p_sock = &s_net_socks[sockfd]; /* 未在用且状态 CLOSED → BADF */ if (!p_sock->in_use && p_sock->state == NET_SOCK_STATE_CLOSED) { - s_net_errno = NET_ERR_BADF; - return -1; + return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 输出参数无效(NULL 或长度不足)→ INVAL */ if (addr == NULL ||addrlen == NULL || *addrlen < (int)sizeof(struct net_sockaddr_in)) { - s_net_errno = NET_ERR_INVAL; - return -1; + return NET_ERR_RETURN_1(NET_ERR_INVAL); } /* 强转为 IPv4 地址结构体 */ diff --git a/Drivers/BSP/NET/net_socket.h b/Drivers/BSP/NET/net_socket.h index d11c12b0..d28c0b34 100644 --- a/Drivers/BSP/NET/net_socket.h +++ b/Drivers/BSP/NET/net_socket.h @@ -261,8 +261,18 @@ int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg); */ /* - * 函数功能:获取 socket 最后错误码 - * 返回值:错误码(NET_ERR_xxx) + * 函数功能:获取 **本任务** 最后一次 net_*() 调用失败时的精确错误码 + * 入口参数:无 + * 出口参数:无 + * 返回值:错误码(NET_ERR_xxx);本任务尚无失败记录时为 NET_OK(0) + * 限定条件:无(任何上下文可调用,含中断——查询为纯本任务索引读) + * 函数说明:1. **按任务隔离**(2026-09-25 E-03 修复):错误码存在每个任务自己的 + * 槽位里,任务间互不干扰,不存在跨任务覆写竞态——旧实现的单一全局 + * 变量会被 netTask/其他任务的 net 调用交错覆盖,调用方可能读到别人的码 + * 2. 语义:本任务最后一次返回 -1 的 net_*() 调用所对应的精确码; + * 调用成功时**不清零**(与本 API 历史行为一致,保持向后兼容) + * 3. 任务句柄槽位用尽时回退到共享槽位(仅影响排障,不影响功能) + * 4. 超时类场景(net_recv_to)请直接用返回码 -2 判定,不要依赖本接口 */ int net_get_errno(void); diff --git a/docs/待处理内容.md b/docs/待处理内容.md index 251be716..4fe4a81f 100644 --- a/docs/待处理内容.md +++ b/docs/待处理内容.md @@ -27,4 +27,4 @@ | # | 事项 | 说明 | 出处 | |---|---|---|---| | N-4 | **pending 机制二期:ACCEPT + SEND 阻塞路径 pending 化**——E-06(accept 轮询计时失真)、E-20(lftpd 控制通道 errno 分类/501)、E-21(timeSync errno 分级)剩余分量共用该机制扩展;届时 L3/L5 基线再重锁、探针族扩充(close/reconfig/EOF/disconnect-during-pending)、看门狗接 selftest 留痕、模拟器差距清单 +1 | 一期(RECV)已落地:`f16dc9a`;设计基线与门禁结果见 `docs/问题记录/Net层阻塞模型业界对照与演进方案.md` §6.7 | 同左 + `错误码传播审计.md` 第二档 #1 | -| N-5 | E-03(`s_net_errno` 跨任务覆写竞态)——pending 化不改变 errno 通道现状;与 N-4 二期或独立小修(错误码经通知值直回,包装器去全局) | 既有 P1 | `错误码传播审计.md` E-03 | +| N-5 | **✅ 已修复(2026-09-25):E-03 `s_net_errno` 跨任务覆写竞态**——采取"**按任务隔离**"路线(而非二期 pending 化):删除全局 `s_net_errno`,改为 `s_net_errno_slots[8]`(每槽 `{任务句柄, err}`);`net_errno_slot()` 无锁快路径只读命中本任务槽、首次占用在 `taskENTER_CRITICAL` 内复查(槽位用尽回退 [0] 槽,**不新增共享槽**以免把竞态带回来);`net_set_errno()` 取代全部赋值、`NET_ERR_RETURN_1()` 宏合并"记码+return -1"共 82 处;`net_get_errno()` 改读本任务槽位、**对外语义不变**(成功不清零)。构建 0 错 0 警 + cppcheck 无告警 + `git diff --check` 干净 + 编码无 `U+FFFD`,BSS +64B。**残留台架项**:本机 COM6 未枚举(ST-Link 虚拟串口不在)→ 硬件闭环回归待补(`错误码传播审计.md` §3.1 E-03 / 页脚) | 2026-09-25 修复 | `错误码传播审计.md` E-03;`Drivers/BSP/NET/net_socket.c`(`net_errno_slot` / `net_set_errno` / `net_get_errno`) | diff --git a/docs/问题记录/错误码传播审计.md b/docs/问题记录/错误码传播审计.md index 86be7bd4..d00c8e78 100644 --- a/docs/问题记录/错误码传播审计.md +++ b/docs/问题记录/错误码传播审计.md @@ -1,542 +1,592 @@ # 错误码传播审计报告(全项目) -> **模块名称**:全项目错误码/返回值/状态传播审计 -> **文档目的**:以 2026-09-21 网络错误码事故(errno 覆盖 → 远端显示误断连)为起点,对全项目做系统性 -> 错误码传播缺陷普查:哪些机制会把"成功"传给调用方(而状态实际是失败)、哪些通道会让错误码丢失/串扰/ -> 永不可达,并按 P0/P1/P2 给出修复计划。**本文档只读审计,不修改任何代码。** -> **日期**:2026-09-21 -> **审计方法**:按 9 类缺陷模式(P1 值拷贝队列陷阱 / P2 共享全局错误变量竞态 / P3 返回值与错误码混同 / -> P4 IPC/HAL 状态未检查 / P5 事件标志语义 / P6 通知值宽度与符号 / P7 ISR 与跨上下文共享状态 / P8 外设与 -> 库特定约定 / P9 超时与错误混同)逐子系统审查;三个并行审计组(配置/日志 IPC、驱动层、net 消费方/模拟器) -> + 父审计组(net 核心/事件标志/UI/启动路径)交叉覆盖,**全部关键发现经父审计组逐行代码复核**(含芯片手册 -> 原文核对)后才入册。 -> **配套文档**:`docs/项目审计改进清单.md`(交叉引用见 §6)、`docs/问题记录/*_Trap_Records.md`(模块陷阱史)、 -> `docs/网络配置说明.md`、`AGENTS.md`(09-21 教训已固化条目)、`docs/问题记录/Net层阻塞模型业界对照与演进方案.md` -> (E-02 配套:业界模式对照 + pending 请求演进方案利弊/改动量,第二档 #1 排期时用)。 +> **模块名称**:全项目错误码 / 返回值 / 状态传播审计 +> **文档目的**:以 2026-09-21 网络错误码事故(errno 被并发覆写 → 远端显示误判断连)为起点, +> 普查全项目的"错误信息传递"链路:哪些机制会把**成功**报给调用方而实际已失败、哪些通道会让 +> 错误码**丢失 / 串扰 / 永远到不了**,并按 P0/P1/P2 排出修复优先级。 +> **本文档只读审计,不修改任何代码。** +> +> **首版**:2026-09-21(发现 59 项) +> **本版**:2026-09-25 重写 —— ① 已闭环项从正文移除,只在 §2 留一行索引;② 全部未闭环项 +> **逐项回到当前代码复核**;③ 新增本轮复核发现。 +> **代码基线**:`v2-on-ui` @ `9a8255f`(2026-09-24),复核日期 2026-09-25 +> **配套文档**:`docs/待处理内容.md`(N-2/N-4/N-5 登记项)、`docs/问题记录/Net层阻塞模型业界对照与演进方案.md`(E-02 配套)、 +> `docs/问题记录/*_Trap_Records.md`(模块陷阱史)、`AGENTS.md`(已固化铁律) --- -## 0. 摘要 +## 0. 结论速览 -**结论**:09-21 的确定性缺陷族(值拷贝回传字段、就绪位消费、EOF 关监听)已修复并在位;本次全项目普查 -**新确认 59 项**错误码传播缺陷:**P0×2、P1×22、P2×35**。2 项 P0 均为"值说成功、状态说失败"的静默 -数据丢失路径,均可在生产触发且当前零告警。(2026-09-21 复核修订:E-32 经手册 §4.8"出厂配置 82H"证伪由 P0 降 P2; -E-60"write_enable WRTC2 与手册矛盾"经三方复核(手册表头 D7~D0 顺序 + DFRobot 官方库/社区代码同序列 0x10←0x80、0x0F←0x84)证伪——0x84 本已含 D2=WRTC2,代码与手册一致,详见 §7-19;E-31 同日修复(commit da1a6e8:resume 失败按错误分类、仅真空白判新卷,其余失败自检上报 + 保留卷 + 主循环有界重试;详见 §3.1/§5 第一档 #2)) +**当前未闭环 25 项:P1×5、P2×20。无 P0。** -| 级别 | 含义 | 数量 | 编号 | +首版 59 项中 **34 项已闭环**(见 §2),其中 3 项 P0 全部修复并在位。剩余问题分三类: + +| 类别 | 数量 | 性质 | 处置 | |---|---|---|---| -| P0 | 生产可触发的活跃缺陷(静默数据丢失) | 2 | E-01, E-31 | -| P1 | 故障条件下的潜在陷阱(触发需特定时序/工况,后果重) | 22 | E-02~E-10, E-33~E-45 | -| P2 | 健壮性/一致性/可维护性 | 35 | E-11~E-30, E-32, E-46~E-59 | +| **A. 待处理的实质缺陷** | 10 | 有明确后果,需改代码 | §3.1,第一 / 二档 | +| **B. 契约缺口(随消费者接入触发)** | 7 | 当前调用者"恰好不会踩",但契约没写死 | §3.2,接消费者前必修 | +| **C. 一致性 / 可维护性** | 8 | 无功能影响或影响极小 | §3.3,第三档 | -### P0 速览 +**最值得先动的两件事**: -| 编号 | 位置 | 一句话 | -|---|---|---| -| E-01 | `App/cfg_file.c:1151` | `cfg_file_poll` 的"文件超长=损坏"检查是死代码(读长被钉死在阈值)→ >4096B 配置文件被截断解析当有效 → 界外配置静默回工厂默认 → 基线粘住错误状态 → 下次开机 load 守卫判"损坏" → **用户配置文件整体销毁**。**已修复(2026-09-22,commit d030cb0)**:poll 超长判定基于 `st.size` 前置到 `s_load_file` 之前(根治死码),走"工厂默认+重建模板+自检留痕"且刻意不触发段回调(保留在线修复通道);save 输出上限收紧 4096 + `s_write_doc` 写盘唯一闸口硬拒超长;load/poll 超长分支补自检留痕。台架验证 PASS;验证中另暴露并修复 `cfg_store_write_atomic` 的 `f_rename` 不覆盖既有目标缺陷(commit 70f1cc8,见 E-01 明细"关联新发现") | -| E-31 | `Drivers/BSP/GD5F2GQ5UE/nand_ftl.c:250-252` | `dhara_map_resume` **任何**失败(含单次瞬态页读失败)→ `dhara_map_clear` → **整卷逻辑映射静默丢弃** → FatFs 探针判卷无效 → 整卷重格式化 → 全部用户数据丢失;dhara 本可区分"真空白"(合法)与 ECC/损坏(必须上报),glue 层把错误分类全坍缩。**已修复(2026-09-21,commit da1a6e8)**:resume 失败按 `dhara_error_t` 分类(仅真空白判 fresh)、其余失败 mount 层自检上报 + 保留卷 + 主循环有界重试 | +1. **E-34(P1)**——CH395F 位命令 SPI 全部用 `HAL_MAX_DELAY`。SPI 硬件异常时会让 netTask + **永久挂死**(无看门狗兜底),代价是整机网络死亡;改有限超时是一行级别的改动。 +2. **E-04(P1)**——`remo_send` 丢弃 `net_send` 返回值。整帧 3204B 远超单次发送上限, + 弱网下短写是**合法返回**,当前完全无感知 → 客户端 CRC 失败断连。 -### P1 速览(22 项) - -| 编号 | 子系统 | 一句话 | -|---|---|---| -| E-02 | net 执行器 | **队列头阻塞**:任一消费者的阻塞 recv(30s)在 netTask 上下文空转 → 全体消费者的队列消息停摆最长 30s(事件仍处理);FTP 客户端空闲 >30s 即可触发。**两阶段闭环(2026-09-22)**:阶段一 `net_recv_to` 5ms 分片有界读缓解(lftpd f8d6e90 / timeSync 4a1605b);阶段二 **L3 pending 化**(f16dc9a)——阻塞 RECV 改挂起请求、执行器零占用零阻塞、调用方分片协议退役;台架全量实证(明细见 §3.2) | -| E-03 | net 层 | 全局 `s_net_errno` 跨任务覆写竞态(09-21 修复的**残留**):恢复与读取之间隔任务切换,健康连接可被误拆(ui_sink :460 每 10ms 读) | -| E-04 | UI 远端推流 | `remo_send` 丢弃 `net_send` 返回值 → 3205B 响应弱网短发送**静默截断**(09-21 事故机制的调用方一侧未闭环) | -| E-05 | net 层 | 3 任务直调 `net_socket/net_bind/net_getsockname` 跨任务写 netTask 属主的 socket 表 → alloc/close 交错可双分同一 fd(SPI 铁律未破,表属主破了) | -| E-06 | lftpd | `lftpd_inet_accept_timeout`:`elapsed+=100` 计时在队列阻塞下失真(名义 5s → 实际十几分钟)+ 跨任务 state 预检误判 → PASV 偶发假 425 | -| E-07 | 存储 | `f_close`(本移植 FTL 的提交点)返回值 3 处丢弃 → 配置原子写/日志追加**假成功**(f_write 可 FR_OK 而数据仅在 cache),掉电静默丢失 | -| E-08 | 日志 | preboot 日志数组"单生产者免锁"前提在内核后不成立(队列惰性创建前,多任务并发无同步读-改-写)→ 自检记录丢失/重复 | -| E-09 | 配置 | UI 读-改-写 RAM 配置副本 × defaultTask poll 原地更新竞态 → 陈旧写回可抹掉 FTP 外部编辑(P1-3 残余,从引擎静态态转移到 RAM 层) | -| E-10 | NAND | init 单次无重试 + 内部超时/扫描失败全吞 → 冷启动瞬态 SPI 毛刺 = 好芯片判"NAND fault"整次开机无存储;反向慢芯片 = 假 OK + 垃圾坏块表 | -| E-33 | CH395F | DMA 数据路径 `void` 无错误通道:HAL 失败/1s 超时静默丢发;**收路径无条件 memcpy 陈旧 DMA 缓冲冒充新数据** | -| E-34 | CH395F | 字节级命令路径 `HAL_MAX_DELAY`(无限超时)且返回值未检查 → SPI2 状态机卡死 = netTask 永久挂死(系统无看门狗) | -| E-35 | 测试 | CH395F 套件 Phase5 块长 4093B > DMA 缓冲 2052B(MSS 2048→1024 后常量未同步)→ 整块静默丢弃,失败签名误导为 "rx stall" → 质量门禁失效 | -| E-36 | RS485 | ISR 重武装清零 `rx_size` → 文档化的 `rs485_rx_get_size` 对任务**恒返回 0**("无数据"误判族;消费者休眠中,首个接入即触发) | -| E-37 | RS485 | `rs485_init` 吞掉 RX 武装失败(`(void)`)→ UART 状态异常时 485 接收终身静默死亡 | -| E-38 | SD2506 | 负温度解码 `(int8_t)(val\|0xF0)` 在 ≤-17°C 全错(芯片用二补码;-30°C 会报 -14°C)且返回 0=OK 无指示(=既有 P2-5,本次升级 P1) | -| E-39 | SD2506 | ARST 在 CTR3(11H) bit7,驱动**从未写过 CTR3**(全库 grep 证实)→ ARST 恒为复位值 0 → `clear_alarm` 的读清除契约**永久不可达**(首次报警后 INTAF 永久锁存,API 返回 0 但什么都没清) | -| E-40 | FTL | `dhara_nand_erase/prog` 把**瞬态**(BUSY_TIMEOUT/ERROR)与单元缺陷(E_FAIL/P_FAIL)混同为坏块并持久化 → 容量永久损失(09-21 容量 95248→95200 事故的机制性同根) | -| E-41 | GD5F | `gd5f_bbt_save` 编程失败仍推进 BBT 版本并返回 OK(`void` 化 API 结构性丢失持久化失败信息)(=既有 P2-3) | -| E-42 | GD5F | `gd5f_bbt_scan` 两个读返回值全弃 → 坏块判定基于上一块/未初始化 `spare[0]`(好块判坏或坏块放行) | -| E-43 | GD5F | `gd5f2gq5ue_erase` 缺容量边界检查(read/write 有,Trap 03 文档与代码不符)→ 越界 erase 的行地址回绕可**静默擦掉块 0**(journal/BTT 所在) | -| E-44 | TPAFE | `read_all` 无任何数据有效性校验——头文件承诺的 FRSTDATA 首通道校验**未实现**(宏有定义、代码零引用)→ 芯片死亡时返回 OK + 常量垃圾 | -| E-45 | TPAFE | `s_ready` 三写者:`start_conv_irq` 触发前置 0 → 重触发即清除未消费帧(电平标志坍缩,高速采样系统性丢样) | +> **2026-09-25 变更**:**E-03 已修复**(错误码改按任务隔离,见 §3.1)。 +> 它是本次审计中唯一"并发条件下会说谎"的通道,且 pending 二期不覆盖它,故独立小修先行。 --- -## 1. 背景与方法 +## 1. 审计方法与口径 -### 1.1 起点 +### 1.1 九类缺陷模式 -2026-09-21 台架事故:`net_msg_t` 按值拷入队列 → netTask 对 `result` 字段的回写在调用方不可见 → -"错误码"读到的是栈垃圾(恒 0)→ `ui_net_sink` 把每次空轮询的 `WOULDBLOCK` 误判为真错误 → 远端连接 -建立即被 `net_close`(监听 fd 同毁,端口"连接被拒",RemoDispBus 7003 无画面)。修复(`c5a9e8c`/ -`a9aa8ce`):错误码改搭 `xTaskNotify()` 通知值回传(失败=负的具体错误码,成功=字节数/描述符),包装器 -在**调用方上下文**恢复全局 errno,成功路径不清 errno。AGENTS.md 已固化"勿给 `net_msg_t` 加回传字段"铁律。 +所有发现按这九类归类,编号 P1~P9(与发现编号 E-xx 区分): -本次审计回答的问题是:**同一个"值说成功、状态说失败"的缺陷族,在项目的其他子系统里还有多少活体?** - -### 1.2 缺陷模式定义(审计用分类法) - -| 模式 | 定义 | 09-21 原型 | -|---|---|---| -| P1 | 值拷贝队列陷阱:结构体按值入队,消费端字段回写对生产端不可见(指针字段安全) | `net_msg_t.result` 回传字段 | -| P2 | 共享全局错误变量跨任务竞态:写者与读者不在同一执行流,中间隔任务切换 | `s_net_errno` | -| P3 | 返回值/错误码混同:0 重载、短写、多套码系(FR_/HAL/负 errno/芯片原生码)互译缺失 | "FR_OK=成功"掩盖短写 | -| P4 | IPC/HAL 状态未检查:队列 put、HAL 调用、FTL 提交的返回值被丢弃或 `(void)` 化 | — | -| P5 | 事件标志语义错位:状态位被消费型等待清掉 / 命令位被状态型等待挂着 | `NET_READY` 被 `WaitAll` 消费 | -| P6 | 通知值宽度/符号/通道碰撞 | 通知值复用 | -| P7 | ISR/跨上下文共享状态无同步(表属主、双缓冲、重武装竞态) | — | -| P8 | 外设/库特定约定违背(写保护、提交点、DMA 缓冲契约、字面量方言) | FatFs FA_OPEN_APPEND 无 FA_WRITE | -| P9 | 超时与错误混同(超时判成功/判错误、无限超时、超时处置副作用) | — | - -### 1.3 覆盖范围与复核方式 - -| 审计组 | 范围 | 状态 | -|---|---|---| -| 父审计组 | `net_socket.c` 全文(1979 行)、`net_select.c`、`ui_task.c`、`net_task.c`、`freertos.c`、`main.c`、UI 层(`ui_port.c`/`ui_net_sink.c`/`ui_page_net_cfg.c`/`ui_page_runlog`/`ui_page_selftest`)、cmsis_os2.c 事件组实现、`lv_conf.h` | 完成(自审) | -| SA1 | `cfg_file.c`/`cfg_file_store_mcu.c` 全文、`sys_log_mcu.c`、`ringbuf.c`、`app_cfg.c`/`app_main.c` 配置段、`freertos.c` defaultTask 全路径 | 完成,关键项已复核 | -| SA2 | `ch395f.c/h`、`rs485.c/h`、`sd2506.c/h`、`gd5f2gq5ue.c/h`、`nand_ftl.c`、`tpafe5160.c/h` 全文 + `Lib/dhara`(error/journal/map)、HAL UART/SPI 错误路径、`app_main.c` 诊断段、三份 Trap_Records | 完成,关键项已复核 | -| SA3 | lftpd 5 文件、`time_sync.c`、`ui_net_sink*`、`test/net_test_task.c`、`simulator/sim/net_socket_sim.c` + 全库 grep 消费方地图 | 完成,关键项已复核 | - -复核方式:每项 P0/P1 的**核心机制**由父审计组独立读码/读手册确认后才入册(本文件 §3 各条的 -"位置"行即为复核落点);P2 条目采信审计组带行号的报告。全库 grep 核验了全部 `net_*`/`osEvent*`/ -`xTaskNotify`/`f_open|close|write`/`0xNNH` 字面量调用面。 - ---- - -## 2. 机制基线(各子系统错误码传播契约,审计基准) - -| 子系统 | 现行契约(修复后状态) | 审计基准 | -|---|---|---| -| **net 层** | 队列消息只携带**输入**(fd/长度/指针);错误码经通知值回传(负=错误码,≥0=字节数/fd);包装器仅**失败**时在调用方上下文恢复 `s_net_errno`,成功**永不**清除(`net_socket.c:1609` 文档化);`net_msg_t` 禁止加回传字段(AGENTS.md 铁律);指针字段安全(`net_accept` 地址回写为先例) | P1 族已消除;残留 = P2 全局竞态(E-03)与直调 API 的表属主(E-05) | -| **事件标志** | CMSIS 事件组 = FreeRTOS 事件组(`xEventGroupWaitBits`);**就绪位=状态位**须 `osFlagsNoClear`;**命令位**(CFG_SAVE/NET_RECONFIG)=可消费,单一属主 | 5 处屏障等待全部 NoClear 在位(§7-2);命令位消费存在非原子窗口(E-17,无载荷→自愈) | -| **配置引擎** | 静态态单属主(defaultTask);队列条目**纯输入型**(段号+路径指针,无回传字段)→ 结构性免疫 P1;CFG_SAVE = 可消费唤醒位 + `s_save_pending` volatile 锁存 + `s_save_result` 单字节状态机回传 UI | 全时序窗口(含 <500ms 双保存合并)无丢失(§7-3);RAM 副本读-改-写竞态残留(E-09) | -| **syslog** | 队列条目纯输入型(id + 静态串指针);defaultTask 唯一写者;内核前 post 走 `s_preboot[8]` 静态数组 → 队列惰性创建(首次 drain 时) | 输入型结构免疫 P1;preboot 数组内核后并发写(E-08) | -| **CH395F** | socket 命令回芯片原生 8 位状态(0x00 成功 / 0x10 BUSY 兼"进行中"与"轮询超时");init 族三值 OK/TIMEOUT/ERROR;**DMA 数据路径 `void` 无错误通道**;字节路径返回值未检查 + `HAL_MAX_DELAY` | E-33/E-34/E-46/E-47 | -| **RS485** | `HAL_StatusTypeDef` 返回;RX 数据**只能**经 `rs485_on_rx` ISR 钩子观察(getter 恒 0);init 吞武装结果 | E-36/E-37/E-48~E-50 | -| **SD2506(RTC)** | int 返回:0=OK / -1=参数 / -2=I2C;**0 仅表 I2C 事务成功 ≠ 芯片接受了写**(WRTC 写保护层,驱动无回读校验);上电默认写禁止;18H 充电寄存器**非易失**(出厂 82H、上电即充电使能);write_enable 序列(0x10←0x80、0x0F←0x84,三者 WRTC 全 1)已三方复核与手册 L392 一致(原 E-60"矛盾"证伪,§7-19) | E-32(P2)/E-38/E-39/E-51/E-52 | -| **GD5F + FTL** | int32 码:0 / -1 ERROR / -2 BUSY_TIMEOUT / -3 ECC / -4 PROGRAM_FAIL / -5 ERASE_FAIL / -6 ID_MISMATCH;**仅 -4/-5 是单元缺陷(可判坏块),-1/-2 是瞬态**;`dhara_error_t` 分类完整(TOO_BAD/ECC/CORRUPT_MAP…)但 **glue 层映射坍缩**;`f_close` = FTL 提交点 | E-31/E-40/E-41/E-42/E-43/E-53~E-57 | -| **TPAFE5160** | 阻塞 API:0 / -1 参数 / -2 BUSY(10ms,唯一硬超时);IRQ 模式 = ISR 双缓冲 + `s_ready`;**FRSTDATA 校验承诺未实现** | E-44/E-45/E-58 | -| **net 消费方** | lftpd(队列+直调混合,控制通道阻塞 recv);timeSync(全队列,从不读 errno → 免疫 E-03);ui_sink(队列 + DONTWAIT + NoClear + EOF 不关监听 = 全项目唯一完全合规消费方,作修复参照);test(直调+队列混合) | E-02/E-04/E-06/E-20~E-24 | -| **模拟器** | 单线程 Winsock 直调,无队列/线程/阻塞模式/代次/回调触发;成功即清 errno(与 MCU 相反) | 16 项保真度差异(附录 D);09-21 类缺陷在 sim **结构性不可复现** | - ---- - -## 3. 发现明细 - -> 每条:位置(复核落点)→ 模式 → 触发 → 影响 → 修复方向。P2 条目压缩为一行式。 - -### 3.1 P0(生产可触发,静默数据丢失) - -#### E-01 `cfg_file_poll` 超长文件检查是死代码 → 用户配置文件销毁链 -- **位置**:`App/cfg_file.c:1151`(`if (len > CFG_FILE_MAX_LEN)`);机制根 `:504`(`s_load_file` 读长钉死 `CFG_FILE_MAX_LEN`,`cfg_file.h:26` =4096);对照组:load 守卫 `:984-993`、save 守卫 `:1032-1041` 均在位,**唯 poll 缺** -- **模式**:P3(阈值检查写在恒假的位置) -- **触发**:`/config.ini` 超过 4096B——文档化的 FTP 编辑工作流 + 外科手术式保存补写(`:774-797`,输出上限 `CFG_OUT_MAX`=5120 > 4096,无回检)都可达 -- **影响链**(已逐环复核):poll 读被截断的前 4096B → 界外段/键当"缺失"静默回工厂默认(`:1164-1183` 原地段复位 + 重解析)→ 回调触发网络按工厂 IP 重建 → 基线刷新(`:1189-1197`)把错误状态"粘住"→ 任何后续保存只按 RAM 副本(截断视图)重写 → **界外内容永久丢失** → 下次开机 `cfg_file_load` 的 `st.size > 4096` 守卫判"损坏" → **整份用户文件销毁**重生工厂模板。全程零错误日志 -- **修复方向**:poll 在 `s_load_file` **之前**查 `cfg_store_stat` 的 `st.size > CFG_FILE_MAX_LEN`,走与 load 相同的"工厂默认 + 重建模板 + **自检留痕**"分支;save 路径加 `out_len ≤ CFG_FILE_MAX_LEN` 守卫(超限拒绝保存并报错,而非写出下一颗雷) -- **已修复(2026-09-22,commit d030cb0,第一档 #3)**:① `cfg_file_poll` 沉降分支超长判定改用 `cfg_store_stat` 真实 `st.size` 且**先于** `s_load_file`(旧检查写在截断后的 `len` 上恒假=死码),命中走"各段复位工厂默认 + `s_write_template()` 重建 + `DBG_ERROR`(含实际字节数)+ `syslog_post(SYSLOG_SELFTEST, "cfg poll oversized")`",**刻意不触发段回调**——避免按工厂 IP 重建网络造成"静默失联",RAM=工厂与硬件现状的分歧在下次保存/重启收敛(重启必收敛:load 读到的已是重建后的合法模板);`p_buf/len` 声明收进正常分支,超长路径省一次 4KB 分配 ② `cfg_file_save`:`s_surgical` 输出上限 `CFG_OUT_MAX`(5120) 收紧至 `CFG_FILE_MAX_LEN`(4096),复用既有溢出中止路径(原文件不动 + 自检 `cfg save overflow`),消除"合法写出 4097~5120B 文件 = 下次开机被销毁的雷" ③ `s_write_doc`(模板/外科手术唯一落盘出口)加 `len > CFG_FILE_MAX_LEN` 硬拒——"引擎永不产出超长 config.ini"成为结构性质,新增调用路径免疫 ④ load 超长分支补 `syslog_post(SYSLOG_SELFTEST, "cfg load oversized")`(销毁用户文件属重大事件,不能只留串口)。`cfg_file.h` v1.2 契约同步(load 第 4 条 / save / poll 说明)。**台架验证(2026-09-22,commit d030cb0+70f1cc8 构建)**:FTP STOR 5047B → 4s 后 `[ERR][CFG] poll: file 5047B > 4096B, corrupt, factory template regenerated`,设备保持在线(无网络重建),RETR 实测文件已重建为 523B 工厂模板;重启 load 守卫同链触发(`load: file 5047B > 4096B...`);再重启 `[CFG] load: OK (523B)` 零 `[ERR]` 无销毁循环;`selftest.log` 落 `cfg poll oversized`/`cfg load oversized` 留痕;模拟器 `--cfgtest` 两个新回归锁场景(伪造 4160B → poll 判损坏重建 + 4000B 注释文件 → save 拒写且原文件分毫未动,修复前必挂)PASS;`@test` 默认闭环 PASS + ST-LINK 全片回读一致 -- **关联新发现(E-01 台架验证暴露,已修复 2026-09-22,commit 70f1cc8)**:`App/cfg_file_store_mcu.c` `cfg_store_write_atomic`——本 FatFs 移植的 `f_rename` 对已存在目标返回 `FR_EXIST`、不覆盖(`Lib/FatFs/ff.c:5235-5238` "new name already in use" 检查,上游同语义)→ 设备端**一切"目标存在的覆盖写"(UI 保存 / 模板重建)静默失败**,仅首启建文件(目标不存在)成功;模拟器后端为宿主 rename(POSIX 覆盖语义),cfgtest 照不出。首轮台架证据:poll 守卫触发后 selftest 落 `cfg template write fail`、RETR 文件仍 5047B。修复 = rename 前先 `f_unlink` 既有目标(非原子窗口掉电 = 文件暂失,开机按首启重建工厂模板,与函数失败语义一致)。本缺陷恰位于 E-01 自愈路径上(模板重建即覆盖写),不修则 poll 守卫只能报无法愈;与 E-07(`f_close` 返回值丢弃)同属原子写可靠性链 - -#### E-31 FTL:`dhara_map_resume` 任何失败 → 整卷映射静默丢弃 -- **位置**:`Drivers/BSP/GD5F2GQ5UE/nand_ftl.c:250-252`(`if (dhara_map_resume(&s_map, &err) < 0) { DBG_INFO("No valid map found..."); dhara_map_clear(&s_map); }`);错误源 `Lib/dhara/journal.c:222-248`(find_checkblock 逐块扫描,单次读失败透传 `*err` 并继续,任一子步骤失败 → resume 返回 -1);分类能力 `Lib/dhara/error.h:20-31` -- **模式**:P3(错误类别坍缩)+ P8(软错误升级为终态数据丢失事件) -- **触发**:resume 扫描(大量页读)中**任何一次**瞬态读失败——ECC glitch / SPI 瞬断 / WIP 轮询超时(2026-09-20 台架瞬态读异常,其低 VDD 归因与"混合页"解读后续均被证伪——见 GD5F Trap 05;曾为它引入的 2ms settle 已于 2026-09-21 移除,就绪判定回归 WIP 轮询) -- **影响**:全部逻辑→物理映射静默丢弃 → FatFs 探针判卷无效 → **整卷重格式化**(2026-09-20 `.fschk` 重格式化循环为同族事故)→ 全部用户数据丢失;仅一行 info 级日志。dhara 本可区分 `DHARA_E_TOO_BAD`(真空白芯片,正当 fresh)与 `DHARA_E_ECC`/`BAD_BLOCK`/`CORRUPT_MAP`(真实完整性故障)——glue 层丢弃了全部区分信息 -- **修复方向**:按 `err` 分类——仅 `DHARA_E_TOO_BAD`/无 checkpoint 判 fresh;其余错误**返回失败给 mount 层**,由自检上报、禁止静默 `dhara_map_clear`(重格式化须显式确认/自愈策略明确) -- **已修复(2026-09-21,commit da1a6e8,第一档 #2)**:① `Lib/dhara/journal.c` 错误源治理——`find_checkblock`/`find_root` 不再把扫描中的读失败覆盖为 `DHARA_E_TOO_BAD`(扫描出现过读失败 → 上报首个 I/O 错误如 ECC;仅扫描全程 I/O 干净且无 checkpoint 才报 `TOO_BAD`);`read_checkpage`/`find_root` 的"读成功但 checkpoint magic/epoch 不符"改报 `DHARA_E_CORRUPT_MAP`(该点之前 checkpoint 环的存在性已被证明,"空白卷"不成立)→ resume 任何 -1 返回值都携带可分类的真实错误码 ② `nand_ftl.c` `disk_initialize` 分类:仅 `err == DHARA_E_TOO_BAD`(真空白卷)走 fresh(journal 已被失败 resume 复位为空态);其余失败 `return STA_NOINIT` 给 mount 层——禁止静默清 map,卷原样保留 → `f_mount` 失败(FR_NOT_READY)→ defaultTask 自检 `filesystem fault` ③ `freertos.c` 明确自愈策略:原启动块重构为 `storage_bring_up()`(挂载+空卷建卷+两击探针+自愈重格式化+日志挂载+配置加载+就绪屏障,均幂等);启动期失败后主循环每 2s 重试(`STORAGE_BRINGUP_RETRIES=3`),成功即补跑"存储就绪"序列(瞬态毛刺数秒内免重启恢复);耗尽重试维持无存储降级态(自检已上报、卷保留、不重格式化,等下次上电重新 resume)。构建 0 错误 0 警告;产品固件台架回归 PASS(2026-09-21 19:19,commit da1a6e8 构建:`@test` 闭环 Required 全中/Forbidden 零命中,`Map resumed: 2441/96064 sectors used` 既有卷 resume 成功、`FATFS mounted`、`[CFG] load: OK (523B)` 用户配置保留、`[FTP] waiting for connection` 屏障链通、ST-LINK 全片回读 No difference);残余特性说明见下条 -- **残余特性(修复范围外,留档)**:`find_checkblock` 首扫窗口 = 块 0 起至多 `DHARA_MAX_RETRIES`(8) 块——环已回卷且回卷后 journal 尚未写满一个 checkpoint 周期(<32 页/64KB)即掉电的窄窗口内,checkpoint 均不在块 0–7 → 扫描 I/O 干净但找不到 checkpoint → 仍判 `TOO_BAD` fresh(与修复前行为一致,非瞬态读失败触发);根治需扩大首扫窗口/环定位,属独立设计项(与 E-10/E-53 驱动错误通道统一一并评估) - -### 3.2 P1(22 项) - -#### E-02 netTask 队列头阻塞:一个消费者的阻塞调用冻结全体 -- **位置**:`Drivers/BSP/NET/net_socket.c:357`(`net_process_messages` 一次取一条,处理完才取下一条)、`:1019-1067`(`net_recv_locked` 阻塞等待循环,30s 超时在 netTask 内空转)、`:961-984`(send 每 chunk 1s 停滞等待);触发源 `App/time_sync.c:84`(flags=0 阻塞 recv)、`lftpd_inet.c:106-109`(read_line 阻塞 recv——**FTP 客户端控制连接空闲 >30s 即触发,常见工况**)、`:135/:156`(阻塞 send) -- **模式**:P4 变体(串行化头阻塞,非丢消息) -- **影响**:最长 30s 全网队列停摆——uiTask 100Hz 轮询、FTP 收发/accept、timeSync 全部排在队列里得不到处理;事件处理不受影响(等待循环内仍调 `net_poll`,`:1020`),受影响的**恰是队列**;与 E-06 叠加 → PASV 假 425;STOR 上传方向:台架实证(2026-09-22,Trap 21)实际行为为"缓冲恰好填满 + 排空"后**芯片 RX 窗口静默冻结**(不 RST/不拆连、数据永不被确认、不自恢复;唯一恢复 = 断连 + 客户端重试)——原"重传耗尽必断"预测不成立(芯片自身发送侧干净,拆连逻辑不触发),结论方向(STOR 方向 = 致命)成立 -- **修复方向**:消费端阻塞 recv 改 `DONTWAIT` 分片 + 调用方重投 + 总超时(ui_sink 模式推广到 lftpd/timeSync);或 `net_process_messages` 在长阻塞间隙按 tick 继续排空队列;`NET_RECV_TIMEOUT_MS` 按服务可容忍量级分离(既有 P2-8 是本条的子集) -- **已修复(2026-09-22,两阶段)**: - **阶段一(缓解,两提交)**:落地方案为方向①的变体——"分片"用**有界阻塞片**而非 DONTWAIT 重投:新增 `net_recv_to` 接口(`msg.timeout_ms` 携带片长;片超时以独立返回码 -2 区分,无跨任务 errno 竞态);调用方累计总预算、片间 `vTaskDelay(1)` 让出 CPU(防 Normal 优先级忙循环饿死 BelowNormal 的 uiTask,M3 实证);5ms 片长 = 其他任务最坏等待(100ms 片下受压帧 ~750ms、5ms 片 ~10ms,`docs/测试说明/链路延迟测试说明.md` §8.3);30s 总预算保持旧超时语义 - ① **lftpd 侧(commit f8d6e90,M3 台架复验)**:`lftpd_inet_read_line`/`lftpd_inet_read` 的 30s 阻塞 recv 改 5ms 分片 + 30s 总预算;M3 压力档(空闲 FTP 半登录态)帧周期 med 17718ms/尖峰 17.7s → **med 65.0ms/max 133.5ms/零尖峰** - ② **timeSync 侧(commit 4a1605b)**:`time_sync_handle_conn` 单次 30s 阻塞 `net_recv` 改同模式(5ms 片/30s 总预算/跨片累积容忍 TCP 分段/超时留痕 `time_sync: no TIME packet in 30s, drop conn`);产品构建 `@test` 闭环 PASS(Required 全中/Forbidden 零命中) - **A/B 台架对照(`test/e02_probe.py`,2026-09-22 13:07~13:16,设备 192.168.1.100)**:挂起客户端连 :8888 后不发数据,2s 后 PC 测 FTP(:21) banner 时延 = 队列响应性——旧固件 **28554ms**(队头冻结;设备日志实证芯片侧握手与 MCU 侧 "connection received" 相隔 27s;旧代码 30s 后静默拆连**零日志**)vs 修复后 **109ms(产品构建)/75ms(测试构建)**;30s 预算耗尽拆连后监听均经 2 次 Trap 09 重试 ~8s 恢复(两侧行为同构,非本修复引入);修复版超时留痕在位、正常路径(8B TIME 包)`wall clock updated` + RTC 回写 + 监听重建全通。(注:旧固件轮的芯片侧监听状态为上次烧录遗留——ST-LINK 仅复位 MCU、CH395F 状态延续,不影响队列冻结证据,冻结主体为旧固件 netTask 的阻塞 recv) - **阶段二(结构闭环,commit f16dc9a,分支 wip/v2-pending-recv,2026-09-22)**:**L3 pending 化**——阻塞 RECV 消息改为**挂起请求**,执行器(netTask)不再为任何调用方阻塞:`net_recv_locked` 阻塞分支在数据不可得时登记 `net_pending_t`(每 fd 至多一个,不覆盖;`caller==NULL`/`caller==netTask` 拒绝自通知死锁)即返回哨兵 `NET_PENDING_REGISTERED`(0x70000000,dispatch 跳过即时通知);完成判定收敛到主循环节拍 `net_pending_tick`(每次 `net_process_messages` 各跑一遍):① 数据(readable 门控 SPI 读,ESTABLISHED/UDP_OPEN)② 截止(`NET_RECV_TIMEOUT_MS` 30s,状态无关、有符号回绕安全差值)→ TIMEDOUT ③ CLOSED:排空→数据否则 EOF(0)+auto-relisten ④ 他态等待(不做状态推断取消)⑤ 1s 看门狗强结算 + DBG_ERROR;`net_init`/`net_close`/RESYNC_LISTEN 陈旧分支/`net_socket` 的 cancel/settle 钩子**一律通知**——调用方永不悬空。调用方(lftpd/timeSync)同步退役分片协议:改为"剩余总预算"循环(`rem = 预算 - 已耗时` 作每次片长,`net_recv_to` 无数据即睡在任务通知上,醒即续——片间 `vTaskDelay(1)` 不再需要)。公开 API/返回语义(>0/0/-1/-2)与"通知值唯一回传通道"不变;pending 表独立静态数组(close 时 memset 的 `net_sock_t` 之外),仿 D10 relisten 表模式。 - **台架全量复验(2026-09-22 下午,pending 构建,分支 wip/v2-pending-recv)**: - - **e02_probe**:队列响应性 **92.5ms**(阶段一 109ms/75ms → 更快,执行器占用归零);30s 截止按节拍精确触发(30.1s,`no TIME packet in 30s, drop conn` 留痕在位);拆连后监听经 2 次 Trap 09 重试 6.6s 恢复(既有芯片行为,与阶段一同构);TIME 正常路径 `wall clock updated` PASS - - **M3 基线重锁(Phase 14,静默 + FTP 压力两档全 PASS)**:静默档 host 帧 med 58.9/p95 69.9/max 72.1ms、零尖峰;**压力档(空闲 FTP 登录头阻塞工况实发)med 60.1/p95 66.4/max 69.3ms/零尖峰**——对照阶段一基线 65.0/133.5,p99 尾部压缩近半;MCU 侧 `prod_cycle_mcu` med 6706µs 在压力下纹丝不动(min 6698/max 6720)——**结构性收益:执行器零占用,头阻塞工况不再向 UI 帧周期传导** - - **功能套件(Phase 6/9/11 组合 + Phase 8-RECV 独占组合,分两次构建)**:64B/1460B 回显 11/11、select 回显 11/11、断开重连、KeepAlive 断连 + 自动重监听全 PASS;**Phase 8-RECV 100KB 阻塞接收(pending 主路径)50/50 块(2048B/块)逐块校验 + CRC32 整包一致**(0x9A0E0C8C),收齐后主动 close + auto-relisten 正常 - - **产品构建 `@test`**:PASS(Required 全中/Forbidden 零命中);netTask 运行栈峰值 **488B/4096(余量 88%)**、静态最坏链 432B——零占用设计实测落点 - - **RAM/Flash 代价(map A/B,同一测试配置)**:SRAM +200B(192B `s_net_pending[8]` + 4B `s_net_task_handle` + 4B 他项)、Flash +608B(code +528/RO +80)——远低于内存预算阈值 - - **台架新发现(与 L3 无关,A/B 归因后转 `docs/待处理内容.md` N-1/N-2)**:① TC-NET-604 已结案为**非缺陷**(2026-09-23:真实生产路径 227 轮全过;原台架"停摆"系测试程序自吞数据 + 判读工具误判,见 Trap 21);② 台架运行中一次 49s 全系统日志静默(net 栈期间照常处理连接 = defaultTask/NAND syslog 排空停滞,既有存储侧现象,L3 无责) - -#### E-03 全局 `s_net_errno` 跨任务覆写竞态(09-21 修复的残留) -- **位置**:写方 = `net_socket.c:39` 单全局;netTask 侧 `:362/365/375/385/410` + **notify 值构造 `:419-423`**(`s_notified` 分支读同一全局);6 个队列包装器在**调用方上下文** `:660/758/818/1233/1286/1333`(仅失败路径写);6 个直调 API 在**调用方上下文**失败时写(`net_socket :442/449`、`net_bind :482-538`、`net_sendto/net_recvfrom`、`net_getsockname :1953-1964`),**成功不写** → stale 滞留(`:1609` 文档化)。读端:`ui_net_sink.c:460`(uiTask 100Hz,BelowNormal 被抢占窗口最大)、`test/net_test_task.c:230/381` -- **模式**:P2 -- **触发**:包装器写回 errno → 调用方读之间的任何任务切换窗口内,netTask 处理下一条消息(任何其它消费者失败)或他任务直调 API 失败 → 覆写 -- **影响**:uiTask 空闲轮询的 `WOULDBLOCK`(正常态)被误读为真错误 → `net_close(g_client)` **拆掉健康远端连接**(单连接 = 监听同毁 → 端口拒连);测试判读漂移(flaky)。09-21 主失败模式(空轮询误判)已除,跨任务覆写仍在 -- **修复方向**:错误码不再经共享全局——包装器把通知值里的精确码经**出参/返回值**直接交回调用方(通道已有,只差"还原到调用方栈");netTask 侧 locked 函数改以局部变量/出参携带结果码(消除 `:419-423` 的全局读) - -#### E-04 `remo_send` 丢弃 `net_send` 返回值 → 弱网静默截断 -- **位置**:`Drivers/BSP/UI/ui_net_sink.c:117`(`net_send(g_client, p, 4+len+1, 0);` 返回值未用) -- **模式**:P3(假定 `net_send` 恰好返回 len) -- **触发**:LCDMEM 响应 3205B > `NET_SEND_CHUNK_MAX`(1KB),须 4 个 chunk;客户端拥塞时某 chunk 停滞 1s(`NET_SEND_TIMEOUT_MS`)→ 短写/0(net 层已打 "short send" 日志,`net_socket.c:987-992`)→ 响应帧截断 -- **影响**:客户端 CRC 不过判"全程无字节" → 断连;**无应用层日志、无客户端清理**。= 09-21 生产事故的机制在调用方一侧仍未闭环 -- **修复方向**:校验 `ret == len`;短发送 → `DBG_ERROR` + `net_close(g_client)` 强制客户端重连补帧(或入队重试),至少让截断可观测 - -#### E-05 socket 表跨任务写竞态(表属主规则被直调 API 打破) -- **位置**:直调点 `lftpd_inet.c:13,25,50`、`lftpd.c:398,640`、`time_sync.c:40,53`、`ui_net_sink.c:350,360`(`net_socket`/`net_bind`/`net_getsockname`,3 个并发任务);被侵入结构 `net_socket.c:38`(`s_net_socks[]`)、`:1732-1742`(`alloc_socket` 线性扫描置 `in_use`)、`:42/:503-509`(`s_dynamic_port` 全局计数器)、`:436-462/:476-548`(写控制块);对照注释 `:346`("独占 CH395F 和 s_net_socks 访问权") -- **模式**:P7(表属主变体)。**已逐函数核验:TCP 直调路径不触 SPI**(`net_bind` 仅 UDP 分支开 socket 走 SPI,`:511-542`)→ SPI 铁律未破 -- **触发**:`alloc_socket` 扫描/置位 与 netTask 的 `net_close_locked`(`:1167` memset 整块)或事件写 state 在同一 tick 边界交错(ftp/timeSync 同为 Normal 优先级,可时间片抢占) -- **影响**:双任务分到同一 fd,或刚释放的 fd 被复用而芯片侧尚未 CLOSED → 两个服务配置同一 CH395F socket → 偶发"端口行为错乱/连接串扰",难复现 -- **修复方向**:`net_socket`/`net_bind`/`net_getsockname` 并入队列(新增消息类型,与 `net_listen` 同模式);或至少 alloc+init 段临界区 + `s_dynamic_port` 收归 netTask - -#### E-06 `lftpd_inet_accept_timeout` 计时失真 + 跨任务状态预检 -- **位置**:`Drivers/BSP/NET/lftpd/lftpd_inet.c:77-98`(`:93` `elapsed += 100`;`:87-91` `net_get_sock` + `state != LISTENING` 预检) -- **模式**:P3 + P2 -- **触发**:(a) `elapsed+=100` 假定每轮 ~100ms,而 `net_accept` 是队列往返——被 E-02 冻结时单次可达 30s → 名义 5s 超时实际**十几分钟**;(b) ftpTask 跨任务读 netTask 属主表:数据客户端"连接即断"时 state=CLOSED(auto_relisten 尚未执行)的窗口内被读到 → 误判 "data socket closed" → 提前 -1 → **425 假失败** -- **修复方向**:超时改墙钟(`HAL_GetTick` 起点)计量;去掉/降级 state 直读(以 accept 返回值 + WOULDBLOCK 为准;保留则经队列或临界区快照) - -#### E-07 `f_close`(FTL 提交点)返回值 3 处丢弃 → 假成功 -- **位置**:`App/cfg_file_store_mcu.c:83`(原子写:`/config.TMP` + `f_write` 后 `(void)f_close`,只查 fr/bw 就 rename 转正——本移植 `f_write` 可 FR_OK 而数据仅在 cache,**close 才经 `ftl_flush_cache` → dhara 提交**);`App/log/sys_log_mcu.c:110,117`(rc 判定只覆盖 `f_write`+bw → 假成功还**误清 `s_fault_reported` 去重标志** `:127`,后续真失败被静默);`Src/freertos.c:278`(卷探针 `(void)f_close(&__chk)` → "假健康"窗口) -- **模式**:P4 + P8 -- **触发**:瞬态 NAND 写失败(2026-09-20 台架实证,供电实测 3.3V 标称;原"欠压工况"表述已更正) -- **影响**:配置/日志报"成功"而数据未提交 → **掉电即静默丢失**;日志去重被假成功解除 → 真故障不报 -- **修复方向**:close 返回非 FR_OK 即判写失败(配置:unlink 临时文件、不 rename;日志:不清去重标志;探针:归一 FR_DENIED) - -#### E-08 preboot 日志数组并发写 -- **位置**:`App/log/sys_log_mcu.c:39-40`(`s_preboot[8]` + volatile cnt)、`:218-223`(队列**惰性创建**于 `syslog_drain`,defaultTask 上下文,`Src/freertos.c:378` 首调)、`:265-273`(preboot 分支无同步读-改-写) -- **模式**:P7 -- **触发**:内核启动后 ~ 首次 drain 前(defaultTask 存储 bring-up 慢时可达数十秒)的 post 落 preboot 数组——netTask 10s 屏障超时后的 "comms fault"(`net_task.c:126`)与 defaultTask 启动段失败 post(`freertos.c:316/322/343`)**同处"无队列时代"并发** -- **影响**:自检记录丢失/重复(有界,NULL 守卫挡崩溃)——恰是最需要日志的故障窗口 -- **修复方向**:preboot 分支加临界区;或队列创建提前(首个任务上下文的 post 点前) - -#### E-09 UI 读-改-写 × poll 原地更新竞态(P1-3 残余) -- **位置**:`Drivers/BSP/UI/ui_page_net_cfg.c:181`(`net_cfg_t cfg = *net_cfg_get();`)→ 改一字节 → `:186`(`net_cfg_set(&cfg)`,`net_cfg.c:296-301` 整 19B 无锁 memcpy 到 `&s_cfg`)× defaultTask poll 的段原地更新(`cfg_file.c:1171-1182`,`s_parse` 直接写 `p_data=&s_cfg`) -- **模式**:P7(跨任务无同步读-改-写) -- **触发**:FTP 外部编辑(poll 原地刷新 RAM 副本)与 UI 页面读-写窗口跨唤醒 tick -- **影响**:陈旧写回**抹掉外部编辑** → 下次保存旧值落盘 → 外部变更永久丢失 -- **修复方向**:UI 页面用局部副本 + 显式保存提交(保存时才合并写回);或写回前基线比对(检测到副本过期则放弃/提示) - -#### E-10 NAND init 单次无重试 + 内部错误吞 -- **位置**:`App/app_main.c:64-75`(`gd5f2gq5ue_init` 单次调用);`gd5f2gq5ue.c:615-668`(init 内 read_id 恒返 OK、唯一真失败=ID 校验)、`:519-520`(bbt_scan 读失败吞)、`:651/655/659`(`wait_busy(100)` 返回值吞 ×3)、`:653-655`(ECC 使能未校验 → E-54) -- **模式**:P4 + P9 -- **影响**:冷启动瞬态 SPI 毛刺 = 好芯片判 "NAND fault" 整次开机无存储(无重试);反向慢芯片 = 假 OK + 垃圾 BBT(E-42) -- **修复方向**:init(或 reset + ID 读)重试 1~2 次;`wait_busy`/`bbt_scan`/ECC 使能 错误向上传递(与 E-42/E-53/E-54 同批改造) - -#### E-33 CH395F DMA 路径 `void` 化:失败不可见,收路径冒充新数据 -- **位置**:`Drivers/BSP/CH395F/ch395f.c:89-135`(DMA 传输:HAL 错误 → log+return;1s 超时 → log + `HAL_SPI_Abort` + return;DMA 错误完成 → **仅 DBG_INFO 当正常** `:130-134`);`:1117-1147`(`write_send_buf` void → `net_socket.c:972-975` 仍计该块已发);`:1161-1191`(`read_recv_buf` void;`:1190` memcpy **无条件**执行 → 失败时调用方缓冲得到 `s_spi2_dma_rx_buf` 的**陈旧内容**) -- **模式**:P4 + P8(与 09-21 生产 bug 同族:状态对调用方不可见) -- **影响**:发送侧整块数据静默丢失;接收侧陈旧字节当"新数据" → 静默数据损坏(TCP 可掩盖、UDP 丢包);日志有 ERROR 行但**无程序化错误码**,net 层对调用方返回"成功" -- **修复方向**:DMA 传输返回状态(或可查询错误标志 + 序号);失败时 `net_send`/`net_recv` 返回 -1;错误完成分支 INFO→ERROR - -#### E-34 CH395F 字节级路径:`HAL_MAX_DELAY` 无限超时 + 返回值未检查 -- **位置**:`ch395f.c:181`(write_cmd)、`:195`(write_data)、`:210`(read_data)——均 `HAL_SPI_TransmitReceive(..., HAL_MAX_DELAY)` 且返回值丢弃 -- **模式**:P9(无限等待)+ P4 -- **触发**:DMA 错误路径 `HAL_SPI_Abort`(`:124`)后 SPI2 状态机残留卡死 → 下一次字节级命令**永久阻塞** -- **影响**:一次 SPI 故障 = netTask 挂死 = 全网瘫痪(系统无看门狗);开机 init/check_exist 路径同样暴露 -- **修复方向**:改有限超时(100~500ms,与 DMA 路径同量级)并检查返回值,失败向上返回错误 - -#### E-35 测试 Phase5 块长 4093B > DMA 缓冲 2052B → 静默丢弃 + 误导签名 -- **位置**:`test/ch395f_test_task.c:1089`(`PHASE5_CHUNK_SIZE 4093`,注释仍写 "4096B/4KB" `:1214/:1285`)、`:1214`(TX 单次写 4093B)、`:1284-1287`(RX 单次读);驱动守卫 `ch395f.c:1126-1129/:1170-1173`(`len+4 > CH395F_DMA_BUF_SIZE(2052)` → **静默 return 无日志**) -- **模式**:P3(契约漂移)× P4(静默丢弃) -- **触发**:MSS 2048→1024(DMA 缓冲 4100→2052B)后测试常量未同步;台架跑硬件层套件即触发 -- **影响**:每个 TX 块被守卫丢弃 → 芯片发送缓冲恒空 → SENE 恒满足 → TX 毫秒"完成"(吞吐行失真)→ PC 端无数据 → RX 5s 后报 "rx stall" → **失败签名指向 RX,真因(TX 静默丢弃)不可见** → 质量门禁失效并误导排障 -- **修复方向**:测试常量对齐 ≤2048;驱动超尺寸守卫加 DBG 告警并把上限写入头文件契约(现仅文档化 NULL/len>0) - -#### E-36 RS485:ISR 重武装清零 `rx_size` → getter 恒 0 -- **位置**:`Drivers/BSP/RS485/rs485.c:108`(`receive_start` 内 `rx_size = 0`)、`:151-161`(RxEventCallback:set_size → on_rx 钩子 → `receive_start` 清零)、契约 `rs485.h:100-105` -- **模式**:P3(文档化契约结构性不可达——"无数据"误判族) -- **影响**:getter 承诺"任务可获取最近一次接收字节数",实际 ISR 内在钩子后几 µs 就清零 → **任何任务时刻调用恒为 0** → 首个按文档 API 轮询"新帧"的消费者将永远"无数据" → 485 帧结构性不可见。当前 `rs485_task` 为空桩而休眠(E-59) -- **修复方向**:重武装不清零(双缓冲/帧序号);或契约改为"仅 on_rx 钩子内有效"并在头文件明示 - -#### E-37 RS485:`rs485_init` 吞掉 RX 武装失败 -- **位置**:`rs485.c:52`(`(void)rs485_receive_start(p_handle)`);`rs485_init` 返回 `void` -- **模式**:P4(init 路径也漏) -- **影响**:开机时 UART4 状态非 READY(MspInit 异常/重复 init)→ `HAL_UARTEx_ReceiveToIdle_IT` 返回 BUSY/ERROR → RX 从未武装 → **485 接收终身静默死亡、零日志** -- **修复方向**:检查返回值、失败记 FAIL;init 契约改返回码 - -#### E-38 SD2506:负温度解码 ≤-17°C 全错(=既有 P2-5,升级 P1) -- **位置**:`Drivers/BSP/SD2506/sd2506.c:300`(`*p_temp = (int8_t)(val | 0xF0U)` 负分支) -- **模式**:P6(符号转换) -- **机制**:手册 L322 证明芯片用二补码("TEMP=FEH → -2℃");`|0xF0` 技巧仅在 val 高半字节已为 0xF(-16~-1)时成立。-30°C(0xE2) → 0xF2 → 报 -14°C;-20°C → 报 -4°C -- **影响**:设备工作温度 -30~+80°C → 低温环境读数恒错且**返回 0=OK 无指示**(现消费者=开机日志) -- **修复方向**:统一 `*p_temp = (int8_t)val` - -#### E-39 SD2506:ARST 在 CTR3(11H),驱动从未写过 CTR3 → `clear_alarm` 永久不可达 -- **位置**:手册 4.1 寄存器表:**ARST = CTR3(11H) D7**(D6=F32K/D5=TDS1/D4=TDS0/D3~D0=FS3~FS0,复位值 0000-0000);`sd2506.h:55` 定义了 `SD2506_REG_CTR3=0x11` 但**全驱动零次写 CTR3**(grep 证实);`clear_alarm` 契约 `sd2506.h:261-267`、实现 `sd2506.c:470-478`(依赖"ARST=1 时读 CTR1 自动清 INTAF",手册 L398) -- **模式**:P3(契约不可达)/ P8 -- **影响**:ARST 恒为复位值 0 → 报警匹配后 INTAF 锁存且读不自动清除 → 首次报警后 INTAF **永久锁存**;`clear_alarm` 成为"返回 0 但什么都没清"的空操作;INT 脚单事件报警模式下锁存低电平不释放 -- **修复方向**:在写允许窗口置 ARST=1(`write_enable` → 写 CTR3=0x80 → `write_disable`);注意 11H 属 00H~71H 保护段,直接写无效(写保护机制已三方复核确认,见 §7-19);若项目确认不使用 RTC 报警,改在契约中明示"报警功能未启用"并评估移除 - -#### E-40 FTL:erase/prog 任何失败(含瞬态)→ 判坏块并持久化 -- **位置**:`nand_ftl.c:104-113`(`dhara_nand_erase`:`ret != GD5F_OK` → `mark_bad` + `DHARA_E_BAD_BLOCK`——前件 `ret==GD5F_ERASE_FAIL` 被后件完全包含,即**一切失败**都判坏块)、`:121-132`(`program_load/exec` 失败 → mark_bad,同坍缩)、`:141-164`(回读/ECC/数据不符 → mark_bad) -- **模式**:P3(瞬态/终态混淆) -- **影响**:健康块因 `GD5F_BUSY_TIMEOUT`/`GD5F_ERROR` 被**永久除名**并持久化进 BBT 池 → 容量永久损失(2026-09-21 "一轮破坏性 GD5F 测试容量 95248→95200" 的机制性同根);每次误标记触发 BBT 池整块擦写(任务上下文数秒 SPI 阻塞) -- **修复方向**:仅 `GD5F_ERASE_FAIL`/`GD5F_PROGRAM_FAIL`(E_FAIL/P_FAIL 状态位 = 真实单元缺陷)映射坏块;timeout/ERROR 返回 `DHARA_E_ERASE`/`DHARA_E_IO` 由 dhara 重试或上报 - -#### E-41 `gd5f_bbt_save` 编程失败仍返回 OK 并推进版本(=既有 P2-3) -- **位置**:`gd5f2gq5ue.c:1009-1016`(`write_enable/program_load/program_exec/wait_busy` 返回值全丢弃 → `s_bbt_version=ver`、槽前进、`return GD5F_OK`);上游 `gd5f2gq5ue_mark_block_bad` 为 `void`(`gd5f2gq5ue.h:189`) -- **模式**:P4 + P8(void 契约隐藏失败) -- **影响**:BBT 池块编程失败 → RAM BBT 说"此块坏"、闪存副本陈旧 → **重启后 dhara 重新分配该坏块** → 写入静默丢失;调用方结构性无法得知持久化失败 -- **修复方向**:检查编程结果返回错误码;`mark_block_bad` 改返回码;失败重试下一槽(槽轮换框架已存在 `:984-989`) - -#### E-42 `gd5f_bbt_scan` 弃两读返回值 → 基于陈旧缓冲判块 -- **位置**:`gd5f2gq5ue.c:516-526`(`:519` `gd5f_page_read`、`:520` `gd5f_read_from_cache` 返回值均弃;`:522` 判 `spare[0]`——首次迭代未初始化、之后为上一块残留) -- **模式**:P4 -- **影响**:2048 块出厂扫描中任一读失败 → 该块判定基于垃圾(好块判坏 = 容量损失;坏块判好 = 未来数据丢失);缓解:与持久化副本 OR 合并(`:960-969`)兜住已持久化坏块,风险集中首启 -- **修复方向**:检查两读返回值,失败按保守方向(标坏)或重读 - -#### E-43 `gd5f2gq5ue_erase` 缺容量边界检查(与 Trap 03 文档矛盾) -- **位置**:`gd5f2gq5ue.c:783-806`(仅块对齐检查;read/write 均有 `offset+size ≤ GD5F_TOTAL_SIZE`,`:705-707/:748-750`) -- **模式**:P3(契约/文档分歧) -- **触发**:`offset = GD5F_TOTAL_SIZE` → `block_addr=2048` → 24 位行地址(block×64)被芯片解码回范围内块(2048≡0 mod 2048)→ **可能静默擦掉块 0**(journal/BTT 所在) -- **影响**:越界擦除命中活块 = 数据丢失;或芯片忽略 = 静默 no-op。`GD5F_Trap_Records.md` Trap 03 称"三个函数入口统一加边界检查"——代码与文档不符,文档高估了修复范围 -- **修复方向**:erase 补同一容量检查(对齐检查之后);更正 Trap 文档 - -#### E-44 TPAFE:`read_all` 无数据有效性校验(承诺的 FRSTDATA 校验未实现) -- **位置**:`Drivers/BSP/TPAFE5160/tpafe5160.c:151-168`(`read_all`:start_conv → wait_busy(10ms) → read_channels → 无条件 `return OK`);`tpafe5160.h:132-133`(承诺"通过 FRSTDATA 验证第一通道");`TPAFE5160_FRSTDATA_READ` 宏(`h:37`)有定义、**代码零引用**(全库 grep 证实) -- **模式**:P8 + P9 -- **触发**:芯片掉电/虚焊/总线冲突 → BUSY 引脚读低/浮空 → wait_busy 立即"通过" → RD 脉冲读回三态总线钳位态(0xFFFF/0x0000 常量) -- **影响**:上层把常量垃圾当有效采样 → 设备"测量正常"数值全错("无数据误判"的镜像) -- **修复方向**:实现 FRSTDATA 首通道校验(首次 RD 后 FRSTDATA 应为高,否则报错),或至少拒绝全 0/全 1 - -#### E-45 TPAFE:`s_ready` 三写者 → 重触发即丢未消费帧 -- **位置**:`tpafe5160.c:288-291`(`start_conv_irq` 在 CONVST 脉冲前 `s_ready=0`)、`:311-313`(`clear_ready`,任务上下文)、`:354-355`(ISR 置 1) -- **模式**:P5(事件标志语义:电平非计数) -- **影响**:帧 N 完成置 ready → 消费者未及读取 → 下一圈触发清零 → 帧 N **永久丢失**;高速采样系统性丢样,上层无法区分"无新数据"与"数据丢了"(现 adc_task 空桩休眠,E-59) -- **修复方向**:从 `start_conv_irq` 移除 `s_ready=0`(生命周期归消费者)或改帧序号制 - -### 3.3 P2(35 项,一行式) - -**配置/日志/事件** - -| 编号 | 位置 | 问题 | 修复方向 | +| 模式 | 名字 | 一句话 | 典型症状 | |---|---|---|---| -| E-11 | `cfg_file.c:1147-1149` | poll 读失败静默 return 无限重试、无日志(load/save 同场景均有日志+自检) | 加日志 + 连续 N 次转自检 | -| E-12 | `ffconf.h:39`(`FF_FS_LOCK=0`)× `sys_log_mcu.c:100-125` | /log 同文件双开:defaultTask 写(容量满 unlink 重建)× uiTask 浏览读 → 孤儿读/乱码/瞬态 FR_INT_ERR(=既有 P2-10,复核一致) | `FF_FS_LOCK` 或队列化读 | -| E-13 | `freertos.c:200` | `osEventFlagsNew` 返回值不检查 → NULL 句柄时屏障全失效 + defaultTask 失 500ms 节拍紧密循环刷 NAND,零日志 | 检查 + FAIL + 自检 | -| E-14 | `net_task.c:78-80/181-184/205-209` | net init/重建失败仅串口日志,无持久自检记录(对照 PHY 有 comms fault、存储有 storage fault)→ 现场网络故障无痕迹(=既有 P1 级可观测性缺口) | `syslog_post(SYSLOG_SELFTEST,…)` | -| E-15 | `ringbuf.c:38-76` | API 陷阱:满/空 len=0 不可区分、短写静默、无溢出计数(现零调用者,休眠) | 修复契约或移除 | -| E-16 | `cfg_file.c:463/475/907-926` | `s_gen_document` 不查 snprintf 返回值 + 注册不限键名长度 → 未来长键名可致模板缓冲越界 | 检查返回值 + 注册限长 | -| E-17 | `net_task.c:201-203` | RECONFIG `Get`+`Clear` 非原子 TOCTOU(=既有 P2-11)——**本次重评**:命令无载荷(执行读最新 RAM 副本、代次在执行时递增),丢失 = 本次重建跳过、下次保存/重启自愈,无数据丢失 → 维持清单、风险低于原评级 | `osEventFlagsWait` 原子消费 | -| E-18 | `freertos.c:422-424` | ftpTask `WaitAll(osWaitForever)` 无超时 → 生产端故障时 FTP 永久静默阻塞 | 超时 + 降级日志 | -| E-19 | `freertos.c:430`;`sys_events.h:28-29` | 死事件位:`FTP_READY` 置位在 `lftpd_start`(无限循环)返回后 = **不可达**;`TIMESYNC_READY` 从未置位 → 未来消费者 Will 永久阻塞 | 删除或改在首次监听成功后置位 | -| E-20 | `lftpd_inet.c:100-127`、`lftpd.c:555-561` | 控制通道 30s 阻塞 recv 兼存活判定 + 行>511B/EOF/真错误同返 -1 → 慢客户端被断、长命令杀会话、叠加 E-02 | DONTWAIT + 调用方状态机;超长回 501 保会话。(30s 阻塞 recv 分量已由 L3 pending 化解,f16dc9a:执行器不再阻塞;errno 分类/501 分量仍待办) | -| E-21 | `time_sync.c:128-144/:80-97` | accept 一切失败=无连接(200ms 静默重试、无 errno 分类/日志)+ 30s 阻塞 recv + close-then-rebuild 空窗 | errno 分级(BADF/INVAL→重建+日志);DONTWAIT + 会话超时(=既有 P2-8 扩展)。(30s 阻塞 recv 分量已由 L3 pending 化解,f16dc9a;errno 分级/空窗分量仍待办) | -| E-22 | `lftpd.c:608-626`、`time_sync.c:96/:146-149` | close-then-relisten 每会话端口空窗 ~0.2-1.7s(合规对照 = `ui_net_sink.c:446-457`:EOF 不 close、靠 auto_relisten 常驻) | 向 ui_sink 模式看齐 | -| E-23 | `test/net_test_task.c:869-917/:296-403` | 监听主循环与 phase7 无代次检查(网络重建后恒 BADF 空转)+ TC704 8 路 socket 与 3 个并发服务抢 8 槽 fd 池 → 假 FAIL | 代次检查 + 降并发/测试构建抑制服务任务 | -| E-24 | `net_socket.c:1971-1975 × :162`;`lftpd.c:398-413` | `s_local_ip` 逐字节拼读 × reconfig 重写不原子 → PASV 227 响应 IP 混拼(低频人工触发) | getsockname 入队列或临界区快照 | -| E-25 | `net_socket.c:1609`;`net_select.c:63-65`;`net_socket.c:751-753` | API 一致性杂项:成功不清 errno(纪律文档化,读端须知);net_select 参数错误不设 errno(仅测试用);net_accept 失败也覆写 `*addrlen`。"LISTENING 恒可读"部分 = 既有 P2-7 | 统一 errno 纪律 + 契约注释 | -| E-26 | `net_socket.c:1351-1519` | UDP 直调 API(sendto/recvfrom/UDP-bind)在调用方上下文做 SPI——**潜在地雷**:生产零调用(timeSync 2026-08-28 起 TCP 化正是为绕开),首个 UDP 用户即破铁律 | 文档化禁令或并入队列 | -| E-27 | `Src/main.c:104-113` | CubeMX `MX_*_Init` 返回值忽略(生成代码)→ 下游驱动级检查兜底(check_exist/gd5f ID/sd2506 init),可接受,记录在案 | 保持(生成区不改) | -| E-28 | `ui_port.c:60/:163-168` | 远端按键单格缓存(同任务无竞态):10ms 内连按前键丢失(UX 微瑕) | 小环形或文档化 | -| E-29 | `ui_net_sink.c:223-227` | LCDMEM 取帧失败静默丢应答、无日志(现网实际不触发) | 加 DBG 日志 | +| **P1** | 值拷贝队列陷阱 | 消息体按值拷进队列,netTask 修改的是**副本**,调用方看不到 | 回传字段恒为初始值 | +| **P2** | 共享全局错误变量竞态 | 全局 `errno` 被多任务交错覆写 | 报出别人的错误码 | +| **P3** | 返回值与错误码混同 | "0 = 成功"其实是"命令已发出";成功路径不清 errno | 失败被当成功 | +| **P4** | 返回值未检查 | `HAL_*` / 库函数返回被丢弃 | 失败静默继续 | +| **P5** | 事件标志语义 | 状态位被当作命令位消费掉 | 等待者永不唤醒 | +| **P6** | 通知值宽度与符号 | 32 位通知值装不下 / 符号位被当成数据 | 大长度变负数 | +| **P7** | ISR 与跨上下文共享状态 | ISR 写、任务读,无同步 | 读到半包 / 撕裂 | +| **P8** | 外设 / 库特定约定 | 该外设 / 该移植版特有的规则 | 照抄上游示例即错 | +| **P9** | 超时与错误混同 | 超时用同一个码表达,调用方无法区分 | 无法复位 / 无法重试 | -**驱动层** +### 1.2 严重度定义 -| 编号 | 位置 | 问题 | 修复方向 | -|---|---|---|---| -| E-32 | `sd2506.c:167-211`(修复后);`sd2506.h:114-125` | **(2026-09-21 复核后由 P0 降档;同日已修复)**init 两笔写在写禁止态发出被芯片忽略(L394/537 属实),**但**手册 §4.8"出厂均配置成 82H"+ 18H 非易失 → 充电器上电即已使能,**原"充电器终身未使能"P0 链证伪**;残留问题 = ① `:187-190` 注释与代码矛盾(注释称写 0x20 且"ARST=1",实际写 0x7B 且 ARST 在 CTR3 不在 CTR1)② init 无回读校验(18H 若被长时掉电腐蚀则无人发现)③ `docs/硬件平台/V0.0_网表详细分析报告.md:280`"已核实无缺陷"无回读证据 ④ `SD2506_CHARGE_5K (1U<<0)` 宏位错误(手册 §4.8:5K=Charge1=bit1,0x82=EN_Charge\|Charge1 已正确;零引用休眠;总表 L119 18H 行转录与 §4.8 不符以 §4.8 为准) | **已修复(2026-09-21,commit 41e111b)**:18H 回读校验(=0x82 不写;≠ 则经写允许窗口重置 0x82 + 回读验证);0FH 0x7B 死写移除(写禁止态被芯片忽略);注释订正(ARST 在 CTR3 非 CTR1);`SD2506_CHARGE_5K` 宏位订正(bit1)并补 10K/断开/INIT_VALUE 宏。台架实证:RTC 套件 Phase 1 6/6 + Phase 2 累计 17/17 PASS(含 set==readback)、产品固件重烧启动校验 PASS、ST-LINK 全片回读 No difference | -| E-46 | `ch395f.h:151` | `0xECH` 无效 ARMCC 字面量(= SD2506 0x0FH 同族);当前零引用休眠,首个使用者即踩 | 改 `0xECU` + pre-commit 禁 `0xNNH` 检查 | -| E-47 | `ch395f.c:606-622` | 轮询超时与芯片忙共用 `0x10` 码 + 跨命令陈旧状态(共享 CMD_STATUS 读即清 → 下条命令可能先读到上条的完成态);现边界统一按可重试处理,影响有限 | 独立超时码或轮次序号 | -| E-48 | `rs485.c:173` | ErrorCallback 重武装返回值丢弃(现良性:ORE 路径 HAL 先复位 RxState=READY;非阻塞错误路径原武装存活自愈) | 非 HAL_OK 记 WARN | -| E-49 | `rs485.c:70-93` | TX 超时立即拉低 DE → 截断在途尾字节(仅超时路径,本身已失败) | 等 TC/短延时后再拉低 | -| E-50 | `rs485.c:37-39` | rx_buf/rx_size 单缓冲无 ISR/任务同步 → 任务拷贝中遇新帧 = 帧撕裂(115200×256B≈22ms 窗口) | 双缓冲+序号或契约限定钩子内 | -| E-51 | `sd2506.c:450-451` | set_alarm CTR2 全量覆盖(=0x25)中途清掉 WRTC1 → 芯片中途重新上锁 → 后续 write_disable 全被忽略,停在混合态(下轮 write_enable 自愈) | 读-改-写保 WRTC 位 | -| E-52 | `app_main.c:89/96/104/109` | 开机诊断块 4 个 `get_*` 返回值全丢弃 → I2C 瞬断时打印**未初始化栈值**当诊断(误导性证据) | 检查返回值,失败打印"读取失败" | -| E-53 | `gd5f2gq5ue.c:456/485/684-685` | 轮询路径 HAL 返回值系统性未检查(DMA 路径有检查)→ 状态字节读垃圾 → 假"芯片空闲" → 后续命令未锁存却报成功 | 统一检查,失败返 GD5F_ERROR | -| E-54 | `gd5f2gq5ue.c:653-655` | init 的 ECC 使能步骤未校验 → "NAND init OK" ≠ ECC 已开 → 存储栈在无 ECC 保护下运行(数据完整性基座静默降级) | 使能后回读 B0h 校验 bit4 | -| E-55 | `nand_ftl.c:206/:180` | `dhara_nand_read/is_free` 弃 `read_from_cache` 返回值 → 传输失败时以陈旧/垃圾冒充成功(is_free 方向:垃圾 0xFF → 已用页判空闲 → 数据覆盖)(=既有 P2-4) | 检查返回值失败上报 | -| E-56 | `gd5f2gq5ue.c:703-737` | 公共 `gd5f2gq5ue_read` 对不可纠错页仍返回 0(契约"0=成功"实为"SPI 传输完成";生产主路径有 dhara glue ECC 检查保护,缺口仅在 raw API) | 加 ECCS 检查或头文件明示 | -| E-57 | `gd5f2gq5ue.h:119/140/189/222-253` | 公共 API 返回类型 `int`/`int32_t` 混用(码值均为小负数,无功能影响) | 统一 `int32_t` | -| E-58 | `tpafe5160.c:322-324/:343/:354` | 双缓冲角色互换 + 任务抢占 → 撕裂读(get_buf 与拷贝完成之间被抢占超一个帧周期 = 8 通道失去同步性) | 原子快照拷贝 API(关中断+memcpy)或文档化时限 | -| E-59 | `adc_task.c:37-43`、`rs485_task.c:37-43` | 边界状态:两任务均为空桩(osDelay 循环),`rs485_on_rx` 无覆盖 → E-36/E-44/E-45/E-58 全部休眠;**真实消费者接入之日即触发** | 消费者实现前先修驱动契约(E-36/E-45/E-44) | - - +| 级别 | 含义 | 判据 | +|---|---|---| +| **P0** | 生产可触发的**静默数据丢失** | 无需特殊工况,正常运行即出错,且无任何告警 | +| **P1** | 故障条件下的潜在陷阱 | 触发需特定时序 / 工况 / 参数,一旦触达后果重(死锁、数据损坏、整机不可用) | +| **P2** | 健壮性 / 一致性 / 可维护性 | 当前路径无实际后果,或仅影响排障与后续维护 | + +### 1.3 静态代码 ≠ 可触发缺陷 + +本项目的硬口径:**只有"生产路径 + 可复现"才算 P0/P1**。多条首版 P0/P1 在本轮复核中被降级, +原因就是"静态上确实存在,但当前没有任何调用者能走到": + +- 共享全局 errno 竞态 → 唯一的错误消费者(`ui_net_sink`)**不读 errno、直接看返回值**, + 只会在 UI 任务与 PC 轮询叠加时偶发(P1 保留) +- `cfg_file` 的 `snprintf` 返回值全丢弃 → 但文件解析器**根本不接受转义序列**, + 溢出所需的字符永远进不来(P2) +- `nand_ftl` 的 `dhara_nand_is_free` 把垃圾 `0xFF` 当已用页 → 偏差方向是**保守**的, + 最多浪费一个页(P2 降级) + +也就是说:**降级不是"不重要",而是"触发不了",触发条件一出现就必须立刻重估**(见 §3.2)。 + +### 1.4 本轮复核范围 + +- 复核对象:首版 59 项中**未标注已闭环**的全部条目 +- 方法:逐项回到当前源码查实际代码(`grep` + 精读调用链),不看注释、不信文档既有结论 +- 已闭环项(§2):仅确认提交存在与机制在位,不重复逐行审 +- 未覆盖:`Lib/FatFs` 移植层内部、`Lib/dhara` 内核、lftpd 字符串 / IO 辅助模块、 + `Middlewares/` 内部(同首版边界) +- 模拟器保真度差异见 §6 --- -## 4. 模式 × 子系统矩阵 +## 2. 已闭环总览(33 项) -> F=发现编号(E-);✓=检查无问题;—=不适用 +只列索引与闭环依据,明细不再展开(首版明细可从 git 历史取回)。 -| 模式 | net 层/执行器 | 事件标志 | 配置引擎 | syslog | CH395F | RS485 | SD2506 | GD5F/FTL | TPAFE | lftpd | timeSync | ui_sink | test | sim | +### 2.1 P0 三项(全部已修复并台架验证) + +| 编号 | 位置 | 危害 | 闭环 | +|---|---|---|---| +| **E-01** | `App/cfg_file.c` | `cfg_file_poll` 的"超长=损坏"检查是**死代码**(读长被钉死在阈值)→ >4096B 配置被截断当有效解析 → 界外内容静默回工厂默认 → 基线粘住错误状态 → 下次开机 load 守卫判"损坏" → **用户配置文件整体销毁** | `d030cb0` + 关联 `70f1cc8`(`f_rename` 不覆盖既有目标) | +| **E-31** | `nand_ftl.c` / `journal.c` | 扫描读失败被**覆盖为 `TOO_BAD`** → 非空白卷在 I/O 故障下被误判"全新"→ 直接 `clear` → 映射表整体丢失 | `da1a6e8` | +| **E-02** | `net_socket.c` / `lftpd` / `time_sync` | 一条空闲 FTP 控制连接把 netTask 独占 30s → **全体消费者队列停摆**(实测冻结远端显示 17.7s) | `f8d6e90`+`4a1605b`(分片)→ `f16dc9a`(L3 pending 化,结构性消灭) | + +### 2.2 P1 十五项 + +E-05(net 建链 API 队列化)、E-06 / E-20 / E-21 的**主要分量**(随 E-02 pending 化落地)、 +E-09(UI 页面局部副本 + 显式提交)、E-17(RECONFIG 原子消费)、 +E-32(SD2506 充电器寄存器回读校验)、E-33(CH395F DMA 错误通道)、 +E-36 / E-37(RS485 契约与 init 返回码)、E-38(SD2506 负温度解码)、 +E-39 / E-51(SD2506 写允许窗口 / `set_alarm` 读-改-写)、E-40(FTL erase/prog 错误分类)、 +E-43(GD5F erase 容量边界)、**E-03(错误码按任务隔离,2026-09-25)**。 + +> 注:E-05 已闭环(建链 API 并入队列),但**同族的 `net_getsockname` 残留通道** +> 在 §3.1-E-24 重新入册。 + +### 2.3 P2 十五项 + +E-07(`f_close` 返回值检查 ×5)、E-08(preboot 分支并轨)、E-11(`cfg_file` 读路径返回值)、 +E-14(net init/重建失败补自检留痕)、E-18(ftpTask 等待超时)、E-19(死事件位清理)、 +E-22 的**主要分量**、E-29(`ui_net_sink` `FD_CLR` 合法性)、E-35(测试常量对齐)、 +E-46(`0xECH` 非法字面量)、E-52(开机诊断检查 `get_*` 返回值)、 +E-53(GD5F 轮询路径 HAL 返回值)、E-54(ECC 使能回读)、E-55(`dhara_nand_read` 返回值)、 +E-57(`int` / `int32_t` 混用)。 + +--- + +## 3. 未闭环发现(25 项) + +每项字段统一为:**位置 → 机制 → 后果 → 当前状态 → 建议**。 +"当前状态"是本轮(2026-09-25)回到代码实测的结论。 + +### 3.1 A 类:待处理的实质缺陷(10 项) + +> **2026-09-25 已闭环 1 项**:E-03(见下方划线条目,保留留档)。 + +--- + +#### ~~E-03 `s_net_errno` 跨任务覆写竞态~~ — **已修复(2026-09-25)** | 模式 P2 + +- **位置**:`Drivers/BSP/NET/net_socket.c`(定义处 + 全部写入点 + `net_get_errno`) +- **机制**:netTask 在 `net_process_messages` 结束处把精确错误码写进全局 `s_net_errno`, + 随后调用方包装器在**自己的上下文**里再写一次。两条写路径分属不同任务,无任何互斥。 +- **后果**:netTask 刚为 A 任务置好码,B 任务的通知处理抢先覆盖 → A 读到 B 的码。 + 任何基于 `net_get_errno()` 做分支的逻辑都可能走错路径(错判"可重试"为"连接已死",或反过来)。 +- **修复(2026-09-25)——取"按任务隔离"路线**: + 1. 删除全局 `s_net_errno`,改为 `s_net_errno_slots[8]`(每槽 `{owner 任务句柄, err}`) + 2. `net_errno_slot()`:无锁快路径命中自己的槽(**只读不写**,并发读者看不到半成品槽位); + 首次占用在 `taskENTER_CRITICAL` 内复查后写入;槽位用尽**不新增共享槽**(那会把竞态带回来), + 统一回退 [0] 槽 → 语义退化为共享,仅影响排障可读性 + 3. `net_set_errno(err)` 取代全部 107 处 `s_net_errno = X` 赋值; + 新增 `NET_ERR_RETURN_1(err)` 宏合并"记码 + `return -1`",共替代 82 处 + 4. `net_get_errno()` 改读**本任务**槽位,对外语义不变(成功不清零,向后兼容) + 5. 被修路径的**真实收益**:三个既有消费者(`ui_net_sink.c:476`、`net_test_task.c:231/382`) + 都是"同任务 `net_recv` 后立即 `net_get_errno()`"——这正是竞态窗口内的模式, + 修复后该判定不再可能读到别的任务的码 +- **验证**:构建 0 错误 0 警告;cppcheck(warning/performance/portability)无告警; + `git diff --check` 无空白问题;编码无 `U+FFFD` 且 BOM 状态与原文一致 +- **代码代价**:BSS +64 B(8×(4+4)) +- **登记**:`docs/待处理内容.md` N-5(可销项) +- **残留**:`net_recv_to` 继续用返回码 `-2` 表达超时(不依赖 errno),保持既有正确做法 + +--- + +#### E-04 `remo_send` 丢弃 `net_send` 返回值 — **P1** | 模式 P3 + +- **位置**:`Drivers/BSP/UI/ui_net_sink.c:101-120`(`:119` 调用点) +- **机制**:`net_send(g_client, p, (int)(4 + len + 1), 0)` 的返回值直接丢弃。 + `net_send` 在弱网 / 对端停滞时的合法返回是**短写**(已发字节数 < 请求),不是 -1。 +- **后果**:应答帧静默截断 → 客户端 CRC 校验失败 → 断开重连 → 用户观感是 + "画面卡住然后黑屏 / 一直卡"。这是**最难定位的一类故障**:设备侧一切日志正常。 +- **当前状态(复核)**:**仍然存在**。已确认 `ui_net_sink.c` 全文件无 `net_send` 返回值检查, + 且 `remo_send` 的整帧(3204B)**远超过**单次可发送上限 + `NET_SEND_CHUNK_MAX`(= 芯片发送缓冲 1KB,`net_socket.c:1394`), + 所以"单次调用的返回值即全量"这一假设**在整帧路径上必然不成立**; + 当前能工作只是因为 `net_send_locked` 内部做了分块循环 + 内部重试。 + 一旦对端读取变慢,内部分块重试会耗尽预算并**合法地短写返回**,此时上层无任何感知。 +- **建议**:`remo_send` 改为返回 `int`,校验 `ret == len`;短写 → `DBG_ERROR` + 计入统计; + 连续失败 → `net_close(g_client)` 强制重连补帧(与 `lftpd_inet_write` 的处理方式对齐)。 +- **关联**:`docs/测试说明/链路延迟测试说明.md:632` 已把 E-04 列为"会掩盖延迟"的既有缺陷。 + +--- + +#### E-24 `net_getsockname` 直读 `s_local_ip`,绕过 netTask 串行化 — **P2** | 模式 P7 + +- **位置**:`Drivers/BSP/NET/net_socket.c:3637-3641`(读 `s_local_ip`); + 写点 `:201`(netTask 在 `net_init` 内 `memcpy`) +- **机制**:`s_local_ip` 是 4 字节静态数组,netTask 写、任意任务读,无同步。 +- **后果**:`net_getsockname` 可能拼出**新旧混合**的 IP。 +- **当前状态(复核)**:**仍然存在,但本轮确认影响面比首版预估的更窄——仍应记录。** + 写入只发生在 `net_init` 内部**一次**(`:201`),此后 IP 不再变化 + (DHCP → `0.0.0.0` 也在同一次写入内)。所以"撕裂"需要恰好在 `net_init` 执行期间调用 + `net_getsockname`,而所有调用方(`lftpd.c:407` PASV、`lftpd.c:673` 连接日志、 + `lftpd_inet.c:64`)都在 **NET_READY 屏障之后**才可能执行 → 当前不可达。 +- **建议**:维持 P2。`net_getsockname` 改为经队列请求(或至少把 4 字节打包成单次 32 位读 + + netTask 侧序贯穿),使契约与其余 socket API 一致;不作为独立修复项排期, + 随 N-4 二期顺带处理。 + +--- + +#### E-10 GD5F init 内多处 HAL 返回值未检查 — **P2** | 模式 P4 + +- **位置**:`Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c:606` 起(init 步骤 3~6 的 setup 序列)、 + 各直接调用的 `HAL_SPI_Transmit` / `gd5f_set_feature` / `gd5f_wait_busy` +- **机制**:扫坏块、使能 ECC、解除块保护、定位保留池等步骤大量调用点忽略返回值。 +- **后果**:init 返回 `GD5F_OK` 但实际配置未生效(例如 ECC 未真正打开), + 存储栈在**无 ECC 保护**下运行 → 数据完整性底线静默降级。 +- **当前状态(复核)**:**仍然存在**。`gd5f_wait_busy` 自身有 `int` 返回值 + (`gd5f2gq5ue.c:147`,超时返 `GD5F_BUSY_TIMEOUT`),但 init 与 BBT rebuild 里 + 至少 5 处(`:548` / `:552` / `:643` / `:647` / `:1000`)直接丢弃返回值; + `gd5f_bbt_scan()` 内部的 `gd5f_page_read` / `gd5f_read_from_cache`(`:505-506`)同样未检查。 +- **建议**:init 全链路失败即返回错误码(`App/task/default_task.c:64` 已有 + `int ret = gd5f2gq5ue_init()` 的检查位,只是慢路径没把错误传上来);init 重试 1~2 次; + 让 `default_task` 能对"扫坏块失败 / ECC 未使能"做出差异化反应(而非静默继续)。 +- **关联**:与 E-42 / E-53 / E-54 同一族,建议合并为一次"GD5F 错误通道统一"改造(§5 第二档 #1)。 + +--- + +#### E-42 `gd5f_bbt_scan` 丢弃读返回值 — **P2** | 模式 P4 + +- **位置**:`Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c:505-506` +- **机制**:`gd5f_page_read(page)` 与 `gd5f_read_from_cache(...)` 的返回值均未接收; + `bbt_scan` 对外恒返回 `GD5F_OK`(`:518`)。 +- **后果**:SPI 读失败时局部 8 字节 `spare[8]` 保持**上一轮的陈旧内容**, + 按 `spare[0] != 0xFF` 判定 → 把好块判成坏块(保守,可接受)或反之。 + 更严重的是**整片扫描全失败时也返回成功**,上层以为坏块表可用。 +- **当前状态(复核)**:**仍然存在**。调用点 `:548`(BBT rebuild)与 `:639`(init) + 都直接忽略返回,rebuild 路径随后照常 `gd5f_bbt_save()` → 可能把**错表**持久化覆盖正确副本。 +- **建议**:扫描中任一读失败即中止并返回错误;`bbt_scan` 增加 `spare[8]` 每轮清零; + `gd5f2gq5ue_bbt_rebuild` 在扫描失败时必须**放弃持久化**(保留既有好副本)。 + 第二项(失败时不动持久区)是比"检查返回值"更关键的语义修正。 + +--- + +#### E-53 GD5F 轮询路径 HAL 返回值系统性未检查 — **P2** | 模式 P4 + +- **位置**:`Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c:155-168`(`gd5f_wait_busy` 内 3 次 HAL 调用)、 + `:239` / `:403` / `:476` / `:545` / `:552` / `:643` / `:647` / `:1000`(`wait_busy` 调用点) +- **机制**:`gd5f_wait_busy` 里 `HAL_SPI_Transmit` / `HAL_SPI_Receive` 的返回值全部丢弃, + 失败时 `status` 保持初值 `0` → `(status & GD5F_STATUS_OIP) == 0` 立即可满足 → + **"操作完成"被伪造出来**。 +- **后果**:SPI 故障 → 状态字节读到垃圾 → 误判操作完成 → 后续命令在未就绪的芯片上执行, + 最终表现为"静默报成功但数据没落盘"。 +- **当前状态(复核)**:**仍然存在**。`wait_busy` 的返回值形式上是"看起来被检查了" + (很多调用点确实写了 `ret = gd5f_wait_busy(...)`),但**函数内部不区分"超时"与"HAL 失败"**, + 且 HAL 失败造成的伪完成会**优先于**超时返回 `GD5F_OK`。 +- **建议**:`gd5f_wait_busy` 内检查三次 HAL 调用,失败即返回独立错误码(`GD5F_ERROR`); + 调用点区分 `GD5F_BUSY_TIMEOUT`(可重试)与 `GD5F_ERROR`(真故障,须上报自检)。 +- **关联**:与 E-10 / E-42 / E-54 合并改造(§5 第二档 #1)。 + +--- + +#### E-56 公开 `gd5f2gq5ue_read` 对不可纠正页仍返回 0 — **P2** | 模式 P3 + +- **位置**:`Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c:703-737`(read 主体)、 + `:239`(`gd5f_read_from_cache` 内) +- **机制**:`gd5f_read_from_cache` 的返回值语义是"**SPI 传输完成**",不是"**数据正确**"。 + ECC 不可纠正页(ECCS 非零)在 SPI 层是正常完成的,因此函数返回 `0`(成功)。 +- **后果**:raw API 使用者会拿到**带有未纠正位翻转**的数据并被告知成功。 +- **当前状态(复核)**:**仍然存在,缺口范围本轮已精确确认。** + 生产主路径是 **dhara glue 层**(`nand_ftl.c`),它自己有 ECC 检查保护,所以是安全的; + 缺口只存在于**直接调用公开 raw API 的场景**。真正的消费者目前只有 `test/` 测试套件与诊断路径。 +- **建议**:两条路选一条——① `gd5f2gq5ue_read` 增加 ECCS 回读并在不可纠正时返回错误码; + ② 在 `gd5f2gq5ue.h` 的 `read` 声明处**显式文档化**"0 = SPI 传输完成, + 数据正确性由 ECC 层保证"。因涉及公开 API 语义,倾向 ② + 在 + `docs/问题记录/GD5F2GQ5UE_Trap_Records.md` 补一条契约说明。 + +--- + +#### E-58 TPAFE 双缓冲角色互换 + 任务抢占 → 撕裂帧 — **P1** | 模式 P7 + +- **位置**:`Drivers/BSP/TPAFE5160/tpafe5160.c:322-324`(`get_buf` 换缓冲)、 + `:343` / `:354`(ISR 与任务侧) +- **机制**:`get_buf` 交换双缓冲指针后,任务侧才逐通道 `memcpy`;若在这两步之间 + 被 ISR 抢占且跨过一个整帧周期,任务拷贝的就是**混合了两帧**的 8 通道数据。 +- **后果**:8 通道失去同一性(同一时刻的 8 路值互相错位)——对外是"数据跳变", + 且**没有任何错误指示**。 +- **当前状态(复核)**:**仍然存在,属"休眠缺陷"**。当前 `adc_task.c:37-43` 是空桩 + (`osDelay` 循环),没有任何消费者调用 `get_buf`。**接入消费者之日即触发**。 +- **建议**:提供**原子快照 API**(临界区短锁 + `memcpy`,把"取指针"与"拷数据"合并为 + 一个不可分割动作),或在文档中明确"必须关中断后拷贝"并落成强制约定。 + 属"接消费者前必修"(§5 第三档前置条件)。 +- **关联**:E-59(消费者接入前置清单) + +--- + +#### E-49 RS485 TX 超时立即拉低 DE — **P2** | 模式 P9 + +- **位置**:`Drivers/BSP/RS485/rs485.c:70-93` +- **机制**:发送超时路径直接拉低 DE 引脚,此时**末尾字节可能仍在移位寄存器中**。 +- **后果**:对端收到被截断的最后一帧(半字节)。 +- **当前状态(复核)**:**仍然存在,但仅在超时路径生效**。正常路径靠 TC 标志正常收尾, + 所以影响局限于"发送本身已经失败"的场景——属于"失败之后再补一刀"型缺陷。 +- **建议**:超时路径改为"等 TC 或短延时后再拉低 DE"(给移位器一个明确的排空窗口), + 代价是超时路径多等 ≤1 字符时间。 + +--- + +#### E-50 RS485 单缓冲无 ISR/任务同步 → 包撕裂 — **P2** | 模式 P7 + +- **位置**:`Drivers/BSP/RS485/rs485.c:37-39`(`rx_buf` / `rx_size`) +- **机制**:ISR 写 `rx_size`、任务侧读并 `memcpy`,无同步;115200bps 下 + 256B 帧仅约 22ms 窗口,任务一旦晚于窗口完成拷贝,新帧就会覆盖旧帧。 +- **后果**:读出**跨两帧**拼接的数据(长度字段与内容不匹配)。 +- **当前状态(复核)**:**仍然存在,休眠缺陷**。当前 `rs485_task.c:37-43` 是空桩, + 无消费者。与 E-36 / E-44 / E-45 / E-58 同一族。 +- **建议**:双缓冲 + 序号(或"任务处理窗口内关 IDLE 中断"的简化约定)。 + 属"接消费者前必修"(§5 第三档前置条件)。 + +--- + +#### E-59 消费者任务均为空桩,驱动层多项 P1 全部休眠 — **P1(元发现)** | 模式 —— + +- **位置**:`App/task/adc_task.c:37-43`、`App/task/rs485_task.c:37-43` +- **机制**:两个任务的函数体都是 `osDelay` 空循环,谁都不调驱动层的 `get_buf` / `on_rx`。 +- **后果**:**首版列出的多项驱动层 P1(E-36 / E-44 / E-45 / E-58)当前全部不可触发。** + 这意味着"审计通过"是假的——一旦接入真实消费者,这些缺陷会同时上线。 +- **当前状态(复核)**:**仍然存在,且是本轮最应强调的一条**。 + 本条不是某个具体 bug,而是一个**风险闸门**:它决定了 §3.2 那一整类"契约缺口" + 是"暂时无害"还是"即将爆炸"。 +- **建议**:把"接消费者前必须先修驱动契约"写成**强制门禁**(建议入 `AGENTS.md`, + 与"改栈前跑 `stack_report.py`"同级)。前置清单 = E-36 / E-44 / E-45 / E-58, + 另加栈预算核算(`adc_task` / `rs485_task` 尚未纳入 `Src/freertos.c` 栈表)。 + +--- + +### 3.2 B 类:契约缺口(随消费者接入触发,7 项) + +这一类的共同特征:**静态缺陷真实存在,但当前没有调用者能走到**。它们现在不产生故障, +但契约没写死,属于"埋着的雷"。**触发条件一旦出现(新消费者、参数变更、库配置变更),立即升 P1。** + +--- + +#### E-16 `cfg_file` 生成文档的边界余量绑死 `snprintf` 返回语义 — **P2** | 模式 P4 + +- **位置**:`App/cfg_file.c:469` / `:481`(`s_gen_document`)、 + 同族丢弃点 `:252` / `:256` / `:261`(`s_encode`)、`:642` / `:663` / `:683`(行生成) +- **机制**:`off += (size_t)snprintf(...)` —— 直接丢弃返回值。`snprintf` 返回**本应写入的长度**, + 截断时该值 **> 剩余容量**,`off` 会**跳过缓冲末尾**(越界),而不是停在边界; + 现有的 `if (cap < off + 112u) return -1;` 前置守卫在 `off` 已经越界后也救不回来。 +- **后果**:极端情况下写越界 / 输出被静默截断。 +- **当前状态(复核)**:**仍然存在,但本轮确认当前不可达。** + 触发本缺陷需要一个**超过 104 字符**的段名或键名,而配置文件的**解析器根本不接受转义序列** + ——含引号 / 反斜杠的键名无法通过解析进入内存,长度上限也受解析缓冲约束。 + 换句话说:**防线是"上游解析器"而不是"本函数的守卫",而这条防线没有写进文档。** +- **建议**:① 把 `snprintf` 返回值改为检查式 + (`int n = snprintf(...); if (n < 0 || (size_t)n >= cap - off) return -1;`); + ② 更根本的做法是在 `cfg_file.h` 中**显式定义段名 / 键名的最大长度**,并让注册 API 拒绝超长名字 + ——这样余量才是**编译期可验证**的,而不是依赖"解析器恰好不接受转义"。②优先级高于①。 + +--- + +#### E-25 `net_select` LISTENING 恒可读 + 参数错误不置 errno — **P2** | 模式 P3 + +- **位置**:`Drivers/BSP/NET/net_socket.c`(`net_select` 实现) +- **机制**:① 监听态 fd 在 `select` 语义下永远"可读"(有连接待 accept 时可读, + 无连接时按 BSD 也应可读),调用方若用 `select` 做"有数据可读"判断会误判; + ② 参数错误时直接返回而**不设置 errno**,调用方拿到 `-1` 却查不到原因。 +- **后果**:无法区分"监听器上有新连接"与"有数据";参数错误无从定位。 +- **当前状态(复核)**:**仍然存在,休眠缺陷**。当前无 `net_select` 调用者 + (各服务都是 `accept` 轮询 / pending 模式)。 +- **建议**:随 N-4 二期或首个 `select` 消费者落地时一并订正; + 同时在头文件把"LISTENING 恒可读"作为**契约**写出来。 + +--- + +#### E-26 UDP 路径未并入队列 — **P2** | 模式 P8 + +- **位置**:`Drivers/BSP/NET/net_socket.c:2647`(`net_sendto`)、`:2776`(`net_recvfrom`) +- **机制**:UDP 的 `sendto` / `recvfrom` 走**直调芯片**路径,不在 `netMsgQueue` 上, + 与 netTask 串行语义冲突(SPI 事务可能与其他 socket 操作交错)。 +- **后果**:UDP 与 TCP 并发时 SPI 事务交错 → 芯片状态机混乱。 +- **当前状态(复核)**:**仍然存在,休眠缺陷**。当前无任何 UDP 用户 + (`net_init` 的 UDP 分支无调用者)。 +- **建议**:三选一并在文档中定案——① 并入队列(新增 `NET_MSG_SENDTO` / `NET_MSG_RECVFROM`); + ② 直接移除 UDP API(无用户,最干净);③ 保留但在头文件明确"仅单任务独占期可用"。 + 倾向 ② 或 ③,避免为无用户的路径增加队列类型与 RAM。 + +--- + +#### E-27 / E-28 socket 表裸指针跨上下文读 — **P2** | 模式 P2 / P7 + +- **位置**:`Drivers/BSP/NET/net_socket.c`(`net_get_sock()` 返回裸指针)、 + `App/time_sync.c` / `Drivers/BSP/NET/lftpd/lftpd.c`(代次号读取) +- **机制**:`net_get_sock()` 把 socket 控制块**裸指针**交给调用方,调用方随后读 + `p_sock->state`;netTask 可能在同一时刻改写该结构。 +- **后果**:读到中间态的 `state`(例如"刚 `CLOSED` 但字段未清")。 +- **当前状态(复核)**:**仍然存在,休眠缺陷**(本轮从 E-05 族拆分并重新入册)。 + 当前唯一代表性读点是 `lftpd_inet_accept_timeout` + (`Drivers/BSP/NET/lftpd/lftpd_inet.c:100-104`)读 `p_sock->state != LISTENING`; + 该路径受 `timeout_ms` 约束、且 netTask 对 `state` 的写是**单次 32 位写**(无撕裂), + 实际风险低。已用 pending 模型重写的路径(`net_recv_to`)不再有此类读。 +- **建议**:长期方向是"状态查询 API 化"(`net_get_state(fd)` 走队列或返回快照值), + 与 E-24 / E-26 同属"消除跨上下文裸指针"的一组,可合并处理。 + +--- + +#### E-31 后续:卷自愈的误判边界 — **P2** | 模式 P3 + +- **位置**:`App/task/default_task.c:311`(`storage_bring_up`)及卷健康探针 +- **机制**:E-31 主体已修(错误分类 + 有界重试),但**自愈触发集**仍需一条纪律: + 只有"真卷损坏"才允许走重格式化,**名字 / 参数类错误码必须排除在外**。 +- **后果**:若把 `FR_INVALID_NAME` 之类的**纯 RAM 判定错误**归入损坏触发集, + 会触发**不可逆的整卷重格式化**。 +- **当前状态(复核)**:**机制在位,纪律已固化为 `AGENTS.md` 铁律** + ("自愈类逻辑严禁把名字/参数类错误码归入卷损坏触发集")。 + 本项保留为**回归监督项**:后续任何自愈逻辑改动都要回头核对这条纪律。 +- **建议**:不需要改动。在 `storage_bring_up` 的错误分类 switch 旁补一条注释 + 指向该铁律,降低后人误改概率。 + +--- + +#### E-47 / E-48 驱动侧边界项 — **P2** | 模式 P8 / P9 + +| 编号 | 位置 | 机制与后果 | 当前状态(复核) | 建议 | +|---|---|---|---|---| +| **E-47** | `Drivers/BSP/CH395F/ch395f.c:606-622` | 轮询超时与"芯片忙"共用 `0x10` 码;且 `CMD_STATUS` 读即清 → 下条命令可能先读到上条的完成态 | **仍然存在**,现边界已统一按"可重试"处理,实际影响有限 | 引入独立超时码或轮次序,使调用方能区分"忙"与"超时" | +| **E-48** | `Drivers/BSP/RS485/rs485.c:173` | `ErrorCallback` 的修复者返回值被丢弃 | **仍然存在,实为良性**:ORE 路径 HAL 会先复位 `RxState=READY`;非阻塞错误路径原本就存活自愈 | 非 `HAL_OK` 时补一条 `WARN` 便于排障即可 | + +--- + +### 3.3 C 类:一致性 / 可维护性(8 项) + +无功能影响或影响极小,排在第三档批量处理。 + +| 编号 | 位置 | 问题 | 当前状态(复核) | 建议 | +|---|---|---|---|---| +| **E-12** | `Lib/FatFs/ffconf.h:39` | `FF_FS_LOCK 0` —— 无文件级互斥,同一文件可被两任务同时打开 | **仍然存在**。当前 `defaultTask` 是 syslog 与配置引擎的**唯一写者**(单写者收敛策略),故无实际冲突 | 开 `FF_FS_LOCK`(代价:每文件 ~几十字节 RAM);或固化"单写者纪律"。**当前选择后者,本项仅为记录** | +| **E-15** | `App/util/ringbuf.c` | ringbuf 实现存在但**全项目零调用者**(本轮 `grep` 确认除自身外无引用) | **仍然存在** | 修复后启用,或直接删除。**保留一个零调用者的工具模块是负债**,建议本轮就定去留 | +| **E-23** | `test/` | 测试套件缺代次检查(accept 失败分支);TC704 并发度偏高 | **仍然存在** | 加代次断言;TC704 降并发或在测试构建抑制服务任务 | +| **E-30** | `simulator/sim/net_socket_sim.c` | sim 后端 errno 语义与 MCU 不对齐(成功即清 errno,MCU 不清) | **仍然存在**(语义差异在 §6 表中仍成立) | 对齐语义;并在缺陷记录中标明"sim 复现盲区" | +| **E-57** | `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h:119/140/189/222-253` | 公开 API 返回类型 `int` / `int32_t` 混用(码值均为小负数,无功能影响) | **仍然存在** | 统一为 `int32_t`,随 GD5F 错误通道改造一同进行 | +| **E-60** | `Drivers/BSP/SD2506/sd2506.c/h` | `SD2506_CHARGE_5K` 宏位与手册 §4.8 不符的风险 | **已订正**(2026-09-21,`41e111b`):宏位已改为 `bit1`,18H 回读校验在位 | 无需改动,仅作为"手册总表 vs 专用表不一致"的案例保留 | +| **E-61** | `Drivers/BSP/SD2506/sd2506.c:482` | `sd2506_clear_int` 注释称"ARST=1 时自动清除 INTAF",实际 ARST 在 **CTR3**(11H)而代码从未写过 CTR3 | **仍然存在(纯注释错误)** | 修注释:明确"ARST 未启用,INTAF 靠读 CTR1 清除" | +| **E-62** | `Lib/FatFs/ffconf.h` | `FF_USE_LFN=0` → 严格 8.3 短名;自愈逻辑若把 `FR_INVALID_NAME` 归入损坏触发集 → 不可逆重格式化(`.fschk` 无限重格式化事故根因) | **铁律已固化**(`AGENTS.md`);`ff.c` `create_short` 全部拒绝点已加 DEBUG 打印 | 无需改动,回归监督项(与 §3.2-E-31 后续同族) | + +--- + +## 4. 模式 × 子系统矩阵(仅未闭环项) + +> 单元格内容 = 发现编号;`✓` = 本轮检查无问题;`—` = 不适用 + +| 模式 | net 层 | 事件标志 | 配置引擎 | syslog | CH395F | RS485 | SD2506 | GD5F/FTL | TPAFE | lftpd | timeSync | ui_sink | test | sim | |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| -| P1 值拷贝 | ✓(契约修复在位) | — | ✓(输入型条目) | ✓(输入型条目) | — | — | — | — | — | — | — | — | — | — | -| P2 共享全局 | **E-03** | ✓ | ✓ | ✓ | — | — | — | — | — | — | ✓(免疫) | **E-03** | E-03 | E-30 | -| P3 混同 | E-25/E-26 | — | **E-01** | — | E-35/E-47 | E-36 | E-39/E-51 | **E-31/E-40**/E-43/E-56 | — | E-06/E-20 | E-21 | **E-04** | ✓ | E-30 | -| P4 未检查 | ✓(put 全查) | E-13 | E-11 | E-08 | **E-33/E-34**/E-35 | E-37/E-48 | E-32/E-52 | E-41/E-42/E-53/E-54/E-55 | — | ✓ | E-21 | ✓ | E-23 | — | -| P5 事件语义 | ✓ | E-17/E-18/E-19 | ✓ | — | — | — | — | — | **E-45** | ✓ | ✓ | ✓ | ✓ | — | -| P6 符号/宽度 | — | — | — | — | — | — | E-38 | — | — | — | — | — | — | — | -| P7 跨上下文 | **E-05** | — | **E-09** | E-08 | — | E-36/E-50 | — | — | E-58 | E-06 | — | — | E-23 | — | -| P8 外设/库 | E-26 | — | **E-07** | E-07 | E-33/E-46 | — | E-32 | E-43/E-54/E-56 | E-44 | — | — | — | — | — | -| P9 超时/错误 | **E-02** | — | — | — | E-34/E-47 | E-49 | — | **E-31/E-40** | E-44 | E-20/E-22 | E-21 | — | E-23 | — | +| P1 值拷贝 | ✓ | — | ✓ | ✓ | — | — | — | — | — | — | — | — | — | — | +| P2 共享全局 | ✓ | ✓ | ✓ | ✓ | — | — | — | — | — | **E-27/E-28** | **E-27/E-28** | ✓ | ✓ | E-30 | +| P3 混同 | **E-25** | — | ✓ | — | E-47 | ✓ | ✓ | **E-31后续**/**E-56** | — | ✓ | ✓ | **E-04** | ✓ | E-30 | +| P4 未检查 | ✓ | ✓ | **E-16** | ✓ | ✓ | E-48 | ✓ | **E-10**/**E-42**/**E-53** | — | ✓ | ✓ | ✓ | ✓ | — | +| P5 事件语义 | ✓ | ✓ | ✓ | — | — | — | — | — | — | ✓ | ✓ | ✓ | ✓ | — | +| P6 符号/宽度 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | +| P7 跨上下文 | **E-24** | — | ✓ | ✓ | — | **E-50** | — | — | **E-58** | E-27/E-28 | ✓ | ✓ | — | — | +| P8 外设/库 | **E-26** | — | ✓ | ✓ | E-47 | — | E-61 | E-56 | — | — | — | — | — | — | +| P9 超时/错误 | ✓ | — | — | — | E-47 | **E-49** | — | ✓ | ✓ | ✓ | ✓ | — | — | — | + +**读法提示**:三处"空白带"(事件标志、syslog、配置引擎的大部分行)不是没审, +而是首版查出的问题已闭环。**当前未闭环项高度集中在 GD5F 驱动、RS485/TPAFE 驱动、 +与 net 层全局状态。** --- -## 5. 修复计划(三档;待批准后实施,本次审计不改代码) +## 5. 修复计划 ### 第一档:立即(改动小、价值高、风险低,无架构变更) -| # | 项 | 内容 | 关联 | +| # | 项 | 内容 | 级别 | |---|---|---|---| -| 1 | **E-32**(降 P2) | SD2506 init:18H 改"先回读验证 0x82,≠ 则 write_enable→写 18H=0x82→回读→write_disable"(18H 非易失、出厂 0x82,正常路径保留原写);注释更正(0x20/ARST 误注 → 0x7B 为手册 L396 推荐写禁止态值;ARST 在 CTR3 非 CTR1);**已修复 2026-09-21(commit 41e111b:18H 回读校验 + 漂移时写允许窗口重置与回读验证;台架 RTC 套件 PASS)** | P2 | -| 2 | **E-31** | `dhara_map_resume` 按 `dhara_error_t` 分类:仅 `DHARA_E_TOO_BAD` 判 fresh,其余返回失败给 mount 层 + 自检上报,禁止静默 clear;**已修复 2026-09-21(commit da1a6e8:journal.c find_checkblock/find_root/read_checkpage 保留真实 I/O 错误(扫描读失败不再覆盖为 TOO_BAD、读成功但 checkpoint 不符判 CORRUPT_MAP)+ nand_ftl 分类(非 TOO_BAD 返回 STA_NOINIT 给 mount 层)+ freertos.c bring-up 重构为 storage_bring_up() 并加 3×2s 有界重试自愈;构建 0 错误 0 警告)** | P0 | -| 3 | **E-01** | `cfg_file_poll` 前置 `st.size > CFG_FILE_MAX_LEN` 守卫(工厂默认+模板+自检留痕);save 加 `out_len ≤ 4096` 守卫;**已修复 2026-09-22(commit d030cb0:poll 判定基于真实 st.size 前置到读取之前根治死码、超长不触发段回调保留在线通道、save 输出上限收紧 4096、`s_write_doc` 写盘唯一闸口硬拒超长、load/poll 超长处置补自检留痕;构建 0 错误 0 警告;台架 FTP 5047B 用例 poll/load 双路径自愈 PASS + 模拟器 cfgtest 双回归锁场景 PASS;验证暴露并修复关联 `f_rename` 不覆盖缺陷 commit 70f1cc8)** | P0 | -| 4 | **E-34** | CH395F 字节级路径:`HAL_MAX_DELAY` → 有限超时(100~500ms)+ 返回值检查 | P1 | -| 5 | **E-33** | CH395F DMA 传输加错误通道(返回状态/错误标志);失败 → net_send/recv 返回 -1;错误完成分支 INFO→ERROR | P1 | -| 6 | **E-36/E-37** | RS485 契约修复:重武装不清零(或 getter 契约改"钩子内有效"并明示);`rs485_init` 返回码 + 失败日志 | P1 | -| 7 | **E-43** | `gd5f2gq5ue_erase` 补容量边界检查;更正 Trap 03 文档 | P1 | -| 8 | **E-38** | SD2506 负温度统一 `(int8_t)val`(1 行) | P1 | -| 9 | **E-40** | FTL erase/prog:仅 E_FAIL/P_FAIL 判坏块;瞬态 → `DHARA_E_ERASE/DHARA_E_IO` | P1 | -| 10 | **E-41/E-42** | `bbt_save` 检查编程结果(返回码+重试下一槽,`mark_block_bad` 去 void);`bbt_scan` 检查读返回值 | P1 | -| 11 | **E-44/E-45** | TPAFE:实现 FRSTDATA 首通道校验(或拒全 0/全 1);`start_conv_irq` 移除 `s_ready=0`(或改帧序号) | P1 | -| 12 | **E-07** | `f_close` 返回值检查 ×3(store 原子写 / syslog ×2 / 卷探针):close 失败 = 写失败 | P1 | -| 13 | **E-04** | `remo_send` 校验 `ret == len`;短发送 → DBG_ERROR + `net_close` 强制重连补帧 | P1 | -| 14 | **E-03** | errno 出参化:包装器把通知值里的精确码经出参/返回值直传调用方;netTask 侧 locked 函数改局部/出参结果码(消除 `:419-423` 全局读) | P1 | -| 15 | **E-09** | UI 页面局部副本 + 显式保存提交(或写回前基线比对) | P1 | -| 16 | **E-08** | preboot 分支临界区(或队列创建提前) | P1 | -| 17 | **E-39/E-51** | SD2506:写允许窗口内置 ARST=1(写 CTR3=0x80,注意 11H 属保护段须经 write_enable);set_alarm CTR2 改读-改-写保 WRTC 位(按手册 L396 推荐值 0xFF/0x7B) | P1/P2 | -| 18 | **E-35** | 测试 Phase5 常量对齐 ≤2048;驱动超尺寸守卫加告警 + 头文件契约 | P1 | -| 19 | **E-46** | `0xECH` → `0xECU`;pre-commit 增加 `0xNNH` 字面量检查 | P2(1 行) | -| 20 | **E-52** | 开机诊断块检查 `get_*` 返回值 | P2 | -| 21 | **E-17** | RECONFIG 改 `osEventFlagsWait` 原子消费(= P2-11 闭环) | P2 | -| 22 | **E-14** | net init/重建失败补 `syslog_post(SYSLOG_SELFTEST,…)` | P2 | +| 1 | **E-34** | CH395F 位命令路径:`HAL_MAX_DELAY` → 有限超时(100~500ms)+ 返回值检查。**一行级改动,消除 netTask 永久挂死风险** | P1 | +| 2 | **E-04** | `remo_send` 改为返回 `int` 并校验 `ret == len`;短写 → `DBG_ERROR` + 统计;连续失败 → `net_close` 强制重连补帧 | P1 | +| 3 | ~~**E-03 / N-5**~~ | **已修复(2026-09-25)**:错误码改按任务隔离,删除全局 `s_net_errno`(见 §3.1) | ✅ | +| 4 | **E-49** | RS485 TX 超时路径:等 TC / 短延时后再拉低 DE | P2 | +| 5 | **E-15** | `ringbuf` 定去留:修复后启用,或删除(零调用者模块不值得维护) | P2 | +| 6 | **E-61** | SD2506 `clear_int` 注释订正(ARST 在 CTR3,代码从未启用) | P2 | ### 第二档:下一迭代(结构性改造) | # | 项 | 内容 | |---|---|---| -| 1 | **E-02+E-06+E-20+E-21** | 阻塞模型改造:lftpd 控制通道/timeSync 改 `DONTWAIT` + 调用方状态机(ui_sink 模式推广);`net_process_messages` 长阻塞间隙按 tick 继续排空队列;`accept_timeout` 墙钟计时 + 去跨任务 state 直读。**E-02 已全量闭环(2026-09-22)**:阶段一 = `net_recv_to` 分片有界读(f8d6e90+4a1605b,缓解);阶段二 = **L3 pending 化**(f16dc9a,wip/v2-pending-recv)——阻塞 RECV 改挂起请求、执行器零占用零阻塞、调用方分片协议退役,台架全量复验(M3 重锁/功能套件/e02_probe/产品 @test 全 PASS,明细见 §3.2 E-02 段);E-06/E-20/E-21 剩余分量(SEND/ACCEPT 阻塞路径、errno 分级、501 保会话)待办——公共地基 pending 机制一期(RECV)已落地,二期(SEND/ACCEPT)沿用同一机制扩展(利弊/改动量/验证门禁见 `docs/问题记录/Net层阻塞模型业界对照与演进方案.md` §6) | -| 2 | **E-05** | `net_socket/net_bind/net_getsockname` 并入队列(新消息类型);过渡方案:alloc+init 临界区 + `s_dynamic_port` 收归 netTask | -| 3 | **E-10/E-42/E-53/E-54** | GD5F 驱动错误通道统一:init 重试 1~2 次;`wait_busy`/轮询路径 HAL 检查;ECC 使能回读校验;错误向上传递 | -| 4 | **E-22** | lftpd/timeSync 向 ui_sink close 规则看齐(EOF 不 close、auto_relisten 常驻、仅代次变化/判死时重建) | -| 5 | **E-23** | 测试套件加代次检查(accept 失败分支);TC704 降并发或测试构建抑制服务任务 | -| 6 | **E-30(部分)** | sim 后端 errno 语义对齐(成功不清)+ 缺陷记录标注 sim 复现盲区 | +| 1 | **GD5F 错误通道统一**(E-10 / E-42 / E-53 / E-54 / E-56 / E-57) | `gd5f_wait_busy` 内检查三次 HAL 调用并区分"超时"与"真故障";`bbt_scan` 检查读返回值 + **扫描失败时放弃持久化**(保护好副本);init 全链路检查返回值 + 重试 1~2 次;init 后回读 B0h 校验 ECC 使能(bit4);`gd5f2gq5ue_read` 的 raw 契约文档化或加 ECCS 检查;返回类型统一 `int32_t`。**注意 RAM 预算**:新增错误分支不得引入新的大缓冲 | +| 2 | **N-4 二期:ACCEPT + SEND 阻塞路径 pending 化** | 沿用一期(RECV)机制扩展;同期消化 **E-25**(`net_select` 语义)、**E-24 / E-27 / E-28**(跨上下文裸指针收口为状态查询 API)。基线重锁 + 探针族扩充 + 看门狗接 selftest 留痕 | +| 3 | **E-16** | 在 `cfg_file.h` 显式定义段名 / 键名最大长度,注册 API 拒绝超长名字(把余量变成编译期可验证);`snprintf` 返回值改检查式 | +| 4 | **E-26** | UDP 路径定案:并入队列 / 移除 / 文档化"单任务独占"。倾向 **移除**(无用户) | +| 5 | **E-30** | sim 后端 errno 语义对齐 + 缺陷记录标注 sim 复现盲区 | +| 6 | **E-23** | 测试套件加代次断言;TC704 降并发或测试构建抑制服务任务 | -### 第三档:长期 +### 第三档:长期 / 前置门禁 -- 模拟器保真度全面对齐(阻塞模式 = select+超时 / connect CONNECTING 状态机 / accept fd 模型),或文档化"阻塞路径仅 MCU 可验证"(附录 D 逐项) -- E-26 UDP 路径并入队列或移除 + 禁令文档化 -- E-12 `FF_FS_LOCK`;E-15 ringbuf 修复/移除;E-19 死事件位清理;E-18 ftpTask 等待超时;E-13 句柄检查 -- 驱动健壮性长尾:E-47/E-48/E-49/E-50/E-55/E-56/E-57/E-58 -- 消费者任务(adc_task/rs485_task)实现时按 AGENTS.md 注册 + 栈预算,**先修 E-36/E-44/E-45 再接入**(E-59) +- **E-59 门禁**:`adc_task` / `rs485_task` 接入消费者**之前**,必须先完成 + **E-36 / E-44 / E-45 / E-58**(驱动契约修复)+ 栈预算核算 + (两任务尚未纳入 `Src/freertos.c` 栈表)。建议写入 `AGENTS.md`, + 与"改栈前跑 `stack_report.py`"同级。 +- **E-50**:RS485 双缓冲 + 序号(随 E-59 门禁) +- **E-12**:`FF_FS_LOCK` 或固化"单写者纪律"(`AGENTS.md`) +- **E-31 后续 / E-62**:卷自愈触发集的回归监督(纪律已固化,需防后人误改) +- **E-47 / E-48**:驱动健壮性长尾(独立超时码 / WARN 留痕) --- -## 6. 与既有《项目审计改进清单》交叉引用 +## 6. 模拟器保真度差异 -| 既有编号 | 本次对应 | 处置 | -|---|---|---| -| P2-2(ch395f DMA 超时未向上传播) | **E-33** | 独立复核一致(DMA 路径 void 无错误通道) | -| P2-3(bbt_save 丢返回值) | **E-41** | 独立复核一致;并入修复计划第一档 #10 | -| P2-4(dhara_nand_read 弃 read_from_cache) | **E-55** | 独立复核一致 | -| P2-5(负温度解码) | **E-38** | 独立复核一致并**升级 P1**(≤-17°C 全错 + 返回 OK 无指示;既有清单 high 优先级,与 P1 定级一致) | -| P2-6(WRTC 写时序) | **E-51**(子集) | 复核一致:set_alarm CTR2 全量覆盖中途清 WRTC1 → 写保护态混合(并入第一档 #17) | -| P2-7(net_select LISTENING 恒可读) | **E-25**(部分) | 复核一致;本次补充同函数"参数错误不设 errno"子项 | -| P2-8(time_sync 30s 卡死) | **E-02/E-21** | **重大扩展**:30s 不只是同步任务卡死,而是**全体消费者队列停摆**(头阻塞)——修复方案相应从"短超时"升级为阻塞模型改造 | -| P2-10(/log unlink×读竞态) | **E-12** | 独立复核一致 | -| P2-11(RECONFIG 非原子) | **E-17** | 复核一致;**重评为 P2**(命令无载荷、丢失自愈、无数据丢失)——建议从清单 high 调整为 medium | -| P1 级"网络故障无痕迹"(清单 :93) | **E-14** | 独立复核一致 | -| P2-1、P2-9、P2-12~16、P3-1~13 | — | 本次未覆盖其主题(P2-1 外科手术式保存超长行为相邻独立项;其余为装饰宏/构建脚本/栈表等),维持原跟踪 | +核心结论:**09-21 类 errno 缺陷在模拟器结构性不可复现**(单线程 + 成功即清 errno 双重掩盖), +该类修复**只能硬件闭环验证**。 + +| # | 项目 | MCU 后端 | 模拟器后端 | 影响 | +|---|---|---|---|---| +| 1 | accept fd 模型 | 返回监听 fd 自身(单连接) | 返回独立新 fd,监听 fd 常驻 | 依赖"fd 生命周期"的写法移植即泄漏 | +| 2 | EOF 后行为 | `auto_relisten`:fd 回 LISTEN、保持 `in_use` | 无:fd 仍 `in_use` 但 Winsock 已关 | "EOF 后 fd 仍活"的写法在 sim 失真 | +| 3 | recv 阻塞模式 | flags=0 阻塞 / pending | 恒非阻塞,`(void)flags` | **生产阻塞路径在 sim 零覆盖** | +| 4 | send 停滞契约 | 分块阻塞;1s/chunk 停滞 → 返回已发字节 + TIMEDOUT | 单次非阻塞;部分发送 → 部分值 + **NET_OK(静默截断无告警)** | 同一条件双端 errno 不同 | +| 5 | 零长度 send | -1 / INVAL | 0 / OK | 零长用例 sim 必假 FAIL | +| 6 | 零长度 recv | -1 / INVAL | 0 = 对端关闭 | 边界契约不一致 | +| 7 | `net_connect` 语义 | 异步:0 = issued,net_poll 推进状态机 | 进行中 = -1 / WOULDBLOCK;state 永不推进 | TC701 假 FAIL / TC703 负向用例**假 PASS** | +| 8 | 错误码粒度 | 12 种精确码(附录 A) | 仅 BADF / INVAL / WOULDBLOCK + 通用 NET_ERR | 依赖精确码的分类逻辑在 sim 失真 | +| 9 | 成功后 errno | **不清除**(stale 滞留) | **清零** | **本类缺陷在 sim 结构性不可复现** | +| 10 | `net_listen` 前置 | 须 BOUND / CREATED | 仅查 `in_use` | sim 更宽松 | +| 11 | `getsockname` 数据源 | `s_local_ip` 缓存 | Winsock `getsockname` | PASV 响应 IP 双端不同 | +| 12 | 代次号 | reconfig 递增 | 恒 0 | 重建 / 重监听路径 sim 零覆盖 | +| 13 | 事件回调 | net_poll 内 `fire_event` | 只存储不触发 | 回调驱动逻辑 sim 全死 | +| 14 | 并发模型 | 队列 + netTask 串行 SPI | 单主线程直调 Winsock | 跨任务竞态 sim 不可复现 | +| 15 | UDP bind | `open_socket` + SPI | 无对应步骤 | 与 E-26 相关,当前无用户 | +| 16 | `net_init` | 芯片全量初始化 | 仅 `WSAStartup` | 设计对齐 | --- -## 7. 健康结论(审计深度证据:检查过且无问题的关键点) +## 7. 审计边界与未覆盖项 -1. **syslog 队列条目纯输入型**(id + 静态串指针,消费者不回写、生产者不回读)→ 结构性免疫 09-21 类值拷贝陷阱(对照 `net_msg_t.result` 回传字段 = 事故根因) -2. **就绪屏障 5 处等待全部 `osFlagsNoClear`**(`freertos.c:422-424`、`time_sync.c:113-114`、`ui_task.c:48-49`、`net_task.c:177-178`、`ch395f_test_task.c:1387-1388`)——09-21 修复在位;命令位均为单一属主消费 -3. **CFG_SAVE 协议无损**:可消费唤醒位 + `s_save_pending` 锁存 + `s_save_result` 单字节状态机回传 UI——全时序窗口(含 <500ms 双保存合并 = 无损,保存恒以当前 RAM 副本为准)无丢失、无"错误显示成功"可能 -4. **通知值通道独占**:全库无 `osSignal/osThreadFlags`/流缓冲调用者;`LV_USE_OS=0`(MCU)/`LV_OS_NONE`(sim)→ LVGL osal 不竞争;陈旧通知值无假唤醒(`xTaskNotifyWait` 以 pending 状态为准) -5. **`net_select` 纯 RAM 轮询**(调用方无 SPI);"LISTENING 恒可读"偏乐观(现无实际影响,= P2-7) -6. **defaultTask 存储自愈健全**:双确认探针、短写归一 FR_DENIED、`FR_INVALID_NAME` 排除重格式化集、写保护类不重格式化 -7. **内核前纪律**:`app_main_init` 零 CMSIS 调用(HAL_Delay = tick 轮询,非 osDelay);对象创建次序(队列/任务先于 `osKernelStart`);栈溢出/堆失败钩子 = 裸 UART(临界安全) -8. **time_sync 全队列路径、从不读 errno** → 免疫 E-03;**netMsgQueue 深度 8 + `osWaitForever` put**(无丢消息)+ put 返回值全检查 -9. **lftpd 静态缓冲与单会话模型一致**(ftpTask 栈峰值复测 1184B/4096B,余量 71%);队列 wrapper 同步语义挡住 fd 复用竞态(`net_close` 阻塞至 netTask 完成 memset) -10. **lftpd PASV IP 双重字节序转换等价正确**(192.168.1.100 各节验证);recv 0=EOF 在 lftpd/timeSync/ui/phase6/8/10 全部正确区分 -11. **CH395F 中断读清**在单一 SPI 事务内完成(无事务内竞态);DMA 1s 超时 ≫ 最坏传输 ~7ms(无超时误报);`check_exist` 主动回显校验(死芯片不误判);边界检查 `!= CH395F_ERR_SUCCESS` 一致 -12. **CH395F/GD5F 共享 DMA 标志** `volatile` + 字对齐(M4 原子);GD5F CCM 守卫(`gd5f_dma_buf_ok` + 阈值)防 DMA 不可达缓冲入 DMA 路径 -13. **GD5F BBT 加载安全方向**:magic+CRC 双校验,读失败降级出厂扫描;slot 轮换有显式终止(无死循环);FTL 生产主路径读/回读均有 ECC 检查 -14. **GD5F Trap 01/02/03 修复在位**(页地址擦除、read/write 边界检查)——唯 erase 缺边界(E-43,文档与代码不符) -15. **RS485 HAL 错误路径自愈**(F4 gState/RxState 分离:阻塞发送期间重武装可用;ORE 路径 HAL 先复位 RxState=READY)→ E-48 仅健壮性问题 -16. **SD2506 时间写原子性**(7 字节一次写,满足"不可单独写时间寄存器");24h 位/2099 回绕正确;电池电压寄存器 1AH[7]+1BH 9 位与手册一致("读错寄存器"假设被手册证伪) -17. **边界调用方正确**:app_main 对 GD5F 7 码 + TPAFE OK/TIMEOUT 分别映射;sys_clock 全检查 sd2506 返回值 -18. **TPAFE 10ms BUSY 超时** ≫ t_CONV(µs 级)无误报;`s_ready/s_buf_sel` volatile 在位 -19. **已证伪假设留档**(防重复劳动):SD2506 电池电压读错寄存器(手册证伪);RS485 错误回调杀死并发发送期 RX(HAL 源证伪);ch395f.c "2s×HAL_Delay(1) 忙等"(当前代码不存在);SD2506"上电 18H 写无效 → 充电器终身未使能"(手册 §4.8"出厂均配置成 82H"+18H 非易失证伪,原 E-32 P0 链不成立,降 P2,见 §3.3);SD2506 "write_enable 仅置 WRTC1/WRTC3、WRTC2=0 与手册矛盾"(原 E-60 前提)——0x84=0b1000_0100 本已含 D2=WRTC2(手册 L119 表头 D7~D0 MSB 先、CTR1 D2=WRTC2/D7=WRTC3、CTR2 D7=WRTC1),write_enable 实为三者全 1 且顺序合规(L392 先 WRTC1 后 WRTC2/WRTC3),开源序列同码(DFRobot GravityRtc 官方库与社区代码均 0x10←0x80、0x0F←0x84,注释 "WRTC2=1,WRTC3=1"),实测 RTC set==readback PASS 证明写允许已生效;原台架实测项无需执行,E-60 移除(§3.3 原行已注释存档) -20. **模拟器方向**:`ui_net_sink` 已双模型自适应(单/多连接 accept fd);sim 构建仅编 UI+net_sink+配置日志引擎 → lftpd/timeSync 的 fd 泄漏类缺陷当前未触发(E-30 潜在项) +- **未逐行审计**:`Lib/FatFs` 移植层内部(已知陷阱已在 `AGENTS.md` 固化)、 + `Lib/dhara` 内核(仅审 glue 映射层与错误分类)、lftpd 的字符串 / IO 辅助模块(边界抽查)、 + `Middlewares/` CMSIS / HAL 内部(错误路径抽查) +- **模拟器**:16 项差异见 §6;09-21 类 errno 缺陷**只能硬件验证** +- **休眠缺陷的性质**:RS485 / TPAFE 侧 **E-50 / E-58** 因消费者为空桩而暂不可触发, + 接入消费者前必须先修(E-59 门禁) +- **基线**:本报告基于 2026-09-24 `v2-on-ui` @ `9a8255f`;后续代码变更需增量复核 +- **文档一致性提醒**:本轮复核发现首版存在若干"**文档内闭环比代码先行**"的情况 + (例如 E-26 / E-27 / E-28 在首版矩阵中已被标为 `✓`,但代码实际未改)。 + **建议后续复核一律以当前源码为准,不以既有矩阵为准。** --- -## 8. 审计边界与未覆盖项 - -- **未逐行审计**:`Lib/FatFs` 移植层内部(已知陷阱已在 AGENTS.md 固化:FA_OPEN_APPEND 无 FA_WRITE、短写 FR_OK、8.3 短名)、`Lib/dhara` 内核(仅 audit glue 映射层与错误分类)、lftpd 的字符串/IO 辅助模块(边界抽查)、`Middlewares/` CMSIS/HAL 内部(错误路径抽查) -- **模拟器保真度**:16 项差异全表见附录 D;核心结论 = **09-21 类 errno 缺陷在 sim 结构性不可复现**(单线程 + 成功即清 errno 双重掩盖),该类修复只能硬件闭环验证 -- **驱动契约缺陷的"休眠"性质**:RS485/TPAFE 侧多项 P1(E-36/E-44/E-45/E-58)因消费者任务为空桩而暂不可触发——**接入消费者之前必须先修**(§5 第三档) -- 本审计基于 2026-09-21 代码状态(`v2-on-ui` @ `a9aa8ce`);后续代码变更需增量复核 - ---- - -## 附录 A:NET_ERR 错误码表(`net_types.h:98-109`) +## 附录 A:NET_ERR 错误码表(`Drivers/BSP/NET/net_types.h:98-109`) | 码 | 值 | 含义 | 通知值语义 | |---|---|---|---| -| NET_ERR_OK | 0 | 成功 | 成功:通知值 = 字节数/fd(≥0) | -| NET_ERR | -1 | 通用错误 | 失败:通知值 = 负的精确错误码 | -| NET_ERR_BADF | -2 | 无效 fd(含代次失效) | | -| NET_ERR_INVAL | -3 | 参数无效 | | -| NET_ERR_NOMEM | -4 | fd 池/缓冲耗尽 | | -| NET_ERR_NOTCONN | -5 | 未连接 | | -| NET_ERR_ISCONN | -6 | 已连接 | | -| NET_ERR_WOULDBLOCK | -7 | 无数据/不可写(正常轮询态,**非错误**) | | -| NET_ERR_CONNRESET | -8 | 对端复位 | | -| NET_ERR_TIMEDOUT | -9 | 超时(recv 30s / send chunk 1s / listen 重开) | | -| NET_ERR_NOSPACE | -10 | 芯片缓冲满 | | -| NET_ERR_BUSY | -11 | 芯片忙(含 listen 端口占用) | | +| `NET_ERR_OK` | 0 | 成功 | 成功:通知值 = 字节数 / fd(≥0) | +| `NET_ERR` | -1 | 通用错误 | 失败:通知值 = 负的精确错误码 | +| `NET_ERR_BADF` | -2 | 无效 fd(含代次失效) | | +| `NET_ERR_INVAL` | -3 | 参数无效 | | +| `NET_ERR_NOMEM` | -4 | fd 池 / 缓冲耗尽 | | +| `NET_ERR_NOTCONN` | -5 | 未连接 | | +| `NET_ERR_ISCONN` | -6 | 已连接 | | +| `NET_ERR_WOULDBLOCK` | -7 | 无数据 / 不可写(**正常轮询态,非错误**) | | +| `NET_ERR_CONNRESET` | -8 | 对端复位 | | +| `NET_ERR_TIMEDOUT` | -9 | 超时(recv / send chunk / listen 重开) | | +| `NET_ERR_NOSPACE` | -10 | 芯片缓冲满 | | +| `NET_ERR_BUSY` | -11 | 芯片忙(含 listen 端口占用) | | -超时参数(`net_config.h`):`NET_SEND_TIMEOUT_MS=1000`、`NET_RECV_TIMEOUT_MS=30000`(E-02/E-21 的改造对象)。 +超时参数(`net_config.h`):`NET_SEND_TIMEOUT_MS=1000`、`NET_RECV_TIMEOUT_MS=30000`。 ## 附录 B:事件位表(`App/sys_events.h`) -| 位 | 名称 | 语义 | 置位者 | 消费者(等待方式) | +| 位 | 名称 | 语义 | 置位者 | 消费者 | |---|---|---|---|---| -| bit0 | NET_READY | 状态位 | netTask(组网成功) | ftp/ui/timeSync(`WaitAny\|NoClear` ✓) | -| bit1 | STORAGE_READY | 状态位 | defaultTask(挂载+配置加载完成) | 同上 | -| bit2 | FTP_READY | **死位**(置位代码不可达,E-19) | (lftpd_start 返回后——永不) | 无 | -| bit3 | TIMESYNC_READY | **死位**(从未置位,E-19) | 无 | 无(timeSync 实际等 NET/STORAGE) | -| bit4 | NET_RECONFIG | 命令位(可消费) | defaultTask(UI 保存网口配置) | netTask(Get+Clear 非原子,E-17) | -| bit5 | CFG_SAVE | 命令位(可消费) | uiTask | defaultTask(`WaitAny` 500ms + pending 锁存 ✓) | -| bit28-30 | — | 保留 | — | — | +| bit0 | `NET_READY` | 状态位 | netTask(组网成功) | ftp / ui / timeSync(`WaitAny\|NoClear` ✓) | +| bit1 | `STORAGE_READY` | 状态位 | defaultTask(挂载 + 配置加载完成) | 同上 | +| bit2 | `FTP_READY` | 状态位(已改为可达) | ftpTask(`lftpd_start` 之后) | 预留给依赖 FTP 的功能 | +| bit3 | `TIMESYNC_READY` | **未置位**(仍无置位者) | 无 | 无(timeSync 实际等 NET / STORAGE) | +| bit4 | `NET_RECONFIG` | 命令位(可消费) | defaultTask(UI 保存网口配置) | netTask(已改原子消费 ✓) | +| bit5 | `CFG_SAVE` | 命令位(可消费) | uiTask | defaultTask(`WaitAny` 500ms + pending 锁存 ✓) | -## 附录 C:项目队列清单与回传机制 +> **bit3 说明**:`TIMESYNC_READY` 仍未置位,属 E-19 的残留项。因当前无消费者, +> 影响为零;建议在 N-4 二期一并处置(置位或删除,不要长期留一个"假位")。 -| 队列 | 定义处 | 深度 | 条目形态 | 回传机制 | 免疫/风险 | +## 附录 C:队列清单与回传机制 + +| 队列 | 定义处 | 深度 | 条目形态 | 回传机制 | 风险 | |---|---|---|---|---|---| -| `netMsgQueue` | `Src/freertos.c:166` | 8 | `net_msg_t` **按值**(fd/长度/输入指针) | 任务通知值(精确错误码/字节数);指针字段回写(net_accept 地址) | 09-21 后契约健全;头阻塞(E-02)经 L3 pending 化解(f16dc9a:执行器零阻塞,阻塞 RECV 等待 = 挂起请求);残留 = 全局 errno 竞态(E-03) | -| syslog 队列 | `sys_log_mcu.c:218-223`(惰性) | 64 | 输入型(id + 静态串指针) | 无回传(fire-and-forget) | 结构免疫 P1;preboot 窗口(E-08) | -| `cfgSaveQueue` | `freertos.c`(defaultTask/uiTask 间) | 8 | 输入型(路径指针) | `s_save_pending` 锁存 + `s_save_result` 状态机 | 协议无损(§7-3) | - -## 附录 D:模拟器保真度差异表(MCU `net_socket.c` vs `simulator/sim/net_socket_sim.c`) - -| # | 项目 | MCU 后端 | sim 后端 | 影响 | -|---|---|---|---|---| -| 1 | accept fd 模型 | 返回监听 fd 自身(单连接 `g_client==g_listen`) | 返回独立新 fd,监听 fd 常驻(`sim:331-347`) | lftpd 式生命周期在 sim 泄漏 fd(移植即触发);ui_sink 已双模型自适应 | -| 2 | EOF 后行为 | auto_relisten:fd 回 LISTEN、in_use 保持 | 无:fd 仍 in_use 但 Winsock 已关,二次 recv 报通用 NET_ERR | 依赖"EOF 后 fd 仍活"的写法在 sim 失真 | -| 3 | recv 阻塞模式 | flags=0 阻塞,30s → -1/TIMEDOUT | 恒非阻塞(FIONBIO),`(void)flags`(`sim:447` 已复核),无 TIMEDOUT 路径 | **生产阻塞路径在主开发后端零覆盖**(E-02/E-06/E-20/E-21 类缺陷 sim 不可见) | -| 4 | send 停滞契约 | 分块阻塞;1s/chunk 停滞 → 返回已发字节(可 0)+ TIMEDOUT | 单次非阻塞:WSAEWOULDBLOCK → 0 + WOULDBLOCK;部分发送 → 部分值 + **NET_OK(静默截断无告警)** | 同一条件双端 errno 不同 | -| 5 | 零长度 send | -1/INVAL | 0/OK | phase10 零长用例 sim 必假 FAIL | -| 6 | 零长度 recv | -1/INVAL(`len<=0`) | 0 = 对端关闭(`sim:453` 检查为 `len<0` → recv(0) → EOF 分支) | 边界契约不一致 | -| 7 | net_connect 语义 | 异步:0=issued,netTask net_poll 推进 CONNECTING→ESTABLISHED | 进行中 = -1/WOULDBLOCK;state 永不推进(net_poll 空操作) | TC701 假 FAIL / TC703 负向用例**假 PASS** | -| 8 | 错误码粒度 | 12 种精确码(附录 A) | 仅 BADF/INVAL/WOULDBLOCK + 通用 NET_ERR | 依赖精确码的分类逻辑在 sim 失真 | -| 9 | 成功后 errno | **不清除**(stale 滞留,`net_socket.c:1609`) | **清零 NET_OK**(12 处,`sim:461` 等已复核) | **09-21 类缺陷在 sim 结构性不可复现**(+ 单线程无交错)——主开发后端对该缺陷族零灵敏度 | -| 10 | net_listen 前置 | 须 BOUND/CREATED | 仅查 in_use | sim 更宽松 | -| 11 | getsockname 数据源 | s_local_ip 缓存(静态 IP;DHCP → 0.0.0.0) | Winsock getsockname(INADDR_ANY → 127.0.0.1) | PASV 响应 IP 双端不同 | -| 12 | 代次号 | reconfig 递增 | 恒 0 | 重建/重监听路径 sim 零覆盖 | -| 13 | 事件回调 | net_poll 内 fire_event | 只存储不触发 | 回调驱动逻辑 sim 全死(当前无注册者) | -| 14 | 并发模型 | 队列 + netTask 串行 SPI | 单主线程直调 Winsock | 跨任务 errno 竞态 sim 不可复现 | -| 15 | UDP bind | open_socket + SPI(仅 UDP 分支) | 无对应步骤 | 当前无 UDP 用户 | -| 16 | net_init | 芯片全量初始化 | 仅 WSAStartup | 设计对齐 | +| `netMsgQueue` | `Src/freertos.c:166` | 8 | `net_msg_t` **按值** | 任务通知值(精确错误码 / 字节数)+ 指针字段回写 | 头阻塞已由 L3 pending 化解;errno 竞态已由 E-03 修复(按任务隔离) | +| syslog 队列 | `App/log/sys_log_mcu.c:218-223`(惰性创建) | 64 | 输入型(id + 静态串指针) | 无(fire-and-forget) | 结构免疫 P1;preboot 窗口已并轨 | +| `cfgSaveQueue` | `Src/freertos.c` | 8 | 输入型(路径指针) | `s_save_pending` 锁存 + `s_save_result` 状态机 | 协议无损 ✓ | --- -*报告完。59 项发现全部带 file:line 落点;P0/P1 核心机制经父审计组逐行复核(含 `docs/芯片手册/SD2506API-G.md` L392-398/L537/§4.8 原文核对与 `SD2506_Trap_Records.md` 硬件观测交叉验证)。2026-09-21 复核修订:E-32 由 P0 降 P2(充电器 P0 链被"出厂 82H"证伪)、E-39 机制改写(ARST 在 CTR3、驱动从未写过 CTR3)、E-60"write_enable WRTC2 与手册矛盾"经手册表头位序 + DFRobot GravityRtc 官方库/社区代码 + 实测三方复核证伪(0x84 本已含 D2=WRTC2,代码与手册 L392 一致)移除,见 §7-19;18H 寄存器事实经手册 §4.8 专用表复核(Bit7=EN_Charge、Bit1=Charge1、Bit0=Charge0,复位 0x03,出厂 0x82=EN+5k,总表 L119 该行转录有误以 §4.8 为准);§6 交叉引用补 P2-2/P2-6。审计期间未修改任何代码、未执行构建;2026-09-21 依第一档 #1 完成 E-32 修复(commit 41e111b:18H 回读校验 + 漂移时写允许窗口重置与回读验证、0FH 死写与误注移除、充电宏位订正;构建 0 错误 0 警告,台架 RTC 套件 Phase 1 6/6 + Phase 2 累计 17/17 PASS 含 set==readback,产品固件重烧后启动校验 PASS,ST-LINK 全片回读 No difference);同日依第一档 #2 完成 E-31 修复(commit da1a6e8:journal.c 三处错误源治理——扫描读失败不再覆盖为 TOO_BAD、读成功但 checkpoint 不符判 CORRUPT_MAP + nand_ftl.c 错误分类——仅真空白判 fresh、其余失败返回 STA_NOINIT 给 mount 层 + freertos.c 启动块重构为 storage_bring_up() 并加 3×2s 有界重试自愈策略;构建 0 错误 0 警告;产品固件台架回归 PASS:`@test` 闭环 Required(含 `Map resumed`/`FATFS mounted`)全中、Forbidden 零命中,既有卷 2441/96064 扇区 resume 成功、用户配置 523B 原样加载、FTP 屏障链通,ST-LINK 全片回读 No difference);2026-09-22 依第一档 #3 完成 E-01 修复(commit d030cb0:poll 超长判定基于 `cfg_store_stat` 真实 st.size 前置到 `s_load_file` 之前根治死码、超长走工厂默认+重建模板+自检留痕且不触发段回调保留在线修复通道、save 输出上限 CFG_OUT_MAX→CFG_FILE_MAX_LEN、`s_write_doc` 写盘唯一闸口硬拒超长、load/poll 超长处置补 SYSLOG_SELFTEST 留痕;构建 0 错误 0 警告;台架验证 PASS:FTP STOR 5047B → poll 守卫 4s 内触发并重建 523B 工厂模板(RETR 实测)、设备保持在线、重启 load 守卫同链自愈、再重启零 `[ERR]` 无销毁循环、selftest.log 双留痕、模拟器 cfgtest 两个新回归锁场景 PASS、@test 默认闭环 PASS、ST-LINK 全片回读 No difference;验证中暴露并修复关联缺陷 `cfg_store_write_atomic` f_rename 对已存在目标不覆盖致设备端一切覆盖写静默失败(commit 70f1cc8:rename 前先 unlink 目标),详见 E-01 明细"关联新发现");2026-09-22 依第二档 #1 完成 E-02 修复(commit f8d6e90:lftpd 侧 `net_recv_to` 5ms 分片有界读 + 30s 总预算,M3 压力档 17.7s 尖峰归零;commit 4a1605b:timeSync 侧同模式改造 + 超时留痕;产品构建 `@test` 闭环 PASS;台架 A/B(`test/e02_probe.py`):挂起对时客户端下 FTP banner 旧 28554ms → 新 109ms/75ms、30s 预算拆连留痕、监听恢复行为同构、正常对时路径全通;详见 E-02 明细"已修复");2026-09-22 依第二档 #1 完成 E-02 阶段二(commit f16dc9a,分支 wip/v2-pending-recv):L3 pending 化——阻塞 RECV 改挂起请求(每 fd 一个、cancel/settle 必通知),完成判定收敛 netTask 主循环节拍 `net_pending_tick`(数据→截止→CLOSED 排空/EOF+relisten→1s 看门狗),执行器零占用零阻塞;lftpd/timeSync 分片协议退役为剩余预算循环;公开 API/返回语义不变。台架全量复验:M3 重锁静默 med 58.9/max 72.1ms、压力档 med 60.1/max 69.3/零尖峰(阶段一基线 65.0/133.5)、MCU 侧 prod_cycle_mcu 6706µs 压力下平坦;功能套件 6/9/11 全 PASS + Phase 8-RECV 100KB 阻塞接收 50/50 块 + CRC32 一致;e02_probe 队列响应性 92.5ms、30s 截止 30.1s 精确触发、TIME 路径全通;产品 `@test` PASS(netTask 栈峰值 488B/4096=88% 余量);RAM +200B/Flash +608B。台架新发现(A/B 归因与 L3 无关,转 `docs/待处理内容.md`):4096B 经 2KB 芯片缓冲的回显残留停滞(切片构建同现)+ 一次 49s defaultTask/NAND syslog 排空停滞。* +*报告完。2026-09-25 重写(**重写时点**:已闭环 33 项移入 §2 索引,未闭环 26 项全部回到 +`v2-on-ui` 当前代码逐项复核,新增 **E-24 / E-26 / E-27 / E-28 / E-61 / E-62** 及 +**E-59 门禁条**)。审计阶段未修改任何代码、未执行构建。* + +*2026-09-25 同日后续:依 §5 第一档 #3 完成 **E-03 修复**(错误码改按任务隔离,删除全局 +`s_net_errno` → `s_net_errno_slots[8]` + `net_set_errno()` + `NET_ERR_RETURN_1()` 宏, +共替代 82 处赋值-返回对;`net_get_errno()` 对外语义不变)。构建 0 错误 0 警告、 +cppcheck 无告警、`git diff --check` 干净、编码无 `U+FFFD` 且 BOM 状态一致;BSS +64 B。 +未闭环数量 26 → **25**(P1 6 → 5)。**台架硬件闭环未执行**——本机 COM6 未枚举 +(ST-Link 虚拟串口不在,`run_boot_test.py` 报 `open failed`)→ E-03 待补台架回归 +(建议复用既有 FTP/UI 推流路径 + `net_test_task` Phase 7 的 errno 判定分支)。*