Files
TCP2UART/项目代码阅读指南.md

16 KiB
Raw Permalink Blame History

TCP2UART 项目代码阅读指南

1. 文档目的

本文面向接手 TCP2UART 的固件开发、联调和调试人员,目标是说明当前代码如何组织、启动后如何运行、TCP 与 UART 数据如何流动,以及应该按什么顺序阅读源码。

本文只描述当前工程的长期有效结构。历史调试交接文档中的某些“下一步动作”属于当时现场状态,不再作为当前项目入口。

2. 系统边界

2.1 硬件边界

  • MCUSTM32F103RCT6
  • 以太网芯片:CH390D
  • 配置口:USART1
  • 数据口:USART2USART3
  • 调试输出:SEGGER RTT

2.2 软件边界

  • 工程:MDK-ARM/TCP2UART.uvprojx
  • 调度:FreeRTOS
  • 网络:lwIP NO_SYS=0 + netconn API
  • 网络输入:tcpip_thread + ethernetif + CH390 驱动
  • 业务链路:2 路 TCP Server2 路 TCP Client2 路 UART 数据口
  • 配置协议:ATMUXNETLINKBAUDSAVERESETDEFAULT

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.cmain()

  1. HAL_Init() 初始化 HAL、Flash 接口和基础 tick。
  2. debug_log_init() 初始化 RTT 日志,并输出 hal-init
  3. SystemClock_Config() 配置系统时钟。
  4. 初始化外设:MX_GPIO_Init()MX_DMA_Init()MX_USART1_UART_Init()
  5. config_init() 从 Flash 读取配置;读取失败则 config_set_defaults()
  6. ApplyConfiguredUartBaudrates() 根据配置设置 USART2/USART3 波特率。
  7. 初始化 USART2USART3SPI1
  8. 初始化 LED 并执行 CH390_HardwareReset()
  9. osKernelInitialize() 初始化 RTOS 内核。
  10. MX_FREERTOS_Init() 创建队列、信号量和基础任务。
  11. 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 打开时,UartRxConfig 会被隔离,这只用于调试,不代表正常产品形态。

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.hdevice_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 解析路径。

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

UARTU0U1,分别对应 USART2USART3

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/USART2UART3/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() 当前支持:

  • AT
  • AT+? / AT+QUERY
  • AT+SAVE
  • AT+RESET
  • AT+DEFAULT
  • AT+MUX? / AT+MUX=0|1
  • AT+NET? / AT+NET=IP,MASK,GW,MAC
  • AT+LINK? / AT+LINK=ROLE,... / AT+LINK=ROLE
  • AT+BAUD? / AT+BAUD=U0|U1,baudrate

修改网络、链路、MUX 或波特率后,代码会返回 AT_NEED_REBOOTConfigTask 会追加提示: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 帧。
  • 启用 S1 TCP 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.ctcp_server_worker()netconn_recv()netbuf,再投递到 xTcpRxQueue
  • App/uart_trans.cUartRxTask()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”的链路。因此如果 S1C2 都绑定 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

处理规则:

  1. DSTMASK=0x00:系统控制帧,PAYLOAD 作为 AT 文本进入 xConfigQueue
  2. DSTMASK 包含 S1/S2/C1/C2:投递到对应 xLinkTxQueues[]
  3. DSTMASK 包含另一个 UART:编码成新的 MUX 帧并转发到另一个数据口。
  4. TCP 到 UART 时,如果目标是 UART 且当前处于 MUX 模式,会带上源端点并编码成 MUX 帧输出。

MUX 模式适合多个 TCP 实例共享一个数据口,或者上位机需要明确指定数据发往哪个逻辑端点的场景。

10. 网络初始化和 CH390 路径

网络运行入口是 App/task_net_poll.cNetPollTask()

  1. 调用 tcpip_init(NULL, NULL) 创建 lwIP 内核线程。
  2. config_get() 中的 NET 参数构造 ipaddr/netmask/gateway
  3. 调用 lwip_netif_init() 初始化 netif 和 CH390 glue。
  4. 初始化成功后设置 g_netif_ready = pdTRUE
  5. 调用 app_start_network_tasks() 创建启用的 TCP Server/Client 任务。
  6. 主循环中等待 xNetSemaphore 或周期轮询,驱动 ethernetif_poll(),并响应网络重启请求。

底层路径主要在:

  • Drivers/CH390/CH390_Interface.c:SPI、CS、寄存器和 FIFO 访问。
  • Drivers/CH390/CH390.c:芯片级 helper。
  • Drivers/LwIP/src/netif/ethernetif.cCH390 与 lwIP netif 的桥接。
  • Drivers/LwIP/src/include/arch/lwipopts.hlwIP 池、线程、core locking 配置。

当前关键 lwIP 配置包括:

  • NO_SYS=0
  • LWIP_NETCONN=1
  • LWIP_TCPIP_CORE_LOCKING=1
  • LWIP_TCPIP_CORE_LOCKING_INPUT=1
  • PBUF_POOL_SIZE=8
  • MEMP_NUM_PBUF=8
  • MEMP_NUM_TCPIP_MSG_INPKT=8
  • LWIP_NETCONN_SEM_PER_THREAD=1

11. 推荐阅读路线

11.1 只想理解系统怎么跑

  1. README.md
  2. 本文第 3、4、5、8 节
  3. Core/Src/main.c
  4. Core/Src/freertos.c

11.2 要改 AT 命令或默认参数

  1. AT固件使用手册.md
  2. App/config.h:结构体、默认值、端点编码。
  3. App/config.c:解析、保存、响应。
  4. App/flash_param.cFlash 存储。

11.3 要改 TCP 或串口透传

  1. 本文第 8、9 节。
  2. App/route_msg.c:先理解消息生命周期。
  3. App/uart_trans.cUART RX/TX、普通透传、MUX。
  4. App/tcp_server.cApp/tcp_client.c:网络收发。

11.4 要查网络底层问题

  1. App/task_net_poll.c
  2. Drivers/LwIP/src/netif/ethernetif.c
  3. Drivers/CH390/CH390_Interface.c
  4. Drivers/LwIP/src/include/arch/lwipopts.h
  5. 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 对象创建断言。如果进入任务后异常,再看 NetPollTaskConfigTaskUartRxTask 的 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 引用计数未释放这一类问题。

这个案例的长期价值不是记住某个临时修改,而是调试方法:

  1. 如果失败次数精确跟池大小相关,优先怀疑引用计数或释放路径。
  2. 扩大池只能延迟问题,不能当根修复。
  3. 抓包、RTT、lwIP 统计和代码引用计数要一起看。

13. 修改代码时的边界

  1. 不要绕过 route_msg 和队列直接让 TCP/UART 任务互相调用。
  2. ISR 中只做通知或投递,不做阻塞等待。
  3. netconn_* 只在网络任务语境中使用,注意每个线程的 netconn_thread_init() / netconn_thread_cleanup()
  4. 改 AT 命令时同步更新 AT固件使用手册.md
  5. 改 MUX 帧格式或端点编码时,同步检查 App/config.hApp/uart_trans.cAT固件使用手册.md
  6. 改 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 网络初始化、轮询和恢复任务