20 KiB
TCP2UART 项目技术实现
一、文档目的
本文档描述 TCP2UART 项目基于 STM32F103RCT6 + FreeRTOS 的最终内部实现口径。
本文档只围绕最终协议模型展开:
MUX:串口承载层NET:全局网络配置层LINK[idx]:实例配置与连接管理层
不再保留历史 S1... / C1... 外部字段模型。
二、当前工程基础
当前工程基础约束如下:
- MCU:
STM32F103RCT6(256KB Flash / 48KB SRAM) - 网络芯片:
CH390D - 软件架构:
FreeRTOS + lwIP NO_SYS=0 - 协议栈:
lwIP socket/netconn API - 调试输出:
SEGGER RTT - 使用
FreeRTOS任务调度 - 不实现 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 任务设计(路径 A:netconn + 多 TCP 任务)
4.1 架构路线
本项目采用 路径 A:NO_SYS=0 + netconn API + 每个 TCP 连接独立任务。
核心决策:
- lwIP 以
NO_SYS=0模式运行,tcpip_thread由 lwIP 自动创建 - TCP 连接使用
netconn阻塞 API(netconn_accept/netconn_recv/netconn_write) - 每个 TCP Server 和 Client 实例各占一个独立任务
- 任务间通过 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_t,Queue 发送指针 - 接收方:从 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指针投递到xTcpRxQueue,UartRxTask 取出后写入 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
实现职责:
- 识别帧边界
- 解析长度字段
- 提取
SRCID - 解析
DSTMASK - 按控制帧或数据帧分流
5.2 控制帧与数据帧分离
控制规则固定如下:
DSTMASK = 0x00:系统控制帧DSTMASK != 0x00:业务数据帧
系统控制帧处理要求:
PAYLOAD解释为 AT 文本- AT 文本必须以
\r\n结束 - 控制帧投递到
ConfigTask
业务数据帧处理要求:
SRCID表示单一源端点DSTMASK表示目标端点集合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:普通透传1:MUX 透传
6.2 NET 记录
NET 为全局静态网络记录:
IP,MASK,GW,MAC
说明:
- 设备只有一张网卡,因此不为每个实例单独配置本地 IP
- 当前实现目标中不包含 DHCP
6.3 LINK 记录
LINK[idx] 为统一实例记录:
EN,LPORT,RIP,RPORT,UART
固定索引映射:
0 = S11 = S22 = C13 = C2
字段职责:
EN:实例启用状态LPORT:本地端口RIP / RPORT:对端地址与端口UART:对应业务数据口
说明:
Server与Client共享同一记录结构Server的RIP / RPORT可作为对端约束或预设Client的RIP / RPORT表示远端目标
七、模块职责
7.1 配置模块 config.c/.h
最终职责:
- 解析
AT+MUX - 解析
AT+NET - 解析
AT+LINK - 加载与保存配置
- 处理
SAVE / RESET / DEFAULT
7.2 UART 透传模块 uart_trans.c/.h
最终职责:
- 保持
USART2 / USART3的DMA + IDLE接收发送基线 - 在
MUX=0时执行普通透传 - 在
MUX=1时执行 MUX 帧收发 - 将控制帧与业务数据帧分流
7.3 TCP Server / Client 模块(需重写)
原 tcp_server.c / tcp_client.c 基于 lwIP RAW API 回调模式,需重写为 netconn 阻塞模式:
- 删除所有
tcp_pcb/tcp_recv/tcp_accept回调代码 - 改用
netconn_new/netconn_bind/netconn_listen/netconn_accept(Server) - 改用
netconn_new/netconn_connect(Client) - 收发改为
netconn_recv/netconn_write阻塞调用 - 内部 ring buffer 可取消(netconn 内部已有 pbuf 缓冲)
- 每个 TCP 任务内直接处理路由,通过 Queue 指针传递数据
7.4 FreeRTOS 初始化 freertos.c
CubeMX 生成的 FreeRTOS 初始化文件,职责:
- 定义默认任务
StartDefaultTask - 用户在
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=0,lwIP 将运行以下线程:
tcpip_thread(优先级 6,栈 512 words):lwIP 核心线程,处理所有协议栈内部事件- 应用任务通过
netconnAPI 与 lwIP 交互,由tcpip_thread消息机制保证线程安全 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 的适配:
sys_thread_new:创建 lwIP 线程(即tcpip_thread)sys_mbox_*:消息邮箱(基于 FreeRTOS Queue,容量TCPIP_MBOX_SIZE=8)sys_sem_*:信号量(基于 FreeRTOS Semaphore)sys_mutex_*:互斥锁(基于 FreeRTOS Mutex)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, ðernetif_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而非SysTickuwTick在TIM4_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 buffer(netconn 内部有 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 最终不够用时,切换为 STM32F103RDT6(pin-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 KB,RDT6 预留 282 KB |
十一、硬件资源
11.1 MCU
- 型号:
STM32F103RCT6 - Flash:
256 KB - SRAM:
48 KB - 主频:
72 MHz
11.2 主要外设
SPI1:连接CH390DUSART1:配置串口USART2:数据透传串口USART3:数据透传串口DMA1:3 路 UART 收发 DMAEXTI0:CH390 中断输入IWDG:独立看门狗TIM4:HAL 时间基准(替代 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 外部晶振 |
十二、实现边界
- 保持单网卡静态网络模型
- 不实现 DHCP
- 不实现旧
S1... / C1...外部协议字段 - 不在文档中保留兼容层描述
- 所有 AT 文本控制统一要求
\r\n结束 - FreeRTOS 堆管理使用
heap_4.c - HAL 时间基准使用
TIM4而非SysTick
十三、文档一致性要求
后续实现、联调、测试与代码注释必须遵守以下统一口径:
- 对外协议只使用
MUX / NET / LINK - 控制帧只使用
DSTMASK=0x00 - MUX 帧格式固定为
SYNC | LEN_H | LEN_L | SRCID | DSTMASK | PAYLOAD | TAIL - AT 手册、需求说明、技术实现三份文档不得再出现历史展开式字段
十四、路径 A 实现清单
14.1 必须重写的模块
| 模块 | 原实现 | 目标实现 | 说明 |
|---|---|---|---|
tcp_server.c/.h |
lwIP RAW API 回调 | netconn 阻塞任务 | tcp_new → netconn_new,回调 → 阻塞循环 |
tcp_client.c/.h |
lwIP RAW API 回调 | netconn 阻塞任务 | 同上 |
sys_arch.c/.h |
NO_SYS=1 空壳 | FreeRTOS 移植层 | sys_mbox、sys_sem、sys_mutex、sys_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_add 的 tcpip_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 任务 |