docs: reorganize project documentation

This commit is contained in:
2026-06-10 11:00:28 +08:00
parent 58361589d8
commit 51b174d330
9 changed files with 561 additions and 1840 deletions
+449
View File
@@ -0,0 +1,449 @@
# TCP2UART 项目代码阅读指南
## 1. 文档目的
本文面向接手 `TCP2UART` 的固件开发、联调和调试人员,目标是说明当前代码如何组织、启动后如何运行、TCP 与 UART 数据如何流动,以及应该按什么顺序阅读源码。
本文只描述当前工程的长期有效结构。历史调试交接文档中的某些“下一步动作”属于当时现场状态,不再作为当前项目入口。
## 2. 系统边界
### 2.1 硬件边界
- MCU`STM32F103RCT6`
- 以太网芯片:`CH390D`
- 配置口:`USART1`
- 数据口:`USART2``USART3`
- 调试输出:`SEGGER RTT`
### 2.2 软件边界
- 工程:`MDK-ARM/TCP2UART.uvprojx`
- 调度:`FreeRTOS`
- 网络:`lwIP NO_SYS=0 + netconn API`
- 网络输入:`tcpip_thread` + `ethernetif` + CH390 驱动
- 业务链路:2 路 TCP Server2 路 TCP Client2 路 UART 数据口
- 配置协议:`AT``MUX``NET``LINK``BAUD``SAVE``RESET``DEFAULT`
### 2.3 目录结构
```text
App/
app_runtime.h 全局任务、队列、信号量声明
config.c/.h AT 命令、运行配置、默认值、Flash 保存
flash_param.c/.h Flash 参数读写与 CRC
route_msg.c/.h 固定消息池和路由消息封装
task_net_poll.c/.h lwIP/CH390 初始化、netif ready、网络重启
tcp_server.c/.h S1/S2 TCP Server 任务
tcp_client.c/.h C1/C2 TCP Client 任务
uart_trans.c/.h USART2/3 业务数据接收、发送、MUX 编解码
Core/Inc, Core/Src/
main.c 上电入口和外设初始化顺序
freertos.c FreeRTOS 队列、信号量、任务创建
stm32f1xx_it.c 中断入口,尤其是 UART IDLE 和 CH390 EXTI
usart.c/dma.c/... STM32CubeMX 生成的外设初始化
debug_log.c/.h RTT 日志和异常提示
Drivers/CH390/
CH390.c/.h 芯片级寄存器/辅助操作
CH390_Interface.c SPI/GPIO 与 CH390 事务封装
Drivers/LwIP/
src/include/arch/lwipopts.h 当前 lwIP 配置
src/netif/ethernetif.c CH390 与 lwIP netif 胶水层
port/sys_arch.c lwIP 在 FreeRTOS 上的 sys_arch 适配
```
## 3. 总体架构
```text
+------------------------------------------------------+
| AT / Control Plane |
| USART1 IDLE DMA -> ConfigTask -> config_process_at_cmd|
+------------------------------------------------------+
| Configuration Model |
| MUX / NET / LINK[S1,S2,C1,C2] / BAUD |
+------------------------------------------------------+
| Routing Layer |
| route_msg fixed pool + xTcpRxQueue + xLinkTxQueues[] |
+------------------------------------------------------+
| Data Tasks |
| UartRxTask + TcpSrvTask_S1/S2 + TcpCliTask_C1/C2 |
+------------------------------------------------------+
| Network Runtime |
| NetPollTask + tcpip_thread + ethernetif + CH390 |
+------------------------------------------------------+
| HAL / DMA / IRQ |
| USART1/2/3 DMA+IDLE, SPI1, EXTI0, TIM4 timebase |
+------------------------------------------------------+
```
这套架构的核心思想是:TCP 任务和 UART 任务不直接互相调用,而是通过 `route_msg_t` 和 FreeRTOS 队列传递数据。这样可以把“从哪里来”“发到哪里去”“数据多长”统一表示,便于普通透传和 MUX 模式共用同一套路由机制。
## 4. 启动流程
启动入口在 `Core/Src/main.c``main()`
1. `HAL_Init()` 初始化 HAL、Flash 接口和基础 tick。
2. `debug_log_init()` 初始化 RTT 日志,并输出 `hal-init`
3. `SystemClock_Config()` 配置系统时钟。
4. 初始化外设:`MX_GPIO_Init()``MX_DMA_Init()``MX_USART1_UART_Init()`
5. `config_init()` 从 Flash 读取配置;读取失败则 `config_set_defaults()`
6. `ApplyConfiguredUartBaudrates()` 根据配置设置 `USART2/USART3` 波特率。
7. 初始化 `USART2``USART3``SPI1`
8. 初始化 LED 并执行 `CH390_HardwareReset()`
9. `osKernelInitialize()` 初始化 RTOS 内核。
10. `MX_FREERTOS_Init()` 创建队列、信号量和基础任务。
11. `osKernelStart()` 启动调度器。
调试启动问题时,优先按 RTT boot 日志确认流程卡在哪个里程碑。
## 5. FreeRTOS 对象和任务
`Core/Src/freertos.c` 是理解运行时结构的关键文件。
### 5.1 全局对象
| 对象 | 类型 | 用途 |
|------|------|------|
| `xNetSemaphore` | Binary Semaphore | `EXTI0` 通知网络轮询任务处理 CH390 事件 |
| `xTcpRxQueue` | Queue | TCP 任务收到网络数据后投递给 `UartRxTask` |
| `xConfigQueue` | Queue | `USART1` AT 命令或 MUX 控制帧投递给 `ConfigTask` |
| `xLinkTxQueues[4]` | Queue | UART 收到的数据投递给指定 S1/S2/C1/C2 TCP 任务 |
| `route_msg` pool | 固定池 | 避免每包动态分配,最大载荷见 `ROUTE_MSG_MAX_PAYLOAD` |
### 5.2 基础任务
`MX_FREERTOS_Init()` 固定创建:
| 任务 | 入口 | 职责 |
|------|------|------|
| `defaultTask` | `StartDefaultTask()` | LED 心跳、看门狗 |
| `NetPoll` | `NetPollTask()` | 初始化 lwIP/netif,轮询或响应 CH390 中断,网络 ready 后启动 TCP 任务 |
| `UartRx` | `UartRxTask()` | 处理 USART2/3 RX,执行普通透传或 MUX 路由 |
| `Config` | `ConfigTask()` | 处理 AT 命令并回复 |
`DIAG_TASK_ISOLATION` 打开时,`UartRx``Config` 会被隔离,这只用于调试,不代表正常产品形态。
### 5.3 网络任务
网络任务不是在 `MX_FREERTOS_Init()` 里立即全部创建,而是在 `NetPollTask()` 完成 `tcpip_init()``lwip_netif_init()` 并设置 `g_netif_ready = pdTRUE` 后,由 `app_start_network_tasks()` 按配置创建:
| 任务 | 条件 | 职责 |
|------|------|------|
| `TcpSrvS1` | `LINK[S1].enabled` | 监听 S1 本地端口,收发 Server 数据 |
| `TcpSrvS2` | `LINK[S2].enabled` | 监听 S2 本地端口,收发 Server 数据 |
| `TcpCliC1` | `LINK[C1].enabled` | 主动连接 C1 远端,断线重连 |
| `TcpCliC2` | `LINK[C2].enabled` | 主动连接 C2 远端,断线重连 |
这种延迟创建能避免 TCP 任务在 netif 尚未就绪时进入 `netconn_*` 路径。
## 6. 配置模型
配置结构定义在 `App/config.h``device_config_t`
### 6.1 端点编码
| 端点 | 代码宏 | 编码 |
|------|--------|------|
| `C1` | `ENDPOINT_C1` | `0x01` |
| `C2` | `ENDPOINT_C2` | `0x02` |
| `UART2` / `U0` | `ENDPOINT_UART2` | `0x04` |
| `UART3` / `U1` | `ENDPOINT_UART3` | `0x08` |
| `S1` | `ENDPOINT_S1` | `0x10` |
| `S2` | `ENDPOINT_S2` | `0x20` |
`SRCID` 是单一源端点;`DSTMASK` 是目标端点位图。`DSTMASK=0x00` 专用于系统控制帧,最终进入 AT 解析路径。
### 6.2 LINK 模型
4 条链路固定为:
| 角色 | 内部索引 | 默认作用 |
|------|----------|----------|
| `S1` | `CONFIG_LINK_S1` | TCP Server 1 |
| `S2` | `CONFIG_LINK_S2` | TCP Server 2 |
| `C1` | `CONFIG_LINK_C1` | TCP Client 1 |
| `C2` | `CONFIG_LINK_C2` | TCP Client 2 |
每条 `LINK` 包含:
```text
EN,LPORT,RIP,RPORT,UART
```
`UART``U0``U1`,分别对应 `USART2``USART3`
### 6.3 默认配置
当前默认值由 `config_set_defaults()` 写入:
- `MUX=0`,即普通透传模式。
- `NET=192.168.31.100,255.255.255.0,192.168.31.1,00:00:00:00:00:00`
- `UART2/USART2``UART3/USART3` 默认波特率为 `115200`
- `reconnect_interval_ms=3000`
完整 AT 命令格式以 `AT固件使用手册.md` 为准。
## 7. AT 配置数据流
AT 配置口固定使用 `USART1`
```text
上位机 AT 文本
-> USART1 DMA 接收
-> USART1 IDLE 中断
-> config_uart_idle_handler()
-> route_send_from_isr(xConfigQueue, ROUTE_CONN_UART1, ...)
-> ConfigTask
-> config_process_at_cmd()
-> config_respond_to_uart()
-> USART1 DMA 发送响应
```
`config_process_at_cmd()` 当前支持:
- `AT`
- `AT+?` / `AT+QUERY`
- `AT+SAVE`
- `AT+RESET`
- `AT+DEFAULT`
- `AT+MUX?` / `AT+MUX=0|1`
- `AT+NET?` / `AT+NET=IP,MASK,GW,MAC`
- `AT+LINK?` / `AT+LINK=ROLE,...` / `AT+LINK=ROLE`
- `AT+BAUD?` / `AT+BAUD=U0|U1,baudrate`
修改网络、链路、MUX 或波特率后,代码会返回 `AT_NEED_REBOOT``ConfigTask` 会追加提示:`Use AT+SAVE then AT+RESET to apply changes`
## 8. 业务数据流示例:无协议透传
本节用一个最常见场景说明数据如何流动。
### 8.1 场景配置
假设现场需要“电脑 TCP 客户端连到设备,数据直接从 `USART2` 输出;`USART2` 收到的数据再原样回到 TCP 客户端”。可以配置:
```text
AT+MUX=0
AT+LINK=S1,1,8080,0.0.0.0,0,U0
AT+SAVE
AT+RESET
```
含义:
- 普通透传模式,不使用 MUX 帧。
- 启用 `S1` TCP Server。
- `S1` 监听本地 `8080` 端口。
- `S1` 绑定 `U0`,也就是 `USART2`
### 8.2 TCP 到 UART
当远端 TCP 客户端连接 `S1:8080` 并发送字节,例如 `01 02 03`
```text
远端 TCP 客户端
-> CH390D 收包
-> ethernetif / lwIP
-> TcpSrvTask_S1
-> tcp_server_worker()
-> netconn_recv()
-> route_send(xTcpRxQueue, src=S1, dst=UART2, data=01 02 03)
-> UartRxTask
-> uart_trans_send_buffer(UART_CHANNEL_U0, data)
-> USART2 DMA TX
-> 外部串口设备收到 01 02 03
```
关键代码位置:
- `App/tcp_server.c``tcp_server_worker()``netconn_recv()``netbuf`,再投递到 `xTcpRxQueue`
- `App/uart_trans.c``UartRxTask()``xTcpRxQueue` 取消息,按 `dst_mask` 发送到 `USART2/USART3`
### 8.3 UART 到 TCP
当外部串口设备向 `USART2` 发送 `A1 B2 C3`
```text
外部串口设备
-> USART2 DMA RX
-> USART2 IDLE 中断
-> uart_trans_notify_rx_from_isr(UART_CHANNEL_U0)
-> UartRxTask
-> uart_route_raw_channel(U0)
-> 查找所有 enabled 且绑定 U0 的 LINK
-> route_send(xLinkTxQueues[S1], src=UART2, dst=S1, data=A1 B2 C3)
-> TcpSrvTask_S1
-> xQueueReceive(xLinkTxQueues[S1])
-> netconn_write()
-> lwIP / CH390D
-> 远端 TCP 客户端收到 A1 B2 C3
```
普通透传模式下,`uart_route_raw_channel()` 会把一个 UART 上收到的数据复制给所有“已启用且绑定该 UART”的链路。因此如果 `S1``C2` 都绑定 `U0` 且都启用,`USART2` 的一段输入会分别投递到 `xLinkTxQueues[S1]``xLinkTxQueues[C2]`
## 9. MUX 模式数据流
MUX 模式由 `AT+MUX=1` 开启。帧格式在 `App/uart_trans.c` 中由 `uart_mux_try_extract_frame()``uart_mux_encode_frame()` 实现:
```text
SYNC | LEN_H | LEN_L | SRCID | DSTMASK | PAYLOAD | TAIL
0x7E | len high | len low | source | destinations | bytes | 0x7F
```
处理规则:
1. `DSTMASK=0x00`:系统控制帧,`PAYLOAD` 作为 AT 文本进入 `xConfigQueue`
2. `DSTMASK` 包含 `S1/S2/C1/C2`:投递到对应 `xLinkTxQueues[]`
3. `DSTMASK` 包含另一个 UART:编码成新的 MUX 帧并转发到另一个数据口。
4. TCP 到 UART 时,如果目标是 UART 且当前处于 MUX 模式,会带上源端点并编码成 MUX 帧输出。
MUX 模式适合多个 TCP 实例共享一个数据口,或者上位机需要明确指定数据发往哪个逻辑端点的场景。
## 10. 网络初始化和 CH390 路径
网络运行入口是 `App/task_net_poll.c``NetPollTask()`
1. 调用 `tcpip_init(NULL, NULL)` 创建 lwIP 内核线程。
2.`config_get()` 中的 `NET` 参数构造 `ipaddr/netmask/gateway`
3. 调用 `lwip_netif_init()` 初始化 netif 和 CH390 glue。
4. 初始化成功后设置 `g_netif_ready = pdTRUE`
5. 调用 `app_start_network_tasks()` 创建启用的 TCP Server/Client 任务。
6. 主循环中等待 `xNetSemaphore` 或周期轮询,驱动 `ethernetif_poll()`,并响应网络重启请求。
底层路径主要在:
- `Drivers/CH390/CH390_Interface.c`:SPI、CS、寄存器和 FIFO 访问。
- `Drivers/CH390/CH390.c`:芯片级 helper。
- `Drivers/LwIP/src/netif/ethernetif.c`CH390 与 lwIP netif 的桥接。
- `Drivers/LwIP/src/include/arch/lwipopts.h`lwIP 池、线程、core locking 配置。
当前关键 lwIP 配置包括:
- `NO_SYS=0`
- `LWIP_NETCONN=1`
- `LWIP_TCPIP_CORE_LOCKING=1`
- `LWIP_TCPIP_CORE_LOCKING_INPUT=1`
- `PBUF_POOL_SIZE=8`
- `MEMP_NUM_PBUF=8`
- `MEMP_NUM_TCPIP_MSG_INPKT=8`
- `LWIP_NETCONN_SEM_PER_THREAD=1`
## 11. 推荐阅读路线
### 11.1 只想理解系统怎么跑
1. `README.md`
2. 本文第 3、4、5、8 节
3. `Core/Src/main.c`
4. `Core/Src/freertos.c`
### 11.2 要改 AT 命令或默认参数
1. `AT固件使用手册.md`
2. `App/config.h`:结构体、默认值、端点编码。
3. `App/config.c`:解析、保存、响应。
4. `App/flash_param.c`Flash 存储。
### 11.3 要改 TCP 或串口透传
1. 本文第 8、9 节。
2. `App/route_msg.c`:先理解消息生命周期。
3. `App/uart_trans.c`UART RX/TX、普通透传、MUX。
4. `App/tcp_server.c``App/tcp_client.c`:网络收发。
### 11.4 要查网络底层问题
1. `App/task_net_poll.c`
2. `Drivers/LwIP/src/netif/ethernetif.c`
3. `Drivers/CH390/CH390_Interface.c`
4. `Drivers/LwIP/src/include/arch/lwipopts.h`
5. RTT 日志和抓包结果一起看,不要只看单侧现象。
## 12. 调试指南
### 12.1 启动阶段
先看 RTT boot 日志:
```text
hal-init
clock-config
peripherals-ready
config-ready
uart-trans-init
tasks-created
freertos-init
scheduler-start
```
如果停在 `scheduler-start` 前,优先看外设初始化、CH390 reset、RTOS 对象创建断言。如果进入任务后异常,再看 `NetPollTask``ConfigTask``UartRxTask` 的 task-entry 日志。
### 12.2 网络阶段
关键日志点:
- `[NET] tcpip-init enter/exit`
- `[NET] netif-init enter/exit`
- `[NET] post-init ok=... hwm=... free=... min=...`
- `[NET] start-network-tasks call`
- `[NET] netif-ready`
如果 netif 未 ready,不要先查 TCP 业务任务;应先查 CH390、SPI、netif 初始化和 lwIP 配置。
### 12.3 路由阶段
`route_send_result_to_str()` 的返回值很重要:
| 返回 | 含义 | 常见方向 |
|------|------|----------|
| `invalid` | 参数或长度非法 | 检查 `dst_mask`、payload 长度 |
| `pool` | `route_msg` 固定池耗尽 | 检查消费者任务是否卡住、队列是否积压 |
| `queue` | 目标队列满 | 检查对应 TCP/UART 任务是否还在运行 |
### 12.4 资源约束
`STM32F103RCT6` 的 SRAM 余量有限,而 FreeRTOS、lwIP、多个 TCP 任务、UART ring buffer 和消息池都会消耗 RAM。遇到 full-task 模式下的非确定性问题时,应同时记录:
- `xPortGetFreeHeapSize()`
- `xPortGetMinimumEverFreeHeapSize()`
- `uxTaskGetStackHighWaterMark()`
- lwIP pbuf/memp 池配置
- 哪些 `LINK` 被启用
如果问题随任务数量、池大小或启用链路数量明显移动,先按资源问题分析,不要急着给业务路径加补丁。
### 12.5 历史 CH390/lwIP pbuf 泄漏教训
历史上曾出现“设备能成功 ping 固定次数,随后不再回应”的问题。现象随 `PBUF_POOL_SIZE` 从 8 改到 16 而从 8 次移动到 16 次,最终定位到 lwIP 输出路径中 pbuf 引用计数未释放这一类问题。
这个案例的长期价值不是记住某个临时修改,而是调试方法:
1. 如果失败次数精确跟池大小相关,优先怀疑引用计数或释放路径。
2. 扩大池只能延迟问题,不能当根修复。
3. 抓包、RTT、lwIP 统计和代码引用计数要一起看。
## 13. 修改代码时的边界
1. 不要绕过 `route_msg` 和队列直接让 TCP/UART 任务互相调用。
2. ISR 中只做通知或投递,不做阻塞等待。
3. `netconn_*` 只在网络任务语境中使用,注意每个线程的 `netconn_thread_init()` / `netconn_thread_cleanup()`
4. 改 AT 命令时同步更新 `AT固件使用手册.md`
5. 改 MUX 帧格式或端点编码时,同步检查 `App/config.h``App/uart_trans.c``AT固件使用手册.md`
6. 改 lwIP 池大小时,同步记录 RAM、heap、水位和实际业务链路数量。
## 14. 术语速查
| 术语 | 含义 |
|------|------|
| `U0` | `USART2` 数据口 |
| `U1` | `USART3` 数据口 |
| `S1/S2` | TCP Server 实例 |
| `C1/C2` | TCP Client 实例 |
| `MUX=0` | 普通透明透传 |
| `MUX=1` | 带 `SRCID/DSTMASK` 的帧化透传 |
| `xTcpRxQueue` | TCP 到 UART 的队列 |
| `xLinkTxQueues[]` | UART 到 TCP 的队列数组 |
| `xConfigQueue` | AT 命令队列 |
| `NetPollTask` | 网络初始化、轮询和恢复任务 |