16 KiB
TCP2UART 项目代码阅读指南
1. 文档目的
本文面向接手 TCP2UART 的固件开发、联调和调试人员,目标是说明当前代码如何组织、启动后如何运行、TCP 与 UART 数据如何流动,以及应该按什么顺序阅读源码。
本文只描述当前工程的长期有效结构。历史调试交接文档中的某些“下一步动作”属于当时现场状态,不再作为当前项目入口。
2. 系统边界
2.1 硬件边界
- MCU:
STM32F103RCT6 - 以太网芯片:
CH390D - 配置口:
USART1 - 数据口:
USART2、USART3 - 调试输出:
SEGGER RTT
2.2 软件边界
- 工程:
MDK-ARM/TCP2UART.uvprojx - 调度:
FreeRTOS - 网络:
lwIP NO_SYS=0 + netconn API - 网络输入:
tcpip_thread+ethernetif+ CH390 驱动 - 业务链路:2 路 TCP Server,2 路 TCP Client,2 路 UART 数据口
- 配置协议:
AT、MUX、NET、LINK、BAUD、SAVE、RESET、DEFAULT
2.3 目录结构
App/
app_runtime.h 全局任务、队列、信号量声明
config.c/.h AT 命令、运行配置、默认值、Flash 保存
flash_param.c/.h Flash 参数读写与 CRC
route_msg.c/.h 固定消息池和路由消息封装
task_net_poll.c/.h lwIP/CH390 初始化、netif ready、网络重启
tcp_server.c/.h S1/S2 TCP Server 任务
tcp_client.c/.h C1/C2 TCP Client 任务
uart_trans.c/.h USART2/3 业务数据接收、发送、MUX 编解码
Core/Inc, Core/Src/
main.c 上电入口和外设初始化顺序
freertos.c FreeRTOS 队列、信号量、任务创建
stm32f1xx_it.c 中断入口,尤其是 UART IDLE 和 CH390 EXTI
usart.c/dma.c/... STM32CubeMX 生成的外设初始化
debug_log.c/.h RTT 日志和异常提示
Drivers/CH390/
CH390.c/.h 芯片级寄存器/辅助操作
CH390_Interface.c SPI/GPIO 与 CH390 事务封装
Drivers/LwIP/
src/include/arch/lwipopts.h 当前 lwIP 配置
src/netif/ethernetif.c CH390 与 lwIP netif 胶水层
port/sys_arch.c lwIP 在 FreeRTOS 上的 sys_arch 适配
3. 总体架构
+------------------------------------------------------+
| AT / Control Plane |
| USART1 IDLE DMA -> ConfigTask -> config_process_at_cmd|
+------------------------------------------------------+
| Configuration Model |
| MUX / NET / LINK[S1,S2,C1,C2] / BAUD |
+------------------------------------------------------+
| Routing Layer |
| route_msg fixed pool + xTcpRxQueue + xLinkTxQueues[] |
+------------------------------------------------------+
| Data Tasks |
| UartRxTask + TcpSrvTask_S1/S2 + TcpCliTask_C1/C2 |
+------------------------------------------------------+
| Network Runtime |
| NetPollTask + tcpip_thread + ethernetif + CH390 |
+------------------------------------------------------+
| HAL / DMA / IRQ |
| USART1/2/3 DMA+IDLE, SPI1, EXTI0, TIM4 timebase |
+------------------------------------------------------+
这套架构的核心思想是:TCP 任务和 UART 任务不直接互相调用,而是通过 route_msg_t 和 FreeRTOS 队列传递数据。这样可以把“从哪里来”“发到哪里去”“数据多长”统一表示,便于普通透传和 MUX 模式共用同一套路由机制。
4. 启动流程
启动入口在 Core/Src/main.c 的 main()。
HAL_Init()初始化 HAL、Flash 接口和基础 tick。debug_log_init()初始化 RTT 日志,并输出hal-init。SystemClock_Config()配置系统时钟。- 初始化外设:
MX_GPIO_Init()、MX_DMA_Init()、MX_USART1_UART_Init()。 config_init()从 Flash 读取配置;读取失败则config_set_defaults()。ApplyConfiguredUartBaudrates()根据配置设置USART2/USART3波特率。- 初始化
USART2、USART3、SPI1。 - 初始化 LED 并执行
CH390_HardwareReset()。 osKernelInitialize()初始化 RTOS 内核。MX_FREERTOS_Init()创建队列、信号量和基础任务。osKernelStart()启动调度器。
调试启动问题时,优先按 RTT boot 日志确认流程卡在哪个里程碑。
5. FreeRTOS 对象和任务
Core/Src/freertos.c 是理解运行时结构的关键文件。
5.1 全局对象
| 对象 | 类型 | 用途 |
|---|---|---|
xNetSemaphore |
Binary Semaphore | EXTI0 通知网络轮询任务处理 CH390 事件 |
xTcpRxQueue |
Queue | TCP 任务收到网络数据后投递给 UartRxTask |
xConfigQueue |
Queue | USART1 AT 命令或 MUX 控制帧投递给 ConfigTask |
xLinkTxQueues[4] |
Queue | UART 收到的数据投递给指定 S1/S2/C1/C2 TCP 任务 |
route_msg pool |
固定池 | 避免每包动态分配,最大载荷见 ROUTE_MSG_MAX_PAYLOAD |
5.2 基础任务
MX_FREERTOS_Init() 固定创建:
| 任务 | 入口 | 职责 |
|---|---|---|
defaultTask |
StartDefaultTask() |
LED 心跳、看门狗 |
NetPoll |
NetPollTask() |
初始化 lwIP/netif,轮询或响应 CH390 中断,网络 ready 后启动 TCP 任务 |
UartRx |
UartRxTask() |
处理 USART2/3 RX,执行普通透传或 MUX 路由 |
Config |
ConfigTask() |
处理 AT 命令并回复 |
当 DIAG_TASK_ISOLATION 打开时,UartRx 和 Config 会被隔离,这只用于调试,不代表正常产品形态。
5.3 网络任务
网络任务不是在 MX_FREERTOS_Init() 里立即全部创建,而是在 NetPollTask() 完成 tcpip_init()、lwip_netif_init() 并设置 g_netif_ready = pdTRUE 后,由 app_start_network_tasks() 按配置创建:
| 任务 | 条件 | 职责 |
|---|---|---|
TcpSrvS1 |
LINK[S1].enabled |
监听 S1 本地端口,收发 Server 数据 |
TcpSrvS2 |
LINK[S2].enabled |
监听 S2 本地端口,收发 Server 数据 |
TcpCliC1 |
LINK[C1].enabled |
主动连接 C1 远端,断线重连 |
TcpCliC2 |
LINK[C2].enabled |
主动连接 C2 远端,断线重连 |
这种延迟创建能避免 TCP 任务在 netif 尚未就绪时进入 netconn_* 路径。
6. 配置模型
配置结构定义在 App/config.h 的 device_config_t。
6.1 端点编码
| 端点 | 代码宏 | 编码 |
|---|---|---|
C1 |
ENDPOINT_C1 |
0x01 |
C2 |
ENDPOINT_C2 |
0x02 |
UART2 / U0 |
ENDPOINT_UART2 |
0x04 |
UART3 / U1 |
ENDPOINT_UART3 |
0x08 |
S1 |
ENDPOINT_S1 |
0x10 |
S2 |
ENDPOINT_S2 |
0x20 |
SRCID 是单一源端点;DSTMASK 是目标端点位图。DSTMASK=0x00 专用于系统控制帧,最终进入 AT 解析路径。
6.2 LINK 模型
4 条链路固定为:
| 角色 | 内部索引 | 默认作用 |
|---|---|---|
S1 |
CONFIG_LINK_S1 |
TCP Server 1 |
S2 |
CONFIG_LINK_S2 |
TCP Server 2 |
C1 |
CONFIG_LINK_C1 |
TCP Client 1 |
C2 |
CONFIG_LINK_C2 |
TCP Client 2 |
每条 LINK 包含:
EN,LPORT,RIP,RPORT,UART
UART 取 U0 或 U1,分别对应 USART2 和 USART3。
6.3 默认配置
当前默认值由 config_set_defaults() 写入:
MUX=0,即普通透传模式。NET=192.168.31.100,255.255.255.0,192.168.31.1,00:00:00:00:00:00。UART2/USART2和UART3/USART3默认波特率为115200。reconnect_interval_ms=3000。
完整 AT 命令格式以 AT固件使用手册.md 为准。
7. AT 配置数据流
AT 配置口固定使用 USART1。
上位机 AT 文本
-> USART1 DMA 接收
-> USART1 IDLE 中断
-> config_uart_idle_handler()
-> route_send_from_isr(xConfigQueue, ROUTE_CONN_UART1, ...)
-> ConfigTask
-> config_process_at_cmd()
-> config_respond_to_uart()
-> USART1 DMA 发送响应
config_process_at_cmd() 当前支持:
ATAT+?/AT+QUERYAT+SAVEAT+RESETAT+DEFAULTAT+MUX?/AT+MUX=0|1AT+NET?/AT+NET=IP,MASK,GW,MACAT+LINK?/AT+LINK=ROLE,.../AT+LINK=ROLEAT+BAUD?/AT+BAUD=U0|U1,baudrate
修改网络、链路、MUX 或波特率后,代码会返回 AT_NEED_REBOOT,ConfigTask 会追加提示:Use AT+SAVE then AT+RESET to apply changes。
8. 业务数据流示例:无协议透传
本节用一个最常见场景说明数据如何流动。
8.1 场景配置
假设现场需要“电脑 TCP 客户端连到设备,数据直接从 USART2 输出;USART2 收到的数据再原样回到 TCP 客户端”。可以配置:
AT+MUX=0
AT+LINK=S1,1,8080,0.0.0.0,0,U0
AT+SAVE
AT+RESET
含义:
- 普通透传模式,不使用 MUX 帧。
- 启用
S1TCP Server。 S1监听本地8080端口。S1绑定U0,也就是USART2。
8.2 TCP 到 UART
当远端 TCP 客户端连接 S1:8080 并发送字节,例如 01 02 03:
远端 TCP 客户端
-> CH390D 收包
-> ethernetif / lwIP
-> TcpSrvTask_S1
-> tcp_server_worker()
-> netconn_recv()
-> route_send(xTcpRxQueue, src=S1, dst=UART2, data=01 02 03)
-> UartRxTask
-> uart_trans_send_buffer(UART_CHANNEL_U0, data)
-> USART2 DMA TX
-> 外部串口设备收到 01 02 03
关键代码位置:
App/tcp_server.c:tcp_server_worker()从netconn_recv()取netbuf,再投递到xTcpRxQueue。App/uart_trans.c:UartRxTask()从xTcpRxQueue取消息,按dst_mask发送到USART2/USART3。
8.3 UART 到 TCP
当外部串口设备向 USART2 发送 A1 B2 C3:
外部串口设备
-> USART2 DMA RX
-> USART2 IDLE 中断
-> uart_trans_notify_rx_from_isr(UART_CHANNEL_U0)
-> UartRxTask
-> uart_route_raw_channel(U0)
-> 查找所有 enabled 且绑定 U0 的 LINK
-> route_send(xLinkTxQueues[S1], src=UART2, dst=S1, data=A1 B2 C3)
-> TcpSrvTask_S1
-> xQueueReceive(xLinkTxQueues[S1])
-> netconn_write()
-> lwIP / CH390D
-> 远端 TCP 客户端收到 A1 B2 C3
普通透传模式下,uart_route_raw_channel() 会把一个 UART 上收到的数据复制给所有“已启用且绑定该 UART”的链路。因此如果 S1 和 C2 都绑定 U0 且都启用,USART2 的一段输入会分别投递到 xLinkTxQueues[S1] 和 xLinkTxQueues[C2]。
9. MUX 模式数据流
MUX 模式由 AT+MUX=1 开启。帧格式在 App/uart_trans.c 中由 uart_mux_try_extract_frame() 和 uart_mux_encode_frame() 实现:
SYNC | LEN_H | LEN_L | SRCID | DSTMASK | PAYLOAD | TAIL
0x7E | len high | len low | source | destinations | bytes | 0x7F
处理规则:
DSTMASK=0x00:系统控制帧,PAYLOAD作为 AT 文本进入xConfigQueue。DSTMASK包含S1/S2/C1/C2:投递到对应xLinkTxQueues[]。DSTMASK包含另一个 UART:编码成新的 MUX 帧并转发到另一个数据口。- TCP 到 UART 时,如果目标是 UART 且当前处于 MUX 模式,会带上源端点并编码成 MUX 帧输出。
MUX 模式适合多个 TCP 实例共享一个数据口,或者上位机需要明确指定数据发往哪个逻辑端点的场景。
10. 网络初始化和 CH390 路径
网络运行入口是 App/task_net_poll.c 的 NetPollTask():
- 调用
tcpip_init(NULL, NULL)创建 lwIP 内核线程。 - 按
config_get()中的NET参数构造ipaddr/netmask/gateway。 - 调用
lwip_netif_init()初始化 netif 和 CH390 glue。 - 初始化成功后设置
g_netif_ready = pdTRUE。 - 调用
app_start_network_tasks()创建启用的 TCP Server/Client 任务。 - 主循环中等待
xNetSemaphore或周期轮询,驱动ethernetif_poll(),并响应网络重启请求。
底层路径主要在:
Drivers/CH390/CH390_Interface.c:SPI、CS、寄存器和 FIFO 访问。Drivers/CH390/CH390.c:芯片级 helper。Drivers/LwIP/src/netif/ethernetif.c:CH390 与 lwIP netif 的桥接。Drivers/LwIP/src/include/arch/lwipopts.h:lwIP 池、线程、core locking 配置。
当前关键 lwIP 配置包括:
NO_SYS=0LWIP_NETCONN=1LWIP_TCPIP_CORE_LOCKING=1LWIP_TCPIP_CORE_LOCKING_INPUT=1PBUF_POOL_SIZE=8MEMP_NUM_PBUF=8MEMP_NUM_TCPIP_MSG_INPKT=8LWIP_NETCONN_SEM_PER_THREAD=1
11. 推荐阅读路线
11.1 只想理解系统怎么跑
README.md- 本文第 3、4、5、8 节
Core/Src/main.cCore/Src/freertos.c
11.2 要改 AT 命令或默认参数
AT固件使用手册.mdApp/config.h:结构体、默认值、端点编码。App/config.c:解析、保存、响应。App/flash_param.c:Flash 存储。
11.3 要改 TCP 或串口透传
- 本文第 8、9 节。
App/route_msg.c:先理解消息生命周期。App/uart_trans.c:UART RX/TX、普通透传、MUX。App/tcp_server.c和App/tcp_client.c:网络收发。
11.4 要查网络底层问题
App/task_net_poll.cDrivers/LwIP/src/netif/ethernetif.cDrivers/CH390/CH390_Interface.cDrivers/LwIP/src/include/arch/lwipopts.h- RTT 日志和抓包结果一起看,不要只看单侧现象。
12. 调试指南
12.1 启动阶段
先看 RTT boot 日志:
hal-init
clock-config
peripherals-ready
config-ready
uart-trans-init
tasks-created
freertos-init
scheduler-start
如果停在 scheduler-start 前,优先看外设初始化、CH390 reset、RTOS 对象创建断言。如果进入任务后异常,再看 NetPollTask、ConfigTask、UartRxTask 的 task-entry 日志。
12.2 网络阶段
关键日志点:
[NET] tcpip-init enter/exit[NET] netif-init enter/exit[NET] post-init ok=... hwm=... free=... min=...[NET] start-network-tasks call[NET] netif-ready
如果 netif 未 ready,不要先查 TCP 业务任务;应先查 CH390、SPI、netif 初始化和 lwIP 配置。
12.3 路由阶段
route_send_result_to_str() 的返回值很重要:
| 返回 | 含义 | 常见方向 |
|---|---|---|
invalid |
参数或长度非法 | 检查 dst_mask、payload 长度 |
pool |
route_msg 固定池耗尽 |
检查消费者任务是否卡住、队列是否积压 |
queue |
目标队列满 | 检查对应 TCP/UART 任务是否还在运行 |
12.4 资源约束
STM32F103RCT6 的 SRAM 余量有限,而 FreeRTOS、lwIP、多个 TCP 任务、UART ring buffer 和消息池都会消耗 RAM。遇到 full-task 模式下的非确定性问题时,应同时记录:
xPortGetFreeHeapSize()xPortGetMinimumEverFreeHeapSize()uxTaskGetStackHighWaterMark()- lwIP pbuf/memp 池配置
- 哪些
LINK被启用
如果问题随任务数量、池大小或启用链路数量明显移动,先按资源问题分析,不要急着给业务路径加补丁。
12.5 历史 CH390/lwIP pbuf 泄漏教训
历史上曾出现“设备能成功 ping 固定次数,随后不再回应”的问题。现象随 PBUF_POOL_SIZE 从 8 改到 16 而从 8 次移动到 16 次,最终定位到 lwIP 输出路径中 pbuf 引用计数未释放这一类问题。
这个案例的长期价值不是记住某个临时修改,而是调试方法:
- 如果失败次数精确跟池大小相关,优先怀疑引用计数或释放路径。
- 扩大池只能延迟问题,不能当根修复。
- 抓包、RTT、lwIP 统计和代码引用计数要一起看。
13. 修改代码时的边界
- 不要绕过
route_msg和队列直接让 TCP/UART 任务互相调用。 - ISR 中只做通知或投递,不做阻塞等待。
netconn_*只在网络任务语境中使用,注意每个线程的netconn_thread_init()/netconn_thread_cleanup()。- 改 AT 命令时同步更新
AT固件使用手册.md。 - 改 MUX 帧格式或端点编码时,同步检查
App/config.h、App/uart_trans.c、AT固件使用手册.md。 - 改 lwIP 池大小时,同步记录 RAM、heap、水位和实际业务链路数量。
14. 术语速查
| 术语 | 含义 |
|---|---|
U0 |
USART2 数据口 |
U1 |
USART3 数据口 |
S1/S2 |
TCP Server 实例 |
C1/C2 |
TCP Client 实例 |
MUX=0 |
普通透明透传 |
MUX=1 |
带 SRCID/DSTMASK 的帧化透传 |
xTcpRxQueue |
TCP 到 UART 的队列 |
xLinkTxQueues[] |
UART 到 TCP 的队列数组 |
xConfigQueue |
AT 命令队列 |
NetPollTask |
网络初始化、轮询和恢复任务 |