4 Commits

13 changed files with 595 additions and 1843 deletions
+23 -13
View File
@@ -2,9 +2,9 @@
## 1. 文档范围
本文档定义 `TCP2UART` 项目的最终 AT 外部协议。
本文档定义 `TCP2UART` 项目的 AT 外部协议。
本文档只描述最终协议模型,不保留任何历史展开式实例字段,不包含测试记录,不讨论旧版兼容命令。
本文档只描述当前代码实现支持的外部命令,不保留历史展开式实例字段,不包含测试记录,不讨论旧版兼容命令。项目结构、任务模型和数据流请阅读 `README.md``项目代码阅读指南.md`
适用对象:
@@ -19,6 +19,13 @@
- 配置口:`USART1`
- 数据口:`USART2``USART3`
### 2.1 固件版本线
- FreeRTOS + lwIP 版本线从 `V2.0.0` 开始。
- 裸机版本线从 `V1.0.0` 开始。
- 当前 FreeRTOS 固件基线 release`TCP2UART RTOS V2.0.0`
- 固件下载:`https://git.furtherverse.com/gaoro-xiao/TCP2UART/releases/tag/V2.0.0`
职责划分:
- `USART1`AT 配置口
@@ -49,12 +56,12 @@ SYNC | LEN_H | LEN_L | SRCID | DSTMASK | PAYLOAD | TAIL
字段定义:
- `SYNC`:帧起始标记,建议固定为 `0x7E`
- `SYNC`:帧起始标记,固定为 `0x7E`
- `LEN_H / LEN_L``PAYLOAD` 长度,高字节在前
- `SRCID`:单字节源端点 ID
- `DSTMASK`:单字节目标端点位图
- `PAYLOAD`:负载数据
- `TAIL`:帧结束标记,建议固定为 `0x7F`
- `TAIL`:帧结束标记,固定为 `0x7F`
规则:
@@ -92,7 +99,7 @@ SYNC | LEN_H | LEN_L | SRCID | DSTMASK | PAYLOAD | TAIL
```text
AT\r\n
AT+MUX?\r\n
AT+NET=192.168.1.100,255.255.255.0,192.168.1.1,02:00:00:00:00:01\r\n
AT+NET=192.168.31.100,255.255.255.0,192.168.31.1,00:00:00:00:00:00\r\n
```
### 6.2 持久化规则
@@ -121,9 +128,11 @@ MUX = 0
### 7.2 NET 默认值
```text
NET = 192.168.1.100,255.255.255.0,192.168.1.1,02:00:00:00:00:01
NET = 192.168.31.100,255.255.255.0,192.168.31.1,00:00:00:00:00:00
```
默认 MAC 为全 0,表示 Flash 中不固化板卡 MAC;运行时使用 `CH390D` 内部 MAC。`AT+?``AT+NET?` 回显的是当前生效 MAC。
### 7.3 LINK 默认值
```text
@@ -171,7 +180,7 @@ AT+QUERY\r\n
推荐返回格式:
```text
+NET:IP=192.168.1.100,MASK=255.255.255.0,GW=192.168.1.1,MAC=02:00:00:00:00:01
+NET:IP=192.168.31.100,MASK=255.255.255.0,GW=192.168.31.1,MAC=<当前生效MAC>
+LINK:S1,EN=1,LPORT=8080,RIP=0.0.0.0,RPORT=0,UART=U0
+LINK:S2,EN=0,LPORT=8081,RIP=0.0.0.0,RPORT=0,UART=U1
+LINK:C1,EN=1,LPORT=9001,RIP=192.168.1.200,RPORT=9000,UART=U1
@@ -213,7 +222,7 @@ OK
#### 设置 NET
```text
AT+NET=192.168.1.100,255.255.255.0,192.168.1.1,02:00:00:00:00:01\r\n
AT+NET=192.168.31.100,255.255.255.0,192.168.31.1,00:00:00:00:00:00\r\n
```
字段顺序:
@@ -231,13 +240,13 @@ AT+NET?\r\n
返回示例:
```text
+NET:IP=192.168.1.100,MASK=255.255.255.0,GW=192.168.1.1,MAC=02:00:00:00:00:01
+NET:IP=192.168.31.100,MASK=255.255.255.0,GW=192.168.31.1,MAC=<当前生效MAC>
OK
```
**MAC 设置说明:**
当MAC设置为全0时,固件将使用硬件MAC地址此时通过AT+?查询到的MAC地址为当前生效的硬件MAC地址。
MAC 设置为全 0 时,固件将使用 `CH390D` 内部 MAC 地址此时 Flash 内仍保存全 0,不会把内部 MAC 写回 Flash;`AT+?``AT+NET?` 查询到的 MAC 地址为当前运行时生效的硬件 MAC 地址。
### 8.5 LINK 类命令
@@ -357,7 +366,7 @@ OK: Defaults restored
## 11. 推荐配置流程
```text
AT+NET=192.168.1.123,255.255.255.0,192.168.1.1,02:00:00:00:00:01\r\n
AT+NET=192.168.31.123,255.255.255.0,192.168.31.1,00:00:00:00:00:00\r\n
AT+LINK=S1,1,10001,0.0.0.0,0,U1\r\n
AT+LINK=S2,1,10003,0.0.0.0,0,U1\r\n
AT+LINK=C1,1,20001,192.168.1.201,10002,U0\r\n
@@ -385,9 +394,10 @@ AT+RESET\r\n
1. `AT+SAVE\r\n`
2. `AT+RESET\r\n`
## 13. 相关文件
## 13. 相关文档与文
- 项目入口:`README.md`
- 代码结构和数据流:`项目代码阅读指南.md`
- AT 命令实现:`App/config.c`
- 配置结构与默认值:`App/config.h`
- FreeRTOS 任务定义:`Core/Src/freertos.c`
- 调试指导:`工程调试指南.md`
+2 -1
View File
@@ -15,6 +15,7 @@
#include "route_msg.h"
#define TCP_CLIENT_CONNECT_TIMEOUT_MS 500
#define TCP_CLIENT_RECONNECT_INTERVAL_MS 3000u
#define TCP_CLIENT_STOP_POLL_MS 50u
static BaseType_t tcp_client_stop_requested(void)
@@ -162,7 +163,7 @@ static void tcp_client_task(uint8_t link_index)
continue;
}
delay_ms = (cfg->reconnect_interval_ms == 0u) ? 3000u : cfg->reconnect_interval_ms;
delay_ms = TCP_CLIENT_RECONNECT_INTERVAL_MS;
if (first_connect_deferred != 0u) {
first_connect_deferred = 0u;
-245
View File
@@ -1,245 +0,0 @@
# CH390 / lwIP 固定次数 ping 失败问题修复复盘
## 1. 问题现象
在 TCP2UART 固件运行后,设备初期可以正常 ARP 和 ping,但连续 ping 一段时间后不再响应。
典型现象:
- 设备 IP`192.168.31.100`
- 设备 MAC`02:00:00:00:00:01`
- 对端/网关 IP`192.168.31.1`
- 对端/网关 MAC`00:e0:4c:28:1e:60`
- 失败后设备仍持续发送 TCP SYN/RST 或 client timeout 相关流量,说明 TX、任务调度和应用层并未整体死机。
- 失败后对端继续向设备 MAC 发送 ICMP/ARP,但设备不再回复。
关键抓包:
- `WiresharkLog/04290150.pcapng`
- `seq=1884..1891` 共 8 次 ping reply 正常。
- 第 9 次 `seq=1892` 开始无 reply。
- `WiresharkLog/04290206.pcapng`
- 曾把 `PBUF_POOL_SIZE` / `MEMP_NUM_TCPIP_MSG_INPKT` 从 8 临时扩大到 16。
- 成功 ping 从 8 次变为 `seq=1900..1915` 共 16 次。
- 第 17 次 `seq=1916` 开始无 reply。
这个“成功次数随池大小移动”的现象证明:问题不是 CH390 随机丢包,也不是 PHY/TX 死掉,而是每次成功处理 ping 后都有某个 pbuf 引用没有释放,最终耗尽 `PBUF_POOL`
## 2. 排查过程中的重要结论
### 2.1 CH390 RX 读包路径曾存在风险,但不是最终根因
早期排查时发现 CH390 RX 路径与参考驱动存在若干不一致,已修正:
- `ch390_receive_packet()` 按参考序列读取 RX ready:先读 `MRCMDX` dummy,再读 `MRCMDX1`
- 校验 RX header 的 `Head` 字节必须为 `CH390_PKT_RDY`
- CH390 RX SRAM 中的 `rx_len` 包含 Ethernet FCS,交给 lwIP 前需要减去 4 字节。
- `ch390_rx_reset()` 显式写 `MPTRCR_RST_RX` 复位 RX memory pointer。
这些修正确保 CH390 RX FIFO 读包更接近参考实现,但无法解释“固定 8 次/16 次后失败”。
### 2.2 扩大 lwIP 池只能延迟问题
曾临时将如下配置从 8 提到 16
```c
#define PBUF_POOL_SIZE 16
#define MEMP_NUM_PBUF 16
#define MEMP_NUM_TCPIP_MSG_INPKT 16
```
结果成功 ping 次数也从 8 变成 16。这说明扩大池子只是延迟耗尽,不能作为根修复。
最终已恢复为 8
```c
#define PBUF_POOL_SIZE 8
#define MEMP_NUM_PBUF 8
#define MEMP_NUM_TCPIP_MSG_INPKT 8
```
### 2.3 `tcpip_input()` 异步队列不是最终根因
项目启用了 lwIP core locking。为避免每个 RX 包占用 `MEMP_TCPIP_MSG_INPKT`,配置已改为同步输入:
```c
#define LWIP_TCPIP_CORE_LOCKING 1
#define LWIP_TCPIP_CORE_LOCKING_INPUT 1
```
这样 `tcpip_input()` 会在 core lock 下同步调用 `ethernet_input()`,不再通过 `TCPIP_MSG_INPKT` 邮箱异步排队。
但用户后续验证仍然固定 8 次后停止,且每次成功 ping 都已经有 reply,因此说明 RX 包确实已经进入 ICMP 处理路径,问题更可能是 reply 输出路径增加了 pbuf 引用但未释放。
## 3. 最终根因
最终根因位于:
```text
Drivers/LwIP/src/netif/ethernet.c
```
`ethernet_output()` 实现:
```c
q = pbuf_alloc(PBUF_RAW_TX, SIZEOF_ETH_HDR, PBUF_RAM);
if (q == NULL) {
LINK_STATS_INC(link.memerr);
LINK_STATS_INC(link.drop);
return ERR_MEM;
}
pbuf_chain(q, p);
ethhdr = (struct eth_hdr *)q->payload;
SMEMCPY(&ethhdr->dest, dst, sizeof(struct eth_addr));
SMEMCPY(&ethhdr->src, src, sizeof(struct eth_addr));
ethhdr->type = lwip_htons(eth_type);
return netif->linkoutput(netif, q);
```
问题在 `pbuf_chain(q, p)`
lwIP 的 `pbuf_chain()` 会执行:
```c
pbuf_ref(t);
```
也就是给被挂接的原始 pbuf `p` 引用计数加 1。
ICMP echo reply 路径会复用 RX pbuf
```text
ethernetif_poll()
-> tcpip_input()
-> ethernet_input()
-> ip4_input()
-> icmp_input()
-> ip4_output_if()
-> etharp_output()
-> ethernet_output()
```
`icmp_input()` 末尾本身会 `pbuf_free(p)`,这部分是正确的。但在原实现中,`ethernet_output()` 通过 `pbuf_chain(q, p)``p` 额外加了一次引用,却没有在 `linkoutput()` 返回后释放临时 header pbuf `q`
因此每次 ping 的引用计数变化是:
```text
RX pbuf 初始 ref = 1
pbuf_chain(q, p) 后 ref = 2
icmp_input() 末尾 pbuf_free(p) 后 ref = 1
=> p 永远没有回到 0PBUF_POOL 泄漏 1 个
```
所以:
- `PBUF_POOL_SIZE=8` 时,8 次 ping reply 后耗尽。
- 临时扩大到 16 时,16 次 ping reply 后耗尽。
## 4. 修复方案
修复 `ethernet_output()`,在同步 `linkoutput()` 完成后释放临时 header pbuf 链:
```c
err_t ethernet_output(struct netif *netif,
struct pbuf *p,
const struct eth_addr *src,
const struct eth_addr *dst,
u16_t eth_type)
{
struct pbuf *q;
struct eth_hdr *ethhdr;
err_t err;
LWIP_ASSERT("netif != NULL", netif != NULL);
LWIP_ASSERT("p != NULL", p != NULL);
LWIP_ASSERT("src != NULL", src != NULL);
LWIP_ASSERT("dst != NULL", dst != NULL);
q = pbuf_alloc(PBUF_RAW_TX, SIZEOF_ETH_HDR, PBUF_RAM);
if (q == NULL) {
LINK_STATS_INC(link.memerr);
LINK_STATS_INC(link.drop);
return ERR_MEM;
}
pbuf_chain(q, p);
ethhdr = (struct eth_hdr *)q->payload;
SMEMCPY(&ethhdr->dest, dst, sizeof(struct eth_addr));
SMEMCPY(&ethhdr->src, src, sizeof(struct eth_addr));
ethhdr->type = lwip_htons(eth_type);
err = netif->linkoutput(netif, q);
pbuf_free(q);
return err;
}
```
为什么这里可以释放 `q`
- 本项目 `low_level_output()` 是同步发送。
- 它会立即遍历 pbuf 链,把数据复制到 `s_tx_buffer`
- 随后调用 `ch390_runtime_send_packet()` 把连续 buffer 发给 CH390。
- `low_level_output()` 返回后不再持有 pbuf 指针。
因此 `ethernet_output()``linkoutput()` 返回后释放 `q` 是正确的。
`pbuf_free(q)` 会同时:
- 释放临时 Ethernet header pbuf `q`
- 解除 `pbuf_chain()` 对原始 RX pbuf `p` 增加的引用;
- 之后 `icmp_input()` 末尾的 `pbuf_free(p)` 可以真正把 RX pbuf 归还 `PBUF_POOL`
## 5. 不要做的错误修复
### 5.1 不要在 `netif->input()` 成功后手动释放 pbuf
驱动层当前逻辑是正确的:
```c
input_err = ch390_netif.input(p, &ch390_netif);
if (input_err != ERR_OK) {
pbuf_free(p);
}
```
`netif->input()` 返回 `ERR_OK` 时,pbuf ownership 已经交给 lwIP。此时驱动不能再 `pbuf_free(p)`,否则会造成 double-free 或 use-after-free。
### 5.2 不要只扩大 `PBUF_POOL_SIZE`
扩大池子只会让失败次数从 8 变 16、32……不会修复泄漏。
### 5.3 不要继续优先怀疑 CH390 PHY/TX
抓包中失败后设备仍持续发送 TCP SYN/RST,说明 TX 和任务仍活着。固定次数失败更符合 pbuf 引用泄漏。
## 6. 验证结果
修复后 Keil 构建通过:
```text
"TCP2UART\TCP2UART.axf" - 0 Error(s), 0 Warning(s).
Program Size: Code=93376 RO-data=2768 RW-data=456 ZI-data=56032
```
用户烧录验证后确认问题已修复。
## 7. 后续排查建议
如后续再次出现固定次数网络停止,优先检查:
1. 是否存在 `pbuf_chain()` / `pbuf_ref()` 后没有配对 `pbuf_free()` 的路径。
2. 是否有 ARP pending queue 长时间持有 pbuf。
3. 是否有 TCP `recvmbox` / 应用桥接队列背压长期持有 pbuf。
4. 是否有人在 `netif->input()` 成功后错误释放 pbuf,导致内存破坏。
推荐排查点:
- `Drivers/LwIP/src/netif/ethernet.c`
- `Drivers/LwIP/src/core/ipv4/icmp.c`
- `Drivers/LwIP/src/core/ipv4/etharp.c`
- `Drivers/LwIP/src/core/pbuf.c`
- `Drivers/LwIP/src/netif/ethernetif.c`
-66
View File
@@ -1,66 +0,0 @@
# TCP2UART 当前交接 Prompt
## 1. 用途
本文件不再承担“项目从零编码任务说明”的职责,而是作为**当前工程的交接入口 Prompt**使用。
长期有效的工程知识、调试经验和现状说明,已经分别固化到其它文档,不再全部堆在本文件中。
---
## 2. 接手后先读什么
请按以下顺序阅读:
1. `交接清单.md` —— 当前状态、接下来要做什么、怎么做
2. `工程调试指南.md` —— 已固化的调试经验与当前工程真实边界
3. `项目技术实现.md` —— 架构、任务模型、协议模型
4. `项目需求说明.md`
5. `AT固件使用手册.md`
---
## 3. 当前工程一句话状态
当前项目已从早期 bring-up 阶段推进到 full-task 运行期调试阶段;`DIAG_TASK_ISOLATION=1` 稳定,`DIAG_TASK_ISOLATION=0` 仍会卡死,但故障边界已被多轮 discriminator 推进到 enabled 的 `netconn_*` 路径。当前在 `STM32F103RCT6` 上的 RAM/heap 余量过低,已被认定为调试噪声的主要来源之一,因此推荐下一阶段先切到 pin2pin 的 `STM32F103RDT6` 再继续分析。
---
## 4. 下一位 agent 的当前目标
请不要把当前任务理解成“立刻继续加逻辑修补”。当前更重要的是:
1. 完成 `STM32F103RCT6 -> STM32F103RDT6` 目标切换
2. 使用真实 Keil 日志重新确认构建成功
3. 在新器件上复测当前代码基线
4. 比较:
- 故障是否消失
- 是否明显后移
- 是否仍停在相同 enabled path
5. 只有拿到新器件上的第一轮 RTT / heap / HWM 证据后,再决定下一步最小化改动
---
## 5. 工作约束
1. 构建真值以 `MDK-ARM/build_capture.txt``TCP2UART.build_log.htm``.map` 为准
2. 不要再把 viewer 当作当前构建真值
3. 不要忽视 `DIAG_TASK_ISOLATION=1 正常、=0 异常` 这个前提
4. 在新器件的第一轮复测前,避免继续做大范围代码改动
5. 每次只做一个能明显改变故障边界的最小改动,并保留 RTT 证据
---
## 6. 可直接复制给下一位 agent 的起始 Prompt
```text
请先阅读:`交接清单.md`、`工程调试指南.md`、`项目技术实现.md`。
当前项目是 STM32F103 + FreeRTOS + lwIP + CH390 的 TCP↔UART 透传工程。此前在 `STM32F103RCT6` 上调试时,`DIAG_TASK_ISOLATION=1` 稳定,`DIAG_TASK_ISOLATION=0` 仍会卡死,但故障边界已被多轮 discriminator 推进到 enabled 的 `netconn_*` 路径。当前最关键的资源事实是:在 `RCT6` 上 full-task 创建完四个 TCP 任务后,FreeRTOS heap 只剩约 944 bytes,静态 RAM 也已逼近物理上限,因此当前推荐先切换到 pin2pin 的 `STM32F103RDT6`,保持现有代码基线基本不变,先完成第一轮换片复测,再根据新器件上的 RTT、free/min heap 和 enabled `S1/C1` 行为决定下一步。
你的当前目标不是立刻修完所有问题,而是:
1. 完成 `RCT6 -> RDT6` 目标切换;
2. 用真实 Keil 日志确认构建通过;
3. 在新器件上复测当前代码,判断故障是否消失、后移或保持原状;
4. 仅在拿到新器件上的第一轮 RTT 后,再继续做最小化的下一步判别。
```
+8 -2
View File
@@ -76,9 +76,15 @@ void MX_GPIO_Init(void)
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* EXTI interrupt init*/
/* EXTI interrupt init
* Keep CH390 INT masked during early boot. PB0 may already be asserted at
* power-on, while the FreeRTOS semaphore is not created until
* MX_FREERTOS_Init(). The network driver enables EXTI0 after CH390 and the
* RTOS objects are ready.
*/
HAL_NVIC_DisableIRQ(EXTI0_IRQn);
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
HAL_NVIC_SetPriority(EXTI0_IRQn, 6, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
+4
View File
@@ -2,6 +2,7 @@
#include "stm32f1xx_it.h"
#include "FreeRTOS.h"
#include "semphr.h"
#include "task.h"
#include "app_runtime.h"
@@ -101,9 +102,12 @@ void EXTI0_IRQHandler(void)
if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET) {
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
if ((xNetSemaphore != NULL) &&
(xTaskGetSchedulerState() == taskSCHEDULER_RUNNING)) {
xSemaphoreGiveFromISR(xNetSemaphore, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
}
void SPI1_IRQHandler(void)
+2
View File
@@ -188,6 +188,8 @@ void ch390_interrupt_init(void)
/* EXTI0 is configured in CubeMX for PB0 */
/* NVIC priority should be >= configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY */
/* for FreeRTOS compatibility */
HAL_NVIC_DisableIRQ(EXTI0_IRQn);
__HAL_GPIO_EXTI_CLEAR_IT(CH390_INT_PIN);
HAL_NVIC_SetPriority(EXTI0_IRQn, 6, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
+105
View File
@@ -0,0 +1,105 @@
# TCP2UART 项目说明
`TCP2UART` 是一个基于 `STM32F103RCT6 + CH390D` 的 TCP 与双串口透传固件。系统运行在 `FreeRTOS` 上,网络协议栈使用 `lwIP NO_SYS=0 + netconn API`,通过 `SEGGER RTT` 输出调试日志,工程入口为 Keil MDK 项目 `MDK-ARM/TCP2UART.uvprojx`
本仓库当前文档已经收敛为两类:
1. 面向开发者的代码阅读与系统理解文档。
2. 面向上位机、测试和联调人员的 AT 命令接口文档。
## 硬件与软件基线
| 项目 | 内容 |
|------|------|
| 主控 | `STM32F103RCT6` |
| 以太网芯片 | `CH390D` |
| 配置串口 | `USART1` |
| 数据串口 | `USART2``USART3` |
| RTOS | `FreeRTOS` |
| TCP/IP | `lwIP``NO_SYS=0`,启用 `netconn` API |
| 调试输出 | `SEGGER RTT` |
| 构建工程 | `MDK-ARM/TCP2UART.uvprojx` |
`USART1` 只承担 AT 配置面。`USART2/USART3` 是业务数据口,可工作在普通透传模式或 MUX 帧模式。网络侧提供 2 路 TCP Server 实例和 2 路 TCP Client 实例,统一使用 `LINK` 配置模型管理。
## 当前文档地图
| 文档 | 读者 | 内容 |
|------|------|------|
| `README.md` | 所有人 | 项目总览、文档入口、快速理解路径 |
| `项目代码阅读指南.md` | 固件开发、调试、交接人员 | 代码目录、启动流程、任务模型、数据流、阅读顺序、调试线索 |
| `AT固件使用手册.md` | 上位机、测试、联调人员 | AT 命令、参数模型、默认值、配置流程、常见错误 |
旧的需求说明、技术实现、调试交接、Prompt 和单点问题复盘文档已经合并到上述文档中。临时调试结论不再作为根目录入口,避免把某一轮现场状态误认为长期项目事实。
## 一句话架构
```text
远端 TCP 连接
|
v
TcpSrvTask_S1/S2 或 TcpCliTask_C1/C2
|
v
route_msg_t + FreeRTOS Queue
|
v
UartRxTask / uart_trans
|
v
USART2 / USART3
```
反向数据流也走同一套路由对象:数据口收到字节后由 `UartRxTask` 判断普通透传或 MUX 帧,再投递到对应 `xLinkTxQueues[]`,最终由 TCP 任务调用 `netconn_write()` 发回网络。
## 快速阅读顺序
首次接手建议按下面顺序阅读:
1. `README.md`:确认硬件、软件、文档入口。
2. `项目代码阅读指南.md` 的“总体架构”和“业务数据流示例”:先建立整体图。
3. `Core/Src/main.c`:理解上电初始化顺序。
4. `Core/Src/freertos.c`:理解队列、信号量和任务创建。
5. `App/config.c``AT固件使用手册.md`:理解配置模型和 AT 命令。
6. `App/tcp_server.c``App/tcp_client.c``App/uart_trans.c``App/route_msg.c`:理解 TCP 与 UART 如何互相转发。
7. `App/task_net_poll.c``Drivers/CH390/*``Drivers/LwIP/src/netif/ethernetif.c`:需要定位网络底层问题时再深入。
## 核心配置模型
固件内部把所有链路抽象为 4 条 `LINK`
| 角色 | 内部索引 | 端点编码 | 含义 |
|------|----------|----------|------|
| `S1` | `CONFIG_LINK_S1` | `0x10` | TCP Server 1 |
| `S2` | `CONFIG_LINK_S2` | `0x20` | TCP Server 2 |
| `C1` | `CONFIG_LINK_C1` | `0x01` | TCP Client 1 |
| `C2` | `CONFIG_LINK_C2` | `0x02` | TCP Client 2 |
| `UART2` / `U0` | `LINK_UART_U0` | `0x04` | 数据串口 0 |
| `UART3` / `U1` | `LINK_UART_U1` | `0x08` | 数据串口 1 |
每条 `LINK` 记录包含:启用状态、本地端口、远端 IP、远端端口、绑定的数据串口。外部配置命令见 `AT固件使用手册.md`
## 常用开发入口
| 目的 | 文件 |
|------|------|
| 上电初始化 | `Core/Src/main.c` |
| 任务和队列创建 | `Core/Src/freertos.c` |
| AT 命令与 Flash 参数 | `App/config.c``App/flash_param.c` |
| 路由消息池 | `App/route_msg.c` |
| UART DMA/IDLE、普通透传、MUX 帧 | `App/uart_trans.c` |
| TCP Server | `App/tcp_server.c` |
| TCP Client | `App/tcp_client.c` |
| 网络初始化和轮询 | `App/task_net_poll.c` |
| CH390 与 lwIP netif | `Drivers/CH390/*``Drivers/LwIP/src/netif/ethernetif.c` |
| RTT 日志封装 | `Core/Src/debug_log.c` |
## 调试提示
1. 启动阶段先看 RTT 中的 `debug_log_boot()` 里程碑,例如 `hal-init``clock-config``peripherals-ready``tasks-created``scheduler-start`
2. 网络阶段重点看 `NetPollTask``tcpip-init``netif-init``netif-ready``post-ready free/min heap` 日志。
3. TCP 或 UART 透传异常时,优先检查 `route_send()` 返回的 `pool``queue``invalid`,它们能区分消息池耗尽、队列满和参数错误。
4. HardFault、MemManage、BusFault、UsageFault 会进入 `Debug_TrapWithRttHint()`,应先保留 RTT 现场再修改代码。
5. `STM32F103RCT6` RAM 余量有限;如果 full-task 模式下问题呈现强资源相关特征,应优先核对堆、栈、水位和 lwIP 池配置,不要只看业务逻辑。
更详细的阅读和调试路线见 `项目代码阅读指南.md`
-208
View File
@@ -1,208 +0,0 @@
# TCP2UART 调试交接清单
## 1. 文档目的
本清单用于把当前 `TCP2UART` 工程的调试状态、已验证结论、后续动作建议一次性交接给下一位 agent 或开发者。
这份文档本身就可以当作下一位 agent 的工作 prompt 使用。
---
## 2. 先读哪些文档
接手本工程后,推荐按以下顺序阅读:
1. `交接清单.md` —— 当前状态、下一步、禁止回退到哪些旧假设
2. `工程调试指南.md` —— 已固化的调试经验与真实工程边界
3. `项目技术实现.md` —— 架构、任务模型、协议模型
4. `项目需求说明.md` —— 用户需求与协议要求
5. `AT固件使用手册.md` —— AT 命令与配置面
---
## 3. 当前工程状态(交接时刻)
### 3.1 当前平台与构建状态
1. 当前 Keil 工程目标仍是 `STM32F103RC`
2. 当前代码可以真实构建通过
3. 当前构建真值应查看:
- `MDK-ARM/build_capture.txt`
- `MDK-ARM/TCP2UART/TCP2UART.build_log.htm`
- `MDK-ARM/TCP2UART/TCP2UART.map`
4. 最近一次 Keil 构建示例:
- `Code=84560`
- `RW-data=432`
- `ZI-data=47056`
- `0 Error(s), 0 Warning(s)`
### 3.2 当前调试结论摘要
1. `DIAG_TASK_ISOLATION=1` 稳定
2. `DIAG_TASK_ISOLATION=0` 仍会卡死
3. 卡死边界已经从更早的启动阶段被推进到更靠后的 enabled `netconn_*` 路径
4.`RCT6` 上,四个 TCP task 创建后 `FreeRTOS heap` 仅剩约 `944 bytes`
5. 这说明当前 `RCT6` 上的资源余量已经严重干扰调试判断
6. 因此当前推荐策略是:**先换到 pin2pin 的 `STM32F103RDT6`,再继续 full-task 调试**
### 3.3 已做过并有信息量的改动/观察
以下工作已经做过,不要在没有新理由的情况下重复一遍:
1. 清理与恢复 `DIAG_TASK_ISOLATION=0`
2. 用真实 Keil 日志替代 viewer 作为构建真值
3. staged creation:将四个 TCP task 延后到 `netif-ready` 后创建
4. `lwIP netif` 初始化、post-init、post-ready 关键 RTT 日志
5. CH390 TX bounded wait / timeout 观察
6. TCP task 栈从 `256` 提高到 `384 words`
7. TCP task 入口 `hwm` 日志
8.`C1` 增加 one-shot first-connect defer discriminator
这些动作都让故障边界后移了,但仍未在 `RCT6` 上把问题彻底消灭。
---
## 4. 当前最可信的判断
当前最可信的判断不是“某一行代码单点必错”,而是:
1. `RCT6` 上的静态 RAM 占用与 FreeRTOS heap 余量都已经接近极限
2. disabled 的 task 可以持续打印,说明调度器与基础日志路径未整体死亡
3. enabled 的 `S1 / C1` 一旦进入真实 `netconn_*` 路径,就更容易触发新的运行期问题
4. 因此如果继续在 `RCT6` 上做更多 discriminator,很容易一直被资源边界噪声带偏
换句话说,**先把“内存极限平台”这个干扰项拿掉,再看逻辑问题还剩多少,是当前性价比最高的路线。**
---
## 5. 下一位 agent 现在应该做什么
### 5.1 第一目标
先完成 `STM32F103RCT6 -> STM32F103RDT6` 的工程切换,然后在**尽量不再改业务逻辑**的前提下复现当前版本。
### 5.2 为什么先这样做
因为下一位 agent 最先需要回答的,不是“立刻怎么修”,而是:
1. 换片后 full-task 模式是否还挂
2. 如果还挂,挂点是否后移
3. 换片后 `free/min heap` 是否显著改善
4. enabled `S1 / C1` 是否能进入更深的 `netconn_*` 路径
---
## 6. 下一位 agent 的推荐工作步骤
### Step 1:切换目标器件到 `STM32F103RDT6`
建议动作:
1. 更新 Keil target 里的 device 选择
2. 对齐 Flash / RAM 容量描述
3. 确认 linker / scatter / startup 相关目标描述与新器件一致
4. 再次真实构建,确认 `0 Error(s), 0 Warning(s)`
### Step 2:保持当前代码基线,直接上板复测
要求:
1. 不要一换片就继续改业务逻辑
2. 使用当前代码基线直接验证
3. 仍以 `build_capture.txt` 和 RTT 为主证据
### Step 3:收集第一轮换片后的关键证据
至少记录:
1. `netif-ready` 后是否仍卡死
2. `free/min heap` 变化
3. enabled `S1 / C1` 的新 RTT 行为
4. `C1 first-connect defer` 是否仍然影响故障边界
5. LED 心跳和 IWDG 表现是否变化
### Step 4:根据换片结果分流
#### 情况 A:换片后明显稳定 / 不再挂
说明:
1. RAM 压力是主要阻碍项之一
2. 后续需要回头做“资源预算收敛”,而不是继续把当前临时参数直接当最终方案
#### 情况 B:换片后仍挂,但更晚 / 更深
说明:
1. 当前逻辑问题仍在
2. 但已经去掉了最主要的资源噪声
3. 这时再围绕 `S1 / C1` 的真实 `netconn_new / bind / connect / listen / accept` 做最小日志/判别,会更有信息量
#### 情况 C:换片后几乎同点位仍挂
说明:
1. 主问题不再是单纯 RAM
2. 应优先检查 enabled 路径的 API 使用、阻塞行为、线程间交互与资源释放
---
## 7. 不要重复的方向
下一位 agent 接手后,除非有新证据,否则不要优先回到以下旧方向:
1. 单纯怀疑 startup/init 早期 bring-up
2. 把 viewer 当构建真值
3. 继续只靠加大 TCP task 栈来解释所有现象
4. 默认把 CH390 TX timeout 当成当前一号嫌疑
5.`RCT6` 上继续大量增加日志、队列、栈或临时 buffer
---
## 8. 关键文件
### 构建/目标
1. `MDK-ARM/TCP2UART.uvprojx`
2. `MDK-ARM/build_capture.txt`
3. `MDK-ARM/TCP2UART/TCP2UART.build_log.htm`
4. `MDK-ARM/TCP2UART/TCP2UART.map`
### 任务与运行期
1. `Core/Inc/FreeRTOSConfig.h`
2. `Core/Src/freertos.c`
3. `App/task_net_poll.c`
4. `App/tcp_server.c`
5. `App/tcp_client.c`
### 网络与驱动
1. `Drivers/LwIP/src/netif/ethernetif.c`
2. `Drivers/CH390/CH390.c`
3. `Drivers/CH390/CH390.h`
### 文档
1. `工程调试指南.md`
2. `项目技术实现.md`
3. `交接清单.md`
4. `CODING_PROMPT.md`
---
## 9. 可直接给下一位 agent 的 Prompt
下面这段文字可以直接作为下一位 agent 的起始 prompt
```text
请先阅读:`交接清单.md`、`工程调试指南.md`、`项目技术实现.md`。
当前项目是 STM32F103 + FreeRTOS + lwIP + CH390 的 TCP↔UART 透传工程。此前在 `STM32F103RCT6` 上调试时,`DIAG_TASK_ISOLATION=1` 稳定,`DIAG_TASK_ISOLATION=0` 仍会卡死,但故障边界已被多轮 discriminator 推进到 enabled 的 `netconn_*` 路径。当前最关键的资源事实是:在 `RCT6` 上 full-task 创建完四个 TCP 任务后,FreeRTOS heap 只剩约 944 bytes,静态 RAM 也已逼近物理上限,因此当前推荐先切换到 pin2pin 的 `STM32F103RDT6`,保持现有代码基线基本不变,先完成第一轮换片复测,再根据新器件上的 RTT、free/min heap 和 enabled `S1/C1` 行为决定下一步。
你的当前目标不是立刻修完所有问题,而是:
1. 完成 `RCT6 -> RDT6` 目标切换;
2. 用真实 Keil 日志确认构建通过;
3. 在新器件上复测当前代码,判断故障是否消失、后移或保持原状;
4. 仅在拿到新器件上的第一轮 RTT 后,再继续做最小化的下一步判别。
```
-469
View File
@@ -1,469 +0,0 @@
# TCP2UART 调试指导
## 1. 适用范围
本指导面向当前 `TCP2UART` 工程,覆盖以下四类调试场景:
1. `STM32F103RCT6 + CH390D` 的基础 bring-up
2. `SEGGER RTT`、异常陷阱与 FreeRTOS 任务运行状态确认
3. `USART1` 配置口、`USART2/USART3` 数据口与 `MUX / NET / LINK[idx]` 协议联调
4. `TCP Server / TCP Client / UART` 三层数据通路联调与问题隔离
本指导默认基线如下:
1. 当前工程采用 `FreeRTOS` 任务调度架构
2. `CH390` 运行时访问由 `xSpiMutex` 保护,`NetworkTask` 持有主要访问权
3. 调试输出统一使用 `SEGGER RTT`
4. 当前应用层协议模型已经收敛到 `MUX / NET / LINK[idx]`
5. 当前代码应以 `MDK-ARM` 工程构建结果为准
---
## 2. 当前工程边界与真实状态
在进入现场调试前,先统一以下工程边界:
1. 当前项目的主要软件路径已经切换为:
- `NET`:网络基础参数
- `LINK[idx]`:链路配置记录
- `MUX`:数据口承载模式
2. 对外 AT 配置面应只围绕以下命令展开:
- `AT` / `AT+?` / `AT+QUERY`
- `AT+MUX` / `AT+NET` / `AT+LINK`
- `AT+SAVE` / `AT+RESET` / `AT+DEFAULT`
3. 已有结论表明:
- MCU 启动、RTT、FreeRTOS 调度、TIM4 心跳路径可工作
- `CH390D` 基础寄存器读写与 `lwIP netif` 基本链路已经打通过一次
- 真实硬件侧曾定位到 `CH390D` 供电滤波电容虚焊问题
4. 当前调试重点是:
- FreeRTOS 任务是否正常创建与调度
- `MUX / NET / LINK[idx]` 协议是否与代码一致
- UART / TCP / CH390 三层通路是否协同稳定
- 参数保存、复位和恢复流程是否可靠
---
## 3. 代码入口与调试责任边界
### 3.1 启动与 FreeRTOS 入口
以下代码路径是 bring-up 的第一现场:
1. `Core/Src/main.c`
- `main()`:总启动入口
- `SystemClock_Config()`:时钟初始化
- `MX_FREERTOS_Init()`FreeRTOS 任务创建(在 `freertos.c` 中实现)
2. `Core/Src/freertos.c`
- `StartDefaultTask()`:默认任务(LED 心跳 + 看门狗)
- `MX_FREERTOS_Init()`:用户任务创建入口
3. `Core/Src/stm32f1xx_it.c`
- 故障与中断入口
- `TIM4_IRQHandler`HAL 时间基准
- `USART1/2/3``EXTI0`、DMA 回调等联调关键入口
### 3.2 CH390 责任边界
当前 CH390 调试必须遵守以下责任边界:
1. `Drivers/CH390/CH390_Interface.c`GPIO / SPI / 寄存器与内存事务
2. `Drivers/CH390/CH390.c`:芯片级 helper
3. `Drivers/CH390/ch390_runtime.c`:唯一的运行时拥有者
4. `Drivers/LwIP/src/netif/ethernetif.c`netif glue 与轮询桥接
5. SPI 访问由 `xSpiMutex` 保护,避免多任务竞争
### 3.3 配置口与业务口边界
1. `USART1`AT 配置口,接收 `AT` 命令
2. `USART2 / USART3`:数据口,普通透传或 MUX 承载
---
## 4. 当前硬件与调试工具基线
### 4.1 核心硬件对象
1. MCU`STM32F103RCT6`256KB Flash / 48KB SRAM
2. 以太网芯片:`CH390D`
3. 配置串口:`USART1`
4. 数据串口:`USART2 / USART3`
5. 调试输出:`SEGGER RTT`
### 4.2 构建与下载基线
1. `MDK-ARM/TCP2UART.uvprojx`
2. 启动文件:`startup_stm32f103xe.s`
3. 目标器件:`STM32F103RC`
4. 预处理器宏:`USE_HAL_DRIVER, STM32F103xE`
### 4.3 常用调试工具
1. `Keil MDK-ARM`
2. `ST-Link / J-Link`
3. `SEGGER RTT Viewer`
4. `PowerShell`
5. `tools/start_tcp_debug_server.ps1`
6. `tools/tcp_debug_server.py`
---
## 5. FreeRTOS 专项调试
### 5.1 任务状态检查
使用 `vTaskList` 获取所有任务运行状态:
```c
char buf[512];
vTaskList(buf);
SEGGER_RTT_WriteString(0, buf);
```
输出格式:
```text
任务名 状态 优先级 剩余栈 编号
NetworkTask R 4 120 1
UartTask B 4 200 2
ConfigTask B 3 150 3
RouteTask R 3 180 4
DefaultTask B 1 80 5
IDLE R 0 100 6
Tmr Svc B 2 90 7
```
状态码:`R`=Ready, `B`=Blocked, `S`=Suspended, `D`=Deleted, `I`=Invalid
### 5.2 栈溢出检测
已启用 `configCHECK_FOR_STACK_OVERFLOW = 2`,溢出时自动调用:
```c
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
SEGGER_RTT_printf(0, "STACK OVERFLOW: %s\n", pcTaskName);
__BKPT(0);
}
```
### 5.3 堆内存失败检测
已启用 `configUSE_MALLOC_FAILED_HOOK`,分配失败时自动调用:
```c
void vApplicationMallocFailedHook(void)
{
SEGGER_RTT_printf(0, "MALLOC FAILED: Free heap = %u\n", xPortGetFreeHeapSize());
__BKPT(0);
}
```
### 5.4 常见 FreeRTOS 调试陷阱
1. **优先级反转**:使用 Mutex(含优先级继承)而非 Binary Semaphore 保护共享资源
2. **死锁**:多 Mutex 场景确保所有任务按相同顺序获取
3. **中断优先级**FreeRTOS 可管理的 ISR 优先级必须 >= `configMAX_SYSCALL_INTERRUPT_PRIORITY`(本工程 5
4. **栈不足**:每个任务定期调用 `uxTaskGetStackHighWaterMark(NULL)` 检查剩余栈
5. **禁止在中断中调用阻塞 API**:必须使用 `FromISR` 后缀版本
---
## 6. 启动阶段调试顺序
建议按 P0 ~ P5 顺序推进,不要跳层。
### 6.1 P0:确认最小基础条件
1. `MDK-ARM` 可构建并产出新的 `axf/hex/map`
2. 板卡可正常下载与复位
3. RTT 可连接并看到启动输出
4. FreeRTOS 任务创建成功,`DefaultTask` LED 心跳可工作
5. `TIM4` 1ms tick 正常运行
### 6.2 P1:确认 FreeRTOS 调度正常
上电或复位后,优先确认:
1. `StartDefaultTask` 是否进入运行
2. `vTaskList` 输出是否显示所有预期任务
3. `xPortGetFreeHeapSize()` 返回值是否合理
4.`STACK OVERFLOW``MALLOC FAILED` 输出
### 6.3 P2:确认 CH390 初始化链路
启动阶段应重点关注 `NetworkTask` 中初始化日志:
1. `ETH init: gpio`
2. `ETH init: spi`
3. `ETH init: reset`
4. `ETH init: probe`
5. `ETH init: default`
6. `ETH init: mac`
7. `ETH init: done`
### 6.4 P3:确认 TCP 链路
1. lwIP `tcpip_thread` 是否正常运行
2. TCP Server 是否在指定端口监听
3. TCP Client 是否成功连接远端
### 6.5 P3.5:确认 ARP / ICMP 基础网络可达
在继续 TCP 联调前,建议先把 `ARP + ping(ICMP)` 跑通。对于当前 `CH390 + lwIP + FreeRTOS` 架构,这一步不是可选项,而是 TCP 可达之前必须成立的网络基线。
#### 6.5.1 推荐最小验证顺序
1. 先确认板卡 IP、掩码、MAC 与 PC 所在网段一致
2. 上电后先观察 RTT,确认 `ETH init: done` 已出现
3. 在 PC 侧执行一次 `ping <板卡IP>`,同时开启 Wireshark 抓包
4. 先看是否出现发往板卡 IP 的 ARP request,再看设备是否回 ARP reply
5. ARP 正常后,再看是否出现 ICMP echo request / echo reply 成对出现
#### 6.5.2 当前工程推荐观察点
如果网络基础链路有疑问,建议按以下分层观察,不要只看某一层:
1. **raw RX 层**CH390 是否确实收到了以太网帧
2. **Ethernet demux 层**`ethernet_input()` 是否识别到 `ETHTYPE_ARP` / `ETHTYPE_IP`
3. **协议处理层**`etharp_input()` / `ip_input()` 是否真正进入
4. **协议发包层**:lwIP 是否已经生成待发送的 ARP reply / ICMP reply
5. **驱动发送层**`low_level_output()` / CH390 TX 是否真正把帧送出
这次 bring-up 证明,`raw RX 正常` 并不等于 `lwIP 已真正处理该帧`。如果只看到底层收到了包,就直接假设协议栈一定会回复,通常会把排查方向带偏。
#### 6.5.3 这次 ARP / ICMP bring-up 的关键结论
本轮调试中,最终根因位于:
- `Drivers/LwIP/src/netif/ethernet.c`
问题本质是:
1. `ethernet_input()``ETHTYPE_ARP` 分支中,曾直接调用 `etharp_input(p, netif)`
2.`etharp_input()` 要求 `p->payload`**ARP 头** 开始,而不是从 Ethernet 头开始
3. 因此如果没有先执行:
```c
pbuf_remove_header(p, SIZEOF_ETH_HDR)
```
则 ARP 包虽然被收到了,但会在 `etharp_input()` 的早期校验中被静默丢弃,最终表现为:
1. Wireshark 能看到 PC 发来的 ARP request
2. 板子侧底层收包计数在增长
3. 但设备始终不回 ARP reply
4. ping 也自然不会成功
当前应保留的正确处理方式如下:
```c
case ETHTYPE_ARP:
if (netif->flags & NETIF_FLAG_ETHARP) {
if (pbuf_remove_header(p, SIZEOF_ETH_HDR)) {
pbuf_free(p);
return ERR_OK;
}
etharp_input(p, netif);
} else {
pbuf_free(p);
}
return ERR_OK;
```
#### 6.5.4 为什么这类问题容易漏看
这类问题常见但隐蔽,原因通常有三点:
1. 根因非常小,外在表现却像“整个发送链路都坏了”
2. 多个低层信号可能同时正常,容易误导为 SPI / TX / CH390 初始化问题
3. `rx ok``ARP 帧计数在涨``链路已 up` 都不代表协议层一定接受了该帧
因此,后续遇到“收得到包但就是不回”的问题时,优先检查:
1. 传给上层协议处理函数时,`pbuf->payload` 是否已经对齐到正确协议头
2. glue-layer 是否和 lwIP 原生调用约定一致
3. 观察点是否已经覆盖到 `demux -> protocol handler -> linkoutput` 这一整条链
#### 6.5.5 建议保留的最小验收标准
在认定网络基线“已经打通”之前,至少应满足:
1. Keil 工程可稳定构建通过
2. 上电后可稳定看到网络初始化完成日志
3. Wireshark 中能看到设备对本机 ARP request 做出 reply
4. PC 对设备 `ping` 时,能看到 ICMP echo request / reply 成对出现
5. RTT 中无 `STACK OVERFLOW``MALLOC FAILED`、异常 trap 等故障信号
---
## 7. MUX / NET / LINK[idx] 联调指导
### 7.1 协议总则
与裸机版本完全一致,参见 `AT固件使用手册.md`
### 7.2 推荐最小 MUX 联调顺序
1. 先在 `MUX=0` 下跑通原始透传
2. 再切换 `MUX=1`
3. 先发一个控制帧,确认 `DSTMASK=0x00` 路径可通
4. 再发一个单目标数据帧
5. 最后验证多目标位图转发
---
## 8. 异常、卡死与假死排查
### 8.1 看到 `TRAP:` 时怎么做
1. 先记录 RTT 中的 trap 标签
2. 立刻用调试器查看当前 PC / LR / 调用栈
3. 结合 `Core/Src/stm32f1xx_it.c` 中对应 handler 定位异常类型
### 8.2 FreeRTOS 任务卡死时怎么做
1. 使用 `vTaskList` 检查各任务状态
2. 如果某个任务始终 `B`(Blocked),检查其等待的队列/信号量
3. 检查是否有 Mutex 被持有但从未释放
4. 使用调试器暂停,查看各任务的调用栈
### 8.3 常见 FreeRTOS 陷阱
1. 在 ISR 中误调用阻塞 API(如 `xQueueSend` 而非 `xQueueSendFromISR`
2. 中断优先级低于 `configMAX_SYSCALL_INTERRUPT_PRIORITY` 但调用了 FreeRTOS API
3. Mutex 持有期间任务被删除导致 Mutex 永不释放
4. 栈溢出导致邻近变量被破坏
---
## 9. 常见误区
1. 不要继续沿用"CH390 恒为全 `0xFF`"过时结论
2. 不要在多个任务中直接访问 CH390 SPI(必须通过 Mutex 保护)
3. 不要在没有芯片脚侧证据前,只凭 GPIO 判断总线正常
4. 不要在基础寄存器读写尚不可信时,直接调高层业务
5. 不要在 ISR 中执行复杂 SPI 事务或调用阻塞 API
6. 不要忽视 `configCHECK_FOR_STACK_OVERFLOW` 报告
7. 不要把“底层已经收到 ARP 包”等同于“lwIP 一定已经正确处理 ARP 包”
8. 不要忽略 glue-layer 对 `pbuf->payload` 起始位置的约定,特别是 `Ethernet header -> ARP/IP header` 的切换
9. 不要在 ARP / ICMP 还没闭环前,就直接怀疑 TCP Server / TCP Client 逻辑
10. 不要在没有抓包和分层观测点的情况下,只凭单一日志就断言故障位于 TX 或 SPI 层
---
## 10. 推荐配套阅读
1. `AT固件使用手册.md`
2. `项目技术实现.md`
3. `项目需求说明.md`
4. `Keil工程配置说明.txt`
---
## 11. 近期调试经验固化(2026-04
本节用于固化这一轮 `FreeRTOS + lwIP + CH390` 联调过程中已经验证过的经验,后续调试默认以本节为前提,不要反复回到已排除的旧假设。
### 11.1 构建结果的真值来源
当前工程的构建真值必须以 `Keil` 实际构建日志为准,而不是辅助 viewer。
推荐优先级如下:
1. `MDK-ARM/build_capture.txt`
2. `MDK-ARM/TCP2UART/TCP2UART.build_log.htm`
3. `MDK-ARM/TCP2UART/TCP2UART.map`
说明:
1. `keil-build-viewer.exe` 只能辅助观察内存占用或旧构建快照
2. viewer 未刷新时,不代表当前代码没有变化,往往只是最新构建没成功或 viewer 数据滞后
3. 后续任何“编译通过 / RAM 变化 / 目标器件切换”的判断,都必须引用以上三类 Keil 真实产物
### 11.2 近期已验证的事实
以下结论已经被本轮调试反复验证:
1. `DIAG_TASK_ISOLATION=1` 时,系统可以稳定运行
2. `DIAG_TASK_ISOLATION=0` 时,full-task 模式仍会卡死
3. 启动阶段 `lwip_netif_init()`、deferred `xTaskCreate()``netif-ready` 等关键日志已经真实跑通
4. 之前通过加日志、分阶段创建 TCP 任务、调整栈大小后,卡死边界会继续后移,但问题不会完全消失
5. 这说明当前问题不是单一固定点故障,而是“逻辑路径 + 极低资源余量”共同作用的结果
### 11.3 已排除或降级优先级的方向
以下方向目前不再应作为一号假设:
1. **startup/init 失败**
- 当前日志已能稳定走到 `netif-ready`
- 因此主要问题不再是最初的 bring-up 链路本身
2. **单纯 CH390 TX 无限等待是当前主因**
- 已为 TX 路径增加过 bounded wait / timeout 观察点
- 最新故障没有先落到 `[ETH] tx timeout` 分支
3. **只靠继续增大 TCP task 栈就能解决问题**
- `256 -> 384 words` 后,故障边界虽然继续后移,但系统仍会卡死
- 栈确实曾是问题的一部分,但不是唯一剩余问题
### 11.4 当前最重要的资源压力结论
当前 `STM32F103RCT6`(48KB SRAM)上的真实资源压力已经高到会直接干扰调试判断:
1. 真实构建中,`ZI-data` 已接近物理 RAM 上限(约 95%+)
2.`DIAG_TASK_ISOLATION=0` 下,创建完四个 TCP 任务后,`xPortGetFreeHeapSize()` 只剩约 `944 bytes`
3. 这个余量不足以让后续 `netconn_*`、semaphore、mailbox、任务栈回旋空间保持清晰边界
4. 因此当前在 `RCT6` 上继续做 discriminator 时,结果会持续混入“资源边界噪声”
这也是为什么本轮调试后半段已经把重点从“继续在 RCT6 上做更多小修补”切换到“先换更大 RAM 的 pin2pin 器件再继续分析”。
### 11.5 当前推荐的硬件调试策略
当前推荐的下一阶段策略如下:
1. 先从 `STM32F103RCT6` 切换到 pin2pin 的 `STM32F103RDT6`
2. 在切换器件后,尽量保持当前代码基线不做大改
3. 先复测当前版本的 RTT 与运行边界,看故障是否:
- 消失
- 明显后移
- 仍然停在相同 enabled path
4. 再根据新器件上的表现,区分:
- 资源压力主导
- 逻辑 / 时序 / API 使用问题仍然存在
### 11.6 换片后第一轮调试目标
切换到 `RDT6` 后,第一轮调试不追求立刻修复,而是优先回答下面几个问题:
1. 当前 full-task 模式是否仍然卡死
2. `free/min heap` 是否明显高于 `RCT6` 版本
3. enabled 的 `S1 / C1` 是否能够继续进入更深的 `netconn_new / bind / connect / listen / accept` 路径
4. 之前为了定位问题而加入的 discriminator(例如 `C1 first-connect defer`)是否仍然影响故障边界
如果这些问题不先回答,后续继续改代码的结论可信度会很差。
### 11.7 当前建议保留的调试原则
后续 agent 或开发者继续接手本工程时,建议遵守以下原则:
1. 不要在 `RCT6` 上继续大量加日志、加栈、加 queue 深度后再试图解释现象
2. 不要把 viewer 当作当前构建真值
3. 不要忽略 `DIAG_TASK_ISOLATION=1 正常、=0 异常` 这个前提
4. 不要一次性修改 `C1/S1/CH390/lwIP` 多个方向,避免再次失去因果关系
5. 每次只做一个能明显改变故障边界的最小改动,并用 RTT + Keil build 结果交叉验证
### 11.8 固定 Client 端口重连策略(TIME_WAIT 取舍)
当前工程的 `Client` 链路保留固定 `LPORT` 配置语义,默认 `C1/C2` 均使用明确的本地源端口。
`lwIP + netconn` 路径下,如果仍沿用优雅 `netconn_close()`,则相同 `LPORT` 的快速重连会受到 TCP `TIME_WAIT` 影响,表现为一段时间内重复 `bind/connect` 失败。
结合本项目的约束,当前版本固化如下取舍:
1. 不取消 `Client` 固定 `LPORT` 语义
2. 不依赖扩大 PCB 池作为主修复手段
3. 不通过降低 `TCP_MSL` 改写全局 TCP 保守语义
4.`Client` 主动断开后的释放路径采用 abortive closeRST),以立即释放 PCB 与本地端口
使用该策略时应明确接受以下副作用:
1. 对端可能看到 `RST` 或“连接被重置”
2. 连接尾部未完成发送的数据不会再走优雅关闭路径
3. 该策略仅用于固定 `Client` 端口快速重连场景,不应直接推广到所有 TCP 关闭路径
+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` | 网络初始化、轮询和恢复任务 |
-646
View File
@@ -1,646 +0,0 @@
# TCP2UART 项目技术实现
## 一、文档目的
本文档描述 `TCP2UART` 项目基于 `STM32F103RCT6 + FreeRTOS` 的最终内部实现口径。
本文档只围绕最终协议模型展开:
- `MUX`:串口承载层
- `NET`:全局网络配置层
- `LINK[idx]`:实例配置与连接管理层
不再保留历史 `S1... / C1...` 外部字段模型。
## 二、当前工程基础
当前工程基础约束如下:
1. MCU`STM32F103RCT6`256KB Flash / 48KB SRAM
2. 网络芯片:`CH390D`
3. 软件架构:`FreeRTOS + lwIP NO_SYS=0`
4. 协议栈:`lwIP socket/netconn API`
5. 调试输出:`SEGGER RTT`
6. 使用 `FreeRTOS` 任务调度
7. 不实现 DHCP
## 三、总体架构
```text
+--------------------------------------------------+
| AT / Control Plane |
| USART1 AT parser + MUX control frame parser |
+--------------------------------------------------+
| Configuration Model |
| MUX / NET / LINK[idx] |
+--------------------------------------------------+
| FreeRTOS Tasks |
| NetworkTask / UartTask / ConfigTask / RouteTask |
+--------------------------------------------------+
| Inter-Task Communication |
| Queue / Semaphore / Mutex / StreamBuffer |
+--------------------------------------------------+
| lwIP TCP/IP Stack (NO_SYS=0) |
| tcpip_thread + socket/netconn + sys_arch |
+--------------------------------------------------+
| Driver Layer |
| CH390 / lwIP netif / UART DMA+IDLE / HAL |
+--------------------------------------------------+
```
## 四、FreeRTOS 任务设计(路径 Anetconn + 多 TCP 任务)
### 4.1 架构路线
本项目采用 **路径 A**`NO_SYS=0 + netconn API + 每个 TCP 连接独立任务`
核心决策:
1. lwIP 以 `NO_SYS=0` 模式运行,`tcpip_thread` 由 lwIP 自动创建
2. TCP 连接使用 `netconn` 阻塞 API`netconn_accept` / `netconn_recv` / `netconn_write`
3. 每个 TCP Server 和 Client 实例各占一个独立任务
4. 任务间通过 Queue 传递指针 + 元数据描述符,实现零拷贝
### 4.2 任务列表(共 9 个任务 + 1 个 lwIP 自建)
| 任务名 | 优先级 | 栈(words) | 模式 | 职责 |
|--------|--------|-----------|------|------|
| `tcpip_thread` | 6 (最高) | 512 | 阻塞 | lwIP 内核线程(自动创建) |
| `NetPollTask` | 5 | 384 | 事件驱动 | `ethernetif_poll` + 链路检测 |
| `TcpSrvTask_S1` | 4 | 384 | 阻塞 | `netconn_accept` + S1 收发 |
| `TcpSrvTask_S2` | 4 | 384 | 阻塞 | `netconn_accept` + S2 收发 |
| `TcpCliTask_C1` | 4 | 256 | 阻塞 | `netconn_connect` + C1 收发 |
| `TcpCliTask_C2` | 4 | 256 | 阻塞 | `netconn_connect` + C2 收发 |
| `UartRxTask` | 4 | 384 | 事件驱动 | UART DMA/IDLE 接收 + MUX 帧提取 |
| `ConfigTask` | 2 | 256 | 阻塞 | AT 命令解析与响应 |
| `DefaultTask` | 1 | 128 | 周期 | LED 心跳 + IWDG 喂狗 |
说明:
- `tcpip_thread` 是 lwIP 自建的内核线程,处理所有协议栈内部事件
- 所有 `netconn_*` API 通过 `tcpip_thread` 消息机制实现线程安全
- `LWIP_TCPIP_CORE_LOCKING=1` 允许应用任务直接调用 `netconn_write` 而不经邮箱中转
- `tcpip_thread` 优先级最高(6),确保 TCP ACK 和超时处理不被延迟
### 4.3 零拷贝路由消息设计
```c
/* 路由消息描述符 - Queue 传递的是此结构的指针,不拷贝负载数据 */
typedef struct {
uint8_t src_id; /* 源端点 ID */
uint8_t dst_mask; /* 目标端点位图 */
uint16_t len; /* 数据长度 */
uint8_t conn_type; /* 连接标识:LINK_S1/S2/C1/C2 */
uint8_t *data; /* 指向预分配静态缓冲区 */
} route_msg_t;
```
静态缓冲池(预分配,避免动态分配):
```c
#define ROUTE_BUF_COUNT 4
#define ROUTE_BUF_SIZE 512
static uint8_t g_route_buf_pool[ROUTE_BUF_COUNT][ROUTE_BUF_SIZE];
static volatile uint8_t g_route_buf_used[ROUTE_BUF_COUNT];
```
- 发送方:从池中获取空闲缓冲区,拷贝数据,填充 `route_msg_t`Queue 发送指针
- 接收方:从 Queue 取 `route_msg_t*`,处理数据后标记缓冲区为可用
- 无动态分配,无堆碎片
### 4.4 任务间通信机制
```text
UART ISR ──[TaskNotify]──> UartRxTask ──[Queue*]──> TcpSrvTask / TcpCliTask
│ ▲
├──[Queue*]──────────────>─┘
DSTMASK=0 └──[Queue]──> ConfigTask
TcpSrvTask / TcpCliTask ──[Queue*]──> UartRxTask (UART TX 方向)
EXTI0 ISR ──[BinarySem]──> NetPollTask ──> ethernetif_poll ──> tcpip_thread
```
通信对象:
| 对象 | 类型 | 生产者 | 消费者 | 用途 |
|------|------|--------|--------|------|
| `xNetSemaphore` | Binary Semaphore | EXTI0 ISR | NetPollTask | CH390 中断通知 |
| `xUartRxNotify` | TaskNotification | UART IDLE ISR | UartRxTask | UART 接收通知 |
| `xTcpRxQueue` | Queue (16, route_msg_t*) | TCP 任务 | UartRxTask | TCP→UART 数据 |
| `xUartTxQueue` | Queue (8, route_msg_t*) | UartRxTask | TCP 任务 | UART→TCP 数据 |
| `xConfigQueue` | Queue (8, char*) | UartRxTask | ConfigTask | AT 文本 |
说明:
- TCP→UART 方向:TCP 任务调用 `netconn_recv` 获取数据,构造 `route_msg_t` 指针投递到 `xTcpRxQueue`UartRxTask 取出后写入 UART DMA
- UART→TCP 方向:UartRxTask 从 UART DMA 读取数据,构造 `route_msg_t` 指针投递到 `xUartTxQueue`,对应 TCP 任务取出后调用 `netconn_write` 发送
- 路由逻辑内联在 UartRxTask 和各 TCP 任务中,不设独立 RouteTask
### 4.5 NetPollTask 实现
```c
void NetPollTask(void *argument)
{
/* 初始化 CH390 + lwIP netif */
ethernetif_init(&ch390_netif);
/* 等待链路就绪后启动 TCP 任务 */
/* ... */
for (;;) {
xSemaphoreTake(xNetSemaphore, pdMS_TO_TICKS(2));
ethernetif_poll(&ch390_netif);
ethernetif_check_link(&ch390_netif);
/* sys_check_timeouts() 由 tcpip_thread 自动执行,此处不需要调用 */
}
}
```
### 4.6 TcpSrvTask 实现模板
```c
void TcpSrvTask_S1(void *argument)
{
struct netconn *conn = netconn_new(NETCONN_TCP);
netconn_bind(conn, IP_ADDR_ANY, cfg->links[0].lport);
netconn_listen(conn);
for (;;) {
struct netconn *newconn;
if (netconn_accept(conn, &newconn) == ERR_OK) {
/* 在本任务内处理唯一客户端 */
tcp_server_worker(newconn, LINK_S1);
netconn_close(newconn);
netconn_delete(newconn);
}
}
}
```
### 4.7 TcpCliTask 实现模板
```c
void TcpCliTask_C1(void *argument)
{
for (;;) {
struct netconn *conn = netconn_new(NETCONN_TCP);
ip_addr_t remote_ip;
IP_ADDR4(&remote_ip, cfg->links[2].rip[0], ...);
if (netconn_connect(conn, &remote_ip, cfg->links[2].rport) == ERR_OK) {
tcp_client_worker(conn, LINK_C1);
}
netconn_close(conn);
netconn_delete(conn);
vTaskDelay(pdMS_TO_TICKS(cfg->links[2].reconnect_interval));
}
}
```
### 4.8 UartRxTask 实现
```c
void UartRxTask(void *argument)
{
for (;;) {
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(10));
/* 处理 UART2/UART3 DMA 接收数据 */
/* MUX=0: 构造 route_msg_t 指针投递到 xUartTxQueue */
/* MUX=1: 提取 MUX 帧,DSTMASK=0 投 xConfigQueue,否则投 xUartTxQueue */
/* 从 xTcpRxQueue 取数据,写入 UART DMA 发送 */
}
}
```
### 4.9 ConfigTask 实现
```c
void ConfigTask(void *argument)
{
for (;;) {
char *cmd;
xQueueReceive(xConfigQueue, &cmd, portMAX_DELAY);
config_process_at_cmd(cmd);
/* 通过 UART1 发送响应 */
}
}
```
## 五、最终协议实现模型
### 5.1 MUX 帧承载层
数据口启用 MUX 后,统一处理如下帧:
```text
SYNC | LEN_H | LEN_L | SRCID | DSTMASK | PAYLOAD | TAIL
```
实现职责:
1. 识别帧边界
2. 解析长度字段
3. 提取 `SRCID`
4. 解析 `DSTMASK`
5. 按控制帧或数据帧分流
### 5.2 控制帧与数据帧分离
控制规则固定如下:
- `DSTMASK = 0x00`:系统控制帧
- `DSTMASK != 0x00`:业务数据帧
系统控制帧处理要求:
1. `PAYLOAD` 解释为 AT 文本
2. AT 文本必须以 `\r\n` 结束
3. 控制帧投递到 `ConfigTask`
业务数据帧处理要求:
1. `SRCID` 表示单一源端点
2. `DSTMASK` 表示目标端点集合
3. `RouteTask` 根据 `DSTMASK` 做多目标分发
### 5.3 统一端点编码
内部与外部文档统一使用以下端点编码:
| 端点 | 编码 |
|------|------|
| `C1` | `0x01` |
| `C2` | `0x02` |
| `UART2` | `0x04` |
| `UART3` | `0x08` |
| `S1` | `0x10` |
| `S2` | `0x20` |
实现要求:
- `SRCID` 为单值
- `DSTMASK` 为位图
- `DSTMASK=0x00` 仅保留为控制帧
## 六、配置层设计
### 6.1 MUX 记录
`MUX` 为全局记录,仅控制设备数据口是否进入 MUX 承载模式。
取值:
- `0`:普通透传
- `1`MUX 透传
### 6.2 NET 记录
`NET` 为全局静态网络记录:
```text
IP,MASK,GW,MAC
```
说明:
- 设备只有一张网卡,因此不为每个实例单独配置本地 IP
- 当前实现目标中不包含 DHCP
### 6.3 LINK 记录
`LINK[idx]` 为统一实例记录:
```text
EN,LPORT,RIP,RPORT,UART
```
固定索引映射:
- `0 = S1`
- `1 = S2`
- `2 = C1`
- `3 = C2`
字段职责:
- `EN`:实例启用状态
- `LPORT`:本地端口
- `RIP / RPORT`:对端地址与端口
- `UART`:对应业务数据口
说明:
- `Server``Client` 共享同一记录结构
- `Server``RIP / RPORT` 可作为对端约束或预设
- `Client``RIP / RPORT` 表示远端目标
## 七、模块职责
### 7.1 配置模块 `config.c/.h`
最终职责:
1. 解析 `AT+MUX`
2. 解析 `AT+NET`
3. 解析 `AT+LINK`
4. 加载与保存配置
5. 处理 `SAVE / RESET / DEFAULT`
### 7.2 UART 透传模块 `uart_trans.c/.h`
最终职责:
1. 保持 `USART2 / USART3``DMA + IDLE` 接收发送基线
2.`MUX=0` 时执行普通透传
3.`MUX=1` 时执行 MUX 帧收发
4. 将控制帧与业务数据帧分流
### 7.3 TCP Server / Client 模块(需重写)
`tcp_server.c` / `tcp_client.c` 基于 lwIP RAW API 回调模式,需重写为 netconn 阻塞模式:
1. 删除所有 `tcp_pcb` / `tcp_recv` / `tcp_accept` 回调代码
2. 改用 `netconn_new` / `netconn_bind` / `netconn_listen` / `netconn_accept`Server
3. 改用 `netconn_new` / `netconn_connect`Client
4. 收发改为 `netconn_recv` / `netconn_write` 阻塞调用
5. 内部 ring buffer 可取消(netconn 内部已有 pbuf 缓冲)
6. 每个 TCP 任务内直接处理路由,通过 Queue 指针传递数据
补充约束(当前实现口径):
1. `Client` 链路保留固定 `LPORT` 配置语义,以满足产品侧对固定源端口的依赖
2.`lwIP + netconn` 模型下,若 `Client` 继续使用优雅 `netconn_close()`,相同本地端口的快速重连会受 `TIME_WAIT` 影响
3. 因此当前工程对 `Client` 会话结束后的释放路径采用 abortive close`tcp_abort` / RST)以立即释放 PCB 与本地端口
4. 该策略只针对 `Client` 固定端口重连路径,不扩展到 `Server` listener 或一般被动关闭场景
5. 该策略的已知代价是:对端可能看到 `RST`,且尾部未完成发送的数据不会再走优雅 `FIN/ACK` 收尾
### 7.4 FreeRTOS 初始化 `freertos.c`
CubeMX 生成的 FreeRTOS 初始化文件,职责:
1. 定义默认任务 `StartDefaultTask`
2. 用户在 `MX_FREERTOS_Init` 中创建自定义任务
### 7.5 FreeRTOS 配置 `FreeRTOSConfig.h`
关键配置项:
| 配置项 | 值 | 说明 |
|--------|-----|------|
| `configUSE_PREEMPTION` | 1 | 抢占式调度 |
| `configTICK_RATE_HZ` | 1000 | 1ms tick |
| `configMINIMAL_STACK_SIZE` | 128 | 最小栈(words |
| `configTOTAL_HEAP_SIZE` | 10240 | FreeRTOS 堆大小 |
| `configMAX_PRIORITIES` | 7 | 最大优先级数 |
| `configUSE_MUTEXES` | 1 | 启用互斥锁 |
| `configUSE_COUNTING_SEMAPHORES` | 1 | 启用计数信号量 |
| `configUSE_RECURSIVE_MUTEXES` | 1 | 启用递归互斥锁 |
| `configCHECK_FOR_STACK_OVERFLOW` | 2 | 栈溢出检测方式 2 |
| `configUSE_MALLOC_FAILED_HOOK` | 1 | 内存分配失败钩子 |
| `configSUPPORT_DYNAMIC_ALLOCATION` | 1 | 动态内存分配 |
## 八、lwIP 配置(NO_SYS=0 + netconn
### 8.1 lwIP 线程模型
由于采用 `NO_SYS=0`lwIP 将运行以下线程:
1. `tcpip_thread`(优先级 6,栈 512 words):lwIP 核心线程,处理所有协议栈内部事件
2. 应用任务通过 `netconn` API 与 lwIP 交互,由 `tcpip_thread` 消息机制保证线程安全
3. `LWIP_TCPIP_CORE_LOCKING=1`:允许应用任务在持有核心锁时直接调用 `netconn_write`,无需通过邮箱中转
### 8.2 lwIP 关键配置项(lwipopts.h
| 配置项 | 值 | 说明 |
|--------|-----|------|
| `NO_SYS` | 0 | 启用 OS 抽象 |
| `LWIP_NETCONN` | 1 | 启用 netconn API |
| `LWIP_SOCKET` | 0 | 不使用 socket API,节省 RAM |
| `LWIP_TCPIP_CORE_LOCKING` | 1 | 允许直接发送,减少延时 |
| `MEM_SIZE` | 8192 | lwIP 堆大小 |
| `PBUF_POOL_SIZE` | 10 | pbuf 池数量 |
| `MEMP_NUM_NETCONN` | 8 | 2监听 + 4连接 + 2余量 |
| `MEMP_NUM_NETBUF` | 8 | netconn 缓冲 |
| `MEMP_NUM_TCP_PCB` | 4 | TCP 控制块 |
| `MEMP_NUM_TCP_PCB_LISTEN` | 2 | TCP 监听 |
| `MEMP_NUM_TCP_SEG` | 24 | TCP 段 |
| `MEMP_NUM_TCPIP_MSG_API` | 8 | API 消息池 |
| `MEMP_NUM_TCPIP_MSG_INPKT` | 8 | 入包消息池 |
| `TCP_MSS` | 536 | 保守 MSS |
| `TCP_SND_BUF` | 8×MSS=4288 | 发送缓冲 |
| `TCP_WND` | 8×MSS=4288 | 接收窗口 |
| `TCPIP_THREAD_STACKSIZE` | 512 | tcpip_thread 栈 |
| `TCPIP_THREAD_PRIO` | 6 (最高) | tcpip_thread 优先级 |
| `LWIP_DHCP` | 0 | 不使用 DHCP |
| `LWIP_UDP` | 0 | 不使用 UDP |
### 8.3 sys_arch 移植层
`Drivers/LwIP/port/sys_arch.c` 需实现 lwIP 到 FreeRTOS 的适配:
1. `sys_thread_new`:创建 lwIP 线程(即 `tcpip_thread`
2. `sys_mbox_*`:消息邮箱(基于 FreeRTOS Queue,容量 `TCPIP_MBOX_SIZE=8`
3. `sys_sem_*`:信号量(基于 FreeRTOS Semaphore
4. `sys_mutex_*`:互斥锁(基于 FreeRTOS Mutex
5. `sys_arch_protect / unprotect`:临界区保护(基于 `vPortEnterCritical / vPortExitCritical`
### 8.4 lwIP 初始化流程
```c
/* 在 MX_FREERTOS_Init 或 NetPollTask 中调用 */
void lwip_init_task(void)
{
/* tcpip_thread 在 lwip_init() 或第一个 sys_thread_new 时自动启动 */
tcpip_init(NULL, NULL);
/* 等待 tcpip_thread 就绪 */
/* 添加网络接口 */
netif_add(&ch390_netif, &ipaddr, &netmask, &gw, NULL, &ethernetif_init, &tcpip_input);
netif_set_default(&ch390_netif);
netif_set_up(&ch390_netif);
}
```
## 九、中断与 HAL 时间基准
### 9.1 HAL 时间基准
FreeRTOS 下 `SysTick` 被 FreeRTOS 占用,HAL 时间基准改用 `TIM4`
- `TIM4` 配置为 1ms 中断(72MHz / (71+1) / (999+1) = 1kHz
- `HAL_InitTick` 使用 `TIM4` 而非 `SysTick`
- `uwTick``TIM4_IRQHandler` 中递增
### 9.2 中断优先级规划
| 中断 | 优先级 | 说明 |
|------|--------|------|
| `SysTick` | 15(最低) | FreeRTOS tick |
| `PendSV` | 15(最低) | FreeRTOS 上下文切换 |
| `SVCall` | 0 | FreeRTOS 服务调用 |
| `TIM4` | 0 | HAL 时间基准 |
| `EXTI0` | 5 | CH390 中断 |
| `DMA1_Ch2~7` | 5 | UART DMA |
| `USART1/2/3` | 5 | UART 中断 |
| `SPI1` | 5 | SPI 中断 |
FreeRTOS 可管理的中断优先级必须 >= `configMAX_SYSCALL_INTERRUPT_PRIORITY`(本工程为 5)。
## 十、内存预算(路径 A 精确估算)
`STM32F103RCT6` 为目标(48KB SRAM):
### 10.1 RAM 预算
| 项目 | 大小 | 说明 |
|------|------|------|
| 启动栈 (MSP) | 2,048 B | `startup_stm32f103xe.s` 定义 0x800 |
| FreeRTOS 堆 (heap_4) | 10,240 B | `configTOTAL_HEAP_SIZE` |
| 任务栈 (9 任务) | 13,312 B | 3,328 words × 4 (见上表) |
| lwIP 堆 (MEM_SIZE) | 8,192 B | |
| PBUF 池 (10 个) | ~6,000 B | 10 × ~600B |
| MEMP 池 | ~4,000 B | netconn/netbuf/tcpip_msg/pcb/seg |
| UART DMA+Ring 缓冲 | 2,304 B | 2 通道 × (256+256+512+384)/2 |
| 路由缓冲池 (4×512B) | 2,048 B | 零拷贝指针传递 |
| 配置结构 | 1,024 B | |
| **合计** | **~49,168 B** | |
| **余量 (RCT6 48KB)** | **~-928 B** | ⚠️ 超出,需优化或换 RDT6 |
| **余量 (RDT6 64KB)** | **~15,264 B** | ✅ 充裕 |
### 10.2 优化空间(RCT6 下)
若坚持使用 RCT6,可通过以下措施压缩到 48KB 以内:
| 优化项 | 节省 |
|--------|------|
| 取消 TCP 任务的 ring buffernetconn 内部有 pbuf 缓冲) | -2,048 B |
| `configTOTAL_HEAP_SIZE` 降至 8KB | -2,048 B |
| `MEM_SIZE` 降至 6KB | -2,048 B |
| TcpCliTask 栈降至 192 words × 2 | -512 B |
| **优化后合计** | **~42,504 B** |
| **RCT6 余量** | **~5,464 B** |
### 10.3 备选 MCU
当 RAM 最终不够用时,切换为 `STM32F103RDT6`pin-to-pin 替代):
| 项目 | RCT6 | RDT6 |
|------|------|------|
| Flash | 256 KB | 384 KB |
| SRAM | 48 KB | 64 KB |
| 引脚 | LQFP64 | LQFP64(完全兼容) |
| 启动文件 | `startup_stm32f103xe.s` | `startup_stm32f103xe.s` |
| 宏定义 | `STM32F103xE` | `STM32F103xE` |
| Flash 算法 | `STM32F10x_HD` | `STM32F10x_HD`(相同) |
| SRAM 大小 | `0xC000` | `0x10000` |
| Flash 大小 | `0x40000` | `0x60000` |
### 10.4 Flash 预算
| 项目 | 估计值 | 说明 |
|------|--------|------|
| FreeRTOS 内核 | ~8 KB | 含 CMSIS-RTOS V2 |
| HAL 驱动 | ~20 KB | GPIO/UART/SPI/DMA/IWDG/TIM |
| lwIP 协议栈 | ~50 KB | core + api + ipv4 + netif + sys_arch |
| CH390 驱动 | ~4 KB | |
| 应用代码 | ~20 KB | config/uart_trans/tcp_server/tcp_client |
| **合计** | ~102 KB | RCT6 预留 154 KBRDT6 预留 282 KB |
## 十一、硬件资源
### 11.1 MCU
- 型号:`STM32F103RCT6`
- Flash`256 KB`
- SRAM`48 KB`
- 主频:`72 MHz`
### 11.2 主要外设
- `SPI1`:连接 `CH390D`
- `USART1`:配置串口
- `USART2`:数据透传串口
- `USART3`:数据透传串口
- `DMA1`3 路 UART 收发 DMA
- `EXTI0`CH390 中断输入
- `IWDG`:独立看门狗
- `TIM4`HAL 时间基准(替代 SysTick
### 11.3 引脚分配
| 引脚 | 功能 | 用途 |
|------|------|------|
| PA2 | USART2_TX | 数据透传串口 |
| PA3 | USART2_RX | 数据透传串口 |
| PA4 | SPI1_NSS | CH390D 片选 |
| PA5 | SPI1_SCK | CH390D SPI 时钟 |
| PA6 | SPI1_MISO | CH390D SPI 数据输入 |
| PA7 | SPI1_MOSI | CH390D SPI 数据输出 |
| PA9 | USART1_TX | 配置串口 |
| PA10 | USART1_RX | 配置串口 |
| PB0 | EXTI0 | CH390D INT |
| PB1 | GPIO_Output | CH390D RESET |
| PB10 | USART3_TX | 数据透传串口 |
| PB11 | USART3_RX | 数据透传串口 |
| PC13 | GPIO_Output | 状态 LED |
| PD0/PD1 | HSE | 8MHz 外部晶振 |
## 十二、实现边界
1. 保持单网卡静态网络模型
2. 不实现 DHCP
3. 不实现旧 `S1... / C1...` 外部协议字段
4. 不在文档中保留兼容层描述
5. 所有 AT 文本控制统一要求 `\r\n` 结束
6. FreeRTOS 堆管理使用 `heap_4.c`
7. HAL 时间基准使用 `TIM4` 而非 `SysTick`
## 十三、文档一致性要求
后续实现、联调、测试与代码注释必须遵守以下统一口径:
1. 对外协议只使用 `MUX / NET / LINK`
2. 控制帧只使用 `DSTMASK=0x00`
3. MUX 帧格式固定为 `SYNC | LEN_H | LEN_L | SRCID | DSTMASK | PAYLOAD | TAIL`
4. AT 手册、需求说明、技术实现三份文档不得再出现历史展开式字段
## 十四、路径 A 实现清单
### 14.1 必须重写的模块
| 模块 | 原实现 | 目标实现 | 说明 |
|------|--------|----------|------|
| `tcp_server.c/.h` | lwIP RAW API 回调 | netconn 阻塞任务 | `tcp_new``netconn_new`,回调 → 阻塞循环 |
| `tcp_client.c/.h` | lwIP RAW API 回调 | netconn 阻塞任务 | 同上 |
| `sys_arch.c/.h` | NO_SYS=1 空壳 | FreeRTOS 移植层 | `sys_mbox``sys_sem``sys_mutex``sys_thread` |
| `lwipopts.h` | NO_SYS=1 | NO_SYS=0 + netconn | 已更新 |
| `main.c` | while(1) 轮询 | FreeRTOS 任务创建 | `App_Poll()` → 各任务函数 |
| `stm32f1xx_it.c` | 裸机 ISR | FreeRTOS ISR | `xxFromISR` API |
### 14.2 可复用(需适配)的模块
| 模块 | 适配内容 |
|------|----------|
| `uart_trans.c/.h` | 添加 `xTaskNotifyFromISR` 替代 poll 模式 |
| `config.c/.h` | 添加 `xQueueReceive` 替代 `config_poll()` |
| `flash_param.c/.h` | 无需修改 |
| `CH390` 驱动 | `ethernetif_poll` 改为 NetPollTask 调用,SPI 加 Mutex 保护 |
| `ethernetif.c` | 添加 `netif_add``tcpip_input` 回调 |
### 14.3 无需修改的模块
| 模块 | 说明 |
|------|------|
| `FreeRTOSConfig.h` | 已更新(任务优先级宏、堆大小) |
| `startup_stm32f103xe.s` | 已适配 RCT6 |
| `TCP2UART.ioc` | 已适配 RCT6 + FreeRTOS + TIM4 |
| `MDK-ARM/TCP2UART.uvprojx` | 已适配 RCT6 + xE 宏 |
| MUX 帧编解码 | 协议逻辑与 RTOS 无关 |
### 14.4 新增模块
| 模块 | 职责 |
|------|------|
| `route_msg.c/.h` | 零拷贝路由消息池管理 |
| `task_tcp_server.c/.h` | netconn Server 任务模板 |
| `task_tcp_client.c/.h` | netconn Client 任务模板 |
| `task_net_poll.c/.h` | CH390 poll + link check 任务 |
-191
View File
@@ -1,191 +0,0 @@
# TCP2UART 项目需求说明
## 一、项目目标
本项目基于 `STM32F103RCT6``CH390D` 实现一台多实例 TCP 与双串口数据透传设备。
最终对外协议模型固定为:
1. `MUX`:控制串口侧是否采用 MUX 承载
2. `NET`:全局静态网络配置
3. `LINK[idx]`:按实例索引组织的链路配置
系统必须支持:
- `2` 路 TCP Server 实例
- `2` 路 TCP Client 实例
- `UART1` 作为 AT 配置口
- `UART2 / UART3` 作为业务数据口
## 二、硬件与软件边界
### 2.1 硬件边界
- 主控:`STM32F103RCT6`256KB Flash / 48KB SRAM
- 备选主控:`STM32F103RDT6`384KB Flash / 64KB SRAM),pin-to-pin 兼容,当 RAM 不够时直接替换
- 以太网芯片:`CH390D`
- 网卡数量:`1`
- 配置口:`UART1`
- 数据口:`UART2``UART3`
### 2.2 软件边界
- 执行模型:`FreeRTOS`
- 网络协议栈:`lwIP NO_SYS=0 + netconn API`(线程安全,每连接独立任务)
- 调试输出:`SEGGER RTT`
- 采用 `FreeRTOS` 任务调度
- TCP 连接使用 `netconn` 阻塞 API`netconn_accept` / `netconn_recv` / `netconn_write`
- 每条 TCP 连路(S1/S2/C1/C2)独立一个任务
- 不包含 DHCP 协议支持
## 三、最终协议需求
### 3.1 MUX 帧格式
所有 MUX 数据承载必须使用如下格式:
```text
SYNC | LEN_H | LEN_L | SRCID | DSTMASK | PAYLOAD | TAIL
```
要求:
- `DSTMASK != 0x00`:业务数据帧
- `DSTMASK = 0x00`:系统控制帧
- 系统控制帧承载 AT 文本命令
- AT 文本命令必须以 `\r\n` 结尾
### 3.2 统一端点编码
系统必须使用统一端点编码,同时覆盖 UART 与 TCP 逻辑实例:
| 端点 | 编码 |
|------|------|
| `C1` | `0x01` |
| `C2` | `0x02` |
| `UART2` | `0x04` |
| `UART3` | `0x08` |
| `S1` | `0x10` |
| `S2` | `0x20` |
要求:
- `SRCID` 为单值
- `DSTMASK` 为位图
- `DSTMASK=0x00` 仅保留给系统控制帧
## 四、AT 接口需求
### 4.1 命令分类
AT 协议必须收敛为以下三类命令:
1. `AT+MUX`
2. `AT+NET`
3. `AT+LINK`
不再保留历史展开式实例字段命令。
### 4.2 MUX 命令需求
- `AT+MUX=0/1`:设置全局 MUX 模式
- `AT+MUX?`:查询当前 MUX 模式
### 4.3 NET 命令需求
`NET` 必须统一表达以下静态网络参数:
```text
IP,MASK,GW,MAC
```
说明:
- 设备只有一张网卡,因此本地 IP 不按实例拆分
- DHCP 不属于协议需求范围
### 4.4 LINK 命令需求
`LINK[idx]` 必须统一表达如下字段:
```text
EN,LPORT,RIP,RPORT,UART
```
要求:
- `idx` 固定映射四个实例:`0=S1``1=S2``2=C1``3=C2`
- `Server``Client` 共用同一条 `LINK` 配置模型
- `LPORT` 必须可配置
- `RIP / RPORT` 必须可配置
- `UART` 必须可配置
## 五、功能需求
### 5.1 TCP 功能
- 支持 `2` 路 Server
- 支持 `2` 路 Client
- 每个实例通过 `LINK[idx]` 配置其本地端口、对端地址、对端端口和串口路由
### 5.2 串口透传功能
- `UART2 / UART3` 支持普通透传模式与 MUX 透传模式
- 当需要多实例共享数据口时,必须启用 MUX 模式
- 业务数据流向由 `SRCID / DSTMASK` 决定
### 5.3 系统控制功能
- 系统控制帧由 `DSTMASK=0x00` 表示
- 系统控制帧进入 AT 解析路径
- 控制文本必须以 `\r\n` 结束
### 5.4 参数保存功能
- 参数修改后支持 `SAVE`
- 支持 `RESET` 后按保存配置启动
- 支持恢复默认配置
## 六、FreeRTOS 任务架构需求
### 6.1 任务划分
系统至少应包含以下 FreeRTOS 任务:
| 任务 | 优先级 | 职责 |
|------|--------|------|
| tcpip_thread | 6 (最高) | lwIP 内核线程(自动创建) |
| NetPollTask | 5 | CH390 事件轮询 + 链路检测 |
| TcpSrvTask_S1 | 4 | S1 netconn_accept + 收发 |
| TcpSrvTask_S2 | 4 | S2 netconn_accept + 收发 |
| TcpCliTask_C1 | 4 | C1 netconn_connect + 收发 |
| TcpCliTask_C2 | 4 | C2 netconn_connect + 收发 |
| UartRxTask | 4 | UART DMA/IDLE 接收 + MUX 帧提取 + 路由 |
| ConfigTask | 2 | AT 命令解析与响应 |
| DefaultTask | 1 | LED 心跳 + 看门狗 |
### 6.2 任务间通信
- 使用 `Queue` 传递指针 + 元数据描述符(零拷贝路由消息)
- 使用 `Binary Semaphore` 同步 CH390 中断事件
- 使用 `TaskNotification` 通知 UART IDLE 事件
- 预分配静态缓冲池,避免动态分配
## 七、非功能需求
1. 满足 `STM32F103RCT6``256KB Flash / 48KB SRAM` 约束,若 RAM 不足可切换 `STM32F103RDT6`pin-to-pin64KB SRAM
2. 工程可在 `MDK-ARM` 下构建
3. 调试输出统一使用 `SEGGER RTT`
4. 不引入 DHCP、DNS、UDP 等当前非目标协议
5. FreeRTOS 堆使用 `heap_4.c`,总堆大小建议 `10KB`
6. 所有任务栈通过 `uxTaskGetStackHighWaterMark` 监控
## 八、验收口径
验收时以下几点必须同时成立:
1. 文档只使用 `MUX / NET / LINK` 作为最终协议模型
2. 文档不再出现历史 `S1... / C1...` 外部字段
3. 串口控制文本统一规定为 `\r\n` 结束
4. MUX 帧格式与端点编码在需求、手册、技术实现三份文档中表述一致
5. FreeRTOS 任务无死锁、无栈溢出、无优先级反转