Files
STM32F4-Base/Drivers/BSP/NET/net_socket.c
T
wandering1 a3c6e89378 fix(网络): 主动拆会话登记即返——drop 路径不再占用 netTask(登记簿 N-17 收口)
道F 批 2 / task-17。定位 + 单点收口,不开 net 层跨上下文大口子。

【定位(链路,全部代码可复核)】
探针挂起 30s → time_sync.c:149 预算耗尽 → :187 net_drop_conn()。NET_MSG_
DROP_CONN 处理器(net_drop_conn_locked)内同步跑 net_sock_reset_sync
(QUARANTINE)(2ms 节奏、400ms 底/600ms 顶,自家注释原记「等待期 netTask
有界占用 ~400-600ms」):netTask 的 1ms 主循环冻结在同一迭代 → NET 位
停摆;串行总线整窗停摆 → UI 推流/FTP/104 在途消息回执顺延;对时调用方
睡在 net_wait_result 等回执期间无分片心跳 → 五位同窗缺席 → 连续 8 个
100ms 抽样停喂 → 声明 [WD] task stall(V5 时代 req=0x37、快照只剩 bit0,
即日志 mask=0x00000001)。被动断连/EOF 走 net_listener_tick 异步沉降机
(D10 条令)故纯 FTP 负载 0 stall——只有主动 drop 留在同步原语里。

【修复】监听型 fd 的芯片拆除改为 net_listener_settle(QUARANTINE) +
net_listener_request(REQ_DROP) 登记即返,拆链/排渣/等 CLOSED/重建全部
交 net_listener_tick 30ms 节奏——与被动释放同一台机器(E16 台架实证该
自建序列 RST 10/10、FIN 5/5)。非监听 client fd 无监听可保,同步形态
原样保留(全仓零生产调用者)。返回值语义显式改为「拆除与重建已登记」
(net_socket.h 注释按 lead 要求写明,防未来调用方按旧语义写)。
APP_ASSERT(S12 试点)分派门与断言位置未动,登记时序下仍成立。

【评估否决留档(lead 要求论证入文档)】「缩短重建重试 5×2s」不采:
该退避在 time_sync_open_listener() bind/listen 失败路径、已被
time_sync_delay_fed() 分片喂保护、不在缺口链上;缩短反撞 TRAP-NET-09
沉降窗。全文见 docs/网络/网络自愈架构设计.md §4.6(已改写)。

【竞态与信源核对】① 主路径置 CLOSED 后迟到的 DISCONNECT/TIMEOUT 被
net_events 状态门拦截(只认活会话态),不会把刚登记的 QUARANTINE 降级;
② 零新增读清点——全局寄存器唯一读者仍 net_poll,隔离窗两处本 Socket
读清是 net_listener_tick 原位,§4.10 三条准入判据不触发;③ E13 物理
边界(对端不配合 FIN_WAIT_2 长尾)逐字不变,只移动等待发生的位置。

【测试】net_socket 主机套件 457/457:TC-NETSOCK-240f/321b/321b2/460e
重锚到登记式(settle+REQ_DROP、rst 机零启动、tick 预算 ≤20、无 [ERR]),
新增 321g/h/i + 460f 钉住 client fd 旧同步形态。全量 --all-files 40 绿。

【体积增量(vs scripts/ram_baseline.tsv 2026-10-25 行)】
ER_IROM1 237328→237344(+16 B);RW_IRAM1 75792(+0);ZI 0。
@build 0 错误 0 警告(Code=215948 RO=21396 RW=1232 ZI=131984)。

gate: only mem-budget/mem-numbers pending lead ledger backfill
test-coverage 本条**绿**(已覆盖行净增,未动用裁决③的覆盖率例外——
旧同步路径行经 client fd 新用例继续被覆盖,净差非负)。其余含
id-check/doc-links/全部主机单测绿。仅 mem-numbers/mem-budget 两红 =
台账 +16B 漂移待回填(lead 单写者禁区)。

【越界申报】① test/host/net_socket/{ns_s3_msg,ns_s4_state,ns_s5_io}.c
(批准③同类:只动本任务相关用例,未碰他人行);② docs/网络/
网络自愈架构设计.md §4.6 改写(lead 本任务指令明示)。登记簿 N-17
销项与 README 波次行归 lead;docs/内存管理 数字未碰(禁区)。

台架:V5 复跑判据(e02_probe + FTP ≥30min 零 stall / 零复位)属网络
对端类——待 lead 排窗,步骤清单随 done 消息发出。
2026-10-25 10:08:46 +08:00

3274 lines
167 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:Network Socket Core
* 模块功能:BSD Socket API 核心实现,包含 Socket 管理、状态机、消息队列(线程安全)、事件处理、数据收发等功能,底层调用 CH395F 驱动
* 适用平台:STM32F407ZGTx + CH395F 以太网芯片
* 作者:王建锋
* 创建日期:2026-07-18
* 修改记录:
* 2026-07-18 王建锋 创建初始版本
* 2026-07-19 王建锋 添加消息队列线程安全机制,替换 xQueueCreate 为 CubeMX osMessageQueue
*/
#include <string.h>
#include <stdio.h>
#include <stdbool.h>
#include "FreeRTOS.h"
#include "task.h"
#include "cmsis_os.h"
#include "net_socket.h"
#include "net_listener.h"
#include "net_events.h" /* 事件处理 + 周期对账(真机专属,同目录内部头) */
#include "net_pending.h"
#include "ch395f.h"
#include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */
#include "net_latency_test.h" /* 延迟探针接口(chunk_wait 采样) */
#include "main.h"
/*
* 调试输出配置
* 深调用链模块(net_bind/net_send/net_poll 是 netTask/ftpTask/timeSyncTask
* 最坏链的主干):日志行缓冲收到 128 B(默认 192)。本模块最长行渲染后
* ≈ 100 B(含时间戳 21 + 标签)< 128 → 不截断
*/
#define DBG_LINE_BUF_MAX 128U
#define DBG_TAG "[NET]"
#include "dbg_log.h"
/*
* 模块级轻量断言(S12,规格 docs/任务规划/测试与架构专项.md §2.1):本模块用
* APP_ASSERT 表达 2 处状态机不变量——生产构建(开关默认关闭)编译期消失、
* .map 无新增符号;打开态复用 configASSERT 的同一个失败钩子(不是 DBG_ERROR,
* 断言点常在"内核状态已不可信"的现场,走裸控制台更稳)。
* 纪律见 app_assert.h 第四节:**不替代任何错误返回**(本模块既有的 BADF /
* INVAL / WOULDBLOCK 分支一条未动),只写"为假就说明我们自己写错了"的内部不变量;
* 断言条件无副作用,故关闭态不求值也不会改变行为。
*/
#include "app_assert.h"
/*
* 私有宏定义区
*/
#define NET_PHY_CHECK_INTERVAL_MS 5000U /* PHY 状态定期检查间隔(ms) */
/* E6 实验插桩(U15 失配频率,docs/网络/网络自愈架构设计.md §4.8;裁决锚点 §5.2 E6):
* 假设=「RX 非空但 readable=0」(RECV_OK 事件未收到的数据面形态)在生产真实
* 发生。置 1 烧实验固件:net_poll 每 32 轮对在用 fd 发一条 GET_RECV_LEN,
* 命中计数 + 低频日志(每小时 ≤1 条防刷屏);跑 24h 常态 + E1 压测。
* 判据:命中 >0 → U15(数据面失配)内化立项;=0(含压测)→ 上层 32 轮
* 盲收保留现状、U15 登记观察项。**默认 0 = 产品构建零成本(编译剔除)**。
* E6 已裁决(2026-10-13,《网络自愈架构设计》§5.2 E6):命中=0(含压测)→ 项 4 不立项,维持默认 0。
* ⚠ 必须定义在文件最前部:bring_up 的 armed 日志与 net_poll 探测环两处消费。 */
#define NET_U15_PROBE 0
#if NET_U15_PROBE
#define NET_U15_PROBE_PERIOD 32U /* 每 32 轮 net_poll 探测一次(≈32ms) */
#define NET_U15_PROBE_LOG_MS 3600000U /* 命中日志每小时至多 1 条 */
static uint32_t s_u15_round; /* net_poll 轮次计数 */
static uint32_t s_u15_hits; /* 累计确认命中次数(判据数据) */
static uint32_t s_u15_last_log; /* 上次命中日志时刻(tick) */
static uint8_t s_u15_cand[NET_MAX_SOCKETS]; /* 两级确认的候选标记(per-socket) */
#endif
/*
* 全局变量定义区 - 尽量避免使用全局变量
*/
static net_sock_t s_net_socks[NET_MAX_SOCKETS]; /* Socket 控制块数组 */
static uint8_t s_net_initialized = 0; /* 初始化标志 */
static uint8_t s_local_ip[4]; /* 本机 IP 缓存(网络序,net_getsockname 用) */
static uint16_t s_dynamic_port = 49152; /* 动态端口分配起始值 */
static volatile uint32_t s_net_generation = 0; /* 网络代次号:网络重建完成时递增(服务据此重建监听) */
/*
* 消息队列(线程安全核心,句柄由本模块私有持有)
* 外部任务经 net_send/net_recv/net_close 等公开 API 投递请求
* netTask 调 net_process_messages() 串行处理所有 CH395F 操作
* 队列对象由 CubeMX 在 MX_FREERTOS_Init()(osKernelInitialize 之后)创建,创建后
* 立刻经 net_msg_queue_adopt() 绑定到这里;绑定之前恒为 NULL,各处投递前的判空
* 因此是真实防线而不是形式(绑定失败的后果是"请求被丢弃",不是崩溃)。
*/
static osMessageQueueId_t s_net_msg_queue;
/*
* 本层执行任务句柄(= net_init 的调用者,NET 层为 netTask、硬件层为 testTask)
* 与 net_pending_capture_owner() 同一时刻、同一语义捕获,判据也一致。
* 用途见 net_ctx_is_owner_task():给"绕过消息队列直调芯片"的接口把危险面关掉。
* 捕获前恒 NULL(net_init 未跑)——那时任何 socket 都不存在,判空放行即可。
*/
static TaskHandle_t s_net_owner_task;
/*
* 函数功能:绑定消息队列句柄(本模块是该句柄的唯一使用者)
* 入口参数:queue_id - 已创建的消息队列句柄
* 返回值:0 = 绑定成功;-1 = 句柄为空或重复绑定(保留首次绑定,不覆盖)
* 限定条件:osKernelInitialize() 之后、netTask 启动之前,由 MX_FREERTOS_Init() 调用一次
* 函数说明:见 net_socket.h 同名声明处的完整说明。重复绑定**不覆盖**是刻意的——
* 真出现两处绑定时,说明有人新加了调用点,此时保留先绑的那个并把后来者
* 拒掉,比静默换句柄(已入队的请求会留在旧队列里永不处理)更容易暴露。
*/
/*
* 函数功能:判当前上下文是否本层执行任务(netTask / 硬件层的 testTask)
* 入口参数:无
* 出口参数:无
* 返回值:1 - 是本层执行任务(或未捕获,见下);0 - 明确不是(应拒绝直调芯片)
* 限定条件:net_init 之后判据才有效
* 函数说明:给 net_sendto / net_recvfrom 这类**绕过消息队列、在调用方上下文里
* 直接 net_poll + 读写芯片缓冲**的接口把危险面结构性关掉:非执行任务
* 调用会与本层的 net_poll 抢同一根 SPI2(TRAP-NET-14 的根因形态),
* 而 SPI 事务没有跨任务互斥(ch395f_spi_begin/end 只是片选与分频)。
* 刻意**不做**成"排队改写":recvfrom 要把源地址交回调用方,按 AGENTS
* 不能塞进按值拷贝的 net_msg_t,真要串行化得设计 per-task 结果槽,
* 那是需要真机回归的独立工程(2026-10-24 定案:本轮先把危险面关掉,串行化留后续)。
* 未捕获(net_init 未跑)时返回 1:那时任何 socket 都不存在,
* 调用只会先在参数检查上拿到 BADF,不构成绕过。
*/
static uint8_t net_ctx_is_owner_task(void) {
if (s_net_owner_task == NULL) {
return 1U;
}
/* 内核未启动/中断上下文里句柄为 NULL:与执行任务不等 → 判不是(不得碰 SPI) */
return (xTaskGetCurrentTaskHandle() == s_net_owner_task) ? 1U : 0U;
}
int net_msg_queue_adopt(osMessageQueueId_t queue_id) {
if (queue_id == NULL) {
return -1;
}
if (s_net_msg_queue != NULL) {
return -1;
}
s_net_msg_queue = queue_id;
return 0;
}
/* ================= 错误码存储(E-03 修复:按任务隔离,消除跨任务覆写竞态) =================
*
* 旧实现是单个全局 `static int s_net_errno`:netTask 在处理消息时写入本层的
* 「最后错误码」,调用方包装器随后在**自己的上下文**再写一次。两条写路径分属不同
* 任务且无互斥,A 任务刚被置好的码会被 B 任务的通知处理抢先覆盖 → A 读到 B 的码。
*
* 新实现把错误码存进「每个任务自己的槽位」:任务读写自己的槽位,天然无竞态,
* 且保留 net_get_errno() 原语义(本任务最后一次失败的精确码、成功不清零)。
*
* 容量:NET_ERRNO_TASK_SLOTS 个任务句柄槽位(应用任务 + netTask + 测试任务够用)。
* 超出时**不新增共享槽位**(那会把竞态带回来),而是统一落到 [0] 槽——
* 语义退化为全局共享,仅影响排障可读性,不影响任何功能判定。
*/
#define NET_ERRNO_TASK_SLOTS 8U
/* 每任务错误码槽位:slot[i].owner == 当前任务句柄 时命中 */
static struct {
void *owner; /* 槽主任务句柄(NULL = 空槽) */
int err; /* 该任务最后一次失败的精确错误码 */
} s_net_errno_slots[NET_ERRNO_TASK_SLOTS];
/*
* 函数功能:取本任务的错误码槽位(命中/分配),无 RTOS 时回退 [0]
* 入口参数:无
* 出口参数:无
* 返回值:槽位下标(0 ~ NET_ERRNO_TASK_SLOTS-1)
* 限定条件:内核未启动或中断上下文返回 0(不做槽位分配,避免在 ISR 里改槽位表)
* 函数说明:1. 已存在的槽位(owner 命中)直接复用;空槽(owner == NULL)首次占用
* 2. 快路径**只读不写**:并发读者绝不会看到"owner 已写、err 未写"的中间态
* 3. 槽位与任务不强绑定释放:任务句柄被回收复用时,新任务沿用该槽位的
* err 初值无碍(语义本就允许残留码)
*/
static uint32_t net_errno_slot(void) {
/* 任务句柄(未启动内核/中断上下文时为 NULL) */
void *me;
/* 空槽候选与循环变量 */
uint32_t i;
uint32_t empty = NET_ERRNO_TASK_SLOTS;
/* 内核未启动或中断上下文:统一用 [0] 槽(语义退化为共享,仅排障用) */
if (osKernelGetState() != osKernelRunning) {
return 0U;
}
me = (void *)xTaskGetCurrentTaskHandle();
if (me == NULL) {
return 0U;
}
/* 无锁快路径:找自己的槽位,顺带记下第一个空槽,全程**绝不写**任何槽
* (写会让并发读者看到半成品槽位) */
for (i = 0U; i < NET_ERRNO_TASK_SLOTS; i++) {
const void *owner = s_net_errno_slots[i].owner;
if (owner == me) {
return i; /* 命中:本任务已有槽位 */
}
if (owner == NULL && empty == NET_ERRNO_TASK_SLOTS) {
empty = i; /* 记录第一个空槽(尚未占用) */
}
}
/* 无自己的槽位:尝试占用空槽。本函数只在任务上下文走到这里(中断/内核未启动
* 已在上方返回 0),故用任务上下文临界区(与 App/log/console_log.c 同一写法) */
if (empty < NET_ERRNO_TASK_SLOTS) {
taskENTER_CRITICAL();
if (s_net_errno_slots[empty].owner == NULL) {
s_net_errno_slots[empty].err = NET_OK;
s_net_errno_slots[empty].owner = me;
taskEXIT_CRITICAL();
return empty;
}
/* 复查失败(被并发占用):不重试,回退 [0] 槽(这是**特意避免**再次循环
* 的取舍——重试会与另一任务形成写入竞争,那就把要修的竞态又带回来了) */
taskEXIT_CRITICAL();
}
/* 槽位用尽:回退 [0] 槽(共享语义,仅排障可读性下降) */
return 0U;
}
/*
* 函数功能:记录**本任务**最后一次失败的精确错误码(取代原 `s_net_errno = X` 赋值)
* 入口参数:err - 错误码(NET_ERR_xxx,负值)
* 出口参数:无
* 返回值:err 原值(便于写成 `return net_set_errno(NET_ERR_XXX);`)
* 限定条件:无
* 函数说明:1. 只写本任务的槽位,其他任务读不到、也改不了 → 消除跨任务覆写竞态
* 2. N-32 §10 模块化:由 static 提升为**模块间可见**(net_listener.c
* 也需要设 errno),声明与 NET_ERR_RETURN_1 宏见 net_socket.h
*/
int net_set_errno(int err) {
s_net_errno_slots[net_errno_slot()].err = err;
return err;
}
/*
* 私有函数声明区
*/
static int alloc_socket(void);
static void parse_ip_string(const char *str, uint8_t *ip_arr);
static int net_send_locked(net_sock_t *p_sock, const void *buf, int len,
TaskHandle_t caller);
static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags,
uint32_t timeout_ms, TaskHandle_t caller);
static int net_close_locked(net_sock_t *p_sock);
static int net_drop_conn_locked(net_sock_t *p_sock);
static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen);
static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int addrlen);
/*
* 函数功能:判断 6 字节 MAC 是否全零
* 入口参数:p_mac - MAC 指针(不可为 NULL) const uint8_t *
* 返回值:true - 全零(= 使用芯片内置 MAC);false - 非全零
* 限定条件:p_mac 非空
*/
static bool s_mac_is_zero(const uint8_t *p_mac) {
for (int i = 0; i < 6; i++) {
if (p_mac[i] != 0u) {
return false;
}
}
return true;
}
/*
* 函数功能:网络子系统初始化(配置 CH395F + 启用多连接模式)
* 入口参数:ip - IP 地址字符串 "192.168.1.100",NULL 使用 DHCP 自动获取 const char *
* mask - 子网掩码字符串 "255.255.255.0",NULL 使用默认值 const char *
* gateway - 网关地址字符串 "192.168.1.1",NULL 使用默认值 const char *
* mac - MAC 地址(6 字节,低字节在前),NULL/全零使用芯片内置 const uint8_t *
* 返回值:0 - 初始化成功 int
* -1 - 初始化失败(芯片检测/复位/协议栈初始化失败) int
* 限定条件:SPI2 已正确初始化;netTask 尚未启动
* 函数说明:1. 检查 CH395F 芯片是否存在并复位
* 2. 启用 TCP Server 多连接模式(bit1=1),设置 TCP MSS、重传参数、KeepAlive 参数
* 3. 配置 IP/网关/子网掩码,或启用 DHCP
* 4. 初始化 CH395F 协议栈并使能 PING 响应
* 5. mac 非空且非全零:初始化**之前**设置 MAC(§9.2.1 ①,TRAP-NET-19;
* 写芯片 EEPROM,每上电重放同值幂等)
*/
int net_init(const char *ip, const char *mask, const char *gateway, const uint8_t *mac) {
ch395f_status_t ret;
/* 网络重建前取消全部挂起请求并通知调用方(调用方阻塞等回执,
* 取消不通知 = 永久挂死);必须早于控制块清零与芯片复位。
* recv / send 两张表各自独立,须分别取消 */
net_pending_cancel_all(NET_ERR_CONNRESET);
net_send_pending_cancel_all(NET_ERR_CONNRESET);
/* 清零 Socket 控制块 */
memset(s_net_socks, 0, sizeof(s_net_socks));
net_set_errno(NET_OK);
s_net_initialized = 0;
net_pending_capture_owner(); /* 执行任务 = net_init 调用者(NET 层 = netTask / 硬件层 = testTask) */
/* 同一时刻捕获本层自己的执行任务句柄(net_ctx_is_owner_task 的判据;
* 必须与上面同源,否则两张守卫会各认一个"谁是 netTask") */
s_net_owner_task = xTaskGetCurrentTaskHandle();
/* 检查 CH395F 芯片 */
if (ch395f_check_exist() != CH395F_STATUS_OK) {
DBG_ERROR("net_init: ch395f_check_exist FAILED");
return NET_ERR_RETURN_1(NET_ERR);
}
/* 复位芯片 */
ch395f_reset();
/* 芯片版本留痕 + 版本门控(CH395F 手册 §5.52 表):bit3 SOCKET_CLOSE
* 需芯片 ≥0x46、TCP_SERVER 需 ≥0x44。本台架芯片实测 ≥0x46(TRAP-NET-20 定案
* SOCKET_CLOSE 实际生效);门控防批次漂移——版本不足则**拒置 bit3**
* (退回芯片自动关断语义)并 [ERR] 留痕,不静默带病运行 */
uint8_t chip_ver = ch395f_get_version();
/* FUN_PARA 实际下发值(版本不足 = 0,bit3 拒置) */
uint8_t fun_para = 0x00U;
if (chip_ver >= 0x46U) {
fun_para = CH395F_FUN_PARA_SOCKET_CLOSE;
} else {
DBG_ERROR("ch395f ver=0x%02X < 0x46: SOCKET_CLOSE(bit3) unsupported, cleared",
chip_ver);
}
/* 版本 + 实际生效 FUN_PARA 留痕(旧日志硬编码 0x08 改为实际值) */
DBG_INFO("ch395f ver=0x%02X fun_para=0x%02X", chip_ver, fun_para);
/* 功能参数(§5.52 表):bit3=1 SOCKET_CLOSE——DISCONNECT/TIMEOUT 后芯片
* 不清缓冲,由 MCU 读出残留数据后再关 Socket(TRAP-NET-20 三次定案的排空契约);
* bit1=0 TCP_SERVER 多连接模式不用(本项目单连接模型:每监听器独占一个
* Socket,监听转数据通道);bit0 保留必须为 0——旧注释"bit0=1 多 Socket
* 模式"手册与网络资料均无出处(多 Socket 由 SN 命令族寻址实现,与 FUN_PARA
* 无关),且违反保留位规定 → 0x09 改 0x08(2026-10-11 定案:官方手册
* V1C/V1E/V2.2/V2.3 的 bit0 全是保留位,官方例程 bit0=0 下 Socket 0~7 照常
* 可用、FTP 例程本身就传 0x08;证据链见 TRAP-NET-15,
* 0x08 下的多 Socket 台架复测登记 N-31) */
ch395f_set_fun_para(fun_para);
/* 设置 TCP MSS:维持 1024(512 裕量方案未采纳,当年支撑它的台架判据不可信,
* 已随 TC-NET-604 结案为非缺陷一并撤回(2026-09-23)) */
/* 手册(§5.45):内部缓冲共 48 块 × 512B = 24KB,每 Socket 分配 6 块(接收 4 块 = 2KB + 发送 2 块 = 1KB) */
/* 块布局固定(与 MSS 解耦);MSS=1024 为满足布局的最大值(接收 = 2×MSS),
* 真值源为 ch395f.h 的 CH395F_TCP_MSS */
ch395f_set_tcp_mss(CH395F_TCP_MSS);
/* 设置 TCP 重传参数(必须在 INIT_CH395 之前调用;P0-a:周期 500→200ms。
* 台架验收 2026-09-22 完成:GET_CMD_STATUS 对 SET 类命令无判别力(500/2000ms
* 行为一致,均为 RESET_ALL 的陈旧 BUSY 残留),值的接受性以手册范围(≤1000ms)
* + 全套件无回归为准——验收探针已移除,结论见《链路延迟测试说明》§10.1 D1 */
ch395f_set_retrans_count(NET_RETRANS_COUNT);
ch395f_set_retrans_period(NET_RETRANS_PERIOD_MS);
/* 设置 TCP KeepAlive 参数(必须在 INIT_CH395 之前,单位 ms,必须为 500 的倍数,且 IDLE > INTVL)
* 值来自 net_config.h 的 NET_KEEPALIVE_*(60s/5s/3)*/
ch395f_set_keepalive_idle(NET_KEEPALIVE_IDLE_MS); /* 空闲 60 秒后开始探测*/
ch395f_set_keepalive_intvl(NET_KEEPALIVE_INTVL_MS); /* 5 秒探测一次*/
ch395f_set_keepalive_cnt(NET_KEEPALIVE_CNT); /* 最多探 3 次*/
/* 设置 MAC(手册 §9.2.1 步骤①:SET_MAC 必须在 INIT_CH395 之前——SET_MAC 写芯片
* EEPROM,而 INIT_CH395 会初始化/加载 MAC;顺序反了则本次上电芯片仍用旧 MAC,
* UI 配置的 MAC 不生效(回落芯片内置/上次值),设备在线身份与 UI 显示不符。
* P1-7,2026-10-04);NULL 或全零 = 保持芯片内置 MAC,不下发;
* 写 EEPROM,每上电重放同值幂等 */
if (mac != NULL && s_mac_is_zero(mac) == false) {
ch395f_set_mac_addr((uint8_t *)mac);
}
/* 设置网络参数(必须在 INIT_CH395 之前调用 */
if (ip != NULL) {
uint8_t ip_arr[4];
uint8_t mask_arr[4];
uint8_t gw_arr[4];
parse_ip_string(ip, ip_arr);
parse_ip_string(mask, mask_arr);
parse_ip_string(gateway, gw_arr);
memcpy(s_local_ip, ip_arr, 4);
ch395f_set_ip_addr(ip_arr);
ch395f_set_gwip_addr(gw_arr);
ch395f_set_mask_addr(mask_arr);
} else {
/* 启用 DHCP */
ch395f_set_dhcp(1);
}
/* 初始化 CH395F 协议栈 */
ret = ch395f_init();
if (ret != CH395F_STATUS_OK) {
return NET_ERR_RETURN_1(NET_ERR);
}
/* 多 Socket 模式:为 Socket 0~7 各分配独立收发缓冲(必须在 OPEN_SOCKET 之前) */
/* 手册(§5.45):48 块 × 512B = 24KB;上电芯片默认仅给 Socket 0~3 分配(接收 8 / 发送 4 块) */
/* Socket 4~7 为 NULL,用到 8 个 Socket 必须重分配;单 Socket 块数为固定布局(ch395f.h,2026-09-22 起与 MSS 解耦) */
/* 每 Socket 6 块:接收 CH395F_RECV_BLOCKS(=4 块=2KB) + 发送 CH395F_SEND_BLOCKS(=2 块=1KB),用满 48 块 */
/* recv_start = i×6,send_start = recv_start+4;Socket 7 占块 42~47,恰好用尽 0~47,互不重叠 */
/* 布局以手册 §5.45 为准;若实际芯片为其它缓冲规格需重新核算 */
/* TRAP-NET-01:先配数据 Socket 1~7,再配监听 Socket 0 */
uint8_t blocks_per_socket = (uint8_t)(CH395F_RECV_BLOCKS + CH395F_SEND_BLOCKS);
for (int i = NET_MAX_SOCKETS - 1; i >= 0; i--) {
uint8_t recv_start = (uint8_t)(i * blocks_per_socket);
uint8_t send_start = (uint8_t)(recv_start + CH395F_RECV_BLOCKS);
ch395f_set_send_buf((uint8_t)i, send_start, (uint8_t)CH395F_SEND_BLOCKS);
ch395f_set_recv_buf((uint8_t)i, recv_start, (uint8_t)CH395F_RECV_BLOCKS);
}
/* 使能 PING 响应 */
ch395f_ping_enable(1);
s_net_initialized = 1;
#if NET_U15_PROBE
/* E6:确认插桩固件已生效(挂机期间据此核对烧录身份) */
DBG_INFO("U15 probe armed: period=%u log_per_h=1", (unsigned int)NET_U15_PROBE_PERIOD);
#endif
return 0;
}
/*
* 函数功能:取网络代次号
* 入口参数:无
* 出口参数:无
* 返回值:代次号(每次网络重建完成递增) uint32_t
* 限定条件:无
* 函数说明:见 net_socket.h 同名函数说明
*/
uint32_t net_get_generation(void) {
/* 直接返回当前代次号(只读,无需锁) */
return s_net_generation;
}
/*
* 函数功能:递增网络代次号(网络重建完成、对外可用时调用)
* 入口参数:无
* 出口参数:无
* 返回值:无
* 限定条件:net_init() 成功且 PHY/链路就绪后调用(当前仅 net_stack_bring_up)
* 函数说明:见 net_socket.h 同名函数说明
*/
void net_generation_inc(void) {
/* 代次号 +1:各 TCP 服务检测到代次变化后重建监听(配置变更免重启生效) */
s_net_generation++;
}
/*
* 函数功能:轮询 CH395F 中断状态,更新 Socket 连接状态并处理事件
* 入口参数:无
* 出口参数:无
* 返回值:本次轮询中发生事件的 Socket 数量 int >= 0
* 限定条件:s_net_initialized == 1;必须在 netTask 上下文中调用
* 函数说明:1. GPIO 轮询 CH395F INT# 引脚电平(低电平有效),无需 EXTI 中断
* 2. 读取 GET_GLOB_INT_STATUS_ALL(2 字节版)获取所有 Socket 的中断状态
* 3. 处理连接/断开/数据接收/发送完成/超时等事件,更新 Socket 状态机
* 4. RECV 中断为电平触发,每次 net_poll() 只处理一批中断,避免主循环饿死
*/
int net_poll(void) {
/* 本次轮询累计发生的事件数(连接/断开/接收/发送/超时各计 1) */
int event_count = 0;
/* 全局中断状态寄存器值(CH395F GET_GLOB_INT_STATUS_ALL 为 16 位) */
/* 防御性检查:尚未初始化则不处理任何中断,直接返回 */
if (!s_net_initialized) {
return 0;
}
/* do-while(0) "单次批处理":每次调用只读取并处理一批中断,处理完立即返回,不循环等待 */
/* 避免 RECV 电平中断导致主循环(如 echo)饿死 */
/* 循环体内部用 break 提前退出(INT# 无效或无中断时) */
do
{
uint16_t int_status;
/* GPIO 轮询 INT# 引脚:低电平有效(有中断待处理),仅当低电平时才读取状态,减少不必要 SPI 事务 */
/* 采用 GPIO 轮询而非 EXTI 边沿中断;若 INT# 为高(无效),直接退出本次轮询 */
if (HAL_GPIO_ReadPin(CH395_INT_GPIO_Port, CH395_INT_Pin) == GPIO_PIN_SET) {
break;
}
/* 读取全局中断状态(2 字节):读操作本身会清除 CH395F 中断并使其拉高 INT#,
* 若此后仍有新中断,CH395F 会再次拉低 INT#,由下一次 net_poll() 处理 */
int_status = ch395f_get_glob_int_status_all();
/* F-DRV-13 接线:把全局位喂驱动——bit2 PHY_CHANGE 时驱动
* 重读 PHY 更新 link_up 缓存 + 变化计数,net 层在 reconciler 订阅留痕
* (运行期拔网线不再静默;不自动重配,重配仍走 UI/配置引擎路径) */
ch395f_glob_event_feed(int_status);
#if defined(NET_BENCH_CMD_LOG)
/* 仅状态跳变时记(电平型 RECV_OK 会每 poll 重复,全记会冲爆日志) */
static uint16_t s_bench_last_glob = 0xFFFF;
if (int_status != s_bench_last_glob) {
s_bench_last_glob = (uint16_t)int_status;
net_bench_cmd_log('P', -1, (int)int_status);
}
#endif
/* 无有效中断(读到的状态为 0),提前退出 */
if (int_status == 0) {
break;
}
/* 遍历所有 Socket,处理各自的中断 */
for (int i = 0; i < NET_MAX_SOCKETS; i++) {
/* 取本 Socket 控制块(直接用数组下标,无需 net_get_sock 的越界判断) */
net_sock_t *p_sock = &s_net_socks[i];
/* 全局中断状态(GET_GLOB_INT_STATUS_ALL 16 位):Socket n 的中断位即 bit(n+4)。
* Socket 0~3 在低位字节 bit4~bit7;Socket 4~7 在高位字节的 bit0~3
* (即 16 位值的 bit8~bit11)。对应位为 0 说明该 Socket 无中断,跳过以节省时间。 */
if (!(int_status & (1U << (i + 4U)))) {
continue;
}
/* 读取该 Socket 的中断状态寄存器(SINT) */
uint8_t sock_int = ch395f_get_sock_int_status(i);
#if defined(NET_BENCH_CMD_LOG)
static uint8_t s_bench_last_sint[NET_MAX_SOCKETS] = { 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF };
if (sock_int != s_bench_last_sint[i]) {
s_bench_last_sint[i] = sock_int;
net_bench_cmd_log('p', i, (int)sock_int);
}
#endif
/* 连接建立事件:交给状态机处理(监听态/客户端/数据 Socket 分别处理) */
if (sock_int & CH395F_SINT_STAT_CONNECT) {
handle_connect_event(p_sock);
event_count++;
}
/* 连接断开事件:更新状态机并触发断开回调 */
if (sock_int & CH395F_SINT_STAT_DISCONNECT) {
handle_disconnect_event(p_sock);
event_count++;
}
/* 接收数据就绪事件:仅通知上层,实际数据由 net_recv_locked 读出(避免重复读 recv_len) */
if (sock_int & CH395F_SINT_STAT_RECV_OK) {
handle_recv_event(p_sock);
event_count++;
}
/* 发送完成事件:通知上层发送缓冲已空,可继续发送 */
if (sock_int & CH395F_SINT_STAT_SEND_OK) {
handle_send_event(p_sock);
event_count++;
}
/* 发送缓冲区空闲:置 send_ready 标志,唤醒等待发送的逻辑 */
if (sock_int & CH395F_SINT_STAT_SENBUF_FREE) {
p_sock->send_ready = 1;
}
/* 超时事件:连接超时或收发超时,更新状态机 */
if (sock_int & CH395F_SINT_STAT_SOCK_TIMEOUT) {
handle_timeout_event(p_sock);
event_count++;
}
}
/* 处理完本批中断后退出 do-while(0):RECV 中断为电平触发,只要缓冲仍有数据 INT# 就保持低 */
/* 若继续循环会反复处理 RECV 中断,饿死主循环中的应用代码(如 echo 回显) */
/* 因此每次 net_poll() 只处理“一批”,剩余数据在后续轮询中读取 */
} while (0);
/* 单一事实源(I1):每轮全量刷新在用 Socket 的驱动状态缓存
* (唯一刷新点;E2 实测单次回读 18.1us,在用 5~6 个 ≈100us ≪ 1ms 轮询
* 周期)。reconciler / 探针 / 镜像降级决策点一律读缓存(
* ch395f_sock_state_cached),不再各自发状态 SPI。放在中断处理之后:
* 事件处理(handle_*_event)内部若再读状态,拿到的也是本轮最新值。 */
for (int i = 0; i < NET_MAX_SOCKETS; i++) {
if (s_net_socks[i].in_use != 0U) {
(void)ch395f_get_sock_state((uint8_t)i);
/* E-02 收口:驱动 sock_reset 状态机在 net_poll 步进
* (驱动头注释契约"在 net_poll 或测试循环里周期调用")。IDLE 槽位
* 纯 RAM 判返回 0 无 SPI;仅活跃复位机做短 SPI。作用:同步包装
* (net_sock_reset_sync)600ms 放弃后复位机不搁浅,芯片条件满足
* (对端回 FIN 等)时自动走完,槽位可复用(begin 不再恒返 -1)。 */
(void)ch395f_sock_reset_poll((uint8_t)i);
}
}
#if NET_U15_PROBE
/* E6 探测(宏说明见文件前部):RX 非空而 readable=0 = RECV_OK 事件未收到
* 的数据面形态。**两级确认去良性竞态**:数据刚到、本轮中断尚未读取的
* 竞态窗(≤1 个 poll)会在下一探测轮自然消失;事件未收到则持续存在直到
* 盲收/超时。候选轮标记 → 下一探测轮仍成立才计确认命中。排除 CLOSED:
* closed-with-data 是已知被救回形态,非 U15 假设对象。独立于上面的
* 缓存刷新环:GET_RECV_LEN 是独立 SPI。 */
s_u15_round++;
if ((s_u15_round % NET_U15_PROBE_PERIOD) == 0U) {
for (int i = 0; i < NET_MAX_SOCKETS; i++) {
uint16_t rlen;
if (s_net_socks[i].in_use == 0U) {
s_u15_cand[i] = 0U;
continue;
}
rlen = ch395f_get_recv_len((uint8_t)i);
if ((rlen > 0U) && (s_net_socks[i].readable == 0U) &&
(s_net_socks[i].state == NET_SOCK_STATE_ESTABLISHED)) {
if (s_u15_cand[i] != 0U) {
s_u15_cand[i] = 0U;
s_u15_hits++;
/* 首次命中必打(last_log=0 会被 uptime<1h 抑制);此后每小时 ≤1 条 */
if ((s_u15_hits == 1U) ||
((uint32_t)(HAL_GetTick() - s_u15_last_log) >= NET_U15_PROBE_LOG_MS)) {
s_u15_last_log = HAL_GetTick();
DBG_INFO("U15 probe: sock%d rx=%u readable=0 hits=%lu",
i, (unsigned int)rlen, (unsigned long)s_u15_hits);
}
} else {
s_u15_cand[i] = 1U;
}
} else {
s_u15_cand[i] = 0U;
}
}
}
#endif
/* 返回本次累计发生的事件数量(0 表示无事件) */
return event_count;
}
/*
* 函数功能:netTask 主循环核心 —— 自愈 tick 宿主 + 消息队列分派器。每轮先跑四个非阻塞
* 自愈 tick(D10 重监听 / 挂起 recv / 挂起 send / TRAP-NET-17 状态对账),
* 再非阻塞排空队列按 msg.type 分派到 _locked 实现。
* 入口参数:无
* 出口参数:无
* 返回值:无(void);消息结果经 xTaskNotify 通知值回传(成功=字节数/描述符,
* 失败=具体错误码),调用方 net_wait_result 拾取,对外统一 -1。
* 限定条件:必须在 netTask 上下文中调用(独占 CH395F 与 s_net_socks 访问权);
* 由 net_task.c 在 net_stack_bring_up 之后每次 1ms 循环调用。
* 函数说明:1. 四个自愈 tick 均非阻塞(绝不 osDelay 忙等,netTask 永不冻结),先于消息分派执行
* 2. net_listener_tick(监听重建**唯一执行者**):消费各路径登记的请求,
* 30ms 节奏 / 2s 预算;门集与"芯片在听就不 open"(U12 防护)都在这一处
* 3. net_pending_tick:按节拍结束挂起 recv(数据/到点[仅显式预算]/断连)
* 4. net_send_pending_tick:按节拍推进挂起 send(断连/推进/写完/无进展超时)
* 5. net_reconcile_tick(TRAP-NET-17 事件面自愈 + 监听重建合一):100ms 对账
* ①②⑤ + ③④ 重建 + U11 重同步,判据读驱动状态缓存;去抖分级 + 1s 冷却
* 6. net_msg_t 按值入队 → 结果只能搭任务通知值回传(32 位整体即结果,不做位域压缩)
* 7. RECV / SEND 特例:超预算登记 pending 哨兵 NET_PENDING_REGISTERED(0x70000000),
* do_notify=0,回执推迟到 net_pending_tick / net_send_pending_tick 完成时
*/
void net_process_messages(void) {
net_msg_t msg;
/* 队列句柄未就绪(net_init 未完成):直接返回 */
if (s_net_msg_queue == NULL) {
return;
}
/* 四个非阻塞自愈 tick 置于消息处理之前:断连后即使无应用消息,监听器也能自主恢复 */
/* 监听重建唯一执行者(TRAP-NET-09 D10 节奏;消费各路径登记的请求) */
net_listener_tick();
/* 挂起 recv 完成检查:数据到达 / 到点 / 断连时通知调用方 */
net_pending_tick(s_net_socks);
/* 挂起 send 推进与完成检查(SEND pending):断连 / 推进写块 / 写完 / 无进展超时 */
net_send_pending_tick(s_net_socks);
/* TRAP-NET-17 状态对账 + 监听重建合一(单一 100ms tick):
* 判据读驱动缓存(net_poll 每轮全量刷新),①②⑤ 失同步 + ③④ 重建 +
* U11 重同步全部在此(本层唯一周期重建入口,N-32 P2 单一所有者) */
net_reconcile_tick();
/* 队列排空:超时 0 非阻塞取,每条处理一轮,直到本轮队列为空 */
while (osMessageQueueGet(s_net_msg_queue, &msg, NULL, 0) == osOK) {
/* 默认处理完即发即时回执;RECV 挂起登记时置 0(回执推迟到
* net_pending_tick 完成检查,避免"登记成功"被误报为处理结果) */
int do_notify = 1;
switch (msg.type) {
case NET_MSG_SEND: {
/* 取控制块;NULL = fd 越界或已关闭 */
net_sock_t *p_sock = net_get_sock(msg.sockfd);
if (p_sock == NULL) {
/* 描述符无效 → BADF,结果 -1 */
net_set_errno(NET_ERR_BADF);
msg.result = -1;
} else if (p_sock->state != NET_SOCK_STATE_ESTABLISHED) {
/* 未建立连接(或已断开)→ NOTCONN */
net_set_errno(NET_ERR_NOTCONN);
msg.result = -1;
} else {
/* 真正写入芯片:1KB/块等 SENBUF_FREE 中断(§5.32/§5.23①),
* 单块等待超出快速路径预算(NET_SEND_SPIN_MS)即登记挂起请求、
* netTask 零占用返回;中途断连立即 CONNRESET;短写留痕,
* 调用方须检查返回值 */
msg.result = net_send_locked(p_sock, msg.buf, msg.len, msg.caller);
if (msg.result == NET_PENDING_REGISTERED) {
/* 挂起已登记:不发即时回执,完成时才通知(防"登记成功"误报为结果) */
do_notify = 0;
}
}
break;
}
case NET_MSG_RECV: {
/* 取控制块;NULL = fd 越界或已关闭 */
net_sock_t *p_sock = net_get_sock(msg.sockfd);
if (p_sock == NULL) {
/* 描述符无效 → BADF,结果 -1 */
net_set_errno(NET_ERR_BADF);
msg.result = -1;
} else {
/* 三条出口:数据就绪 → 即时 SPI 读出;DONTWAIT 无数据 → 0(对端关闭)
* / WOULDBLOCK;阻塞无数据 → 登记挂起请求(哨兵,netTask 零占用) */
msg.result = net_recv_locked(p_sock, msg.buf, msg.len, msg.flags,
msg.timeout_ms, msg.caller);
if (msg.result == NET_PENDING_REGISTERED) {
/* 挂起已登记:不发即时回执,完成时才通知(防"登记成功"误报为结果) */
do_notify = 0;
}
}
break;
}
case NET_MSG_CLOSE: {
/* 取控制块;NULL = fd 越界或已关闭 */
net_sock_t *p_sock = net_get_sock(msg.sockfd);
if (p_sock == NULL) {
/* 描述符无效 → BADF,结果 -1 */
net_set_errno(NET_ERR_BADF);
msg.result = -1;
} else {
/* 先结束本 fd 挂起 recv(排空或取消,必通知),再关芯片侧 Socket;
* 单连接模型监听 fd 断开后自动恢复监听,应用不得对它在 EOF 时 close */
msg.result = net_close_locked(p_sock);
}
break;
}
case NET_MSG_CONNECT: {
/* 保存对端地址(公开 API 已 memcpy 入 msg.addr)并配置芯片:
* open(失败重试 3 次)+ 源/目的端口 + 目的 IP,清残留中断,
* 下发 TCP CONNECT 并置 CONNECTING(异步,CONNECT 中断后才成立) */
msg.result = net_connect_locked(msg.sockfd,
(const struct net_sockaddr *)&msg.addr,
msg.addrlen);
break;
}
case NET_MSG_LISTEN: {
/* 芯片侧 open + TCP_LISTEN(失败重试 3 次),置 LISTENING 并启用
* auto_relisten(断开后自动重监听,模拟 BSD 监听常驻);单连接模型下
* 该 fd 后续即数据通道本身(g_client==g_listen) */
msg.result = net_listen_locked(msg.sockfd);
break;
}
case NET_MSG_ACCEPT: {
/* 对端地址指针与长度经 msg.buf/msg.len 字段携带(公开 API net_accept 填入) */
struct net_sockaddr *p_addr = (struct net_sockaddr *)msg.buf;
int addrlen = msg.len;
/* 仅在调用方给出有效 addr 且 addrlen>0 时,成功后回填对端地址长度 */
int *p_addrlen = (p_addr != NULL && addrlen > 0) ? &addrlen : NULL;
/* 单连接:ESTABLISHED 即有可接受连接(返回同一 fd,监听 Socket
* 自身已转数据通道);否则先尝试恢复监听,仍无则 WOULDBLOCK */
msg.result = net_accept_locked(msg.sockfd, p_addr, p_addrlen);
break;
}
case NET_MSG_DROP_CONN: {
/* E-02 收口:应用放弃会话的主动拆除(拆连接、保监听)。监听型
* 登记即返(N-17:拆链排渣交 net_listener_tick,本消息不再占用
* netTask 数百 ms) */
net_sock_t *p_sock = net_get_sock(msg.sockfd);
if (p_sock == NULL) {
/* 描述符无效 → BADF */
net_set_errno(NET_ERR_BADF);
msg.result = -1;
} else if (!p_sock->in_use || p_sock->type != NET_SOCK_STREAM) {
/* 不在用 / 非 TCP 流 → INVAL */
net_set_errno(NET_ERR_INVAL);
msg.result = -1;
} else if (p_sock->state != NET_SOCK_STATE_ESTABLISHED &&
p_sock->state != NET_SOCK_STATE_CONNECTING) {
/* 无活动会话:幂等成功(监听原样,不动芯片) */
msg.result = 0;
} else {
msg.result = net_drop_conn_locked(p_sock);
}
break;
}
/* F-NET-3:NET_MSG_RESYNC_LISTEN / NET_MSG_RELISTEN 两个
* 手动自愈消息已删——RESYNC 语义并入对账⑤(net_reconcile_tick),
* RELISTEN 与 reconciler/D10 完全重叠且对健康监听器是 footgun;
* 生产调用者本就为零(仅 test/board Phase 13,已改由自动机制接管) */
default:
/* 未知消息类型(枚举闭合,正常不应触发):置通用错误码与结果 -1,
* 保证通知值恒为本条消息的确定结果(无上一条残留) */
net_set_errno(NET_ERR);
msg.result = -1;
break;
}
#if defined(NET_BENCH_CMD_LOG)
{
/* 台架构建:每条消息留一行命令日志(R/S/C/A/L/N/M) */
char bench_tag = (msg.type == NET_MSG_RECV) ? 'R' :
(msg.type == NET_MSG_SEND) ? 'S' :
(msg.type == NET_MSG_CLOSE) ? 'C' :
(msg.type == NET_MSG_ACCEPT) ? 'A' :
(msg.type == NET_MSG_LISTEN) ? 'L' :
(msg.type == NET_MSG_CONNECT) ? 'N' : 'M';
/* 记录命令类型、fd 与结果(负错误码)供台架判读 */
net_bench_cmd_log(bench_tag, msg.sockfd, (int)msg.result);
}
#endif
/* 结果回传:消息体按值拷入队列,调用方看不到 netTask 对 msg 的修改
* → 只能搭 32 位任务通知值回来(整体即结果:位域会把上限压到
* 32767,TEST_BUF_SIZE=32768 会溢出为负) */
if (do_notify != 0) {
/* 成功:结果的 32 位原值(字节数 / 描述符) */
uint32_t notify_val = (uint32_t)msg.result;
/* 失败:回传本条消息**自身**的精确错误码(NET_ERR_WOULDBLOCK 等)。
* E-03 后取的是 netTask 自己的错误码槽位——锁定实现(*_locked)在
* 失败返回前记录的码,不受其他任务影响;未置位时回退通用 NET_ERR。 */
if (msg.result < 0) {
int te = s_net_errno_slots[net_errno_slot()].err;
notify_val = (uint32_t)((te != 0) ? te : NET_ERR);
}
/* 覆盖式写入通知值:调用方零等待拾取即得最新回执 */
xTaskNotify(msg.caller, notify_val, eSetValueWithOverwrite);
}
}
}
/*
* 函数功能:结果等待(两段式,防丢失通知;结果按 32 位原值回传)
* 入口参数:kind = 操作简称;sockfd = Socket(两者仅用于调试留痕,避免未用参数告警)
* 出口参数:无
* 返回值:netTask 回传的结果(成功=字节数/描述符;失败=负错误码)
* 限定条件:netTask 上下文(消息分派路径内部使用,非公开 API)
* 函数说明:1. 零等待先拾取:"入队→等待入口"间隙内的通知已在任务通知值里,
* 单段阻塞等待会错过它(2026-09-22 台架实证)
* 2. 清除掩码必须全 1:xTaskNotifyWait 以位掩码清值,传 0 = 不清除
* 3. 否则上次结果残留、被每次零等待重读成"本次结果"(run3~run11 L7 挂死/整段错位的真正根因)
* 4. 必须以返回值(pdTRUE/pdFALSE)判定是否已落地,不能判 v != 0
* 5. 合法的 0 结果(recv 0 = 对端关闭)会被误判成"无通知" → 阻塞等待 → 永久挂死
* 6. 通知值 32 位整体作为结果(不切分位域),保证 TEST_BUF_SIZE=32768 这类大单次收发不被截断
* 7. 禁止在本路径加逐调用日志:曾加 pick/block 两行,占日志 98.6%,
* 115200 下冲掉 [LAT] 证据行、并让 wait-idle 永远判不出空闲(run14 实证)
* 8. 诊断需求请在 [LAT] 段报告侧做聚合
*/
static int net_wait_result(const char *kind, int sockfd) {
/* 32 位结果值(任务通知值整体) */
uint32_t v = 0U;
/* kind/sockfd 仅供调用链调试留痕,函数体不直接使用 */
(void)kind;
(void)sockfd;
/* 零等待:拾取并消费"入队→此处"间隙内已落地的通知(含结果为 0 的通知) */
if (xTaskNotifyWait(0xFFFFFFFFU, 0xFFFFFFFFU, &v, 0) == pdTRUE) {
return (int)v;
}
/* 无通知在途:阻塞等待本消息回执(进入临界区的瞬间若有通知落地,
* xTaskNotifyWait 的状态位判定会立即返回,不会睡过去) */
xTaskNotifyWait(0xFFFFFFFFU, 0xFFFFFFFFU, &v, portMAX_DELAY);
return (int)v;
}
/*
* 函数功能:创建 Socket,分配空闲 Socket 控制块并初始化状态
* 入口参数:domain - 地址族,仅支持 NET_AF_INET int
* type - SOCK_STREAM(TCP) 或 SOCK_DGRAM(UDP) int
* 出口参数:无
* 返回值:Socket 描述符 (0~7),失败返回 -1 int
* 限定条件:net_init() 已调用成功;s_net_initialized == 1
* 函数说明:自动分配一个空闲的 Socket 控制块(in_use=0),初始化为 CREATED 状态
*/
int net_socket(int domain, int type) {
/* 新分配的 Socket 描述符 */
int sockfd;
/* 分配到的控制块指针 */
net_sock_t *p_sock;
/* 参数检查:地址族仅支持 NET_AF_INET */
if (domain != NET_AF_INET) {
/* 不支持的地址族 → INVAL */
return NET_ERR_RETURN_1(NET_ERR_INVAL);
}
/* 分配空闲控制块(in_use=0 的槽位) */
sockfd = alloc_socket();
if (sockfd < 0) {
/* 无空闲控制块 → NOMEM */
return NET_ERR_RETURN_1(NET_ERR_NOMEM);
}
/* 初始化控制块:芯片侧 Socket 编号与描述符一一对应 */
p_sock = &s_net_socks[sockfd];
/* 芯片侧 Socket 索引(CH395F 命令的 sock 参数) */
p_sock->ch395_sock = (uint8_t)sockfd;
/* 协议类型:SOCK_STREAM(TCP) / SOCK_DGRAM(UDP) */
p_sock->type = (uint8_t)type;
/* 初始状态 CREATED(TCP 待 bind+listen;UDP 在 bind 时直接开芯片侧) */
p_sock->state = NET_SOCK_STATE_CREATED;
/* 标记在用 */
p_sock->in_use = 1;
/* 发送缓冲初始为空闲(可发) */
p_sock->send_ready = 1;
/* 无可读数据 */
p_sock->readable = 0;
/* 阻塞模式默认 = 阻塞(BSD/Python 语义;非阻塞用
* net_setblocking(fd, 0) 显式设置)。close 的 memset 会清零本字段,
* 新 fd 必须在此显式置 1(同 released_abnormal 的"不得继承"纪律) */
p_sock->blocking = 1;
/* 新 fd 不得继承上一条连接的释放原因(TRAP-NET-18 待办 #3) */
p_sock->released_abnormal = 0;
/* N-32(P1):新 fd 不得继承"上一条会话已结束"电平(否则新建即 EOF,
* 消费侧连数据都收不到)——与 released_abnormal 同理,显式清零 */
p_sock->session_dead = 0;
/* 新 fd 不得继承上一次的待重试状态(D10) */
net_listener_clear(sockfd);
/* 新 fd 不得继承上一次挂起的 recv 请求 */
net_pending_clear(sockfd);
/* 新 fd 不得继承上一次挂起的 send 请求 */
net_send_pending_clear(sockfd);
/* 返回描述符(控制块已就绪) */
return sockfd;
}
/*
* 函数功能:绑定 Socket 到本地 IP 地址和端口号
* 入口参数:sockfd - Socket 描述符 (0~7) int
* addr - 本地地址结构体指针 const struct net_sockaddr *
* addrlen - 地址结构体长度 int >= sizeof(struct net_sockaddr_in)
* 出口参数:无
* 返回值:0 - 绑定成功 int
* -1 - 绑定失败(参数无效、Socket 未使用等) int
* 限定条件:Socket 已通过 net_socket() 创建且未被其他操作占用;net_init() 已调用
* 函数说明:1. TCP Socket 仅记录本地端口,进入 BOUND 状态等待 listen
* 2. UDP Socket(SOCK_DGRAM)立即打开 CH395F Socket 并配置为 Server 模式(Socket 4~7 需显式分配收发缓冲区)
*/
int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) {
/* Socket 控制块指针 */
net_sock_t *p_sock;
/* 本地地址(IPv4)强转 */
const struct net_sockaddr_in *p_addr_in;
/* 参数检查:描述符范围 */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
/* 越界 → BADF */
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 取控制块 */
p_sock = &s_net_socks[sockfd];
/* 控制块未在用(未创建/已关闭)→ BADF */
if (!p_sock->in_use) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 地址指针/长度无效 → INVAL */
if (addr == NULL || addrlen < (int)sizeof(struct net_sockaddr_in)) {
return NET_ERR_RETURN_1(NET_ERR_INVAL);
}
/* 地址强转为 IPv4 结构体 */
p_addr_in = (const struct net_sockaddr_in *)addr;
/* 设置本地端口(结构体中为网络序 → 转主机序) */
p_sock->local_port = net_ntohs(p_addr_in->sin_port);
/* port=0 时自动分配动态端口(49152~65535) */
if (p_sock->local_port == 0) {
/* 取当前动态端口值 */
p_sock->local_port = s_dynamic_port;
/* 推进动态端口,供下次分配 */
s_dynamic_port++;
/* 回绕:越过 65535 后回到动态区起始 */
if (s_dynamic_port == 0) {
s_dynamic_port = 49152;
}
}
/* UDP (SOCK_DGRAM):直接在此打开 CH395F Socket(无需等 listen) */
if (p_sock->type == NET_SOCK_DGRAM) {
/* open_socket 返回状态 */
uint8_t status;
/* net_init 已为全部 8 个 Socket 分配缓冲;此处仅对 Socket 4~7 兜底重设,
* 使用与 net_init 相同的布局(固定 6 块/Socket,与 MSS 解耦,见 net_init) */
if (sockfd >= 4) {
/* 每 Socket 块数 = 接收 + 发送 */
uint8_t blocks_per_socket = (uint8_t)(CH395F_RECV_BLOCKS + CH395F_SEND_BLOCKS);
/* 本 Socket 接收缓冲起始块 */
uint8_t recv_start = (uint8_t)(sockfd * blocks_per_socket);
/* 发送缓冲起始块(接收块之后) */
uint8_t send_start = (uint8_t)(recv_start + CH395F_RECV_BLOCKS);
/* (重)设置发送缓冲块 */
ch395f_set_send_buf((uint8_t)sockfd, send_start, (uint8_t)CH395F_SEND_BLOCKS);
/* (重)设置接收缓冲块 */
ch395f_set_recv_buf((uint8_t)sockfd, recv_start, (uint8_t)CH395F_RECV_BLOCKS);
}
/* 设置协议类型为 UDP */
ch395f_set_proto_type((uint8_t)sockfd, CH395F_PROTO_TYPE_UDP);
/* 设置本地(源)端口 */
ch395f_set_sour_port((uint8_t)sockfd, p_sock->local_port);
/* DesIP=0xFFFFFFFF:UDP Server 模式(接受任意来源) */
{
/* 任意源地址(全 0xFF) */
uint8_t broadcast[4] = {0xFF, 0xFF, 0xFF, 0xFF};
/* 目的 IP 设为任意源 */
ch395f_set_des_ip((uint8_t)sockfd, broadcast);
}
/* 目的端口 0(服务端不限定对端端口) */
ch395f_set_des_port((uint8_t)sockfd, 0);
/* 打开芯片侧 Socket */
status = ch395f_open_socket((uint8_t)sockfd);
if (status != CH395F_ERR_SUCCESS) {
/* open 失败:留痕并返回 BUSY(芯片忙/复位冲突) */
DBG_ERROR("sock%d open_socket FAIL: status=0x%02X (expected 0x00)", sockfd, status);
return NET_ERR_RETURN_1(NET_ERR_BUSY);
}
/* UDP 打开即可用(无监听态) */
p_sock->state = NET_SOCK_STATE_UDP_OPEN;
} else {
/* TCP 仅记录本地端口,待 listen 打开芯片侧 */
p_sock->state = NET_SOCK_STATE_BOUND;
}
/* 绑定成功 */
return 0;
}
/*
* 函数功能:驱动 sock_reset 原语的有界确定性排渣等待(排渣收口;
* 替代原"close + 盲等 500ms"式忙等)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* mode - CH395F_RESET_GRACEFUL(等 100ms)/ QUARANTINE(等 400ms)
* 出口参数:无
* 返回值:0 - 复位完成(芯片 byte0=00H、事件已排空);-1 - 启动失败或 600ms 超时 int
* 限定条件:仅 netTask 上下文(SPI 串行契约);调用前本层已放弃该会话
* 函数说明:1. 原语本身非阻塞(begin/poll 分步),本包装是**同步 API 路径**
* (listen open 重试 / close)用的有界等待:2ms 节奏 poll,600ms 封顶
* 2. 内部序列=E4 定案:读空 RX -> CLOSE -> 排空事件(quiescent) ->
* 轮询 byte0=00H;WAIT_CLOSE 内置 LISTEN 态二段 CLOSE 补发
* (E2 补充发现:一次 CLOSE 只拆监听 byte0 恒 05)
* 3. 超时留痕返 -1,调用方按原语义兜底(TRAP-NET-09 重试环 / close 放行)
*/
static int net_sock_reset_sync(int sockfd, ch395f_reset_mode_t mode) {
/* 起始时刻(600ms 上限基准) */
uint32_t t0 = HAL_GetTick();
/* 状态机步进返回值 */
int rc;
/* 启动状态机(本 fd 已在运行返 -1:netTask 串行上下文不应发生) */
if (ch395f_sock_reset_begin((uint8_t)sockfd, mode) != 0) {
return -1;
}
/* 有界等待:2ms 节奏步进(每步仅短 SPI 事务,netTask 无长忙等) */
for (;;) {
rc = ch395f_sock_reset_poll((uint8_t)sockfd);
/* 0=完成 / -1=失败(槽位已复位可重开):都退出等待 */
if (rc != 1) {
break;
}
if ((uint32_t)(HAL_GetTick() - t0) > 600U) {
DBG_ERROR("sock%d sock_reset_sync timeout (600ms)", sockfd);
return -1;
}
osDelay(2);
}
return (rc == 0) ? 0 : -1;
}
/*
* 函数功能:启动 TCP Server 监听(内部版本,必须在 netTask 上下文调用)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* 出口参数:无
* 返回值:0 - 成功,-1 - 失败 int
* 限定条件:sockfd 已绑定且为 TCP;netTask 上下文
* 函数说明:1. 芯片侧 open + TCP_LISTEN(端口=控制块 local_port;open 失败重试 3 次,见 TRAP-NET-09)
* 2. 成功置 LISTENING 并启用 auto_relisten(BSD 风格监听常驻)
* 3. 启动监听前清**本 Socket** 中断状态寄存器(读即清),防残留误触发;
* 全局中断寄存器交 net_poll 集中读清(B3,详见函数体内注释)
*/
int net_listen_locked(int sockfd) {
/* 取监听 Socket 控制块(直接下标访问,sockfd 已在上层 net_listen 校验过范围) */
net_sock_t *p_sock = &s_net_socks[sockfd];
/* CH395F 打开 Socket 的返回状态 */
uint8_t status;
/* 合法性检查:Socket 必须已创建且为 TCP 流类型 */
if (!p_sock->in_use || p_sock->type != NET_SOCK_STREAM) {
return NET_ERR_RETURN_1(NET_ERR_INVAL);
}
/* 状态检查:仅允许在 BOUND(已 bind)或 CREATED(未 bind 直接用随机端口)状态下启动监听 */
if (p_sock->state != NET_SOCK_STATE_BOUND &&
p_sock->state != NET_SOCK_STATE_CREATED) {
return NET_ERR_RETURN_1(NET_ERR_INVAL);
}
/* 单连接:监听 Socket 自身即数据通道;收发缓冲已在 net_init 为全部
* 8 个 Socket 显式重分配(6 块/Socket,net_socket.c:339-345),此处不再分配 */
/* 设置协议类型为 TCP */
ch395f_set_proto_type(sockfd, CH395F_PROTO_TYPE_TCP);
/* 本地端口 = 控制块 local_port */
ch395f_set_sour_port(sockfd, p_sock->local_port);
/* CH395F 关闭 Socket 后需延时才能重新打开(TRAP-NET-09),故失败重试最多 3 次。
* 排渣收口:原"close + 盲等 500ms"改**确定性排渣**——
* sock_reset(读空 RX -> CLOSE -> 排空事件 -> 等 byte0=00H,内置 LISTEN
* 态二段 CLOSE 补发;E4 定案 60 轮实证复位后 open 必成)。GRACEFUL
* 最短等 100ms ≪ 原 500ms 盲等;复位失败/超时仍走下一轮重试,TRAP-NET-09 语义保留 */
for (int open_try = 0; open_try < 3; open_try++) {
/* 尝试打开芯片侧 Socket */
status = ch395f_open_socket(sockfd);
if (status == CH395F_ERR_SUCCESS) {
/* 打开成功,退出重试 */
break;
}
/* 打开失败:确定性复位本 Socket(排渣/二段关闭/等 CLOSED 一体) */
(void)net_sock_reset_sync(sockfd, CH395F_RESET_GRACEFUL);
}
/* 3 次仍失败,返回错误 */
if (status != CH395F_ERR_SUCCESS) {
return NET_ERR_RETURN_1(NET_ERR_BUSY);
}
/* 排事件(读即清)必须在 LISTEN 命令**之前**(E14 复核实验 2026-10-14:
* 原形态为 listen 后读清——快速复连时新连接的 CONNECT 在 LISTEN 生效后、
* 读清前落进 SINT,被本方读清吞掉,即"CONNECT 事件未收到→数据被吞"的
* 生产侧自伤机制(台架矩阵 3/48 被 drain-int 探针现场抓获);芯片侧
* 从不丢事件。挪前后若仍有滞后 DISCONNECT 落进新会话,
* handle_disconnect_event 的 LISTENING 分支回读状态裁决(状态面为真相),
* 无害。
* 读即清:仅本 Socket 的中断状态——丢弃上一条会话残留的 DISCONNECT/
* TIMEOUT,避免监听刚开始就误触发事件。
* ⚠ B3(2026-10-12 审计):此处原为"遍历 8 个 Socket 全清 + 再读清全局",
* git 考古证明是多连接时代的调试遗留(e862009bc 无注释引入 → 319d00e3
* 原样搬进单连接模型 → db64e6acf 又复制进 net_relisten_locked),不属于
* 任何已登记 Trap 的修复。其危害:§5.15 明文全局中断状态"整体读即清"
* (芯片无按位清除手段),而 net_poll 只在**全局位置位时**才去读该
* Socket 的 SINT(见 :469 门控)→ 全量清等于把别的在用 Socket 的门一并
* 焊死,其 CONNECT/DISCONNECT 等边沿事件永久丢失(RECV 属条件位,下轮
* 可补回,见 TRAP-NET-08)。
* 全局寄存器交 net_poll 集中读清(AGENTS 模块约定:所有 CH395F 中断经
* net_poll 集中读取并清除);本 Socket 残留的全局位被 net_poll 读到时
* SINT 已为 0,自然收尾。TRAP-NET-12/-18 指定的两级预清落在
* net_connect_locked / net_close_locked / relisten 隔离段,均为单 Socket,
* 本次未动。 */
(void)ch395f_get_sock_int_status((uint8_t)sockfd);
/* 下发 TCP LISTEN 命令,进入监听状态 */
status = ch395f_tcp_listen(sockfd);
/* LISTEN 失败 → BUSY */
if (status != CH395F_ERR_SUCCESS) {
/* U12 对称:open 已成而 LISTEN 失败必须 close,
* 否则芯片 Socket 卡"OPEN 无监听"、后续 open 恒返 USE(同
* net_relisten_locked 对应分支) */
ch395f_close_socket(sockfd);
return NET_ERR_RETURN_1(NET_ERR_BUSY);
}
/* 监听 Socket 进入 LISTENING 态 */
p_sock->state = NET_SOCK_STATE_LISTENING;
/* 单连接模式:连接断开后由 net 层自动回到 LISTEN(模拟 BSD 监听常驻) */
p_sock->auto_relisten = 1;
/* N-30(TRAP-NET-20):新监听会话,事件未收到去抖计数清零 */
p_sock->lost_conn_seen = 0U;
/* 监听建立完成 */
return 0;
}
/*
* 函数功能:启动 TCP Server 监听(线程安全版本,通过消息队列委托给 netTask)
* 入口参数:sockfd - Socket 描述符 int
* 出口参数:无
* 返回值:0 - 成功,-1 - 失败 int
* 限定条件:net_init() + MX_FREERTOS_Init() 已执行;必须在任务上下文中调用
* 函数说明:通过消息队列委托给 netTask,确保 SPI 串行化
*/
int net_listen(int sockfd) {
/* 消息体(按值拷入队列,netTask 改的是其副本) */
net_msg_t msg;
/* 描述符越界 → BADF */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 消息队列未创建(网络层未初始化)→ 通用错误 */
if (s_net_msg_queue == NULL) {
return NET_ERR_RETURN_1(NET_ERR);
}
/* 消息类型:启动 TCP 监听 */
msg.type = NET_MSG_LISTEN;
/* 目标 Socket */
msg.sockfd = sockfd;
/* 无超时(监听立即完成) */
msg.timeout_ms = 0U;
/* 调用方任务句柄(netTask 凭它回传回执) */
msg.caller = xTaskGetCurrentTaskHandle();
/* 结果占位(netTask 侧覆写实际结果) */
msg.result = -1;
/* 入队:队列满时阻塞调用方直至有空位 */
if (osMessageQueuePut(s_net_msg_queue, &msg, 0, osWaitForever) != osOK) {
/* 入队失败 → BUSY */
return NET_ERR_RETURN_1(NET_ERR_BUSY);
}
/* 阻塞等待 netTask 回执(两段式拾取,防丢失通知) */
msg.result = net_wait_result("lis", sockfd);
if (msg.result < 0) {
/* netTask 把具体错误码(如 NET_ERR_WOULDBLOCK)以负值搭通知值回传,此处恢复为
* errno;对外仍保持 BSD 语义——失败一律返回 -1 */
return NET_ERR_RETURN_1(msg.result);
}
/* 返回结果(成功 = 描述符/字节数) */
return msg.result;
}
/*
* 函数功能:TCP Server 接受新连接(内部版本,必须在 netTask 上下文调用)
* 入口参数:sockfd - 监听 Socket 描述符 int
* 出口参数:addr - 输出:客户端地址结构体指针 struct net_sockaddr *(可为 NULL)
* addrlen - 输入输出:地址结构体长度 int *(可为 NULL)
* 返回值:新连接的 Socket 描述符,失败返回 -1 int
* 限定条件:Socket 处于 LISTENING 状态
* 函数说明:单连接:监听 Socket 自身在 CONNECT 后已转为 ESTABLISHED
*/
/* ===== N-30 accept 路径"CONNECT 事件未收到补 accept"(2026-10-11 台架实证;
* E14 复核 2026-10-14:事件非芯片侧丢失,是被读清消费——机制不变)=====
* 台架结论(剥离 OS/net/FTP 直接对芯片做受控 close→open→listen→connect 矩阵):
* 芯片关闭连接后的拆链收尾窗口**变长且不可见**(状态寄存器显示 CLOSED 也不代表
* 拆完,实测 >2s 仍可能未拆完);该窗口内重新 listen 后,新连接握手照常完成
* (芯片状态寄存器 → ESTABLISHED)但 **CONNECT 事件未收到**(E14 复核:被读清
* 消费竞态吞掉)——纯事件驱动的 accept
* 永远等不到 → 数据被吞 / 425。固定沉降延时、Socket 轮换、listen 后 close 重试、
* 缓冲重分配均已被实验证伪;唯一有效 = "中断 + 状态轮询"双信源(修复组 15/15
* 全过,机制触发 5 次全部救回)。→ CH395F_Trap_Records.md TRAP-NET-20
* 幽灵态甄别:拆链窗口内旧连接未拆净,状态如实为 ESTABLISHED(E15:bit3=1 保留现场
* 契约,非谎报),此时回读
* 对端端口 = 上一条连接端口;新连接临时端口几乎必然不同 → 端口相同不 accept,
* 等旧连接拆净 / 新 SYN 到达(新连接若事件也未收到,后续轮询以新端口命中)。 */
/*
* 函数功能:TCP Server 接受新连接(内部版本,必须在 netTask 上下文调用)
* 入口参数:sockfd - 监听 Socket 描述符 int
* 出口参数:addr - 输出:客户端地址结构体指针 struct net_sockaddr *(可为 NULL)
* addrlen - 输入输出:地址结构体长度 int *(可为 NULL)
* 返回值:新连接的 Socket 描述符,失败返回 -1 int
* 限定条件:Socket 处于 LISTENING 状态
* 函数说明:1. 单连接:监听 Socket 自身在 CONNECT 后已转为 ESTABLISHED
* 2. 等待期叠加芯片状态轮询探针(CONNECT 事件未收到自愈,TRAP-NET-20)
* 3. CLOSED + 芯片 RX 缓冲有残留数据:接受"已关但可读"连接排空
* (FUN_PARA §5.52 ② 保留现场契约,TRAP-NET-20 三次定案)
*/
static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen) {
/* 监听 Socket 控制块 */
net_sock_t *p_listen_sock;
/* 取控制块(sockfd 已在上层校验范围) */
p_listen_sock = &s_net_socks[sockfd];
/* 控制块未在用 → INVAL */
if (!p_listen_sock->in_use) {
return NET_ERR_RETURN_1(NET_ERR_INVAL);
}
/* 先轮询一次:把状态机刷新到芯片侧最新状态 */
net_poll();
/* N-30(TRAP-NET-20):监听等待期兼做芯片状态第二信源——CONNECT 事件未收到时
* 去抖补 accept(台架实证:拆链窗口事件未收到是数据被吞/425 的主根因,
* E14 复核:事件非芯片丢失、被读清消费吞掉;纯事件等待无自愈可能;
* 详见 net_lost_connect_probe 区块注释) */
if (p_listen_sock->state == NET_SOCK_STATE_LISTENING) {
net_lost_connect_probe(p_listen_sock);
}
/* 单连接:本 Socket 自身在 CONNECT 后处于 ESTABLISHED 才表示有可接受的连接
* (含上一步探针补 accept 的命中);其余失同步由 net_reconcile_tick()
* 周期对账(带去抖)兜底 */
if (p_listen_sock->state != NET_SOCK_STATE_ESTABLISHED) {
/* N-30(2026-10-11 手册 §5.23⑤ + 生产时间线定案):
* "closed-but-readable" 接受——STOR 类传输客户端**发完数据立即关数据连接**
* (正常 FTP 行为,Python ftplib sendall 后即 close),芯片按
* FUN_PARA **bit3 SOCKET_CLOSE=1**(`0x08`,§5.52 ②)规格保留 Socket 与 RX 缓冲供 MCU 排空
* (实测 DISCONNECT 后 st=0x05/0x00、recv_len=1027=完整载荷)。
* 若 accept 轮询晚于对端 FIN,盲目 relisten(close+open 清缓冲)会把
* 已到达的数据销毁 → 425(此前 N-30"425/被吞"的主根因,并非芯片杀连接)。
* 处置:芯片 RX 缓冲有数据 → 接受这条"已关但可读"连接,应用经
* net_recv_locked 快路径(无状态门槛)排空,排空后 EOF 自然触发 relisten。 */
int drain_pending = 0;
if (p_listen_sock->state == NET_SOCK_STATE_CLOSED && p_listen_sock->auto_relisten) {
if (ch395f_get_recv_len((uint8_t)sockfd) > 0) {
drain_pending = 1;
DBG_INFO("sock%d closed-with-data accept (drain pending)", sockfd);
} else {
/* 连接已断开(CLOSED)且无残留数据:登记重建请求,下一次 accept
* 即可等待新连接(BSD 风格监听常驻)。
* N-32 P2 修正:**保留本处即时登记** —— 它由消费者调 accept 触发
* (消费者正在等新连接),不是周期性抢先;删掉会把 CLOSED 窗口
* 拉长、导致调用方高频空转 accept(实测 [WD] task stall)。
* 单一所有者收口后:动作发生在下一轮 net_listener_tick(≤1ms),非阻塞 */
net_listener_request(sockfd, NET_LISTENER_REQ_ACCEPT);
}
}
if (drain_pending == 0) {
/* 当前无连接 → WOULDBLOCK */
return NET_ERR_RETURN_1(NET_ERR_WOULDBLOCK);
}
/* drain_pending:落入下方 accept 返回(对端簿记仍在控制块,排空后以 EOF 收尾) */
}
/* 填充对端地址(若调用方需要) */
if (addr != NULL && addrlen != NULL) {
/* 强转为 IPv4 地址结构体 */
struct net_sockaddr_in *p_addr_in = (struct net_sockaddr_in *)addr;
/* 地址族 = IPv4 */
p_addr_in->sin_family = NET_AF_INET;
/* 对端端口(主机序 → 网络序) */
p_addr_in->sin_port = net_htons(p_listen_sock->remote_port);
/* 对端 IP(控制块按网络序保存) */
p_addr_in->sin_addr.s_addr = p_listen_sock->remote_ip;
/* 回填结构体长度 */
*addrlen = sizeof(struct net_sockaddr_in);
}
/* 单连接:返回的就是监听 Socket 自身(已转为数据通道) */
return sockfd;
}
/*
* 函数功能:TCP Server 接受新连接(线程安全版本,通过消息队列委托给 netTask)
* 入口参数:sockfd - 监听 Socket 描述符 int
* 出口参数:addr - 输出:客户端地址结构体指针 struct net_sockaddr *(可为 NULL)
* addrlen - 输入输出:地址结构体长度 int *(可为 NULL)
* 返回值:新连接的 Socket 描述符,失败返回 -1 int
* 限定条件:Socket 处于 LISTENING 状态;net_init() + MX_FREERTOS_Init() 已执行;非 netTask 上下文
* 函数说明:将接受请求放入消息队列,阻塞等待 netTask 处理完毕
*/
int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) {
/* 消息体(按值拷入队列,netTask 改的是其副本) */
net_msg_t msg;
/* 描述符越界 → BADF */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 消息队列未创建(网络层未初始化)→ 通用错误 */
if (s_net_msg_queue == NULL) {
return NET_ERR_RETURN_1(NET_ERR);
}
/* 消息类型:接受新连接 */
msg.type = NET_MSG_ACCEPT;
/* 监听 Socket */
msg.sockfd = sockfd;
/* 对端地址指针经 buf 字段携带 */
msg.buf = (void *)addr;
/* addrlen 经 len 字段携带(不回填时记 0) */
msg.len = (addrlen != NULL) ? *addrlen : 0;
/* 无超时(accept 立即完成) */
msg.timeout_ms = 0U;
/* 调用方任务句柄(netTask 凭它回传回执) */
msg.caller = xTaskGetCurrentTaskHandle();
/* 结果占位(netTask 侧覆写实际结果) */
msg.result = -1;
/* 入队:队列满时阻塞调用方直至有空位 */
if (osMessageQueuePut(s_net_msg_queue, &msg, 0, osWaitForever) != osOK) {
/* 入队失败 → BUSY */
return NET_ERR_RETURN_1(NET_ERR_BUSY);
}
/* 阻塞等待 netTask 回执(两段式拾取,防丢失通知) */
msg.result = net_wait_result("acc", sockfd);
/* 成功时回填地址结构体长度供调用方读取 */
if (addrlen != NULL &&addr != NULL) {
*addrlen = sizeof(struct net_sockaddr_in);
}
if (msg.result < 0) {
/* netTask 把具体错误码(如 NET_ERR_WOULDBLOCK)以负值搭通知值回传,此处恢复为
* errno;对外仍保持 BSD 语义——失败一律返回 -1 */
return NET_ERR_RETURN_1(msg.result);
}
/* 返回结果(成功 = 新连接描述符) */
return msg.result;
}
/* F-NET-4:net_can_accept 已删除——它只认 ESTABLISHED,作为 accept
* 门控会把"监听器在服役但本轮还没 CONNECT"的窗口全挡掉(:2404 CONNECT 事件补 accept 探针
* 因此从不运行,与已修的 :7003 探针失效同型);唯一生产调用者 iec104 已迁
* net_listener_armed。net_can_read(RECV 门控)不受影响、保留。 */
/*
* 函数功能:查询监听 Socket 是否处于"应该尝试 accept"的就绪态(纯 RAM 读,不发起 RPC)
* 入口参数:sockfd - 监听 Socket 描述符 int
* 出口参数:无
* 返回值:1 - 就绪(LISTENING,或 CLOSED + auto_relisten = 断连待重建态);
* 0 - 未就绪(fd 未分配 / 非监听型 / 正处隔离期以外的不就绪态) int
* 限定条件:任意任务上下文;net_init() 已调用
* 函数说明:1. 不变量 II(单一重建所有者):取代已退役的 net_can_accept()(只认
* ESTABLISHED,F-NET-4 删除)作为监听 accept 门控。原门控下
* :7003 永不进 net_accept →
* net_lost_connect_probe(CONNECT 事件未收到补 accept)对该端口从不运行
* (F-NET-4)
* 2. 为什么放行 LISTENING:探针与"closed-but-readable 接受"(:1338)
* 只在 net_accept_locked 内部发生,门控挡在外面就永远到不了;
* LISTENING 态 accept 由探测补 accept 或返 WOULDBLOCK,均安全
* 3. 为什么放行 CLOSED + auto_relisten:单连接模型断连后 fd 停在
* CLOSED 等 relisten(D10 非阻塞重试 / 对账④),此窗口内新 SYN
* 可能已被芯片接住(对账②)→ 放行让 net_accept_locked 的状态门
* 自行裁决(含 RX 有数据的 drain_pending 接受)
* 4. 只读控制块,不入队、不等通知 → uiTask 不被 netTask 拖住
* 5. 与已退役 net_can_accept 的语义差异是**有意的**:本函数表达"这个监听器
* 还在服役",而非"这一轮一定有连接"
*/
int net_listener_armed(int sockfd) {
/* Socket 控制块(纯 RAM 读,可能为 NULL = 描述符越界或未分配) */
const net_sock_t *p_sock = net_get_sock(sockfd);
if (p_sock == NULL) {
return 0;
}
/* 连接已到、应用尚未 accept:单连接模型下 CONNECT 处理(net_poll 分派 /
* 探针补 accept / 对账② standalone accept)把监听 fd 转 ESTABLISHED,
* 这就是"待 accept"电平——**必须放行**(原 net_can_accept 唯一放行态;
* i104 台架实证:200ms 任务周期抢不过对账②的 ~400ms 去抖,
* 对账②先转 ESTABLISHED → 不放行则应用永不 accept、会话整场饿死)。
* 无 RPC 风暴风险:此态下 accept 必返 fd,一次调用即离开本分支 */
if (p_sock->state == NET_SOCK_STATE_ESTABLISHED) {
return 1;
}
/* 正常监听态:放行。net_lost_connect_probe(CONNECT 事件未收到补 accept)与常规
* accept 都发生在 net_accept_locked 内部,门控挡在外面就永远到不了 */
if (p_sock->state == NET_SOCK_STATE_LISTENING) {
return 1;
}
/* 断连待重建态:**仅当芯片 RX 可能仍有残留数据**时放行,让
* net_accept_locked 的 "closed-but-readable" 接受(N-30,对端"发完即 FIN")
* 仍能拿到该连接。
* ⚠ 无残留时必须返回 0 —— 否则调用方会在整个 CLOSED 窗口内每轮都发起
* net_accept(RPC:入队 + xTaskNotifyWait 回执**无上界**)→ 队列积压 →
* 调用方任务喂不上狗。台架实测:删掉即时登记重建后 CLOSED
* 窗口被拉长到 reconciler 周期 + 隔离期(~100ms),:21 0ms×50 期间出现 48 条
* `[WD] task stall`(mask=0x67/req=0x77,缺 bit4=UI)。
* 重建本身由 net_reconcile_tick 形态③④ 负责,调用方**无需**在此催促。
* readable 由 net_poll 在 RECV_OK 置位,"对端发完即 FIN"场景下 CLOSED 之后
* 仍为 1(无人清除),可作"可能仍有残留"的纯 RAM 判据(不发起 SPI)。 */
if ((p_sock->state == NET_SOCK_STATE_CLOSED) &&
(p_sock->auto_relisten != 0U) &&
(p_sock->local_port != 0U) &&
(p_sock->readable != 0U)) {
return 1;
}
return 0;
}
/*
* 函数功能:查询 Socket 是否"值得发一次 recv"(纯 RAM 读,不发起 RPC)
* 入口参数:sockfd - Socket 描述符 int
* 出口参数:无
* 返回值:1 - 应发 recv;0 - 当前无数据可读,可跳过本轮 int
* 限定条件:任意任务上下文;net_init() 已调用
* 函数说明:1. ESTABLISHED → 看 readable(net_poll 在 RECV_OK 置位、读空清零)
* 2. 非 ESTABLISHED / 描述符失效一律返回 1:必须让上层真的 recv 一次
* 拿到 EOF/错误并复位自身状态,门掉它会把"连接已不存在"永久藏住
* 3. ⚠ readable 仅由 RECV_OK 中断置位,TRAP-NET-17 对账不含"RX 非空但
* readable=0" → 事件未收到时本函数恒返 0,调用方须保留低频兜底 recv
*/
int net_can_read(int sockfd) {
/* Socket 控制块(纯 RAM 读,可能为 NULL = 描述符越界或未分配) */
const net_sock_t *p_sock = net_get_sock(sockfd);
/* 描述符已失效:让 recv 去报 BADF,走调用方既有错误分支重建,勿静默卡死 */
if (p_sock == NULL) {
return 1;
}
/* 连接已非 ESTABLISHED(CLOSED/LISTENING/…):需要发一次 recv 取回 EOF/错误 */
if (p_sock->state != NET_SOCK_STATE_ESTABLISHED) {
return 1;
}
return (p_sock->readable != 0U) ? 1 : 0;
}
/*
* 函数功能:TCP Client 发起与服务器的连接(线程安全版本,通过消息队列委托给 netTask)
* 入口参数:sockfd - Socket 描述符 (0~7),必须为已创建的 TCP Socket int
* addr - 服务器地址结构体指针 const struct net_sockaddr *
* addrlen - 地址结构体长度 int >= sizeof(struct net_sockaddr_in)
* 出口参数:无
* 返回值:0 - 连接请求发送成功(异步,需等待 ESTABLISHED) int
* -1 - 连接失败(参数无效、状态错误等) int
* 限定条件:Socket 已通过 net_socket() 创建且 type == NET_SOCK_STREAM;尚未连接
* 函数说明:1. 将连接请求放入消息队列,阻塞等待 netTask 处理完毕
* 2. 实际的 SPI 操作在 netTask 上下文中执行,避免与 net_poll() 的 SPI 访问竞争(否则会破坏 CH395F 寄存器)
* 3. 连接建立需等待 net_poll() 检测到 CONNECT 事件并将状态更新为 ESTABLISHED
*/
int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen) {
/* 消息体(按值拷入队列,netTask 改的是其副本) */
net_msg_t msg;
/* 描述符越界 → BADF */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 消息队列未创建(网络层未初始化)→ 通用错误 */
if (s_net_msg_queue == NULL) {
return NET_ERR_RETURN_1(NET_ERR);
}
/* 服务器地址无效 → INVAL */
if (addr == NULL || addrlen < (int)sizeof(struct net_sockaddr_in)) {
return NET_ERR_RETURN_1(NET_ERR_INVAL);
}
/* 消息类型:发起 TCP 连接 */
msg.type = NET_MSG_CONNECT;
/* 目标 Socket(须为已创建的 TCP Socket) */
msg.sockfd = sockfd;
/* buf/len/flags 在 connect 中不使用(显式置空) */
msg.buf = NULL;
msg.len = 0;
msg.flags = 0;
/* 服务器地址拷入消息(队列按值拷贝,指针传不过去) */
memcpy(&msg.addr, addr, sizeof(struct net_sockaddr_in));
/* 地址长度 */
msg.addrlen = addrlen;
/* 无超时(connect 为异步命令,成败由中断裁定) */
msg.timeout_ms = 0U;
/* 调用方任务句柄(netTask 凭它回传回执) */
msg.caller = xTaskGetCurrentTaskHandle();
/* 结果占位(netTask 侧覆写实际结果) */
msg.result = -1;
/* 入队:队列满时阻塞调用方直至有空位 */
if (osMessageQueuePut(s_net_msg_queue, &msg, 0, osWaitForever) != osOK) {
/* 入队失败 → BUSY */
return NET_ERR_RETURN_1(NET_ERR_BUSY);
}
/* 阻塞等待 netTask 回执(两段式拾取,防丢失通知) */
msg.result = net_wait_result("con", sockfd);
if (msg.result < 0) {
/* netTask 把具体错误码(如 NET_ERR_WOULDBLOCK)以负值搭通知值回传,此处恢复为
* errno;对外仍保持 BSD 语义——失败一律返回 -1 */
return NET_ERR_RETURN_1(msg.result);
}
/* 返回结果(0 = 连接命令已下发,ESTABLISHED 须以状态轮询等待) */
return msg.result;
}
/*
* 函数功能:TCP Client 发起连接(netTask 内部锁定版本,由消息队列委托执行)
* 入口参数:sockfd - Socket 描述符 (0~7),必须为已创建的 TCP Socket int
* addr - 服务器地址结构体指针 const struct net_sockaddr *
* addrlen - 地址结构体长度 int >= sizeof(struct net_sockaddr_in)
* 出口参数:无
* 返回值:0 - 连接请求已发出(异步,成功以 CONNECT 事件为准);-1 - 失败 int
* 限定条件:net_init() 已调用;必须在 netTask 上下文中调用
* 函数说明:1. 校验控制块(in_use、TCP 类型、地址参数)并保存对端地址
* 2. 芯片侧 OPEN(失败重试 3 次,每次失败走 sock_reset 确定性排渣,
* 见 TRAP-NET-09)+ 配置源/目的端口与目的 IP
* 3. 清本 Socket 残留中断状态(丢弃上一条连接遗留的 DISCONNECT)
* 4. 发起 TCP_CONNECT(驱动内已轮询 CMD_STATUS)并置 CONNECTING;
* 命令级被拒立即关 Socket 返 -1,TCP 层成败仍由 CONNECT/TIMEOUT 裁定
*/
static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int addrlen) {
/* Socket 控制块指针 */
net_sock_t *p_sock;
/* 服务器地址(IPv4)强转 */
const struct net_sockaddr_in *p_addr_in;
/* 芯片命令返回状态 */
uint8_t status;
/* 参数检查:描述符范围 */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
/* 越界 → BADF */
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 取控制块 */
p_sock = &s_net_socks[sockfd];
/* 未在用或非 TCP 流类型 → INVAL */
if (!p_sock->in_use || p_sock->type != NET_SOCK_STREAM) {
return NET_ERR_RETURN_1(NET_ERR_INVAL);
}
/* 服务器地址无效 → INVAL */
if (addr == NULL || addrlen < (int)sizeof(struct net_sockaddr_in)) {
return NET_ERR_RETURN_1(NET_ERR_INVAL);
}
/* 地址强转为 IPv4 结构体 */
p_addr_in = (const struct net_sockaddr_in *)addr;
/* 保存远端地址(IP 按网络序、端口转主机序) */
p_sock->remote_ip = p_addr_in->sin_addr.s_addr;
p_sock->remote_port = net_ntohs(p_addr_in->sin_port);
/* 转换 IP 为数组格式(芯片要的是**线序**数组:[0]=第一段,与 ch395f_set_des_ip
* 期望一致,同 Phase2 phase2_tc201 的 test_parse_ip 结果 [192,168,1,2])。
* s_addr 是网络序整数,其**内存字节顺位**就是线序(2026-10-24 定案:全仓 s_addr 只此一族),
* 所以这里按字节顺位取、不做数值右移:旧写法 remote_ip_arr[0] = remote_ip >> 24
* 取的是数值的最高字节,只配得上当时那套「第一段在最高字节」的 inet_addr;
* inet_addr 已改成 POSIX 网络序后,右移写法会把地址拆成 [100,1,168,192] 反序,
* 表现为「连谁都不知去哪了」。用 memcpy 取顺位与 net_events.c 填 remote_ip_arr
* 的写法同一形制(那边一直就是线序顺位),两端不再各说一套。 */
(void)memcpy(p_sock->remote_ip_arr, &p_sock->remote_ip,
sizeof(p_sock->remote_ip_arr));
/* 配置 CH395F Socket:先 OPEN 再写源/目的端口(避免 open 复位寄存器) */
/* 设置协议类型为 TCP */
ch395f_set_proto_type(sockfd, CH395F_PROTO_TYPE_TCP);
/* CH395F 关闭 Socket 后需延时才能重新打开(TRAP-NET-09),故失败重试最多 3 次。
* 排渣收口:原"close + 盲等 500ms"改**确定性排渣**——
* sock_reset(读空 RX -> CLOSE -> 排空事件 -> 等 byte0=00H,内置 LISTEN
* 态二段 CLOSE 补发;E4 定案 60 轮实证复位后 open 必成)。GRACEFUL
* 最短等 100ms ≪ 原 500ms 盲等;复位失败/超时仍走下一轮重试,TRAP-NET-09 语义保留 */
for (int open_try = 0; open_try < 3; open_try++) {
/* 尝试打开芯片侧 Socket */
status = ch395f_open_socket(sockfd);
if (status == CH395F_ERR_SUCCESS) {
/* 打开成功,退出重试 */
break;
}
/* 打开失败:确定性复位本 Socket(排渣/二段关闭/等 CLOSED 一体) */
(void)net_sock_reset_sync(sockfd, CH395F_RESET_GRACEFUL);
}
/* 3 次仍失败 → BUSY */
if (status != CH395F_ERR_SUCCESS) {
return NET_ERR_RETURN_1(NET_ERR_BUSY);
}
/* 清掉本 Socket 可能残留的中断状态,丢弃上一条连接遗留的 DISCONNECT。
* F-NET-7:只清**本 Socket** SINT(驱动护栏原语)——原
* "再读清全局"会焊死其它在用 Socket 的事件位(全局寄存器唯一读者=net_poll) */
(void)ch395f_sock_events((uint8_t)sockfd);
/* 设置本地(源)端口 */
ch395f_set_sour_port(sockfd, p_sock->local_port);
/* 设置目的 IP(大端数组) */
ch395f_set_des_ip(sockfd, p_sock->remote_ip_arr);
/* 设置目的端口 */
ch395f_set_des_port(sockfd, p_sock->remote_port);
/* 诊断:回读目的 IP/端口与 Socket 状态(GET_REMOT_IPP 为小端,故需按小端解析)
* 注意 ch395f_get_socket_status 输出**2 字节**:[0]=Socket 开关状态、[1]=TCP 状态,
* 必须给 2 字节缓冲(曾只给 1 字节 → 栈越界 1 字节) */
{
/* 回读的目的 IP/端口(6 字节:4B IP + 2B 端口,小端) */
uint8_t rip[6];
/* 两字节 Socket 状态(须给 2 字节缓冲) */
uint8_t st[2] = {0, 0};
/* 目的 IP 解析成的 32 位值(小端 → 主机序) */
uint32_t dip;
/* 目的端口 */
uint16_t dp;
/* 回读目的 IP + 端口 */
ch395f_get_remot_ipp((uint8_t)sockfd, rip);
/* 回读 Socket 状态 */
ch395f_get_socket_status((uint8_t)sockfd, st);
/* 小端 IP 组装为主机序 */
dip = ((uint32_t)rip[3] << 24) | ((uint32_t)rip[2] << 16) |
((uint32_t)rip[1] << 8) | rip[0];
/* 小端端口组装 */
dp = ((uint16_t)rip[5] << 8) | rip[4];
/* 留痕:目的地址 + Socket 状态(现场排障) */
DBG_INFO("[CONN] sock%d openOK dest=%d.%d.%d.%d:%d status=0x%02X tcp=0x%02X",
sockfd,
(dip >> 24) & 0xFF, (dip >> 16) & 0xFF,
(dip >> 8) & 0xFF, dip & 0xFF, dp, st[0], st[1]);
}
/* 发起 TCP 连接:驱动内部已按 §9.2.2 ⑧ 轮询 GET_CMD_STATUS(4 s 上限),
* 并非"立即返回"。⚠ §9.2.2 ⑧ 明文:返回 SUCCESS 仅表示**命令**执行成功,
* 不代表 TCP 连接成功;连接结果仍由 CONNECT/TIM_OUT 中断裁定。 */
status = ch395f_tcp_connect(sockfd);
/* BUSY 是 poll 超时的返回值 = 芯片仍在执行,命令并未被拒绝 → 维持原行为,
* 交给 CONNECT/TIM_OUT 裁定,不得当成失败(否则慢芯片会被误杀)。
* 其余非 SUCCESS 均为明确的命令级失败(VAL/ARG/USE/OPEN/IF/MEM/BUF/RTE…):
* 此前返回值被直接丢弃 → 本层停在 CONNECTING 干等 recv 30 s 超时才暴露
* (2026-10-12 审计 B2)。按 §9.2.2 ⑧"再次连接需从 ⑤ 重开"回滚。 */
if ((status != CH395F_ERR_SUCCESS) && (status != CH395F_ERR_BUSY)) {
DBG_ERROR("[CONN] sock%d TCP_CONNECT rejected by chip: 0x%02X", sockfd, status);
(void)ch395f_close_socket((uint8_t)sockfd);
/* N-32(P1 不变量 I):命令级失败同样是"本会话已结束"——此前这里只置
* CLOSED,而 CONNECTING 态从未置 session_dead,挂起的 recv 只能干等
* deadline(B2 审计发现的"停在 CONNECTING 干等 30s 超时才暴露"同源) */
p_sock->session_dead = 1U;
p_sock->state = NET_SOCK_STATE_CLOSED;
return NET_ERR_RETURN_1(NET_ERR_INVAL);
}
if (status == CH395F_ERR_BUSY) {
DBG_INFO("[CONN] sock%d TCP_CONNECT still BUSY after poll, await irq", sockfd);
}
/* 状态 → CONNECTING(等待 CONNECT/TIMEOUT 中断裁定) */
p_sock->state = NET_SOCK_STATE_CONNECTING;
/* 连接请求已发出(0) */
return 0;
}
/*
* 单次写入 CH395F 发送 FIFO 的最大字节数 NET_SEND_CHUNK_MAX 已移入
* Drivers/BSP/NET/net_pending.h —— 发送快速路径(本文件)与挂起 send 推进
* (net_pending.c 的 net_send_pending_tick)必须共用同一定义,否则两条路径的
* 分块粒度漂移(TRAP-NET-16:单次写超 1KB 发送 FIFO 会写穿)。
*/
/*
* 函数功能:发送数据(内部锁定版本,必须在 netTask 上下文调用)
* 入口参数:p_sock - Socket 控制块指针 net_sock_t *
* buf - 待发送数据缓冲区指针 const void *
* len - 数据长度 int > 0
* caller - 调用方任务句柄(挂起 send 的完成通知目标) TaskHandle_t
* 出口参数:无
* 返回值:实际发送字节数(>= 0);-1 表示参数无效或 Socket 未连接;
* NET_PENDING_REGISTERED 表示已登记挂起 send(分派据此跳过即时通知) int
* 限定条件:p_sock 非空;Socket 处于 ESTABLISHED 状态;netTask 上下文
* 函数说明:1. 按 NET_SEND_CHUNK_MAX(硬件发送缓冲上限,见 TRAP-NET-16)分块写入 CH395F 发送 FIFO
* 2. 等待发送缓冲腾出采用**有界自旋**:单次等待 ≤ NET_SEND_SPIN_MS
* (健康路径实测 219µs 中位,5 倍余量)→ 多块帧仍在一次分派内发完,
* 健康路径行为与分片时代逐位一致
* 3. 单次等待超出预算 → 登记挂起 send(进度 = 已发字节数)并立即返回,
* 由 net_send_pending_tick 按主循环节拍继续推进 ——
* netTask 最坏停滞由 1s 压到 NET_SEND_SPIN_MS。
* ⚠ 仅**写块前**的等待可挂起:末块写完后 sent == len,此时超预算必须
* 直接返回 sent 而非登记(登记函数按 sent >= len 拒收,会把完整发送
* 报成 -1;详见《Net层阻塞模型架构设计.md》 §5.5 末块边界)
* 4. 发送中途断连(状态非 ESTABLISHED)→ 立即失败并返回已发字节
* (errno=CONNRESET,不再空等超时)
* 5. 未写完即返回(含挂起完成)均为**短写**:调用方须按 ret != len 判定
*/
static int net_send_locked(net_sock_t *p_sock, const void *buf, int len,
TaskHandle_t caller) {
/* 累计实际发送字节数 */
int sent = 0;
/* 当前等待阶段起始时刻(tick) */
uint32_t tick_start;
/* 由控制块指针反推描述符(数组下标) */
int sockfd = (int)(p_sock - s_net_socks);
/* 参数检查:缓冲区无效 → INVAL */
if (buf == NULL || len <= 0) {
return NET_ERR_RETURN_1(NET_ERR_INVAL);
}
/* 未建立连接 → NOTCONN */
if (p_sock->state != NET_SOCK_STATE_ESTABLISHED) {
return NET_ERR_RETURN_1(NET_ERR_NOTCONN);
}
/* 记录起始时刻(用于停滞超时判定) */
tick_start = HAL_GetTick();
/* 分块发送循环:直到全部写入或失败退出 */
while (sent < len) {
/* 剩余待发字节 */
int chunk = len - sent;
/* 单块以硬件发送缓冲为上限(TRAP-NET-16:防写穿 1KB FIFO) */
if (chunk > NET_SEND_CHUNK_MAX) {
chunk = NET_SEND_CHUNK_MAX;
}
/* 发送缓冲未空:等待腾出后再写下一块 */
if (!p_sock->send_ready) {
/* 发送中途断连(DISCONNECT/TIMEOUT 已由 net_poll 处理并置状态非 ESTABLISHED):
* 立即失败,不再空等满超时(旧行为:白等 1s 后报 TIMEDOUT) */
if (p_sock->state != NET_SOCK_STATE_ESTABLISHED) {
(void)net_set_errno(NET_ERR_CONNRESET);
break;
}
/* 停滞超时:NET_SEND_TIMEOUT_MS 后 send_ready 仍未置位(仅快速路径可见;
* 挂起路径的停滞判定由 net_send_pending_tick 负责) */
if (NET_SEND_TIMEOUT_MS > 0 &&
(HAL_GetTick() - tick_start) >= NET_SEND_TIMEOUT_MS) {
DBG_ERROR("sock%d send stall: send_ready not set (%d/%d)", sockfd, sent, len);
(void)net_set_errno(NET_ERR_TIMEDOUT);
break;
}
/* 快速路径预算用尽(仍无进展):登记挂起 send,把等待交回调用方上下文。
* 先于上面的 1s 停滞判定生效,故健康/轻微拥塞路径不会走到 1s 分支。
* 登记失败(同 fd 已有挂起 / netTask 自身调用)→ 返回 -1,调用方见负值。
* 不变量:此处位于 **写块之前**,且循环条件 `sent < len` 尚未被改写
* → `sent < len` 恒成立,登记必获受理(与末块等待的差异点)。 */
if ((HAL_GetTick() - tick_start) >= (uint32_t)NET_SEND_SPIN_MS) {
return net_send_pending_register(sockfd, buf, len, sent, caller);
}
/* 轮询一次(更新 send_ready/状态),下一轮再试 */
net_poll();
continue;
}
/* 本块写入芯片发送 FIFO */
ch395f_write_send_buf(sockfd, (uint8_t *)buf + sent, (uint16_t)chunk);
/* 标记发送缓冲占用中 */
p_sock->send_ready = 0;
/* 累计发送偏移 */
sent += chunk;
/* 累计发送字节统计 */
p_sock->send_bytes += (uint32_t)chunk;
#if defined(TEST_LATENCY_PROBE)
/* 延迟探针起点(chunk_wait):发送缓冲写入刚完成
* (样本 = 写缓冲 → SENBUF_FREE 等待,见 链路延迟测试说明 §3) */
uint32_t c_l3 = lat_cycles_now();
#endif
/* 重置本等待阶段计时 */
tick_start = HAL_GetTick();
/* 等待本块发完(SENBUF_FREE 中断重新置 send_ready) */
while (!p_sock->send_ready) {
/* 发送中途断连:立即失败(同外层,避免白等超时) */
if (p_sock->state != NET_SOCK_STATE_ESTABLISHED) {
(void)net_set_errno(NET_ERR_CONNRESET);
break;
}
/* 轮询:处理 SEND_OK/SENBUF_FREE 中断 */
net_poll();
/* 末块已全部交付(sent == len):无剩余块可写,SENBUF_FREE 等待是
* **纯粹的保守自旋**(原本是为"等缓冲腾出再写下一块"服务)——直接跳出,
* 把等待顺延为下一批的责任。同时也是下面挂起判定的前置守卫:
* net_send_pending_register 会按 `sent >= len` 拒收并回 -1,
* 把一次完整发送报成失败(2026-10-14 台架实证:RETR 1KB 单块帧
* `write error, ret=-1`)。本检查置于**超时/自旋判定之前**,
* 保证末块路径不因预算/超时判定产生任何额外延迟。 */
if (sent >= len) {
break;
}
/* 等待超时:本块放弃(外层统一处理)。置于自旋判定之前,
* 保证 NET_SEND_SPIN_MS 被配置得比 NET_SEND_TIMEOUT_MS 大时仍以超时收口 */
if (NET_SEND_TIMEOUT_MS > 0 &&
(HAL_GetTick() - tick_start) >= NET_SEND_TIMEOUT_MS) {
break;
}
/* 快速路径预算用尽(仍无进展):登记挂起 send(进度 = 已发字节数),
* 由 net_send_pending_tick 续传;健康路径(219µs 量级)不会到达此处。
* 此处位于**写块之后**,但上面的 `sent >= len` 守卫已保证伴随
* `sent < len`(仍有剩余块)才具到达资格,登记必获受理。 */
if ((HAL_GetTick() - tick_start) >= (uint32_t)NET_SEND_SPIN_MS) {
return net_send_pending_register(sockfd, buf, len, sent, caller);
}
}
#if defined(TEST_LATENCY_PROBE)
/* 探针已武装:累计本段等待样本 */
if (lat_probe_armed() != 0U) {
lat_probe_add_seg("chunk_wait", lat_cycles_diff_us(c_l3));
}
#endif
}
/* 发送未完成 = 短写 */
if (sent < len) {
/* 短写告警:调用方必须检查返回值。静默截断会让对端永久等待剩余字节
* (2026-09-21 实证:UI 推流应答被截断 → 客户端反复判定"全程无字节"失联)。 */
DBG_ERROR("sock%d short send %d/%d (errno=%d)", sockfd, sent, len,
s_net_errno_slots[net_errno_slot()].err);
}
/* 返回实际发送字节数(调用方必须检查,勿只看非负) */
return sent;
}
/*
* 函数功能:接收数据(内部锁定版本,必须在 netTask 上下文调用)
* 入口参数:p_sock - Socket 控制块指针 net_sock_t *
* buf - 接收缓冲区指针 void *
* len - 缓冲区大小 int > 0
* flags - 标志位,NET_MSG_DONTWAIT 表示非阻塞 int
* timeout_ms - 本次等待预算(0 = 无限等待,无默认值)uint32_t
* caller - 调用方任务句柄(挂起请求的完成通知目标) TaskHandle_t
* 出口参数:buf - 填充接收到的数据
* 返回值:实际接收字节数(> 0);0 表示对端关闭;-1 表示出错/非阻塞无数据 int
* NET_PENDING_REGISTERED 表示已登记挂起请求(分派据此跳过即时通知)
* 限定条件:p_sock 非空;netTask 上下文
* 函数说明:1. 数据已在(快速路径)→ 立即读出返回
* 2. 非阻塞(NET_MSG_DONTWAIT 单次覆盖 **或** socket blocking 属性为 0,
* 模式属性)无数据立即返回 -1(NET_ERR_WOULDBLOCK)
* 3. 无数据 + 阻塞等待 → 登记挂起请求(pending 化,E-02 结构演进)
* 4. netTask 零占用立即返回;完成条件(数据/到点[仅显式预算]/断连)由 net_pending_tick 按主循环节拍检查
* 5. 并通知 caller;关闭/网络重建/陈旧重同步由对应路径显式结束(取消必通知)
* 6. CLOSED 状态语义(排空 + EOF + 自动重监听)逐项保留在 tick 内
*/
static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags,
uint32_t timeout_ms, TaskHandle_t caller) {
/* 由控制块指针反推描述符(数组下标) */
int sockfd = (int)(p_sock - s_net_socks);
/* 参数检查:缓冲区无效 → INVAL */
if (buf == NULL || len <= 0) {
return NET_ERR_RETURN_1(NET_ERR_INVAL);
}
/* 超时来源:只有调用方显式预算(net_recv_to);0 = 无限等待
* (NET_RECV_TIMEOUT_MS 30s 默认预算已删,API 不强加隐藏值) */
uint32_t to_ms = timeout_ms;
/* 先轮询一次:刷新状态机与可读标志 */
net_poll();
/* 查询芯片侧接收缓冲可用字节数 */
int recv_len = ch395f_get_recv_len(sockfd);
/* 快速路径:数据已在 */
if (recv_len > 0) {
/* 本次读取量 = 缓冲容量与芯片可用量的较小值 */
int read_len = (recv_len > len) ? len : recv_len;
/* 读出到调用方缓冲区 */
ch395f_read_recv_buf(sockfd, (uint8_t *)buf, (uint16_t)read_len);
/* 累计接收字节统计 */
p_sock->recv_bytes += (uint32_t)read_len;
/* 已消费数据(部分读安全:手册 §5.34 未读数据保留可续读,旧"部分读归零
* 部分读归零"已降级为未证实历史观察;下一次 RECV_OK 重新置 readable) */
p_sock->readable = 0;
/* 返回本次读出字节数 */
return read_len;
}
/* 非阻塞模式:NET_MSG_DONTWAIT 单次覆盖,或 socket 的 blocking 属性为 0
* (net_setblocking(fd, 0),模式挂控制块属性) */
if ((flags & NET_MSG_DONTWAIT) || (p_sock->blocking == 0U)) {
/* 非阻塞且当前无数据:若连接已断开则视为 EOF(0) 并自动重监听,
* 否则返回 WOULDBLOCK(-1) 表示暂时无数据 */
/* 连接已断开(CLOSED)**或**本会话已结束(session_dead):按 EOF 处理。
* 判据从纯 `state == CLOSED` 放宽为 `|| session_dead`(N-32 P1 不变量 I):
* 抢先 relisten 后 state 已是 LISTENING 而会话确实已结束,旧判据在此
* 恒返 WOULDBLOCK → EOF 永久失踪(新连接被旧会话静默吸收)。
* 有数据时走上方快路径(recv_len>0 优先),本判据只在"无数据"时生效,
* 故不会把新连接的起始数据误判为 EOF。 */
if ((p_sock->state == NET_SOCK_STATE_CLOSED) || (p_sock->session_dead != 0U)) {
/* 启用自动重监听:登记重建请求(动作在 net_listener_tick 一处)。
* ⚠ 原此处内层 `state == CLOSED` 门已**上收到 tick**:它防的是"对已
* LISTENING 的 fd 再 open+listen → ch395f_open_socket 返 USE → 失败分支
* ch395f_close_socket → 把正听着新 SYN 的 Socket 当场关掉"(U12 同源
* 事故)。这是**执行侧安全不变量**,不该由每个登记点各自复述——
* 漏一处即事故;收口后 tick 另加"芯片已在听即了结",结构上不可能再犯。
* N-32 P2 修正(2026-10-12 台架实证):**保留本处即时登记**——
* 它是"消费者动作触发"(消费者正在等这条连接的结局),不是周期性
* "抢先";删掉它会把 CLOSED 窗口从 ~0 拉长到 reconciler 周期+隔离期
* (~100ms),期间调用方高频发起 net_accept(RPC 无上界)→
* `[WD] task stall`(实测 uis 往返 1063ms)。周期性兜底由 reconciler 收敛。 */
if (p_sock->auto_relisten != 0U) {
/* 非阻塞:下一轮 net_listener_tick 执行,30ms 节奏重试 */
net_listener_request(sockfd, NET_LISTENER_REQ_EOF);
}
/* EOF:返回 0(非错误) */
return 0;
}
/* 暂时无数据 → WOULDBLOCK */
return NET_ERR_RETURN_1(NET_ERR_WOULDBLOCK);
}
/* 无数据 + 阻塞等待:登记挂起请求,立即返回(netTask 任何时刻都不被占用)。
* 调用方在自己上下文睡任务通知(LwIP netconn 模型);等待期间 netTask
* 继续主循环(事件泵 + 队列),其他消费者零头阻塞。 */
/* 登记挂起请求:返回值即登记结果 */
return net_pending_register(sockfd, buf, len, to_ms, caller);
}
/*
* 函数功能:关闭 Socket(内部版本,不经过消息队列)
* 入口参数:p_sock - 要关闭的 Socket 控制块指针 net_sock_t *
* 出口参数:无
* 返回值:0 - 关闭成功 int
* 限定条件:必须在 netTask 上下文中调用;p_sock 有效
* 函数说明:1. 排渣收口:原"close + 500ms 盲等排空循环"改
* **确定性 sock_reset**(读空 RX -> CLOSE(幂等) -> 排空本 Socket
* 事件(quiescent) -> 等 byte0=00H,内置 LISTEN 态二段 CLOSE 补发
* ——E2 补充发现:一次 CLOSE 只拆监听 byte0 恒 05)
* 2. F-NET-7:排空只碰本 Socket SINT,**不再读清全局中断寄存器**
* (唯一读者=net_poll;原循环读全局会焊死其它 Socket 事件位)
* 3. 快路径:芯片已 CLOSED(byte0=00H)→ 跳过复位只清残留事件
* 4. 复位超时照常放行(同原 500ms 超时容忍;后续 relisten 由
* TRAP-NET-09 open 重试环吸收)
*/
static int net_close_locked(net_sock_t *p_sock) {
/* S12 试点①(状态机不变量,规格 §2.1):本函数的「限定条件:p_sock 有效」
* 此前只写在函数头注释里,函数体一项都不检查——一旦调用方给出数组外指针或
* **已释放(in_use==0)**的控制块,下面按指针反推描述符 +
* memset(sizeof(net_sock_t)) 会直接改写 s_net_socks 之外的静态数据(fd 表
* 整体被打脏,故障表现为"谁都不连了",极难归因)。故断言排在任何解引用
* **之前**,它不是装饰。
* ⚠ 不是错误返回的替身:net_close / 消息分派该返 -1(NET_ERR_BADF)的分支
* 一条没动——那些处理"外部给错 fd",本断言守"内部走到这儿必须是在用槽位"。
* ⚠ 上下文:仅 netTask、每次会话拆链各一次,非中断非高频(纪律 1);
* 条件全是纯读,无副作用(纪律 4)。 */
APP_ASSERT((p_sock >= &s_net_socks[0]) &&
(p_sock < &s_net_socks[NET_MAX_SOCKETS]) &&
(p_sock->in_use != 0U));
/* 由控制块指针反推描述符 */
int sockfd = (int)(p_sock - s_net_socks);
/* 先结束本 fd 的挂起 recv(排空/取消 + 必通知),再关芯片 Socket
* ——关闭不得让等待中的调用方永久悬挂 */
net_pending_settle_close((int)(p_sock - s_net_socks), p_sock);
/* 挂起 send 同样先结束(以已发字节数通知;未发部分放弃),再关芯片 Socket */
net_send_pending_cancel((int)(p_sock - s_net_socks), NET_ERR_CONNRESET);
/* 排渣 + 关闭 + 等 CLOSED 一体(见函数头注释 1);
* 已 CLOSED 的 fd 只清本 Socket 残留事件(防下一连接误判"刚建立就断开") */
if (ch395f_cmd_done((uint8_t)sockfd, CH395F_DONE_CLOSE) == 1) {
(void)ch395f_sock_events((uint8_t)sockfd);
} else if (net_sock_reset_sync(sockfd, CH395F_RESET_GRACEFUL) != 0) {
DBG_ERROR("sock%d close: sock_reset not done, proceed anyway", sockfd);
}
/* 清零控制块(memset 0 = 释放本 fd 全部状态) */
/* N-30(TRAP-NET-20):幽灵甄别基准跨 fd 释放保留(幽灵属芯片 Socket 级状态,
* 与 fd 复用无关——close 后新 listen 落在同一芯片 Socket 上仍可能撞幽灵) */
uint16_t keep_peer = p_sock->remote_port;
memset(p_sock, 0, sizeof(net_sock_t));
p_sock->last_peer_port = keep_peer;
/* 关闭即取消该 fd 的待重试(D10 重试队列) */
net_listener_clear(sockfd);
/* 关闭即清除该 fd 的挂起状态(幂等) */
net_pending_clear(sockfd);
/* 挂起 send 同样幂等清理(cancel 已在上方通知,此处仅保证状态归零) */
net_send_pending_clear(sockfd);
/* 关闭成功 */
return 0;
}
/*
* 函数功能:主动拆掉当前 TCP 会话、保留监听(内部版本,netTask 上下文)
* 入口参数:p_sock - 会话所在 Socket 控制块(调用方已判 ESTABLISHED/CONNECTING)
* 出口参数:无
* 返回值:0 - 拆除完成(监听型 fd 已重监听或交 D10) int
* 限定条件:netTask 上下文;p_sock 在用且为 TCP 流
* 函数说明:1. E-02 收口(《网络自愈架构设计》§5.2 E13):应用放弃的会话若 TCP 仍活
* (PC 内核自动 ACK keepalive,僵尸判不死),单连接监听被永久
* 占住;应用又不得 net_close 监听 fd(毁监听)——本函数补上
* "拆连接不拆监听"的应用侧形态
* 2. 芯片侧拆除(TRAP-NET-18 异常释放语义,N-17 收口改异步):监听型 fd
* = net_listener_settle(QUARANTINE) + REQ_DROP 登记即返,动作全部交
* net_listener_tick(隔离窗内持续排空/关闭正是该机的既有语义);
* 非监听 client fd = 同步 sock_reset QUARANTINE(600ms 封顶,原形态)
* ——本处理器不得再对监听型做同步等待(netTask 占用窗见 .h 注)
* 3. auto_relisten(监听型 fd):收口 CLOSED 后登记重建请求(动作在
* net_listener_tick,≤1ms 后执行;芯片已 CLOSED 直接 open 必成,
* E4 定案;未成则 30ms 节奏重试 + reconciler 形态④兜底);
* 非 auto_relisten(client fd):收口 CLOSED,
* 是否重连由调用方决定
* 4. 挂起 recv 先结束(settle_close:排空尝试或取消,必通知)
*/
static int net_drop_conn_locked(net_sock_t *p_sock) {
/* S12 试点②(状态机不变量,规格 §2.1):函数头「限定条件:p_sock 在用且为
* TCP 流」+ 分派处的"非 ESTABLISHED/CONNECTING 视为幂等成功"这两条契约,
* 在本函数体内**没有任何机器表达**,而函数体是无条件的:置 CLOSED、清
* readable、给监听型 fd 登记重建。也就是说——将来任何新增路径绕过
* NET_MSG_DROP_CONN 的分派门直接调它,就会把一个**健康监听器**当会话拆掉
* (症状是端口偶发不再 accept,与 N-30 一族同源难查)。断言把这条"进来时
* 必须处于活动会话态"钉成机器可查的不变量,正是"不该发生"而不是"外部输入"。
* ⚠ 分派处(case NET_MSG_DROP_CONN)的三条错误返回原样保留——它们负责真机
* 上把误用变成 -1,本断言负责在测试构建里把误用**当场喊出来**。
* ⚠ 上下文:仅 netTask、应用主动弃会话时各一次,非中断非高频;纯读无副作用。 */
APP_ASSERT((p_sock >= &s_net_socks[0]) &&
(p_sock < &s_net_socks[NET_MAX_SOCKETS]) &&
(p_sock->in_use != 0U) &&
(p_sock->type == NET_SOCK_STREAM) &&
((p_sock->state == NET_SOCK_STATE_ESTABLISHED) ||
(p_sock->state == NET_SOCK_STATE_CONNECTING)));
/* 由控制块指针反推描述符 */
int sockfd = (int)(p_sock - s_net_socks);
/* 先结束本 fd 挂起 recv(排空/取消 + 必通知),再拆芯片会话 */
net_pending_settle_close((int)(p_sock - s_net_socks), p_sock);
/* 挂起 send 同样先结束(以已发字节数通知;未发部分放弃),再拆芯片会话 */
net_send_pending_cancel((int)(p_sock - s_net_socks), NET_ERR_CONNRESET);
/* 芯片侧拆除按 fd 形态分流(异常释放 = QUARANTINE 语义,TRAP-NET-18)。
* 监听型 fd:只登记沉降隔离 + 重建请求,**立即返回**——“排渣/FIN 关闭/
* 排空事件/等 byte0=00H/重建”整套动作由 net_listener_tick 以 30ms 节奏执行
* (与被动超时释放同一台机器;E16 台架实证该 net 层自建序列 = RST 10/10、
* FIN 5/5 的通过版本)。旧形态在本处理器里 net_sock_reset_sync 同步等待,
* netTask 被占用 400ms 底/600ms 顶:串行总线整窗停摆、UI/FTP/对时的 API 回
* 执顺延、NET 与对时位同窗丢 100ms 抽样 → 800ms 停喂声明 stall,距 IWDG
* 最坏超时余量仅 ~0.7s(登记簿 N-17;方向评估与否决论证 → 《网络自愈架构
* 设计》§4.6)。返回语义随之明确:返回 = 已登记,芯片未必已拆完。
* 非监听 client fd:无监听可保,维持同步确定性排渣(600ms 封顶;全仓
* 零生产调用者,形态原样保留防漂移)。 */
if (p_sock->auto_relisten != 0U) {
net_listener_settle(sockfd, NET_LISTENER_SETTLE_QUARANTINE);
} else if (net_sock_reset_sync(sockfd, CH395F_RESET_QUARANTINE) != 0) {
DBG_ERROR("sock%d drop_conn: sock_reset not done", sockfd);
/* 超时继续收口本层;芯片侧交 reconciler/pump 对账接管 */
}
/* 会话簿记收口(保留 fd:不 memset、不动 local_port/auto_relisten) */
p_sock->state = NET_SOCK_STATE_CLOSED;
p_sock->readable = 0U;
/* N-30(TRAP-NET-20):新会话起点,事件未收到去抖计数清零 */
p_sock->lost_conn_seen = 0U;
if (p_sock->auto_relisten) {
/* 监听型 fd:登记重建请求(非阻塞;下一轮 net_listener_tick 执行) */
net_listener_request(sockfd, NET_LISTENER_REQ_DROP);
}
/* 拆除完成 */
return 0;
}
/*
* 函数功能:发送数据(线程安全版本,通过消息队列委托给 netTask)
* 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS
* buf - 待发送数据缓冲区指针(调用返回前须保持有效,本函数不拷贝数据) const void *
* len - 数据长度(字节),> 0 int
* flags - 标志位,当前实现忽略(保留 BSD send 接口兼容) int
* 出口参数:无
* 返回值:实际发送字节数 int >= 0 || -1
* -1 - 发送失败(参数非法 / 消息队列入队失败 / netTask 侧发送错误) int
* 限定条件:1. 必须在【非 netTask】上下文中调用(应用/测试任务);若在 netTask 内调用会自己等自己 → 死锁
* (netTask 阻塞在 xTaskNotifyWait 而无人处理消息);netTask 内请直接用 net_send_locked()
* 2. Socket 须处于 ESTABLISHED 状态
* 3. net_init() + MX_FREERTOS_Init() 已执行,消息队列已创建并绑定到本模块
* 函数说明:1. 所有 CH395F SPI 访问只允许在 netTask 内串行执行(见 TRAP-NET-14)
* 2. 本函数不直接触碰 SPI,而是把发送请求打包成 net_msg_t 投递到本模块的消息队列
* 3. netTask 的 net_process_messages() 取出并在 netTask 上下文调用 net_send_locked() 真正写 CH395F
* 4. 调用方经 FreeRTOS TaskNotify 阻塞等待处理结果,对外呈现"同步发送"语义
* 5. 函数返回 = 本次发送已被 netTask 处理完毕(成功或失败)
*/
int net_send(int sockfd, const void *buf, int len, int flags) {
/* flags 当前未使用,显式 void 以消除未用参数告警 */
(void)flags;
/* 一次性消息结构体;入队后由 netTask 取出处理(注意只传指针不拷贝数据) */
net_msg_t msg;
/* ---- 1. 参数合法性检查 ---- */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
/* 描述符越界 */
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* ---- 2. 运行环境检查:消息队列须已创建(网络层已初始化) ---- */
if (s_net_msg_queue == NULL) {
/* 网络层未初始化 */
return NET_ERR_RETURN_1(NET_ERR);
}
/* ---- 3. 组装消息:把本次 send 的全部上下文填入 msg ---- */
/* 消息类型,netTask 据此分派到 net_send_locked */
msg.type = NET_MSG_SEND;
/* 目标 Socket */
msg.sockfd = sockfd;
/* 数据缓冲(队列仅拷贝指针,buf 须有效至函数返回) */
msg.buf = (void *)buf;
/* 待发送字节数 */
msg.len = len;
/* 实际标志位(忽略入参 flags) */
msg.flags = 0;
/* 发送不使用超时字段 */
msg.timeout_ms = 0U;
/* 记录调用任务句柄,供 netTask 回执时精准通知 */
msg.caller = xTaskGetCurrentTaskHandle();
/* 预置失败;netTask 处理成功后会改写为实际发送字节数 */
msg.result = -1;
/* ---- 4. 入队:将消息投递给 netTask,osWaitForever 表示队列满时阻塞调用方 ---- */
if (osMessageQueuePut(s_net_msg_queue, &msg, 0, osWaitForever) != osOK) {
/* 入队失败(队列异常等极端情况) */
return NET_ERR_RETURN_1(NET_ERR_BUSY);
}
/* ---- 5. 阻塞等待 netTask 处理完毕:netTask 调 net_send_locked 后通过
* xTaskNotify() 唤醒本任务,并把结果(实际发送字节数或 -1)作为通知值传回 ---- */
/* 两段式等待(防丢失通知,见 net_wait_result) */
msg.result = net_wait_result("snd", sockfd);
/* ---- 6. 结果回传:负数视为错误并设置线程级错误码;否则返回实际发送字节数 ---- */
if (msg.result < 0) {
/* netTask 把具体错误码(如 NET_ERR_WOULDBLOCK)以负值搭通知值回传,此处恢复为
* errno;对外仍保持 BSD 语义——失败一律返回 -1 */
return NET_ERR_RETURN_1(msg.result);
}
/* 返回实际发送字节数 */
return msg.result;
}
/*
* 函数功能:接收数据(线程安全版本,通过消息队列委托给 netTask;
* **默认无限阻塞**,30s 默认预算已删)
* 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS
* buf - 接收缓冲区指针 void *
* len - 缓冲区大小 int > 0
* flags - 标志位,0=按 socket 阻塞属性等待,NET_MSG_DONTWAIT=单次非阻塞 int
* 出口参数:无
* 返回值:实际接收字节数 int >= 0 || 0 || -1
* 0 - 对端关闭连接 int
* -1 - 接收失败(参数无效、非阻塞无数据=WOULDBLOCK、
* close/网络重建/陈旧重同步取消=CONNRESET 等,errno 可查) int
* 限定条件:Socket 处于 ESTABLISHED 状态;net_init() + MX_FREERTOS_Init() 已执行;非 netTask 上下文
* 函数说明:1. 将接收请求放入消息队列,等待 netTask 处理完毕并返回结果。netTask 内请使用 net_recv_sock()
* 2. 无数据 + 阻塞(blocking 属性为默认值 1 且未置 DONTWAIT)→ 登记挂起
* **无限等待**;唤醒源 = 数据 / EOF / close / 网络重建(CONNRESET);
* 会话死亡由对账① ~400ms 判死兜底,不存在"对端消失后永久挂死"窗口
* 3. ⚠ 文档契约(嵌入式特有):IWDG required_mask 内任务禁止无限阻塞
* recv——必须 net_select 超时或 net_recv_to 显式预算(超时归使用者
* 对受监控任务是义务不是选项),否则"对端连上不发数据"= IWDG 复位
* 4. 超时首选 net_select(...);net_recv_to(...) 为显式预算便捷入口
*/
int net_recv(int sockfd, void *buf, int len, int flags) {
/* 一次性消息结构体(队列按值存结构副本) */
net_msg_t msg;
/* 描述符越界 → BADF */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 消息队列未创建(网络层未初始化)→ 通用错误 */
if (s_net_msg_queue == NULL) {
return NET_ERR_RETURN_1(NET_ERR);
}
/* 消息类型:接收数据 */
msg.type = NET_MSG_RECV;
/* 目标 Socket */
msg.sockfd = sockfd;
/* 接收缓冲(队列仅拷贝指针,缓冲须有效至调用返回) */
msg.buf = buf;
/* 缓冲容量 */
msg.len = len;
/* 标志:0=按 socket 阻塞属性,NET_MSG_DONTWAIT=单次非阻塞 */
msg.flags = flags;
/* 0 = 无限等待(无默认预算;显式预算走 net_recv_to) */
msg.timeout_ms = 0U;
/* 调用方任务句柄(netTask 凭它回传回执) */
msg.caller = xTaskGetCurrentTaskHandle();
/* 结果占位(netTask 侧覆写实际结果) */
msg.result = -1;
/* 入队:队列满时阻塞调用方直至有空位 */
if (osMessageQueuePut(s_net_msg_queue, &msg, 0, osWaitForever) != osOK) {
/* 入队失败 → BUSY */
return NET_ERR_RETURN_1(NET_ERR_BUSY);
}
/* 阻塞等待 netTask 回执(两段式拾取,防丢失通知) */
msg.result = net_wait_result("rcv", sockfd);
if (msg.result < 0) {
/* netTask 把具体错误码(如 NET_ERR_WOULDBLOCK)以负值搭通知值回传,此处恢复为
* errno;对外仍保持 BSD 语义——失败一律返回 -1 */
return NET_ERR_RETURN_1(msg.result);
}
/* 返回结果(接收字节数;0 = EOF) */
return msg.result;
}
/*
* 函数功能:接收数据(非阻塞具名入口——原 NET_MSG_DONTWAIT 路径转正,
* 对齐 BSD recv(MSG_DONTWAIT) / Python recv 非阻塞模式的"名如其义")
* 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS
* buf - 接收缓冲区指针 void *
* len - 缓冲区大小 int > 0
* 出口参数:buf - 填充接收到的数据
* 返回值:实际接收字节数(> 0);0 = 对端关闭(net 层已自动重监听); int >= 0 || -1
* -1 = 错误(无数据 = NET_ERR_WOULDBLOCK,其余 errno 可查) int
* 限定条件:非 netTask 上下文;net 层已初始化
* 函数说明:1. 等价 net_recv(sockfd, buf, len, NET_MSG_DONTWAIT):单次强制非阻塞,
* 不改变 socket 的 blocking 属性(模式用 net_setblocking)
* 2. 受 IWDG 监控 / 有周期职责的任务(netTask 除外)的推荐默认入口;
* 需要"等但不无限等"用 net_select / net_recv_to
*/
int net_recv_nb(int sockfd, void *buf, int len) {
return net_recv(sockfd, buf, len, NET_MSG_DONTWAIT);
}
/*
* 函数功能:接收数据(带调用方指定超时的阻塞版,用于分片有界读;
* 为**显式预算**便捷入口,等价 Python settimeout,
* 不再有隐藏默认值)
* 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS
* buf / len - 接收缓冲与容量 void * / int > 0
* timeout_ms - 单次占用 netTask 的最长等待(毫秒,>0) uint32_t
* 出口参数:无
* 返回值:>0 字节数;0 = 对端关闭(EOF);-1 = 真错误(errno 可查);**-2 = 超时无数据**
* 限定条件:非 netTask 上下文;net 层已初始化
* 函数说明:1. E-02 修复配套接口:lftpd 等"等对端下一行/下一块数据"的调用方
* 若无限阻塞 recv,会把 netTask(全部 socket 操作的唯一执行者)长期占住
* 2. 导致其余任务的网络操作整体冻结(M3 实测一次空闲 FTP 登录冻结远端显示 17.7s)
* 3. 用本接口把等待切成 ≤timeout_ms 的片,调用方自行累计总预算:总超时语义不变、单次占用有界
* 4. 超时用独立返回码 -2(而非靠 errno 区分)→ 无跨任务 errno 竞态
* 5. ⚠ IWDG required_mask 内任务必须走本接口或 net_select(net_recv 契约)
*/
int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) {
/* 一次性消息结构体:队列按值存结构副本(仅指针、不拷数据),入队后由 netTask 取出处理 */
net_msg_t msg;
/* ---- 1. 参数合法性检查 ---- */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
/* 描述符越界 */
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* ---- 2. 运行环境检查:消息队列须已创建(网络层已初始化) ---- */
if (s_net_msg_queue == NULL) {
/* 网络层未初始化 */
return NET_ERR_RETURN_1(NET_ERR);
}
/* ---- 3. 分片长度检查:timeout_ms = 0 在 netTask 侧意味着"**无限等待**"
* (已删除 30s 默认预算),恰与本接口"有界等待"的存在
* 理由相反,直接拒绝 ---- */
if (timeout_ms == 0U) {
/* 分片长度必须 > 0 */
return NET_ERR_RETURN_1(NET_ERR_INVAL);
}
/* ---- 4. 组装消息:把本次分片读的全部上下文填入 msg ---- */
/* 消息类型,netTask 据此分派到 net_recv_locked */
msg.type = NET_MSG_RECV;
/* 目标 Socket */
msg.sockfd = sockfd;
/* 接收缓冲(队列仅拷贝指针,buf 须有效至本函数返回) */
msg.buf = buf;
/* 缓冲容量:单次读取字节数上限(允许部分读) */
msg.len = len;
/* 阻塞式:netTask 侧轮询至"有数据"或"本片超时"(勿置 NET_MSG_DONTWAIT) */
msg.flags = 0;
/* 本片预算:netTask 最多等待 timeout_ms,保证对 netTask 的占用有界(E-02) */
msg.timeout_ms = timeout_ms;
/* 记录调用任务句柄,netTask 处理后经 xTaskNotify() 精准回执 */
msg.caller = xTaskGetCurrentTaskHandle();
/* 预置失败(仅防御值;真实结果经任务通知值回传,见步骤 6) */
msg.result = -1;
/* ---- 5. 入队:将消息投递给 netTask,osWaitForever 表示队列满时阻塞调用方 ---- */
if (osMessageQueuePut(s_net_msg_queue, &msg, 0, osWaitForever) != osOK) {
/* 入队失败(队列异常等极端情况) */
return NET_ERR_RETURN_1(NET_ERR_BUSY);
}
/* ---- 6. 阻塞等待 netTask 处理完毕:netTask 执行 net_recv_locked 后经 xTaskNotify()
* 把结果(字节数 / 0 / 负错误码)以 32 位通知值回传本任务
* net_msg_t 按值拷入队列,result 字段不会回来——通知值是唯一的真实回传通道(勿加"回传用"字段) ---- */
/* 两段式等待:零等待拾取 + 阻塞,防丢失通知(见 net_wait_result) */
msg.result = net_wait_result("rcv", sockfd);
/* ---- 7. 回传结果(三分支:分片超时 / 真错误 / 数据或 EOF) ---- */
if (msg.result == NET_ERR_TIMEDOUT) {
/* 本片超时:timeout_ms 内未等到数据,非致命 → 返回 -2 */
/* 超时只凭返回码 -2 判定、不得依赖 errno:返回码是 netTask 经任务通知值
* 专为本任务回传的,天然无竞态(本分支也不改写错误码) */
/* 调用方见到 -2 后累计自身总超时预算,决定是否发起下一片 */
/* 本判断必须先于下方通用 <0 分支:NET_ERR_TIMEDOUT = -9 同样满足 <0 */
return -2;
}
if (msg.result < 0) {
/* 真错误:netTask 把具体错误码(如 NET_ERR_WOULDBLOCK)以负值回传,
* 此处恢复为线程级错误码(可经 net_get_errno() 读取);
* 对外保持 BSD 语义——失败一律返回 -1 */
return NET_ERR_RETURN_1(msg.result);
}
/* >= 0:0 = 对端关闭(EOF,net 层尽力自动重监听,失败由重试轮询接管);
* > 0 = 实际接收字节数(可能小于 len,属正常部分读) */
/* 返回结果(0=EOF / >0=字节数) */
return msg.result;
}
/*
* 函数功能:关闭 Socket(线程安全版本,通过消息队列委托给 netTask)
* 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS
* 出口参数:无
* 返回值:0 - 关闭成功 int
* -1 - 关闭失败 int
* 限定条件:Socket 已创建且处于非 CLOSED 状态;net_init() + MX_FREERTOS_Init() 已执行;非 netTask 上下文
* 函数说明:将关闭请求放入消息队列,阻塞等待 netTask 处理完毕。TCP Socket 先断开连接再释放控制块。
* netTask 内请使用 net_close_sock()
*/
int net_close(int sockfd) {
/* 消息体(按值拷入队列,netTask 改的是其副本) */
net_msg_t msg;
/* 描述符越界 → BADF */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 消息队列未创建(网络层未初始化)→ 通用错误 */
if (s_net_msg_queue == NULL) {
return NET_ERR_RETURN_1(NET_ERR);
}
/* 消息类型:关闭 Socket */
msg.type = NET_MSG_CLOSE;
/* 目标 Socket */
msg.sockfd = sockfd;
/* 无超时(关闭立即完成) */
msg.timeout_ms = 0U;
/* 调用方任务句柄(netTask 凭它回传回执) */
msg.caller = xTaskGetCurrentTaskHandle();
/* 结果占位(netTask 侧覆写实际结果) */
msg.result = -1;
/* 入队:队列满时阻塞调用方直至有空位 */
if (osMessageQueuePut(s_net_msg_queue, &msg, 0, osWaitForever) != osOK) {
/* 入队失败 → BUSY */
return NET_ERR_RETURN_1(NET_ERR_BUSY);
}
/* 阻塞等待 netTask 回执(两段式拾取,防丢失通知) */
msg.result = net_wait_result("clo", sockfd);
if (msg.result < 0) {
/* netTask 把具体错误码(如 NET_ERR_WOULDBLOCK)以负值搭通知值回传,此处恢复为
* errno;对外仍保持 BSD 语义——失败一律返回 -1 */
return NET_ERR_RETURN_1(msg.result);
}
/* 关闭成功(0) */
return msg.result;
}
/*
* 函数功能:主动拆掉当前 TCP 会话、保留监听(线程安全版本,经消息队列委托 netTask)
* 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS
* 出口参数:无
* 返回值:0 - 成功(含幂等:无活动会话直接 0) int
* -1 - 失败(BADF/INVAL/入队失败) int
* 限定条件:1. 必须在【非 netTask】上下文调用(同其它 net_* API,netTask 内会自等死锁)
* 2. 场景=应用放弃会话(对时 30s 预算耗尽、协议超时);EOF 路径**不要**调
* (net 层 auto_relisten 已接管,见 AGENTS 单连接断开语义)
* 函数说明:1. 单连接模型下挂起但 TCP 仍活的客户端会永久占住监听(keepalive
* 被 PC 内核自动 ACK 判不死,《网络自愈架构设计》§5.2 E13);应用不得
* net_close 监听 fd → 本 API 拆连接不拆监听(E-02 收口)
* 2. 监听型 fd:netTask 侧仅登记隔离沉降 + 重建请求即返回(动作在
* net_listener_tick,netTask 零占用,登记簿 N-17 收口);非监听 client
* fd 仍同步排渣(600ms 封顶)。见 net_drop_conn_locked
*/
int net_drop_conn(int sockfd) {
/* 消息体(按值拷入队列,netTask 改的是其副本) */
net_msg_t msg;
/* 描述符越界 → BADF */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 消息队列未创建(网络层未初始化)→ 通用错误 */
if (s_net_msg_queue == NULL) {
return NET_ERR_RETURN_1(NET_ERR);
}
/* 消息类型:拆会话保监听 */
msg.type = NET_MSG_DROP_CONN;
/* 目标 Socket */
msg.sockfd = sockfd;
/* 无超时 */
msg.timeout_ms = 0U;
/* 调用方任务句柄(netTask 凭它回传回执) */
msg.caller = xTaskGetCurrentTaskHandle();
/* 结果占位 */
msg.result = -1;
/* 入队:队列满时阻塞调用方直至有空位 */
if (osMessageQueuePut(s_net_msg_queue, &msg, 0, osWaitForever) != osOK) {
/* 入队失败 → BUSY */
return NET_ERR_RETURN_1(NET_ERR_BUSY);
}
/* 阻塞等待 netTask 回执(两段式拾取,防丢失通知) */
msg.result = net_wait_result("dcp", sockfd);
if (msg.result < 0) {
/* 错误码经通知值回传;对外保持 BSD 语义失败一律 -1 */
return NET_ERR_RETURN_1(msg.result);
}
/* 拆除成功(0,含幂等) */
return msg.result;
}
/*
* 函数功能:发送 UDP 数据报(含目标地址)
* 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS
* buf - 待发送数据缓冲区指针 const void *
* len - 数据长度 int > 0
* flags - 标志位,通常为 0 int
* dest_addr - 目标地址结构体指针 const struct net_sockaddr *
* addrlen - 目标地址结构体长度 int >= sizeof(struct net_sockaddr_in)
* 出口参数:无
* 返回值:实际发送字节数 int > 0 || -1
* -1 - 发送失败 int
* 限定条件:Socket 已创建且为 UDP 类型(SOCK_DGRAM);net_init() 已调用
* 函数说明:1. UDP 数据报无需连接:目标地址随消息一并投递
* 2. 与 net_send 同一消息通道,SPI 访问串行在 netTask
*/
int net_sendto(int sockfd, const void *buf, int len, int flags,
const struct net_sockaddr *dest_addr, int addrlen) {
/* Socket 控制块指针 */
net_sock_t *p_sock;
/* 目标地址(IPv4)强转 */
const struct net_sockaddr_in *p_addr_in;
/* 累计实际发送字节数 */
int sent = 0;
/* 当前等待阶段起始时刻(tick) */
uint32_t tick_start;
/* 参数检查 */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
/* 描述符越界 → BADF */
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 取控制块 */
p_sock = &s_net_socks[sockfd];
/* 控制块未在用 → BADF */
if (!p_sock->in_use) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 缓冲区无效 → INVAL(本块必须留在守卫之前:参数错误先报参数错误) */
if (buf == NULL || len <= 0) {
return NET_ERR_RETURN_1(NET_ERR_INVAL);
}
/* netTask 上下文守卫:本接口在本任务上下文里直接 net_poll + 读写芯片缓冲,
* 非执行任务调用会与本层 net_poll 抢同一根 SPI2(TRAP-NET-14)。放在参数
* 校验**之后**:越界/未在用/参数非法这些正常错误路径照旧先报,守卫不吞它们。
* 判据与留痕见 net_ctx_is_owner_task()。 */
if (net_ctx_is_owner_task() == 0U) {
DBG_ERROR("sendto from non-exec task: refused (SPI race, use netTask)");
return NET_ERR_RETURN_1(NET_ERR_NOT_IN_NETTASK);
}
/* UDP 模式:设置目标地址 */
if (dest_addr != NULL && addrlen >= (int)sizeof(struct net_sockaddr_in)) {
/* 强转为 IPv4 地址结构体 */
p_addr_in = (const struct net_sockaddr_in *)dest_addr;
/* 转换 IP 为线序数组(同 TCP 客户端:set_des_ip 要 [0]=第一段)。
* s_addr 的内存字节顺位即线序(2026-10-24 定案:全仓 s_addr 只此一族),按顺位取,
* 不再按数值右移取最高字节——理由与 net_connect_locked 内同一处相同 */
/* 线序 IP 数组 */
uint8_t ip_arr[4];
/* 目的 IP(网络序 32 位) */
uint32_t ip = p_addr_in->sin_addr.s_addr;
/* 字节顺位拷贝:ip_arr[i] = s_addr 的第 i 个内存字节 */
(void)memcpy(ip_arr, &ip, sizeof(ip_arr));
/* 设置目的 IP */
ch395f_set_des_ip(sockfd, ip_arr);
/* 设置目的端口(网络序 → 主机序) */
ch395f_set_des_port(sockfd, net_ntohs(p_addr_in->sin_port));
}
/* 记录起始时刻(用于停滞超时判定) */
tick_start = HAL_GetTick();
/* 分块发送循环 */
while (sent < len) {
/* 剩余待发字节 */
int chunk = len - sent;
/* 单块以硬件发送缓冲为上限(TRAP-NET-16) */
if (chunk > NET_SEND_CHUNK_MAX) {
chunk = NET_SEND_CHUNK_MAX;
}
/* 发送缓冲未空:等待腾出 */
if (!p_sock->send_ready) {
/* 停滞超时 */
if (NET_SEND_TIMEOUT_MS > 0 &&
(HAL_GetTick() - tick_start) >= NET_SEND_TIMEOUT_MS) {
DBG_ERROR("sock%d send stall: send_ready not set (%d/%d)", sockfd, sent, len);
(void)net_set_errno(NET_ERR_TIMEDOUT);
break;
}
/* 轮询一次,下一轮再试 */
net_poll();
continue;
}
/* 本块写入芯片发送 FIFO */
ch395f_write_send_buf(sockfd, (uint8_t *)buf + sent, (uint16_t)chunk);
/* 标记发送缓冲占用中 */
p_sock->send_ready = 0;
/* 累计发送偏移 */
sent += chunk;
/* 累计发送字节统计 */
p_sock->send_bytes += (uint32_t)chunk;
/* 等待发送完成 */
/* 重置本等待阶段计时 */
tick_start = HAL_GetTick();
/* 等待本块发完(SENBUF_FREE 中断重新置 send_ready) */
while (!p_sock->send_ready) {
/* 轮询:处理 SEND_OK/SENBUF_FREE 中断 */
net_poll();
/* 等待超时:本块放弃(外层统一处理) */
if (NET_SEND_TIMEOUT_MS > 0 &&
(HAL_GetTick() - tick_start) >= NET_SEND_TIMEOUT_MS) {
break;
}
}
}
/* 发送未完成 = 短写 */
if (sent < len) {
/* 短写告警:调用方必须检查返回值。静默截断会让对端永久等待剩余字节
* (2026-09-21 实证:UI 推流应答被截断 → 客户端反复判定"全程无字节"失联)。 */
DBG_ERROR("sock%d short send %d/%d (errno=%d)", sockfd, sent, len,
s_net_errno_slots[net_errno_slot()].err);
}
/* 返回实际发送字节数(调用方必须检查) */
return sent;
}
/*
* 函数功能:接收 UDP 数据报(含源地址)
* 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS
* buf - 接收缓冲区指针 void *
* len - 缓冲区大小 int > 0
* flags - 标志位,通常为 0 int
* 出口参数:src_addr - 输出:发送方地址结构体指针 struct net_sockaddr *(可为 NULL)
* addrlen - 输入输出:源地址长度 int *(可为 NULL)
* 返回值:实际接收字节数 int >= 0 || -1
* -1 - 接收失败 int
* 限定条件:Socket 已创建且为 UDP 类型(SOCK_DGRAM);net_init() 已调用;有数据可用
* 函数说明:1. 轮询检查芯片接收缓冲(net_poll + get_recv_len),有数据即返回
* 2. UDP 数据报前 8 字节为报文头(源端口+源 IP),据此填充 src_addr
* 3. 无数据时(与 net_recv 同构):DONTWAIT / 非阻塞属性
* 立即返回 -1(WOULDBLOCK);阻塞模式**无限等待**(30s 默认预算已删;
* 本接口为轮询自旋实现且无生产调用者,受 IWDG 监控的任务勿用无限形态)
*/
int net_recvfrom(int sockfd, void *buf, int len, int flags,
struct net_sockaddr *src_addr, int *addrlen) {
/* Socket 控制块指针 */
net_sock_t *p_sock;
/* 参数检查 */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
/* 描述符越界 → BADF */
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 取控制块 */
p_sock = &s_net_socks[sockfd];
/* 控制块未在用 → BADF */
if (!p_sock->in_use) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 缓冲区无效 → INVAL */
if (buf == NULL || len <= 0) {
return NET_ERR_RETURN_1(NET_ERR_INVAL);
}
/* netTask 上下文守卫:本接口自旋 net_poll + 读芯片缓冲,同样只允许执行任务调;
* 位置与 net_sendto 同一原则(参数校验之后、任何芯片事务之前) */
if (net_ctx_is_owner_task() == 0U) {
DBG_ERROR("recvfrom from non-exec task: refused (SPI race, use netTask)");
return NET_ERR_RETURN_1(NET_ERR_NOT_IN_NETTASK);
}
/* 轮询直到:有数据 / 非阻塞(阻塞 = 无限等待,无默认预算) */
while (1) {
/* 轮询一次:更新状态机与可读标志 */
net_poll();
/* 查询芯片侧可用字节数 */
int recv_len = ch395f_get_recv_len(sockfd);
/* 有数据 */
if (recv_len > 0) {
/* UDP Server 模式:前 8 字节为固定格式(IP + 端口 + 长度)*/
/* UDP 数据报:解析 8 字节报文头 */
if (p_sock->type == NET_SOCK_DGRAM && recv_len >= 8) {
/* 8 字节报文头 */
uint8_t header[8];
/* 读出报文头(剥头 = 消费芯片缓冲) */
ch395f_read_recv_buf(sockfd, header, 8);
/* 数据长度 = 总长 - 头长 */
recv_len -= 8;
/* 填充来源地址 */
if (src_addr != NULL &&addrlen != NULL &&
*addrlen >= (int)sizeof(struct net_sockaddr_in)) {
/* 强转为 IPv4 地址结构体 */
struct net_sockaddr_in *p_addr_in = (struct net_sockaddr_in *)src_addr;
/* 地址族 = IPv4 */
p_addr_in->sin_family = NET_AF_INET;
/* CH395F UDP 头 [0-1] 数据长度(LE) [2-3] src_port(LE) [4-7] src_ip(§9.2.4 表) */
/* 源端口(小端 2 字节 → 主机序 → 网络序) */
p_addr_in->sin_port = net_htons((uint16_t)(header[2] | (header[3] << 8)));
/* 源 IP:芯片给的是线序字节([4]=第一段),按"首段落在最低
* 字节"组装即得网络序整数——与本层 s_addr 唯一约定同族
* (2026-10-24 定案:全仓 s_addr 只此一族),所以这里**不能再加一次换端**。
* 源 IP(小端 4 字节组装为网络序) */
p_addr_in->sin_addr.s_addr = ((uint32_t)header[4]) |
((uint32_t)header[5] << 8) |
((uint32_t)header[6] << 16) |
((uint32_t)header[7] << 24);
/* 回填结构体长度 */
*addrlen = sizeof(struct net_sockaddr_in);
}
}
/* 读取数据 */
if (recv_len > 0) {
/* 本次读取量 = min(可用, 缓冲容量) */
int read_len = (recv_len > len) ? len : recv_len;
/* 读入调用方缓冲区 */
ch395f_read_recv_buf(sockfd, (uint8_t *)buf, (uint16_t)read_len);
/* 累计接收字节统计 */
p_sock->recv_bytes += (uint32_t)read_len;
/* 返回本次读出字节数 */
return read_len;
}
}
/* 非阻塞模式:NET_MSG_DONTWAIT 单次覆盖,或 socket blocking 属性为 0
* (与 net_recv_locked 同判据):无数据立即返回 */
if ((flags & NET_MSG_DONTWAIT) || (p_sock->blocking == 0U)) {
/* 置 WOULDBLOCK */
return NET_ERR_RETURN_1(NET_ERR_WOULDBLOCK);
}
/* 阻塞模式:无数据继续轮询等待(无限等待,无默认预算——
* 旧 NET_RECV_TIMEOUT_MS 30s 兜底已删;唤醒源 = 数据,与 net_recv 的
* 差异是本接口为自旋实现,勿在受 IWDG 监控任务中使用无限形态) */
}
}
/*
* 函数功能:发送数据(内部版本,绕过消息队列,供 netTask 直接使用)
* 入口参数:p_sock - Socket 控制块指针 net_sock_t *
* buf - 待发送数据缓冲区指针 const void *
* len - 数据长度 int > 0
* 出口参数:无
* 返回值:实际发送字节数(>= 0);-1 表示参数无效 / 需等待但被拒(BUSY) int
* 限定条件:p_sock 非空;netTask 上下文(避免向自身发送消息造成死锁)
* 函数说明:1. 直接调用 net_send_locked 完成发送
* 2. SEND pending 化后:发送缓冲未及时腾出(超 NET_SEND_SPIN_MS)时,
* 挂起登记会被"拒绝执行者自等待"守卫拒绝(netTask 内无人能完成它)
* → 返回 -1(BUSY)。本接口因此只适合"发送缓冲可用"的场合;
* netTask 内需要长时间等待的发送,须自行分片或改用挂起模型
*/
int net_send_sock(net_sock_t *p_sock, const void *buf, int len) {
/* 控制块指针为空 → BADF */
if (p_sock == NULL) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 直接委托锁定实现(netTask 上下文,无队列往返);
* caller = netTask 自身 → 挂起登记必被拒(自等死锁守卫) */
return net_send_locked(p_sock, buf, len, xTaskGetCurrentTaskHandle());
}
/*
* 函数功能:接收数据(内部版本,绕过消息队列,供 netTask 直接使用)
* 入口参数:p_sock - Socket 控制块指针 net_sock_t *
* buf - 接收缓冲区指针 void *
* len - 缓冲区大小 int > 0
* flags - 标志位,NET_MSG_DONTWAIT=非阻塞 int
* 出口参数:buf - 填充接收到的数据
* 返回值:实际接收字节数 int >= 0 || 0 || -1
* 0 - 对端关闭连接 int
* -1 - 接收失败 int
* 限定条件:必须在 netTask 上下文中调用;p_sock->in_use == 1
* 函数说明:1. 直接操作 Socket 控制块,不经过消息队列。避免 netTask 向自身发送消息造成死锁
* 2. pending 化后:netTask 上下文的阻塞等待会被挂起登记拒绝(自通知死锁)
* 3. 本函数阻塞用法仅对 DONTWAIT 有效
*/
int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags) {
/* 控制块指针为空 → BADF */
if (p_sock == NULL) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 直接委托锁定实现(netTask 上下文,无队列往返);
* timeout_ms = 0(无限等待语义,但 netTask 自调用会被挂起登记拒绝 →
* 本接口实际仅 DONTWAIT / 非阻塞属性可用);caller = netTask 自身 */
return net_recv_locked(p_sock, buf, len, flags, 0U,
xTaskGetCurrentTaskHandle());
}
/*
* 函数功能:关闭 Socket(内部版本,绕过消息队列,供 netTask 直接使用)
* 入口参数:p_sock - Socket 控制块指针 net_sock_t *
* 出口参数:无
* 返回值:0 - 关闭成功 int
* -1 - 关闭失败 int
* 限定条件:必须在 netTask 上下文中调用;p_sock->in_use == 1
* 函数说明:直接操作 Socket 控制块,不经过消息队列。TCP Socket 先断开连接再释放控制块
*/
int net_close_sock(net_sock_t *p_sock) {
/* 控制块指针为空 → BADF */
if (p_sock == NULL) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 直接委托锁定实现(netTask 上下文,无队列往返) */
return net_close_locked(p_sock);
}
/*
* 函数功能:注册 Socket 事件回调函数
* 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS
* cb - 回调函数指针 net_event_cb_t(可为 NULL 取消注册)
* arg - 用户自定义参数指针 void *
* 出口参数:无
* 返回值:0 - 注册成功 int
* -1 - 注册失败 int
* 限定条件:Socket 已创建且 in_use == 1;net_init() 已调用
* 函数说明:1. 回调在 netTask 上下文触发(事件分派路径内),实现须短小且不得阻塞
* 2. cb 传 NULL 可取消注册
*/
int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg) {
/* Socket 控制块指针 */
net_sock_t *p_sock;
/* 参数检查 */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
/* 描述符越界 → BADF */
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 取控制块 */
p_sock = &s_net_socks[sockfd];
/* 控制块未在用 → BADF */
if (!p_sock->in_use) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 注册回调函数(NULL = 取消注册) */
p_sock->event_cb = cb;
/* 注册用户自定义参数 */
p_sock->event_arg = arg;
/* 注册成功 */
return 0;
}
/*
* 函数功能:设置 Socket 阻塞/非阻塞**模式属性**(模式挂控制块属性,
* 对齐 BSD O_NONBLOCK / Python setblocking)
* 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS
* on - 1 = 阻塞(net_socket 默认);0 = 非阻塞 int
* 出口参数:无
* 返回值:0 - 设置成功 int
* -1 - 失败(描述符越界 / 未占用) int
* 限定条件:socket 已创建(in_use == 1);任意上下文(同 net_set_event_cb 先例)
* 函数说明:1. 属性对 net_recv / net_recv_sock 生效:非阻塞下无数据立即
* NET_ERR_WOULDBLOCK;EOF 判定语义不变(CLOSED/session_dead 无数据
* 仍返回 0 并触发自动重监听)
* 2. 直写控制块单字节(ARM 原子写,先例 net_set_event_cb):与 netTask
* 竞态的最坏结果 = 一次在途 recv 用旧模式,无撕裂、无需过消息队列
* 3. 与超时预算正交:模式是粘滞属性;单次预算用 net_recv_to / net_select
*/
int net_setblocking(int sockfd, int on) {
/* Socket 控制块指针 */
net_sock_t *p_sock;
/* 参数检查:描述符范围 */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
/* 描述符越界 → BADF */
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 取控制块 */
p_sock = &s_net_socks[sockfd];
/* 控制块未在用 → BADF */
if (!p_sock->in_use) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 写模式属性:非 0 = 阻塞,0 = 非阻塞(单字节原子写) */
p_sock->blocking = (on != 0) ? 1U : 0U;
/* 设置成功 */
return 0;
}
/*
* 函数功能:获取 **本任务** 最后一次 net_*() 调用失败时的精确错误码
* 入口参数:无
* 出口参数:无
* 返回值:错误码(NET_OK=0, NET_ERR_xxx=-N);本任务无失败记录时为 NET_OK
* 限定条件:无(任何上下文可调用;中断上下文读 [0] 槽)
* 函数说明:1. E-03 修复:错误码**按任务隔离**——只读本任务自己的槽位,
* 其他任务的 net 调用再也覆盖不了它(旧实现为单一全局量,跨任务竞态)
* 2. 调用成功时**不清零**(保持历史语义,向后兼容既有调用方)
* 3. 超时场景(net_recv_to)请直接用返回码 -2 判定,不要依赖本接口
*/
int net_get_errno(void) {
/* 返回本任务槽位的错误码(只读快照;无槽位时回退 [0] 槽) */
return s_net_errno_slots[net_errno_slot()].err;
}
/*
* 函数功能:查询 Socket 当前状态(只读,无锁)
* 入口参数:sockfd - Socket 描述符 int
* 出口参数:无
* 返回值:状态值(net_sock_state_t);描述符无效/未占用 → -1
* 限定条件:无(单字节读,无撕裂;值可能滞后于 netTask 的最新状态)
* 函数说明:1. 跨任务只读快照;典型用途:检测"芯片静默回收"失步(2026-09-22 台架实证)
* 2. peer 发 FIN 后芯片自行把 Socket 回收回 LISTEN 且不发 DISCONNECT 中断
* 3. MCU 侧状态滞留 ESTABLISHED → 新连接字节被旧会话吞掉
* 4. 上层可据此执行 close+listen 重同步
*/
int net_sock_state(int sockfd) {
/* 描述符越界:返回 -1(不设错误码,只读查询) */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
return -1;
}
/* 控制块未在用:返回 -1 */
if (!s_net_socks[sockfd].in_use) {
return -1;
}
/* 返回状态快照(单字节读,无撕裂;值可能滞后于 netTask 最新状态) */
return (int)s_net_socks[sockfd].state;
}
/* F-NET-3:手动自愈 API net_resync_listen / net_relisten 已删除——
* RESYNC 语义并入对账⑤(net_reconcile_tick 自动认领"芯片静默回收回 LISTEN"形态,
* 200ms×2 去抖 + 1s 冷却沿用);RELISTEN 与 reconciler/D10 完全重叠且对健康监听器是
* footgun(无条件 open→USE→close 自毁 ~1s,原注释自认)。生产调用者本就为零
* (仅 test/board Phase 13,已改由自动机制接管)。历史实现见本文件 git 历史。 */
/*
* 函数功能:将点分十进制 IP 地址字符串转换为网络字节序的 32 位地址
* 入口参数:cp - 点分十进制 IP 字符串(如 "192.168.1.100") const char *
* 出口参数:无
* 返回值:网络字节序的 32 位 IP 地址(0 表示参数无效) uint32_t
* 限定条件:无
* 函数说明:1. 逐段解析并按「第一段在主机序最高字节」累加,再经 NET_HTONL 转网络序,
* 使结果满足 POSIX in_addr_t 的不变量:&s_addr 的第 0 个字节 == 点分第一段。
* 本文件内 s_addr 只有这一族(2026-10-24:两套 IP 字节序约定统一为 POSIX 网络序):net_inet_ntoa 的
* 「先 NTOHL、再高段在前」正是本函数的解析逆运算;net_getsockname、
* net_getpeername、net_recvfrom 填的 s_addr 也同族,故三者可直接相等比较。
* 2. 小端主机(STM32F407 与 PC 模拟器均是)上 "192.168.1.100" 的数值为
* 0x6401A8C0(内存字节 C0 A8 01 64)。旧实现在小端累加后又多做一次
* HTONL,得到 0xC0A80164,与 ntoa 不互逆——那是同一文件里的第二套约定,
* 已连同其消费者(connect/sendto 的 des_ip 拆装)一起改为本族。
* 3. 段值超过 255 时按低 8 位截断(旧实现会把溢出位溅进相邻段,更不可预期);
* 非法字符仍按 0 处理,失败返回值维持 0,不改 NET_INADDR_NONE 的语义。
*/
uint32_t net_inet_addr(const char *cp) {
/* 主机序累加值(第一段最终落在最高字节) */
uint32_t addr = 0;
/* 段循环索引 */
int i;
/* 参数为空:返回 0(无效) */
if (cp == NULL) {
return 0;
}
/* 逐段解析(4 段) */
for (i = 0; i < 4; i++) {
/* 本段数值 */
uint32_t val = 0;
/* 逐字符解析十进制数字 */
while (*cp >= '0' && *cp <= '9') {
val = val * 10 + (*cp - '0');
cp++;
}
/* 高位先入:第 0 段左移三次后落在最高字节 */
addr = (addr << 8) | (val & 0xFFU);
/* 跳过分隔符点(末段可无点) */
if (*cp == '.') {
cp++;
}
}
/* 转网络字节序后返回(此时最低地址字节 = 第一段) */
return NET_HTONL(addr);
}
/*
* 函数功能:将网络字节序的 32 位地址转换为点分十进制字符串
* 入口参数:addr - 网络字节序的 32 位 IP 地址 uint32_t
* buf - 输出缓冲区(至少 16 字节) char *
* 出口参数:buf - 填充点分十进制字符串
* 返回值:成功返回 buf 指针;buf 为 NULL 时返回 NULL char *
* 限定条件:buf 非空且长度足够(>= 16 字节)
* 函数说明:1. 先经 NET_NTOHL 转为主机字节序再格式化输出
* 2. ⚠ 段序(E13 定案 修复):NTOHL 后主机序数值的高字节才是
* 点分第一段。**入参必须是网络序整数**(&addr 的第 0 字节 = 第一段,
* 小端上 192.168.1.100 = 0x6401A8C0,见 net_inet_addr 的约定说明),
* 于是 0x6401A8C0 → NTOHL → 0xC0A80164 → 高字节 0xC0 = 192 首段 ✓。
* 原实现低段在前,同一入参会显示成字节反转的 "100.1.168.192"。
* 3. ⚠ 本注释旧版举的例子写成「192.168.1.100 → 0xC0A80164 → 同串」,
* 那个 0xC0A80164 取的是**已废弃的另一族**(当时 net_inet_addr 的返回
* 值),按本函数的真实算法它打印出来恰是 "100.1.168.192"。两套 s_addr
* 约定能在同一文件里长期共存而不被发现,源头就是这行错例(2026-10-24
* 2026-10-24 修正;函数行为未变,只把例子改对)
*/
char *net_inet_ntoa(uint32_t addr, char *buf) {
/* 网络序 → 主机序 */
uint32_t naddr = NET_NTOHL(addr);
/* 输出缓冲为 NULL:返回 NULL */
if (buf == NULL) {
return NULL;
}
/* 四段格式化为点分十进制(高段在前,16B 缓冲) */
snprintf(buf, 16, "%lu.%lu.%lu.%lu",
(unsigned long)((naddr >> 24) & 0xFF),
(unsigned long)((naddr >> 16) & 0xFF),
(unsigned long)((naddr >> 8) & 0xFF),
(unsigned long)(naddr & 0xFF));
/* 返回输出缓冲 */
return buf;
}
/*
* 函数功能:端口字节序转换(主机序 -> 网络序)
* 入口参数:hostshort - 主机字节序的 16 位端口 uint16_t
* 出口参数:无
* 返回值:网络字节序的 16 位端口 uint16_t
* 限定条件:无
* 函数说明:直接调用 NET_HTONS 完成转换
*/
uint16_t net_htons(uint16_t hostshort) {
/* 主机序 → 网络序(16 位端口) */
return NET_HTONS(hostshort);
}
/*
* 函数功能:端口字节序转换(网络序 -> 主机序)
* 入口参数:netshort - 网络字节序的 16 位端口 uint16_t
* 出口参数:无
* 返回值:主机字节序的 16 位端口 uint16_t
* 限定条件:无
* 函数说明:直接调用 NET_NTOHS 完成转换
*/
uint16_t net_ntohs(uint16_t netshort) {
/* 网络序 → 主机序(16 位端口) */
return NET_NTOHS(netshort);
}
/*
* 函数功能:IP 地址字节序转换(主机序 -> 网络序)
* 入口参数:hostlong - 主机字节序的 32 位 IP 地址 uint32_t
* 出口参数:无
* 返回值:网络字节序的 32 位 IP 地址 uint32_t
* 限定条件:无
* 函数说明:直接调用 NET_HTONL 完成转换
*/
uint32_t net_htonl(uint32_t hostlong) {
/* 主机序 → 网络序(32 位 IP) */
return NET_HTONL(hostlong);
}
/*
* 函数功能:IP 地址字节序转换(网络序 -> 主机序)
* 入口参数:netlong - 网络字节序的 32 位 IP 地址 uint32_t
* 出口参数:无
* 返回值:主机字节序的 32 位 IP 地址 uint32_t
* 限定条件:无
* 函数说明:直接调用 NET_NTOHL 完成转换
*/
uint32_t net_ntohl(uint32_t netlong) {
/* 网络序 → 主机序(32 位 IP) */
return NET_NTOHL(netlong);
}
/*
* 函数功能:分配一个空闲的 Socket 控制块
* 入口参数:无
* 出口参数:无
* 返回值:空闲 Socket 索引(0~NET_MAX_SOCKETS-1);-1 表示无空闲 Socket int
* 限定条件:s_net_socks 已定义
* 函数说明:1. 线性扫描 s_net_socks,返回首个 in_use == 0 的控制块索引
* 2. 调用方负责初始化该控制块
*/
static int alloc_socket(void) {
/* 循环索引 */
int i;
/* 线性扫描首个空闲控制块 */
for (i = 0; i < NET_MAX_SOCKETS; i++) {
/* in_use == 0:空闲槽位 */
if (!s_net_socks[i].in_use) {
/* 返回空闲索引(初始化由调用方负责) */
return i;
}
}
/* 无空闲控制块:返回 -1 */
return -1;
}
/*
* 函数功能:将点分十进制 IP 地址字符串解析为 4 字节数组
* 入口参数:str - 点分十进制 IP 字符串(如 "192.168.1.100") const char *
* ip_arr - 输出:4 字节 IP 数组(网络序,低字节在前) uint8_t *
* 出口参数:ip_arr - 填充解析后的 IP 地址
* 返回值:无
* 限定条件:str 与 ip_arr 均非空
* 函数说明:1. 逐段解析十进制数写入 ip_arr[0..3]
* 2. 参数非法(NULL)时直接返回,不做解析
*/
static void parse_ip_string(const char *str, uint8_t *ip_arr) {
/* 段循环索引 */
int i;
/* 参数非法(NULL):直接返回,不做解析 */
if (str == NULL ||ip_arr == NULL) {
return;
}
/* 逐段解析(4 段) */
for (i = 0; i < 4; i++) {
/* 本段数值 */
uint32_t val = 0;
/* 逐字符解析十进制数字 */
while (*str >= '0' && *str <= '9') {
val = val * 10 + (*str - '0');
str++;
}
/* 本段写入 ip_arr(段 0 = 最低字节,小端) */
ip_arr[i] = (uint8_t)val;
/* 跳过分隔符点(末段可无点) */
if (*str == '.') {
str++;
}
}
}
/*
* 函数功能:根据描述符获取 Socket 控制块指针(内部使用)
* 入口参数:sockfd - Socket 描述符 int 0 - NET_MAX_SOCKETS-1
* 出口参数:无
* 返回值:Socket 控制块指针;描述符越界或未分配时返回 NULL net_sock_t *
* 限定条件:无
* 函数说明:1. 描述符越界(<0 或 >= NET_MAX_SOCKETS)返回 NULL
* 2. 未使用(in_use == 0)的 Socket 返回 NULL
*/
net_sock_t *net_get_sock(int sockfd) {
/* 描述符越界:返回 NULL */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
return NULL;
}
/* Socket 未在用:返回 NULL */
if (!s_net_socks[sockfd].in_use) {
return NULL;
}
/* 返回控制块指针 */
return &s_net_socks[sockfd];
}
/*
* 函数功能:直取 Socket 控制块数组元素,**不判 in_use**(仅供 net 层内部周期对账使用)
* 入口参数:sockfd - Socket 描述符 (0~7),调用方保证在范围内
* 返回值:控制块指针(永不为 NULL)
* 限定条件:netTask 上下文
* 函数说明:net_get_sock() 对未在用槽位返回 NULL,而周期对账必须看到 in_use==0 /
* state==CLOSED 的槽位(形态④ 认领孤儿监听正依赖 CLOSED 态)→ 单独提供。
* 声明在内部头 net_events.h(不进公开 net_socket.h);应用层用 net_get_sock()。
*/
net_sock_t *net_sock_raw(int sockfd) {
return &s_net_socks[sockfd];
}
/*
* 函数功能:获取 Socket 本地地址(IP + Port)
* 入口参数:sockfd - socket 描述符 int
* 出口参数:addr - 输出:本地地址 struct net_sockaddr *
* addrlen - 输入输出:地址结构体长度 int *
* 返回值:0 - 成功,-1 - 失败 int
* 限定条件:socket 已创建或已绑定
* 函数说明:返回 sin_family + sin_port(网络序)+ sin_addr(网络序,从 net_init 缓存的 s_local_ip)
*/
int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen) {
/* 描述符越界 → BADF */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 控制块(const:本函数只读 Socket 信息) */
const net_sock_t *p_sock = &s_net_socks[sockfd];
/* 未在用且状态 CLOSED → BADF */
if (!p_sock->in_use && p_sock->state == NET_SOCK_STATE_CLOSED) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 输出参数无效(NULL 或长度不足)→ INVAL */
if (addr == NULL ||addrlen == NULL || *addrlen < (int)sizeof(struct net_sockaddr_in)) {
return NET_ERR_RETURN_1(NET_ERR_INVAL);
}
/* 强转为 IPv4 地址结构体 */
struct net_sockaddr_in *p_addr_in = (struct net_sockaddr_in *)addr;
/* 清零地址结构体(防御:全部字段归零) */
memset(p_addr_in, 0, sizeof(struct net_sockaddr_in));
/* 地址族 = IPv4 */
p_addr_in->sin_family = NET_AF_INET;
/* 本地端口(主机序 → 网络序) */
p_addr_in->sin_port = net_htons(p_sock->local_port);
/* 本地 IP:s_local_ip[] 是线序数组([0]=第一段),按"首段落在最低字节"
* 组装即得网络序整数 —— 与 net_inet_addr / net_inet_ntoa 同族(2026-10-24 定案),
* 故 getsockname 的结果可直接与 inet_addr 的返回值相等比较 */
p_addr_in->sin_addr.s_addr = ((uint32_t)s_local_ip[0]) |
((uint32_t)s_local_ip[1] << 8) |
((uint32_t)s_local_ip[2] << 16) |
((uint32_t)s_local_ip[3] << 24);
/* 回填结构体长度 */
*addrlen = sizeof(struct net_sockaddr_in);
/* 成功(0) */
return 0;
}
/*
* 函数功能:获取 Socket 对端地址(IP + Port)—— E13 定案 lftpd 对端显示收口
* 入口参数:sockfd - socket 描述符 int
* 出口参数:addr - 输出:对端地址 struct net_sockaddr *
* addrlen - 输入输出:地址结构体长度 int *
* 返回值:0 - 成功,-1 - 失败(NOTCONN:尚无 CONNECT 事件回读过对端) int
* 限定条件:socket 已连接(standalone accept / connect 成功后)
* 函数说明:1. 数据源=CONNECT 事件时 ch395f_get_remot_ipp 回读并缓存的
* remote_ip/remote_port(net_types.h),**不发 SPI**
* 2. 单连接模型下监听 fd 转数据通道后同样有效(对端=刚接入的客户端)
*/
int net_getpeername(int sockfd, struct net_sockaddr *addr, int *addrlen) {
/* 描述符越界 → BADF */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 控制块(const:本函数只读 Socket 信息) */
const net_sock_t *p_sock = &s_net_socks[sockfd];
/* 未在用 → BADF */
if (!p_sock->in_use) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 输出参数无效(NULL 或长度不足)→ INVAL */
if (addr == NULL || addrlen == NULL || *addrlen < (int)sizeof(struct net_sockaddr_in)) {
return NET_ERR_RETURN_1(NET_ERR_INVAL);
}
/* 尚无对端(未连接/未收到 CONNECT 回读)→ NOTCONN */
if (p_sock->remote_port == 0U) {
return NET_ERR_RETURN_1(NET_ERR_NOTCONN);
}
/* 强转为 IPv4 地址结构体 */
struct net_sockaddr_in *p_addr_in = (struct net_sockaddr_in *)addr;
/* 清零地址结构体(防御:全部字段归零) */
memset(p_addr_in, 0, sizeof(struct net_sockaddr_in));
/* 地址族 = IPv4 */
p_addr_in->sin_family = NET_AF_INET;
/* 对端端口(主机序 → 网络序) */
p_addr_in->sin_port = net_htons(p_sock->remote_port);
/* 对端 IP:CONNECT 时已按网络序缓存,直传 */
p_addr_in->sin_addr.s_addr = p_sock->remote_ip;
/* 回填结构体长度 */
*addrlen = sizeof(struct net_sockaddr_in);
/* 成功(0) */
return 0;
}