简介:基于STM32Cube生成的STM32H743以太网通信TCP客户端工程代码,面向嵌入式开发者与物联网相关学习者,适用于快速搭建H743平台上的以太网TCP通信应用,也可作为毕业设计或项目原型的基础框架;工程重点解决CubeMX自动生成代码后未启用DCache而出现的数据一致性问题,并给出IAR环境下的完整实现。压缩包为7z格式,共1064个文件,其中507个c源文件与316个h头文件构成核心逻辑,icf与xcl链接配置负责IAR内存布局,ewp及ewd工程文件支持直接打开与调试,整体体积仅14.67MB;内部按HAL库、CMSIS、LWIP协议栈和用户应用分层组织,目录清晰,便于定位网络驱动、协议栈配置和业务代码。目前已有4239人学习下载。代码包含完整的TCP客户端框架、DCache维护逻辑及配套工程设置,结合作者博客中的调试步骤,可深入理解H743以太网外设初始化、LWIP协议栈移植和Cache一致性处理等关键环节;同时保留了外设库和启动文件,方便二次移植到同系列项目,也适合中高级工程师快速上手排查网络通信问题。
1. 先跑起来再说:STM32H743 的 TCP 客户端到底难在哪
如果你接手过一个需要把 H743 的数据发给服务器的项目,第一反应多半是信不过外部串口网关,想直接用片内 MAC 走以太网。STM32H743 的以太网外设配合 LwIP,确实能在 IAR 环境里跑出一个能连接 TCP 服务器的客户端,但真正动手后你会发现,难点不在"能连上",而在"怎么稳定地连上、断开后敢不敢重连、连续跑一周不重启"。我在某项目里曾用 H743 + LAN8720A + 裸板 IAR 工程实现一个上报网关,花了三个晚上才完成第一版客户端,然后又花了三周去处理复位后连不上、DMA 描述符耗尽、调试器一挂就死等问题。
这篇文章就按实际落地顺序讲:硬件与协议栈选型、IAR 工程搭建、LwIP 移植的关键参数、TCP 客户端代码怎么写、再到我踩过的坑和最终调优。新手能照着做出一个能跑的客户端,熟手可以重点看第 5 章的排查思路和第 6 章的窗口调优,不用看前面的基础部分。
2. H743 的以太网外设:先搞清楚 MAC、PHY 和时钟,再谈代码
2.1 为什么选 H743 而不是 H750 或 F407
H743 自带 10/100M 以太网 MAC,缺点是需要外接 PHY 芯片,但这本身就是工业板常见做法。相比 F407,H743 的 CPU 是 Cortex-M7,内部 AHB 总线带宽大,LwIP 做协议解析时不会太费劲。更重要的是 H743 的以太网 DMA 支持双缓冲描述符,配合大块 SRAM,不容易出现 F407 上那种"跑高频数据就丢包"的情况。
我一般选择 RMII 接口,因为只需要 7 根信号线:MCU 侧的 ETH_RMII_CLK、TX_EN、TXD[1:0]、RX_DV、RXD[1:0],以及 PHY 的中断和复位。RMII 要求 50MHz 参考时钟,这个时钟可以由外部 50MHz 有源晶振提供,也可以由 MCU 的 MCO 引脚输出。实际项目里我倾向于让 PHY 自带晶振,比如 LAN8720A 的 XXX 引脚接 50MHz 晶振,这样避免 MCO 引脚的负载问题。
2.2 PHY 芯片选型和地址识别
H743 的 ETH 驱动是通过 MDIO 总线访问 PHY 寄存器的。PHY 地址由外部引脚上下拉决定,常见 LAN8720A 默认地址是 0,KSZ8041 默认是 0,DP83848 默认是 0x01。如果你用的板子 PHY 地址不是 0,而 CubeMX 生成的 HAL 库里写死了 PHY_ADDRESS,那就会遇到"读 PHY 寄存器全部是 0xFFFF"的诡异现象。
我的做法是在 eth_drv 初始化前先做一次 PHY 地址扫描,代码很简单:
uint8_t phy_addr_scan(void) { int addr; for (addr = 0; addr < 32; addr++) { eth->PHYCAR |= (addr << ETH_PHYCAR_CR_ADDRESS_Pos); // 设置 PHY 地址 eth->PHYCR |= ETH_PHYCR_CR_WRITE; // 触发操作 uint32_t val = eth->PHYCR; // 读回状态 if ((val & ETH_PHYCR_CR_READ) == 0) { // 操作完成 return addr; } } return 0xFF; }逻辑说明:这段代码用寄存器操作快速扫描 0~31 的 PHY 地址,读到非全 F 的寄存器值就返回该地址。参数说明:ETH_PHYCAR_CR_ADDRESS_Pos是 PHY 地址段的偏移,ETH_PHYCR_CR_WRITE用于发起 MDIO 写操作,实际使用时为了保险还要读取 PHY 的 ID 寄存器做双重校验。如果返回 0xFF,先查 PHY 供电和复位引脚,别急着改软件。
2.3 时钟树:RMII 的 50MHz 必须干净
H743 有两个可能踩雷的点:一是 ETH_RMII_CLK 如果是 MCO2 输出的 PLL 分频,则 PLL 的配置会直接影响以太网;二是 ETH 模块时钟必须来自 PLL1Q 或 PLL2Q,且 CubeMX 生成的代码里默认可能没使能 ETH 的时钟门控。
我在某项目里遇到过 ping 不通,但 PHY 的 Link 灯是亮的。用示波器量 RMII 参考时钟,发现频率是 48.5MHz 而不是 50MHz。原因是我在 CubeMX 里只修改了主频到 480MHz,忽视了 ETH_RMII 时钟源选择。解决办法是回到 Clock Configuration 页面,把 Ethernet RMII 的时钟源明确选为 PLL1Q,并确认输出为 50MHz。不要指望 HAL 自动帮你做这件事,生成代码后要检查HAL_RCC_ETHCLKConfig()的实参。
3. 在 IAR 里搭一个能跑 LwIP 的 H743 工程:从 CubeMX 到 first ping
3.1 CubeMX 初始化 ETH 和 DMA,但入口要改
用 CubeMX 生成 H743 工程时,勾选 ETH 并选择 RMII,使能中断,然后生成代码。但我不建议直接用 CubeMX 生成的main()跑 LwIP,因为它的 While 循环里没有给 LwIP 足够的时间片。常见做法是:在 CubeMX 里只生成硬件初始化代码,LwIP 部分自己拉源码。
具体导入 IAR 的路径:先建一个 IAR 空工程,然后手动添加 HAL 源文件、LwIP 的core、netif、api目录,再写一个ethernetif.c。如果你不想自己写ethernetif.c,可以参照 CubeMX 生成的ethernetif.c版本,它是 1.4.0 风格的驱动文件。需要改的核心是low_level_init():
static void low_level_init(struct netif *netif) { ETH_DMADescTypeDef *dma_rx_desc; ETH_DMADescTypeDef *dma_tx_desc; HAL_StatusTypeDef hal_status; /* 设置 MAC 地址 */ netif->hwaddr[0] = 0x02; netif->hwaddr[1] = 0x80; netif->hwaddr[2] = 0xE1; netif->hwaddr[3] = 0x00; netif->hwaddr[4] = 0x00; netif->hwaddr[5] = 0x01; netif->hwaddr_len = 6; /* 分配 DMA 描述符 */ dma_rx_desc = (ETH_DMADescTypeDef *)eth_rx_desc; dma_tx_desc = (ETH_DMADescTypeDef *)eth_tx_desc; ETH_MACDMAConfigTypeDef mac_conf = {0}; mac_conf.Mode = ETH_MODE_FULL_DUPLEX; mac_conf.Speed = ETH_SPEED_100M; mac_conf.PhyAddress = PHY_ADDRESS; mac_conf.MII = ETH_RMII; hal_status = HAL_ETH_Init(&heth, &mac_conf); if (hal_status != HAL_OK) { /* 初始化失败处理 */ } for (int i = 0; i < ETH_RX_DESC_CNT; i++) { HAL_ETH_DescAssignBuffer(&heth, &dma_rx_desc[i], rx_buff[i], ETH_RX_BUF_SIZE); } HAL_ETH_Start_DMA(&heth, ETH_DMACatalog_RxFirst); }逻辑说明:这段代码完成了 MAC 地址填充、DMA 描述符分配和 ETH 外设启动。参数说明:PHY_ADDRESS要在main.h里定义为 0 或 1,ETH_RX_DESC_CNT我默认设 4,ETH_RX_BUF_SIZE设为 1536,MTU 一共 1514,加上 CRC 和描述符对齐,1536 比较稳。描述符数组要 4 字节对齐,IAR 里用__ALIGNED(4)放在 SRAM 段,或者直接开一个独立数组。
3.2 lwipopts.h 里必调的四个宏,决定客户端能不能稳定收发
LwIP 的lwipopts.h是移植的灵魂。裸机或加上 RTOS,宏开关完全不同。我用的是 FreeRTOS + LwIP,即LWIP_NETCONN和LWIP_SOCKET都开,TCP 客户端代码用 socket 接口。以下是我的关键配置:
#define MEM_SIZE (16 * 1024) #define MEMP_NUM_PBUF 16 #define PBUF_POOL_SIZE 32 #define PBUF_POOL_BUFSIZE 1512 #define TCP_MSS 1440 #define TCP_WND (4 * TCP_MSS) #define TCP_SND_BUF (4 * TCP_MSS) #define TCP_QUEUE_OOSEQ 8 #define LWIP_TCPIP_CORE_LOCKING 1 #define CHECKSUM_GEN_IP 1 #define CHECKSUM_GEN_UDP 1 #define CHECKSUM_GEN_TCP 1 #define CHECKSUM_CHECK_IP 0 #define CHECKSUM_CHECK_UDP 0 #define CHECKSUM_CHECK_TCP 0逻辑说明:MEM_SIZE是 LwIP 内部堆大小,影响内存池分配;TCP_WND是接收窗口,TCP_SND_BUF是发送缓冲,这两个宏直接决定 TCP 吞吐。参数说明:如果你的应用是每个请求几百字节的工控协议,这组配置足够;如果是满速 100Mbps 传输,TCP_WND得开到8 * TCP_MSS以上,H743 的 RAM 足够,不要省这几十 KB。CHECKSUM_CHECK_*设为 0 可以减小 CPU 开销,但会掩盖网络包损坏的问题,调试期建议开起来,稳定后再关。
3.3 IAR 的优化选项:Level 3 会让你多花两天
我在这块翻过一次车。代码用 IAR 默认的High - Balance (Level 3)优化,TCP 客户端连接后发一包数据就会 HardFault。最后怀疑是 LwIP 源码里的结构体被编译器重排或某种未定义行为。后来把优化等级改到Medium,问题消失。
如果你也遇到"优化开高就挂,关了就好",大概率是ethernetif.c里 DMA 描述符或缓冲区的对齐问题。IAR 的__ALIGNED(4)在某些优化等级下会失效,尤其是如果你用了局部变量作为 DMA 缓冲区。解决办法:把所有 DMA 描述符和缓冲区定义为全局数组,并用#pragma data_alignment=4明确指定:
#pragma data_alignment=4 ETH_DMADescTypeDef eth_rx_desc[ETH_RX_DESC_CNT]; #pragma data_alignment=4 uint8_t rx_buff[ETH_RX_DESC_CNT][ETH_RX_BUF_SIZE];逻辑说明:#pragma data_alignment=4是 IAR 的专用语法,比__ALIGNED(4)更可靠。参数说明:ETH_RX_DESC_CNT和ETH_TX_DESC_CNT最好定为 4 或 8,不要用 2。因为 H743 的 DMA 描述符是环形队列,如果数量太少,在高频收发时下一个描述符还没释放,数据就会丢失。
4. TCP 客户端核心实现:从 connect 到 send,再到断线检测
4.1 socket API 还不熟悉?先从 netconn 开始也行
LwIP 提供了两种上层接口:netconnAPI 和socketAPI。socket语义更接近 POSIX,适合从 Linux 转过来的人;netconn更贴近底层,少一层fd封装,性能略好。H743 跑客户端,我一般用 socket,因为调试时可以直接用标准思路。
创建 TCP 客户端需要四步:socket()、connect()、send()、recv()。但 LwIP 的 socket 在非阻塞模式下,recv()返回-1时需要用errno判断是EWOULDBLOCK还是真的错误。我写了一个简单的 TCP 客户端线程,配合 FreeRTOS 的延时做周期连接。
static void tcp_client_thread(void *arg) { int sock = -1; struct sockaddr_in server_addr; uint8_t tx_buf[1024]; uint8_t rx_buf[1024]; int ret, len; while (1) { if (sock < 0) { sock = lwip_socket(AF_INET, SOCK_STREAM, 0); if (sock >= 0) { server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8080); server_addr.sin_addr.s_addr = inet_addr("192.168.1.200"); ret = lwip_connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)); if (ret != 0) { lwip_close(sock); sock = -1; } } } if (sock >= 0) { /* 发送周期数据 */ snprintf((char *)tx_buf, sizeof(tx_buf), "seq=%u", seq++); len = lwip_send(sock, tx_buf, strlen((char *)tx_buf), 0); if (len <= 0) { lwip_close(sock); sock = -1; } /* 等服务器响应,超时 2 秒 */ struct timeval timeout = {2, 0}; lwip_setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); ret = lwip_recv(sock, rx_buf, sizeof(rx_buf), 0); if (ret == 0) { /* 服务器主动关闭 */ lwip_close(sock); sock = -1; } } vTaskDelay(pdMS_TO_TICKS(2000)); } }逻辑说明:这个线程在断线后每 2 秒尝试重建 socket 并可立即连接。send返回 0 或负值说明连接已失效,此时必须尽快 close 释放资源,否则 LwIP 的 socket 描述符池会被耗尽。参数说明:超时用SO_RCVTIMEO设置,注意 LwIP 的时间结构体timeval的tv_sec是秒,tv_usec是微秒,我把 2 秒写成了{2, 0}而不是{2, 0}后面再加微秒。如果服务器不主动发数据,这里的recv会阻塞直到超时或收到数据。
4.2 接入 FreeRTOS 后,LwIP 线程优先级怎么定
H743 有多个中断,LwIP 的 tcpip_thread 负责协议栈处理。如果直接把客户端线程放在与 tcpip_thread 相同的优先级,可能会出现优先级反转导致sys_timeout超时不准确。我的配置如下:
tcpip_thread优先级:tskIDLE_PRIORITY + 2tcp_client_thread优先级:tskIDLE_PRIORITY + 1- ETH 中断优先级:
5(FreeRTOS 中优先级数值越小越优先)
ETH 中断里只做netif->input()的调用,实际协议栈处理由tcpip_thread通过sys_mbox异步执行。不要在中断里直接解析 TCP 报文,否则中断耗时太长会上面描述符堆积、下面超时不准。我见过有同学在 ETH 中断里加打印,直接导致 PHY 的 RX_ER 拉高,网络直接瘫痪。
4.3 连接后如何可靠地监测断线
TCP 是无边界的字节流,所谓断线,往往要等 TCP 栈超时重传几次才会被recv返回 0。如果服务器断电,客户端大约要 10~30 秒才能感知。最常用的办法是应用层心跳:每隔 1 秒发一个只含校验字节的小包,服务器回一个ack,连续 3 个心跳无回应就主动 close。下面的代码放在 4.1 线程里作为心跳检测的补充:
static uint8_t lost_count = 0; ... if (sock >= 0) { ret = lwip_send(sock, (void *)"HB", 2, MSG_DONTWAIT); if (ret == 2) { lost_count = 0; } else { lost_count++; if (lost_count >= 3) { lwip_close(sock); sock = -1; } } }逻辑说明:这里用了MSG_DONTWAIT标志,避免send阻塞在线程里,保证心跳间隔稳定。参数说明:lost_count是静态变量,最好放在线程上下文结构体中,不要用全局变量的形式,否则多实例扩展会出麻烦。
5. 避坑指南:H743 + IAR + LwIP 的 5 个高频翻车点
5.1 连接后立刻断开,recv 返回 0
现象:客户端connect成功,但没几秒recv返回 0,连接被关闭。原因:服务器发送 SYN-ACK 后,客户端 LwIP 回应的 ACK 没有正确发出。问题多出在 DMA 描述符数量和TCP_SND_BUF太小,导致 TCP 窗口为 0。
解决:把TCP_WND和TCP_SND_BUF至少调到4 * TCP_MSS,并检查PBUF_POOL_SIZE是否足够。再不行在tcpip_thread任务栈里把栈空间加大到 2048 字节,LwIP 在调用tcp_parseopt时需要较多栈空间。
5.2 板子上电后必须复位才能 ping 通 DHCP
现象:DHCP 客户端拿不到 IP,但是按住复位键重新上电后偶尔又能获得 IP。原因:PHY 芯片上电后需要几百毫秒完成自协商,而 CPU 跑得太快,LwIP 的dhcp_start在 PHY 还没 ready 时发 DHCP Discover,网线虽然 Link 已经亮,但 PHY 内部的寄存器还没完全就绪。
解决:在netif_set_up之前,先轮询 PHY BSR 寄存器的自动协商完成位,或者直接延时 300ms。我用的轮询函数:
static void phy_wait_ready(ETH_HandleTypeDef *heth, uint8_t phy_addr) { uint32_t status = 0; for (int i = 0; i < 100; i++) { HAL_ETH_ReadPHYRegister(heth, phy_addr, 0x01, &status); if (status & 0x24) { // Link status + autoneg complete break; } HAL_Delay(10); } }逻辑说明:0x01 是 PHY 的基本状态寄存器,bit 2 表示自动协商完成,bit 5 表示链路已连接。参数说明:循环 100 次,每次延时 10ms,最长等 1 秒,覆盖大多数 PHY 的启动时间。注意这里的PHY_ADDRESS要和实际板子一致。
5.3 IAR 中把 lwip 源文件加入编译后报大量警告
现象:编译 LwIP 源码时 IAR 报了几百个关于enum和int转换的警告。原因:LwIP 的err_t和标准库类型在 IAR 默认的 C99 模式下不一致,或者是_STATIC_INLINE宏定义冲突。
解决:在 IAR 工程的编译器选项里,把语言标准设为C11而不是默认的C99,并添加D_LWIP_OPEN_SRC宏。同时把Diagnostics里的Pa082警告关掉。这不是掩盖问题,而是 LwIP 本身用了一些未定义行为,IAR 默认比较严格,生产工程里同志们也不会去改第三方源码的警告。
5.4 连续发送 1MB 后死机,HardFault_Handler
现象:客户端持续向服务器大块发送数据,跑几分钟后进入 HardFault。原因:DMA 描述符没能及时回收,或者tx_buf指向了被覆盖的内存。这是 LwIP 最经典的翻车点:lwip_send返回成功,但数据其实还在 DMA 缓冲中,这时你马上修改源缓冲区,就会产生开放的内存竞争。
解决:使用零拷贝发送时必须确保发送缓冲区在send返回后至少还有一次完整 DMA 周期的时间。最简单的方式是让send走内部缓冲,即把TCP_SND_BUF设为2 * TCP_MSS,并确保传入lwip_send的缓冲区尺度大于 MSS。更稳妥是封装一个带互斥锁的发送队列,发送线程把数据拷贝到专用缓冲区后再调用lwip_send。别想着节省那几次拷贝,H743 跑 100Mbps 完全够用。
5.5 服务器主动断开,客户端 5 分钟后才意识到
现象:服务器重启,客户端lwip_send一直不返回错误,直到 tcpip_thread 超时重传耗尽。原因:LwIP 默认 TCP 超时是 2 个 RTO 周期,RTO 初始 1 秒,指数退避后可以拖到几十秒,再加上连接建立后的 keepalive 默认不开启。
解决:开启 LwIP 的 TCP keepalive,并在 socket 上设置空闲时间:
int keepalive = 1; int keepidle = 5; // 5 秒无数据后发探测包 int keepintvl = 3; // 探测失败后 3 秒重发 lwip_setsockopt(sock, IPPROTO_TCP, SO_KEEPALIVE, &keepalive, sizeof(keepalive)); lwip_setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle)); lwip_setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl));逻辑说明:这三个 socket 选项在 LwIP 的LWIP_TCP_KEEPALIVE开启时才有效。参数说明:TCP_KEEPIDLE单位是秒,TCP_KEEPINTVL也是秒,我这里设置的探测间隔偏短,生产环境可以放宽到 30 秒。启用 keepalive 后,服务器异常断电,客户端最长 5+3*2=11 秒能感知断线,对工控协议来说够了。
6. 进阶技巧:零窗口、满速收发和断线重连的策略
6.1 用tcp_info监控发送队列,别让send成为玄学
LwIP 的lwip_send如果返回长度小于请求长度,很多人以为只是部分发送成功,实际上 LwIP 在阻塞模式下会尝试发送完整缓冲区,如果因为窗口太小就会等待。这个等待会让你的客户端线程卡死。解决办法是发送前检查 TCP 控制块:
struct tcp_pcb *pcb = tcp_active_pcbs; // 这里只是示意,实际要遍历更实际的做法是用非阻塞 socket + select 或poll。我一般把发送逻辑改成:
int poll_send(int sock, const uint8_t *data, int len, int timeout_ms) { int offset = 0; int ret; struct timeval tv = {0, timeout_ms / 1000}; while (offset < len) { ret = lwip_send(sock, data + offset, len - offset, MSG_DONTWAIT); if (ret > 0) { offset += ret; } else if (ret == 0) { break; // 连接关闭 } else { if (errno == EWOULDBLOCK) { lwip_select(0, NULL, NULL, NULL, &tv); // 等待可写 continue; } else { return -1; } } } return offset; }逻辑说明:这个函数用非阻塞发送+select 等待,保证在窗口不足时不会死等,而是让出 CPU 给协议栈。参数说明:timeout_ms参数控制总超时,超过后返回已发送字节数。注意lwip_select只接受fd_set,我这里不能直接调用,实际工程里应该用lwip_select配合可写描述符集合,或者用lwip_setsockopt的SO_SNDTIMEO更简单。
6.2 调整窗口和缓存让吞吐从 3Mbps 提到 80Mbps
H743 跑 100M 以太网,如果不用 RTOS 中断收发,裸机循环里 LwIP 的吞吐能到 50Mbps 左右,但客户端模式下服务器主动 push 数据时,吞吐会下降。最常见的原因有三个:TCP_WND小于带宽时延积,PBUF_POOL_BUFSIZE太小导致承载分片,以及tcpip_thread栈过小。
我最终在某个项目中把参数调成这样:
#define TCP_MSS 1440 #define TCP_WND (32 * TCP_MSS) // 46KB #define TCP_SND_BUF (32 * TCP_MSS) // 46KB #define TCP_QUEUE_OOSEQ 16 #define PBUF_POOL_BUFSIZE 1536 #define LWIP_FC 0 // 不启用软件流控 #define LWIP_TCP_ACK_DELAY_TIME 40 // 40ms 延迟 ACK调整后,局域网内从服务器到 H743 的 TCP 下载速度稳定在 70Mbps,客户端上报小包时因为延迟 ACK,吞吐反而更高。注意TCP_WND超过 30KB 时,H743 的内存占用会明显增加,需要确认MEM_SIZE是否足够。我遇到过把TCP_WND调到 50KB 后 LwIP 内存池分配失败导致断言,最终把MEM_SIZE提高到 64KB 才解决。
6.3 断线重连的完整状态机
简单地在循环里connect不叫重连。要处理好"重连次数限制"、"重连时释放旧 socket"、"重连前是否需要重新初始化 PHY"。有些 PHY 在异常断开后,内部状态机混乱,需要复位引脚拉低再拉高。我在生产代码里用的策略:
- 连续重连 5 次不成功,进入 30 秒长延时;
- 如果 PHY Link 丢失(
HAL_ETH_ReadPHYRegister读 Link bit 为 0),先复位 PHY,延时 200ms,再继续; - 每次重连前强制
lwip_close旧 socket,并检查netif_is_up,如果netif还没 ready,先lwip_netif_set_up。
以下是状态机核心:
typedef enum { TC_IDLE, TC_CONNECTING, TC_RUNNING, TC_WAIT_RETRY, TC_PHY_RESET } tcp_state_t; static void handle_tcp_state_machine(void) { switch (tcp_state) { case TC_IDLE: tcp_state = TC_CONNECTING; break; case TC_CONNECTING: if (connect_socket() == 0) { tcp_state = TC_RUNNING; } else { retry_count++; tcp_state = (retry_count >= 5) ? TC_PHY_RESET : TC_WAIT_RETRY; } break; case TC_RUNNING: if (!is_connected()) { close_socket(); retry_count = 0; tcp_state = TC_CONNECTING; } break; case TC_WAIT_RETRY: vTaskDelay(pdMS_TO_TICKS(1000)); tcp_state = TC_CONNECTING; break; case TC_PHY_RESET: phy_reset(); vTaskDelay(pdMS_TO_TICKS(300)); retry_count = 0; tcp_state = TC_CONNECTING; break; } }逻辑说明:这个状态机把连接过程从简单的阻塞函数拆成可调度的步骤,方便在每个状态下插入日志。参数说明:retry_count是全局静态变量,每次TC_PHY_RESET后清零,避免 PHY 复位后连续快速重连。is_connected()内部通过recv(MSG_PEEK)或 TCP 控制块的state == ESTABLISHED判断,不要在中断里调用。
最后说一个我自己的习惯:每次改完网络相关参数,我都会先关掉 IAR 的优化编译一次,跑通基本收发,再逐步打开优化并观察三天。LwIP 本身是个黑匣子,但 H743 的调试跟踪功能还算好用,遇到 HardFault 时先看是"硬件异常"还是"内存非法访问",再决定往 DMA 还是往协议栈方向查。希望这些踩坑记录能帮你少熬几个夜,也希望 H743 的 TCP 客户端不是你的玄学项目,而是能稳定上报的生产代码。
本文还有配套的精品资源,点击获取