450 lines
16 KiB
Markdown
450 lines
16 KiB
Markdown
# 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` | 网络初始化、轮询和恢复任务 |
|