Files
TCP2UART/项目技术实现.md
T

21 KiB
Raw Permalink Blame History

TCP2UART 项目技术实现

一、文档目的

本文档描述 TCP2UART 项目基于 STM32F103RCT6 + FreeRTOS 的最终内部实现口径。

本文档只围绕最终协议模型展开:

  • MUX:串口承载层
  • NET:全局网络配置层
  • LINK[idx]:实例配置与连接管理层

不再保留历史 S1... / C1... 外部字段模型。

二、当前工程基础

当前工程基础约束如下:

  1. MCUSTM32F103RCT6256KB Flash / 48KB SRAM
  2. 网络芯片:CH390D
  3. 软件架构:FreeRTOS + lwIP NO_SYS=0
  4. 协议栈:lwIP socket/netconn API
  5. 调试输出:SEGGER RTT
  6. 使用 FreeRTOS 任务调度
  7. 不实现 DHCP

三、总体架构

+--------------------------------------------------+
| AT / Control Plane                               |
| USART1 AT parser + MUX control frame parser      |
+--------------------------------------------------+
| Configuration Model                              |
| MUX / NET / LINK[idx]                            |
+--------------------------------------------------+
| FreeRTOS Tasks                                   |
| NetworkTask / UartTask / ConfigTask / RouteTask  |
+--------------------------------------------------+
| Inter-Task Communication                         |
| Queue / Semaphore / Mutex / StreamBuffer         |
+--------------------------------------------------+
| lwIP TCP/IP Stack (NO_SYS=0)                     |
| tcpip_thread + socket/netconn + sys_arch         |
+--------------------------------------------------+
| Driver Layer                                     |
| CH390 / lwIP netif / UART DMA+IDLE / HAL         |
+--------------------------------------------------+

四、FreeRTOS 任务设计(路径 Anetconn + 多 TCP 任务)

4.1 架构路线

本项目采用 路径 ANO_SYS=0 + netconn API + 每个 TCP 连接独立任务

核心决策:

  1. lwIP 以 NO_SYS=0 模式运行,tcpip_thread 由 lwIP 自动创建
  2. TCP 连接使用 netconn 阻塞 APInetconn_accept / netconn_recv / netconn_write
  3. 每个 TCP Server 和 Client 实例各占一个独立任务
  4. 任务间通过 Queue 传递指针 + 元数据描述符,实现零拷贝

4.2 任务列表(共 9 个任务 + 1 个 lwIP 自建)

任务名 优先级 栈(words) 模式 职责
tcpip_thread 6 (最高) 512 阻塞 lwIP 内核线程(自动创建)
NetPollTask 5 384 事件驱动 ethernetif_poll + 链路检测
TcpSrvTask_S1 4 384 阻塞 netconn_accept + S1 收发
TcpSrvTask_S2 4 384 阻塞 netconn_accept + S2 收发
TcpCliTask_C1 4 256 阻塞 netconn_connect + C1 收发
TcpCliTask_C2 4 256 阻塞 netconn_connect + C2 收发
UartRxTask 4 384 事件驱动 UART DMA/IDLE 接收 + MUX 帧提取
ConfigTask 2 256 阻塞 AT 命令解析与响应
DefaultTask 1 128 周期 LED 心跳 + IWDG 喂狗

说明:

  • tcpip_thread 是 lwIP 自建的内核线程,处理所有协议栈内部事件
  • 所有 netconn_* API 通过 tcpip_thread 消息机制实现线程安全
  • LWIP_TCPIP_CORE_LOCKING=1 允许应用任务直接调用 netconn_write 而不经邮箱中转
  • tcpip_thread 优先级最高(6),确保 TCP ACK 和超时处理不被延迟

4.3 零拷贝路由消息设计

/* 路由消息描述符 - Queue 传递的是此结构的指针,不拷贝负载数据 */
typedef struct {
    uint8_t  src_id;       /* 源端点 ID */
    uint8_t  dst_mask;     /* 目标端点位图 */
    uint16_t len;          /* 数据长度 */
    uint8_t  conn_type;    /* 连接标识:LINK_S1/S2/C1/C2 */
    uint8_t *data;         /* 指向预分配静态缓冲区 */
} route_msg_t;

静态缓冲池(预分配,避免动态分配):

#define ROUTE_BUF_COUNT  4
#define ROUTE_BUF_SIZE   512

static uint8_t  g_route_buf_pool[ROUTE_BUF_COUNT][ROUTE_BUF_SIZE];
static volatile uint8_t g_route_buf_used[ROUTE_BUF_COUNT];
  • 发送方:从池中获取空闲缓冲区,拷贝数据,填充 route_msg_tQueue 发送指针
  • 接收方:从 Queue 取 route_msg_t*,处理数据后标记缓冲区为可用
  • 无动态分配,无堆碎片

4.4 任务间通信机制

UART ISR ──[TaskNotify]──> UartRxTask ──[Queue*]──> TcpSrvTask / TcpCliTask
                              │                         ▲
                              ├──[Queue*]──────────────>─┘
                              │
                    DSTMASK=0 └──[Queue]──> ConfigTask

TcpSrvTask / TcpCliTask ──[Queue*]──> UartRxTask (UART TX 方向)

EXTI0 ISR ──[BinarySem]──> NetPollTask ──> ethernetif_poll ──> tcpip_thread

通信对象:

对象 类型 生产者 消费者 用途
xNetSemaphore Binary Semaphore EXTI0 ISR NetPollTask CH390 中断通知
xUartRxNotify TaskNotification UART IDLE ISR UartRxTask UART 接收通知
xTcpRxQueue Queue (16, route_msg_t*) TCP 任务 UartRxTask TCP→UART 数据
xUartTxQueue Queue (8, route_msg_t*) UartRxTask TCP 任务 UART→TCP 数据
xConfigQueue Queue (8, char*) UartRxTask ConfigTask AT 文本

说明:

  • TCP→UART 方向:TCP 任务调用 netconn_recv 获取数据,构造 route_msg_t 指针投递到 xTcpRxQueueUartRxTask 取出后写入 UART DMA
  • UART→TCP 方向:UartRxTask 从 UART DMA 读取数据,构造 route_msg_t 指针投递到 xUartTxQueue,对应 TCP 任务取出后调用 netconn_write 发送
  • 路由逻辑内联在 UartRxTask 和各 TCP 任务中,不设独立 RouteTask

4.5 NetPollTask 实现

void NetPollTask(void *argument)
{
    /* 初始化 CH390 + lwIP netif */
    ethernetif_init(&ch390_netif);
    /* 等待链路就绪后启动 TCP 任务 */
    /* ... */

    for (;;) {
        xSemaphoreTake(xNetSemaphore, pdMS_TO_TICKS(2));
        ethernetif_poll(&ch390_netif);
        ethernetif_check_link(&ch390_netif);
        /* sys_check_timeouts() 由 tcpip_thread 自动执行,此处不需要调用 */
    }
}

4.6 TcpSrvTask 实现模板

void TcpSrvTask_S1(void *argument)
{
    struct netconn *conn = netconn_new(NETCONN_TCP);
    netconn_bind(conn, IP_ADDR_ANY, cfg->links[0].lport);
    netconn_listen(conn);

    for (;;) {
        struct netconn *newconn;
        if (netconn_accept(conn, &newconn) == ERR_OK) {
            /* 在本任务内处理唯一客户端 */
            tcp_server_worker(newconn, LINK_S1);
            netconn_close(newconn);
            netconn_delete(newconn);
        }
    }
}

4.7 TcpCliTask 实现模板

void TcpCliTask_C1(void *argument)
{
    for (;;) {
        struct netconn *conn = netconn_new(NETCONN_TCP);
        ip_addr_t remote_ip;
        IP_ADDR4(&remote_ip, cfg->links[2].rip[0], ...);

        if (netconn_connect(conn, &remote_ip, cfg->links[2].rport) == ERR_OK) {
            tcp_client_worker(conn, LINK_C1);
        }
        netconn_close(conn);
        netconn_delete(conn);
        vTaskDelay(pdMS_TO_TICKS(cfg->links[2].reconnect_interval));
    }
}

4.8 UartRxTask 实现

void UartRxTask(void *argument)
{
    for (;;) {
        ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(10));
        /* 处理 UART2/UART3 DMA 接收数据 */
        /* MUX=0: 构造 route_msg_t 指针投递到 xUartTxQueue */
        /* MUX=1: 提取 MUX 帧,DSTMASK=0 投 xConfigQueue,否则投 xUartTxQueue */
        /* 从 xTcpRxQueue 取数据,写入 UART DMA 发送 */
    }
}

4.9 ConfigTask 实现

void ConfigTask(void *argument)
{
    for (;;) {
        char *cmd;
        xQueueReceive(xConfigQueue, &cmd, portMAX_DELAY);
        config_process_at_cmd(cmd);
        /* 通过 UART1 发送响应 */
    }
}

五、最终协议实现模型

5.1 MUX 帧承载层

数据口启用 MUX 后,统一处理如下帧:

SYNC | LEN_H | LEN_L | SRCID | DSTMASK | PAYLOAD | TAIL

实现职责:

  1. 识别帧边界
  2. 解析长度字段
  3. 提取 SRCID
  4. 解析 DSTMASK
  5. 按控制帧或数据帧分流

5.2 控制帧与数据帧分离

控制规则固定如下:

  • DSTMASK = 0x00:系统控制帧
  • DSTMASK != 0x00:业务数据帧

系统控制帧处理要求:

  1. PAYLOAD 解释为 AT 文本
  2. AT 文本必须以 \r\n 结束
  3. 控制帧投递到 ConfigTask

业务数据帧处理要求:

  1. SRCID 表示单一源端点
  2. DSTMASK 表示目标端点集合
  3. RouteTask 根据 DSTMASK 做多目标分发

5.3 统一端点编码

内部与外部文档统一使用以下端点编码:

端点 编码
C1 0x01
C2 0x02
UART2 0x04
UART3 0x08
S1 0x10
S2 0x20

实现要求:

  • SRCID 为单值
  • DSTMASK 为位图
  • DSTMASK=0x00 仅保留为控制帧

六、配置层设计

6.1 MUX 记录

MUX 为全局记录,仅控制设备数据口是否进入 MUX 承载模式。

取值:

  • 0:普通透传
  • 1MUX 透传

6.2 NET 记录

NET 为全局静态网络记录:

IP,MASK,GW,MAC

说明:

  • 设备只有一张网卡,因此不为每个实例单独配置本地 IP
  • 当前实现目标中不包含 DHCP

LINK[idx] 为统一实例记录:

EN,LPORT,RIP,RPORT,UART

固定索引映射:

  • 0 = S1
  • 1 = S2
  • 2 = C1
  • 3 = C2

字段职责:

  • EN:实例启用状态
  • LPORT:本地端口
  • RIP / RPORT:对端地址与端口
  • UART:对应业务数据口

说明:

  • ServerClient 共享同一记录结构
  • ServerRIP / RPORT 可作为对端约束或预设
  • ClientRIP / RPORT 表示远端目标

七、模块职责

7.1 配置模块 config.c/.h

最终职责:

  1. 解析 AT+MUX
  2. 解析 AT+NET
  3. 解析 AT+LINK
  4. 加载与保存配置
  5. 处理 SAVE / RESET / DEFAULT

7.2 UART 透传模块 uart_trans.c/.h

最终职责:

  1. 保持 USART2 / USART3DMA + IDLE 接收发送基线
  2. MUX=0 时执行普通透传
  3. MUX=1 时执行 MUX 帧收发
  4. 将控制帧与业务数据帧分流

7.3 TCP Server / Client 模块(需重写)

tcp_server.c / tcp_client.c 基于 lwIP RAW API 回调模式,需重写为 netconn 阻塞模式:

  1. 删除所有 tcp_pcb / tcp_recv / tcp_accept 回调代码
  2. 改用 netconn_new / netconn_bind / netconn_listen / netconn_acceptServer
  3. 改用 netconn_new / netconn_connectClient
  4. 收发改为 netconn_recv / netconn_write 阻塞调用
  5. 内部 ring buffer 可取消(netconn 内部已有 pbuf 缓冲)
  6. 每个 TCP 任务内直接处理路由,通过 Queue 指针传递数据

补充约束(当前实现口径):

  1. Client 链路保留固定 LPORT 配置语义,以满足产品侧对固定源端口的依赖
  2. lwIP + netconn 模型下,若 Client 继续使用优雅 netconn_close(),相同本地端口的快速重连会受 TIME_WAIT 影响
  3. 因此当前工程对 Client 会话结束后的释放路径采用 abortive closetcp_abort / RST)以立即释放 PCB 与本地端口
  4. 该策略只针对 Client 固定端口重连路径,不扩展到 Server listener 或一般被动关闭场景
  5. 该策略的已知代价是:对端可能看到 RST,且尾部未完成发送的数据不会再走优雅 FIN/ACK 收尾

7.4 FreeRTOS 初始化 freertos.c

CubeMX 生成的 FreeRTOS 初始化文件,职责:

  1. 定义默认任务 StartDefaultTask
  2. 用户在 MX_FREERTOS_Init 中创建自定义任务

7.5 FreeRTOS 配置 FreeRTOSConfig.h

关键配置项:

配置项 说明
configUSE_PREEMPTION 1 抢占式调度
configTICK_RATE_HZ 1000 1ms tick
configMINIMAL_STACK_SIZE 128 最小栈(words
configTOTAL_HEAP_SIZE 10240 FreeRTOS 堆大小
configMAX_PRIORITIES 7 最大优先级数
configUSE_MUTEXES 1 启用互斥锁
configUSE_COUNTING_SEMAPHORES 1 启用计数信号量
configUSE_RECURSIVE_MUTEXES 1 启用递归互斥锁
configCHECK_FOR_STACK_OVERFLOW 2 栈溢出检测方式 2
configUSE_MALLOC_FAILED_HOOK 1 内存分配失败钩子
configSUPPORT_DYNAMIC_ALLOCATION 1 动态内存分配

八、lwIP 配置(NO_SYS=0 + netconn

8.1 lwIP 线程模型

由于采用 NO_SYS=0lwIP 将运行以下线程:

  1. tcpip_thread(优先级 6,栈 512 words):lwIP 核心线程,处理所有协议栈内部事件
  2. 应用任务通过 netconn API 与 lwIP 交互,由 tcpip_thread 消息机制保证线程安全
  3. LWIP_TCPIP_CORE_LOCKING=1:允许应用任务在持有核心锁时直接调用 netconn_write,无需通过邮箱中转

8.2 lwIP 关键配置项(lwipopts.h

配置项 说明
NO_SYS 0 启用 OS 抽象
LWIP_NETCONN 1 启用 netconn API
LWIP_SOCKET 0 不使用 socket API,节省 RAM
LWIP_TCPIP_CORE_LOCKING 1 允许直接发送,减少延时
MEM_SIZE 8192 lwIP 堆大小
PBUF_POOL_SIZE 10 pbuf 池数量
MEMP_NUM_NETCONN 8 2监听 + 4连接 + 2余量
MEMP_NUM_NETBUF 8 netconn 缓冲
MEMP_NUM_TCP_PCB 4 TCP 控制块
MEMP_NUM_TCP_PCB_LISTEN 2 TCP 监听
MEMP_NUM_TCP_SEG 24 TCP 段
MEMP_NUM_TCPIP_MSG_API 8 API 消息池
MEMP_NUM_TCPIP_MSG_INPKT 8 入包消息池
TCP_MSS 536 保守 MSS
TCP_SND_BUF 8×MSS=4288 发送缓冲
TCP_WND 8×MSS=4288 接收窗口
TCPIP_THREAD_STACKSIZE 512 tcpip_thread 栈
TCPIP_THREAD_PRIO 6 (最高) tcpip_thread 优先级
LWIP_DHCP 0 不使用 DHCP
LWIP_UDP 0 不使用 UDP

8.3 sys_arch 移植层

Drivers/LwIP/port/sys_arch.c 需实现 lwIP 到 FreeRTOS 的适配:

  1. sys_thread_new:创建 lwIP 线程(即 tcpip_thread
  2. sys_mbox_*:消息邮箱(基于 FreeRTOS Queue,容量 TCPIP_MBOX_SIZE=8
  3. sys_sem_*:信号量(基于 FreeRTOS Semaphore
  4. sys_mutex_*:互斥锁(基于 FreeRTOS Mutex
  5. sys_arch_protect / unprotect:临界区保护(基于 vPortEnterCritical / vPortExitCritical

8.4 lwIP 初始化流程

/* 在 MX_FREERTOS_Init 或 NetPollTask 中调用 */
void lwip_init_task(void)
{
    /* tcpip_thread 在 lwip_init() 或第一个 sys_thread_new 时自动启动 */
    tcpip_init(NULL, NULL);
    
    /* 等待 tcpip_thread 就绪 */
    /* 添加网络接口 */
    netif_add(&ch390_netif, &ipaddr, &netmask, &gw, NULL, &ethernetif_init, &tcpip_input);
    netif_set_default(&ch390_netif);
    netif_set_up(&ch390_netif);
}

九、中断与 HAL 时间基准

9.1 HAL 时间基准

FreeRTOS 下 SysTick 被 FreeRTOS 占用,HAL 时间基准改用 TIM4

  • TIM4 配置为 1ms 中断(72MHz / (71+1) / (999+1) = 1kHz
  • HAL_InitTick 使用 TIM4 而非 SysTick
  • uwTickTIM4_IRQHandler 中递增

9.2 中断优先级规划

中断 优先级 说明
SysTick 15(最低) FreeRTOS tick
PendSV 15(最低) FreeRTOS 上下文切换
SVCall 0 FreeRTOS 服务调用
TIM4 0 HAL 时间基准
EXTI0 5 CH390 中断
DMA1_Ch2~7 5 UART DMA
USART1/2/3 5 UART 中断
SPI1 5 SPI 中断

FreeRTOS 可管理的中断优先级必须 >= configMAX_SYSCALL_INTERRUPT_PRIORITY(本工程为 5)。

十、内存预算(路径 A 精确估算)

STM32F103RCT6 为目标(48KB SRAM):

10.1 RAM 预算

项目 大小 说明
启动栈 (MSP) 2,048 B startup_stm32f103xe.s 定义 0x800
FreeRTOS 堆 (heap_4) 10,240 B configTOTAL_HEAP_SIZE
任务栈 (9 任务) 13,312 B 3,328 words × 4 (见上表)
lwIP 堆 (MEM_SIZE) 8,192 B
PBUF 池 (10 个) ~6,000 B 10 × ~600B
MEMP 池 ~4,000 B netconn/netbuf/tcpip_msg/pcb/seg
UART DMA+Ring 缓冲 2,304 B 2 通道 × (256+256+512+384)/2
路由缓冲池 (4×512B) 2,048 B 零拷贝指针传递
配置结构 1,024 B
合计 ~49,168 B
余量 (RCT6 48KB) ~-928 B ⚠️ 超出,需优化或换 RDT6
余量 (RDT6 64KB) ~15,264 B 充裕

10.2 优化空间(RCT6 下)

若坚持使用 RCT6,可通过以下措施压缩到 48KB 以内:

优化项 节省
取消 TCP 任务的 ring buffernetconn 内部有 pbuf 缓冲) -2,048 B
configTOTAL_HEAP_SIZE 降至 8KB -2,048 B
MEM_SIZE 降至 6KB -2,048 B
TcpCliTask 栈降至 192 words × 2 -512 B
优化后合计 ~42,504 B
RCT6 余量 ~5,464 B

10.3 备选 MCU

当 RAM 最终不够用时,切换为 STM32F103RDT6pin-to-pin 替代):

项目 RCT6 RDT6
Flash 256 KB 384 KB
SRAM 48 KB 64 KB
引脚 LQFP64 LQFP64(完全兼容)
启动文件 startup_stm32f103xe.s startup_stm32f103xe.s
宏定义 STM32F103xE STM32F103xE
Flash 算法 STM32F10x_HD STM32F10x_HD(相同)
SRAM 大小 0xC000 0x10000
Flash 大小 0x40000 0x60000

10.4 Flash 预算

项目 估计值 说明
FreeRTOS 内核 ~8 KB 含 CMSIS-RTOS V2
HAL 驱动 ~20 KB GPIO/UART/SPI/DMA/IWDG/TIM
lwIP 协议栈 ~50 KB core + api + ipv4 + netif + sys_arch
CH390 驱动 ~4 KB
应用代码 ~20 KB config/uart_trans/tcp_server/tcp_client
合计 ~102 KB RCT6 预留 154 KBRDT6 预留 282 KB

十一、硬件资源

11.1 MCU

  • 型号:STM32F103RCT6
  • Flash256 KB
  • SRAM48 KB
  • 主频:72 MHz

11.2 主要外设

  • SPI1:连接 CH390D
  • USART1:配置串口
  • USART2:数据透传串口
  • USART3:数据透传串口
  • DMA13 路 UART 收发 DMA
  • EXTI0CH390 中断输入
  • IWDG:独立看门狗
  • TIM4HAL 时间基准(替代 SysTick

11.3 引脚分配

引脚 功能 用途
PA2 USART2_TX 数据透传串口
PA3 USART2_RX 数据透传串口
PA4 SPI1_NSS CH390D 片选
PA5 SPI1_SCK CH390D SPI 时钟
PA6 SPI1_MISO CH390D SPI 数据输入
PA7 SPI1_MOSI CH390D SPI 数据输出
PA9 USART1_TX 配置串口
PA10 USART1_RX 配置串口
PB0 EXTI0 CH390D INT
PB1 GPIO_Output CH390D RESET
PB10 USART3_TX 数据透传串口
PB11 USART3_RX 数据透传串口
PC13 GPIO_Output 状态 LED
PD0/PD1 HSE 8MHz 外部晶振

十二、实现边界

  1. 保持单网卡静态网络模型
  2. 不实现 DHCP
  3. 不实现旧 S1... / C1... 外部协议字段
  4. 不在文档中保留兼容层描述
  5. 所有 AT 文本控制统一要求 \r\n 结束
  6. FreeRTOS 堆管理使用 heap_4.c
  7. HAL 时间基准使用 TIM4 而非 SysTick

十三、文档一致性要求

后续实现、联调、测试与代码注释必须遵守以下统一口径:

  1. 对外协议只使用 MUX / NET / LINK
  2. 控制帧只使用 DSTMASK=0x00
  3. MUX 帧格式固定为 SYNC | LEN_H | LEN_L | SRCID | DSTMASK | PAYLOAD | TAIL
  4. AT 手册、需求说明、技术实现三份文档不得再出现历史展开式字段

十四、路径 A 实现清单

14.1 必须重写的模块

模块 原实现 目标实现 说明
tcp_server.c/.h lwIP RAW API 回调 netconn 阻塞任务 tcp_newnetconn_new,回调 → 阻塞循环
tcp_client.c/.h lwIP RAW API 回调 netconn 阻塞任务 同上
sys_arch.c/.h NO_SYS=1 空壳 FreeRTOS 移植层 sys_mboxsys_semsys_mutexsys_thread
lwipopts.h NO_SYS=1 NO_SYS=0 + netconn 已更新
main.c while(1) 轮询 FreeRTOS 任务创建 App_Poll() → 各任务函数
stm32f1xx_it.c 裸机 ISR FreeRTOS ISR xxFromISR API

14.2 可复用(需适配)的模块

模块 适配内容
uart_trans.c/.h 添加 xTaskNotifyFromISR 替代 poll 模式
config.c/.h 添加 xQueueReceive 替代 config_poll()
flash_param.c/.h 无需修改
CH390 驱动 ethernetif_poll 改为 NetPollTask 调用,SPI 加 Mutex 保护
ethernetif.c 添加 netif_addtcpip_input 回调

14.3 无需修改的模块

模块 说明
FreeRTOSConfig.h 已更新(任务优先级宏、堆大小)
startup_stm32f103xe.s 已适配 RCT6
TCP2UART.ioc 已适配 RCT6 + FreeRTOS + TIM4
MDK-ARM/TCP2UART.uvprojx 已适配 RCT6 + xE 宏
MUX 帧编解码 协议逻辑与 RTOS 无关

14.4 新增模块

模块 职责
route_msg.c/.h 零拷贝路由消息池管理
task_tcp_server.c/.h netconn Server 任务模板
task_tcp_client.c/.h netconn Client 任务模板
task_net_poll.c/.h CH390 poll + link check 任务