分层:流程与规范/ 单元测试/ 板级测试/ 专项验证/ 测试工具/ 测试数据/, 顶层只留新增的目录导航 README.md(归层判据 + 硬约束 + 本次处置记录)。 - 搬迁 28 个路径:测试数据三个目录 58 个文件为纯改名(R100,内容逐字节不变) - 全库引用同步 124 个文件:AGENTS / docs 索引 / skills / 门禁注释 / scripts - 门禁面:scripts/check_test_traceability.py 的 glob 改 rglob (分层后不递归扫描=追溯门禁直接瞎掉) - scripts/rename_ref.py:收尾的 ℹ 提示在 GBK 代码页抛 UnicodeEncodeError, 让进程以退出码 1 收场而改写其实已落盘(RC 与真实结果相反);改为把 stdout/stderr reconfigure 成 UTF-8,与 check_doc_links.py 等同法 - docs/文档规范.md §1.1 补「子目录文件过多时再分一层」;§3.6 补 rename_ref 搬整目录的两个实测坑:① 会改写目录内部文件的内容 ② 内部文件的 ../ 出链 不补层级(本次 23 个被误改的证据文件已按 HEAD 原始字节逐字节还原) - docs/README.md §6 改为与六个子目录一一对应,并补登原先漏登记的 「启动日志原子性复验流程」一行 - .cubemx_survival_baseline.json:App/util/task_watchdog.c 的注释路径也随分层更新, 该文件是存活审计的 watchdog-module 漂移锚点,按脚本自己的提示跑 --write 重建基线 (实测重建后仅 watchdog-module 一条哈希变化) - 铁律 4 例外(本次在任务树里改了单写者文件):AGENTS.md 与 .pre-commit-config.yaml 只被 rename_ref 改写路径字面量(后者全在注释行), 任务表块与钩子声明一字未动 验证:check_doc_links.py --sections-strict 覆盖全部 88 个改动 .md 全绿; check_test_traceability.py 输出与主树逐行一致;196 个文本文件无 BOM、无新增 CR、无 U+FFFD、无行尾空白;陈旧引用面由主树 25 处降到 6 处(无新增断链)。
53 KiB
BSD Socket API 函数调用关系与设计说明
定位:本文档是
docs/网络/BSD_Socket_API_使用指南.md(下称"使用指南")的内部视图伴读文档。 使用指南回答"上层该怎么调"(公开语义 / 示例 / 常见问题);本文回答"内部函数各自做什么、互相怎么调用、为什么这么设计"(调用关系 / 设计意图 / 踩坑)。范围:MCU 端
Drivers/BSP/NET/net_socket.c+ 其拆出的Drivers/BSP/NET/net_listener.c(监听生命周期)+Drivers/BSP/NET/net_pending.c(挂起 recv / send 请求表)。 模拟器端后端simulator/sim/net_socket_sim.c(Winsock)是另一套实现,见docs/网络/BSD_Socket_API_使用指南.md§2.3;net_select.c不在此展开,见docs/网络/BSD_Socket_API_使用指南.md§7 / §10.5。net_listener.c/net_pending.c真机专属(模拟器不编译);设计依据分别见docs/网络/网络自愈架构设计.md§11 与docs/网络/Net层阻塞模型架构设计.md。交叉引用约定:机制标 Trap 号 →
docs/问题记录/CH395F_Trap_Records.md;用法语义引用使用指南 §;关键参数值与net_config.h/net_socket.c内宏一致。
1. 一句话架构
一切芯片(CH395F)操作收敛到唯一执行者 netTask 串行完成;其他任务通过"入队消息 + 任务通知"表达请求,结果只经任务通知值回传;阻塞等待不独占 netTask(挂起登记 + 主循环节拍完成);监听器常驻(auto_relisten + D10 非阻塞重试 + TRAP-NET-17 周期对账 + 监听重建合一 net_reconcile_tick(唯一重建入口=形态③④,100ms) + TRAP-NET-18 排渣隔离,四层自愈;原监听器 pump 已并入对账)。
2. 三个执行域
| 执行域 | 运行位置 / 节奏 | 允许的操作 | 典型调用者 |
|---|---|---|---|
| netTask 主循环 | 独立 FreeRTOS 任务,1ms/tick | 唯一可碰芯片(SPI2)的地方;4 个非阻塞 tick(D10 重试 / 挂起 recv / 挂起 send / TRAP-NET-17 对账+监听重建合一 reconciler);队列分派;事件处理器;用户回调 | net_poll / net_process_messages 及其下游全部 _locked / _tick / handle_* |
| 应用任务侧 | ftpTask / uiTask / timeSyncTask / testTask 等 | 经队列的公共 API(入队 + xTaskNotifyWait 等回执);纯 RAM 直调 API;禁止直碰芯片 |
各服务任务 |
| 直调快路径 | 同步调用,不经过队列 | net_socket / 查询族 / 回调注册(纯 RAM);直碰 SPI 的三例外:net_bind(UDP) / net_sendto / net_recvfrom |
初始化阶段、netTask 上下文、测试套件(硬件层测试模式下 netTask 自挂起,testTask 独占芯片) |
⚠ 直碰 SPI 的三例外只能在"芯片访问者唯一"的上下文调用:产品构建中应用任务一律走队列(TCP 全路径);三例外的实际消费者是测试套件(见 AGENTS.md"测试构建")。从其他任务直接调用会与
net_poll的 SPI 事务竞争,破坏 CH395F 寄存器(TRAP-NET-14 实证:CONNECT 超时)。
3. 总体调用关系图
flowchart TB
subgraph APP["应用任务(非 netTask)"]
PUB["经队列公共 API<br/>netListen · netAccept · netConnect · netSend<br/>netRecv · netRecvTo · netClose · netDropConn"]
RAM["直调 · 纯 RAM<br/>netSocket · netSockState · netGetSock<br/>netGetErrno · netSetEventCb · netGetSockname<br/>字节序(netHtons / netInetAddr …)"]
SPI["直调 · 直碰 SPI(须芯片唯一上下文)<br/>netBind(UDP) · netSendto · netRecvFrom"]
CB["用户事件回调<br/>(在 netTask 上下文执行!)"]
end
MQ[("netMsgQueue<br/>8 槽 · 按值拷贝")]
XN["xTaskNotify / xTaskNotifyWait<br/>(结果回传的唯一通道)"]
subgraph NT["netTask 主循环 — 1ms/tick;芯片操作唯一执行者"]
NPM["netProcessMessages<br/>先跑 4 个 tick,再排空队列分派"]
NP["netPoll<br/>INT# GPIO 轮询 → 全局中断 → 各 Socket SINT → 状态缓存全量刷新 + sock_reset 步进 → 每轮一批"]
RetryTick["① netListenerRetryTick<br/>D10 重监听重试:30ms / 2s 时限"]
PendTick["② netPendingTick<br/>挂起 recv:①数据 ②到点(仅显式预算) ③断连 ④tripwire 60s 只留痕"]
PendShimTick["③ netSendPendingTick<br/>挂起 send:①断连 ②推进(每轮≤1块) ③写完 ④无进展超时"]
ReconcileTick["④ netReconcileTick<br/>TRAP-NET-17 对账 + 监听重建合一:100ms / 分级去抖 / 冷却<br/>读驱动状态缓存(①②③④⑤ + U11)"]
DISP["队列分派 switch<br/>SEND · RECV · CLOSE<br/>CONNECT · LISTEN · ACCEPT · DROP_CONN"]
subgraph LK["_locked / 内部分派目标(仅 netTask 上下文)"]
LL["netListenLocked<br/>初次监听(open 重试 3× + sock_reset 排渣)"]
LN["netConnectLocked<br/>open 重试 3×(sock_reset 排渣)+ tcpConnect"]
LS["netSendLocked<br/>分块 ≤1KB + sendReady 门控;超 NET_SEND_SPIN_MS 登记挂起 send"]
LR["netRecvLocked<br/>快路径 / DONTWAIT / 登记挂起"]
LA["netAcceptLocked<br/>先 netPoll + 状态判定"]
LC["netCloseLocked<br/>结束挂起 + cmd_done 快路径 / sock_reset(GRACEFUL)"]
DC["netDropConnLocked<br/>拆会话保监听:sock_reset(QUARANTINE) + 登记重监听"]
RLS["netListenerTry → netRelistenLocked<br/>单次尝试 + TRAP-NET-18 排渣隔离"]
end
EV["事件处理器<br/>handleConnect · handleDisconnect<br/>handleRecv · handleSend · handleTimeout"]
ST["netListenerSettle<br/>只置沉降/隔离事实(100ms / 400ms)"]
FE["fireEvent → 用户回调"]
PR["s_net_pending<br/>(每 fd 至多一个挂起 recv)"]
SR["s_net_send<br/>(每 fd 至多一个挂起 send)"]
CH[("ch395f.* 驱动<br/>SPI2 串行事务")]
end
PUB -->|入队| MQ
MQ --> DISP
DISP --> LL & LN & LS & LR & LA & LC & DC & RLS
NPM --> RetryTick & PendTick & PendShimTick & ReconcileTick & NP
RetryTick --> RLS
ReconcileTick -->|"① 已断"| EV
ReconcileTick -->|"② 补 accept"| EV
ReconcileTick -->|"⑤ 静默回收:RESYNC 只修本层 + 取消挂起(不发芯片命令,吸收原 net_resync_listen 语义)"| RSY["reconcile⑤ RESYNC 动作"]
ReconcileTick -->|"③④ 监听失位 / 孤儿监听即重建(原 pump)"| RLS
DC -->|"拆会话后登记重监听"| RLS
NP --> EV
EV --> FE
FE -.-> CB
EV -->|"断连/超时:只置事实"| ST
ST -.->|"事实 → 由 reconciler 形态③④ 统一重建"| ReconcileTick
LR -->|无数据+阻塞| PR
PR --> PendTick
PendTick -->|xTaskNotify| XN
LS -->|等发送缓冲超预算| SR
SR --> PendShimTick
PendShimTick -->|xTaskNotify| XN
XN -->|唤醒| PUB
NP --> CH
LL & LN & LS & LR & LA & LC & DC & RLS --> CH
SPI -->|直碰 SPI| CH
CH -->|INT# 低电平| NP
读图要点:
- 应用侧只有两种"等":① 入队后
xTaskNotifyWait等回执(同步语义);② 挂起 recv / send 的等待(同一通道,只是"完成"由netPendingTick/netSendPendingTick判定,不由 netTask 立即处理)。 net_process_messages()开头的 4 个 tick 是网络层自愈主干:没有任何应用消息时,重试 / 挂起 recv 完成 / 挂起 send 推进 / 状态对账 + 监听重建照常运转——"断连后即使没人发消息,监听器也能自己活过来"靠的就是它们。- 事件处理器只由
net_poll驱动,例外是net_reconcile_tick(分级去抖 + 1s 冷却)可再入handle_disconnect_event/handle_connect_event——对账是事件未收到的自愈通道(TRAP-NET-17)。注意职责边界(对账与重建合并后):对账与事件处理器只置事实(置 CLOSED / 登记沉降),重建监听的动作统一由net_reconcile_tick形态③④ 做(本层唯一重建入口)——"谁先看到 DISCONNECT 就谁重建"的竞争在结构上消失。 - 回调
event_cb在 netTask 上下文执行:回调内不得阻塞、不得发网络操作。 - 模块归属:
RetryTick(D10 重试)/RLS的net_listener_request/ST(settle)在net_listener.c(真机专属、模拟器不编译);挂起 recv 完成检查(net_pending_tick)、挂起 send 推进(net_send_pending_tick)与两张挂起表(PR/SR)在net_pending.c(真机专属);其余 tick(含合并后的ReconcileTickreconciler)与_locked在net_socket.c。
4. 函数清单:设计作用与调用关系
4.1 初始化与代次(直调;net_init 的调用者 = netTask)
| 函数 | 设计作用 | 主要调用 | 意图 / 注意 |
|---|---|---|---|
net_init(ip,mask,gw,mac) |
芯片一次性配置:探测/复位 → 参数 → MAC → 协议栈初始化 → 8 Socket 缓冲布局 → 使能 PING | net_pending_cancel_all → ch395f_check_exist / ch395f_reset / set_fun_para(0x08) / set_tcp_mss / set_retrans_count+period / set_keepalive_idle+intvl+cnt → set_mac_addr → parse_ip_string + set_ip_addr / set_gwip_addr / set_mask_addr(或 set_dhcp)→ ch395f_init → set_send_buf / set_recv_buf ×8 → ping_enable |
① 重建前先取消全部挂起 recv 并逐一通知(不通知 = 调用方永久挂死),且必须早于控制块清零与芯片复位;② 重传/KeepAlive/MAC/IP/GW/MASK 参数必须在 ch395f_init 之前下发(docs/芯片手册/CH395F.md §9.2.1:SET_MAC 为步骤①,INIT_CH395 加载 MAC——顺序反了本次上电不生效);③ fun_para=0x08:bit3 SOCKET_CLOSE(DISCONNECT/TIMEOUT 后不清缓冲,软件排空后手动关);bit0/bit1 保持 0——8 个 Socket 由索引 _SN 寻址 + 缓冲显式分配实现,与 FUN_PARA 无关(bit0 是保留位,必须为 0);④ 缓冲布局每 Socket 6 块(收 4 + 发 2)×8 = 48 块恰好用尽,Socket 0~7 必须显式重分配(TRAP-NET-01/-02);⑤ 捕获 xTaskGetCurrentTaskHandle 存入 s_net_task_handle(防自通知死锁的判据) |
net_get_generation() |
读网络代次号(纯 RAM) | 无 | FTP/UI/对时服务据此感知"网络已重建"→ 放弃旧监听并重建 |
net_generation_inc() |
代次号 +1 | 无 | 唯一合法递增点 = net_stack_bring_up() 末尾(PHY/链路就绪后)——太早递增会让服务在网络不可用时抢跑重建;模拟器端恒为 0(无重建概念,上层自愈逻辑空转) |
4.2 主循环:事件泵与队列分派(仅 netTask)
| 函数 | 设计作用 | 主要调用 | 意图 / 注意 |
|---|---|---|---|
net_poll() |
事件泵:GPIO 轮询 INT#(低=有待处理)→ GET_GLOB_INT_STATUS_ALL(读=清)→ 按位图逐 Socket 读 SINT → 分派 5 类事件;每轮末尾对在用 Socket 全量刷新驱动状态缓存 + 步进 ch395f_sock_reset_poll |
HAL_GPIO_ReadPin → ch395f_get_glob_int_status_all → ch395f_get_sock_int_status ×n → handle_connect/disconnect/recv/send/timeout_event → ch395f_get_sock_state ×n(刷新缓存)+ ch395f_sock_reset_poll ×n |
RECV 中断是电平触发:每次调用只处理一批(do-while(0)),处理完立即返回,否则主循环(echo/应用逻辑)饿死(TRAP-NET-08);SENBUF_FREE 只置 send_ready 不记事件;状态缓存唯一刷新点在此(reconciler 只读缓存,E2 实测单次回读 18.1µs);sock_reset_poll 步进使同步包装 600ms 放弃后的复位机不搁浅(IDLE 槽纯 RAM 判零 SPI) |
net_process_messages() |
队列入口 + 自愈 tick 宿主:每轮先跑 4 个非阻塞 tick,再排空队列逐条分派,结果 xTaskNotify 回传 |
net_listener_tick → net_pending_tick → net_send_pending_tick → net_reconcile_tick(对账+重建合一)→ osMessageQueueGet(0 超时) ×n → switch 分派 → xTaskNotify |
① tick 放在队列之前:断连后即使没有任何应用消息,监听器也能自主恢复、挂起 recv / send 也能完成;② RECV 若返回 NET_PENDING_REGISTERED(哨兵 0x70000000)则不发即时回执(do_notify=0),完成时才通知;③ 失败结果把 s_net_errno(具体负错误码)压入通知值——调用方恢复 errno 后对外统一 -1(BSD 语义);④ 通知值整个 32 位都是结果(不压位域标签,否则 32768 这类大字节数会溢出为负);⑤ 对账①置 CLOSED 后形态③④同轮即可重建(合并后,原"pump 置于对账之后"的顺序依赖在单 tick 内自然成立) |
4.3 经队列的公共 API 及其 netTask 侧实现
调用侧四件套:参数检查 → osMessageQueuePut(osWaitForever) → net_wait_result → errno 恢复/返回值。差异只在消息类型、timeout_ms 与结果映射。
| 公共 API | 消息 | netTask 侧实现 | 关键芯片命令 | 返回语义 | 设计意图 / 注意 |
|---|---|---|---|---|---|
net_listen |
NET_MSG_LISTEN |
net_listen_locked |
set_proto_type + set_sour_port + open_socket(open 重试 3×,失败走 net_sock_reset_sync 确定性排渣)+ tcp_listen + 清本 Socket 中断状态 |
0 / -1 | 初次监听允许有界重试(启动期无竞争);成功后置 auto_relisten=1——监听常驻的源头;启动前只清本 Socket SINT(唯一读者红线:全局寄存器唯一读者 = net_poll,全量读清会焊死其它 Socket 事件位,见 docs/网络/网络自愈架构设计.md §4.10);open 失败重试原"close+盲等 500ms"已换 sock_reset(GRACEFUL 100ms 底,E4 定案复位后 open 必成) |
net_accept |
NET_MSG_ACCEPT |
net_accept_locked |
无(先 net_poll 泵一次 + 状态判定) |
监听 fd 自身(单连接)/ -1(WOULDBLOCK) | 单连接:监听 Socket 在 CONNECT 后自身即数据通道,返回的就是它;CLOSED + auto_relisten 时触发非阻塞 net_listener_request;accept 路径不做自愈——单次状态读会误判"刚 listen 未生效"致重建风暴(2026-09-23 实测),统一交给 net_reconcile_tick 形态③④(合并后的单一重建入口) |
net_connect |
NET_MSG_CONNECT |
net_connect_locked |
set_proto_type + open_socket(open 重试 3× + net_sock_reset_sync 排渣)+ 清残留中断(只清本 Socket SINT,ch395f_sock_events)+ set_sour_port / set_des_ip(大端数组)/ set_des_port + tcp_connect |
0 = 请求已发出(异步)/ -1 | open 先于寄存器写入(open 会复位寄存器);open 后清残留 DISCONNECT,防新连接秒断(TRAP-NET-12);目的 IP 字节序写反 = CONNECT TIMEOUT(TRAP-NET-14);返回 0 ≠ 已建立——调用方以 net_sock_state 轮询等 ESTABLISHED |
net_send |
NET_MSG_SEND |
net_send_locked |
ch395f_write_send_buf(分块)+ 等 SENBUF_FREE(net_poll 轮询,有界自旋 ≤ NET_SEND_SPIN_MS);超预算登记挂起 send(net_send_pending_register) |
实际字节数 / -1 | 分块 ≤ NET_SEND_CHUNK_MAX = 1KB(硬件发送 FIFO 硬上限,TRAP-NET-16:写穿 FIFO = 数据错位);send_ready 门控用 net_poll 轮询而非 osDelay,自旋超 NET_SEND_SPIN_MS(2ms)即转挂起(netTask 最坏停滞由 1s 压到 2ms);短写必须告警——静默截断会让对端永久等待剩余字节(2026-09-21 实证:UI 推流应答截断 → 客户端失联) |
net_recv |
NET_MSG_RECV(timeout=0→无限等待,无 30s 默认预算) |
net_recv_locked |
net_poll + get_recv_len / read_recv_buf |
>0 / 0(EOF) / -1(errno = WOULDBLOCK / CONNRESET;阻塞形态无 TIMEDOUT 一档) | 快路径:数据已在 → 立即读出返回(允许部分读);无数据 + 阻塞(blocking 属性=1 且未 DONTWAIT)→ 登记挂起后立即返回(挂起登记,netTask 零占用),唤醒源=数据/EOF/close/网络重建;DONTWAIT / blocking=0 + CLOSED/session_dead → EOF + 触发重监听;⚠ IWDG 受监控任务禁用无限形态(net_socket.h 文档契约) |
net_recv_nb |
NET_MSG_RECV(flags=DONTWAIT) |
net_recv_locked |
同上 | >0 / 0 / -1(WOULDBLOCK 等) | 原 NET_MSG_DONTWAIT 路径转正的具名入口;不改变 blocking 属性;受 IWDG 监控 / 有周期职责任务的推荐默认(ui_net_sink / iec104_serv 已迁) |
net_recv_to |
NET_MSG_RECV(timeout>0) |
net_recv_locked |
同上 | >0 / 0 / -1 / -2(本片超时无数据) | 队头阻塞修复配套 + 显式预算入口(等价 Python settimeout,无隐藏默认值):调用方把长等待切成有界片(如剩余预算),总超时语义不变、单次占用有界;timeout_ms=0 直接拒(0 = 无限等待,与本接口"有界"存在理由相反);超时只凭 -2 判定、不得依赖 errno(s_net_errno 是跨任务共享全局量,随时被别的任务覆盖) |
net_close |
NET_MSG_CLOSE |
net_close_locked |
ch395f_cmd_done(CLOSE) 快路径(芯片已 CLOSED → 只清本 Socket 残留事件)/ net_sock_reset_sync(GRACEFUL) 确定性排渣 |
0 / -1 | 先结束本 fd 挂起 recv(排空/取消 + 必通知,关不得让等待者悬挂);排空只碰本 Socket SINT,不再读清全局中断寄存器(唯一读者红线:全局唯一读者 = net_poll,见 docs/网络/网络自愈架构设计.md §4.10);原"close + 500ms 盲等排空循环"已换 sock_reset(内置 LISTEN 态二段 CLOSE 补发,E2 补充发现);控制块整体清零(last_peer_port 幽灵基准跨 fd 保留,属「relisten 沉降段原语化受阻」) |
net_drop_conn |
NET_MSG_DROP_CONN |
net_drop_conn_locked |
net_pending_settle_close + net_sock_reset_sync(QUARANTINE) +(auto_relisten)net_listener_request |
0(含幂等)/ -1 | 拆会话保监听(队头阻塞收口):应用放弃会话(对时 30s 预算耗尽 / 协议超时)而单连接监听被挂起对端永久占住时的应用侧出口;EOF 路径不得调(auto_relisten 已接管);非 ESTABLISHED/CONNECTING 直接返 0 不动监听;本层收口 CLOSED 不清 fd(保留 local_port/auto_relisten),监听型 fd 登记重监听、失败交 D10 + reconciler 形态④ |
net_relisten |
NET_MSG_RELISTEN |
已删除(消息类型同删) | — | — | 原"无条件芯片侧重建"与 pump/D10 完全重叠,且对健康监听器是 footgun(open→USE→close 自毁 ~1s);生产零调用。历史实现见 net_socket.c git 历史 |
net_resync_listen |
NET_MSG_RESYNC_LISTEN |
已删除(语义并入对账⑤) | — | — | D14 静默回收(芯片自行回监听、不发 DISCONNECT)现由 net_reconcile_tick 判据⑤自动认领:本层 ESTABLISHED 且芯片 0x05/0x01 → 只修本层(state→LISTENING + 转登 last_peer_port + 清 lost_conn_seen + 取消挂起 recv),不发芯片命令(纪律原样保留,run8 实证 170s 拒绝);去抖/冷却沿用对账框架 |
4.4 直调 API(不经队列)
| 函数 | 设计作用 | 主要调用 | 意图 / 注意 |
|---|---|---|---|
net_socket(domain,type) |
分配空闲控制块(alloc_socket 线性扫描 in_use=0)→ CREATED |
alloc_socket + net_listener_clear + net_pending_clear |
新 fd 不得继承上一次的待重试/挂起状态(复用描述符的经典坑);domain 仅 NET_AF_INET |
net_bind(sockfd,addr,addrlen) |
TCP:仅记录本地端口(port=0 → 动态端口 49152 起递增)→ BOUND,无芯片命令;UDP:完整开 Socket → UDP_OPEN | UDP 路径:set_proto_type(UDP) + set_sour_port + set_des_ip(0xFFFFFFFF) + set_des_port(0) + open_socket(Socket 4~7 兜底重设缓冲) |
TCP 的 bind 只是"记账",真正的芯片动作在 listen/connect;UDP 直碰 SPI(§2 三例外之一);动态端口回绕 65535→49152 |
net_sendto / net_recvfrom |
UDP 收发(直碰 SPI,不经队列) | set_des_ip / set_des_port + write_send_buf(分块 1KB + net_poll 门控);net_poll + get_recv_len + read_recv_buf |
同 §2 上下文约束;recv 时 UDP Server 模式前 8 字节是固定头(源 IP + 端口 + 长度),先剥头填 src_addr 再读数据;recvfrom 阻塞语义与 net_recv 同构(blocking 属性 / DONTWAIT,无默认预算;轮询自旋实现,生产零调用) |
net_setblocking(sockfd,on) |
设置阻塞模式属性(直写控制块,不经队列) | 写 s_net_socks[fd].blocking |
对齐 BSD O_NONBLOCK / Python setblocking:"模式归使用者、超时归调用方";单字节原子写(先例 net_set_event_cb),最坏一次在途 recv 用旧模式;net_socket() 默认置 1(close 的 memset 清零后新 fd 必须显式重置) |
net_wait_result(kind,sockfd) |
调用侧两段式等待:零等待拾取 → 阻塞等回执 | xTaskNotifyWait ×2 |
① 零等待段兜住"入队→等待入口"间隙落地的通知(2026-09-22 台架实证丢通知);② 清除掩码必须全 1(传 0 = 不清 → 上次结果残留被重读,run3~11 挂死/错位的真根因);③ 必须判 pdTRUE 而非 v!=0(合法结果 0 = EOF 会被误判"无通知"→ 永久挂死);④ 32 位整体作结果(32768 大字节数不截断);⑤ 本路径禁止加逐调用日志(曾占日志 98.6%、冲掉 [LAT] 证据行,run14 实证) |
net_sock_state(sockfd) |
跨任务只读状态快照(单字节读,无锁) | 无 | 描述符无效/未占用 → -1;值可能滞后 netTask 最新状态——用于轮询(客户端等 ESTABLISHED),不替代 accept/connect |
net_get_sock(sockfd) |
返回控制块指针 | 无 | netTask 上下文内用;跨任务只可读单字节 state(即 net_sock_state),不得读结构体其他成员 |
net_get_errno() |
读最后错误码 | 无 | s_net_errno 是跨任务共享全局量:只作参考,判定以返回值为准(net_recv_to 的 -2 同理) |
net_set_event_cb(sockfd,cb,arg) |
注册/注销事件回调 | 无 | 回调由 fire_event 在 netTask 上下文调用 → 回调内不得阻塞/发网络操作;NULL 注销 |
net_getsockname(sockfd,addr,addrlen) |
本地地址(IP + 端口) | 读 s_local_ip(net_init 缓存)+ local_port |
IP 是 net_init 时的值(DHCP 场景不更新——本构建固定 IP);port=0 时返回已分配动态端口 |
字节序族 net_htons/ntohs/htonl/ntohl/inet_addr/inet_ntoa |
纯计算转换 | 无 | 无状态、任意上下文可调 |
4.5 挂起 recv 子系统(阻塞等待不独占 netTask)
模型:阻塞等待 = 挂起请求登记(每 fd 至多一个,禁止覆盖)+ 主循环节拍完成检查 + 通知唤醒(LwIP netconn 模型)。消除队头阻塞(旧实现:netTask 自旋等数据,一次 30s 空闲 recv 冻住全网 17.7s)。
| 函数 | 设计作用 | 主要调用 | 意图 / 注意 |
|---|---|---|---|
net_pending_register |
登记挂起(net_recv_locked 的"无数据 + 阻塞"路径) |
写 s_net_pending[fd] |
每 fd 至多一个:覆盖会丢失前调用方通知 = 永久挂死;执行任务自身(netTask)发起的等待直接拒绝(自等自通知 = 死锁,netTask 内须用 DONTWAIT) |
net_pending_tick |
完成检查(net_process_messages 每轮调用,非阻塞) |
① get_recv_len + read_recv_buf ② 到点(仅显式预算) ③ get_recv_len 排空 + net_listener_request ④ net_pending_complete |
按 ①数据 ②到点(仅 net_recv_to 显式预算;无限登记无 deadline)③断连 顺序检查(逐项对齐旧自旋语义);CONNECTING/LISTENING 等状态保持等待(CONNECT→ESTABLISHED 是合法迁移,不做状态推断取消,防误杀正在建链的等待);挂起超时留痕探针(编译期选项 NET_PENDING_TRIPWIRE,默认关):挂起超阈值 = 完成逻辑缺位 → 只 [ERR] 留痕一次,绝不强制结束、绝不伪造 TIMEDOUT——③ 与显式 cancel 是无限 recv 的全部唤醒责任方 |
net_pending_complete |
所有完成/取消路径的公共出口 | xTaskNotify(caller, result) + 清 active |
通知值 = 结果原值(>0 字节 / 0 EOF / 负错误码,与消息分派同一回传通道);失败结果同步 s_net_errno(netTask 上下文一致性);无在位挂起时静默返回(防重复通知) |
net_pending_settle_close |
close 前结束本 fd 挂起 | get_recv_len + read_recv_buf + net_pending_complete |
关闭不得丢已到达数据(芯片缓冲 close 前可读):有数据 → 按字节数通知;无数据 → 取消(CONNRESET);先于 ch395f_close_socket 执行 |
net_pending_cancel / cancel_all |
显式取消(必通知) | net_pending_complete |
取消路径:陈旧 resync(单 fd)/ 网络重建(全部,net_init 开头,早于控制块清零)——"取消不通知 = 调用方永久挂死" |
net_pending_clear |
清除不通知 | 清 active |
仅用于"新 fd 不得继承"(net_socket)与 close 后幂等清理(net_close_locked)——这两处前驱通知已由 settle 路径发出 |
4.6 D10 非阻塞重监听重试子系统(net_listener.c)
模型:重开失败 = 登记待重试(30ms 节奏 / 2s 时限),net_listener_tick 到点重试——任何时刻不阻塞任务。旧实现 osDelay(500) 忙等:netTask 冻结 500ms(全网停摆)、实测恢复 588ms、期间重连被拒(M3 台架)。
模块归属:本子系统的函数在
net_listener.c(真机专属,模拟器不编译);沉降等级只经net_listener_settle登记,外部不得直接写s_relisten_quarantine[]。
| 函数 | 设计作用 | 主要调用 | 意图 / 注意 |
|---|---|---|---|
net_listener_settle |
登记"本 fd 会话已结束,重建前需沉降/隔离"(只置事实) | 写 s_relisten_quarantine[fd](等级:GRACEFUL 100ms / QUARANTINE 400ms) |
由 handle_disconnect_event(GRACEFUL)/ handle_timeout_event(QUARANTINE)调用。只置事实,动作只在 net_listener_tick 一处发生;越界 fd 静默忽略 |
net_listener_request |
登记"本 fd 期望回到 LISTEN"(只置意图,不碰芯片) | 写 s_relisten_req[fd](来源 +1)+ next=0(下一轮 tick 即尝试) |
取代原 net_listener_try(当场动作 → 4 个调用点各自补 state == CLOSED 门,U12 漏一处即事故;E17 收口后门上收到 tick)。已在请求中 → 不重置重试时刻:否则 10ms 一次的 accept 轮询会把重试时刻不断往后推 → 永不重试(饥饿);越界 fd 静默忽略 |
net_relisten_locked |
真正的 open + listen(单次尝试) | (沉降期)net_drain_chip_rx + close_socket + get_sock_int_status / get_glob_int_status_all + get_socket_status → set_proto_type + set_sour_port + open_socket + tcp_listen + 清中断 |
① 严禁 osDelay 忙等:失败只清理并返回,沉降交给重试轮询;② TRAP-NET-18 沉降门:带沉降标记的 socket(异常释放 TIMEOUT → QUARANTINE 400ms;正常释放 DISCONNECT → GRACEFUL 100ms)→ 排空 RX 残渣 + 主动 close + 排空中断 + 等芯片回 CLOSED/TCP_CLOSED;上限(800ms)只留痕、不强行放行——芯片未回 CLOSED 时 open 注定吃 USE 错,兜底 = D10 预算到期 + 形态④ 重新登记;③ 成功只置 state=LISTENING,不 memset(fd 与 auto_relisten 保留——BSD 风格监听常驻) |
net_listener_tick |
监听重建的唯一执行者(netTask 每轮,非阻塞) | net_relisten_locked → net_relisten_done / 顺延 30ms / 超预算清登记 + [ERR] |
门集集中这一处:控制块有效 + in_use + 状态 ∈ {LISTENING, CLOSED} + 监听型(auto_relisten / STREAM / local_port≠0)+ 芯片已在 LISTEN 即视为请求已达成、静默了结、绝不 open(U12 结构性防护,读驱动状态缓存零 SPI)+ 同 fd 有在途挂起 recv / send 时跳过(重开 Socket 会摧毁在途连接 / 截断在途传输,挂起保险)+ 30ms 节奏 / 2s 预算(D10;超预算清登记并留痕,之后由 reconciler 形态③④ 重新登记,监听不永久失效) |
net_relisten_done |
标记完成 | net_listener_clear +(探针构建)恢复计时样本 |
恢复时延 = 首次尝试 → 完成(延迟探针 relisten 段) |
net_listener_clear |
取消该 fd 的重建请求 / 沉降 | 清 req/next/dead + settle_dead/settle_start + quarantine |
触发点:net_socket(新 fd 不继承)/ net_close_locked(关闭即取消)/ tick 内"登记已陈旧或已达成";沉降/隔离状态必须一并清,否则残留标记会污染下一次复监听 |
net_drain_chip_rx |
排空芯片 RX 残留(256B 分块,≤16 次 = 4KB 上限) | get_recv_len + read_recv_buf |
TRAP-NET-18:中止/异常断开时芯片 RX 缓冲残留上一条连接的数据(实测 ~2048B),不排空会被下一条连接当自己的数据读走(静默截断);分块读避免新增静态缓冲(内存预算紧张) |
4.7 监听重建(原监听器 pump;已并入 net_reconcile_tick 形态③④/U11)
现状:
net_listener_pump()已删除——其全部三种情形作为形态③④ + U11 并入net_reconcile_tick(§4.8,单一 100ms tick,判据读驱动状态缓存)。本节保留"单一重建所有者"的设计口径与情形表;实现位置与参数以 §4.8 为准。
设计意图(设计文档 §4 不变量 II):其它所有位置只置事实(会话结束 / 状态迁移 / 事件派发),不做重建动作——于是"谁先看到 DISCONNECT 就谁重建"这类多所有者竞争在结构上消失。net_reconcile_tick 形态③④是本层唯一的周期性重建决策者。
覆盖三种情形(认领门槛:在用 + auto_relisten + TCP + 已绑端口——原 pump 口径,少了它"本层 CLOSED + 已绑端口"的出向连接会被误判为监听器并被 relisten):
| # | 本层状态 / 芯片状态 | 动作 | 来源 |
|---|---|---|---|
| ③ | 本层 LISTENING 而芯片监听已失位 | 重建(net_listener_request) |
原对账③ / 原 pump① |
| ④ | 本层 CLOSED(会话已结束)而芯片不在 LISTEN | 重建(net_listener_request) |
原对账④ / 原 pump② |
| U11 | 本层 CLOSED 而芯片正在 LISTEN | 只做状态重同步(CLOSED→LISTENING),不发任何芯片命令 | U11 缺口 |
情形 U11 的理由:芯片监听器是健康的,重下发 open+listen 会因 USE 错误走进关闭分支、把正听着的 Socket 关掉(U12 同源事故);只把本层状态收回 LISTENING 即可(与对账⑤ RESYNC 同一纪律——原 net_resync_listen 已退役)。
去抖 + 冷却(③④ 沿用原实测结论,取值不变):同一失配需连续 2 轮(NET_RECON_STABLE_RELISTEN,≈200ms)才动手;同一 Socket 动手后冷却 1s(防抖成风暴,2026-09-23 实证);U11 被动重同步不去抖、不占冷却(零风险)。tick 周期 100ms(原 pump 周期;合并后对账也从 200ms 提到 100ms):比 200ms 快、又不至于像 50ms 那样频繁 close/open 与消费者收发撞车(2026-10-12 台架实测 50ms 会把 uiTask 最坏单轮从 172ms 推到 1034ms)。
session_dead不在此清零(与net_relisten_locked同一契约:重建监听 ≠ 应用已确认会话结束;清零只发生在handle_connect_event)。
4.8 TRAP-NET-17 周期对账 + 监听重建(合一 net_reconcile_tick,事件未收到自愈,net_events.c)
net_reconcile_tick()(net_process_messages 每轮调用;周期 100ms,NET_RECONCILE_PERIOD_MS;含原 pump 形态③④/U11):
问题:芯片在快速复连场景会收不到 CONNECT/DISCONNECT 事件(读清消费竞态——E14 复核:非芯片侧丢失;或芯片静默回收 Socket 不发中断);事件未收到时纯事件驱动无法自愈——典型:对端连接一直占着单连接监听的"后备"(芯片 TCP 仍 ESTABLISHED),后续 SYN 全被拒,而应用因未收到 DISCONNECT 仍以为有客户端(永不调 accept)。
机制:对每个在用 Socket 读驱动状态缓存(ch395f_sock_state_cached,唯一刷新点 = net_poll 每轮全量刷新,E2 实测单次回读 18.1µs;本 tick 不再自发状态 SPI,唯一按需 SPI = 形态② 命中时回读对端端口做幽灵甄别),判定五类失同步 + 一类被动重同步并修正:
| # | 失同步 | 动作 | 复用的实现 |
|---|---|---|---|
| ① | 本层 ESTABLISHED 而芯片已无连接(sock/tcp = CLOSED) | 按断开处理 → CLOSED + 回调(应用读到 EOF 后由形态③④重建监听) | handle_disconnect_event(内置陈旧事件甄别) |
| ② | 本层 LISTENING 而芯片已完成握手(tcp = ESTABLISHED) | 补做 standalone accept 簿记 | handle_connect_event |
| ③ | 本层 LISTENING(监听型 fd)而芯片监听失位 | 重建 | net_listener_request(原 pump①) |
| ④ | 本层 CLOSED(监听型 fd)而芯片不在 LISTEN | 重建 | net_listener_request(原 pump②) |
| ⑤ | 本层 ESTABLISHED 而芯片静默回收回 LISTEN(0x05/0x01,无 DISCONNECT 中断) | RESYNC 只修本层、不发芯片命令 | 原 net_resync_listen 语义(已并入) |
| U11 | 本层 CLOSED(监听型 fd)而芯片正在 LISTEN | 只重同步 CLOSED→LISTENING,不发芯片命令 | 原 pump③ |
⚠ 单一所有者不变量:重建决策只此一处(原
net_listener_pump已删除,③④/U11 就在本 tick); 认领门槛见 §4.7(在用 +auto_relisten+ TCP + 已绑端口)——少了它"本层 CLOSED + 已绑端口"的 出向连接会被误判为监听器并 relisten。
去抖 + 冷却(缺一不可):单次状态读可能落在"刚 open/listen 尚未生效"的瞬态——2026-09-23 实测:无去抖的 accept 路径自愈把监听反复重建直至 relisten give up(三服务口全死、89 行 ERROR)。故分级去抖(单位=轮,100ms/轮):②=1 轮(NET_RECON_STABLE_ACCEPT,幽灵甄别兜底假阳性);③④⑤=2 轮(NET_RECON_STABLE_RELISTEN);①=4 轮(NET_RECON_STABLE_DEATH,勿改小——风暴态三机制里 M3 可疑门的让位再入 400ms 租约,见 docs/网络/网络自愈架构设计.md §4.1.6,E9a 实证);同一 Socket 动手后冷却 1s(NET_RECONCILE_COOLDOWN_MS;U11 被动重同步不去抖不占冷却)。开销:每 100ms 每在用 Socket 的状态回读已收拢在 net_poll(≈100µs/轮),本 tick 本身零额外 SPI。
4.9 事件处理器(netTask 上下文;由 net_poll 驱动,对账 tick 可再入)
| 函数 | 设计作用 | 主要调用 | 意图 / 注意 |
|---|---|---|---|
handle_connect_event |
LISTENING → standalone accept(回读对端地址 → ESTABLISHED + send_ready=1 + 清残留 TIMEOUT + 回调);CONNECTING → 客户端连接成功(回读对端 + 使能 KeepAlive + 回调) |
ch395f_get_remot_ipp + get_sock_int_status +(client)set_keepalive_enable + fire_event |
GET_REMOT_IPP 为小端输出(与 set_des_ip 的大端相反)——解析别抄反;session_dead 的唯一清零点 |
handle_disconnect_event |
状态机迁移 + 陈旧事件甄别 + 登记沉降事实(不再自己重建) | ch395f_get_socket_status(事件路径主动 SPI,不用缓存——缓存刷新在 net_poll 末尾,事件路径上必然滞后)→(LISTENING:只回读留痕,监听失位交 reconciler 形态③ 重建;仍在监听 → 判陈旧丢弃)/(ESTABLISHED:芯片仍 ESTABLISHED → 判陈旧丢弃;M3 可疑门 → 置可疑电平让位对账①,见 docs/网络/网络自愈架构设计.md §4.1.6) |
① 陈旧 DISCONNECT 甄别(D20):同一 fd"关闭→重监听→再连接"循环复用,上一条连接的 FIN/ACK 可能在 fd 已复用时才上报 → 不甄别则新连接秒断(实测:FTP 同会话第二个数据连接必 425);② 正常释放(FIN)→ GRACEFUL 沉降 100ms(只置事实,重建由 net_reconcile_tick 形态③④ 做);③ 置 CLOSED 后不立即关芯片 Socket——留给 net_recv_locked 排空剩余数据(FIN 可能先于数据到达) |
handle_recv_event |
置 readable + 回调;不读 recv_len |
fire_event |
不读长度是为了避免与 net_recv_locked 的 ch395f_get_recv_len 竞争(旧"分批读归零"机理,未证实);readable 供 net_select 无 SPI 访问判读 |
handle_send_event |
发送缓冲已空:触发回调(send_ready 由 SENBUF_FREE 前置位) |
fire_event |
— |
handle_timeout_event |
CONNECTING → CREATED(连接超时);ESTABLISHED → 登记隔离 + CLOSED(TRAP-NET-18);LISTENING → 仅记日志 | net_listener_settle(fd, NET_LISTENER_SETTLE_QUARANTINE) + fire_event |
TIMEOUT = 异常释放(如客户端中止上传):对端在途数据随后落进 RX 缓冲 → 必须隔离(400ms)后才许复监听;正常 FIN 释放走 disconnect 路径(GRACEFUL 100ms),不走隔离 |
fire_event |
调用已注册回调 | p_sock->event_cb |
回调上下文 = netTask:回调内不得阻塞、不得发网络操作(会自等死锁) |
4.10 工具函数
| 函数 | 设计作用 |
|---|---|
alloc_socket |
线性扫描 s_net_socks,返回首个 in_use=0 的索引(-1 = 8 个全占) |
parse_ip_string |
点分十进制 → 4 字节数组(网络序,供 ch395f_set_*_addr) |
s_mac_is_zero |
MAC 全零判定(NULL/全零 = 用芯片内置 MAC) |
net_bench_cmd_log |
台架命令序列日志(NET_BENCH_CMD_LOG 编译开关,默认关;每个真实 SPI 命令点一行,排障用) |
5. 典型端到端路径
5.1 发送(队列路径)
sequenceDiagram
participant A as appTask(如 ftpTask)
participant Q as netMsgQueue
participant N as netTask
participant C as CH395F
A->>Q: net_send:入队(fd/buf 指针/len,按值拷贝)
Note over A: net_wait_result:零等待拾取 → 无则阻塞
Q->>N: NET_MSG_SEND
N->>C: net_send_locked:write_send_buf(≤1KB/块)
alt 健康路径(块间等待 < NET_SEND_SPIN_MS=2ms)
loop 每块
N->>C: 等 SENBUF_FREE(net_poll 有界自旋,不睡)
end
else 超预算(对端零窗口 / 不读)
N->>N: net_send_pending_register(每 fd 至多 1;do_notify=0)
Note over A,N: netTask 立即解放;A 在自己上下文睡通知
loop 每个主循环节拍(1ms)
N->>C: net_send_pending_tick:①断连 ②推进 ③写完 ④无进展超时
end
end
C-->>N: SENBUF_FREE / SEND_OK(net_poll 置 send_ready)
N->>A: xTaskNotify(实际字节数 / 负错误码)
A->>A: 返回字节数 / -1(errno 已恢复;短写按 ret < len 处理)
5.2 阻塞接收无数据(挂起 recv 路径)
sequenceDiagram
participant A as appTask
participant Q as netMsgQueue
participant N as netTask
participant C as CH395F
A->>Q: net_recv_to:入队(fd/buf/len/单片预算)
Q->>N: NET_MSG_RECV
N->>C: net_poll + get_recv_len
C-->>N: 无数据
N->>N: net_pending_register(每 fd 至多 1;do_notify=0)
Note over A,N: netTask 立即解放;A 在自己上下文睡任务通知
loop 每个主循环节拍(1ms)
N->>C: net_pending_tick:①数据 ②到点 ③断连
end
C-->>N: 数据到达 / 到点 / 断连
N->>C: read_recv_buf(断连时先排空残留)
N->>A: xTaskNotify(字节数 / 0 / 负错误码)
A->>A: >0 数据 · 0 EOF(自动重监听)· -1 真错误 · -2 本片超时
Note over N: close / 网络重建 / 陈旧 resync → 显式 settle/cancel(必通知)
5.3 断连 → 自动重监听(含 TRAP-NET-18 沉降)
sequenceDiagram
participant N as netTask
participant C as CH395F
participant A as appTask
Note over C: 客户端 FIN(正常)或 RST/无响应(异常)
N->>C: net_poll 读 SINT
alt DISCONNECT(正常)
N->>N: handle_disconnect_event:回读甄别 → CLOSED + 回调<br/>+ net_listener_settle(GRACEFUL 100ms)
else TIMEOUT(异常,如中止上传)
N->>N: handle_timeout_event:net_listener_settle(QUARANTINE 400ms) + CLOSED
end
Note over N: 只置事实 —— 重建统一由 net_reconcile_tick 形态③④(100ms)决定
N->>C: net_listener_request(登记)→ net_listener_tick → net_relisten_locked
opt 沉降期内(TRAP-NET-18)
N->>C: 排空 RX 残渣(256B 分块)+ close + 排空中断
C-->>N: 未到 CLOSED/TCP_CLOSED → 返回(30ms 后重试轮询复检)
end
N->>C: set_proto_type + set_sour_port + open_socket + tcp_listen
alt open 失败(TRAP-NET-09 沉降)
N->>N: 登记重试(30ms 节奏 / 2s 时限)→ net_listener_tick 到点重试
else 成功
N->>N: state = LISTENING(auto_relisten 保留)
N-->>A: xTaskNotify(accept 返回 / recv 返回 0)
end
Note over N: 2s 超限时 → 清状态 + "relisten give up"(错误日志,应用可感知)
5.4 事件未收到自愈(TRAP-NET-17 对账 + 监听重建合一 reconciler)
sequenceDiagram
participant N as netTask
participant C as CH395F
participant A as appTask
Note over C: 快速复连:芯片完成握手但 CONNECT 事件未收到
Note over N: 本层滞留 LISTENING → 应用以为无客户端(永不调 accept)
loop 每 100ms(net_poll 每轮刷新驱动状态缓存)
N->>N: net_reconcile_tick:读 ch395f_sock_state_cached(每个在用 socket)
C-->>N: 失配(本层 LISTENING,芯片 ESTABLISHED)
end
Note over N: 形态② 去抖=1 轮(幽灵甄别挡假阳性;仅命中时回读对端端口,本 tick 唯一按需 SPI)
N->>N: handle_connect_event:补 accept 簿记 → ESTABLISHED
N-->>A: 下一次 net_accept 返回客户端(连接不再"吞掉")
Note over N: 其余形态:① 本层 ESTABLISHED 而芯片已断 → handle_disconnect_event(去抖 4 轮)<br/>③④ 本层 LISTENING 丢监听 / CLOSED 孤儿 → 本 tick 形态③④ 重建(唯一重建入口,去抖 2 轮)<br/>⑤ 静默回收 → RESYNC 只修本层;U11 芯片在听 → 只重同步
5.5 网络重建(IP 变更免重启)
sequenceDiagram
participant N as netTask
participant C as CH395F
participant A as 各应用任务
N->>A: net_init 开头:net_pending_cancel_all(CONNRESET)——逐一取消并通知
N->>N: 控制块全部清零
N->>C: check_exist → reset → fun_para(0x08)/MSS/重传/KeepAlive
N->>C: IP·GW·掩码(或 DHCP)→ ch395f_init → MAC → 8 Socket 缓冲布局
Note over N,A: net_stack_bring_up:PHY/链路就绪 → net_generation_inc<br/>FTP/UI/对时据**代次变化**放弃旧监听并重建(见 docs/子系统设计/网络配置说明.md §4)
6. 设计铁律(为什么是这样)
| # | 铁律 | 实证来源 |
|---|---|---|
| 1 | 一切芯片访问收敛 netTask(唯一执行者);跨任务调用走队列 | TRAP-NET-14:跨任务 SPI 竞争破坏芯片寄存器 → 连接必超时 |
| 2 | 结果只经任务通知值回传;消息按值入队,调用方看不到 netTask 对消息的写入;勿给消息加"回传用"字段 | TRAP-NET-10:6 个 API 永远返回 -1;"加 msg.err 字段"的修法曾致 ui_net_sink 每次空轮询误判真错误 → 连上即断 |
| 3 | netTask 不为任何人自旋等待:阻塞 recv = 挂起登记;重开失败 = 重试登记(D10);初次 listen/connect 的 open 重试环(net_sock_reset_sync 有界等待,GRACEFUL 100ms 底 / 600ms 封顶)是唯一豁免(启动期无竞争,原 3×500ms 盲等已换确定性排渣) |
队头阻塞整改:一次空闲 30s recv 冻住全网 17.7s;旧重监听 osDelay(500) → 恢复 588ms、期间重连被拒 |
| 4 | netTask 不向自身发消息 / 不自我挂起(挂起登记对执行任务自身发起直接拒绝) | 自等自通知 = 死锁 |
| 5 | 基于状态读的重建动作必须去抖 + 冷却:分级门槛 ②=1 轮 / ③④⑤=2 轮 / ①=4 轮(100ms/轮)+ 冷却 1s | 2026-09-23:无去抖 → 监听反复重建直至 relisten give up,三服务口全死 |
| 6 | 异常释放(TIMEOUT)后复监听前必须隔离:排空 RX 残渣 + 等 CLOSED/TCP_CLOSED;隔离上限 < 重试放弃时限 | TRAP-NET-18:~2KB 残渣被下条连接当自己的数据 → 首笔上传截断 2048B 并误报 226 成功;上限=2s 时隔离耗尽重试预算 → 监听被弃 |
| 7 | RECV 电平触发中断每轮只处理一批 | TRAP-NET-08:连续循环 → 主循环饿死 |
| 8 | close 前先结束挂起 recv + 排空残留 DISCONNECT | TRAP-NET-12:锁存的陈旧 DISCONNECT 秒断下一连接 |
| 9 | 两段式等待防丢失通知:零等待拾取 + 阻塞;清除掩码全 1;以 pdTRUE 判定 | run3~11:丢失通知 → 挂死 / 结果错位;0 结果误判"无通知" → 永久挂死 |
| 10 | 网络重建先取消并通知全部挂起,再复位芯片 | 取消不通知 = 调用方永久挂死 |
| 11 | 监听重建只有一个所有者(net_reconcile_tick 形态③④;原 net_listener_pump 已删);其它位置只置事实(置 CLOSED / 登记沉降),不做重建动作 |
B10:多所有者导致"谁先看到 DISCONNECT 就谁重建"的竞争与重建风暴 |
7. ch395f 驱动接口使用图(net_socket.c / net_listener.c → 芯片命令)
| 芯片命令 | 使用处 | 说明 |
|---|---|---|
ch395f_check_exist / ch395f_reset |
net_init |
上电探测与复位 |
ch395f_set_fun_para(0x08) |
net_init |
bit3 手动关缓冲(SOCKET_CLOSE);bit0/1=0,Socket 个数与 FUN_PARA 无关 |
ch395f_set_tcp_mss / set_retrans_count / set_retrans_period / set_keepalive_idle+intvl+cnt |
net_init(均须在 ch395f_init 之前) |
值见 net_config.h |
ch395f_set_mac_addr |
net_init(须在 ch395f_init 之前) |
写芯片 EEPROM;INIT_CH395 加载 MAC(docs/芯片手册/CH395F.md §9.2.1 步骤①) |
ch395f_set_ip_addr / set_gwip_addr / set_mask_addr / set_dhcp |
net_init(均须在 ch395f_init 之前) |
固定 IP 或 DHCP |
ch395f_init / ch395f_ping_enable |
net_init |
协议栈初始化(INIT_CH395)/ PING(须在 init 之后) |
ch395f_set_send_buf / ch395f_set_recv_buf |
net_init(8 Socket 布局);net_bind(UDP)(Socket 4~7 兜底) |
每 Socket 收 4 块 + 发 2 块 |
ch395f_get_glob_int_status_all |
仅 net_poll(唯一读者红线:全局寄存器唯一读者,见 docs/网络/网络自愈架构设计.md §4.10;net_listen_locked / net_connect_locked / net_close_locked / net_relisten_locked 的原全局读清已全部删除) |
读 = 清(中断清除机制,无按位手段)——其他位置读清会焊死 net_poll 的按 Socket 门控 |
ch395f_get_sock_int_status |
net_poll(逐 Socket SINT);各 open 路径后清残留;事件处理器清 TIMEOUT |
读 = 清 |
ch395f_sock_events |
net_connect_locked(open 后清残留,唯一读者红线的护栏原语,见 docs/网络/网络自愈架构设计.md §4.10);net_close_locked(芯片已 CLOSED 快路径只清本 Socket 残留事件) |
只碰本 Socket SINT 的护栏原语 |
ch395f_open_socket / ch395f_close_socket |
net_bind(UDP)、net_listen_locked、net_connect_locked、net_relisten_locked(含沉降期主动 close);net_close_locked 的 CLOSE 已改经 ch395f_cmd_done / ch395f_sock_reset_* |
TRAP-NET-09:close 后需沉降才能重开 |
ch395f_tcp_listen / ch395f_tcp_connect |
net_listen_locked / net_relisten_locked;net_connect_locked |
— |
ch395f_set_proto_type / set_sour_port / set_des_ip / set_des_port |
net_bind(UDP)、net_listen_locked、net_connect_locked、net_relisten_locked、net_sendto |
open 之后写端口/IP(open 会复位寄存器);set_des_ip 期望大端数组 |
ch395f_write_send_buf |
net_send_locked / net_send_pending_tick(挂起 send 推进)/ net_sendto |
单次 ≤1KB(硬件 FIFO 上限,TRAP-NET-16) |
ch395f_get_recv_len / ch395f_read_recv_buf |
net_recv_locked、net_pending_tick、net_pending_settle_close、net_recvfrom、net_drain_chip_rx、handle_disconnect_event(M3 可疑门 RX 非空判据) |
handle_recv_event 故意不读(防竞争,旧机理未证实) |
ch395f_get_socket_status |
handle_disconnect_event(两处甄别——事件路径主动 SPI,缓存必然滞后于刚到达的中断)、net_relisten_locked(沉降门)、net_connect_locked(诊断回读)。⚠ 周期对账不再直接调——改读缓存(下行) |
输出 2 字节([0]=Socket 状态 [1]=TCP 状态),缓冲必须给 2 字节 |
ch395f_get_sock_state / ch395f_sock_state_cached |
刷新仅 net_poll 每轮对在用 Socket 调 get_sock_state(唯一刷新点);sock_state_cached 读缓存 = net_reconcile_tick(形态①②③④⑤/U11 判据) |
芯片状态唯一事实源;E2 实测单次回读 18.1µs,缓存新鲜度 ≤1ms |
ch395f_sock_reset_begin / ch395f_sock_reset_poll |
net_sock_reset_sync(net_listen_locked / net_connect_locked open 重试、net_close_locked、net_drop_conn_locked);net_poll 每轮对在用 Socket 步进(IDLE 槽纯 RAM 判零 SPI) |
驱动复位状态机:读空 RX → CLOSE → 排空事件(quiescent) → 等 byte0=00H;内置 LISTEN 态二段 CLOSE 补发(E2 补充发现);GRACEFUL 100ms 底 / QUARANTINE 400ms 底 |
ch395f_cmd_done |
net_close_locked(CLOSED 快路径判据) |
命令完成判定(OPEN/LISTEN/CLOSE 三形态,读缓存不额外发 SPI) |
ch395f_link_up_cached / ch395f_link_change_count |
net_reconcile_tick(链路态订阅留痕,见 docs/网络/网络自愈架构设计.md §4.4 链路态喂料缓存) |
驱动 link 变化计数,net_poll 喂 |
ch395f_get_remot_ipp |
handle_connect_event、net_reconcile_tick(幽灵甄别,形态② 命中时唯一按需 SPI)、net_connect_locked(诊断) |
小端输出(与 set_des_ip 相反) |
ch395f_set_keepalive_enable |
handle_connect_event(client 连接成功时) |
— |
8. 改动意图速查
| 想改什么 | 先看哪些函数 |
|---|---|
| 监听恢复策略(节奏/时限/沉降) | net_listener_request / net_relisten_locked / net_listener_tick;常量 NET_RELISTEN_*_MS(net_listener.c 内);重建决策在 net_reconcile_tick 形态③④(下行) |
| 阻塞接收等待语义 | net_pending_register / net_pending_tick / net_pending_complete;常量 NET_PENDING_WATCHDOG_MS |
| 阻塞发送等待语义(挂起 send) | net_send_pending_register / net_send_pending_tick / net_send_pending_complete;常量 NET_SEND_SPIN_MS(有界自旋预算)/ NET_SEND_TIMEOUT_MS(无进展超时)/ NET_SEND_CHUNK_MAX(分块上限,net_pending.h 唯一定义处) |
| 事件未收到自愈 + 监听重建策略(合一 tick) | net_reconcile_tick(NET_RECONCILE_PERIOD_MS / NET_RECONCILE_COOLDOWN_MS / `NET_RECON_STABLE_ACCEPT |
| 新增一个公共 API | 模式:net_socket.h 声明(NET_NO_OS 条件编译)→ net_msg_type_t 加类型(如需要)→ net_process_messages 加 dispatch 分支 → netTask 侧 _locked 实现;调用侧复制"参数检查 → 入队 → net_wait_result → errno 恢复"骨架 |
| 缓冲布局 / MSS / 重传 / KeepAlive 参数 | net_init + net_config.h + ch395f.h(CH395F_RECV/SEND_BLOCKS、CH395F_TCP_MSS);改完必须 0 错 0 警构建 + 全测试套件回归 |
| 队列容量 / 消息结构 | CubeMX 生成的 MX_FREERTOS_Init(8 槽)+ net_socket.h 的 net_msg_t(勿加回传字段) |
| 超时参数(收发/重试/对账) | net_config.h(收发/重传/KeepAlive);net_listener.c 内 NET_RELISTEN_*;net_socket.c 内 NET_RECONCILE_* / NET_RECON_STABLE_* / NET_PENDING_WATCHDOG_MS |
9. 相关文档
docs/网络/BSD_Socket_API_使用指南.md—— 公开 API 语义、示例、常见问题(本文的上半部)docs/网络/网络自愈架构设计.md—— 监听生命周期权威设计(边沿→电平、单一所有者重建 =net_reconcile_tick形态③④、net_listener.c/net_events.c模块化边界 §11)docs/问题记录/CH395F_Trap_Records.md—— TRAP-NET-01~23 机制细节(本文引用处)docs/网络/Net层阻塞模型架构设计.md—— 阻塞等待模型的现状架构设计(pending 请求模型 / 五路完成语义)docs/测试说明/专项验证/链路延迟测试说明.md—— 芯片 / 队列 / 挂起 recv 等分段时延基线与自愈路径的台架证据docs/子系统设计/网络配置说明.md§4 —— 代次号与网络重建时序docs/测试说明/板级测试/CH395F_Test_Guide.md—— 单连接模型测试规则(TC-NET-6xx)