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

1160 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```text
PC TCP Client
-> 连接设备 IP:8080
-> TCP payload
-> CH390D
-> lwIP RAW TCP Server S1
-> App_RouteTcpTraffic()
-> USART2 TX
-> 外部串口设备
```
反方向是:
```text
外部串口设备
-> 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`
```text
外部串口设备
-> USART3 RX DMA
-> uart_trans RX ring
-> App_RouteRawUartTraffic()
-> TCP Client C1
-> CH390D
-> 远端 TCP Server 192.168.1.200:9000
```
反方向是:
```text
远端 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` 为例:
```text
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` 为例:
```text
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 用这个例子读代码的最短路径
如果只想先看懂无协议透传,按这个顺序读:
1. `AT固件使用手册.md` 的默认 `MUX / NET / LINK / BAUD`
2. `App/config.h``device_config_t``CONFIG_LINK_*``LINK_UART_U0/U1`
3. `Core/Src/main.c``App_Init()``App_ConfigureLinks()`
4. `Core/Src/main.c``App_Poll()`
5. `Core/Src/main.c``App_RouteRawUartTraffic()`
6. `Core/Src/main.c``App_RouteTcpTraffic()`
7. `App/uart_trans.c``uart_trans_read()``uart_trans_write()` 和 DMA/IDLE handler。
8. `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 业务转发。
关键文件:
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。
### 4.3 阶段三:网络 link up 后启动 TCP 实例
主循环每轮都会调用:
```text
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()` 每轮都做四类事情:
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.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. 顶层目录说明
```text
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. 总体运行时架构
```text
+--------------------------------------------------+
| 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.c``tcp_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.c``main()` 是总入口。典型顺序是:
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_mode`RAW 或 MUX 模式。
4. `net`IP、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 命令路径
```text
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 控制帧路径
```text
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_buffer`DMA 接收缓冲。
3. `tx_dma_buffer`DMA 发送缓冲。
4. `rx_ring`:应用层接收环形缓冲。
5. `tx_ring`:应用层发送环形缓冲。
6. `rx_dma_read_index`DMA 快照消费位置。
7. `rx_head/rx_tail`RX ring 指针。
8. `tx_head/tx_tail`TX 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 路径
```text
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 帧固定为:
```text
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()` 在主循环中按时间重试。若远端静默导致长时间停留在 `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 帧头尾。
典型流向:
```text
UART2/UART3 RX ring
-> App_RouteRawUartTraffic()
-> 根据 LINK 配置选择 TCP Server/Client 实例
-> tcp_server_send() / tcp_client_send()
```
TCP 到 UART
```text
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
```text
UART RX ring
-> uart_mux_try_extract_frame()
-> DSTMASK == 0x00 ? 控制帧 : 业务帧
-> 控制帧进入 config_build_response_frame()
-> 业务帧按 DSTMASK 分发到 TCP Server/Client 或另一个 UART
```
TCP 到 UART
```text
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.c``spi.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 工程作为实机验收基线:
```text
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/Inc``App``Drivers/CH390``Drivers/LwIP/src/include``Drivers/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.c``config_set_defaults()`
3. `AT固件使用手册.md` 默认配置章节。
4. `项目需求说明.md` 如默认行为属于需求。
注意:只改文档不改 `config_set_defaults()` 不会改变固件行为;只改代码不改手册会造成联调误解。
### 15.3 修改 UART 波特率策略
修改位置:
1. `App/config.h` 的默认波特率。
2. `App/config.c``AT+BAUD` 解析和范围。
3. `App/uart_trans.c``apply_uart_config()`
4. `Core/Src/usart.c` 的 CubeMX 初始值。
5. `AT固件使用手册.md` 的 BAUD 命令章节。
当前策略是:`AT+BAUD` 修改运行配置,执行 `AT+SAVE``AT+RESET` 后,重启时按保存值配置 `USART2/USART3`
### 15.4 修改 TCP 实例数量
这类修改影响面大,不建议作为小改动处理。涉及:
1. `CONFIG_LINK_COUNT``CONFIG_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. 调试日志、构建输出、现场临时记录不要长期留在项目根目录。