# 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 Server,2 路 TCP Client,2 路 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` | 网络初始化、轮询和恢复任务 |