2026-03-14 08:47:57 +08:00

6.7 KiB
Raw Blame History

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 SRAMFreeRTOS 堆配置为 12KB
  • WCHNET 是 WCH 私有 TCP/IP 协议栈,非 BSD socket 模型——采用中断回调通知 + 同步轮询接收的混合模式
  • WCHNET 必须周期性调用 WCHNET_MainTask() 和全局中断处理才能驱动协议栈运转
  • 当前 net_config.hWCHNET_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 的阻塞/半阻塞语义
  • 补全 OnConfigUpdateOnDetectionResult 回调逻辑

Non-Goals:

  • 不修改 qdx_tcp_logic.cqdx_protocol.cqdx_preprocess.c 的内部逻辑
  • 不实现外部硬件 DI 触发模式和连拍功能(属于后续 change
  • 不移植到 LwIP 或其他 TCP/IP 协议栈

Decisions

Decision 1FreeRTOS 任务划分

将系统拆分为 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 2WCHNET 接收桥接方案——信号量 + 环形缓冲

WCHNET 通过 SINT_STAT_RECV 中断通知数据到达,而 qdx_port_tcp_recv 被调用方期望为可阻塞/超时返回。桥接方案:

  1. 为每个 socket 维护一个接收环形缓冲区RxRingBuf2920 字节)和一个二值信号量xSemaphoreRx
  2. WCHNET_HandleSockIntSINT_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 3Socket 映射管理

WCHNET 使用 uint8_t socketid0~30标识 socketqdx_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 改为 2WCHNET_MAX_SOCKET_NUM 相应变为 2。

Decision 4TIM2 共享——FreeRTOS Tick + WCHNET Timer

当前 TIM2 以 10ms 周期驱动 WCHNET_TimeIsr()。FreeRTOS 需要 2ms tickconfigTICK_RATE_HZ = 500)。方案:

  • 将 TIM2 周期改为 2ms(匹配 FreeRTOS tick
  • TIM2 ISR 中每次调用 xPortSysTickHandler()FreeRTOS tick
  • 设软件计数器,每累计 5 次(= 10ms调用一次 WCHNET_TimeIsr(WCHNETTIMERPERIOD)

理由:共用一个硬件定时器节省外设资源,软件分频几乎无开销。

Decision 5qdx_port_tcp_connect 中的连接等待

WCHNET 的 WCHNET_SocketConnect() 是异步的——它发起三次握手后立即返回,连接完成通过 SINT_STAT_CONNECT 中断通知。但 qdx_port_tcp_connect 语义是阻塞直到连接建立。

方案:在 SocketCtx_t 中增加 SemaphoreHandle_t connect_sem。调用 WCHNET_SocketConnectxSemaphoreTake(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_SocketRecvxSemaphoreGiveFromISR 必须确保安全。缓解:将 WCHNET_HandleGlobalInt 移出 ISR改为在 task_wchnet 中轮询调用 WCHNET_QueryGlobalInt,这样 recv 回调运行在任务上下文,可安全使用 FreeRTOS API。

[双 Socket 内存增长]WCHNET_NUM_TCP 从 1 变为 2WCHNET_MEM_HEAP_SIZEWCHNET_NUM_POOL_BUF 相应增加约 3KB。缓解CH32V307 有 64KB SRAM增量可接受。