94 lines
6.7 KiB
Markdown
94 lines
6.7 KiB
Markdown
## 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,增量可接受。
|