## Context 当前 TCPClient 工程运行在裸机模式下,主循环直接轮询 WCHNET 和 DVP 任务。QDX 网络栈的 `qdx_tcp_logic` 被设计为多线程架构(3 个后台线程:1 个连接管理 + 2 个接收线程),依赖 `qdx_port.h` 定义的 11 个 HAL 函数(线程、互斥锁、延时、TCP socket),但这些函数在 `qdx_port_template.c` 中全部为空 stub。 关键约束: - CH32V307 拥有 64KB SRAM,FreeRTOS 堆配置为 12KB - WCHNET 是 WCH 私有 TCP/IP 协议栈,**非 BSD socket 模型**——采用中断回调通知 + 同步轮询接收的混合模式 - WCHNET 必须周期性调用 `WCHNET_MainTask()` 和全局中断处理才能驱动协议栈运转 - 当前 `net_config.h` 中 `WCHNET_NUM_TCP = 1`,但双流架构(5511 控制流 + 5512 数据流)需要 2 个 TCP socket - FreeRTOS 的 RISC-V 移植已存在于 `prj/FreeRTOS_Core/FreeRTOS/portable/GCC/RISC-V/` ## Goals / Non-Goals **Goals:** - 实现 `qdx_port.c` 全部 11 个 HAL 函数,使 `qdx_tcp_logic` 三线程架构实际运行 - 将裸机主循环迁移为 FreeRTOS 多任务模型 - 桥接 WCHNET 回调模型与 `qdx_port_tcp_recv` 的阻塞/半阻塞语义 - 补全 `OnConfigUpdate` 和 `OnDetectionResult` 回调逻辑 **Non-Goals:** - 不修改 `qdx_tcp_logic.c`、`qdx_protocol.c`、`qdx_preprocess.c` 的内部逻辑 - 不实现外部硬件 DI 触发模式和连拍功能(属于后续 change) - 不移植到 LwIP 或其他 TCP/IP 协议栈 ## Decisions ### Decision 1:FreeRTOS 任务划分 将系统拆分为 4+3 个 FreeRTOS 任务: | 任务 | 优先级 | 栈大小 | 职责 | |------|--------|--------|------| | `task_wchnet` | 6 (高) | 1024 words | 周期调用 `WCHNET_MainTask()` + 全局中断处理 | | `task_business` | 5 | 1024 words | DVP 采集轮询 + 触发判定 + 预处理 + 封包发送 | | `tcp_mgr` | 3 | 512 words | 由 `TcpLogic_Start()` 创建,连接管理与心跳 | | `tcp_rx_c` | 4 | 512 words | 由 `TcpLogic_Start()` 创建,控制流接收 | | `tcp_rx_d` | 4 | 512 words | 由 `TcpLogic_Start()` 创建,数据流接收 | **理由**:`task_wchnet` 优先级最高,因为 WCHNET 协议栈需要及时处理底层以太网帧和 TCP 状态机;`task_business` 次之,确保 DVP 帧不丢失;TCP 后台线程优先级最低,属于非实时任务。 **备选方案**:将 WCHNET 轮询放在定时器回调中而非独立任务——但 `WCHNET_MainTask()` 执行时间不确定,不适合放在中断上下文。 ### Decision 2:WCHNET 接收桥接方案——信号量 + 环形缓冲 WCHNET 通过 `SINT_STAT_RECV` 中断通知数据到达,而 `qdx_port_tcp_recv` 被调用方期望为可阻塞/超时返回。桥接方案: 1. 为每个 socket 维护一个**接收环形缓冲区**(`RxRingBuf`,2920 字节)和一个**二值信号量**(`xSemaphoreRx`) 2. 在 `WCHNET_HandleSockInt` 的 `SINT_STAT_RECV` 分支中:调用 `WCHNET_SocketRecv()` 将数据读入 `RxRingBuf`,然后 `xSemaphoreGiveFromISR(xSemaphoreRx)` 3. `qdx_port_tcp_recv()` 实现:先检查 `RxRingBuf` 是否有数据,有则直接拷贝返回;无则 `xSemaphoreTake(xSemaphoreRx, pdMS_TO_TICKS(100))` 阻塞等待最多 100ms,超时返回 0 **理由**:这种方式让 recv 线程在无数据时让出 CPU(通过信号量阻塞),同时避免了忙等 10ms 轮询的 CPU 浪费。环形缓冲解耦了中断上下文读取和应用层消费的速率差异。 **备选方案**:FreeRTOS Stream Buffer——语义更匹配但引入额外依赖,且 WCHNET 的 `SocketRecv` 已提供了长度信息,环形缓冲更简单可控。 ### Decision 3:Socket 映射管理 WCHNET 使用 `uint8_t socketid`(0~30)标识 socket,而 `qdx_port.h` 使用 `void* qdx_socket_t` 不透明句柄。映射方案: - 维护一个静态数组 `SocketCtx_t g_sock_ctx[MAX_SOCKETS]`(MAX_SOCKETS = 2),每个元素包含: - `uint8_t wchnet_sock_id` — WCHNET socket ID - `uint8_t connected` — 是否已连接 - `RxRingBuf_t rx_ring` — 接收环形缓冲 - `SemaphoreHandle_t rx_sem` — 接收通知信号量 - `qdx_port_tcp_connect()` 分配空闲的 `SocketCtx_t`,调用 `WCHNET_SocketCreat` + `WCHNET_SocketConnect`,返回 `&g_sock_ctx[i]` 作为句柄 - `qdx_port_tcp_send/recv/close()` 从句柄中提取 `wchnet_sock_id` 操作 WCHNET API **Config 变更**:`WCHNET_NUM_TCP` 从 1 改为 2,`WCHNET_MAX_SOCKET_NUM` 相应变为 2。 ### Decision 4:TIM2 共享——FreeRTOS Tick + WCHNET Timer 当前 TIM2 以 10ms 周期驱动 `WCHNET_TimeIsr()`。FreeRTOS 需要 2ms tick(`configTICK_RATE_HZ = 500`)。方案: - 将 TIM2 周期改为 **2ms**(匹配 FreeRTOS tick) - TIM2 ISR 中每次调用 `xPortSysTickHandler()`(FreeRTOS tick) - 设软件计数器,每累计 5 次(= 10ms)调用一次 `WCHNET_TimeIsr(WCHNETTIMERPERIOD)` **理由**:共用一个硬件定时器节省外设资源,软件分频几乎无开销。 ### Decision 5:`qdx_port_tcp_connect` 中的连接等待 WCHNET 的 `WCHNET_SocketConnect()` 是异步的——它发起三次握手后立即返回,连接完成通过 `SINT_STAT_CONNECT` 中断通知。但 `qdx_port_tcp_connect` 语义是阻塞直到连接建立。 方案:在 `SocketCtx_t` 中增加 `SemaphoreHandle_t connect_sem`。调用 `WCHNET_SocketConnect` 后 `xSemaphoreTake(connect_sem, pdMS_TO_TICKS(5000))` 阻塞。在 `SINT_STAT_CONNECT` 回调中 `xSemaphoreGive(connect_sem)`。超时返回 NULL。 ## Risks / Trade-offs **[RAM 占用偏紧]** → FreeRTOS 堆 12KB + 5 个任务栈约 9KB + WCHNET 内部约 10KB + 2×2920 socket 缓冲 + 2×2920 环形缓冲 + 2×10KB 发送缓冲区 ≈ 52KB / 64KB。**缓解**:严格控制任务栈大小,使用 `uxTaskGetStackHighWaterMark` 运行时监测;发送缓冲保持 10KB 不变(已经预分配)。如仍紧张可将 FreeRTOS 堆缩减至 8KB。 **[WCHNET 非线程安全]** → WCHNET API 未声明线程安全性,多个 FreeRTOS 任务可能并发调用 send/recv。**缓解**:所有 WCHNET API 调用(send、recv、socket 操作)统一通过 `task_wchnet` 任务的消息队列委托执行,或者使用全局互斥锁保护。初期采用互斥锁方案,复杂度更低。 **[中断上下文限制]** → `WCHNET_HandleSockInt` 在中断上下文被调用(通过 `WCHNET_HandleGlobalInt` → TIM2/ETH ISR 链),其中调用 `WCHNET_SocketRecv` 和 `xSemaphoreGiveFromISR` 必须确保安全。**缓解**:将 `WCHNET_HandleGlobalInt` 移出 ISR,改为在 `task_wchnet` 中轮询调用 `WCHNET_QueryGlobalInt`,这样 recv 回调运行在任务上下文,可安全使用 FreeRTOS API。 **[双 Socket 内存增长]** → `WCHNET_NUM_TCP` 从 1 变为 2,`WCHNET_MEM_HEAP_SIZE` 和 `WCHNET_NUM_POOL_BUF` 相应增加约 3KB。**缓解**:CH32V307 有 64KB SRAM,增量可接受。