43 KiB
TCP2UART 代码结构与阅读指南
1. 文档目的
本文档是一份面向接手维护者的代码说明书。目标不是简单列出目录,而是帮助读者在没有原作者口头说明的情况下,能够完成以下工作:
- 理解
TCP2UART固件的整体架构和运行时数据流。 - 找到每个功能对应的源码入口。
- 看懂启动、配置、网络、串口、TCP、MUX、Flash 参数保存之间的调用关系。
- 按正确顺序阅读代码,避免从历史文件或非主路径入口误入。
- 修改 AT 命令、串口透传、TCP 链路、CH390 驱动或构建配置时知道应该同步检查哪些文件。
- 排查现场问题时能按层次定位,而不是在多个模块之间盲目跳转。
本文档只描述当前主线实现。历史阶段性计划、旧 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 这个例子解决什么问题
假设现场有两类设备:
- 一个串口设备接在
USART2,希望远端 PC 通过 TCP 连接访问它。 - 另一个串口设备接在
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 先看 S1:PC 通过 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.c、App/tcp_server.c |
| TCP 收包 | tcp_server_on_recv() |
App/tcp_server.c |
| TCP 到串口 | App_RouteTcpTraffic() -> App_SendTcpPayloadToUartRaw() -> uart_trans_write() |
Core/Src/main.c、App/uart_trans.c |
| 串口收包 | uart_trans_idle_handler() / DMA 回调 -> RX ring |
App/uart_trans.c、Core/Src/stm32f1xx_it.c |
| 串口到 TCP | App_RouteRawUartTraffic() -> App_SendTcpServerPayload() -> tcp_server_send() |
Core/Src/main.c、App/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.c、App/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.c、App/uart_trans.c |
| 串口到 TCP | App_RouteRawUartTraffic() -> App_SendTcpClientPayload() -> tcp_client_send() |
Core/Src/main.c、App/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.c、App/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 用这个例子读代码的最短路径
如果只想先看懂无协议透传,按这个顺序读:
AT固件使用手册.md的默认MUX / NET / LINK / BAUD。App/config.h的device_config_t、CONFIG_LINK_*、LINK_UART_U0/U1。Core/Src/main.c的App_Init()和App_ConfigureLinks()。Core/Src/main.c的App_Poll()。Core/Src/main.c的App_RouteRawUartTraffic()。Core/Src/main.c的App_RouteTcpTraffic()。App/uart_trans.c的uart_trans_read()、uart_trans_write()和 DMA/IDLE handler。App/tcp_server.c或App/tcp_client.c的send/peek/drop/poll。
看完这一条线,再回头看 MUX 模式、CH390 runtime 和 lwIP glue,会容易很多。
4. 系统从上电到稳定转发的工作过程
理解整个系统可以按“配置生效、网络就绪、链路启动、持续搬运、异常恢复”五个阶段来看。
4.1 阶段一:上电和外设初始化
main() 先完成 HAL、时钟、GPIO、DMA、USART、SPI、TIM、IWDG 初始化。这个阶段只让 MCU 外设进入可用状态,还没有真正开始 TCP/UART 业务转发。
关键文件:
Core/Src/main.cCore/Src/gpio.cCore/Src/dma.cCore/Src/usart.cCore/Src/spi.cCore/Src/tim.cCore/Src/iwdg.c
4.2 阶段二:应用初始化
App_Init() 让应用配置和软件模块进入可运行状态:
- 从 Flash 加载配置,失败时使用默认配置。
- 按配置初始化
USART2/USART3业务通道。 - 初始化 RTT 和栈保护。
- 初始化 lwIP。
- 用
NET配置创建ch390_netif。 - 把
LINK配置转换成 TCP Server/Client 内部配置。 - 打印 CH390 启动诊断。
- 启动
USART1AT 配置口接收。
这时 TCP 实例还不一定已经启动,因为启动还要等待 CH390 link up。
4.3 阶段三:网络 link up 后启动 TCP 实例
主循环每轮都会调用:
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 down,App_StopLinksIfNeeded() 会停止 TCP 实例并清除 g_links_started。所以下一次 link up 后,系统会重新启动 TCP 链路。
4.4 阶段四:主循环持续搬运数据
稳定运行后,App_Poll() 每轮都做四类事情:
- 推进网络设备:
ethernetif_poll()、ethernetif_check_link()、sys_check_timeouts()。 - 推进 TCP 状态:
tcp_server_poll()、tcp_client_poll()。 - 推进 UART 状态:
uart_trans_poll()。 - 执行业务路由:先
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.c、App/flash_param.c |
| 数据平面 | UART 与 TCP 之间的数据转发,RAW/MUX 路由 | Core/Src/main.c、App/uart_trans.c、App/tcp_server.c、App/tcp_client.c |
| 设备平面 | CH390、lwIP netif、SPI、GPIO、DMA、中断 | Drivers/CH390/*、Drivers/LwIP/src/netif/ethernetif.c、Core/Src/* |
阅读代码时不要把这三个平面混在一起。例如,AT 命令解析失败时先看 config.c;CH390 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_S1、CONFIG_LINK_S2、CONFIG_LINK_C1、CONFIG_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 |
+--------------------------------------------------+
关键原则:
Core/Src/main.c负责调度,不直接展开复杂 CH390 寄存器事务。ch390_runtime.c是 CH390 运行时唯一拥有者。ethernetif.c只做 lwIP netif glue,不承载复杂策略。tcp_server.c和tcp_client.c只管理 TCP 实例,不理解 AT 命令。uart_trans.c只管理 UART ring、DMA、MUX 帧,不直接理解 TCP 链路配置。config.c只管理配置、AT 和 Flash 参数,不直接执行网络收发。
8. 启动顺序说明
8.1 main() 的职责
Core/Src/main.c 的 main() 是总入口。典型顺序是:
HAL_Init()初始化 HAL 和 SysTick。SystemClock_Config()配置系统时钟。MX_GPIO_Init()初始化 GPIO。MX_DMA_Init()初始化 DMA。MX_SPI1_Init()初始化 CH390 使用的 SPI。MX_USART1_UART_Init()初始化 AT 配置口。MX_USART2_UART_Init()/MX_USART3_UART_Init()初始化业务串口。MX_TIM4_Init()、MX_IWDG_Init()等外设初始化。App_Init()初始化应用层。- 进入
while (1),持续调用App_Poll()。
8.2 App_Init() 的职责
App_Init() 是应用层启动的关键函数。当前顺序如下:
config_init():加载 Flash 参数,失败时使用默认配置。config_get():取得当前配置。uart_trans_init():初始化业务串口传输模块上下文。- 根据
cfg->uart_baudrate[0/1]配置U0/U1。 uart_trans_start(U0/U1):启动业务串口 DMA 接收。SEGGER_RTT_Init():初始化 RTT 输出。StackGuard_Init():初始化栈保护检查。- 打印启动日志和时钟 fallback 警告。
lwip_init():初始化 lwIP。- 从
cfg->net组装 IP、Mask、Gateway。 lwip_netif_init():添加并启用ch390_netif。App_ConfigureLinks(cfg):把LINK配置下发到 TCP Server/Client 模块。BootDiag_ReportCh390():打印 CH390 启动诊断。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() |
阅读主循环时应重点关注两个状态:
g_links_started:表示 TCP 实例是否已经在当前 link up 周期启动。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 包含:
magic:结构标识。version:结构版本。mux_mode:RAW 或 MUX 模式。net:IP、Mask、Gateway、MAC。links[4]:四条 LINK 配置。uart_baudrate[2]:两个业务串口波特率。crc:结构校验。
10.2 App/config.c
config.c 是 AT 命令和配置持久化的核心。建议按以下顺序阅读:
- 顶部静态变量,理解
g_config、命令缓冲区、pending 命令标志。 config_calc_crc(),理解 CRC 覆盖范围。config_set_defaults(),理解默认配置。config_load(),理解 Flash 加载、版本判断、默认值 fallback。try_load_legacy_config()和migrate_legacy_config(),理解 v2 到 v3 的参数迁移。config_process_at_cmd(),理解 AT 命令分发入口。handle_summary_query()、handle_net_query()、handle_link_query()等具体命令处理。config_uart_rx_byte()和config_poll(),理解 USART1 接收如何进入 AT 解析。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:
DSTMASK=0x00:系统控制帧,payload 是 AT 文本。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() |
校验参数页完整性 |
修改配置结构时必须同步检查:
CONFIG_VERSION是否需要递增。device_config_t是否仍适合 1KB 参数页。config_set_defaults()是否覆盖新增字段。config_calc_crc()覆盖范围是否正确。- 是否需要新增旧版本迁移逻辑。
10.4 App/uart_trans.c / App/uart_trans.h
UART 传输模块管理两个业务串口。它不处理 USART1 配置口。
10.4.1 内部上下文
每个业务串口有一个 uart_channel_ctx_t,主要包含:
huart:对应的 HAL UART handle。rx_dma_buffer:DMA 接收缓冲。tx_dma_buffer:DMA 发送缓冲。rx_ring:应用层接收环形缓冲。tx_ring:应用层发送环形缓冲。rx_dma_read_index:DMA 快照消费位置。rx_head/rx_tail:RX ring 指针。tx_head/tx_tail:TX ring 指针。tx_busy:当前是否有 DMA TX 正在进行。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
当前实现中:
SYNC = 0x7E。TAIL = 0x7F。LEN_H/LEN_L是 payload 长度。payload最大 256 字节。- 只有完整帧到齐才应推进 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 接收窗口。
这个设计的目的:
- UART 慢时,TCP 接收窗口会自然收缩。
- 不需要为每个连接增加大块 pending buffer。
- 在 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() 在主循环中按时间重试。若远端静默导致长时间停留在 CONNECTING,tcp_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 路由修改检查表
修改路由层时必须检查:
- RAW 和 MUX 两种模式是否都覆盖。
- TCP 到 UART 是否保持背压语义。
DSTMASK=0x00是否仍只进入控制帧路径。SRCID是否使用正确端点编码。LINK中的uart字段是否正确映射到UART_CHANNEL_U0/U1。- 修改是否增加大块栈数组或静态数组。
12. CH390 与 lwIP 说明
12.1 Drivers/CH390/CH390_Interface.c / .h
这是最底层硬件访问层,职责包括:
ch390_gpio_init():初始化相关 GPIO。ch390_spi_init():初始化 SPI 相关访问条件。ch390_interrupt_init():初始化中断相关条件。ch390_hardware_reset():硬件复位 CH390。ch390_read_reg()/ch390_write_reg():寄存器访问。ch390_read_mem()/ch390_write_mem():CH390 RX/TX SRAM 访问。
这里不应加入 TCP、AT、MUX、lwIP 策略。
12.2 Drivers/CH390/CH390.c / .h
这是芯片级 helper 层,提供寄存器定义和芯片操作封装。常见职责:
- 软件复位。
- 默认配置。
- PHY 访问。
- MAC 地址读写。
- 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 中的诊断字段判断:
id_valid是否为 1。vendor_id/product_id/revision是否可信。link_up是否符合网线状态。rx_pbuf_alloc_failed是否持续增长。rx_filtered_*是否说明现场有大量无关网络噪声。tx_packets_timeout是否持续增长。
12.4 RX pre-pbuf 过滤
为降低 20KB SRAM 下 lwIP pbuf pool 压力,CH390 RX 入口会在分配 pbuf 前读取帧前缀并过滤不相关协议。
当前允许进入 lwIP 的主要流量:
- ARP。
- IPv4 ICMP。
- IPv4 TCP。
默认过滤:
- IPv6。
- IPv4 UDP。
- IPv4 IGMP。
- LLDP。
- 其他未知 EtherType。
- 畸形帧。
如果现场抓包看到大量 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。当前配置重点:
- Master 模式。
- 8-bit 数据。
- CPOL Low。
- CPHA 1Edge,即 SPI Mode 0。
- 软件 NSS,由 GPIO 控制 CS。
- 当前 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.c、spi.c、PCB 原理图和实际波形。
13.4 Core/Src/stm32f1xx_it.c
中断入口职责:
- Fault handler 统一进入
Debug_TrapWithRttHint()。 - DMA channel handler 进入 HAL DMA handler。
USART2/USART3IRQ 先处理 IDLE,再交给 HAL。HAL_UART_RxCpltCallback()将USART1字节交给config_uart_rx_byte(),并重新启动单字节接收。HAL_UART_TxCpltCallback()、DMA complete callback 将事件交给uart_trans。- CH390 EXTI 中断只应置 pending,不应在中断里跑长 SPI 事务。
14. 构建与产物说明
14.1 Keil MDK
当前建议优先以 Keil 工程作为实机验收基线:
MDK-ARM/TCP2UART.uvprojx
检查 Keil 工程时重点确认:
- Target 是否为
TCP2UART。 - 芯片型号和 Flash/RAM 区域是否匹配 STM32F103R8。
App/*.c是否全部纳入工程。Drivers/CH390/*.c是否全部纳入工程。Drivers/LwIP/src/netif/ethernetif.c是否纳入工程。Middlewares/Third_Party/SEGGER_RTT/*.c是否纳入工程。- Include path 是否包含
Core/Inc、App、Drivers/CH390、Drivers/LwIP/src/include、Drivers/LwIP/src/include/arch。
14.2 CMake
CMake 入口用于交叉检查源码组织:
CMakeLists.txtcmake/stm32cubemx/CMakeLists.txtCMakePresets.jsoncmake/gcc-arm-none-eabi.cmake
cmake/stm32cubemx/CMakeLists.txt 是查看当前源码清单的好入口。若 Keil 构建缺符号,可先对比这里是否有同名 .c 文件未加入 Keil 工程。
15. 常见修改任务说明
15.1 新增或修改 AT 命令
修改位置:
AT固件使用手册.md:先定义外部协议。App/config.h:如需新增字段,修改结构体和版本。App/config.c:在config_process_at_cmd()中增加命令分支。config_set_defaults():补默认值。handle_summary_query():必要时补查询输出。config_save()/config_load():确认持久化。项目需求说明.md和项目技术实现.md:如协议模型变化需同步。
检查点:
- 命令是否仍以
AT+...且\r\n结束。 - 是否破坏
MUX / NET / LINK三层模型。 - 是否需要保存后重启才生效。
- 是否需要 Flash 版本迁移。
15.2 修改默认网络或链路参数
修改位置:
App/config.h中的DEFAULT_NET_*。App/config.c的config_set_defaults()。AT固件使用手册.md默认配置章节。项目需求说明.md如默认行为属于需求。
注意:只改文档不改 config_set_defaults() 不会改变固件行为;只改代码不改手册会造成联调误解。
15.3 修改 UART 波特率策略
修改位置:
App/config.h的默认波特率。App/config.c的AT+BAUD解析和范围。App/uart_trans.c的apply_uart_config()。Core/Src/usart.c的 CubeMX 初始值。AT固件使用手册.md的 BAUD 命令章节。
当前策略是:AT+BAUD 修改运行配置,执行 AT+SAVE 和 AT+RESET 后,重启时按保存值配置 USART2/USART3。
15.4 修改 TCP 实例数量
这类修改影响面大,不建议作为小改动处理。涉及:
CONFIG_LINK_COUNT和CONFIG_LINK_*。TCP_SERVER_INSTANCE_COUNT。TCP_CLIENT_INSTANCE_COUNT。device_config_t.links[]。App_ConfigureLinks()。- MUX 端点编码。
- AT 手册和需求文档。
- SRAM 占用和 pbuf pool。
修改前必须先估算 RAM。每增加一路 TCP 或 UART ring 都会消耗静态内存。
15.5 修改 CH390 驱动
建议顺序:
- 先明确问题是硬件访问、芯片配置、runtime 策略还是 lwIP glue。
- 硬件访问问题改
CH390_Interface.c。 - 寄存器/PHY/MAC helper 问题改
CH390.c。 - 收发、过滤、link、health check 问题改
ch390_runtime.c。 - netif 接口问题才改
ethernetif.c。
禁止把 CH390 寄存器访问散回 main.c 或中断 handler。
16. 调试路线图
16.1 设备无 RTT 输出
检查顺序:
- 是否下载了正确 axf/hex。
- MCU 是否复位后运行。
SystemClock_Config()是否进入 fallback 或 trap。SEGGER_RTT_Init()是否执行。- Fault handler 是否进入
Debug_TrapWithRttHint()。
16.2 AT 配置口无响应
检查顺序:
USART1引脚接线是否正确。- 主机串口是否为
115200 8N1。 - 命令是否以
\r\n结束。 HAL_UART_RxCpltCallback()是否被触发。config_uart_rx_byte()是否收到字节。g_pending_cmd_ready是否置位。config_poll()是否在主循环中执行。config_process_at_cmd()是否返回响应。
16.3 CH390 identity 异常
检查顺序:
- CH390 供电和复位脚。
- SPI CS/SCK/MISO/MOSI 波形。
- SPI mode 和时钟。
ch390_hardware_reset()是否执行。ch390_runtime_probe_identity()读回值。CH390_最终结论报告.md中的历史硬件问题。
16.4 ping 不通
检查顺序:
NET配置是否和 PC 同网段。- MAC 是否有效且不冲突。
link_up是否为 1。- RTT 中是否看到 ARP/ICMP 统计增长。
- CH390 RX 过滤是否误过滤目标帧。
- pbuf pool 是否耗尽。
- PC 侧抓包是否看到 ARP request/reply 和 ICMP request/reply。
16.5 TCP 连接不上
检查顺序:
- 对应
LINK是否启用。 - Server 本地端口是否正确。
- Client 远端 IP/端口是否正确。
App_StartLinksIfNeeded()是否已执行。tcp_server_start()是否 listen 成功。tcp_client_connect()是否进入CONNECTING。connect_timeout_count是否增长。- PC 侧是否有防火墙或端口未监听。
16.6 TCP 到 UART 丢包
检查顺序:
- UART TX ring 是否长期接近满。
- TCP RX ring 是否堆积。
hold_pbuf是否长期存在。tcp_recved()是否只在 drop 后调用。- MUX 模式下是否整帧入队后才 drop。
- 上位机是否按目标波特率打开串口。
17. 代码审查检查表
提交前建议逐项检查:
- 是否引入新的大块静态数组。
- 是否在 ISR 中加入耗时 SPI、Flash 或网络操作。
- 是否破坏 TCP 背压,即提前调用
tcp_recved()。 - 是否把旧 AT 展开式命令重新暴露为对外协议。
- 是否同时更新了代码和对应中文文档。
- 是否修改了
device_config_t却没有处理版本和默认值。 - 是否修改了 MUX 帧格式却没有更新手册和需求。
- 是否修改了 lwIP 内存配置却没有重新评估 20KB SRAM。
- 是否把调试临时代码留在主路径。
- 是否让
Drivers/LwIP/port/sys_arch.c重新进入当前裸机主路径。
18. 推荐阅读路径
18.1 第一次接手项目
项目文档索引.md项目需求说明.mdAT固件使用手册.md- 本文档第 1 到第 9 节
Core/Src/main.cApp/config.h/App/config.c
18.2 做应用层修改
Core/Src/main.cApp/config.cApp/uart_trans.cApp/tcp_server.cApp/tcp_client.c项目技术实现.md
18.3 做网络驱动修改
工程调试指南.mdCH390_最终结论报告.mdDrivers/CH390/ch390_runtime.cDrivers/CH390/CH390_Interface.cDrivers/CH390/CH390.cDrivers/LwIP/src/netif/ethernetif.c
18.4 做构建或工程配置修改
MDK-ARM/TCP2UART.uvprojxcmake/stm32cubemx/CMakeLists.txtTCP2UART.iocCMakeLists.txtSTM32F103XX_FLASH.ld
19. 维护原则
- 当前项目优先保证可维护性和现场可诊断性,再考虑性能优化。
- SRAM 余量很小,新增缓冲前必须先确认是否能复用现有 ring 或 pbuf 机制。
- 中断只做短操作,长事务放回主循环。
- CH390 运行时所有权集中在
ch390_runtime。 - AT 外部协议只维护
MUX / NET / LINK模型。 - 修改行为必须同步更新中文文档。
- 调试日志、构建输出、现场临时记录不要长期留在项目根目录。