嵌入式上位机开发入门(十八):修复首次连接超时问题
目录一、前言二、整体通信时序三、accept 的工作原理四、问题根因分析五、修复方案六、修复前后对比七、总结八、结尾一、前言大家好这里是Hello_Embed。上篇完成了 AT_Socket 的移植并成功编译。本篇来解决实测时遇到的一个典型问题——首次连接总会出现一闪而过的超时错误。通信架构如下Modbus Poll上位机 ↕ WiFi 热点 W800 模块 ↕ UART 串口 STM32H5Server上位机通过 WiFi 热点连接 W800W800 再通过串口与开发板通信。正是这里的串口速度瓶颈埋下了竞态条件的隐患。二、整体通信时序完整的 Modbus TCP 通信时序如下步骤操作代码1Server 监听 IP 地址与端口modbus_tcp_listen(ctx, 1)2Modbus Poll 发起连接请求—3Server 接受连接modbus_tcp_accept(ctx, s)4Modbus Poll 发送 Modbus 数据—5Server 读取网络数据modbus_receive(ctx, query)6Server 回应数据modbus_reply(ctx, query, rc, mb_mapping)7Modbus Poll 读到回应—Server 监听阶段代码Draw_String(0,48,Waiting connect ...,0xff0000,0);while(1){smodbus_tcp_listen(ctx,1);if(s0)break;}Server 接受连接阶段代码while(1){smodbus_tcp_accept(ctx,s);if(s0)break;}Draw_String(0,64,Modbus client connected,0xff0000,0);主循环——接收并回复do{rcmodbus_receive(ctx,query);/* Filtered queries return 0 */}while(rc0);rcmodbus_reply(ctx,query,rc,mb_mapping);三、accept 的工作原理要理解这个 Bug先看modbus_tcp_accept内部的执行流程第一步构造 AT 命令查询 socket 状态sprintf((char*)buf,ATSKSTT%d\r,(int)ptDev-sockets[socket].user_data);第二步通过串口执行 AT 命令等待响应errat_exec_cmd(ptDev,(int8_t*)buf,(uint8_t*)buf,sizeof(buf),resp_len,AT_TIMEOUT);第三步解析返回结果提取对端 IP、端口和硬件 socket 号sscanf((constchar*)start,%d,%d,\%[^\]\,%d,%d,%d,hw_socket,status,remote_ip,remote_port,local_port,rx_data);第四步为这个连接分配一个软件 socket并记录相关信息new_socketw800_get_socket(ptDev,hw_socket);xTaskResumeAll();{/* 记录 hw_socket */ptSocketptDev-sockets[new_socket];ptSocket-user_data(void*)hw_socket;ptSocket-typeSOCK_STREAM;/* 记录 local 地址信息 */memcpy(ptSocket-local,ptDev-sockets[socket].local,sizeof(ptSocket-local));/* 记录 remote 地址信息 */sin(structsockaddr_in*)ptSocket-remote;ipaddr_aton(remote_ip,sin-sin_addr);sin-sin_porthtons(remote_port);memcpy(name,ptSocket-remote,sizeof(*name));*namelensizeof(*name);returnnew_socket;}关键点accept需要通过串口来回两次发 AT 命令 等响应才能完成 socket 的分配和信息记录。这段时间内W800 可能已经收到了上位机的第一帧数据。四、问题根因分析问题本质是一个竞态条件Race Condition时间轴 ──────────────────────────────────────────────────────────→ 上位机发起连接 │ ├─ W800 收到连接请求 │ ├─ accept 开始执行需要串口来回查询耗时较长 │ ├─ 上位机没等 accept 完成直接发送了第一帧 Modbus 数据 │ ├─ 后台线程收到数据包SKTRPT调用 w800_recv_packet │ → 根据 hw_socket 号查找软件 socket 结构体 │ → 此时 accept 还未执行完软件 socket 还没分配 │ → 找不到对应的 socket数据被丢弃 │ └─ accept 执行完毕软件 socket 分配好了……但已经太迟了根本原因串口通信速度远慢于网络通信速度。accept依赖串口往返查询来完成 socket 分配而上位机在这段空窗期内已经推送了第一帧数据导致第一帧数据因找不到对应 socket 而被丢弃上位机显示超时。五、修复方案既然问题在于数据到了但 socket 还没分配解决思路就是在后台线程收到数据包、却找不到对应软件 socket 时主动给它分配一个。修改w800_recv_packet中的处理逻辑在后台线程中/* 如果根据 hw_socket 找不到软件 socket 结构体也给它分配一个 */if(socket-1){socketw800_socket(AF_INET,0,0);if(socket!-1){w800_set_hwsocket(socket,hw_socket);}}同时accept中的逻辑也需要对应调整——原来是没有就新建现在要改为先查找是否已由后台线程分配有就直接复用new_socketw800_get_socket(ptDev,hw_socket);if(new_socket-1){/* 后台线程未提前分配这才是全新连接新建一个 */new_socketw800_socket(AF_INET,SOCK_STREAM,0);if(new_socket-1)return-1;}xTaskResumeAll();{/* 记录 hw_socket */ptSocketptDev-sockets[new_socket];ptSocket-user_data(void*)hw_socket;ptSocket-typeSOCK_STREAM;/* 记录 local 地址信息 */memcpy(ptSocket-local,ptDev-sockets[socket].local,sizeof(ptSocket-local));/* 记录 remote 地址信息 */sin(structsockaddr_in*)ptSocket-remote;ipaddr_aton(remote_ip,sin-sin_addr);sin-sin_porthtons(remote_port);memcpy(name,ptSocket-remote,sizeof(*name));*namelensizeof(*name);returnnew_socket;}六、修复前后对比修复前修复后数据包到达时 socket 未分配数据被丢弃后台线程紧急分配 socket数据保留accept 执行完毕时新建软件 socket查找已分配的 socket直接复用首次连接结果数据丢失上位机显示超时数据正常接收连接成功修复的核心思想将 socket 分配的时机从accept 完成时提前到数据包到达时堵上串口延迟造成的时间窗口。七、总结通信架构Modbus Poll → WiFi → W800 → UART → STM32H5根因串口速度慢accept未完成时上位机已发送第一帧数据竞态点后台线程收到SKTRPT数据包时软件 socket 还未由accept分配修复后台线程在找不到 socket 时主动分配accept改为优先复用已分配的 socket本质将 socket 分配时机从accept驱动改为数据到达驱动消除时间窗口八、结尾本篇完成了首次连接超时 Bug 的排查与修复。后续还将对 Modbus TCP 程序进行进一步改进使其能够及时感知 socket 连接状态处理断线重连等异常场景。Hello_Embed继续带你从原理到实践掌握嵌入式上位机开发的核心技能敬请关注