- D-6 目录规范化:docs 根目录散落文档归入 UI设计/子系统设计/架构设计/审查记录/ 环境与部署/网络 等子目录,去掉目录名空格,重命名 嵌入式C语言代码规范_V1.0.md - D-7 三档一致性门禁:新增 scripts/check_doc_links.py(链接存在性)、 scripts/gen_task_table.py(FreeRTOS 任务表数值)、scripts/check_mem_numbers.py (内存/Flash 体积),均支持 --check/--write/--diff,pre-commit 钩子同步注册 - D-5 文档索引:新建 docs/README.md,新建 docs/文档规范.md 约束目录命名与链接写法 - D-1 芯片手册瘦身:删除 1069 张无引用孤儿图片(77.06 MB), docs/芯片手册 120 MB → 41 MB,已引用图片零断链 - 同步 C 源码注释中的文档路径引用(D-6 重组后),构建 0 错误 0 警告
37 KiB
BSD Socket API 函数调用关系与设计说明
定位:本文档是
docs/网络/BSD_Socket_API_使用指南.md(下称"使用指南")的内部视图伴读文档。 使用指南回答"上层该怎么调"(公开语义 / 示例 / 常见问题);本文回答"内部函数各自做什么、互相怎么调用、为什么这么设计"(调用关系 / 设计意图 / 踩坑)。范围:MCU 端
Drivers/BSP/NET/net_socket.c(约 2870 行,全部函数)。 模拟器端后端simulator/sim/net_socket_sim.c(Winsock)是另一套实现,见使用指南 §2.3;net_select.c不在此展开,见使用指南 §7 / §10.5。交叉引用约定:机制标 Trap 号 →
docs/问题记录/CH395F_Trap_Records.md;用法语义引用使用指南 §;关键参数值与net_config.h/net_socket.c内宏一致。
1. 一句话架构
一切芯片(CH395F)操作收敛到唯一执行者 netTask 串行完成;其他任务通过"入队消息 + 任务通知"表达请求,结果只经任务通知值回传;阻塞等待不独占 netTask(挂起登记 + 主循环节拍完成);监听器常驻(auto_relisten + D10 非阻塞重试 + Trap 22 周期对账 + Trap 23 排渣隔离,四层自愈)。
2. 三个执行域
| 执行域 | 运行位置 / 节奏 | 允许的操作 | 典型调用者 |
|---|---|---|---|
| netTask 主循环 | 独立 FreeRTOS 任务,1ms/tick | 唯一可碰芯片(SPI2)的地方;3 个非阻塞 tick;队列分派;事件处理器;用户回调 | 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 16 实证:CONNECT 超时)。
3. 总体调用关系图
flowchart TB
subgraph APP["应用任务(非 netTask)"]
PUB["经队列公共 API<br/>netListen · netAccept · netConnect · netSend<br/>netRecv · netRecvTo · netClose<br/>netRelisten · netResyncListen"]
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/>先跑 3 个 tick,再排空队列分派"]
NP["netPoll<br/>INT# GPIO 轮询 → 全局中断 → 各 Socket SINT → 每轮一批"]
T1["① netRelistenRetryTick<br/>D10 重监听重试:30ms / 2s 时限"]
T2["② netPendingTick<br/>L3 挂起 recv:①数据 ②到点 ③断连 ④看门狗 1s"]
T3["③ netReconcileTick<br/>Trap 22 对账:200ms / 去抖 2 次 / 冷却 1s"]
DISP["队列分派 switch<br/>SEND · RECV · CLOSE · CONNECT · LISTEN<br/>ACCEPT · RESYNC_LISTEN · RELISTEN"]
subgraph LK["_locked / 内部分派目标(仅 netTask 上下文)"]
LL["netListenLocked<br/>初次监听(3×500ms 阻塞重试)"]
LN["netConnectLocked<br/>3×500ms 重试 + tcpConnect"]
LS["netSendLocked<br/>分块 ≤1KB + sendReady 门控"]
LR["netRecvLocked<br/>快路径 / DONTWAIT / 登记挂起"]
LA["netAcceptLocked<br/>先 netPoll + 状态判定"]
LC["netCloseLocked<br/>了结挂起 + close + 等 CLOSED + 排空中断"]
RLS["netRelistenTry → netRelistenLocked<br/>单次尝试 + Trap 23 排渣隔离"]
RSY["RESYNC 分支:改状态 + 取消挂起<br/>(不发芯片命令)"]
end
EV["事件处理器<br/>handleConnect · handleDisconnect<br/>handleRecv · handleSend · handleTimeout"]
FE["fireEvent → 用户回调"]
PR["s_net_pending<br/>(每 fd 至多一个挂起)"]
CH[("ch395f.* 驱动<br/>SPI2 串行事务")]
end
PUB -->|入队| MQ
MQ --> DISP
DISP --> LL & LN & LS & LR & LA & LC & RLS & RSY
NPM --> T1 & T2 & T3 & NP
T1 --> RLS
T3 -->|"① 已断"| EV
T3 -->|"② 补 accept"| EV
T3 -->|"③ 重建监听"| RLS
NP --> EV
EV --> FE
FE -.-> CB
EV -->|"LISTENING:监听丢失"| RLS
EV -->|"TIMEOUT:置隔离"| RLS
LR -->|无数据+阻塞| PR
PR --> T2
T2 -->|xTaskNotify| XN
XN -->|唤醒| PUB
NP --> CH
LL & LN & LS & LR & LA & LC & RLS --> CH
SPI -->|直碰 SPI| CH
CH -->|INT# 低电平| NP
读图要点:
- 应用侧只有两种"等":① 入队后
xTaskNotifyWait等回执(同步语义);② 挂起 recv 的等待(同一通道,只是"完成"由netPendingTick判定,不由 netTask 立即处理)。 net_process_messages()开头的 3 个 tick 是网络层自愈主干:没有任何应用消息时,监听恢复 / 挂起完成 / 状态对账照常运转——"断连后即使没人发消息,监听器也能自己活过来"靠的就是它们。- 事件处理器只由
net_poll驱动,例外是net_reconcile_tick(带 2 次去抖 + 1s 冷却)可再入handle_disconnect_event/handle_connect_event/net_relisten_try——对账是丢中断的唯一自愈通道(Trap 22)。 - 回调
event_cb在 netTask 上下文执行:回调内不得阻塞、不得发网络操作。
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(0x09) / set_tcp_mss / set_retrans_count+period / set_keepalive_idle+intvl+cnt → parse_ip_string + set_ip_addr / set_gwip_addr / set_mask_addr(或 set_dhcp)→ ch395f_init → set_mac_addr → set_send_buf / set_recv_buf ×8 → ping_enable |
① 重建前先取消全部挂起 recv 并逐一通知(不通知 = 调用方永久挂死),且必须早于控制块清零与芯片复位;② 重传/KeepAlive 参数必须在 ch395f_init 之前下发;③ fun_para=0x09:多 Socket 模式 + TCP Server 单连接 + DISCONNECT/TIMEOUT 不清缓冲(软件排空后手动关);④ 缓冲布局每 Socket 6 块(收 4 + 发 2)×8 = 48 块恰好用尽,Socket 0~7 必须显式重分配(Trap 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 类事件 |
HAL_GPIO_ReadPin → ch395f_get_glob_int_status_all → ch395f_get_sock_int_status ×n → handle_connect/disconnect/recv/send/timeout_event |
RECV 中断是电平触发:每次调用只处理一批(do-while(0)),处理完立即返回,否则主循环(echo/应用逻辑)饿死(Trap 08);SENBUF_FREE 只置 send_ready 不记事件 |
net_process_messages() |
队列入口 + 自愈 tick 宿主:每轮先跑 3 个非阻塞 tick,再排空队列逐条分派,结果 xTaskNotify 回传 |
net_relisten_retry_tick → net_pending_tick → net_reconcile_tick → osMessageQueueGet(0 超时) ×n → switch 分派 → xTaskNotify |
① tick 放在队列之前:断连后即使没有任何应用消息,监听器也能自主恢复、挂起 recv 也能完成;② RECV 若返回 NET_PENDING_REGISTERED(哨兵 0x70000000)则不发即时回执(do_notify=0),完成时才通知;③ 失败结果把 s_net_errno(具体负错误码)压入通知值——调用方恢复 errno 后对外统一 -1(BSD 语义);④ 通知值整个 32 位都是结果(不压位域标签,否则 32768 这类大字节数会溢出为负) |
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(3×500ms 阻塞重试)+ tcp_listen + 清全部中断状态 |
0 / -1 | 初次监听允许阻塞重试(启动期无竞争);成功后置 auto_relisten=1——监听常驻的源头;启动前清空各 Socket/全局中断状态,防历史残留误触发事件 |
net_accept |
NET_MSG_ACCEPT |
net_accept_locked |
无(先 net_poll 泵一次 + 状态判定) |
监听 fd 自身(单连接)/ -1(WOULDBLOCK) | 单连接:监听 Socket 在 CONNECT 后自身即数据通道,返回的就是它;CLOSED + auto_relisten 时触发非阻塞 net_relisten_try;accept 路径不做自愈——单次状态读会误判"刚 listen 未生效"致重建风暴(2026-09-23 实测),统一交给对账 |
net_connect |
NET_MSG_CONNECT |
net_connect_locked |
set_proto_type + open_socket(3×500ms 重试)+ 清残留中断 + set_sour_port / set_des_ip(大端数组)/ set_des_port + tcp_connect |
0 = 请求已发出(异步)/ -1 | open 先于寄存器写入(open 会复位寄存器);open 后清残留 DISCONNECT,防新连接秒断(Trap 14/17);目的 IP 字节序写反 = CONNECT TIMEOUT(Trap 16);返回 0 ≠ 已建立——调用方以 net_sock_state 轮询等 ESTABLISHED |
net_send |
NET_MSG_SEND |
net_send_locked |
ch395f_write_send_buf(分块)+ 等 SENBUF_FREE(net_poll 轮询) |
实际字节数 / -1 | 分块 ≤ NET_SEND_CHUNK_MAX = 1KB(硬件发送 FIFO 硬上限,Trap 20:写穿 FIFO = 数据错位);send_ready 门控用 net_poll 轮询而非 osDelay;短写必须告警——静默截断会让对端永久等待剩余字节(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 / TIMEDOUT / CONNRESET) | 快路径:数据已在 → 立即读出返回(允许部分读);无数据 + 阻塞 → 登记挂起后立即返回(L3,netTask 零占用);DONTWAIT + CLOSED → EOF + 触发重监听 |
net_recv_to |
NET_MSG_RECV(timeout>0) |
net_recv_locked |
同上 | >0 / 0 / -1 / -2(本片超时无数据) | E-02 头阻塞修复配套:调用方把长等待切成有界片(如 5ms/剩余预算),总超时语义不变、单次占用有界;timeout_ms=0 直接拒(会被解释成默认 30s,恰是长阻塞);超时只凭 -2 判定、不得依赖 errno(s_net_errno 是跨任务共享全局量,随时被别的任务覆盖) |
net_close |
NET_MSG_CLOSE |
net_close_locked |
close_socket + 等 CLOSED(5ms 节奏 / ≤500ms)+ 排空 Socket/全局中断 |
0 / -1 | 先了结本 fd 挂起 recv(排空/取消 + 必通知,关不得让等待者悬挂);关后排空残留 DISCONNECT(Trap 14/17:否则下一连接被"刚建立就断开"秒断);控制块整体清零 |
net_relisten |
NET_MSG_RELISTEN |
net_relisten_try → net_relisten_locked |
set_proto_type + set_sour_port + open_socket + tcp_listen(单次尝试;隔离期另有 close + get_socket_status + 排空) |
0 / -1 | 无条件芯片侧重建:芯片侧已在监听 → 首次 open 失败 → close 重建(短暂中断 ~1s);失败不阻塞,登记 D10 重试;用途 = 代次变化后显式重建 / 判死后确定性重建;-1 = "恢复中"(重试轮询接管),不是永久失败 |
net_resync_listen |
NET_MSG_RESYNC_LISTEN |
分派内直接处理(无芯片命令) | 无 | 0 / -1 | D14 静默回收场景:芯片已自行把 Socket 回收回监听且不发 DISCONNECT 中断 → 本层滞留 ESTABLISHED。只修 MCU 侧(state → LISTENING + 取消该 fd 挂起 recv);严禁发芯片 close/重监听——会摧毁芯片侧健康监听器 → 连接永拒(run8 实证 170s 拒绝)。非 ESTABLISHED 时视为已一致,返回 0 |
4.4 直调 API(不经队列)
| 函数 | 设计作用 | 主要调用 | 意图 / 注意 |
|---|---|---|---|
net_socket(domain,type) |
分配空闲控制块(alloc_socket 线性扫描 in_use=0)→ CREATED |
alloc_socket + net_relisten_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 再读数据 |
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 L3 pending recv 子系统(阻塞等待不独占 netTask)
模型:阻塞等待 = 挂起请求登记(每 fd 至多一个,禁止覆盖)+ 主循环节拍完成检查 + 通知唤醒(LwIP netconn 模型)。消除 E-02 头阻塞(旧实现: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_relisten_try ④ net_pending_complete |
按 ①数据 ②到点 ③断连 顺序检查(逐项对齐旧自旋语义);CONNECTING/LISTENING 等状态保持等待(CONNECT→ESTABLISHED 是合法迁移,不做状态推断取消,防误杀正在建链的等待);看门狗:到点后 1s 仍未了结 = 完成逻辑缺位 → 强制了结 + 错误日志 |
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 非阻塞重监听重试子系统(监听器自愈)
模型:重开失败 = 登记待重试(30ms 节奏 / 2s 时限),net_relisten_retry_tick 到点重试——任何时刻不阻塞任务。旧实现 osDelay(500) 忙等:netTask 冻结 500ms(全网停摆)、实测恢复 588ms、期间重连被拒(M3 台架)。
| 函数 | 设计作用 | 主要调用 | 意图 / 注意 |
|---|---|---|---|
net_relisten_try |
尝试一次;失败登记重试(不阻塞) | net_relisten_locked → 成功 net_relisten_done / 失败置 s_relisten_wait/next/dead |
已在重试队列 → 直接返回 -1(不重置重试时刻):否则 10ms 一次的 accept 轮询会不断把重试时刻往后推 → 永不重试(饥饿) |
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 23 隔离门:带隔离标记的 socket(异常释放 TIMEOUT,如中止上传)→ 持续排空 RX 残渣 + 主动 close + 排空中断 + 等芯片回 CLOSED/TCP_CLOSED,隔离满 400ms 才放行;800ms 上限强制放行(必须 < 2s 重试时限,否则隔离耗尽重试预算 → relisten give up 监听被弃——2026-09-23 实测修正);③ 成功只置 state=LISTENING,不 memset(fd 与 auto_relisten 保留——BSD 风格监听常驻) |
net_relisten_retry_tick |
每轮到点重试 | net_relisten_locked → net_relisten_done / 顺延 / 放弃 |
同 fd 有在途挂起 recv 时跳过(重开 Socket 会摧毁在途连接,L3 保险);到 2s 时限 → 清状态 + relisten give up 错误日志(应用可见) |
net_relisten_done |
标记完成 | net_relisten_clear +(探针构建)恢复计时样本 |
恢复时延 = 首次尝试 → 完成(L3 探针 relisten 段) |
net_relisten_clear |
取消该 fd 的待重试 | 清 wait/next/dead + 沉降/隔离状态 | 触发点:net_socket(新 fd 不继承)/ net_close_locked(关闭即取消);隔离状态必须一并清,否则残留标记会污染下一次复监听 |
net_drain_chip_rx |
排空芯片 RX 残留(256B 分块,≤16 次 = 4KB 上限) | get_recv_len + read_recv_buf |
Trap 23:中止/异常断开时芯片 RX 缓冲残留上一条连接的数据(实测 ~2048B),不排空会被下一条连接当自己的数据读走(静默截断);分块读避免新增静态缓冲(内存预算紧张) |
4.7 Trap 22 周期对账(丢中断自愈)
net_reconcile_tick()(net_process_messages 每轮调用;周期 200ms):
问题:芯片在快速复连场景会丢 CONNECT/DISCONNECT 中断(或静默回收 Socket 不发中断);丢事件后纯事件驱动无法自愈——典型:对端连接一直占着单连接监听的"后备"(芯片 TCP 仍 ESTABLISHED),后续 SYN 全被拒,而应用因未收到 DISCONNECT 仍以为有客户端(永不调 accept)。
机制:对每个在用 Socket(ESTABLISHED / LISTENING)回读 GET_SOCKET_STATUS,判定三类失同步并修正:
| # | 失同步 | 动作 | 复用的实现 |
|---|---|---|---|
| ① | 本层 ESTABLISHED 而芯片已无连接(sock/tcp = CLOSED) | 按断开处理 → CLOSED + 回调(应用读到 EOF 后自动重监听) | handle_disconnect_event(内置陈旧事件甄别) |
| ② | 本层 LISTENING 而芯片已完成握手(tcp = ESTABLISHED) | 补做 standalone accept 簿记 | handle_connect_event |
| ③ | 本层 LISTENING 而芯片监听已消失(异常关闭) | 重建监听 | net_relisten_try(D10 非阻塞重试) |
去抖 + 冷却(缺一不可):单次状态读可能落在"刚 open/listen 尚未生效"的瞬态——2026-09-23 实测:无去抖的 accept 路径自愈把监听反复重建直至 relisten give up(三服务口全死、89 行 ERROR)。故:同一失配需连续 2 次(≈400ms)才动手;同一 Socket 动手后冷却 1s。开销:每 200ms 每在用 Socket 一条状态命令,可忽略。
4.8 事件处理器(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 的大端相反)——解析别抄反 |
handle_disconnect_event |
状态机迁移 + 陈旧事件甄别 + 监听丢失重建 | ch395f_get_socket_status →(LISTENING:芯片已不可监听 → net_relisten_try,仍在监听 → 判陈旧丢弃)/(ESTABLISHED:芯片仍 ESTABLISHED → 判陈旧丢弃) |
① 陈旧 DISCONNECT 甄别(D20):同一 fd"关闭→重监听→再连接"循环复用,上一条连接的 FIN/ACK 可能在 fd 已复用时才上报 → 不甄别则新连接秒断(实测:FTP 同会话第二个数据连接必 425);② LISTENING 分支已不可监听即重建(Trap 22 修复的一半);③ 置 CLOSED 后不立即关芯片 Socket——留给 net_recv_locked 排空剩余数据(FIN 可能先于数据到达) |
handle_recv_event |
置 readable + 回调;不读 recv_len |
fire_event |
不读长度是为了避免与 net_recv_locked 的 ch395f_get_recv_len 竞争(Trap 13:分批读会归零);readable 供 net_select 无 SPI 访问判读 |
handle_send_event |
发送缓冲已空:触发回调(send_ready 由 SENBUF_FREE 前置位) |
fire_event |
— |
handle_timeout_event |
CONNECTING → CREATED(连接超时);ESTABLISHED → 置重监听隔离 + CLOSED(Trap 23);LISTENING → 仅记日志 | (隔离)写 s_relisten_quarantine[fd] + fire_event |
TIMEOUT = 异常释放(如客户端中止上传):对端在途数据随后落进 RX 缓冲 → 必须隔离后才许复监听;正常 FIN 释放走 disconnect 路径,不走隔离(零改动) |
fire_event |
调用已注册回调 | p_sock->event_cb |
回调上下文 = netTask:回调内不得阻塞、不得发网络操作(会自等死锁) |
4.9 工具函数
| 函数 | 设计作用 |
|---|---|
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/块)
loop 每块
N->>C: 等 SENBUF_FREE(net_poll 轮询,不睡)
end
C-->>N: SENBUF_FREE / SEND_OK(net_poll 置 send_ready)
N->>A: xTaskNotify(实际字节数 / 负错误码)
A->>A: 返回字节数 / -1(errno 已恢复)
5.2 阻塞接收无数据(L3 pending 路径)
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 23 隔离)
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 + 回调
else TIMEOUT(异常,如中止上传)
N->>N: handle_timeout_event:置重监听隔离 + CLOSED
end
A->>N: 下一次 net_recv(无数据)/ net_accept
N->>C: net_relisten_try → net_relisten_locked
opt 隔离期内(Trap 23)
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 09 沉降)
N->>N: 登记重试(30ms 节奏 / 2s 时限)→ 每轮到点重试
else 成功
N->>N: state = LISTENING(auto_relisten 保留)
N-->>A: xTaskNotify(accept 返回 / recv 返回 0)
end
Note over N: 2s 超限时 → 清状态 + "relisten give up"(错误日志,应用可感知)
5.4 丢中断自愈(Trap 22 对账)
sequenceDiagram
participant N as netTask
participant C as CH395F
participant A as appTask
Note over C: 快速复连:芯片完成握手但 CONNECT 中断丢失
Note over N: 本层滞留 LISTENING → 应用以为无客户端(永不调 accept)
loop 每 200ms
N->>C: net_reconcile_tick:get_socket_status(每个在用 socket)
C-->>N: 失配(本层 LISTENING,芯片 ESTABLISHED)
end
Note over N: 连续 2 次失配(去抖 ≈400ms)且不在冷却期 → 动手
N->>N: handle_connect_event:补 accept 簿记 → ESTABLISHED
N-->>A: 下一次 net_accept 返回客户端(连接不再"吞掉")
Note over N: 另两类:① 本层 ESTABLISHED 而芯片已断 → handle_disconnect_event<br/>③ 本层 LISTENING 而芯片监听消失 → net_relisten_try
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(0x09)/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 16:跨任务 SPI 竞争破坏芯片寄存器 → 连接必超时 |
| 2 | 结果只经任务通知值回传;消息按值入队,调用方看不到 netTask 对消息的写入;勿给消息加"回传用"字段 | Trap 10:6 个 API 永远返回 -1;"加 msg.err 字段"的修法曾致 ui_net_sink 每次空轮询误判真错误 → 连上即断 |
| 3 | netTask 不为任何人自旋等待:阻塞 recv = 挂起登记(L3);重开失败 = 重试登记(D10);初次 listen/connect 的 3×500ms 是唯一豁免(启动期无竞争) | E-02:一次空闲 30s recv 冻住全网 17.7s;旧重监听 osDelay(500) → 恢复 588ms、期间重连被拒 |
| 4 | netTask 不向自身发消息 / 不自我挂起(挂起登记对执行任务自身发起直接拒绝) | 自等自通知 = 死锁 |
| 5 | 基于单次状态读的重建动作必须去抖(连续 2 次)+ 冷却(1s) | 2026-09-23:无去抖 → 监听反复重建直至 relisten give up,三服务口全死 |
| 6 | 异常释放(TIMEOUT)后复监听前必须隔离:排空 RX 残渣 + 等 CLOSED/TCP_CLOSED;隔离上限 < 重试放弃时限 | Trap 23:~2KB 残渣被下条连接当自己的数据 → 首笔上传截断 2048B 并误报 226 成功;上限=2s 时隔离耗尽重试预算 → 监听被弃 |
| 7 | RECV 电平触发中断每轮只处理一批 | Trap 08:连续循环 → 主循环饿死 |
| 8 | close 前先了结挂起 recv + 排空残留 DISCONNECT | Trap 14/17/19:锁存的陈旧 DISCONNECT 秒断下一连接 |
| 9 | 两段式等待防丢失通知:零等待拾取 + 阻塞;清除掩码全 1;以 pdTRUE 判定 | run3~11:丢失通知 → 挂死 / 结果错位;0 结果误判"无通知" → 永久挂死 |
| 10 | 网络重建先取消并通知全部挂起,再复位芯片 | 取消不通知 = 调用方永久挂死 |
7. ch395f 驱动接口使用图(net_socket.c → 芯片命令)
| 芯片命令 | 使用处 | 说明 |
|---|---|---|
ch395f_check_exist / ch395f_reset |
net_init |
上电探测与复位 |
ch395f_set_fun_para(0x09) |
net_init |
多 Socket + TCP 单连接 + 手动关缓冲 |
ch395f_set_tcp_mss / set_retrans_count / set_retrans_period / set_keepalive_idle+intvl+cnt |
net_init(均须在 ch395f_init 之前) |
值见 net_config.h |
ch395f_set_ip_addr / set_gwip_addr / set_mask_addr / set_dhcp |
net_init |
固定 IP 或 DHCP |
ch395f_init / ch395f_set_mac_addr / ch395f_ping_enable |
net_init |
协议栈初始化 / MAC(EEPROM,幂等)/ PING |
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;net_listen_locked / net_connect_locked / net_close_locked / net_relisten_locked(清残留) |
读 = 清(中断清除机制) |
ch395f_get_sock_int_status |
net_poll(逐 Socket SINT);各 open 路径后清残留;事件处理器清 TIMEOUT |
读 = 清 |
ch395f_open_socket / ch395f_close_socket |
net_bind(UDP)、net_listen_locked、net_connect_locked、net_close_locked、net_relisten_locked(含隔离期主动 close) |
Trap 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_sendto |
单次 ≤1KB(硬件 FIFO 上限,Trap 20) |
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_recv_event 故意不读(防竞争,Trap 13) |
ch395f_get_socket_status |
net_reconcile_tick、handle_disconnect_event(两处甄别)、net_close_locked(等 CLOSED)、net_relisten_locked(隔离门)、net_connect_locked(诊断回读) |
输出 2 字节([0]=Socket 状态 [1]=TCP 状态),缓冲必须给 2 字节 |
ch395f_get_remot_ipp |
handle_connect_event、net_connect_locked(诊断) |
小端输出(与 set_des_ip 相反) |
ch395f_set_keepalive_enable |
handle_connect_event(client 连接成功时) |
— |
8. 改动意图速查
| 想改什么 | 先看哪些函数 |
|---|---|
| 监听恢复策略(节奏/时限/隔离) | net_relisten_try / net_relisten_locked / net_relisten_retry_tick;常量 NET_RELISTEN_*_MS(net_socket.c 内) |
| 阻塞接收等待语义 | net_pending_register / net_pending_tick / net_pending_complete;常量 NET_PENDING_WATCHDOG_MS |
| 丢中断自愈策略 | net_reconcile_tick(周期/去抖/冷却三常量)+ handle_disconnect_event LISTENING 分支 |
| 新增一个公共 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_socket.c 内 NET_RELISTEN_* / NET_RECONCILE_* / NET_PENDING_WATCHDOG_MS |
9. 相关文档
docs/网络/BSD_Socket_API_使用指南.md—— 公开 API 语义、示例、常见问题(本文的上半部)docs/问题记录/CH395F_Trap_Records.md—— Trap 01~23 机制细节(本文引用处)docs/问题记录/Net层阻塞模型业界对照与演进方案.md—— L3 pending 化的设计推演docs/问题记录/错误码传播审计.md—— E-02 头阻塞与错误码回传全审计docs/测试说明/链路延迟测试说明.md—— D1/D10/L3/D14 等时延探针与自愈路径的台架证据docs/子系统设计/网络配置说明.md§4 —— 代次号与网络重建时序docs/测试说明/CH395F_Test_Guide.md—— 单连接模型测试规则(TC-NET-6xx)