STM32H7 长时间运行后 HTTP 断连:从“串口还活着”到 TX 描述符错账

摘要

一块 STM32H743 控制板在重新上电或下载固件后可以正常连接上位机,但连续运行数小时后会突然停止发起 HTTP 请求。串口仍能看到 FreeRTOS 心跳,COM3 也未被其他进程占用,因此问题一度看起来像"网络服务断了"。

通过网卡原始抓包、串口日志和 ST-Link 在线内存检查,最终发现故障时 CPU 和主调度仍在运行,但 LwIP 的 tcpip_thread 卡在以太网发送等待中;ETH HAL 的 TX 描述符指针与 BuffersInUse 计数已经不一致。发送驱动在 HAL_ETH_Transmit_IT() 返回忙时无限重试,使调用者一直持有 LwIP 核心锁。修复为串行化 TX 描述符提交与回收,并限制忙状态重试时间。重新构建、下载和 Flash 校验后,板端恢复周期性请求,15 秒抓包中观察到 42 个 HTTP 请求/响应,全部成功。

故障现象

设备正常启动后能连接 192.168.137.177:8000,按周期请求上位机。经过数小时运行后,上位机不再看到新请求。用户确认 COM3 没有被其他程序占用;串口仍持续输出 RTOS 心跳。这说明 MCU 没有整体停机,但还不能据此断定网络任务正常。

故障时的串口心跳节选:

复制代码
[rtos] heartbeat=11880 tick=11881000 ... app_running=0 generation=4
[rtos] heartbeat=11881 tick=11882000 ... app_running=0 generation=4
...
[rtos] heartbeat=11899 tick=11900000 ... app_running=0 generation=4

同一故障窗口的以太网采集文件只有 524 字节,基本只有 pcapng 文件头,没有板端 IP 或 ARP 报文:故障窗口空抓包。因此当时板子确实没有把新的网络帧送到物理网卡。需要注意,串口心跳是另一个任务的活动证明,不能证明 tcpip_thread 仍在处理网络工作。

先用抓包确定故障停在哪一层

更早的一段持续抓包在 16:10:22 捕获到板子向 192.168.137.177:8000 发起 TCP SYN;上位机返回 SYN-ACK,并在约 1、2、4 秒后重传,但板端没有 ACK,握手没有完成:早期握手失败抓包。这与"上位机没有监听"不同:服务端已经收到 SYN 并作出响应,但后续数据链路没有继续。

抓包回答了两个重要问题:

  1. 有时板端仍尝试建立连接,但无法完成 TCP 握手。

  2. 到最终故障窗口时,连新的 SYN、ARP 等板端报文都看不到。

故障因此不能只归结为 HTTP JSON 格式或 COM 口问题。HTTP 请求尚未发出之前,网络接口的发送/处理路径已经停止前进。

在线调试把范围缩到 ETH TX 描述符

故障现场通过 ST-Link 在线检查 FreeRTOS 任务、队列和 ETH HAL 状态。关键观察如下:

• RTOS 心跳任务继续运行;问题集中在网络处理链路。

• tcpip_thread 阻塞在 TxPktSemaphore 等待路径,并持有 LwIP 核心互斥锁。

• HttpWave 等待 LwIP 核心互斥锁,TCP/IP 邮箱已满(6/6)。

• ETH HAL 状态显示 CurTxDesc=2,对应描述符的 PacketAddress2 仍非空,地址为 0x3000814c;DMA 的 OWN 位已经清零,但 BuffersInUse=0,HAL 同时返回 BUSY。

最后一组状态是根因定位的关键。HAL 在准备 TX 描述符时,不只检查 DMA OWN 位,也检查 PacketAddressidx 是否为空:

复制代码
/* Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal_eth.c */
if ((READ_BIT(dmatxdesc->DESC3, ETH_DMATXNDESCWBF_OWN) ==
     ETH_DMATXNDESCWBF_OWN)
    || (dmatxdesclist->PacketAddress[descidx] != NULL))
{
  return HAL_ETH_ERROR_BUSY;
}

而 HAL_ETH_ReleaseTxPacket() 的主循环受 BuffersInUse 控制:

复制代码
uint32_t numOfBuf = dmatxdesclist->BuffersInUse;
uint32_t idx = dmatxdesclist->releaseIndex;

while ((numOfBuf != 0U) && (pktTxStatus != 0U))
{
  /* 检查完成状态,释放 pbuf,再清空 PacketAddress 并更新计数 */
}

当计数已经变成 0,而对应 PacketAddress 仍非空时,正常回收循环不会再检查该项;下一次发送又会因为非空指针返回 BUSY。它形成了"回收看不到、发送不能复用"的死锁状态。HAL 的 HAL_ETH_TxFreeCallback() 在本工程中已经实现并调用 pbuf_free(),现场没有证据指向单纯的 pbuf 引用泄漏。

TX 描述符提交和回收分别发生在网络发送路径与以太网输入任务。此前两处都可能调用 HAL 的 TX 管理函数,却没有共用互斥保护。结合在线快照中的指针/计数不一致,可以判断 HAL 的共享描述符账本可能被并发访问破坏。这里的"并发破坏"是由内存状态和调用路径推得的根因,不是单靠 pcap 能直接证明的结论。

原代码为什么会把局部异常放大成全网断连

修复前的发送循环在 BUSY 后等待信号量、回收描述符,再无限重试。下面保留关键控制流,省略了非 BUSY 错误分支:

复制代码
do
{
  if (HAL_ETH_Transmit_IT(&heth, &tx_config) == HAL_OK)
  {
    errval = ERR_OK;
  }
  else
  {
    if (HAL_ETH_GetError(&heth) & HAL_ETH_ERROR_BUSY)
    {
      osSemaphoreAcquire(TxPktSemaphore, ETHIF_TX_TIMEOUT);
      HAL_ETH_ReleaseTxPacket(&heth);
      errval = ERR_BUF;
    }
    else
    {
      /* 其他错误:释放 pbuf 引用并返回 ERR_IF */
      errval = ERR_IF;
    }
  }
} while (errval == ERR_BUF);

这段代码有两个风险:

  1. TX 状态损坏时,等待超时也不会退出,循环会永久重试。

  2. low_level_output() 在 LwIP 核心上下文中执行;它一直不返回,就会持续占用核心锁。其他 HTTP/TCP 操作无法进入核心,TCP/IP 邮箱逐渐填满,最终板子连新的 SYN 和 ARP 都发不出来。

因此外部看到的是"几小时后所有连接突然消失",而内部起点是一个 TX 描述符无法复用的状态。

修复:串行访问,并为重试设置上限

在 ethernetif.c 中新增 TxPktMutex,由发送路径和以太网输入任务共同持有。发送前先回收已完成帧,再提交新帧;接收任务也必须在同一互斥锁内回收 TX 描述符:

复制代码
/* low_level_output() */
(void)HAL_ETH_ReleaseTxPacket(&heth);
tx_status = HAL_ETH_Transmit_IT(&heth, &tx_config);
(void)osMutexRelease(TxPktMutex);

/* ethernetif_input() */
if ((TxPktMutex != NULL) &&
    (osMutexAcquire(TxPktMutex, osWaitForever) == osOK))
{
  (void)HAL_ETH_ReleaseTxPacket(&heth);
  (void)osMutexRelease(TxPktMutex);
}

同时,BUSY 等待时间设为 100 ms,最多尝试 3 次。失败时释放之前为 DMA 持有而增加的 pbuf 引用,并将错误返回给 LwIP:

复制代码
#define ETHIF_TX_TIMEOUT (100U)
#define ETHIF_TX_RETRY_COUNT (3U)

for (attempt = 0U; attempt < ETHIF_TX_RETRY_COUNT; attempt++)
{
  /* 加锁、回收完成项、提交 TX,然后释放锁 */
  if (tx_status == HAL_OK)
  {
    return ERR_OK;
  }

  if (tx_busy == 0U)
  {
    errval = ERR_IF;
    break;
  }

  if ((attempt + 1U) < ETHIF_TX_RETRY_COUNT)
  {
    (void)osSemaphoreAcquire(TxPktSemaphore, ETHIF_TX_TIMEOUT);
  }
}

/* HAL 未接管本次失败发送的引用。 */
pbuf_free(p);
return errval;

互斥锁保护的是 HAL 的共享描述符状态,不会在等待发送完成期间一直持有;重试有明确上限,因此异常情况下 LwIP 核心锁也能释放。

相关推荐
Lxx_Zw1 小时前
串口命令如何从字符串长成协议:帧头、长度、校验和与状态机
c语言·stm32·单片机·嵌入式硬件
凯基迪科技2 小时前
瑞萨R5F100解密 R8C/RL78 MCU 固件读取与保护机制详解
服务器·网络·单片机
PMAOS2 小时前
4G 功能机操作系统选型指南:KaiOS、Mocor、MAUI、PMAOS 的能力边界与验证方法
嵌入式硬件·ai·操作系统
mk0153 小时前
单片机同步降压稳压器输出最低1V,关断时损耗3uA
单片机·嵌入式硬件·硬件工程·智能硬件·pcb工艺
然后,是第八天4 小时前
platformio使用使用pioarduino流程
单片机
糖糖单片机设计4 小时前
基于 STM32 + LU-ASR01 的智能台灯
stm32·单片机·嵌入式硬件·51单片机·语音识别
en.en..4 小时前
Keil 文件管理避坑(基于 STM32F4 HAL 通用模板讲解)
单片机·嵌入式硬件
女神下凡7 小时前
芯参谋(87):Linux 系统(SPI NOR)驱动开发指南
linux·运维·arm开发·驱动开发·嵌入式硬件·设计规范
起个网名真难~17 小时前
STM32F103_Bootloader_从零到UART升级成功_博客原稿
stm32·单片机·嵌入式硬件