From 0e594164773125aa571e0b8a3f5fd04b6e8f0b40 Mon Sep 17 00:00:00 2001 From: xiao Date: Wed, 10 Jun 2026 10:17:53 +0800 Subject: [PATCH] docs: add code structure reading guide --- 代码结构与阅读指南.md | 1159 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1159 insertions(+) create mode 100644 代码结构与阅读指南.md diff --git a/代码结构与阅读指南.md b/代码结构与阅读指南.md new file mode 100644 index 0000000..a5b5bb2 --- /dev/null +++ b/代码结构与阅读指南.md @@ -0,0 +1,1159 @@ +# 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. 调试日志、构建输出、现场临时记录不要长期留在项目根目录。