Files
TCP2UART/代码结构与阅读指南.md
T

43 KiB
Raw Blame History

TCP2UART 代码结构与阅读指南

1. 文档目的

本文档是一份面向接手维护者的代码说明书。目标不是简单列出目录,而是帮助读者在没有原作者口头说明的情况下,能够完成以下工作:

  1. 理解 TCP2UART 固件的整体架构和运行时数据流。
  2. 找到每个功能对应的源码入口。
  3. 看懂启动、配置、网络、串口、TCP、MUX、Flash 参数保存之间的调用关系。
  4. 按正确顺序阅读代码,避免从历史文件或非主路径入口误入。
  5. 修改 AT 命令、串口透传、TCP 链路、CH390 驱动或构建配置时知道应该同步检查哪些文件。
  6. 排查现场问题时能按层次定位,而不是在多个模块之间盲目跳转。

本文档只描述当前主线实现。历史阶段性计划、旧 AT 展开式命令、FreeRTOS/socket/netconn 方案不再作为当前代码阅读依据。

2. 项目一句话说明

TCP2UART 是一个基于 STM32F103R8T6 + CH390D 的裸机 TCP 与双串口透传固件。它通过 CH390D 提供以太网能力,通过 USART2/USART3 提供业务串口数据通道,通过 USART1 提供 AT 配置口,并用 MUX / NET / LINK 三层配置模型组织外部控制协议。

当前主线边界如下:

项目 当前口径
MCU STM32F103R8T6
Flash / SRAM 64KB Flash / 20KB SRAM
以太网芯片 CH390D
网络协议栈 lwIP RAW API + NO_SYS=1
调度模型 裸机主循环,不使用 FreeRTOS 做业务调度
配置口 USART1
数据口 USART2 / USART3
TCP 实例 2 路 Server + 2 路 Client
配置模型 MUX / NET / LINK
调试输出 SEGGER RTT
主要构建基线 MDK-ARM/TCP2UART.uvprojx

3. 先看一个业务例子:无协议透传

如果直接从目录和 API 开始读,容易觉得模块很多、入口很多。更推荐先用一个业务例子建立直觉:当前默认配置下的无协议透传,也就是 MUX=0 的普通透明传输模式。

3.1 这个例子解决什么问题

假设现场有两类设备:

  1. 一个串口设备接在 USART2,希望远端 PC 通过 TCP 连接访问它。
  2. 另一个串口设备接在 USART3,希望本设备主动连接远端 TCP Server 后转发数据。

在默认配置中,这两个业务已经对应到两条链路:

默认链路 默认状态 TCP 角色 TCP 参数 串口 业务含义
S1 启用 Server 本地监听 8080 U0=USART2 远端 PC 连接设备 8080 后访问 USART2 设备
C1 启用 Client 本地端口 9001,远端 192.168.1.200:9000 U1=USART3 设备主动连接远端 Server 后转发 USART3 数据

默认 MUX=0,所以数据不带任何额外帧头、长度、端点 ID。串口收到什么字节,就尽量原样送到对应 TCP;TCP 收到什么 payload,就尽量原样写到对应串口。

3.2 先看 S1PC 通过 TCP 访问 USART2

这个场景中,PC 是 TCP Client,设备是 TCP Server

PC TCP Client
  -> 连接设备 IP:8080
  -> TCP payload
  -> CH390D
  -> lwIP RAW TCP Server S1
  -> App_RouteTcpTraffic()
  -> USART2 TX
  -> 外部串口设备

反方向是:

外部串口设备
  -> USART2 RX DMA
  -> uart_trans RX ring
  -> App_RouteRawUartTraffic()
  -> TCP Server S1 当前连接
  -> CH390D
  -> PC TCP Client

对应代码入口:

步骤 关键函数 文件
Server 配置 App_ConfigureLinks() Core/Src/main.c
Server 启动 App_StartLinksIfNeeded() -> tcp_server_start() Core/Src/main.cApp/tcp_server.c
TCP 收包 tcp_server_on_recv() App/tcp_server.c
TCP 到串口 App_RouteTcpTraffic() -> App_SendTcpPayloadToUartRaw() -> uart_trans_write() Core/Src/main.cApp/uart_trans.c
串口收包 uart_trans_idle_handler() / DMA 回调 -> RX ring App/uart_trans.cCore/Src/stm32f1xx_it.c
串口到 TCP App_RouteRawUartTraffic() -> App_SendTcpServerPayload() -> tcp_server_send() Core/Src/main.cApp/tcp_server.c

3.3 再看 C1:设备主动连接远端 Server 并桥接 USART3

这个场景中,设备是 TCP Client,远端 PC 或服务器监听 192.168.1.200:9000

外部串口设备
  -> USART3 RX DMA
  -> uart_trans RX ring
  -> App_RouteRawUartTraffic()
  -> TCP Client C1
  -> CH390D
  -> 远端 TCP Server 192.168.1.200:9000

反方向是:

远端 TCP Server
  -> TCP payload
  -> CH390D
  -> lwIP RAW TCP Client C1
  -> App_RouteTcpTraffic()
  -> USART3 TX
  -> 外部串口设备

对应代码入口:

步骤 关键函数 文件
Client 配置 App_ConfigureLinks() Core/Src/main.c
Client 发起连接 App_StartLinksIfNeeded() -> tcp_client_connect() Core/Src/main.cApp/tcp_client.c
连接成功回调 tcp_client_on_connected() App/tcp_client.c
自动重连 tcp_client_poll() App/tcp_client.c
TCP 到串口 App_RouteTcpTraffic() -> App_SendTcpPayloadToUartRaw() -> uart_trans_write() Core/Src/main.cApp/uart_trans.c
串口到 TCP App_RouteRawUartTraffic() -> App_SendTcpClientPayload() -> tcp_client_send() Core/Src/main.cApp/tcp_client.c

3.4 一字节数据在系统里怎么走

USART2 收到 1 个字节并转发到 S1 为例:

USART2 引脚收到字节
  -> DMA 写入 uart_trans 的 rx_dma_buffer
  -> USART2 IDLE / DMA half / DMA complete 中断
  -> uart_trans_idle_handler() 或 uart_trans_rx_*_handler()
  -> process_rx_snapshot() 搬入 rx_ring
  -> App_Poll()
  -> App_RouteRawUartTraffic()
  -> uart_trans_read(UART_CHANNEL_U0)
  -> 找到所有启用且 uart==U0 的 LINK
  -> S1 命中 CONFIG_LINK_S1
  -> tcp_server_send(0, data, len)
  -> lwIP tcp_write() / tcp_output()
  -> CH390 runtime 发送以太网帧

以 TCP Server S1 收到 1 个 payload 字节并转发到 USART2 为例:

CH390D 收到以太网帧
  -> ch390_runtime_poll()
  -> ch390_runtime_input_frame()
  -> lwIP TCP input
  -> tcp_server_on_recv()
  -> pbuf 数据进入 tcp_server RX ring 或 hold_pbuf
  -> App_Poll()
  -> App_RouteTcpTraffic()
  -> tcp_server_rx_available(0)
  -> tcp_server_peek(0)
  -> uart_trans_tx_free(U0) 判断串口发送空间
  -> uart_trans_write(U0)
  -> tcp_server_drop(0)
  -> tcp_recved() 释放 TCP 接收窗口
  -> UART TX DMA 发出字节

3.5 这个例子里最重要的两个缓冲

无协议透传并不是“中断里收到就立刻发出去”。当前实现依赖两类缓冲把中断、主循环、TCP 栈解耦:

缓冲 所在模块 作用
UART RX/TX ring App/uart_trans.c 吸收串口 DMA 和主循环转发之间的速率差
TCP RX ring + hold_pbuf App/tcp_server.cApp/tcp_client.c 吸收 TCP 收包和 UART 发送之间的速率差

TCP 到 UART 方向有明确背压:只有数据真正写入 UART TX ring 后,代码才调用 tcp_server_drop()tcp_client_drop(),进而执行 tcp_recved() 释放 TCP 接收窗口。这样 UART 慢时,TCP 对端会被窗口自然压住。

UART 到 TCP 方向更接近实时透传:App_RouteRawUartTraffic() 会先从 UART RX ring 取出一段数据,再尝试发给所有绑定该 UART 的启用 LINK。如果 TCP 未连接或发送窗口不足,这段 UART 数据不会进入额外的应用级重试队列。因此现场要做无丢失透传时,应先确认 TCP 链路已建立、远端及时收取数据、串口输入速率不超过链路承载能力。

3.6 用这个例子读代码的最短路径

如果只想先看懂无协议透传,按这个顺序读:

  1. AT固件使用手册.md 的默认 MUX / NET / LINK / BAUD
  2. App/config.hdevice_config_tCONFIG_LINK_*LINK_UART_U0/U1
  3. Core/Src/main.cApp_Init()App_ConfigureLinks()
  4. Core/Src/main.cApp_Poll()
  5. Core/Src/main.cApp_RouteRawUartTraffic()
  6. Core/Src/main.cApp_RouteTcpTraffic()
  7. App/uart_trans.cuart_trans_read()uart_trans_write() 和 DMA/IDLE handler。
  8. App/tcp_server.cApp/tcp_client.csend/peek/drop/poll

看完这一条线,再回头看 MUX 模式、CH390 runtime 和 lwIP glue,会容易很多。

4. 系统从上电到稳定转发的工作过程

理解整个系统可以按“配置生效、网络就绪、链路启动、持续搬运、异常恢复”五个阶段来看。

4.1 阶段一:上电和外设初始化

main() 先完成 HAL、时钟、GPIO、DMA、USART、SPI、TIM、IWDG 初始化。这个阶段只让 MCU 外设进入可用状态,还没有真正开始 TCP/UART 业务转发。

关键文件:

  1. Core/Src/main.c
  2. Core/Src/gpio.c
  3. Core/Src/dma.c
  4. Core/Src/usart.c
  5. Core/Src/spi.c
  6. Core/Src/tim.c
  7. Core/Src/iwdg.c

4.2 阶段二:应用初始化

App_Init() 让应用配置和软件模块进入可运行状态:

  1. 从 Flash 加载配置,失败时使用默认配置。
  2. 按配置初始化 USART2/USART3 业务通道。
  3. 初始化 RTT 和栈保护。
  4. 初始化 lwIP。
  5. NET 配置创建 ch390_netif
  6. LINK 配置转换成 TCP Server/Client 内部配置。
  7. 打印 CH390 启动诊断。
  8. 启动 USART1 AT 配置口接收。

这时 TCP 实例还不一定已经启动,因为启动还要等待 CH390 link up。

主循环每轮都会调用:

ethernetif_poll()
ethernetif_check_link()
App_StopLinksIfNeeded()
App_StartLinksIfNeeded()

netif_is_link_up(&ch390_netif) 为真,并且 g_links_started == 0 时,App_StartLinksIfNeeded() 会尝试启动所有 TCP Server 和 TCP Client 实例。

如果 link downApp_StopLinksIfNeeded() 会停止 TCP 实例并清除 g_links_started。所以下一次 link up 后,系统会重新启动 TCP 链路。

4.4 阶段四:主循环持续搬运数据

稳定运行后,App_Poll() 每轮都做四类事情:

  1. 推进网络设备:ethernetif_poll()ethernetif_check_link()sys_check_timeouts()
  2. 推进 TCP 状态:tcp_server_poll()tcp_client_poll()
  3. 推进 UART 状态:uart_trans_poll()
  4. 执行业务路由:先 App_RouteTcpTraffic(),再按 MUX 选择 RAW 或 MUX 的 UART 到 TCP 路由。

这说明系统不是多线程并发模型,而是“中断收集事件,主循环分层消费事件”的模型。

4.5 阶段五:周期健康检查和软件复位

主循环还会周期性调用 ch390_runtime_health_check()。如果 AT 命令请求复位,config_is_reset_requested() 会触发 NVIC_SystemReset()

所以 AT+RESET 不是直接在串口中断里复位,而是先设置请求标志,再由主循环在安全位置执行系统复位。

5. 阅读前必须建立的概念

5.1 三个平面

本项目可以按三个平面理解:

平面 职责 关键文件
控制平面 AT 命令、配置查询、参数保存、系统复位 App/config.cApp/flash_param.c
数据平面 UART 与 TCP 之间的数据转发,RAW/MUX 路由 Core/Src/main.cApp/uart_trans.cApp/tcp_server.cApp/tcp_client.c
设备平面 CH390、lwIP netif、SPI、GPIO、DMA、中断 Drivers/CH390/*Drivers/LwIP/src/netif/ethernetif.cCore/Src/*

阅读代码时不要把这三个平面混在一起。例如,AT 命令解析失败时先看 config.cCH390 identity 读不出来时先看 ch390_runtime.c 和硬件引脚;TCP 到 UART 丢包时先看 main.c 的路由和 TCP 背压,不要直接改 lwIP core。

5.2 内部索引和外部名字

配置层内部用 LINK[idx] 数组管理四条链路:

内部索引 对外角色名 类型 说明
0 S1 TCP Server 1 监听本地端口,接收远端连接
1 S2 TCP Server 2 第二路 Server
2 C1 TCP Client 1 主动连接远端 IP/端口
3 C2 TCP Client 2 第二路 Client

AT 命令对外使用 S1/S2/C1/C2。代码内部使用 CONFIG_LINK_S1CONFIG_LINK_S2CONFIG_LINK_C1CONFIG_LINK_C2

5.3 UART 编号和端点编码

业务数据口在配置中用 U0/U1 表示:

配置名 硬件外设 代码枚举
U0 USART2 UART_CHANNEL_U0
U1 USART3 UART_CHANNEL_U1

MUX 模式下,UART 和 TCP 实例共享一套端点编码:

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

SRCID 必须是单一端点值,DSTMASK 是目标端点位图。DSTMASK=0x00 专门表示系统控制帧,进入 AT 解析路径。

6. 顶层目录说明

TCP2UART/
├── App/                         应用层模块
├── Core/                        STM32CubeMX 生成的启动、外设和中断代码
├── Drivers/
│   ├── CH390/                   CH390D 驱动与运行时封装
│   ├── LwIP/                    lwIP 源码、配置和 netif glue
│   ├── STM32F1xx_HAL_Driver/    STM32 HAL 驱动
│   └── CMSIS/                   CMSIS 与芯片头文件
├── Middlewares/Third_Party/
│   └── SEGGER_RTT/              RTT 调试输出
├── MDK-ARM/                     Keil MDK 工程
├── EWARM/                       IAR 工程文件
├── cmake/                       CMake 工程片段
├── Reference/                   芯片与外设资料
├── PCB/                         PCB 工程与 Gerber 归档
├── tools/                       主机侧调试脚本
├── TCP2UART.ioc                 CubeMX 配置源
├── CMakeLists.txt               CMake 顶层入口
└── STM32F103XX_FLASH.ld         GCC 链接脚本

6.1 哪些目录是当前主路径

当前固件主路径主要在以下目录:

路径 是否主路径 说明
Core/ 启动、外设、中断、主循环入口
App/ 应用层功能模块
Drivers/CH390/ CH390D 驱动和运行时
Drivers/LwIP/src/netif/ethernetif.c CH390 与 lwIP 的胶水层
Drivers/LwIP/src/include/arch/lwipopts.h lwIP 裁剪配置
Middlewares/Third_Party/SEGGER_RTT/ RTT 日志输出
Drivers/LwIP/port/sys_arch.c 历史 FreeRTOS port 文件,当前裸机 RAW 主路径不要从这里开始

6.2 相关工程文件

文件 用途
MDK-ARM/TCP2UART.uvprojx Keil 主工程,当前实机构建优先参考
TCP2UART.ioc CubeMX 配置源,外设和引脚变更应回到这里核对
cmake/stm32cubemx/CMakeLists.txt CMake 下的源码和 include 路径清单,可用来和 Keil 工程互相核对
CMakePresets.json CMake preset 配置
STM32F103XX_FLASH.ld GCC 链接脚本

7. 总体运行时架构

+--------------------------------------------------+
| AT / Control Plane                               |
| USART1 AT parser + MUX control frame parser      |
+--------------------------------------------------+
| Configuration Model                              |
| MUX / NET / LINK[idx] / UART baud                |
+--------------------------------------------------+
| Routing & Session Layer                          |
| TCP instances + UART dispatch + MUX routing      |
+--------------------------------------------------+
| Poll Loop                                        |
| config_poll / uart_trans_poll / ethernetif_poll  |
| ethernetif_check_link / sys_check_timeouts       |
+--------------------------------------------------+
| Driver Layer                                     |
| CH390 runtime / SPI / UART DMA+IDLE / HAL        |
+--------------------------------------------------+

关键原则:

  1. Core/Src/main.c 负责调度,不直接展开复杂 CH390 寄存器事务。
  2. ch390_runtime.c 是 CH390 运行时唯一拥有者。
  3. ethernetif.c 只做 lwIP netif glue,不承载复杂策略。
  4. tcp_server.ctcp_client.c 只管理 TCP 实例,不理解 AT 命令。
  5. uart_trans.c 只管理 UART ring、DMA、MUX 帧,不直接理解 TCP 链路配置。
  6. config.c 只管理配置、AT 和 Flash 参数,不直接执行网络收发。

8. 启动顺序说明

8.1 main() 的职责

Core/Src/main.cmain() 是总入口。典型顺序是:

  1. HAL_Init() 初始化 HAL 和 SysTick。
  2. SystemClock_Config() 配置系统时钟。
  3. MX_GPIO_Init() 初始化 GPIO。
  4. MX_DMA_Init() 初始化 DMA。
  5. MX_SPI1_Init() 初始化 CH390 使用的 SPI。
  6. MX_USART1_UART_Init() 初始化 AT 配置口。
  7. MX_USART2_UART_Init() / MX_USART3_UART_Init() 初始化业务串口。
  8. MX_TIM4_Init()MX_IWDG_Init() 等外设初始化。
  9. App_Init() 初始化应用层。
  10. 进入 while (1),持续调用 App_Poll()

8.2 App_Init() 的职责

App_Init() 是应用层启动的关键函数。当前顺序如下:

  1. config_init():加载 Flash 参数,失败时使用默认配置。
  2. config_get():取得当前配置。
  3. uart_trans_init():初始化业务串口传输模块上下文。
  4. 根据 cfg->uart_baudrate[0/1] 配置 U0/U1
  5. uart_trans_start(U0/U1):启动业务串口 DMA 接收。
  6. SEGGER_RTT_Init():初始化 RTT 输出。
  7. StackGuard_Init():初始化栈保护检查。
  8. 打印启动日志和时钟 fallback 警告。
  9. lwip_init():初始化 lwIP。
  10. cfg->net 组装 IP、Mask、Gateway。
  11. lwip_netif_init():添加并启用 ch390_netif
  12. App_ConfigureLinks(cfg):把 LINK 配置下发到 TCP Server/Client 模块。
  13. BootDiag_ReportCh390():打印 CH390 启动诊断。
  14. HAL_UART_Receive_IT():启动 USART1 单字节中断接收。

如果启动阶段卡死或无日志,先看 App_Init() 前后的 RTT 输出,再看 trap handler,不要直接跳到 TCP 或 MUX 层。

9. 主循环说明

App_Poll() 是当前固件运行时的中心。理解它就能理解大多数现场行为。

当前主循环逻辑可以按以下顺序阅读:

顺序 调用 作用
1 ethernetif_poll() 轮询 CH390 收包和 IRQ pending
2 ethernetif_check_link() 刷新 link up/down 状态
3 sys_check_timeouts() 驱动 lwIP RAW 定时器
4 App_StopLinksIfNeeded() link down 时停止已启动 TCP 实例
5 App_StartLinksIfNeeded() link up 后启动 TCP Server/Client
6 tcp_server_poll() / tcp_client_poll() 推进 TCP 实例内部状态、pending pbuf、Client 重连与超时
7 uart_trans_poll() 推进业务串口 DMA/RX/TX 状态
8 StackGuard_Check() 检查栈保护字
9 config_poll() 处理 USART1 已收完整 AT 命令
10 App_RouteTcpTraffic() 转发 TCP 到 UART,处理背压
11 App_RouteMuxUartTraffic()App_RouteRawUartTraffic() 根据 MUX 模式处理 UART 到 TCP/控制帧路径
12 ch390_runtime_health_check() 周期性 CH390 健康检查
13 config_is_reset_requested() 若 AT 请求复位,则执行 NVIC_SystemReset()

阅读主循环时应重点关注两个状态:

  1. g_links_started:表示 TCP 实例是否已经在当前 link up 周期启动。
  2. netif_is_link_up(&ch390_netif):表示 CH390/lwIP netif 看到的链路状态。

10. 应用层模块说明

10.1 App/config.h

config.h 是配置模型的入口。需要重点理解以下定义:

定义 说明
CONFIG_MAGIC Flash 参数结构 magic,当前为 0x54435055
CONFIG_VERSION 当前配置版本,当前为 0x0003
CONFIG_UART_COUNT 业务 UART 数量,当前为 2
CONFIG_LINK_COUNT LINK 数量,当前为 4
CONFIG_LINK_S1/S2/C1/C2 内部 LINK 索引
ENDPOINT_* MUX 端点编码
LINK_UART_U0/U1 LINK 中 UART 字段取值
device_config_t 当前持久化配置总结构

device_config_t 包含:

  1. magic:结构标识。
  2. version:结构版本。
  3. mux_modeRAW 或 MUX 模式。
  4. netIP、Mask、Gateway、MAC。
  5. links[4]:四条 LINK 配置。
  6. uart_baudrate[2]:两个业务串口波特率。
  7. crc:结构校验。

10.2 App/config.c

config.c 是 AT 命令和配置持久化的核心。建议按以下顺序阅读:

  1. 顶部静态变量,理解 g_config、命令缓冲区、pending 命令标志。
  2. config_calc_crc(),理解 CRC 覆盖范围。
  3. config_set_defaults(),理解默认配置。
  4. config_load(),理解 Flash 加载、版本判断、默认值 fallback。
  5. try_load_legacy_config()migrate_legacy_config(),理解 v2 到 v3 的参数迁移。
  6. config_process_at_cmd(),理解 AT 命令分发入口。
  7. handle_summary_query()handle_net_query()handle_link_query() 等具体命令处理。
  8. config_uart_rx_byte()config_poll(),理解 USART1 接收如何进入 AT 解析。
  9. config_try_process_frame(),理解 MUX 控制帧如何复用 AT 解析。

10.2.1 USART1 AT 命令路径

USART1 RX interrupt
  -> HAL_UART_RxCpltCallback()
  -> config_uart_rx_byte(byte)
  -> 收到完整 CRLF 命令后写入 pending buffer
  -> App_Poll()
  -> config_poll()
  -> config_try_process_frame()
  -> config_process_at_cmd()
  -> HAL_UART_Transmit(USART1 response)

对外文档要求 AT 命令以 \r\n 结束。代码中 config_uart_rx_byte()\r 标记、以 \n 完成一帧;维护时不要把现场工具可能容忍的发送细节写成新的协议口径。

10.2.2 MUX 控制帧路径

USART2/USART3 MUX frame
  -> uart_mux_try_extract_frame()
  -> DSTMASK == 0x00
  -> config_build_response_frame()
  -> config_process_at_cmd()
  -> config_build_response_frame() 生成响应文本
  -> main.c 使用 g_mux_response_frame 编码响应 MUX 帧
  -> uart_trans_write()

控制帧和普通数据帧的关键区别只有 DSTMASK

  1. DSTMASK=0x00:系统控制帧,payload 是 AT 文本。
  2. DSTMASK!=0x00:业务数据帧,进入路由分发。

10.3 App/flash_param.c / App/flash_param.h

Flash 参数模块使用 STM32F103R8 最后一页 Flash 保存配置:

页大小 1024 字节
参数页起始 0x0800FC00
参数页结束 0x08010000

核心 API

API 作用
flash_param_init() 初始化参数存储模块,当前为空实现
flash_param_read() 从参数页直接 memcpy 读取
flash_param_write() 擦除参数页并半字写入新数据
flash_param_erase() 擦除参数页
flash_param_crc32() 计算 CRC32
flash_param_verify() 校验参数页完整性

修改配置结构时必须同步检查:

  1. CONFIG_VERSION 是否需要递增。
  2. device_config_t 是否仍适合 1KB 参数页。
  3. config_set_defaults() 是否覆盖新增字段。
  4. config_calc_crc() 覆盖范围是否正确。
  5. 是否需要新增旧版本迁移逻辑。

10.4 App/uart_trans.c / App/uart_trans.h

UART 传输模块管理两个业务串口。它不处理 USART1 配置口。

10.4.1 内部上下文

每个业务串口有一个 uart_channel_ctx_t,主要包含:

  1. huart:对应的 HAL UART handle。
  2. rx_dma_bufferDMA 接收缓冲。
  3. tx_dma_bufferDMA 发送缓冲。
  4. rx_ring:应用层接收环形缓冲。
  5. tx_ring:应用层发送环形缓冲。
  6. rx_dma_read_indexDMA 快照消费位置。
  7. rx_head/rx_tailRX ring 指针。
  8. tx_head/tx_tailTX ring 指针。
  9. tx_busy:当前是否有 DMA TX 正在进行。
  10. stats:串口统计信息。

10.4.2 关键 API

API 作用
uart_trans_init() 初始化两个业务通道上下文
uart_trans_config() 配置波特率
uart_trans_start() 启动 DMA 接收
uart_trans_stop() 停止通道
uart_trans_poll() 推进接收快照和发送状态
uart_trans_rx_available() 查询 RX ring 可读字节数
uart_trans_read() 从 RX ring 取走数据
uart_trans_write() 写入 TX ring 并触发 DMA 发送
uart_trans_tx_free() 查询 TX ring 剩余空间
uart_mux_try_extract_frame() 从 RX ring 尝试解析一帧 MUX
uart_mux_encode_frame() 把 payload 编码为 MUX 帧

10.4.3 DMA 与 IDLE 路径

USART2/USART3 收到数据
  -> DMA 写入 rx_dma_buffer
  -> IDLE / half complete / complete interrupt
  -> uart_trans_*_handler()
  -> process_rx_snapshot()
  -> 搬入 rx_ring
  -> App_Poll() 中被路由层读取

USART2_IRQHandler()USART3_IRQHandler() 会先检查 UART_FLAG_IDLE,清除 IDLE 后调用 uart_trans_idle_handler(),再进入 HAL handler。DMA half/full complete 也会映射到相应 handler。

10.4.4 MUX 帧格式

MUX 帧固定为:

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

当前实现中:

  1. SYNC = 0x7E
  2. TAIL = 0x7F
  3. LEN_H/LEN_L 是 payload 长度。
  4. payload 最大 256 字节。
  5. 只有完整帧到齐才应推进 RX ring 读指针。

10.5 App/tcp_server.c / App/tcp_server.h

TCP Server 模块管理两路 Server 实例。每个实例只能持有一个当前 client pcb。

10.5.1 状态和配置

说明
TCP_SERVER_INSTANCE_COUNT 2
TCP_SERVER_RX_BUFFER_SIZE 480 字节
tcp_server_instance_config_t port + enabled
tcp_server_status_t 状态、收发字节、连接次数、错误数

10.5.2 关键 API

API 作用
tcp_server_init_all() 清空两个 Server 上下文
tcp_server_config() 配置某个 Server 实例
tcp_server_start() bind/listen 并注册 accept 回调
tcp_server_stop() 关闭 listen pcb 和 client pcb
tcp_server_send() 向当前 client 发送数据
tcp_server_recv() 从 RX ring 读取并消费数据
tcp_server_peek() 查看 RX ring 数据但不消费
tcp_server_drop() 消费数据并释放 TCP 接收窗口
tcp_server_poll() 继续搬运 hold pbuf 到 RX ring

10.5.3 背压模型

TCP Server 接收回调中不会无条件 tcp_recved()。收到 pbuf 后先持有,再尽量搬入 RX ring。只有当主循环确认数据已经被下游 UART 接收进入发送路径,才通过 tcp_server_drop() 释放 TCP 接收窗口。

这个设计的目的:

  1. UART 慢时,TCP 接收窗口会自然收缩。
  2. 不需要为每个连接增加大块 pending buffer。
  3. 在 20KB SRAM 限制下保持可控内存占用。

10.6 App/tcp_client.c / App/tcp_client.h

TCP Client 模块管理两路 Client 实例。

10.6.1 状态和配置

说明
TCP_CLIENT_INSTANCE_COUNT 2
TCP_CLIENT_RX_BUFFER_SIZE 480 字节
TCP_CLIENT_RECONNECT_DELAY_MS 3000ms
TCP_CLIENT_CONNECT_TIMEOUT_MS 10000ms
tcp_client_instance_config_t remote IP、本地端口、远端端口、重连间隔、启用状态

10.6.2 关键 API

API 作用
tcp_client_init_all() 清空两个 Client 上下文
tcp_client_config() 配置某个 Client 实例
tcp_client_connect() 创建 pcb、可选 bind 本地端口、主动 connect
tcp_client_disconnect() 主动断开并清理状态
tcp_client_send() 发送数据
tcp_client_recv() 从 RX ring 读取并消费数据
tcp_client_peek() 查看 RX ring 数据但不消费
tcp_client_drop() 消费数据并释放 TCP 接收窗口
tcp_client_poll() 超时、重连和 pending pbuf 搬运

10.6.3 自动重连

Client 如果启用 auto_reconnect,在断开或连接失败后会设置 next_retry_ms,由 tcp_client_poll() 在主循环中按时间重试。若远端静默导致长时间停留在 CONNECTINGtcp_client_abort_connect_timeout() 会 abort 当前 pcb,并进入下一轮重连等待。

11. 路由层说明

路由层在 Core/Src/main.c 中实现,负责把 UART 和 TCP 模块接起来。

11.1 RAW 模式

cfg->mux_mode == MUX_MODE_RAW 时,数据不带 MUX 帧头尾。

典型流向:

UART2/UART3 RX ring
  -> App_RouteRawUartTraffic()
  -> 根据 LINK 配置选择 TCP Server/Client 实例
  -> tcp_server_send() / tcp_client_send()

TCP 到 UART

tcp_server/client RX ring
  -> App_RouteTcpTraffic()
  -> App_SendTcpPayloadToUartRaw()
  -> uart_trans_tx_free() 判断下游空间
  -> uart_trans_write()
  -> tcp_server_drop() / tcp_client_drop()

关键点是:只有写入 UART TX ring 的字节数确认后,才 drop TCP RX ring 数据并释放 TCP 窗口。

11.2 MUX 模式

cfg->mux_mode == MUX_MODE_FRAME 时,业务数据口必须使用 MUX 帧。

UART 到 TCP

UART RX ring
  -> uart_mux_try_extract_frame()
  -> DSTMASK == 0x00 ? 控制帧 : 业务帧
  -> 控制帧进入 config_build_response_frame()
  -> 业务帧按 DSTMASK 分发到 TCP Server/Client 或另一个 UART

TCP 到 UART

TCP RX ring
  -> App_RouteTcpTraffic()
  -> App_SendTcpPayloadToUartMux()
  -> 确认 UART TX free >= payload_len + 6
  -> uart_mux_encode_frame()
  -> uart_trans_write() 整帧入队
  -> tcp_server_drop() / tcp_client_drop()

MUX 模式要求整帧完整入队,不能把半帧写入 UART TX ring 后就释放 TCP 窗口。

11.3 路由修改检查表

修改路由层时必须检查:

  1. RAW 和 MUX 两种模式是否都覆盖。
  2. TCP 到 UART 是否保持背压语义。
  3. DSTMASK=0x00 是否仍只进入控制帧路径。
  4. SRCID 是否使用正确端点编码。
  5. LINK 中的 uart 字段是否正确映射到 UART_CHANNEL_U0/U1
  6. 修改是否增加大块栈数组或静态数组。

12. CH390 与 lwIP 说明

12.1 Drivers/CH390/CH390_Interface.c / .h

这是最底层硬件访问层,职责包括:

  1. ch390_gpio_init():初始化相关 GPIO。
  2. ch390_spi_init():初始化 SPI 相关访问条件。
  3. ch390_interrupt_init():初始化中断相关条件。
  4. ch390_hardware_reset():硬件复位 CH390。
  5. ch390_read_reg() / ch390_write_reg():寄存器访问。
  6. ch390_read_mem() / ch390_write_mem()CH390 RX/TX SRAM 访问。

这里不应加入 TCP、AT、MUX、lwIP 策略。

12.2 Drivers/CH390/CH390.c / .h

这是芯片级 helper 层,提供寄存器定义和芯片操作封装。常见职责:

  1. 软件复位。
  2. 默认配置。
  3. PHY 访问。
  4. MAC 地址读写。
  5. link status、速率、双工状态读取。

12.3 Drivers/CH390/ch390_runtime.c / .h

ch390_runtime 是当前 CH390 运行时的核心。它负责把底层芯片操作组织成适合主循环调用的网络设备运行时。

主要 API

API 作用
ch390_runtime_init() 初始化 CH390 并绑定 netif
ch390_runtime_input_frame() 从 CH390 RX FIFO 取一帧并生成 pbuf
ch390_runtime_set_irq_pending() 中断侧置 pending 标志
ch390_runtime_poll() 主循环消费 IRQ pending 和 RX ready
ch390_runtime_check_link() 刷新 link 状态
ch390_runtime_output() 把 lwIP pbuf 发送到 CH390
ch390_runtime_get_diag() 读取诊断结构
ch390_runtime_is_ready() 查询 CH390 是否已通过 identity gate
ch390_runtime_health_check() 周期健康检查
ch390_runtime_emergency_reset() 异常情况下恢复芯片

ch390_diag_t 包含 identity、PHY、寄存器、link、收发统计、过滤统计等现场诊断字段。出现 CH390 异常时,应先用 RTT 中的诊断字段判断:

  1. id_valid 是否为 1。
  2. vendor_id/product_id/revision 是否可信。
  3. link_up 是否符合网线状态。
  4. rx_pbuf_alloc_failed 是否持续增长。
  5. rx_filtered_* 是否说明现场有大量无关网络噪声。
  6. tx_packets_timeout 是否持续增长。

12.4 RX pre-pbuf 过滤

为降低 20KB SRAM 下 lwIP pbuf pool 压力,CH390 RX 入口会在分配 pbuf 前读取帧前缀并过滤不相关协议。

当前允许进入 lwIP 的主要流量:

  1. ARP。
  2. IPv4 ICMP。
  3. IPv4 TCP。

默认过滤:

  1. IPv6。
  2. IPv4 UDP。
  3. IPv4 IGMP。
  4. LLDP。
  5. 其他未知 EtherType。
  6. 畸形帧。

如果现场抓包看到大量 IPv6、mDNS、DHCPv6、LLDP、IGMP,这些不应直接进入 lwIP pbuf pool。

12.5 Drivers/LwIP/src/netif/ethernetif.c

这是 lwIP 和 CH390 runtime 的胶水层。关键函数:

函数 作用
lwip_netif_init() netif_add()、设置 default、link down、netif up
ethernetif_init() 设置 output/linkoutput,调用低层初始化
ethernetif_poll() 调用 ch390_runtime_poll()
ethernetif_check_link() 调用 ch390_runtime_check_link()
sys_now() / sys_jiffies() HAL_GetTick() 驱动 lwIP 时间

ethernetif.c 不应重新实现复杂 CH390 策略。如果需要改收发细节,优先进入 ch390_runtime.c

12.6 lwipopts.h

Drivers/LwIP/src/include/arch/lwipopts.h 是 lwIP 裁剪配置。当前关键设置包括:

配置 意义
NO_SYS 1 裸机模式
LWIP_SOCKET 0 不启用 socket API
LWIP_NETCONN 0 不启用 netconn API
LWIP_IPV4 1 启用 IPv4
LWIP_IPV6 0 不启用 IPv6
LWIP_DHCP 0 不启用 DHCP
LWIP_UDP 0 当前业务不启用 UDP
LWIP_TCP 1 启用 TCP
LWIP_ARP 1 启用 ARP
LWIP_ICMP 1 支持 ping 诊断
MEM_SIZE 4KB lwIP heap
PBUF_POOL_SIZE 8 pbuf pool 数量

在 20KB SRAM 限制下,调整这些值会直接影响链接结果和运行稳定性。不要单纯为了“更稳”盲目增大 pool 或 TCP buffer。

13. Core/HAL 层说明

13.1 Core/Src/usart.c

定义三个 UART

外设 角色 默认配置
USART1 AT 配置口 115200 8N1
USART2 业务数据口 U0 默认 115200 8N1,启动后按 Flash 配置可变
USART3 业务数据口 U1 默认 115200 8N1,启动后按 Flash 配置可变

USART1 使用中断单字节接收进入 config_uart_rx_byte()USART2/USART3 使用 DMA + IDLE 路径进入 uart_trans

13.2 Core/Src/spi.c

SPI1 用于访问 CH390D。当前配置重点:

  1. Master 模式。
  2. 8-bit 数据。
  3. CPOL Low。
  4. CPHA 1Edge,即 SPI Mode 0。
  5. 软件 NSS,由 GPIO 控制 CS。
  6. 当前 prescaler 为 2,对应 APB2 下较高 SPI 时钟。

如果 CH390 读写不稳定,先确认 SPI mode、CS 时序和硬件连线,不要先改 lwIP。

13.3 Core/Src/gpio.c

关键 GPIO

引脚 当前用途
PC13 LED
PA4 CH390 SPI CS,默认置高
PB0 CH390 INT,下降沿中断
PB1 CH390 reset 或相关控制输出
PA5/PA6/PA7 SPI1 SCK/MISO/MOSI

CH390 bring-up 失败时,应同时核对 gpio.cspi.c、PCB 原理图和实际波形。

13.4 Core/Src/stm32f1xx_it.c

中断入口职责:

  1. Fault handler 统一进入 Debug_TrapWithRttHint()
  2. DMA channel handler 进入 HAL DMA handler。
  3. USART2/USART3 IRQ 先处理 IDLE,再交给 HAL。
  4. HAL_UART_RxCpltCallback()USART1 字节交给 config_uart_rx_byte(),并重新启动单字节接收。
  5. HAL_UART_TxCpltCallback()、DMA complete callback 将事件交给 uart_trans
  6. CH390 EXTI 中断只应置 pending,不应在中断里跑长 SPI 事务。

14. 构建与产物说明

14.1 Keil MDK

当前建议优先以 Keil 工程作为实机验收基线:

MDK-ARM/TCP2UART.uvprojx

检查 Keil 工程时重点确认:

  1. Target 是否为 TCP2UART
  2. 芯片型号和 Flash/RAM 区域是否匹配 STM32F103R8。
  3. App/*.c 是否全部纳入工程。
  4. Drivers/CH390/*.c 是否全部纳入工程。
  5. Drivers/LwIP/src/netif/ethernetif.c 是否纳入工程。
  6. Middlewares/Third_Party/SEGGER_RTT/*.c 是否纳入工程。
  7. Include path 是否包含 Core/IncAppDrivers/CH390Drivers/LwIP/src/includeDrivers/LwIP/src/include/arch

14.2 CMake

CMake 入口用于交叉检查源码组织:

  1. CMakeLists.txt
  2. cmake/stm32cubemx/CMakeLists.txt
  3. CMakePresets.json
  4. cmake/gcc-arm-none-eabi.cmake

cmake/stm32cubemx/CMakeLists.txt 是查看当前源码清单的好入口。若 Keil 构建缺符号,可先对比这里是否有同名 .c 文件未加入 Keil 工程。

15. 常见修改任务说明

15.1 新增或修改 AT 命令

修改位置:

  1. AT固件使用手册.md:先定义外部协议。
  2. App/config.h:如需新增字段,修改结构体和版本。
  3. App/config.c:在 config_process_at_cmd() 中增加命令分支。
  4. config_set_defaults():补默认值。
  5. handle_summary_query():必要时补查询输出。
  6. config_save() / config_load():确认持久化。
  7. 项目需求说明.md项目技术实现.md:如协议模型变化需同步。

检查点:

  1. 命令是否仍以 AT+...\r\n 结束。
  2. 是否破坏 MUX / NET / LINK 三层模型。
  3. 是否需要保存后重启才生效。
  4. 是否需要 Flash 版本迁移。

15.2 修改默认网络或链路参数

修改位置:

  1. App/config.h 中的 DEFAULT_NET_*
  2. App/config.cconfig_set_defaults()
  3. AT固件使用手册.md 默认配置章节。
  4. 项目需求说明.md 如默认行为属于需求。

注意:只改文档不改 config_set_defaults() 不会改变固件行为;只改代码不改手册会造成联调误解。

15.3 修改 UART 波特率策略

修改位置:

  1. App/config.h 的默认波特率。
  2. App/config.cAT+BAUD 解析和范围。
  3. App/uart_trans.capply_uart_config()
  4. Core/Src/usart.c 的 CubeMX 初始值。
  5. AT固件使用手册.md 的 BAUD 命令章节。

当前策略是:AT+BAUD 修改运行配置,执行 AT+SAVEAT+RESET 后,重启时按保存值配置 USART2/USART3

15.4 修改 TCP 实例数量

这类修改影响面大,不建议作为小改动处理。涉及:

  1. CONFIG_LINK_COUNTCONFIG_LINK_*
  2. TCP_SERVER_INSTANCE_COUNT
  3. TCP_CLIENT_INSTANCE_COUNT
  4. device_config_t.links[]
  5. App_ConfigureLinks()
  6. MUX 端点编码。
  7. AT 手册和需求文档。
  8. SRAM 占用和 pbuf pool。

修改前必须先估算 RAM。每增加一路 TCP 或 UART ring 都会消耗静态内存。

15.5 修改 CH390 驱动

建议顺序:

  1. 先明确问题是硬件访问、芯片配置、runtime 策略还是 lwIP glue。
  2. 硬件访问问题改 CH390_Interface.c
  3. 寄存器/PHY/MAC helper 问题改 CH390.c
  4. 收发、过滤、link、health check 问题改 ch390_runtime.c
  5. netif 接口问题才改 ethernetif.c

禁止把 CH390 寄存器访问散回 main.c 或中断 handler。

16. 调试路线图

16.1 设备无 RTT 输出

检查顺序:

  1. 是否下载了正确 axf/hex。
  2. MCU 是否复位后运行。
  3. SystemClock_Config() 是否进入 fallback 或 trap。
  4. SEGGER_RTT_Init() 是否执行。
  5. Fault handler 是否进入 Debug_TrapWithRttHint()

16.2 AT 配置口无响应

检查顺序:

  1. USART1 引脚接线是否正确。
  2. 主机串口是否为 115200 8N1
  3. 命令是否以 \r\n 结束。
  4. HAL_UART_RxCpltCallback() 是否被触发。
  5. config_uart_rx_byte() 是否收到字节。
  6. g_pending_cmd_ready 是否置位。
  7. config_poll() 是否在主循环中执行。
  8. config_process_at_cmd() 是否返回响应。

16.3 CH390 identity 异常

检查顺序:

  1. CH390 供电和复位脚。
  2. SPI CS/SCK/MISO/MOSI 波形。
  3. SPI mode 和时钟。
  4. ch390_hardware_reset() 是否执行。
  5. ch390_runtime_probe_identity() 读回值。
  6. CH390_最终结论报告.md 中的历史硬件问题。

16.4 ping 不通

检查顺序:

  1. NET 配置是否和 PC 同网段。
  2. MAC 是否有效且不冲突。
  3. link_up 是否为 1。
  4. RTT 中是否看到 ARP/ICMP 统计增长。
  5. CH390 RX 过滤是否误过滤目标帧。
  6. pbuf pool 是否耗尽。
  7. PC 侧抓包是否看到 ARP request/reply 和 ICMP request/reply。

16.5 TCP 连接不上

检查顺序:

  1. 对应 LINK 是否启用。
  2. Server 本地端口是否正确。
  3. Client 远端 IP/端口是否正确。
  4. App_StartLinksIfNeeded() 是否已执行。
  5. tcp_server_start() 是否 listen 成功。
  6. tcp_client_connect() 是否进入 CONNECTING
  7. connect_timeout_count 是否增长。
  8. PC 侧是否有防火墙或端口未监听。

16.6 TCP 到 UART 丢包

检查顺序:

  1. UART TX ring 是否长期接近满。
  2. TCP RX ring 是否堆积。
  3. hold_pbuf 是否长期存在。
  4. tcp_recved() 是否只在 drop 后调用。
  5. MUX 模式下是否整帧入队后才 drop。
  6. 上位机是否按目标波特率打开串口。

17. 代码审查检查表

提交前建议逐项检查:

  1. 是否引入新的大块静态数组。
  2. 是否在 ISR 中加入耗时 SPI、Flash 或网络操作。
  3. 是否破坏 TCP 背压,即提前调用 tcp_recved()
  4. 是否把旧 AT 展开式命令重新暴露为对外协议。
  5. 是否同时更新了代码和对应中文文档。
  6. 是否修改了 device_config_t 却没有处理版本和默认值。
  7. 是否修改了 MUX 帧格式却没有更新手册和需求。
  8. 是否修改了 lwIP 内存配置却没有重新评估 20KB SRAM。
  9. 是否把调试临时代码留在主路径。
  10. 是否让 Drivers/LwIP/port/sys_arch.c 重新进入当前裸机主路径。

18. 推荐阅读路径

18.1 第一次接手项目

  1. 项目文档索引.md
  2. 项目需求说明.md
  3. AT固件使用手册.md
  4. 本文档第 1 到第 9 节
  5. Core/Src/main.c
  6. App/config.h / App/config.c

18.2 做应用层修改

  1. Core/Src/main.c
  2. App/config.c
  3. App/uart_trans.c
  4. App/tcp_server.c
  5. App/tcp_client.c
  6. 项目技术实现.md

18.3 做网络驱动修改

  1. 工程调试指南.md
  2. CH390_最终结论报告.md
  3. Drivers/CH390/ch390_runtime.c
  4. Drivers/CH390/CH390_Interface.c
  5. Drivers/CH390/CH390.c
  6. Drivers/LwIP/src/netif/ethernetif.c

18.4 做构建或工程配置修改

  1. MDK-ARM/TCP2UART.uvprojx
  2. cmake/stm32cubemx/CMakeLists.txt
  3. TCP2UART.ioc
  4. CMakeLists.txt
  5. STM32F103XX_FLASH.ld

19. 维护原则

  1. 当前项目优先保证可维护性和现场可诊断性,再考虑性能优化。
  2. SRAM 余量很小,新增缓冲前必须先确认是否能复用现有 ring 或 pbuf 机制。
  3. 中断只做短操作,长事务放回主循环。
  4. CH390 运行时所有权集中在 ch390_runtime
  5. AT 外部协议只维护 MUX / NET / LINK 模型。
  6. 修改行为必须同步更新中文文档。
  7. 调试日志、构建输出、现场临时记录不要长期留在项目根目录。