Files
TCP2UART/工程调试指南.md
T

17 KiB
Raw Blame History

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_IRQHandlerHAL 时间基准
    • USART1/2/3EXTI0、DMA 回调等联调关键入口

3.2 CH390 责任边界

当前 CH390 调试必须遵守以下责任边界:

  1. Drivers/CH390/CH390_Interface.cGPIO / SPI / 寄存器与内存事务
  2. Drivers/CH390/CH390.c:芯片级 helper
  3. Drivers/CH390/ch390_runtime.c:唯一的运行时拥有者
  4. Drivers/LwIP/src/netif/ethernetif.cnetif glue 与轮询桥接
  5. SPI 访问由 xSpiMutex 保护,避免多任务竞争

3.3 配置口与业务口边界

  1. USART1AT 配置口,接收 AT 命令
  2. USART2 / USART3:数据口,普通透传或 MUX 承载

4. 当前硬件与调试工具基线

4.1 核心硬件对象

  1. MCUSTM32F103RCT6256KB 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 获取所有任务运行状态:

char buf[512];
vTaskList(buf);
SEGGER_RTT_WriteString(0, buf);

输出格式:

任务名        状态  优先级  剩余栈  编号
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,溢出时自动调用:

void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
    SEGGER_RTT_printf(0, "STACK OVERFLOW: %s\n", pcTaskName);
    __BKPT(0);
}

5.3 堆内存失败检测

已启用 configUSE_MALLOC_FAILED_HOOK,分配失败时自动调用:

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 OVERFLOWMALLOC 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->payloadARP 头 开始,而不是从 Ethernet 头开始
  3. 因此如果没有先执行:
pbuf_remove_header(p, SIZEOF_ETH_HDR)

则 ARP 包虽然被收到了,但会在 etharp_input() 的早期校验中被静默丢弃,最终表现为:

  1. Wireshark 能看到 PC 发来的 ARP request
  2. 板子侧底层收包计数在增长
  3. 但设备始终不回 ARP reply
  4. ping 也自然不会成功

当前应保留的正确处理方式如下:

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 okARP 帧计数在涨链路已 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 OVERFLOWMALLOC 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 关闭路径