Files
TCP2UART/项目代码阅读指南.md

450 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TCP2UART 项目代码阅读指南
## 1. 文档目的
本文面向接手 `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` | 网络初始化、轮询和恢复任务 |