E11:服务器侧 KeepAlive 未开启(静态证据链 + 注入手段两次修正 + 实测 生产参数 30.0s 判死、空闲存活会话不误杀),给出采纳建议;防火墙规则 对已建立连接无效、有效注入 = 禁用 PC 网卡,两条方法学留痕。 E12:缓存区读写路径长度→数据零间隔属规格偏离(读 0.4us/写 0.2us), 但 460 轮/约 58MB 字节级校验未测得可复现危害;依 Trap 25 登记偏离、 B 变体(两段+TSL,+28B,吞吐无回退)留作待用补丁。 残渣风暴态跨烧录存活污染 A/B 连接级对比,教训写入 E12:实验前后必须 冷启动(断电重上电),MCU 覆写烧录/RESET_ALL 不足以清除。 新增 test/host/e11_keepalive_probe.py(idle/block 两模式 + 注入正对照) 与 test/host/ftp_integrity_test.py(STOR→RETR 全量 SHA-256 比对, 建议纳入常备回归集)。
97 KiB
CH395F 驱动陷阱记录
记录开发过程中遇到的所有坑、根因、解决方案,避免重复踩坑。
Trap 01:Socket 4~7 自动分配不到
现象
多连接模式下最多接受 3 个客户端(Socket 13),Socket 47 的 CONNECT 中断从不触发,新客户端 SYN 被静默丢弃。
根因
net_listen() 中数据 Socket 的配置顺序错误。CH395F 在 TCP_LISTEN 时刻一次性扫描当前所有可用 Socket 并建立内部查找表。如果数据 Socket 4~7 在 TCP_LISTEN 之后才配置缓冲区/协议/端口,自动分配逻辑不认识它们。
正确顺序
- 先配置数据 Socket 1~7:
SET_SEND_BUF → SET_RECV_BUF → SET_SOUR_PORT → SET_PROTO_TYPE_TCP - 最后配置监听 Socket 0:
SET_SEND_BUF → SET_RECV_BUF → SET_PROTO_TYPE_TCP → SET_SOUR_PORT → OPEN_SOCKET → TCP_LISTEN
注意事项
- 数据 Socket 不调
OPEN_SOCKET,由 CH395F 在连接到达时自动打开 - DISCONNECT 后 CH395F 自动关闭 Socket,但保留协议/端口/缓冲区配置,可被再次分配
docs/芯片手册/CH395F.md§9.2.6 要求数据 Socket 先设源端口再设协议类型(与监听 Socket 顺序相反)
对应文件
Drivers/BSP/NET/net_socket.c → net_listen()
发现时间
Phase 8 压力测试,2025-07
Trap 02:数据 Socket 缓冲区重叠
现象
多连接模式下多个 Socket 数据互相覆盖,收到错误数据或客户端挂死。
根因
CH395F 默认只为 Socket 03 各分配独立收发缓冲(共 48 块 × 512B 用满,见7 为零块。docs/芯片手册/CH395F.md §5.45),Socket 4net_listen() 未对 Socket 47 显式分配独立缓冲区块,后续代码误用相同块号(如 2831)导致数据重叠。
解决方案
每个数据 Socket 独占 4 个缓冲区块(2 发 + 2 收),按 ds * 4 基址分配,确保不重叠:
| Socket | 发送块 | 接收块 |
|---|---|---|
| 1 | 4~5 | 6~7 |
| 2 | 8~9 | 10~11 |
| 3 | 12~13 | 14~15 |
| 4 | 16~17 | 18~19 |
| 5 | 20~21 | 22~23 |
| 6 | 24~25 | 26~27 |
| 7 | 28~29 | 30~31 |
对应文件
Drivers/BSP/NET/net_socket.c → net_listen()
发现时间
Phase 8 压力测试,2025-07
Trap 03:DHCP 包 xid 偏移错误
现象
Python DHCP Server 收到 DISCOVER 包后无法匹配 Offer/Ack 的 xid,握手失败。
根因
错误使用了 data[232:236] 作为 xid(Wireshark 某旧版显示误导),实际 DHCP 标准偏移为 data[4:8]。
正确偏移
- xid:
data[4:8] - msg_type:
data[242] - client_mac:
data[28:34] - magic cookie:
data[236:240]
对应文件
test/host/ch395f_socket_test.py → DHCP server 模式
发现时间
Phase 6, 2025-07
Trap 04:UDP 发送后必须等待 SENDBUF_FREE
现象
UDP 发送后立即执行接收操作,接收长度始终为 0 或数据错误。
根因
CH395F 手册要求每次 WRITE_SEND_BUF 后必须等待 SINT_STAT_SENBUF_FREE 中断,否则下次写入或接收操作可能失败。
解决方案
每次 ch395f_write_send_buf() 后轮询 ch395f_get_sock_int_status() 检查 SINT_STAT_SENBUF_FREE 标志。
对应文件
Drivers/BSP/CH395F/ch395f.c / test code in ch395f_test.c
发现时间
Phase 6 广播宣告模式,2025-07
Trap 05:CH395F 与 RTL8305NBI 自动协商不兼容
现象
ch395f_get_phy_status() 始终返回 PHY_DISCONN,链路无法建立。
根因
CH395F PHY 与 RTL8305NBI-CG 直连时自动协商失败。
解决方案
初始化协议栈后强制设为 100M 全双工:
ch395f_set_phy(CH395F_PHY_100M_FULL);
对应文件
Drivers/BSP/CH395F/ch395f.c
发现时间
项目初期硬件联调
Trap 06:TCP KeepAlive 参数必须为 500ms 倍数
现象
设置 KeepAlive 后 TCP 连接几秒内 TIMEOUT 断开。
根因
CH395F 内部定时器以 500ms 为基准单位,传入非 500ms 倍数的值导致未定义行为。
且 IDLE 必须 > INTVL。
正确值
ch395f_set_keepalive_idle(60000); // 60s(500 倍数)
ch395f_set_keepalive_intvl(5000); // 5s(500 倍数)
ch395f_set_keepalive_cnt(3); // 3 次
对应文件
Drivers/BSP/NET/net_socket.c → net_init()
发现时间
Phase 2 测试,2025-07
Trap 07:TCP 关闭重连直接 close,不要 disconnect
现象
disconnect → close 后 Socket 卡在 FIN_WAIT_2 数分钟,无法重新打开。
根因
disconnect 发送 FIN 将 Socket 推入 FIN_WAIT_2,此后 close 不再发 RST,必须等远端发 FIN 才能关闭。
解决方案
直接调用 ch395f_close_socket()(做法保留,原"发 RST"的说法见下方定案更正)。
关闭后轮询 ch395f_get_socket_status() 的第 1 字节等待 sock=0x00 再 open_socket。
定案更正(2026-10-11):机制说法作废,实测 disconnect → close 不卡死
新增定案用例 TC-NET-204(test/board/ch395f_test_task.c,用例表见 docs/测试说明/CH395F_Test_Guide.md):同一 Socket、同一 PC 回显服务、FUN_PARA=0x08、硬件层独占,三模式各 5 轮(45 项判定全过):
| 模式 | 关闭序列 | 回到 SOCKET_CLOSED 耗时(5 轮) |
重开+重连+回显 |
|---|---|---|---|
| A | close |
0 / 0 / 150 / 0 / 0 ms | 5/5 OK |
| B | disconnect → close |
0 / 0 / 0 / 0 / 0 ms | 5/5 OK |
| C | disconnect(不 close) |
10 s 内从未回到,byte0 恒 0x05、byte1 0x00 |
直接 open → 0x20 FAIL(5/5);补 close 后 +0 ms 回 CLOSED,重开 5/5 OK |
三条推翻原根因:
FIN_WAIT_2(0x06) 全程未出现(15 轮 × 9 个采样点)。实测过渡态只有TIME_WAIT(0x0A)(A 模式一轮在 +0/+50/+100 ms 可见,+150 ms 回 CLOSED)与TCP_CLOSED(0x00)。close无 RST 语义:§5.35 只说"关闭 Socket 前 CH395 会自动断开 TCP 连接"。手册中芯片主动发 RST 只出现在 §5.29(监听 Socket 已有连接时再来连接 →"发送 TCP RESET")与 §9.2.6(多连接找不到可用数据 Socket →"将连接复位")两处,均与close/disconnect无关;此外仅有错误码CH395_ERR_RST与 RST/RSTI 引脚。且主动发 FIN 先进FIN_WAIT_1(05H)、收到 ACK 才进FIN_WAIT_2(06H)——"disconnect 推入 FIN_WAIT_2"本身就跳了一拍。- 观测口径错在把 byte1 当独立事实:§5.22 第 1 字节只有
00H/05H两个取值,第 2 字节才是 TCP 态;官方 SDKCH395CMDGetSocketStatus()注释明确"Second byte — TCP state, meaningful only if TCP mode and the first byte is open"。当年 Phase 2 收尾代码的判据用的是正确的 byte0(st[0]==CH395F_SOCKET_CLOSED),break 之后照样打印 byte1 → 一个"已 CLOSED 状态下的残留 TCP 态读数"被读成了"卡在某个 TCP 态"。
真正的原因(C 模式 5/5 复现):FUN_PARA bit3 SOCKET_CLOSE=1(§5.52 ②,0x08/0x09 都置了)下,产生 DISCONNECT 后芯片不关 Socket、不清缓冲,Socket 长期停在 byte0=0x05(OPEN) / byte1=0x00(TCP_CLOSED);此时直接 open_socket 必返 0x20 FAIL——正是 2026-07-19 测试指南问题表里那条 Phase 2 open_socket: 0x20 (FAIL) / socket 未完全关闭。把 Socket 拉回 CLOSED 的动作是 close,与 RST 无关。
git 侧佐证:ch395f_tcp_disconnect() 自首个提交起只有定义、零调用点(git log --all -S 在 NET/App/test 仅命中本仓 bca6cc6 一次),"disconnect→close" 序列从未进入任何提交代码;本条"发现时间 2025-07"也与仓库不符(仓库首个提交 b1d05091 = 2026-06-05,Phase 2 对应 2026-07-19 的 17109d9b→7a8a97f4→a08d26bc)。当时驱动已在轮询 GET_CMD_STATUS(4 s 上限),故也不是"命令没等完成"。
做法为何仍然对(换成三条有支撑的理由):① §5.35 close 已自动断链,disconnect 纯属多余;② disconnect 会制造一个 DISCONNECT 事件,bit3=1 下芯片不自动收尾,该事件必须被消费/预清,否则污染下一条连接(Trap 14 / 17 / 22 实证);③ 它把芯片推进 closing 过渡态,重开需要沉降(Trap 09)。本轮 15 轮重开均未观察到残留 DISCONNECT(0x10)——close 顺带清掉了锁存,这正是"直接 close"安全的原因。
与 Trap 17 的张力已消解:Trap 17 写"close 后芯片要等对端 FIN/ACK 才把 Socket 置 CLOSED(耗时数毫秒)",与本条原"close 发 RST 瞬间回 CLOSED"矛盾;实测支持 Trap 17 一侧(A 模式可见 TIME_WAIT 过渡、0~150 ms),不支持 RST 说法。
发现时间
Phase 2 TCP Client 测试,2025-07(日期有误,实为 2026-07-19;定案更正与 TC-NET-204 实测 2026-10-11)
Trap 08:RECV 中断电平触发,避免无限循环
现象
net_poll() 中 RECV 中断无限触发,主循环(echo 等)永远得不到执行。
根因
CH395F 的 RECV 中断是电平触发的——只要接收缓冲区有数据就保持 INT# 低电平。
do { ... } while(int_status != 0) 会无限循环。
解决方案
net_poll() 使用 do { ... } while(0) 每次只处理一批中断,由主循环负责读取数据。
对应文件
Drivers/BSP/NET/net_socket.c → net_poll()
附:CH395F 驱动关键点
初始化顺序
- 必须按手册9.2.1节顺序:
SET_MAC → SET_IP/GWIP/MASK → INIT_CH395 → SET_PHY(违反此序曾导致 MAC 不生效,见 Trap 24) - IP/网关/掩码必须在
INIT_CH395之前设置,INIT 会读取并锁定当前寄存器值到协议栈 SET_PHY必须在INIT_CH395之后,复位 MAC/PHY 建立物理链路,不影响已锁定的协议栈参数CMD_PING_ENABLE不需要显式调用,INIT 后默认可用
SPI 通信
- 每次 SPI 事务需调用
ch395f_spi_begin()/ch395f_spi_end()包裹 - 大数据量收发使用 SPI2 DMA:
ch395f_write_send_buf()和ch395f_read_recv_buf()已改造为 DMA 批量传输(DMA1_Stream3 RX, DMA1_Stream4 TX),命令和配置操作仍使用逐字节轮询 - ⚠ TSL 规格偏离(N34 E12,2026-10-13 登记):手册 §7.4 要求缓存区命令在
"长度与数据之间"留 TSL(读 0.4us / 写 0.2us;其余字节间无需间隔;非缓存区命令
另有 TSC=0.6us / TSD=0.3us)。本仓两个缓存区函数把命令头与数据打包进同一
DMA 突发(CS 全程拉低)→ 该间隔为 0,属规格偏离;其余命令逐字节传输,
TSC/TSD 有大余量。SPI2 = 5.25MHz(分频 8 @42MHz,单字节 1.52us)。
实测未发现危害:460 轮 / ≈58MB 双向 SHA-256 全量比对仅 1 次未复现异常,
且"两段+TSL"变体(+28B,吞吐无回退)与现状无差异 → 依 Trap 25 铁律暂不改产品
路径,偏离与证据登记于此、变体留作待用补丁。完整 A/B 与方法学见
docs/网络/N34-自愈审计/实验方案.mdE12。 - DMA 缓冲区:
s_spi2_dma_tx_buf/rx_buf[CH395F_SPI_DMA_BUF_SIZE]= 接收缓冲 2048 + 4 = 2052(ch395f.h:148),4 字节对齐;单次有效载荷上限 2048(历史值 1500 / 4100 均已作废,见 Trap 13 复核注)
TCP 参数配置(必须在 INIT_CH395 之前)
- 重传参数:
SET_RETRAN_COUNT(默认12次,最大20)和SET_RETRAN_PERIOD(默认500ms,最大1000ms),总重传时间 = 次数 × 周期 - KeepAlive 参数必须为 500ms 的倍数:
IDLE(默认20000ms)、INTVL(默认15000ms),且 IDLE > INTVL;传入非 500 倍数的值导致 CH395F 内部定时器异常 - KeepAlive 默认关闭:需在
SINT_STAT_CONNECT后调用ch395f_set_keepalive_enable(sock, 1)启用 - ⚠ 本仓现状与 E11 实测(2026-10-13):
ch395f_set_keepalive_enable全仓仅 客户端分支(CONNECTING)调用,而App/无任何net_connect()→ 产品上 KeepAlive 实际全程关闭;服务器侧调用 2026-10-12 曾加后撤回(理由仅覆盖 物理链路失效)。E11 用"禁用 PC 网卡"注入"链路在、对端静默"(防火墙规则对 已建立连接无效,见实验方案 E11 方法学),实测服务器侧开启后:生产参数 60s/5s/3 → 判死 ≈30.0s(早于手册公式 75s,以实测为准);短参数 6s/2s/2 → ≈5.1s(证明参数生效);静默但存活的对端不被误杀(90s 空闲 KEEPLIVE 回显正常)。消费链路现成:TIM_OUT →handle_timeout_eventQUARANTINE → pump relisten → 端口恢复。完整记录见docs/网络/N34-自愈审计/实验方案.mdE11。
Socket 4~7 缓冲区
- CH395F 默认只给 Socket 0
3 分配独立收发缓冲(共 48 块 × 512B 用满),Socket 47 为零块 - 多连接模式下使用 Socket 4~7 时,必须在
open_socket之前显式分配:ch395f_set_send_buf(sock, start_block, count)ch395f_set_recv_buf(sock, start_block, count)
- 不分配会导致发送数据为固定垃圾内容(
0x0028填充)、接收缓冲区无法存储数据
TCP 关闭重连
- 直接调用
ch395f_close_socket(),不要先调ch395f_tcp_disconnect()(做法不变;机理口径已于 2026-10-11 由 TC-NET-204 实测更正,见 Trap 07「定案更正」) - ✅
close直接:§5.35 关闭前芯片自动断链,无需先 disconnect;实测 0~150 ms 回到SOCKET_CLOSED,过渡态可见TIME_WAIT(0x0A)——不是 RST(手册无 RST 语义) - ❌ 只
disconnect不close:FUN_PARAbit3=1(§5.52 ②)下芯片不自动关 Socket,Socket 长期停在0x05(OPEN)/0x00(TCP_CLOSED),再open_socket必返0x20FAIL - ❌
disconnect → close本身不卡死(实测 5/5 全部 +0 ms 回 CLOSED),只是多余:它制造一个 DISCONNECT 事件,而 bit3=1 下该事件须由我方消费/预清(Trap 14 / 17 / 22) - 关闭后需轮询
ch395f_get_socket_status()的第 1 字节等待sock=0x00再open_socket;第 2 字节(TCP 态)仅在 byte0=OPEN 时有意义,别拿它当"卡住"的判据
SOCK_TIMEOUT 处理
- 长时间无数据时可能触发
SINT_STAT_SOCK_TIMEOUT,不应视为致命错误——记录日志后继续操作即可,不要因此关闭 Socket
UDP 模式区分
- DesIP=0xFFFFFFFF → UDP Server 模式:接受任意来源数据,接收数据前 8 字节为信息头(
reserved[2] src_port[2] src_ip[4]),回发前需设置SET_DES_IP和SET_DES_PORT - DesIP=具体 IP → UDP Client 模式:只接收指定 IP:Port 的数据,接收数据无信息头
UDP 发送缓冲
- 每次
ch395f_write_send_buf()后必须等待SINT_STAT_SENBUF_FREE中断,否则下次写入会被 CH395F 静默丢弃
Trap 09:PHY link up 后立即 open_socket 失败
现象
net_listen() 在 PHY_CHANGE 中断同一时刻调用 ch395f_open_socket() 返回错误码(非 BUSY 超时),导致首次 listen 失败。3 秒后重试成功。
[NET] PHY_CHANGE: 0x08 ← PHY 刚 link up (100M FULL)
[ERR] error listening on socket ← 同一秒 open_socket 失败
[FTP] waiting for connection... ← 3秒后重试成功
根因
CH395F 的 PHY link up 后,内部 TCP/IP 协议栈需要额外时间完成初始化(ARP 缓存、路由表等)。在 PHY_CHANGE 中断触发的同一 net_poll() 迭代内立即调用 OPEN_SOCKET,芯片可能返回 ERR_BUSY 或其他错误码。
解决方案
- 应用层重试兜底:FTP Server (
lftpd_start) 的while(1)循环中,listen 失败后vTaskDelay(3000)重试,3 秒足够 PHY 稳定 - 启动延时消峰:FTP 任务启动时
osDelay(7000),等 netTask 完成net_init() → PHY 协商 → 首轮 net_poll()后再发起 listen,此时 PHY 已稳定 2 秒以上,首次就成功
注意事项
- 此问题在单连接模式 (
FUN_PARA=0x00) 和多连接模式 (0x02) 下均存在,与 FUN_PARA 无关 - 如果多个应用任务同时启动 listen,可能全部首次失败、全部重试成功——建议各任务错峰启动
Trap 10:消息队列值拷贝导致结果无法回传
现象
net_send() / net_recv() / net_listen() / net_accept() / net_close() 等线程安全 API(通过消息队列委托 netTask 执行)永远返回 -1,即使 netTask 内部操作成功。
根因
CMSIS-RTOS v2 osMessageQueue 传递 net_msg_t 结构体是值拷贝。调用方 puts 后 msg.result = -1(栈变量),netTask 侧 osMessageQueueGet 拿到的是队列中的副本,修改副本的 msg.result = 0 后 xTaskNotifyGive 通知调用方。调用方的栈变量 msg.result 仍然是 -1,从未被更新。
// 调用方(ftpTask)
msg.result = -1;
osMessageQueuePut(&msg); // 值拷贝到队列
xTaskNotifyWait(¬ified); // 等待通知
return msg.result; // 永远是 -1!
// 处理方(netTask)
osMessageQueueGet(&msg); // 拿到的是队列副本
msg.result = actual_result; // 修改的是副本
xTaskNotify(...); // 通知回去了,但值丢了
解决方案
不通过消息队列回传结果,改用 FreeRTOS 通知值(notification value)携带返回值:
Producer(netTask):
xTaskNotify(msg.caller, (uint32_t)msg.result, eSetValueWithOverwrite);
Consumer(调用方):
uint32_t notified;
xTaskNotifyWait(0, 0, ¬ified, portMAX_DELAY);
msg.result = (int)notified;
注意事项
- 指针成员(
msg.buf)不受值拷贝影响——netTask 拿到的是同一个指针,可以读写调用方的缓冲区 net_connect()不使用消息队列(在 netTask 内直接调用_locked版本),不受此 bug 影响- 内核级 API(
net_send_sock/net_recv_sock/net_listen_locked)直接操作,同样不受影响
Trap 11:net_accept_locked 拒绝 standalone_accept 的 ESTABLISHED 状态
现象
FTP 客户端 TCP 连接成功(standalone accept),220 欢迎语不发送,lftpd_inet_accept() 死循环。客户端连接成功但永远收不到任何 FTP 响应。
根因
net_accept_locked() 入口检查:
if (p_listen_sock->state != NET_SOCK_STATE_LISTENING) {
return -1; // ← 直接拒绝!
}
standalone accept 后 state 已变为 ESTABLISHED,永远到不了后续的 standalone_accept 检查——死锁。
解决方案
if (p_listen_sock->state != NET_SOCK_STATE_LISTENING &&
!(p_listen_sock->standalone_accept && p_listen_sock->state == NET_SOCK_STATE_ESTABLISHED)) {
return -1;
}
允许 standalone_accept + ESTABLISHED 组合通过。
注意事项
- 不影响多连接模式(多连接监听 Socket 状态始终为 LISTENING,由数据 Socket 承载连接)
- 仅影响 standalone_accept 模式(PASV 数据通道、单连接 FTP)
Trap 12:FatFS FF_USE_LFN=0 + 路径 / 前缀 → FR_INVALID_NAME
现象
FTP STOR 上传文件时 f_open("/upload_test.txt", FA_WRITE | FA_CREATE_ALWAYS) 返回 err=6(FR_INVALID_NAME)。upload_test.txt 文件名 11+3 字符 > 8.3 限制同样触发 err=6。
根因
ffconf.h中FF_USE_LFN = 0(长文件名禁用),最大文件名 8+3 字符- FatFS 不接受
/file格式的路径(需要file或0:file) lftpd_io_canonicalize_path("/", "file")输出必然带/前缀- 根目录路径
"/"剥离后为空字符串,f_stat("")同样失败
解决方案
/* 剥离 "/" 前缀,根目录用 "." 表示 */
static const char *to_fatfs_path(const char *path) {
if (path == NULL || *path == '\0') return ".";
if (*path == '/') {
path++;
if (*path == '\0') return ".";
}
return path;
}
所有 lftpd_io_* 函数调用 FatFS 前统一使用 to_fatfs_path(path)。
注意事项
- 长文件名需设置
FF_USE_LFN = 1并配置FF_LFN_UNICODE,会显著增加 RAM 占用 CWD .和CWD /同样受此问题影响,修复后正常工作
附:FTP 支持改造完整问题清单
| # | 问题 | 类型 | 修复位置 |
|---|---|---|---|
| 1 | 消息队列值拷贝,6 个 API 永远返回 -1 | Trap 10 | net_socket.c |
| 2 | PHY link up 后 open_socket 失败 | Trap 09 | freertos.c + lftpd.c |
| 3 | net_accept_locked 拒绝 ESTABLISHED |
Trap 11 | net_socket.c |
| 4 | CH395F 多连接模式不支持多监听 | 架构 | net_socket.c: fun_para=0x00 |
| 5 | PASV 端口 0(net_bind 无自动分配) | 缺功能 | net_socket.c: s_dynamic_port |
| 6 | FatFS 路径 / 不兼容 + LFN 禁用 |
Trap 12 | lftpd_io.c: to_fatfs_path() |
| 7 | cmd_pasv 连关连开 listen 失败 |
Trap 09 子类 | lftpd.c: 3次重试 |
| 8 | ftpTask 启动时 PHY 未稳定 |
Trap 09 子类 | freertos.c: osDelay(7000) |
| 9 | 会话结束后立即重建监听失败 | Trap 09 子类 | lftpd.c: vTaskDelay(200ms) |
| 10 | s_file_open 单文件限制 |
已知限制 | lftpd_io.c (全局变量) |
| 11 | CH395F DMA buffer 太小,分批读导致 recv_len 归零 | Trap 13 | ch395f.c + lftpd.c |
| 12 | malloc 嵌入式环境失败 |
Trap 13 子类 | lftpd.c: static buffer |
| 13 | net_listen_locked 缓冲区覆盖(s0/s1 争用 block 0-3) |
Trap 02 翻版 | net_socket.c: 单连接用默认 buf |
| 14 | CH395F TIMEOUT 立即触发(数据连接无数据) | Trap 09 子类 | net_socket.c: CLOSED 状态处理 |
| 15 | 发送单次写 >1KB 写穿 1KB 硬件发送 FIFO | Trap 20 | net_socket.c: NET_SEND_CHUNK_MAX |
| 15 | NET_RECV_TIMEOUT_MS=5s 控制通道超时断开 |
配置 | net_config.h: 30000 |
| 16 | receive_file 0 bytes 当作成功 |
逻辑 | lftpd.c: total==0 则失败 |
| 17 | MobaXterm 连数据端口不发数据(客户端特殊行为) | 外部 | Python 抓包确认,超时恢复 |
Trap 13:CH395F recv_len 分批读取时归零
现象
MobaXterm 上传文件时 CH395F 收到数据(s1 int=0x04 RECV_OK 持续触发),但 ch395f_get_recv_len(1) 第一次返回 2920,读 1024 字节后第二次永远返回 0。GINT=0x0020 反复触发但无数据可读——死循环。
根因
ch395f_read_recv_buf 读部分数据后,CH395F 的 recv_len 寄存器被重置为 0(非递减)。缓冲区中剩余的 1896 字节仍然存在(RECV_OK 标志为真),但 ch395f_get_recv_len 报告 0。
⚠ 2026-10-11 复核:该机理未证实,且与手册直接相左——§5.34 明文"读取的数据长度可以小于缓冲区的实际有效数据长度,未读取的数据仍然保留在接收缓冲区中,单片机可以通过此命令继续读取";本仓自己的 TC-NET-201 注释也是相反口径(
test/board/ch395f_test_task.c:419"只查询积压字节数…可反复调用"、:476-477"剩余字节下一轮 get_recv_len 会继续报告,不会丢")。同族结论"部分排空致命"已由 Trap 21 结案撤回,本条当时未同步。现象本身(当年确实观察到recv_len归零)保留为历史记录;定案前须补一条"部分读 → 再查recv_len"的定向 TC。下方解法数字均为历史值:现CH395F_SPI_DMA_BUF_SIZE = 2048+4 = 2052(ch395f.h:148)、lftpd.c:228是static unsigned char buffer[1024],s_recv_buf[4096]已不存在。
同时 CH395F_DMA_BUF_SIZE=1500(header 4 + max 1496 data),无法一次读取完整的 4096 字节 CH395F 接收缓冲区。
解决方案
CH395F_DMA_BUF_SIZE从 1500 增大到 4100(支持一次读 4096 字节 header+data)receive_file缓冲区从 1024 增大到 4096,一次性读空 CH395F 接收缓冲区- 不能使用
malloc(嵌入式堆可能不可用),改用static unsigned char s_recv_buf[4096]
注意事项
- CH395F 接收缓冲区默认 socket 0: 4096B, socket 1: 4096B。读空后客户端继续发数据,recv_len 重新递增——正常
- 清理中断状态后
handle_timeout_event中的ch395f_close_socket必须显式调用,确保下次open_socket成功
Trap 14:close 后 DISCONNECT 滞后锁存,污染下次会话首次中断读取
现象
close_socket → 轮询 GET_SOCKET_STATUS 已确认 CLOSED 后,紧接的下一会话(重新 open + connect)在第一次读 GET_INT_STATUS_SN 时读到上一会话遗留的 DISCONNECT 事件(0ms 即命中),导致负向判定/状态机误判。实测:TC-202 三轮重连后立即跑 TC-203,PC 端握手实际成功(连接被接受),固件却因陈旧 DISCONNECT 抢先判"未连接"。
根因
wait-CLOSED轮询用的GET_SOCKET_STATUS是纯查询,不清中断;- TCP 拆除过程中断(DISCONNECT)可能在与 CLOSED 状态达成几乎同时、甚至之后才锁存;
- 该事件无人消费,跨会话存活到下一次
open_socket之后。
解决
- 预清:新会话 OPEN 前读一次
GET_GLOB_INT_STATUS_ALL+GET_INT_STATUS_SN丢弃陈旧事件; - 反核实:依据中断做结论前,用
GET_SOCKET_STATUS验证实际状态一致(如"负向事件 vs ESTABLISHED"矛盾时以状态为准)。
参考
test/board/ch395f_test_task.c: phase2_tc203()(双保险实现);同类思路见 Phase 1 入口的 PHY_CHANGE 预清。
Trap 15:DHCP 成功后 IP 寄存器提交滞后
现象
GET_DHCP_STATUS 已置 0x00 后立即 GET_IP_INF,回读到的仍是旧静态配置(如 192.168.1.100),而芯片实际已按新租约(192.168.1.77)正常收发——串口打印与 Socket6 广播宣告均带出过期 IP。2026-08-24 上板实测:固件各检查全 PASS(6/6),但 PC 侧 HELLO 发往租约地址才得到应答,三方数据矛盾构成假阳性。
根因
DHCP 状态位仅代表协商流程完成;IP/MASK/GW 寄存器的提交与状态置位是异步的,存在数百毫秒级窗口(实测 ~1s 内完成翻转)。
解决
- 稳定轮询:连续 3 次读
GET_IP_INF(间隔 300ms,上限 8s)结果一致且非零才采信; - 交叉比对:PC 端将收到的宣告 IP 与所分配租约比对,不一致即报错兜底。
参考
test/board/ch395f_test_task.c: phase4_run();docs/测试说明/CH395F_Test_Guide.md 阶段 4 风险表 #4。
Trap 16:NET 层 TCP Client 目的 IP 字节序写反 → CONNECT TIMEOUT
现象
NET 层 TCP Client(阶段 7)net_connect() 返回 0,但 net_poll() 报 sock0 CONNECT TIMEOUT,连接永远建立不起来;而硬件层 TCP Client(阶段 2,同一 net_init/FUN_PARA=0x08,同一目标 192.168.1.2:8081)完全正常。PC 同网段、防火墙关闭、服务端正常监听。
根因
net_connect_locked() 把 sockaddr_in.sin_addr.s_addr(网络字节序,net_inet_addr 返回)按 低字节在前 拆成 remote_ip_arr[4],得到 [2,1,168,192],再交给 ch395f_set_des_ip()。但 SET_IP_ADDR_SN 族按点分顺序([192,168,1,2])写入才生效(阶段 2 用 test_parse_ip("192.168.1.2") 直接得到 [192,168,1,2] 即成功,可证;官方例程 EXAM5_TCP_Server/main.c:39 CH395IPAddr={192,168,1,101} 同向)。⚠ 2026-10-11 复核:手册 §5.8 的总括"所有包含 IP 输入或输出的命令,字节顺序均为 IP 低字节在前"与官方例程自相矛盾,本仓以实测 + 例程为准;端口不在此列——§5.25/§5.26 明确低字节在前,驱动 ch395f_set_des_port/set_sour_port 即如此且工作正常,勿把"大端数组"套到端口上。于是芯片把 SYN 发往 2.1.168.192——一个跨网段、不可达的地址,SYN 被网关丢弃 → 无 SYN-ACK → CONNECT TIMEOUT。
误导性排查:CMD_GET_REMOT_IPP_SN 回读的是小端字节序(ch395f.c 注释 "IP和端口均为低字节在前")。把写反的 [2,1,168,192] 读回再按小端解释,恰好又得到 192.168.1.2,使回读日志看起来"目的地址正确",掩盖了真实写反。
解决
net_connect_locked()提取remote_ip_arr改为 大端:arr[0]=(ip>>24)&0xFF … arr[3]=ip&0xFF(ip 为网络字节序 s_addr)。- 同样修正 UDP 发送路径
net_send_locked()中set_des_ip的目的 IP 数组(同样低字节在前 bug)。 - 诊断回读
GET_REMOT_IPP时按其小端语义还原:dip=(rip[3]<<24)|…|rip[0],再按>>24…打印,方可显示芯片实际目的 IP。
注意事项
net_inet_addr返回网络字节序;sin_addr.s_addr一律按网络字节序理解。- 任何写 CH395F IP 四字节数组(set_des_ip / set_ip_addr / set_gwip_addr / set_mask_addr)都传点分顺序数组,与
test_parse_ip产物一致;16 位端口一律低字节在前(§5.25/§5.26),两者口径不同。 - 回读类命令(GET_REMOT_IPP / GET_IP_INF 等)若文档标注"低字节在前",打印时务必先按小端重组再格式化,否则会误判。
对应文件
Drivers/BSP/NET/net_socket.c(net_connect_locked 提取 remote_ip_arr 大端;net_send_locked UDP 目的 IP 同样修正;[CONN] 回读按小端重组);Drivers/BSP/NET/net_types.h(remote_ip_arr 注释改为大端);test/board/ch395f_test_task.c(test_parse_ip 可作为大端构建范本)。
发现时间
阶段 7 NET 层 TCP Client 上板实测,2026-08-25。
Trap 17:单连接模式 client 重连被上一条连接的残留 DISCONNECT 秒断
现象
阶段 7 TC701(首次 TCP Client 连接 + 回显)正常;TC702(close 后同 Socket 0 重连 ×3,每轮不同本地端口)每次都 [CONN] openOK 成功、但紧接着 sock0 DISCONNECTED, releasing,连接被秒断,PC 端看到"已建立连接但对端立即关闭、未收到数据"。阶段 6 的 auto_relisten/断线处理逻辑同源,但 auto_relisten 标志本身不是元凶(client 该标志为 0)。
根因
单连接模式只有 Socket 0,新旧连接复用它。net_close_locked() 发 close_socket() 后芯片要等对端 FIN/ACK 才把 Socket 置 SOCK_CLOSED 并上报 DISCONNECT 中断(耗时数毫秒)。TC702 几乎在 TC701 关闭同时复用 Socket 0 去 tcp_connect:旧连接的 DISCONNECT 中断在 TC702 已 ESTABLISHED 之后才到达,net_poll() 的 handle_disconnect_event() 把状态置 CLOSED → 新连接被误判为"刚建立就断开"。GET_REMOT_IPP 回读仅确认目的地址正确,掩盖不了此事件串扰。
解决
net_close_locked():close_socket()后轮询ch395f_get_socket_status()直到CH395F_SOCKET_CLOSED,再读ch395f_get_sock_int_status()/ch395f_get_glob_int_status_all()清掉中断状态(上限 500ms,LAN 通常几毫秒),彻底排空上一条连接残留的DISCONNECT。net_connect_locked():open_socket()成功后再次读空本 Socket 中断状态,作为双保险。
注意事项
- 任何"关闭→复用同一 Socket 0 重新建连"的场景都必须先排空旧连接的中断,否则旧
DISCONNECT会串扰新连接。 - 若关闭后阻塞等待影响实时性(多连接模式另有 Socket),应改为非阻塞的"代次/epoch"机制;单连接模式直接排空即可。
net_close_locked内阻塞osDelay仅在 netTask 上下文,单连接模式无其它并发 Socket,安全。
对应文件
Drivers/BSP/NET/net_socket.c(net_close_locked 排空残留 DISCONNECT;net_connect_locked open 后清中断)。
发现时间
阶段 7 NET 层 TCP Client 上板实测,2026-08-25。
Trap 18:多 Socket 模式切换 + 8 个 Socket 缓冲分配
现象
阶段 7 仅测了 Socket 0。要验证 Socket 17 也能作为 client,当时推论必须把 7 完全不可用。FUN_PARA 从"单 Socket 模式"切到"多 Socket 模式",否则 open_socket(1~7) 被芯片拒绝/无响应,1
⚠ 2026-10-11 复核:这句"必须切多 Socket 模式"是当时的前置推论,不是实测记录——本段无串口日志摘录、无 TC 编号、无主机侧独立证据;且当年 Socket 1~7 根本没有显式分配缓冲(net_init 注释"先不配置 buffer"、MSS=1460),按 Trap 01/02 未配缓冲的 Socket 中断不触发,混淆因子未被排除。bit0 语义见本节末「定案更正」。
根因
CMD_SET_FUN_PARA 的 bit0 决定 Socket 数量模式:0=单 Socket 模式(仅 Socket 0,8KB 缓冲全归它);1=多 Socket 模式(Socket 07 全部可用,缓冲需自行分配)。之前 7 不存在。注意 bit1(TCP Server 单/多连接)与 bit0 是两回事:本方案只翻 bit0(多 Socket),bit1 保持 0(TCP Server 单连接),即"8 个 Socket 可用,但 Socket 0 作服务端时只接 1 客户端"。net_init 用 0x08(bit0=0)即单 Socket 模式,故 1
⚠ 此"根因"已于 2026-10-11 证伪(bit0 在所有版本官方手册里都是「保留,必须为0」;官方例程 bit0=0 下 8 个 Socket 照常可用)。当年真正起作用的是下面第 2 条的缓冲显式分配,不是 bit0。完整证据见本节末「定案更正」。
解决
net_init:ch395f_set_fun_para(0x09)(bit0=1, bit1=0, bit3=1)。⚠ 2026-10-11 已改回0x08(bit0 保留必须为 0,见「定案更正」)。ch395f_init()之后,循环 07 用47 块、互不重叠。ch395f_set_send_buf(sock, i*6+4, 2)/ch395f_set_recv_buf(sock, i*6, 4)给每个 Socket 分配独立收发缓冲。按docs/芯片手册/CH395F.md§5.45(48 块 × 512B = 24KB)+ §9.2.8 最优分配:每 Socket 6 块 = 接收 4 块(2KB) + 发送 2 块(1KB),ch395f_set_tcp_mss(1024),恰好用尽 0- MSS 选取依据 §9.2.8:接收缓冲(2KB) ≥ 2×MSS(1024) 满足建议、≥ MSS 满足必须;发送缓冲(1KB) ≤ 8KB 满足必须。MSS 取 1024 为 8-Socket 共享 24KB 时的最大值。
net_init缓冲分配顺序遵循 Trap 01:先配数据 Socket,再配监听 Socket 0(本 Net 层 Socket 按需 open,故统一在 init 一次性配好)。UDP 路径net_bind()(net_socket.c:1000-1014)中对 Socket 4~7 的兜底重设也同步为同一布局。- PC 端
tcp_server改为每连接一线程的并发回显(--max-conn为并发计数),否则 Phase 7 全量连接会触发累计计数上限而拒掉 4~7。
注意事项
- 4~7 的缓冲必须显式分配,否则其 CONNECT/RECV 中断不触发(Trap 01/02)。
- 上述布局以
docs/芯片手册/CH395F.md§5.45 的 48×512B 缓冲几何为准,并已由 Phase 7 TC704(40/40 PASS)实测确认块 047 全部有效(此前文档中"32×1KB"记录有误)。Socket 47 分配与 8 路并发回显均验证通过。 - 多 Socket 模式下 TCP Server 单连接语义不变:Socket 0 监听且连接停留在本 Socket,Sockets 1~7 由应用当作独立 client 使用。
net_poll用GET_GLOB_INT_STATUS_ALL(2 字节,支持 07)并遍历 07,Socket n 中断位即bit(n+4)(Socket 03→bit47,Socket 47→bit811),原代码正确无需改动。
对应文件
Drivers/BSP/NET/net_socket.c(net_init FUN_PARA + 缓冲分配 + MSS);test/board/net_test_task.c(新增 TC704 八 Socket 并发回显);test/host/ch395f_socket_test.py(tcp_server 并发 + --max-conn)。
定案更正(2026-10-11):FUN_PARA 取 0x08,bit0 与 Socket 个数无关
结论:net_init 用 ch395f_set_fun_para(0x08)(bit3 SOCKET_CLOSE=1、bit0=0)。本节"bit0=单/多 Socket 模式"的说法无任何出处,已撤回;当年让 Socket 1~7 真正可用的是上面第 2 条的缓冲显式分配,不是 bit0。
证据(一手,按强度排序):
- 官方手册位表:本机
D:\Code\DTU\使用的芯片手册\以太网控制器_CH395F(1).pdf(WCH《CH395 手册 1》V2.2,页眉https://wch.cn)§5.52 表跨 PDF 第 21→22 页,bit0 行原文0 - - 保留,必须为0;bit2、[5:6]、[31:8]同为保留;注释①~⑥ 无一处提 Socket 数量。官方现行版 V2.3 逐位一致。旧版(V1C、英文 V1E、2020 版 CH395L/Q)bit0 同样是保留(旧版多出的位是 bit2=LOW_PWR,V2.2 起收回为保留)→ 任何版本都不存在"bit0=多 Socket"这一行。 - 官方 SDK 位宏:
CH395EVT\EVT\EXAM\PUB\CH395INC.H:349-353只定义 bit1/3/4/7/8,没有 bit0 宏。 - 官方例程直接反例:
EXAM5_TCP_Server\main.c:296传0x02(bit0=0),同文件:337-359给 Socket 0~7 全部配缓冲 → bit0=0 下 8 个 Socket 照常可用;EXAM11_FTPClient\FTP\CH395.C:319官方直接传0x08, 0x00, 0x00, 0x00(与本仓取值逐字节相同);全 EVT 包 grepSetFunPapr仅 3 处(0x02 / 0x02 / 0x08),无一处 0x09。 - 8 Socket 是芯片原生能力:
docs/芯片手册/CH395F.md命令章前言「CH395 内部可以同时提供 8 个 Socket,其索引值分别为 0、1、2、3、4、5、6、7」;Socket 类命令一律_SN(Socket 索引)寻址。 - 真正的门槛是缓冲:
docs/芯片手册/CH395F.md§5.45「如果单片机需要用到的 Socket 个数大于 4,需要重新对缓冲区进行分配」——正是本节第 2 条措施。 - 概念讹传源:手册里唯一的"多"字样是 bit1
FUN_PARA_FLAG_TCP_SERVER(TCP 服务器多连接,全局配置),docs/芯片手册/CH395F.md§9.2.6「默认的设置下,服务器多连接功能被关闭……每个 Socket 只可以建立一个 TCP 连接」——单/多之分指一个服务器能接几个客户端,不是"能用几个 Socket"。 - 互联网侧:多轮检索无任何来源称 bit0 为单/多 Socket 模式,无公开代码传 0x09;CH390/CH392/CH397 同族也无此位可串源。
为什么 0x09 当年"看起来有效":芯片忽略了保留位(Trap 25 旁证:bit0=1 下 sock1/2/3 并行正常)。TC704 40/40 PASS 只证明"0x09 + 缓冲布局"通过,不构成"0x08 下不通过"的对照(与 bit0 结论出自同一提交,属自证)。留着 bit0=1 的代价是违反保留位规定——后续固件版本一旦启用该位,现网设备静默失效。
实测闭环(2026-10-11 20:18,N-31 已销项):FUN_PARA=0x08 下跑 TC-NET-704(PHASE7_MULTI_COUNT 降到 5:E-23 未修,NET_MAX_SOCKETS=8 而 FTP/对时/远显三个监听常驻占 3 个 fd)→ Phase 7 34/34 全通过(failed=0):5 路并发全部 ESTABLISHED、每路 64 B 回显字节级比对 OK;芯片 Socket 实测落在 sock2 / sock4 / sock5 / sock6 / sock7(0/1/3 被三个监听占用)→ Socket 4~7 在 bit0=0 下完全可用,与手册口径一致,定案成立。烧录后回读 No difference found。
⚠ 台架前置(本轮踩到,务必先排):① PC 的以太网适配器落在 Public 防火墙配置文件 → 入站 8081 被静默丢弃,症状是 TC701/702/704 全部 CONNECT TIMEOUT(而 PC→设备 21 端口正常、ping 通、ARP 正常),极易误判成"0x08 毁了 client";② 上一轮遗留的 tcp_server 进程仍占着 8081,新起的服务器绑不上端口却不报错。两条都排除(临时关 Public 配置文件 + 清残留 python 进程)后才拿到上面的结果——判"芯片行为"前先确认对端真的收到了 SYN(Trap 25 总纲铁律的又一次命中)。
发现时间
阶段 7 扩展 8 Socket 测试,2026-08-25。
Trap 20:发送缓冲区单次写入超过硬件 FIFO 上限(写穿)
现象
大文件/批量发送(如阶段 8 的 100KB,或 FTP 上传)时,数据在中间某处错乱、连接被对端 RST、或在 WRITE_SEND_BUF 后芯片不再产生 SEND_OK,表现为发送卡死或后续字节丢失。该问题只在单次发送长度 > 1KB 时稳定复现。
根因
net_send_locked(TCP)与 net_sendto(UDP)在分块时以 NET_DMA_MAX_PAYLOAD(2KB) / NET_SEND_BUF_SIZE(4KB) 作为单次写长度上限,但 CH395F 每个 Socket 的硬件发送 FIFO 只有 CH395F_SEND_BLOCKS × CH395F_RAM_BLOCK_SIZE = 2×512 = 1KB(2026-09-22 起为固定布局 2×512=1KB,与 MSS 解耦,见 Trap 21;ch395f.h)。当某次调用 net_send/sendto 传入 >1KB 数据,驱动会在 send_ready 置位后一次性 WRITE_SEND_BUF(2KB/4KB),直接写穿 1KB 发送 FIFO(超出部分覆盖/丢失或破坏 FIFO 指针),导致后续数据错位。原代码误把"SPI DMA 缓冲上限(2KB)"或"软件 API 缓冲(4KB)"当成硬件发送缓冲上限。
解决
ch395f.h新增硬件缓冲字节宏:CH395F_SEND_BUF_SIZE_BYTES = CH395F_SEND_BLOCKS × CH395F_RAM_BLOCK_SIZE(=1KB),并补充CH395F_RECV_BUF_SIZE_BYTES。net_socket.c新增NET_SEND_CHUNK_MAX = min(NET_DMA_MAX_PAYLOAD, CH395F_SEND_BUF_SIZE_BYTES),即以硬件发送 FIFO 为硬上限(当前 1KB < 2KB DMA 载荷,故生效 1KB)。net_send_locked与net_sendto的单次写长度上限均由原来的NET_DMA_MAX_PAYLOAD/NET_SEND_BUF_SIZE改为NET_SEND_CHUNK_MAX。send_ready门控保证只有 FIFO 空闲时才写入,故写入 ≤1KB 安全(写满后等SEND_OK再写下一块)。- 接收方向不受影响:
net_recv_locked只读ch395f_get_recv_len(≤ 接收 FIFO 2KB)并受调用方len截断,不会写穿。
注意事项
- 该上限随
CH395F_SEND_BLOCKS(2026-09-22 起固定 2 块=1KB,与 MSS 解耦,见 Trap 21);若将来发送缓冲改变,NET_SEND_CHUNK_MAX自动跟随(仍取硬件缓冲与 DMA 载荷的较小者),无需改此处逻辑。 - 调用方(阶段 8、FTP
lftpd)仍可按任意长度调用net_send,实际分块由驱动保证 ≤ 硬件 FIFO。
对应文件
Drivers/BSP/CH395F/ch395f.h(CH395F_SEND_BUF_SIZE_BYTES 等);Drivers/BSP/NET/net_socket.c(NET_SEND_CHUNK_MAX + net_send_locked/net_sendto 分块上限)。
发现时间
阶段 8 大文件传输实现,2026-08-26。
Trap 21:接收缓冲"恰好填满 + 排空"窗口冻结(TC-NET-604)—— 结论:非缺陷(2026-09-23 结案)
结案结论
原报告(阶段 8 大文件传输期间:同连接前序突发后 4096B 突发"前 2KB 正常回显、其余静默无回显")在当前固件上无法复现,判定为非缺陷。可信判据(产品固件 + 真实 lftpd FTP 上传,成功 = FTP 协议结果 + 上传后 RETR 回读比对 SHA-256)实测:227 轮全过(8KB/64KB/256KB/1MB;含零间隔快速重连与 100 轮浸泡),并发生产 UI 流(:7003)600 帧 CRC 全过,设备侧日志无 FAIL/ERROR/WARN。
为什么原台架会"复现"——教训,务必先读
2026-09-22"精确定位轮"给出的全部"冻结/停摆/WEDGE"结论均为台架假象,已撤回:
- 测试程序自吞数据:台架配方里的固件内读者(Phase 9/11 的 64B 读、固件探针读)把对端数据读走后不回显——PC 端自然少收,被误当成"设备停摆"。实测短收量恰等于被吞字节数(例:
4736−4608 = 128 = 2×64探针读);所谓"V1 配方必冻结"就是 Phase 11 吞掉整包(设备日志avail=64 len=64,PC 侧下一轮rx=0/64)。对照组(仅 Phase 6、无额外读者)40/40 全过。 - 判读工具误判:
probe_classify.pyA/C 模式把通过的运行报成STALL(C0 的脚本层实测为1460/1460+ 尾段2100/2100,且内容逐字节匹配)。 - 由此派生的"部分排空致命 / 再武装区间 ≥160ms / 相位刀口 / C 配方复现生产停摆"等结论全部作废;MSS=512 实验的"停摆"亦未经可信判据复核。
现在怎么做(台架纪律)
- 判读一律用
MDK-ARM/bench_verdict.py:以脚本层 STEP 行为真值,并扣除固件读者吞噬的字节; - 冻结/卡死类问题走真实生产路径 + 端到端内容校验:
MDK-ARM/ftp_freeze_probe.py(FTP 上传 + RETR 回读 SHA-256);卡住时用控制通道 NOOP 区分"数据面卡死"与"整层卡死"; - 禁止把"固件内非回显读者"与固定期望值混用在同一配方里——这是本轮最大的教训。
保留的代码变更(与被撤回的机理无关)
缓冲块布局与 MSS 解耦(ch395f.h)、超限读 OVERSIZE 金丝雀(ch395f.c)保留:属防御性/可观测性改进,不回退。
相关
- 真实缺陷见 Trap 23(上传中止后首笔复连上传被静默截断,已可信复现并修复);
- 发现:2026-08-26(阶段 8);结案:2026-09-23。
Trap 22:快速复连丢 CONNECT/DISCONNECT 中断 → 监听静默死亡(2026-09-23 复现并修复)
现象(生产固件实测)
单连接监听 socket 在快速复连后永久失效(后续 SYN 被拒或超时,10s 内不自恢复),而芯片/网栈其余部分完全正常(其他监听端口照常工作)。两种表现:
- 拒连(ConnectionRefused):芯片完成握手但 CONNECT 中断未上报 → 控制块滞留 LISTENING;或本层因 DISCONNECT 中断丢失仍以为有客户端 → 两种情况都会让芯片侧那条连接一直占住单连接监听,后续 SYN 全被拒;且丢事件后本层收不到任何中断,无法靠事件自愈。
- 应用侧连锁:
net_send被拒(send -1);或应用读不到 EOF(其g_client永不释放,因而永不调用 accept)。
复现(可信用例,纯客户端判据;工具 MDK-ARM/tcp_reconnect_probe.py)
- 确定性复现:产品固件 + 待机 60s 后,对 UI 监听
:7003以 300ms 间隔快速复连 → 修复前首 2 次成功、之后永久拒连(10 轮 2/10,10s 恢复窗口内无自恢复); - 概率复现:FTP 控制
:21以 100ms 间隔连 50 次 → 修复前第 29 轮起永久拒连; - 对照:FTP
:21在 0ms / 600ms 间隔下通过;:8888(对时)同期亦正常 → 触发与"复连间隔 <~1s + 轻负载(待机)"相关,与 2026-09-22 记录的边界一致。
机制(设备侧日志实证)
- 芯片丢 CONNECT 中断 → 无
standalone accept,控制块滞留 LISTENING;或丢 DISCONNECT 中断 → 本层仍以为 ESTABLISHED; - 此后本层收不到任何中断(芯片静默持有/丢弃该连接),纯事件驱动无路可走——实测把
handle_disconnect_event的 LISTENING 分支从"仅告警"改为"重建监听"后仍然无效(因为连 DISCONNECT 都没来); - 故必须周期回读芯片状态对账才能发现失同步。
修复(已实施,Drivers/BSP/NET/net_socket.c)
- 周期对账
net_reconcile_tick()(net_process_messages每轮调用;周期 200ms):对在用 socket 回读芯片状态,修正三类失同步—— ① 本层 ESTABLISHED 而芯片已无连接 → 按断开处理(应用读到 EOF 后自动重监听); ② 本层 LISTENING 而芯片已完成握手 → 补做 standalone accept(复用handle_connect_event); ③ 本层 LISTENING 而芯片监听已消失 → 重建监听(net_relisten_try)。 去抖 + 冷却:同一失配需连续 2 次(≈400ms)才动手;同一 socket 动手后冷却 1s。 handle_disconnect_event的 LISTENING 分支:由"仅告警"改为"回读芯片状态——已不可监听即重建"(仍在监听则判陈旧事件丢弃)。
踩坑记录(务必先读)
最初的修法放在 accept 路径(每次 accept 时回读状态并自愈):单次状态读会把"刚 open/listen 尚未生效"的瞬态误判为失配 → 监听被反复重建直至 relisten give up → 三个监听口全部死亡(风暴)。改为"周期对账 + 去抖 + 冷却"后消除。
结论:凡基于单次状态读的重建动作,必须去抖。
验证(修复前 → 修复后)
| 用例 | 修复前 | 修复后 |
|---|---|---|
待机后 UI :7003 快速复连 ×10 |
2/10(之后永久拒连) | 10/10 |
FTP :21 间隔 100ms ×50 |
28/50(第 29 轮起死亡) | 50/50 |
FTP :21 间隔 0ms / 600ms ×50 |
通过 | 50/50 / 50/50 |
UI :7003 间隔 200ms ×20 |
— | 20/20 |
| FTP 正常上传 10×64KB(SHA-256 往返) | 通过 | 10/10 |
| 上传中止 RST / FIN 回归(Trap 23) | 通过 | 5/5 + 5/5 |
设备侧 relisten give up / 重建风暴 |
— | 0 |
自愈实测计数(整轮会话):chip closed -> disconnect 12 次、connection completed w/o CONNECT irq -> accept 1 次、listen lost -> relisten 1 次——三类失同步均真实出现并被自愈;relisten give up 0 次。
对应文件
Drivers/BSP/NET/net_socket.c(net_reconcile_tick / handle_disconnect_event LISTENING 分支);MDK-ARM/tcp_reconnect_probe.py(可信用例);docs/任务规划/README.md N-6(✅ 已销项,2026-09-26 清理)。
发现时间
2026-09-22(台架偶发 1/1)→ 2026-09-23(生产固件确定性复现 + 当日修复闭环)。
Trap 23:上传中止后首笔复连上传被静默截断(≤2048B)并误报成功
现象
客户端上传(STOR)中途断开(用户取消 / 网络断;RST 或优雅 FIN 均可),紧随其后的第一笔复连上传被截断到 ≤2048 字节(= 芯片单 socket RX 缓冲大小),且服务端回报 226 上传成功 → 客户端(ftplib 等)据此认为上传成功 = 静默数据损坏;第二笔及以后恢复正常。
证据(可信复现:生产固件、零固件探针;工具 MDK-ARM/ftp_abort_probe.py)
- 判据 = FTP 协议结果 + 紧接一笔正常上传的 SHA-256 往返校验(STOR→RETR 比对) + 控制通道 NOOP 存活探测;
- RST 中止:10/10 复现首笔复连上传截断(请求 4096B → 设备日志
UPLOAD '/ab_ok.bin' 2048 bytes OK); - 优雅 FIN 中止:5/5 同样复现 → 与 RST/FIN 无关,凡"数据未发完即断开"均触发;
- 中止那一笔自身也被记为
UPLOAD ... 2048 bytes OK+ 226(FTP 无长度声明,语义边界;但客户端会误判成功); - 对照(无中止):177 轮(8KB/64KB/256KB/1MB,含 100 轮零间隔浸泡)全部 SHA-256 匹配(
MDK-ARM/ftp_freeze_probe.py),并发 UI 流 600 帧 CRC 全过 → 中止路径是唯一触发条件。
机制(设备侧日志实证)
- 中止时数据 socket 被释放(RST 时芯片上报
sock4 TIMEOUT),net 层随即auto-relisten ok+relisten retry ok(监听端口复用); - 下一笔连接:
sock4 standalone accept→CMD STOR→ 紧接着一条陈旧sock4 DISCONNECTED, releasing (chip tcp=0x00)(旧连接收尾状态落在新连接上); - lftpd 数据读循环据此读到 EOF → 把已到的 2048B 当作整个文件 →
UPLOAD ... 2048 bytes OK+ 226; - 再下一笔连接时芯片状态已干净 → 正常(故为"一次性"而非持久故障)。
影响
- 静默数据损坏:上传被中断后"重试的第一笔"会生成截断文件且客户端收到成功回报(FTP 客户端通常不做长度校验)→ 现场可能表现为"上传的文件损坏/不完整";
- 主风险面 = 生产路径 STOR(上传);RETR(下载)不涉及;
- 与 TC-NET-604 的关系:本条(Trap 23)是独立、已可信复现并已修复的真实缺陷;TC-NET-604 已结案为非缺陷(见 Trap 21)。现场"上传失败/文件不完整/像卡死"类报告,应优先用本条的判据复核:FTP 协议结果 + 上传后 RETR 回读 SHA-256(+ 卡住时控制通道 NOOP 探活)。
复现
& '.venv\Scripts\python.exe' MDK-ARM\ftp_abort_probe.py --rounds 10 --send 8192 --gap 1
& '.venv\Scripts\python.exe' MDK-ARM\ftp_abort_probe.py --rounds 5 --send 8192 --gap 1 --graceful
修复方向(待办 N-8,全部闭环)
- ✅(2026-09-23 一阶段)relisten 前让芯片 socket 真正沉降——隔离期:持续排空 RX + 主动 close + 等
CLOSED/TCP_CLOSED,隔离满 400ms 才open+listen; - ✅(2026-09-23/24)新连接的陈旧中断防御——陈旧 DISCONNECT 以芯片状态回读甄别(D20:芯片 TCP 仍 ESTABLISHED 判陈旧丢弃)+ 释放残留经两级沉降/隔离(100ms/400ms)后不再投递给下一条连接;
- ✅(2026-09-25)应用侧兜底——异常释放(TIMEOUT/RST)的挂起 recv 以
NET_ERR_CONNRESET了结(正常 FIN 仍回 0——协议边界)→ STOR 异常结束回 450 并删除截断文件; - ✅ 回归判据——
ftp_abort_probe.pyfollowup:RST 10/10 + FIN 5/5(一阶段)、8/8 + 5/5 + 5/5(二阶段);--check-abort断言中止 STOR 应答码(RST→450 / FIN→226 边界);177 轮对照当时全匹配,其后各轮正常路径回归全过。
发现时间
2026-09-23(可信复现首轮:真实生产路径 FTP 中止/复连场景,由 MDK-ARM/ftp_abort_probe.py 自动判定)
修复实施与验证(2026-09-23 当日闭环)
真因(设备侧日志实证,含正常对照):
- 客户端中止上传时对端仍有在途数据;芯片给出
sock4 TIMEOUT(此刻 MCU 侧recv=0),net 层随即释放该 socket 并复监听; - 那些在途段随后才落进芯片 RX 缓冲(约一个缓冲 = 2048B 残渣),而正常传输时 DISCONNECT 到达刻芯片里也仍有数据(对照实测
recv=64512/65536,由应用读空 → 完整); - 中止后立即
open+listen,残渣被下一条连接当作自己的数据读走:新上传只“收到”2048B 就被判定结束并回报 226(首笔复连必中招,第二笔起芯片已干净 → 恢复正常)。
修复(Drivers/BSP/NET/net_socket.c,仅异常释放路径,正常 FIN 路径零改动):
handle_timeout_event(ESTABLISHED 超时释放)置 per-fd 隔离标记s_relisten_quarantine[];net_relisten_locked对带隔离标记的 socket:隔离期内持续排空芯片 RX(新增net_drain_chip_rx(),分块读空、零新增静态缓冲)+ 主动ch395f_close_socket并排空该 socket 中断 + 等芯片回到CLOSED/TCP_CLOSED,隔离满NET_RELISTEN_QUARANTINE_MS(400ms) 才open+listen;- 上限
NET_RELISTEN_SETTLE_MS(800ms) 强制放行 —— 必须小于NET_RELISTEN_DEADLINE_MS(2000ms):实测取 2000ms 会把重试预算耗尽 → 重试轮询"give up" → 监听器死亡(:21 拒连),已修正并在注释中留痕; - 隔离状态在
net_relisten_clear内一并清除(关闭/新建 fd 时不残留)。
验证(可信判据:FTP 协议结果 + 紧接一笔上传的 SHA-256 往返 + 控制通道存活):
- 修复前:RST 中止 0/10、优雅 FIN 中止 0/5(首笔复连上传全部被截断且回报成功);
- 修复后:RST 中止 10/10、优雅 FIN 中止 5/5 全部通过;
- 正常路径回归:30×64KB 零间隔 30/30、20×64KB 20/20,SHA-256 全匹配;
- 启动门禁:
App init OK/net init: OK、FORBID 0 行。
第二阶段:正常释放(FIN)路径——边界甄别与 100ms 沉降(2026-09-24)
边界再甄别(现场"1~2s"时间假设被推翻):中止后 gap 500/2000/5000ms 台架复测 → 设备实存尺寸全部仍 2048(各 4/4,合计 12/12)→ 与时间无关;监听重开本征即时(t_accept 30~47ms)。真实边界:
| 维度 | 结论 | 证据 |
|---|---|---|
| 触发 | 设备侧收过数据的数据连接被对端关闭(纯 FIN 即触发,不要求 RST) | 纯 close()(FIN)4/4 复现;RST 变体 5/5 |
| 受影响路径 | 仅正常 DISCONNECT 释放路径缺芯片状态沉降 | RST(TIMEOUT)路径已有一阶段 400ms 隔离保护 |
| 投递形式 | 芯片上一条连接的收尾残留,投递给复用同一 fd 的下一条连接(accept 后约 10~20ms 一条伪 DISCONNECT) | 设备日志:standalone accept → DISCONNECTED, releasing (chip tcp=0x00) |
| 影响面 | 一次性(首条新连接中招,第二条起干净);同 fd 非 FTP 服务(:7003 UI sink)不受影响(其 socket 不按传输复用) |
一次性 3/3 全对;:7003 复用 8/8 全过 |
| 时间 | 无关(gap 5s 仍失败)→ 现场"1~2s"只是人工重触的常见窗口,不是芯片机制 | gap 500/2000/5000ms 共 12/12 全部失败 |
修复(第二阶段,Drivers/BSP/NET/net_socket.c +36/−7,netTask 串行上下文,零新增缓冲):
- relisten 前沉降分两级:异常释放(芯片 TIMEOUT)=清理 + 最短等 400ms(
NET_RELISTEN_QUARANTINE_MS,一阶段行为不变);正常释放(芯片 DISCONNECT)=清理 + 最短等 100ms(NET_RELISTEN_GRACEFUL_MS,新增); - 正常释放沉降仅在本连接收过数据时登记(
recv_bytes != 0):纯连接即关(Trap 22 的 0ms 复连用例)与只发不收的下载连接无残留 → 免沉降保住快速复连时序; - 日志区分
relisten settle(正常)/relisten quarantine(异常),便于台架分辨触发路径。
沉降值实测悬崖(只清理不等待 = 0ms):0ms 无效(4/4 仍截断)→ 30ms 仍失败(4/4)→ 100ms 通过(多轮稳定)→ 取 100ms(对 30ms 重试节拍 NET_RELISTEN_RETRY_MS 留 3× 余量)。
验证(第二阶段后;设备日志 UPLOAD '<path>' N bytes OK 为权威真值):
| 用例 | 修复前 | 修复后 |
|---|---|---|
| FIN 关闭后首笔上传 ×8 | 0/8(实存 2048 + 回报 226) | 8/8(实存 4096,sha OK) |
纯 close() 变体 ×4 |
中招 | 4/4 |
ftp_abort_probe --graceful ×5 |
0/5 | 5/5 |
ftp_abort_probe(RST)×5 |
5/5(一阶段) | 5/5 |
| 无中止对照 ×4 / 一次性连发 3 笔 ×1 | 4/4 / 第 1 笔坏 | 4/4 / 3/3 |
| 正常往返 freeze 4096×4 / 64KB×2 | 通过 | 4/4 / 2/2(t_stor 0.68→0.18s,t_retr 1.13→0.19s) |
非 FTP :7003 sink 复用 ×8(RST+FIN) |
8/8 | 8/8 |
Trap 22::21 0ms×50 / 100ms×50,:7003 300ms×10,:8888 0ms×20 |
50/50 / 50/50 / 10/10 / 20/20 | 50/50 / 50/50 / 10/10 / 20/20 |
| 启动门禁 / 控制台 / 堆 | — | App init OK / net init: OK,FORBID 0,CONSOLE dropped 0,heap free=8776 min_ever=4816 |
结论:缺陷位于 fd 复用生命周期(芯片上一条连接的残留状态投递给下一条连接),不在 FTP 命令逻辑——lftpd 只是把它放大成误报 226。RST/TIMEOUT 半边由一阶段隔离处理,DISCONNECT 半边由本阶段沉降处理,合计覆盖全部"收过数据后关闭"的释放路径。已知残余边界:中止那一笔 STOR 自身仍回报 226(STOR 无长度声明、在途 FIN 与正常完成不可区分)——后续硬化见待办 #3。
对应文件:Drivers/BSP/NET/net_socket.c(handle_disconnect_event 沉降登记 + net_relisten_locked 两级沉降);MDK-ARM/ftp_abort_probe.py / MDK-ARM/bench_diag/bench_diag_abort_gap.py(边界甄别与回归判据)。
后续硬化(2026-09-25):异常结束回 5xx(待办 #3 闭环)
目标:中止那一笔 STOR 自身不再误报 226——异常结束(连接重置/超时)时回 450 并删除截断文件,把"静默损坏"变成"显式失败"(纵深防御;客户端按协议重试)。
实现(3 个工程文件 + 1 个台架脚本):
net_types.h:net_sock_t增加释放来源标记released_abnormal(RAM +8B:1B×8 socket,BSS,运行堆零影响);net_socket.c:handle_timeout_event(异常释放/TIMEOUT)置 1;handle_disconnect_event(正常释放/DISCONNECT)置 0;net_socket()(新 fd)与net_relisten_locked成功尾(新监听)清零——新连接不得继承上一条连接的释放原因;net_socket.cnet_pending_tick断连了结(③):排空缓冲后,标记为 1 以NET_ERR_CONNRESET了结(对端未发正常 FIN,不是正常 EOF),标记为 0 仍 EOF(0);lftpd.creceive_file:err < 0时unlink截断文件(原先残留于盘)并返回错误码 → STOR 处理器回 450;MDK-ARM/ftp_abort_probe.py增加--check-abort:RST 中止要求中止 STOR 控制应答 4xx/5xx,优雅 FIN 要求 2xx(协议边界,见下)。
协议边界(不可判定,留痕):FTP STOR 无长度声明,"在途 FIN 关闭"与"正常完成"逐字节不可区分(芯片均报 DISCONNECT、缓冲均排空)→ 优雅中止仍回 226(中止方客户端通常也不等应答);仅 RST/掉链(芯片 TIMEOUT)可识别 → 450。FIN 边界由客户端长度校验(SIZE/RETR 往返)兜底。
各消费端负值处理复核(均按"负值=结束"与 0 同路,行为不变):
- lftpd 控制通道
read_line:负值 →line_len != 0→ 会话清理,与 0 同路(控制通道不 close,auto_relisten 恢复); - time_sync(
:8888):n<0 → break 弃连接,与 0 路径一致;8B 时间包已收齐时仍写 RTC; - UI sink(
:7003):DONTWAIT 非阻塞,不走 L3 pending,零影响; - 测试套件:产品构建不启用;测试对端干净关闭,无影响。
验证(硬化后,2026-09-25 台架):
| 检查项 | 结果 |
|---|---|
ftp_abort_probe --check-abort --rounds 5(RST) |
5/5——中止 STOR → 450(原 226),followup 5/5,控制存活 5/5 |
| 设备侧证据 | sock4 TIMEOUT → [ERR][FTP] UPLOAD '/ab0001.bin' FAILED → 450;followup UPLOAD '/ab_ok.bin' 4096 bytes OK |
--check-abort --graceful --rounds 5(FIN) |
5/5——中止 STOR → 226(边界不变),followup 5/5 |
| 正常往返 freeze 4096×4 / 64KB×2 | 4/4 / 2/2(SHA-256 全 MATCH) |
Trap 22::21 0ms×50 / 100ms×50,:7003 300ms×10,:8888 0ms×20 |
50/50 / 50/50 / 10/10 / 20/20 |
非 FTP :7003 sink 复用(RST×4 / FIN×4,400 帧在途) |
4/4 / 4/4(ack 2000/2000,LCD 3209/3209,CRC 全过) |
时间同步 :8888 真实收包 |
wall clock updated (epoch=…) 写 RTC 成功,监听恢复 |
| 构建 / 回读 / 启动门禁 / 控制台 / 堆 | 0 错 0 警、No difference found、FORBID 0、CONSOLE dropped 0、heap free=8776 min_ever=4816(不变) |
对应文件:Drivers/BSP/NET/net_types.h(released_abnormal)、Drivers/BSP/NET/net_socket.c(三处置位/清除 + net_pending_tick 了结分支)、Drivers/BSP/NET/lftpd/lftpd.c(receive_file 错误路径 unlink)、MDK-ARM/ftp_abort_probe.py(--check-abort)。
Trap 24:MAC 写入顺序违反docs/芯片手册/CH395F.md §9.2.1(SET_MAC 必须在 INIT_CH395 之前)
现象(静态审查发现;手册确证)
net_init() 原顺序为 ch395f_init()(CMD_INIT_CH395)→ ch395f_set_mac_addr(),与docs/芯片手册/CH395F.md §9.2.1 规定的初始化顺序相反。后果:INIT_CH395 已按 EEPROM 旧值加载 MAC,其后写入 EEPROM 的新 MAC 本次上电不生效(回落芯片内置/上次值,实际"滞后一次上电")→ UI 配置的 MAC 与设备实际在线 MAC 不一致,设备在线身份错误、网口排查困难。本轮未做"UI 改 MAC + 重启"台架复现,结论由手册条文确证。
根因
docs/芯片手册/CH395F.md §9.2.1「初始化 CH395,必要操作」顺序为:
① SET_MAC_ADDR → ② SET_IP_ADDR → ③ SET_GWIP_ADDR → ④ SET_MASK_ADDR → ⑤ INIT_CH395。
- §5.7:
SET_MAC_ADDR把 MAC 写入芯片内部 EEPROM(约 30us); - §5.13:
INIT_CH395初始化 CH395 的 MAC、PHY 以及 TCP/IP 协议栈(约 5ms)。
即 INIT_CH395 会从 EEPROM 加载 MAC,故 SET_MAC 必须先于 INIT_CH395。原码把 MAC 放在 INIT 之后。
加剧误排的注释:ch395f.c 原把 set_mac_addr/set_ip_addr/set_gwip_addr/set_mask_addr 的限定条件标为"芯片已初始化"——与手册矛盾(这四个 SET 实际都在 INIT 之前下发),易被误读为"须在 init 之后调用"。
解决
Drivers/BSP/NET/net_socket.cnet_init():把ch395f_set_mac_addr()从ch395f_init()之后移到之前(置于 SET_IP/GWIP/MASK 之前,对齐手册步骤①);ch395f.c:四个 SET 函数的限定条件注释统一改为"必须在ch395f_init()之前调用";net_socket.h:net_init注释同步(MAC 于协议栈初始化之前下发)。
影响面
- 仅
net_init()一处调用(产品路径);改动为纯顺序调整,无新增/删除指令、无 RAM 变化; net_stack_bring_up()在net_init后回读芯片 MAC 写 UI 影子(net_cfg_set_active_mac)——修复后回读值 = 实际在线 MAC,UI 显示与线上身份一致。
对应文件
Drivers/BSP/NET/net_socket.c(net_init SET_MAC 顺序)、Drivers/BSP/NET/net_socket.h(net_init 注释)、Drivers/BSP/CH395F/ch395f.c(四个 SET 的限定条件注释);docs/网络/BSD_Socket_API_函数调用关系.md(§4.1 / §7 顺序)。
发现时间
2026-10-04(静态审查项 P1-7;依据docs/芯片手册/CH395F.md §9.2.1 / §5.7 / §5.13 定案)。
Trap 25:FTP 数据连接三连故障——丢 CONNECT 中断 / accept 竞态自毁缓冲 / 背靠背小段毒化 TX(N-30,2026-10-11 定案并修复,回归 4 轮 11/11)
现象(生产 FTP 回归,多轮复现)
同一 FTP 会话内第 2 次及以后的数据连接(LIST/STOR/RETR 各一次)出现两类失败,两种面孔同一根因:
- 吞数据(SWALLOW):新数据连接建立成功(芯片 0x05/0x04 ESTABLISHED)、服务端
net_send返回成功,客户端 20 s 收不到任何字节(无 RST、无 DISCONNECT,直到客户端放弃); - 425:新数据连接被接受后 ~10-20 ms 内出现 DISCONNECT(
chip tcp=0x00),服务端 accept 失败 →425 Can't open data connection。(⚠ 本条当时的解读"芯片自杀连接"有误,见文末"三次定案"——实为客户端发完数据后的正常 FIN + 我方 accept 竞态自毁缓冲。)
失败概率随上一条数据连接的负载大小与间隔变化(NAND 目录文件越多 LIST 越大越易触发),与提交历史无关——在 D20 基线 e5733b34 上以快速连发同样复现(D20 时代"11/11 通过"是慢速台架 + 干净目录的运气)。
定案实验(裸机芯片台架,剥离 OS/net/FTP)
实验分支 n30-chip-bench(worktree),test/board/ch395f_reuse_bench.c 在内核启动前直接调 ch395f_* 做受控矩阵:{socket, close 后复开延迟, TX 长度, 关闭顺序} 组合循环,每 50 ms 打印芯片两字节状态 + 中断寄存器;主机侧 test/host/ch395f_reuse_host.py 同步做纯 TCP 客户端。OS/net/FTP 全部剥离后 bug 依旧复现 → 纯芯片操作时序问题,非上层逻辑、非芯片硬件缺陷。
关键现场(芯片日志):
post-listen st=0x05/0x04 ← listen 返回成功,但 TCP 态停在旧连接的 ESTABLISHED(幽灵态)
unexpected int=0x10 while wait ← 只等来旧连接滞后的 DISCONNECT
no CONNECT within 10s ← 新连接的 CONNECT 中断永远没来(芯片状态却已是 0x04)
芯片级规律:CH395F 关闭连接后的拆链收尾窗口变长且不可观测——状态寄存器显示 CLOSED 持续 1 s 以上也不代表拆完(实测窗口可 >2 s 且时长不定)。窗口内重新 open+listen:
- 新连接握手照常完成(状态寄存器 → ESTABLISHED)但 CONNECT 中断被丢(Trap 22 丢中断机制的窗口期形态)→ 事件驱动的 accept 永远等待 → 吞数据;
- 或 listen 后状态直接假报 0x04(幽灵态),此时回读对端端口 = 旧连接端口;
- 或旧连接滞后的 DISCONNECT 打到新监听 → 触发 relisten 毁掉刚到的新连接 → 425。
已证伪的修复(全部经台架受控实验)
| 尝试 | 结果 |
|---|---|
| close 后固定沉降 100/300/800/2000/3000 ms | 仍挂(2 s 也挂过)——窗口变长,固定等待无保证 |
| Socket 轮换(sock3↔sock4 交替) | 仍挂——上一轮拆链尾巴跨 socket 毒化下一轮 |
| listen 后 close+重试 / 耐心等幽灵消退 | 持平对照组,无效 |
| 收发缓冲重分配(clear_recv_buf + set_*_buf) | 无效 |
| close 前 200 ms TX 排空延时 | 无效 |
| 禁用对账 tick(验证因果) | 仍挂——对账不是元凶 |
有效修复(台架修复组 15/15,机制触发 5 次全部救回)
双信源原则:中断只是快路径,状态寄存器才是真相。 等连接时不能只等中断:
- accept 路径内联探针(
net_lost_connect_probe,net_accept_locked每 100 ms 轮询挂探针):芯片状态已 ESTABLISHED 而 CONNECT 中断未到 → 连续 2 轮去抖后补做 accept(复用handle_connect_event簿记)。 - 幽灵态甄别:补 accept 前回读对端端口(
GET_REMOTE_IPP),端口 = 上一条连接端口(last_peer_port)→ 幽灵,不 accept——否则会把数据发给已死的旧连接(这正是生产 LIST#4"accept 成功但数据被吞"的直接成因:对账②曾把幽灵连接补 accept 了)。 - 对账②同步加幽灵甄别(
net_reconcile_tick类型②分支同样比对last_peer_port)。 last_peer_port登记点:handle_disconnect_event/handle_timeout_event/net_relisten_try(清零 remote_port 前转登)/net_close_locked(跨 memset 保留——幽灵属芯片 Socket 级,与 fd 复用无关)/ NET_MSG_FIX 陈旧 ESTABLISHED 修复路径。
与 Trap 22 的关系
Trap 22 的周期对账(200ms 节拍 + 400ms 去抖 + 1s 冷却)设计目标是"自愈兜底",对常驻监听器有效;但 FTP 数据通道是"活 1 秒就死"的短命监听器,且对账②无幽灵甄别——它既救不全(冷却期内 accept 已超时)又反过来帮凶(把幽灵补 accept)。Trap 25 把"看状态补 accept"内联进 accept 热路径(短去抖、无冷却)并补上端口甄别,对账②保留为兜底网。
对应文件
Drivers/BSP/NET/net_socket.c(net_lost_connect_probe / net_accept_locked(含 closed-with-data accept)/ net_reconcile_tick② / last_peer_port 各登记点)、Drivers/BSP/NET/net_types.h(last_peer_port / lost_conn_seen 字段)、Drivers/BSP/NET/lftpd/lftpd.c(send_list/send_nlst 批量发送);台架工具 test/board/ch395f_reuse_bench.{c,h} + test/host/ch395f_reuse_host.py(实验分支 n30-chip-bench 注册编译,主分支只存档不注册)。
验证(台架层)
ch395f_reuse_bench 修复组(丢中断捕获自 accept)15/15,机制触发 5 次全部救回(对照同表 4/15 挂)。⚠ 生产回归当时未达 11/11——生产主导模式与台架不同,见下两节。
三次定案:所谓"kill 模式"是寄存器状态误读(2026-10-11 当日终案,双向验证)
第二轮验证(仅双信源探针修复,4 轮:1/11、5/11、2/11、2/11)暴露 STOR #2 仍 100% 失败,当时解读为"芯片在 accept 后 10-20 ms 杀连接"。后经手册条文 + 网络资料双向核对推翻:
寄存器读数本身全部正确(GET_SOCKET_STATUS_SN §5.22:05H=SOCKET_OPEN/00H=SOCKET_CLOSED;第二字节 TCP 态 00H=CLOSED/04H=ESTABLISHED,与 WCH 官方数据手册表 5-8/5-9 一致;GET_RECV_LEN_SN §5.33 低字节在前,驱动实现一致)。错在对 DISCONNECT 事件语义的解读:
net_init的ch395f_set_fun_para(0x08)置 FUN_PARA_FLAG_SOCKET_CLOSE=1(§5.52 ②):产生 DISCONNECT/TIMEOUT 后芯片不关 Socket、不清缓冲,就是留给单片机把残留数据读出后再关 Socket。- 判别探针现场:
kill-check recv_len=1027 re-read st=0x05/0x00——Socket 仍 OPEN、TCP 已 CLOSED、RX 缓冲 1027 字节 = STOR 载荷一个不少全在。这是芯片按 §5.52 规格保留现场,不是杀连接。 - 那个 DISCONNECT 是客户端的正常 FIN:Python ftplib
storbinary语义 =sendall发完立即关数据连接,然后回控制通道等 226——标准 FTP 行为。局域网小文件下"连接→发完→关闭"在 ~5 ms 内完成。
425 真根因(竞态 + 自毁缓冲):客户端 5 ms 内完成发送并关闭;cmd_stor 的 accept RPC 赶到 net 层时 MCU 态已是 CLOSED;旧 net_accept_locked 把 CLOSED 一律当"无连接" → net_relisten_try(close+open 清空芯片 RX 缓冲)→ 1027 字节被我方亲手销毁 → WOULDBLOCK → lftpd 判"数据套接字已关" → 425。芯片全程无辜(此模式)。
有效修复(已落地,net_accept_locked CLOSED 分支):CLOSED + auto_relisten + 芯片 RX 缓冲有数据(GET_RECV_LEN_SN>0)→ 接受这条"已关但可读"连接(日志 closed-with-data accept (drain pending)),应用经 net_recv_locked 快路径(无状态门槛)排空,排空后 EOF 自然触发 relisten——完全对齐 §5.52 ②"读出残留数据后再关闭"的芯片契约。验证:修复后 4 轮回归 STOR #2 从 4/4 失败 → 4/4 通过(7/11、6/11、9/11、5/11,失败项中已无 STOR)。
连带修正:台架 mode0(主机先关)观察到的"accept 后 10-20 ms DISCONNECT"大概率同为误读——那是主机收完数据自己关的。"拆链窗口杀连接"理论对生产 425 不成立;台架丢 CONNECT 中断模式(主机侧 got=0 + 芯片 0x04 + 无中断,主机端独立证据)仍然成立,双信源探针修复保留。
当时的遗留:LIST #4/#7 超时(服务端 net_send 目录、客户端 20 s 收 0 字节、无 DISCONNECT)——发送方向的真问题,与上述 425 机制不同——已由下节"四次定案"解决。
四次定案:LIST 吞包 = 背靠背小段写毒化芯片 TX(2026-10-11 终案,修复后 8/8 通过)
探针定位过程(全部生产固件 + 临时探针,一轮复现):
- PASV→accept 间隔 2 s(
net_listen_lockedopen 重试在跑)→ 曾疑"listen 撞拆链窗口"; - accept 正常、CONNECT 正常、发送后中断位
0x03(SENBUF_FREE+SEND_OK)——芯片自认发送成功; GET_REMOTE_IPP对端 IP:port 完全正确(排除对端混淆);- Socket 轮换(服务端先关后 4 s 冷却、数据连接轮转 sock3~7):证伪——LIST 换到全新 sock5 仍 100% 挂(芯片行为与失败前完全一致);
- 决定性对照:同一目录同一载荷,LIST #1(会话首连,FAT 冷、readdir 慢、行间天然间隔大)通过;LIST #4/#7(FAT 热、每行一个
net_send背靠背小段)必挂。
定案:send_list/send_nlst 每行一次 net_send(~60 B 小段、间隔仅 ms 级)——CH395F 对同一连接的背靠背多段小写入会把 TX 整条连接毒化(芯片报 SEND_OK 但对端协议栈全部丢弃,无 RST 无 DISCONNECT)。net_send_locked 的逐块协议(等 SENBUF_FREE 再写下一块)本身合规(§5.45),但芯片对该模式的内部处理不可靠;RETR 的 1 KB 满块连发不受影响(实测通过)。
修复(lftpd.c):send_list/send_nlst 整份目录一次性批量发送(static 1536 B 缓冲,snprintf 累积 + 单次 lftpd_inet_write;溢出截断判 -1→550)。注意 lftpd_inet_write 成功返回字节数非 0(首版误按 0=成功判 → 全 550,已纠)。
验证:修复后 ftp_regression 连跑 4 轮 11/11(终版:批量发送 + closed-with-data accept + 丢中断探针 + 幽灵甄别,无轮换无探针)+ ftp_auth_bench.py 13/13 + @test 启动标记 PASS;此前同条件 LIST #4/#7 为 100% 失败、STOR #2 曾 100% 425。轮换冷却机制随证伪已撤(不留未证明的复杂度;其 5 轮 11/11 一并存档备查)。
教训(Trap 25 总纲):本轮先后误读三次——把客户端正常 FIN 读成"芯片杀连接"、把 open 重试读成"拆链窗口"、把芯片假 SEND_OK 读成"数据已上线"。铁律:CH395F 的"成功"(中断位/状态码)只代表芯片本地动作完成,不代表对端收到;判定必须回到对端可见证据(主机侧收发)+ 手册语义(FUN_PARA §5.52、状态码 §5.22)。
顺带发现的疑点(2026-10-11 已定案):set_fun_para(0x09) 的 bit0——§5.52 表明确写 bit0 保留、必须为 0,而 net_init 注释声称"bit0=1 多 Socket 模式"(手册与网络资料均无此说法,CH395 的 8 Socket 是芯片原生能力,靠 Socket 索引 _SN 寻址)。现固件 bit0=1 下 sock1/2/3 多路并行正常 → 该位被芯片忽略。定案:改回 0x08——官方手册 V1C/V1E/V2.2/V2.3 的 bit0 全是保留位,官方例程 bit0=0 下 8 个 Socket 照常可用(EXAM5_TCP_Server 传 0x02 却给 Socket 0~7 配缓冲),官方 FTP 例程本身就传 0x08。完整证据链见 Trap 18「定案更正」;0x08 下的多 Socket 已台架实测通过(2026-10-11 20:18,Phase 7 34/34,并发回显落在 sock2/4/5/6/7,N-31 已销项)。
发现时间
2026-10-11(登记簿 N-30;裸机芯片台架定案实验见本节,实验分支 n30-chip-bench)。
2026-10-11 寄存器 / 数值口径全量审计(智能体团队,只读)
方法:四路并行——驱动层逐命令(含官方 SDK 逐字对照)、NET/测试层逐调用点、Trap 01–12 复核、Trap 13–25 复核。三源交叉:docs/芯片手册/CH395F.md + WCH 官方例程 CH395EVT/EXAM/PUB/CH395CMD.c、CH395INC.H + git 历史。
干净清单(核对过、无异常)
| 项 | 计数 | 结论 |
|---|---|---|
CH395F_CMD_* 命令码 |
55 | 与 §5 命令表 + SDK CMDxx_ 逐个一致,无一抄错 |
| 16/32 位量字节序 | 9 处 | 全部低字节在前,与手册明文 + SDK 逐字节一致 |
| 读命令输出字节数 | 14 条 | 无少读/多读(GET_SOCKET_STATUS=2、GET_IP_INF=20 且顺序 IP→GW→MASK→DNS1→DNS2、GET_UNREACH=8、GET_REMOT_IPP=6) |
| 常量表 | 60 个 | ERR_*18 / TCP_*11 / SOCKET_*2 / PHY_*6 / PROTO_*4 / SINT_STAT_*6 位号全对;唯一错值见 A3 |
GET_GLOB_INT_STATUS_ALL 拼装方向 |
— | 无异常:我们 第1字节|第2字节<<8 与官方 CH395CMDGetGlobIntStatus() 逐字相同;glob & (1<<(sock+4)) = §5.16 bit4..bit11 = SDK GINT_STAT_SOCKn |
get_socket_status 缓冲区 |
17 处调用点 | 全部 uint8_t st[2],无 1 字节残留(db64e6ac 引入 → e5733b34 修复,HEAD 无回归) |
| 中断判据 | 17 掩码 / 32 全局读 / 27 SINT 读 | 位号与 §5.16/§5.23 一致;只用 16 位 ALL 版 |
| 单位与取值范围 | 12 个参数点 | retrans 8/200 ms、KeepAlive 60000/5000(500 倍数、IDLE>INTVL)、ARP 100 ms 单位、MSS 1024 全部合规 |
| 缓冲布局 | 3 处组合 | net_init 8×(4收+2发)=48 块恰好用满、无交叠(零裕量) |
| 显示假象 | 10 处 %d.%d.%d.%d |
只有已定案的 net_socket.c:1631 是 MSB 重排假象,无新增同类 |
新发现(去重后按严重度;已修 = 本次提交内)
| # | 严重度 | 位置 | 问题 | 状态 |
|---|---|---|---|---|
| A1 | 中 | test/board/ch395f_test_task.c:1005 |
TC-203 的 Trap 14 反核实写成 st_chk[0] == CH395F_TCP_ESTABLISHED——byte0 只有 00H/05H,条件恒假 → 该防线永久失效且日志无痕(与 Trap 07 同类:byte0/byte1 语义越界) |
✅ 已修(改 byte0==OPEN && byte1==ESTABLISHED) |
| A2 | 中 | 同上 :1150 / :1321 |
Phase 3/4 把 GET_RECV_LEN 返回值直接当读取长度(本地 rx_buf[300]/[64],芯片 RX 缓冲 2 KB)→ 栈溢出 |
✅ 已修(夹到 sizeof + 留痕) |
| A3 | 低 | ch395f.h:253 |
CH395F_GINT_STAT_DHCPV6 = 0x8000(bit15);SDK 是 (1<<12)、§5.16 明确 CH395F 的 [12:15] 保留 |
✅ 已修(改 0x1000 + 注明本芯片恒 0;全仓零引用) |
| A4 | 低 | ch395f.h:33-91、ch395f.c:484/506、ch395f.h:701 |
分组注释字节数错 4 处(GET_GLOB_INT_STATUS_ALL/GET_UNREACH_IPPT 归"1 字节输出"、SET_FUN_PARA 归"1 入 1 出"、SET_RETRAN_PERIOD 归"3 字节输入"、GET_SOCKET_STATUS_SN 归"1 字节输出");KeepAlive 参数注释写"秒"(§5.53/5.54 是 ms);set_retrans_count 写"0=默认值5"(§5.17 默认 12、无 0=默认语义)。实现全部正确,注释误导——Trap 25 的 1 字节缓冲栈越界正是这类注释发起的 |
✅ 已修 |
| A5 | 低 | net_socket.c:238/1005/1080/1297/1666/708/3056 |
注释与手册/HEAD 相反:FUN_PARA(SOCKET_CLOSE=0)(实为 bit3=1,正是 closed-with-data accept 的契约依据)、"初始化成功后设置 MAC"(§9.2.1 ① 是之前)、"复用默认缓冲"(已 8 Socket 显式分配)、"布局由 MSS 推导"(固定布局、Trap 21 已解耦)、"等 SEND_OK"(门控是 SENBUF_FREE)、UDP 头 [0-1] reserved(§9.2.4 是数据长度) |
✅ 已修 |
| A6 | 低 | ch395f.c:1176 |
write_send_buf 超限静默 return(void、无日志),且上限取 DMA 2048 而非发送缓冲 1024;读路径有 OVERSIZE 金丝雀、写路径没有 |
✅ 已修(补 [ERR] 留痕 + >1 KB 告警,行为不变) |
| B1 | net_socket.c:3553 |
原判"产品路径从未启用 Socket KeepAlive → 静默断开无人知晓"(§5.56 服务器侧应在 CONNECT 后开,产品三服务全是 Server,60s/5s/3 参数空转)。2026-10-12 实测推翻前提:连接静置时禁用 PC 网卡(真·静默死亡),芯片 +2 s 即报 [ERR][NET] sock0 TIMEOUT → 本层 relisten quarantine 释放重开;2 s 恰为 §5.17 重传 8×200 ms=1.6 s 量级,与 KeepAlive 无关(IDLE 最短 60 s)。链路层失效本就被快速覆盖。 |
✅ 已按 §5.56 补过使能并实测,依 Trap 25「不留未证明的复杂度」撤回,代码内留注释记录;残余场景见下「台架注入陷阱」 | |
| B2 | 中低 | net_socket.c:1638 |
ch395f_tcp_connect() 返回值被丢弃(同文件 open/listen 都查)。§9.2.2 ⑧ 明文:GET_CMD_STATUS 返回 SUCCESS 仅表示命令执行成功、其他值表示命令执行失败,丢弃它 = 命令级被拒要等 30 s recv 超时才暴露 |
✅ 已修(2026-10-12):非 SUCCESS 且非 BUSY 才判失败——poll_cmd_status 超时返回的就是 ERR_BUSY(芯片仍在执行、命令未被拒),把它当失败会误杀慢芯片;明确失败 → [ERR] rejected by chip: 0xNN + 按 §9.2.2 ⑧"从 ⑤ 重开"关 Socket 回滚 + 返 -1。Phase 7 实测回归:TC701 回显 64 B 校验 PASS、TC702 重连 ×3 全 PASS(正常连接无误杀);不可达对端时芯片回的是 SUCCESS、ret=0 且不打 rejected by chip,失败仍由 CONNECT TIMEOUT(~1.6 s) 裁定 → 行为未变。⚠ 残余:net_reconcile_tick 仍只裁 ESTABLISHED/LISTENING,CONNECTING 丢中断无人收口(产品不走出向连接、无法实测,按"不加未证明复杂度"暂不动);TC703 断言 rc==0 把"返回值被丢弃"写进了判据,真发生命令级被拒时它会 FAIL(属正确告警) |
| B3 | 低 | 两处:net_socket.c:1120-1125(net_listen_locked)+ net_socket.c:2060-2065(net_relisten_locked,审计原只登记前者,后者才是每次断连的热路径) |
两处对全部 8 个 Socket GET_INT_STATUS_SN(读即清)+ 无条件读清全局。机制:§5.15 明文「此命令执行完毕后…将全局中断状态清除」= 全局寄存器整体读即清、无按位清除手段,而 net_poll 只在全局位置位时才去读该 Socket 的 SINT(:469 门控)→ 丢弃式读全局把别的在用 Socket 的门一并焊死;SINT 又粘滞到被读(Trap 14 实证 close/open 都不清 SINT)→ 全量读走即永久销毁其 CONNECT/DISCONNECT/TIM_OUT/SENBUF_FREE(唯 RECV 是条件位可下轮补,Trap 08)。git 考古:e862009bc(2026-07-19 多连接时代,无注释引入,同提交还删掉了 net_poll 里"勿在此读 per-socket 中断"的警告) → 319d00e3 原样搬进单连接 → db64e6acf 复制进 relisten;不属于任何已登记 Trap 的修复(Trap 14/17/23 指定的预清都在别处且是单 Socket)。官方 SDK 21 个例程一律"全局位图门控 + 单 Socket 读 + 立刻转存",open/listen 前后不清任何中断,无一例全量读 8 个 SINT |
✅ 已修(2026-10-12):两处收窄为只清本 Socket 的 SINT + 删除丢弃式全局读清(全局交 net_poll 集中读清,与 AGENTS 模块约定「所有 CH395F 中断在 net_poll 内集中读取并清除」一致);本 Socket 残留的全局位被 net_poll 读到时 SINT 已为 0,无害了结。两路子 Agent 曾分歧:一路建议保留全局读清("只搁浅不销毁"),一路警告"保留=修一半"——采后者,芯片无按位清除,保留即热路径继续焊门。回归:@build 0/0 Code=197008(−24 B);@test PASS;Trap 25 ftp_regression 11/11 ×3;Trap 23 ftp_abort_probe --check-abort RST 5/5(450) + FIN 5/5(226);Trap 22 :21 0ms×50 50/50、100ms×50 50/50、:8888 0ms×20 20/20(均与文档基线一致);:7003 腿见 B10(A/B 已证明与本改动无关;B10 修完后该腿回到 10/10)。⚠ 残余:net_connect_locked:1601 / net_close_locked:2571 / net_relisten_locked:1986 三处无条件全局读清仍会搁浅(延后、非销毁)其它 Socket 挂起事件——它们是 Trap 14/17/23 的指定两级预清,本次不动,宜与 B4 一并处置 |
| B4 | 低 | net_socket.c:446/469 |
全局中断 bit0-3(UNREACH/IP_CONFLI/PHY_CHANGE/DHCP)被读清但从不消费;ch395f_get_unreach_info() 全仓零调用 → IP 冲突、ARP 不可达、链路抖动三类事件完全静默 |
⏳ 待修(可观测性缺口) |
| B5 | 低 | net_socket.c:3878-3881(net_getsockname,函数 :3851-3887;原登记 :3849-3854 已陈旧)→ lftpd.c:803-809 |
net_getsockname 的 s_addr 组装方向与本层约定(net_inet_addr/net_inet_ntoa)相反 → FTP 日志把设备自身 IP 打成 100.1.168.192。⚠ 同一 getsockname 的 PASV 227 消费点(lftpd.c:497-503)做了 net_htonl 再 MSB 拆,双重反转恰好正确,勿误判;风险前瞻:若把该 s_addr 喂给 net_connect/net_sendto 就会连到反序地址 |
⏳ 待修(改一处须同时核对 PASV) |
| B6 | 低 | net_socket.c:591/1225/3608 |
3 处状态判据只看 byte1、未按 §5.22 用 byte0 门控;reconcile 的 else-if 链里 (00H, 陈旧 04H) 会先命中"补 accept"而非"重建监听" |
⏳ 待修(Trap 07 实测 byte0=00H 时 byte1 读回 0x00,故为低概率) |
| B7 | 低 | ch395f_test_task.c:1379/1504 |
Phase 5 单次写 4093 B 超 1 KB 发送缓冲与 2048 DMA 载荷 → 整块被丢;注释仍写"4096 B = 发送缓冲上限"(2026-09-22 布局解耦前的旧事实)。A6 修后会打 [ERR] 而非静默 stall |
⏳ 待修(chunk 改 ≤1024 或分块) |
| B8 | 低 | ch395f_test_task.c:1290-1292 |
Phase 4 把 sock6 缓冲重排到块 24-27,与 net_init 固定布局中 sock4 的 24-27 交叠(§5.45 禁止) |
⏳ 待修 |
| B9 | 低 | ch395f_reuse_bench.c:93、net_socket.c:39、net_socket.c:262 |
bench 用 get_phy_status() != 0 判链路(01H=断开也成立,net_task.c:122 的 != CH395F_PHY_DISCONN 才对);NET_PHY_CHECK_INTERVAL_MS 死宏;无芯片版本门控(§5.16 <0x44 不支持 ALL 版、§5.52 bit3/4 需 ≥0x46,实测 0x4A 无影响) |
⏳ 待修(前两项死代码/卫生) |
| B10 | 中 | net_reconcile_tick 认领范围 + handle_disconnect_event 释放分支(net_socket.c:3663-3686) |
孤儿监听 → 端口永久拒连。断开时释放分支把状态置 CLOSED 并注释「留给 net_recv_locked 把剩余数据读出来再关,等待应用重新监听」——即由应用负责重建;但 ui_net_sink 只在 net_recv 返 0(EOF)时才清 g_client,而 EOF 已被这个事件分支消费掉 → net 层等应用重建 / 应用等 net 层给 EOF / 对账只裁 ESTABLISHED·LISTENING 直接跳过 / 芯片不会自己回 LISTEN,四方互等。实测(临时探针):TEMP orphan sock2 state=0 port=7003 chip=0x05/0x00 每秒一条持续 12 s+,:7003 300ms×10 0/10、+30 s 仍拒连。与 Trap 22 的关系:Trap 22 的①③两类都要求本层处于 ESTABLISHED/LISTENING,孤儿态恰好落在两类之外,所以那套对账全程沉默 |
✅ 已修(2026-10-12):net_reconcile_tick 新增类型④认领孤儿(in_use + state==CLOSED + auto_relisten + type==STREAM + local_port!=0,芯片侧不在 LISTEN)→ 复用 net_relisten_try(自带"已在重试队列则不重复登记"保护,与 D10 重试队列、Trap 23 沉降隔离兼容)。⚠ auto_relisten 是决定性门:少了它,"本层 CLOSED + 已绑端口"的出向连接(板级 Phase 7 TC701net_connect 绑本地口)会被误判孤儿并被 relisten 成监听器。回归::7003 300ms×10 0/10 → 10/10(+20 s 存活);:21 0ms×50 50/50、100ms×50 50/50;:8888 0ms×20 20/20;ftp_regression 11/11 ×2;Trap 23 RST 5/5、FIN 5/5;@test PASS(600 ms、回读无差异);Code 197008→197084。⚠ 残余(未根治,已立项根治):生产者仍在——释放分支依旧把状态停在 CLOSED 等应用重建,④是兜底(实测救援后端口 1 s 内恢复,日志 reconcile: orphan listener … -> relisten 可见)。⚠ ④ 有副作用:net_recv 的 EOF 是边沿(只在 recv 那一刻 state==CLOSED 才给 0,:1880-1888;已被别处 relisten 回 LISTENING 则恒 WOULDBLOCK,:1890)→ ④ 抢先 relisten 会把慢消费者的 EOF 永久吞掉,g_client 不清、新客户端数据被旧会话静默吸收。原理与分阶段根治方案 → docs/子系统设计/网络Socket监听生命周期设计说明.md(含:芯片层为何永不自动回 LISTEN、本仓 fun_para=0x08 使 0x05/0x00 成为合法常驻态、RFC 1122 §4.2.2.18 与 POSIX accept 的规范冲突、12 个重建所有者与 U1 |
文档侧待修清单(本轮只改了最要紧的几处,其余登记在此)
- 最高优先级:Trap 13 的"部分读后
recv_len归零"与 §5.34 直接冲突(已在本条加复核注),但同一机理仍被当作现行约束写在CH395F_Test_Guide.md:28/69/930/932/958、docs/网络/BSD_Socket_API_函数调用关系.md:196/362、net_socket.c两处注释里——须与 Trap 21 的撤回同批处置,并补一条"部分读 → 再查recv_len"的定向 TC。 - 陈旧数字:
CH395F_Test_Guide.md:14/24/645/660/1141/1200(DMA 4100 / 载荷 4096,现 2052/2048)、:72/1201(NET_DMA_MAX_PAYLOAD=4096,现 2048;实际分块上限是NET_SEND_CHUNK_MAX=1024)、:1020-1040(TC-NET-1002/1003 期望 4096/4097 单次 DMA)、docs/网络/BSD_Socket_API_使用指南.md:1246(NET_SEND_BUF_SIZE宏已不存在)。 - 悬空引用:
Trap 19从未存在(Test_Guide.md:878仍引用);Test_Guide.md:353/418的"见风险 #4 / 风险表 #1"指向已删除的表。 - 口径冲突:
Test_Guide.md:1135的 TRAP-07 行仍写"卡 FIN_WAIT_2 /close_socket(RST)"(与 Trap 07 定案更正冲突);:45的"TC704 40/40"与:880的"PHASE7_MULTI_COUNT≤5"矛盾(HEAD 宏值仍是 8);Drivers/BSP/NET/README.md:35与net_socket.c:1864的"正常 FIN 路径零改动"与 Trap 23 二阶段矛盾。 - 机理措辞:Trap 17"等对端 FIN/ACK"应按 Trap 07 实测改写(自身 FIN 的 ACK + 本地 TIME_WAIT,0~150 ms);Trap 20"硬件发送 FIFO 1 KB"应改"本仓 2 块分配的发送缓冲区"(§5.45 上电默认是 4 块=2 KB)、"等
SEND_OK"应为SENBUF_FREE;Trap 25 的NET_MSG_FIX应为NET_MSG_RESYNC_LISTEN、GET_REMOTE_IPP应为GET_REMOT_IPP、"§5.45"应为 §5.32+§5.23①。 - 已失效条目未标注:Trap 01/02/11 的多连接模型随 2026-08-25
319d00e3删除;Trap 09 的osDelay(7000)已被就绪屏障取代;Trap 10 的"net_connect()不使用消息队列"已失效;Trap 12 的 #2/#4 与仓内 FatFs 源码矛盾(ff.c:3101-3109剥前导分隔符、:3127-3129空名返回根目录),且FF_FS_RPATH=0下"."不是合法路径段(HEAD 用"0:")。 - 日期:Trap 01/02/03/04/06/07 的"2025-07"均早于仓库首提交
b1d05091(2026-06-05);Trap 03/04/06 的 Phase 编号也错(DHCP=阶段 4、UDP=阶段 3、KeepAlive=阶段 11)。 - 证据通则:
*.log在.gitignore内 → 本文所有台架计数(40/40、34/34、15/15、4 轮 11/11、13/13…)脚本在库、日志未入库 = 不可复核;Trap 25 引用的实验分支n30-chip-bench(tipeddbede)不含 bench 源文件与 uvprojx 注册,裸机芯片台架无法从任何已提交状态重建。
主机侧测试工具过期(2026-10-12 实测)
MDK-ARM/ftp_abort_probe.py已修:raw_login用USER anonymous、followup_ok用login("anonymous","x"),N-19 后匿名被 530 拒 → Trap 23 两条腿假 0/5(round 1 报RuntimeError: PASV failed,且这条半认证会话占死 FTP 唯一会话,导致后续轮全变ConnectionRefused)。改admin/admin(两处)后 RST 5/5、FIN 5/5,与:779/:781基线一致。教训:跑既有回归腿前先确认凭据仍有效,否则会把工具过期读成固件回归。- 同族未修:
test/host/ftp_up_dl_test.py(:123-126)、ftp_speed_test.py(:116-121)、ftp_test.py(:93-99)仍用匿名登录 → 对当前固件必挂登录步;docs/测试说明/FTP_Test_Guide.md推荐的这几个入口因此已失效(现行可用入口是test/host/ftp_regression.py,默认admin/admin)。
台架注入陷阱(2026-10-12,B1 复核过程中踩到)
- Windows 出站防火墙规则不能用来模拟"对端静默死亡":WFP 对已建立的连接有状态缓存,中途
New-NetFirewallRule -Direction Outbound -Action Block -RemoteAddress <设备IP>不影响既有流——实测静置 + 阻断 250 s 芯片毫无反应,看似"KeepAlive 不生效",实则 ACK 一直在正常往返。 - 更糟的副作用:该规则会吃掉本机后来发出的 FIN → 设备侧留下一条幽灵 ESTABLISHED,之后所有新连接被 §5.29"向试图连接的远端发送 TCP RESET"直接 RST,表现为"端口拒连",容易被误判成设备故障(实为自伤)。撤规则后设备自行恢复。
- 能真正掐断链路的手段:
Disable-NetAdapter(链路真的 down)。⚠ 本机 PowerShell 的该 cmdlet 只有-Name/-InterfaceDescription,没有-InterfaceIndex/-ifIndex;网卡名是中文,用(Get-NetAdapter -InterfaceIndex 19).Name取值再传入,避免命令行里写中文字面量。禁用/恢复必须写在try/finally里,否则网卡留在关闭状态。 - 仍未覆盖的场景:
Disable-NetAdapter只能造"链路层失效";"链路仍在、对端静默"(NAT 会话过期、中间节点黑洞)本轮没有安全注入手段(静态 ARP 黑洞会被交换机泛洪兜底、删 IP 会连带拆掉本机连接)。这才是服务器侧 KeepAlive 唯一可能的价值场景,未证实,故不保留该改动。 - 主机侧回显服务器收不到入站连接 → Phase 7 全量
CONNECT TIMEOUT(2026-10-12 实测):本机对C:\programdata\anaconda3\python.exe有两条 Public 入站 Block 规则(C:\python314\python.exe才是 Allow),而.venv\Scripts\python.exe的基座解释器正是 Anaconda python(构建报错自曝C:\ProgramData\anaconda3\python.exe: can't open file ...)。Windows 防火墙块规则优先于放行规则 → 中途加的New-NetFirewallRule -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8081 -Profile Any完全无效(实测两轮全灭),只有Set-NetFirewallProfile -Name Public -Enabled False才通。症状极具误导性:芯片 ~1.6 s 报CONNECT TIMEOUT、net_connect仍返 0、[CONN] openOK照常打印,看着像芯片/驱动故障,实为主机防火墙。固定跑法:try { 关 Public → 复跑 } finally { 开 Public }(或改用C:\python314\python.exe起服务器)。 - Phase 7 的两条既有失败与改动无关,别追:① TC704
socket create #5FAIL = E-23(NET_MAX_SOCKETS=8减 3 个常驻监听 → 并发上限 5,而PHASE7_MULTI_COUNT仍是 8);② TC703 要求 PC 服务器离线,与 701/702/704 环境互斥,同轮连跑必有一侧失真(本轮服务器在线时它反而 PASS,只因芯片先CONNECT TIMEOUT)。 - 判读教训(与 Trap 25 同源):看到
TIMEOUT先算时序再归因——2 s 与 60 s 一眼可分;否则会把"重传超时"误记成"KeepAlive 生效",把相关性当因果。 - A/B 归因前必须每腿硬复位(B10,2026-10-12):
:7003这类腿一旦把监听打死就不自恢复,下一腿是接着死状态跑的 → 对照组失真。本次第一腿(基线 1/10)就是这么废的;正确做法是每个样本前stlink_flash.py reset+ 等 12 s 干净启动,并取多样本(本轮基线 5/1/1/5 vs 改动 3/7/2/1,均值 3.0 vs 3.25 → 统计不可区分,才敢判"与改动无关")。 [PROF]/[PROF2]字段不是计数器,别拿来判"任务卡死"(B10):netp=net_poll段最长耗时(µs)、netn= 采样瞬间在段内的时长(App/util/task_prof.c:172)→ 数值恒定只说明"没刷新最大值"。真正的停摆信号是[ERR][WD] task stall;且其miss=打印的是累计s_miss_total(不是连续缺席数,连续数封顶在容忍值 8),容忍 = 8 轮 ×100 ms = 连续 800 ms 有任务缺席即停喂、IWDG 2.5 s 溢出(App/util/task_watchdog.c:466-492)。缺席位图解码:req=0x77为 DEFAULT/NET/FTP/UI/TIMESYNC/ADC,mask=0x41→ 缺席 NET+FTP+UI+TIMESYNC 而 DEFAULT/ADC 正常 = "netTask 卡住、所有做net_*的任务阻塞在osMessageQueuePut(osWaitForever)+xTaskNotifyWait(portMAX_DELAY)后面"的指纹。- 静默黑洞用临时探针抓状态,测完即删(B10 手法):B10 的孤儿态在日志里毫无痕迹,靠在对账跳过分支加一条
TEMP orphan … state=%d port=%u chip=0x%02X/0x%02X才直接看到state=0 / chip=0x05/0x00;同一轮还顺手用探针否掉了自己的第一假设(怀疑net_listen_locked的osDelay(500)冻结 netTask → 探针 0 次命中)。假设写盘前先测,比推理便宜。