6.7 KiB
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 被调用方期望为可阻塞/超时返回。桥接方案:
- 为每个 socket 维护一个接收环形缓冲区(
RxRingBuf,2920 字节)和一个二值信号量(xSemaphoreRx) - 在
WCHNET_HandleSockInt的SINT_STAT_RECV分支中:调用WCHNET_SocketRecv()将数据读入RxRingBuf,然后xSemaphoreGiveFromISR(xSemaphoreRx) 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 IDuint8_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,增量可接受。