## 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，增量可接受。
