# 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 先看 S1:PC 通过 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. 调试日志、构建输出、现场临时记录不要长期留在项目根目录。