好的,您提供的代码是基于正点原子 STM32F407 开发板,使用 UCOSII 操作系统和 LwIP 网络协议栈(netconn API)实现的 TCP 服务器。当前遇到的核心问题是网线热插拔后无法自动重新联网。 为了实现网线热插拔后的自动重连,我们需要关注以下几个关键点: 物理层链路状态检测 (PHY Status Check):需要能够检测到网线是连接上了还是断开了。 网络接口 (netif) 管理:当链路状态改变时,需要通知 LwIP 更新网络接口的状态,并相应地处理 IP 地址的获取(例如通过 DHCP)。 TCP 服务器的健壮性:服务器需要能够处理因链路断开导致的现有连接异常,并在网络恢复后能继续接受新的连接。
下面是如何修改和添加代码来实现这一功能的思路和步骤: 一、 实现 PHY 链路状态检测及 LwIP 通知 这通常不是在 TCP 服务器应用层代码中直接完成的,而是需要在 LwIP 的网络接口层(通常是 ethernetif.c 或类似文件)或者一个专门的监控任务中实现。 创建 PHY 状态监控任务 (如果 LwIP 驱动中没有内置此功能): 这个任务会周期性地通过 SMI/MII 总线读取 LAN8720 PHY 芯片的状态寄存器,以判断链路是否连接。LAN8720 的 Basic Status Register (BSR) 中的 "Link Status" 位可以指示链路状态。 当检测到链路状态从“断开”变为“连接”时,调用 netif_set_link_up(struct netif *netif)。 当检测到链路状态从“连接”变为“断开”时,调用 netif_set_link_down(struct netif *netif)。
示例代码结构 (需要您根据实际的 STM32 HAL/LL 库和 PHY 驱动进行调整):
假设您有一个全局的网络接口变量,例如 extern struct netif gnetif; (通常在 lwip_comm.c 或 main.c 中定义和初始化)。 // 在一个新的 .c 文件或者一个合适的地方添加,例如 phy_monitor.c#include "lwip/opt.h"#include "lwip/netif.h"#include "lan8720.h" // 假设您有访问LAN8720寄存器的函数#include "lwip_comm.h" // 为了 gnetif (如果在这里声明)#include "os_cpu.h" // For OS_CPU_SR#include "ucos_ii.h" // For UCOSII API// 假设 gnetif 在其他地方定义extern struct netif gnetif;#define PHY_LINK_CHECK_INTERVAL_MS 1000 // 每1秒检查一次#define PHY_LINK_TASK_PRIO (TCPSERVER_PRIO + 1) // 确保优先级合适#define PHY_LINK_TASK_STK_SIZE 128OS_STK PhyLinkTaskStk[PHY_LINK_TASK_STK_SIZE];static u8_t last_link_state = 0; // 0 = down, 1 = up// 您需要实现这个函数来读取LAN8720的链路状态// 返回值: 1 表示链路UP, 0 表示链路DOWNu8_t LAN8720_ReadLinkStatus(void) { // 示例:具体实现依赖于您的ETH HAL驱动和PHY访问函数 // uint32_t phy_reg_val; // if (HAL_ETH_ReadPHYRegister(&heth, LAN8720_PHY_ADDRESS, PHY_BSR, &phy_reg_val) == HAL_OK) { // if (phy_reg_val & PHY_LINKED_STATUS) { // PHY_BSR_LS_BIT or similar macro // return 1; // Link is Up // } // } // return 0; // Link is Down // 这是一个占位符,您需要用实际的PHY读取逻辑替换它 // 例如,很多STM32的LwIP移植会在ethernetif.c中有一个类似的函数 // 或者直接通过ETH_ReadPHYRegister uint16_t bsr_val; // 伪代码: your_read_phy_register_function(LAN8720_PHY_ADDRESS, 1, &bsr_val); // 1 is BSR address // if (bsr_val & (1 << 2)) return 1; // Bit 2 is Link Status // else return 0; return ethernetif_link_status(&gnetif); // 如果您的驱动有类似函数}void phy_link_check_thread(void *arg) { LWIP_UNUSED_ARG(arg); u8_t current_link_state; // 确保 gnetif 已经初始化 while(gnetif.hwaddr_len == 0) { // 简单等待netif被添加 OSTimeDlyHMSM(0, 0, 0, 100); } last_link_state = LAN8720_ReadLinkStatus(); if(last_link_state) { netif_set_link_up(&gnetif); } else { netif_set_link_down(&gnetif); } while (1) { current_link_state = LAN8720_ReadLinkStatus(); if (current_link_state != last_link_state) { if (current_link_state) { // Link transitions to UP printf("Network cable connected.\r\n"); netif_set_link_up(&gnetif); // 通知LwIP链路已连接 // LwIP的netif status callback通常会处理DHCP重启等 } else { // Link transitions to DOWN printf("Network cable disconnected.\r\n"); netif_set_link_down(&gnetif); // 通知LwIP链路已断开 } last_link_state = current_link_state; } OSTimeDlyHMSM(0, 0, 0, PHY_LINK_CHECK_INTERVAL_MS); }}void phy_link_task_init(void) { INT8U err; err = OSTaskCreate(phy_link_check_thread, (void *)0, &PhyLinkTaskStk[PHY_LINK_TASK_STK_SIZE - 1], PHY_LINK_TASK_PRIO); if (err != OS_ERR_NONE) { printf("Failed to create PHY Link Check Task\r\n"); }}``` **初始化**:在您的系统初始化代码中(例如 `main` 函数,LwIP 初始化之后),调用 `phy_link_task_init();`。
LwIP 网络接口回调 (Netif Status Callback):
LwIP 允许为网络接口注册一个状态改变回调函数。当 IP 地址改变、接口 UP/DOWN 时,这个回调会被调用。确保这个回调函数能够正确处理: 当链路恢复 (netif_set_link_up 被调用后) 且接口配置为使用 DHCP 时,它应该重新启动 DHCP 客户端以获取新的 IP 地址。 许多正点原子的 LwIP 移植例程中,ethernetif_init() 函数内部或者 netif_add() 时设置的 netif->status_callback (例如 netif_set_status_callback(&gnetif, lwip_netif_status_callback);) 会处理这个逻辑。当 netif_is_link_up() 时,如果 DHCP 使能,会调用 dhcp_start() 或类似的函数。
您需要检查您的 ethernetif.c (或类似的网络接口驱动文件) 和 LwIP 的初始化过程,确保: netif_set_link_up(&gnetif) 会触发 netif_set_up(&gnetif)(如果接口之前是 down 的)。 如果使用 DHCP (LWIP_DHCP 为 1),dhcp_start(&gnetif) 会在链路和接口都 UP 之后被调用。 netif_set_link_down(&gnetif) 会导致 netif_set_down(&gnetif) 被调用,并且 DHCP 客户端(如果运行)会被停止 (dhcp_stop(&gnetif) 或 dhcp_release_and_stop(&gnetif))。
二、 增强 TCP 服务器代码的健壮性 您的 TCP 服务器代码需要能够优雅地处理因网络断开导致的连接错误,并在网络恢复后能继续工作。 // tcp_server_demo.c (您的代码)// ... (includes 和其他定义保持不变) ...// 假设 gnetif 在其他地方定义,并且是全局可访问的extern struct netif gnetif; //tcp服务器任务static void tcp_server_thread(void *arg){ // OS_CPU_SR cpu_sr; // 在UCOSII中,OS_ENTER_CRITICAL/OS_EXIT_CRITICAL宏内部处理cpu_sr u32 data_len = 0; struct pbuf *q; err_t err,recv_err; u8 remot_addr[4]; struct netconn *conn, *newconn; static ip_addr_t ipaddr; // 保持 static 以便在函数调用间持久 static u16_t port; // 保持 static LWIP_UNUSED_ARG(arg); // 创建主监听连接 conn = netconn_new(NETCONN_TCP); if (conn == NULL) { printf("TCP Server: Failed to create new netconn (conn).\r\n"); return; // 任务无法继续 } err = netconn_bind(conn, IP_ADDR_ANY, TCP_SERVER_PORT); if (err != ERR_OK) { printf("TCP Server: Failed to bind netconn (conn), err=%d.\r\n", err); netconn_delete(conn); return; // 任务无法继续 } netconn_listen(conn); // 进入监听模式 // conn->recv_timeout = 10; // 设置监听conn的超时会导致accept非阻塞。 // 如果希望accept阻塞直到有连接,或者有较长超时,可以修改或移除这里。 // 如果移除,accept将阻塞。如果设置为0,也会阻塞。 // 短超时会导致accept频繁返回ERR_TIMEOUT,增加CPU消耗。 // 对于监听socket,通常建议阻塞或长超时。 // 假设我们暂时保持,但需注意其影响。 while (1) // 主循环,接受新连接 { // 检查网络接口和链路状态,如果网络未就绪,则延迟并重试 accept // 这是为了避免在网络故障时 netconn_accept 持续快速返回错误或超时 while (!netif_is_up(&gnetif) || !netif_is_link_up(&gnetif) || ip_addr_isany(netif_ip4_addr(&gnetif))) { // printf("TCP Server: Waiting for network to be ready...\r\n"); // 如果 conn 设置了短超时,这里可以不用 OSTimeDly,否则accept会阻塞 // 如果 conn 是阻塞 accept,这里需要一个延时来避免在网络恢复前卡死 OSTimeDlyHMSM(0, 0, 1, 0); // 等待1秒 } err = netconn_accept(conn, &newconn); // 接收连接请求 if (err == ERR_OK) { // newconn->recv_timeout = 10; // 设置已连接socket的接收超时,10ms可能太短,根据需要调整 // 例如设置为1000 (1秒) 或根据应用需求 newconn->recv_timeout = 1000; // 改为1秒超时,以便在无数据时不会卡死,也能检测到连接问题 netconn_getaddr(newconn, &ipaddr, &port, 0); //获取远端IP地址和端口号 remot_addr[3] = (u8_t)(ipaddr.addr >> 24); remot_addr[2] = (u8_t)(ipaddr.addr >> 16); remot_addr[1] = (u8_t)(ipaddr.addr >> 8); remot_addr[0] = (u8_t)(ipaddr.addr); printf("Host %d.%d.%d.%d:%d connected to server.\r\n", remot_addr[0], remot_addr[1], remot_addr[2], remot_addr[3], port); while(1) // 处理单个客户端连接的循环 { // 检查链路状态,如果链路断开,主动关闭这个 newconn if (!netif_is_link_up(&gnetif)) { printf("Link down, closing connection with %d.%d.%d.%d:%d\r\n", remot_addr[0], remot_addr[1], remot_addr[2], remot_addr[3], port); // 后续的 netconn_recv 或 netconn_write 应该会失败 // 或者直接关闭 // netconn_close(newconn); // 会在下面recv_err处理中覆盖 // netconn_delete(newconn); // break; // 跳出对此客户端的处理 } if((tcp_server_flag & LWIP_SEND_DATA) == LWIP_SEND_DATA) //有数据要发送 { // 发送前也检查一下链路 if (netif_is_link_up(&gnetif)) { err_t write_err = netconn_write(newconn ,tcp_server_sendbuf,strlen((char*)tcp_server_sendbuf),NETCONN_COPY); if(write_err != ERR_OK) { printf("Send failed, err: %d. Closing connection with %d.%d.%d.%d:%d\r\n", write_err, remot_addr[0], remot_addr[1], remot_addr[2], remot_addr[3], port); // 发送失败通常意味着连接已损坏 netconn_close(newconn); // 尝试关闭 netconn_delete(newconn); // 删除 tcp_server_flag &= ~LWIP_SEND_DATA; // 清除标志 goto next_client; // 跳到外层循环等待下一个客户端 } } else { printf("Link down, cannot send data.\r\n"); // 可以选择清除发送标志或等待链路恢复 } tcp_server_flag &= ~LWIP_SEND_DATA; } recv_err = netconn_recv(newconn, &recvbuf); //接收到数据 if(recv_err == ERR_OK) { OS_ENTER_CRITICAL(); memset(tcp_server_recvbuf,0,TCP_SERVER_RX_BUFSIZE); data_len = 0; // 每次接收前重置 data_len for(q=recvbuf->p;q!=NULL;q=q->next) { if(q->len > (TCP_SERVER_RX_BUFSIZE-data_len)) { memcpy(tcp_server_recvbuf+data_len,q->payload,(TCP_SERVER_RX_BUFSIZE-data_len)); data_len = TCP_SERVER_RX_BUFSIZE; // 已满 } else { memcpy(tcp_server_recvbuf+data_len,q->payload,q->len); data_len += q->len; } if(data_len >= TCP_SERVER_RX_BUFSIZE) break; } OS_EXIT_CRITICAL(); // data_len=0; // 这行是错误的,data_len是本次接收到的总长度,不应在这里清零 if (data_len > 0) { // 确保真的收到了数据再打印 printf("Received from %d.%d.%d.%d:%d: %s\r\n", remot_addr[0], remot_addr[1], remot_addr[2], remot_addr[3], port, tcp_server_recvbuf); } netbuf_delete(recvbuf); // 释放接收到的pbuf } else if (recv_err == ERR_TIMEOUT) { // 接收超时是正常的,因为设置了 newconn->recv_timeout // 在这里可以检查应用层的心跳或者其他逻辑 // 如果长时间超时且无数据,客户端可能已掉线,但TCP本身未断开 // 或者,如果此时链路已断开,则应关闭连接 if (!netif_is_link_up(&gnetif)) { printf("Link down during recv timeout. Closing connection with %d.%d.%d.%d:%d\r\n", remot_addr[0], remot_addr[1], remot_addr[2], remot_addr[3], port); netconn_close(newconn); netconn_delete(newconn); goto next_client; // 或 break; } } else // 其他错误 (ERR_CLSD, ERR_RST, ERR_ABRT, ERR_CONN等) 都表示连接问题 { if (recv_err == ERR_CLSD) { printf("Host %d.%d.%d.%d:%d disconnected (closed by remote).\r\n",remot_addr[0], remot_addr[1],remot_addr[2],remot_addr[3], port); } else { printf("Receive error (err: %d) on connection with %d.%d.%d.%d:%d. Closing.\r\n", recv_err, remot_addr[0], remot_addr[1], remot_addr[2], remot_addr[3], port); } netconn_close(newconn); netconn_delete(newconn); goto next_client; // 或 break; } } // end inner while for client handling next_client:; // 标签用于跳出内层循环 } else if (err == ERR_TIMEOUT) { // netconn_accept 超时,如果 conn->recv_timeout 设置了。 // 这是正常的,表示在超时期间内没有新连接。 // 可以检查一下全局状态或简单地继续循环。 // printf("TCP Server: No new connection within timeout.\r\n"); // 在这里可以短暂延时,避免CPU空转太快,如果 accept 超时很短 if (conn->recv_timeout > 0 && conn->recv_timeout < 100) { // 如果超时非常短 OSTimeDlyHMSM(0,0,0,50); // 短暂延时 } } else // netconn_accept 返回其他错误 { printf("TCP Server: netconn_accept failed with err = %d.\r\n", err); // 如果accept失败,可能监听socket本身出了问题,或者LwIP栈状态有问题 // 此时可以尝试延时后重试,或者更极端地,重新创建监听 conn // 如果错误是 ERR_MEM 之类的,那问题更严重 if (err == ERR_ABRT || err == ERR_CONN) { // 比较严重的错误 printf("TCP Server: Recreating listening socket due to accept error.\r\n"); netconn_close(conn); netconn_delete(conn); conn = netconn_new(NETCONN_TCP); if (conn == NULL) { printf("TCP Server: CRITICAL - Failed to recreate listening netconn. Halting task.\r\n"); return; } err_t bind_err = netconn_bind(conn, IP_ADDR_ANY, TCP_SERVER_PORT); if (bind_err != ERR_OK) { printf("TCP Server: CRITICAL - Failed to re-bind. Halting task. Err: %d\r\n", bind_err); netconn_delete(conn); return; } netconn_listen(conn); // conn->recv_timeout = 10; // 重新设置(如果需要) printf("TCP Server: Listening socket recreated.\r\n"); } OSTimeDlyHMSM(0, 0, 1, 0); // 发生错误后等待一段时间再重试 } }}// ... (tcp_server_init 函数保持不变,但要确保在main中调用了 phy_link_task_init)
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. CIGNORE_WHEN_COPYING_END
关键修改点解释: phy_link_check_thread 和 phy_link_task_init:新增的物理链路监控任务。 LAN8720_ReadLinkStatus:这是一个需要您根据硬件和驱动具体实现的函数,用于读取PHY的链路状态。很多时候,LwIP 的 ethernetif 驱动中已经有类似机制或函数,可以直接调用或参考。例如,正点原子的例程中 ethernetif_link_check_thread 实现了类似功能,并调用 ethernetif_set_link()。您需要确保这个机制是有效的。 TCP 服务器线程 (tcp_server_thread) 内的健壮性增强: 在 netconn_accept 之前,增加了对网络状态 (netif_is_up, netif_is_link_up, IP地址是否有效) 的检查。 调整了 newconn->recv_timeout,10ms 对于检测断连可能太短,可以适当加长。 在处理已连接客户端的循环中,增加了对 netif_is_link_up(&gnetif) 的检查。如果链路断开,可以更早地判断连接已失效。 netconn_write 失败时,认为连接已损坏,关闭并删除 newconn,然后使用 goto next_client; 跳出当前客户端处理循环,准备接受下一个连接。 netconn_recv 返回 ERR_TIMEOUT 时,也检查链路状态。如果链路断开,则关闭连接。 对 netconn_recv 的其他错误类型(如 ERR_RST, ERR_CONN 等)也统一作为连接断开处理。 对 netconn_accept 的错误处理:如果 accept 返回 ERR_TIMEOUT,这是正常的(如果设置了 conn->recv_timeout)。对于其他严重错误,增加了重新创建监听 conn 的逻辑,这是一种更强的恢复机制。 data_len=0; 在 for 循环之前是正确的,在 OS_EXIT_CRITICAL() 之后清零是错误的,因为它会丢失刚接收数据的长度信息。应在下一次 memset 前或作为循环初始化的一部分。 OS_ENTER_CRITICAL() 和 OS_EXIT_CRITICAL() 仅保护了 tcp_server_recvbuf 的访问和 data_len 的累加,这是合理的。
监听 conn 的 recv_timeout:
原代码中 conn->recv_timeout = 10; 会让 netconn_accept 每10ms超时一次。这对于监听socket来说,通常不是最佳实践,因为它会导致CPU频繁唤醒。如果主要目的是等待连接,建议: 移除 conn->recv_timeout = 10;,让 netconn_accept 阻塞。 或者设置一个更长的超时,例如 conn->recv_timeout = 1000; (1秒)。
如果选择阻塞式 accept,则 tcp_server_thread 中 while (!netif_is_up(&gnetif) || ...) 循环里的 OSTimeDlyHMSM 是有意义的,以避免在网络未就绪时 accept 卡死。
集成和测试步骤: 实现 LAN8720_ReadLinkStatus():这是最关键的一步,确保能准确获取链路状态。参考您的 STM32 以太网驱动和 LAN8720 数据手册。 初始化 PHY 监控任务:在 main() 函数中,LwIP 初始化 (lwip_init() 或 MX_LWIP_Init()) 和网络接口添加 (netif_add()) 之后,创建并启动 phy_link_check_thread 任务。 编译并烧录:将修改后的代码编译并下载到 STM32F407。 测试热插拔: 启动设备,等待网络连接成功,TCP 服务器应该可以正常工作。 拔掉网线。通过串口打印确认PHY监控任务检测到链路断开,LwIP 的 netif_set_link_down 被调用。如果之前有客户端连接,该连接应该会超时或出错并关闭。 重新插上网线。通过串口打印确认PHY监控任务检测到链路恢复,LwIP 的 netif_set_link_up 被调用。如果使用 DHCP,观察 DHCP 客户端是否重新获取 IP 地址。 一旦网络恢复,TCP 服务器应该能够再次接受新的客户端连接。
请仔细检查您的 LwIP 移植版本和相关的驱动代码(特别是 ethernetif.c),因为链路状态的处理与驱动实现紧密相关。正点原子的LwIP例程通常会有一个 ethernetif_set_link() 函数以及一个链路检查线程,您可以基于那个框架进行修改或确认其行为。
|