1160 lines
43 KiB
Markdown
1160 lines
43 KiB
Markdown
# 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. 调试日志、构建输出、现场临时记录不要长期留在项目根目录。
|