ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

ESP32 WiFi TCP通信实战:从原理到代码实现(Arduino IDE)

2026/9/29 8:43:13 拓冰建站 浏览量
ESP32 WiFi TCP通信实战:从原理到代码实现(Arduino IDE) 好这次咱们来聊一个很实在的话题ESP32怎么通过WiFi和电脑上的程序“对话”。这个系列从最开始的点灯、按键一路走到现在终于开始接触真正意义上的“网络通信”了。我一直觉得光会点灯算是电子爱好者的入门但能让设备联网、和外部交换数据才算是真正摸到了“物联网”这三个字的门槛。而TCP通信恰恰是这扇门后面最基础也最常用的一条路。这篇文章就基于系列里的第六个实验“ESP32利用WiFi进行TCP通信Arduino IDE”展开讲讲从原理、环境、代码到调试踩坑的完整过程。如果你手里正好有一块ESP32开发板也卡在“不知道怎么和设备通信”这一步那这篇内容就是为你准备的。它能帮你把数据从开发板上的传感器、引脚或者是单纯的字符串通过网络送到电脑上也能让电脑反向给ESP32发送指令控制它干活。搞懂这个后面无论是接阿里云、腾讯云的物联网平台还是做局域网里的智能家居控制中枢都会顺畅很多。1. 项目解析为什么是ESP32、WiFi和TCP1.1 ESP32在物联网开发中的“生态位”聊这个实验之前得先说说为什么选择ESP32。很多新手会纠结用Arduino Uno行不行加个ESP8266模块行不行答案是可以但不推荐。乐鑫的ESP32系列芯片相比传统的Arduino板子比如UNO上的ATmega328P有一个本质区别它天生自带WiFi和蓝牙射频电路。不需要外挂一个笨重的串口WiFi模块也不需要额外研究AT指令怎么配置模块进入透传模式。这意味着从硬件层面它就为网络应用做好了准备而且整个开发流程还是Arduino那一套——这极大降低了从“单片机思维”切换到“联网设备思维”的门槛。更加难得的是ESP32的性能并不弱双核240MHz的主频、520KB的SRAM让它在处理TCP数据流、解析JSON或者同时跑点小型Web服务器的时候富余量都非常大。相比于ESP8266那捉襟见肘的RAMESP32在实际项目里做复杂一点的状态机和数据缓存基本不会出现内存溢出的诡异问题。从成本角度说一片带WiFi的ESP32开发板现在的价格已经非常亲民了几乎和一片STM32F103C8T6的核心板差不多。但STM32没有WiFi功能要联网还得再买模块、写驱动。所以无论从学习成本、硬件成本还是开发体验来看ESP32都是入门物联网通信的最优选择。1.2 TCP协议为什么需要“三次握手”和“长连接”下面说说TCP。在计算机网络里TCP和UDP是传输层的两兄弟。UDP就像寄明信片写个收件地址扔出去就不管了快是快但丢了也不负责。而TCP则像是打电话发送数据之前必须先拨号、对方接听、互相确认。这个确认过程就是常说的“三次握手”。第一次握手客户端ESP32说“你好我想建立连接”。 第二次握手服务器电脑回答“收到我可以和你建立连接”。 第三次握手客户端再说“确认收到连接正式建立”。只有经过这三个步骤ESP32和电脑之间才会形成一条逻辑上的“专线”所有数据在这条线上按顺序传输丢了会自动重传接收方也能通过校验机制确保数据没有被篡改或乱序。对于控制类指令、传感器状态的可靠上报来说这种“保证送达”的特性至关重要。在本次实验里我们需要关注TCP的另一个特性短连接和长连接。短连接是每次发完数据就断开下次再发再重新握手就像和同事说话每次都要重新自我介绍长连接则是建立连接后一直保持双方随时可以向对方扔数据像挂了电话线的两个对讲机。我在这篇文章里选择的是长连接方案。原因很简单我们的目标是做一个持续工作的物联网终端它的核心任务是“实时上报数据”和“实时接收指令”。如果使用短连接每次发送温湿度数据都要先握手再断开不仅增加WiFi射频开销还会引入不必要的时间延迟。而长连接一旦建立后续的数据交换基本就是“零握手成本”哪怕一秒发一次数据系统开销也很小。当然长连接也有代价就是需要处理“断线重连”这部分我在后面的代码里做了处理也算给读者一个比较完整的参考。1.3 方案选型局域网通信是物联网的第一步可能有人会问现在都在聊“上云”为什么还提电脑上的局域网TCP服务器一个很现实的原因是任何上云的项目底层原理都离不开TCP。无论是MQTT协议的发布订阅还是HTTP协议的请求响应它们在传输层都是跑在TCP之上的。如果你的设备连局域网里的TCP服务器都搞不定那距离连接几百上千公里之外的云平台还有一段很长的路要走。再说用局域网调试还有一个巨大优势排查方便。通过抓包工具或者串口监视器你能够非常直观地看到数据是否发送、是否接收、内容是否准确。如果直接连云平台一旦不通你根本不知道问题是出在云端服务器配置、本地路由器还是代码逻辑上。先把局域网内TCP链路打通后面的“云”之路才有底气和基础。本实验就是在局域网环境下让ESP32作为TCP客户端主动连接运行在电脑上的TCP服务器从而实现双向通信。2. 环境准备Arduino IDE、硬件与网络规划2.1 搭建Arduino IDE ESP32开发环境虽然ESP32官方推荐的IDE是Espressif-IDE基于VS Code深度定制或者ESP-IDF命令行工具但如果你的目的是“快速验证想法”、“降低入门门槛”那Arduino IDE绝对是最快捷的选择。这里简单提一下环境搭建的步骤和容易出错的地方因为很多新手第一次卡住的点往往在“添加开发板地址”这一步。下载并安装最新版的Arduino IDE注意是2.x版本界面清爽编译速度比1.8.x快不少内置了串口监视器。打开软件后进入“文件 - 首选项 - 附加开发板管理器网址”填入乐鑫官方的下载地址https://espressif.github.io/arduino-esp32/package_esp32_index.json接着打开“工具 - 开发板 - 开发板管理器”搜索框输入“ESP32”找到“esp32 by Espressif Systems”点击安装。这里需要提醒一下这个安装包的下载地址是外网如果你所在网络环境不佳安装过程可能会卡住很久。这时不要急可以考虑用手机热点试试或者等到网络状态好的时段再装。实测下来下载最新的2.0.17版本对应Arduino ESP32 core体积大约在200MB左右。安装完成后在“工具 - 开发板”里面找到你的板子型号。市面上最常见的开发板是“ESP32 Dev Module”也就是板子上印着“ESP32-WROOM-32E”那种模组。选择这个型号基本能适配大多数通用开发板。另外选择开发板的时候注意看板子底部的USB转串口芯片是什么。老款开发板用的是CP2102新款有些是CH340。这两者在电脑上的驱动是互不兼容的如果用USB线插上电脑之后开发板管理器里没出现新的COM口多半就是驱动没有正确安装。CH340的驱动网上很好找CP2102一般是即插即用但也建议从官方源下载一次。2.2 硬件准备与基础接线硬件方面这个实验其实非常简单只需要一块ESP32开发板、一个USB数据线注意必须是数据传输线不是充电线和一台路由器或者手机热点。如果你手里正好有DHT11或者DHT22温湿度传感器也可以接上去让实验内容更丰富一点但不是必需的。这里分享一下DHT11的接法因为和我后面代码里的扩展逻辑有关。DHT11有三个引脚VCC、DATA、GND有些模块是四针但多余的那个NC脚不用管。VCC接ESP32的3.3VGND接GNDDATA引脚接GPIO4。我不建议把DHT11接在5V上虽然它有对应的电平转换但ESP32的GPIO容忍电压是3.3V万一数据线没有做分压长期供电会有烧毁GPIO的风险实测中看到不少板子就是这么烧的。如果你确定这轮只做TCP通信不想折腾传感器那这个实验的硬件准备到这里就算齐了——真的就是一块板子和一根线的事。2.3 网络规划路由器还是手机热点这轮的通信双端都需要“在线”在同一个局域网里。我建议初次实验时使用手机热点。原因有两点一是手机热点默认开启了AP隔离各终端之间互相隔离设备数量少网络环境简单不容易被路由器的“AP隔离”功能干扰二是ESP32连接的WiFi和电脑连接的WiFi能保证在同一网段不易出现电脑连的是“5G频段”而ESP32连的是“2.4G频段”导致二层隔离的诡异问题。如果你用的是路由器尽量确保ESP32连接的是2.4G频段。因为绝大多数ESP32模块不支持5G频段即使你看到WiFi列表里能扫描到5G信号也连不上。这就引出一个很容易踩的坑路由器的“双频合一”功能建议关掉把2.4G和5G分开命名让ESP32明确连接2.4G那个SSID。另外不管是路由器还是热点都要记住当前的网关IP地址。一般来说电脑在局域网里的IP地址就是这个网段里的某个数值在Windows终端里输入ipconfig就能看到IPv4地址。这个地址就是ESP32要连接的“目标服务器地址”。如果ESP32和电脑不在同一个网段通讯是不可能通的这个后续排查的时候我会再强调。3. 核心代码实现一步一步打通TCP链路3.1 代码总体架构在写代码之前我们先在脑子里把整个程序分成几个模块WiFi连接模块、TCP客户端模块、数据处理模块、心跳与重连模块。这四个模块互相配合形成了一个稳定的通信闭环。其中WiFi连接模块负责让ESP32上网TCP客户端模块负责与电脑上的服务器通信数据处理模块负责把收到的指令解析成可控行为心跳与重连模块则负责检查链路是否健在一旦断线自动恢复。下面我给出一个可以直接使用的代码示例代码中集成了DHT11传感器数据的读取和TCP上报但如果你没有传感器也可以直接改成一个简单的“温度随机数”或者纯字符串上报不影响TCP通信的核心功能。3.2 WiFi连接从“裸跑”到“入网”在Arduino框架下WiFi连接是极其简单的一步#include WiFi.h const char* ssid 你的WiFi名称; const char* password 你的WiFi密码; void setup() { Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() ! WL_CONNECTED) { delay(500); Serial.print(.); } Serial.println(WiFi connected); Serial.print(IP Address: ); Serial.println(WiFi.localIP()); }这段代码的逻辑很简单调用WiFi.begin()然后不断轮询连接状态如果连上了就打印分配的IP地址。这里有个细节值得注意WiFi.begin()是非阻塞的它会立刻返回但连接过程是在后台进行的。所以千万不能以为调用完begin()就立刻连上WiFi了必须用循环等待状态变化。连接成功打印IP地址不仅仅是为了让你确认“设备联网了”更重要的是后面调试TCP时你需要知道ESP32在局域网里的IP以便在路由器后台或者抓包工具里确认数据走向。另外建议把WiFi.setAutoReconnect(true)也加上这样当路由器重启或WiFi信号抖动之后ESP32能自动尝试重新连接WiFi不用每次都手动按下复位键。这里有一个坑如果ssid或者密码错了WiFi.status()会一直停留在WL_DISCONNECTED状态循环会永远打印“.”。此时可以增加一个超时退出机制int timeout 0; while (WiFi.status() ! WL_CONNECTED timeout 20) { delay(500); timeout; }超过10秒没连上就说明网络配置大概率有问题可以打印错误状态让人一眼看出问题所在。3.3 TCP客户端实现从“入网”到“找朋友”电脑上跑的服务器程序本质是一个“在网络某个端口上等待连接”的进程。ESP32作为客户端需要主动发起连接。在Arduino中WiFiClient类封装了TCP客户端的全部功能。我们定义一个WiFiClient client;对象然后使用client.connect(serverIP, serverPort)方法进行连接。connect函数返回到的是一个bool值如果返回true则说明“三次握手”成功。以下是我在测试中实际使用的代码片段#include WiFi.h const char* ssid your_wifi_ssid; const char* password your_wifi_password; const char* host 192.168.1.100; // 电脑在局域网里的IP const uint16_t port 8080; WiFiClient client; void setup() { Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() ! WL_CONNECTED) { delay(500); Serial.print(.); } Serial.println(\nWiFi connected); connectToServer(); } void loop() { if (client.connected()) { // 设备数据上报 String data Hello from ESP32, uptime: String(millis() / 1000) s; client.println(data); Serial.println(Sent: data); // 接收服务器下发指令非阻塞 while (client.available()) { String line client.readStringUntil(\n); line.trim(); Serial.println(Recv: line); if (line ON) { // 控制某个引脚或者设备 digitalWrite(LED_BUILTIN, HIGH); } else if (line OFF) { digitalWrite(LED_BUILTIN, LOW); } } delay(5000); } else { Serial.println(Connection lost, reconnecting...); connectToServer(); } } void connectToServer() { while (!client.connected()) { if (client.connect(host, port)) { Serial.println(TCP server connected); client.println(Hello, server!); } else { Serial.println(Connection failed, retry in 5s); delay(5000); } } }这段代码的核心逻辑一共有三层第一层连接WiFi。第二层建立TCP连接。第三种在loop()中维持连接数据上报和指令收取都是非阻塞的。这里有一个典型错误就是很多新手习惯在setup()里调用一次client.connect()然后在loop()里直接写client.println()。这样一旦服务器断开或者网络波动程序就会因为访问一个已被断开的对象而崩溃或者表现为数据丢失。更好的做法是像上面的代码一样在loop()里每个周期都检查client.connected()。如果发现连接已经断开说明服务器挂了、网络断了或者被对端中止了会话这时应该阻塞式地尝试重连而不是继续盲目发送数据。我写的connectToServer()函数采用了一个while循环直到连接成功才退出这样保证了ESP32不会在断线的状态下做无意义的动作。3.4 心跳保活怎么检测“假连接”和“死连接”这里必须讲一个面试和实际开发都非常爱考的细节长连接断开的检测。在TCP协议里连接是逻辑上的。如果一端断电、网线拔了、程序崩溃了另一端并不会立刻感知到因为物理链路上可能没有任何数据包交换。这就造成了一种“假死”状态ESP32以为自己还连着电脑服务器也以为ESP32还在线但两边已经无法正常通信了。解决方案是“心跳包”。在应用层规定ESP32每隔固定时间比如5秒或者30秒发送一个特定的字符串比如PING服务器收到后回复PONG。如果ESP32连续几次没有收到服务器的回复就判定连接已失效主动调用client.stop()并重新发起连接。我在实际项目中通常会定义三个常量心跳发送周期、心跳超时时间、最大重试次数。比如下面这样const unsigned long HEARTBEAT_INTERVAL 15000; // 15秒发送一次心跳 const unsigned long HEARTBEAT_TIMEOUT 90000; // 90秒没有收到服务端数据就认为连接超时 unsigned long lastHeartbeatTime 0; unsigned long lastServerDataTime 0;在loop()里每次收到任何来自服务器的数据都刷新lastServerDataTime millis()。而在主循环的另一端检查当前时间与lastHeartbeatTime的差值如果超过心跳周期则发送一个自定义心跳指令。随后再检查当前时间与lastServerDataTime的差值如果超过超时阈值就主动放弃当前连接并重连。这种方法虽然看似简单但在实际工程中是极其有效的。它解决了“TCP连接看似还活着实际上已经死了”的问题这是所有长连接系统都必须面对的生命周期管理问题。3.5 数据解析让TCP传输变得“有意义”上面的代码展示了一个最简单的字符串收发但在真实物联网场景里你不可能只发送“Hello from ESP32”这种无意义的数据。通常你会传送传感器数据、设备状态、报警信息等这就需要制定一个双方约定的数据封装格式。我这里推荐JSON格式。原因有三第一JSON在几乎所有编程语言里都有现成解析库电脑端用Python的json模块、Node.js的JSON.parse、C#的JsonSerializer都能零成本解析第二可读性好调试时直接用串口监视器或者网络调试助手看原始数据一眼就能看出问题出在哪第三扩展性强后续想加一个字段只需要在JSON对象里追加不用像二进制协议那样考虑帧格式和字节序。ESP32端发送的示例数据格式可以定义如下{device:esp32-dev-01,type:sensor,temp:26.5,humi:60.2,uptime:3600}在ESP32的代码里我们可以用ArduinoJson库来构造和解析JSON。这是一个非常成熟的库在Arduino IDE的库管理器里搜ArduinoJson就能直接安装最新版本是7.x。假设我们已经读取了DHT11的数据到temperature和humidity变量中就可以这样构造JSON#include ArduinoJson.h StaticJsonDocument256 doc; doc[device] esp32-dev-01; doc[temp] temperature; doc[humi] humidity; doc[uptime] millis() / 1000; String output; serializeJson(doc, output); client.println(output);这里需要特别注意StaticJsonDocument的容量大小。如果发送的数据量超过了这个容量序列化会被截断从而产生一个损坏的JSON字符串。对于这种数据量很小的传感器上报场景256字节绰绰有余。但如果你要发送一个包含几十条数据的数组就要考虑改用DynamicJsonDocument或者JsonDocument7.x版本新语法。接收服务器下发的指令同样可以用JSON解析。假设服务器下发的指令是{cmd:set_gpio,pin:2,value:1}ESP32端解析if (client.available()) { String line client.readStringUntil(\n); JsonDocument doc; DeserializationError err deserializeJson(doc, line); if (!err) { const char* cmd doc[cmd]; int pin doc[pin] | -1; int value doc[value] | 0; if (strcmp(cmd, set_gpio) 0 pin ! -1) { pinMode(pin, OUTPUT); digitalWrite(pin, value ? HIGH : LOW); client.println({\status\:\ok\}); } } }这一套逻辑下来TCP通信就不再是简单的“发一句话”而是变成了真正可控、可反馈的“指令通道”。就算你后续接入了某个物联网云平台这套“构造JSON上报、解析JSON指令”的框架也能原封不动地搬过去只是把WiFiClient换成PubSubClient或者HTTPClient的事。4. 调试、排障与实测心得4.1 电脑端搭建TCP测试服务器没有服务器ESP32就只能“空转”。所以我们需要在电脑上先搭一个简单的TCP测试服务器。推荐使用“网络调试助手”这类可视化工具甚至有绿色免安装版在软件里把协议类型设为TCP ServerIP地址填本机IP可以选0.0.0.0表示监听所有接口端口号填8080或任意未占用的端口然后点击“打开监听”。当ESP32连接上来时网络调试助手会显示“有客户端连接”并且能看到ESP32发来的原始数据你也可以在发送区域输入想下发的数据点击发送给ESP32。如果没有现成的桌面工具用Python也能快速搭一个非常轻量的TCP服务器便于后续根据项目逻辑做定制化处理。下面是我在调试时常用的一个Python脚本import socket HOST 0.0.0.0 PORT 8080 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind((HOST, PORT)) s.listen(1) print(fListening on {HOST}:{PORT}...) conn, addr s.accept() with conn: print(Connected by, addr) while True: data conn.recv(1024) if not data: break print(Received:, data.decode().strip()) conn.sendall(bPONG\n)这个脚本在测试期最大的好处是你可以随时改动下行指令并且立刻看到ESP32返回的结果。比如让它每隔5秒发送一个{cmd:query}下去再看ESP32是否响应就能快速确定数据链路是否双向畅通。4.2 常见问题排查速查表我在这个实验上前后调试了不少时间有一半时间花在网络环境的定位上面。以下是我整理出的最常见问题清单遇到问题可以直接对标排查现象原因分析解决办法串口一直打印“.”WiFi名称或密码错误路由器开了MAC地址过滤ESP32不支持5G频段确认SSID和密码正确确认连接的是2.4G频段关闭MAC过滤或白名单WiFi连接成功但TCP连接一直失败服务器没开启IP或端口错误防火墙拦截了端口ESP32和电脑不在同一网段在电脑上确认ipconfig地址用网络调试助手确认端口是LISTENING关闭或添加入站规则保证两端在同一路由器能连接但过几秒就断开服务器程序主动踢掉连接网络广播或AP隔离TCP保活机制未设置检查服务器日志在代码中增加心跳关闭路由器的AP隔离功能接收数据是乱码编码不一致ESP32发送的是GBK而电脑以UTF-8显示接收缓冲区分段处理错误统一使用UTF-8在代码里加client.setTimeout(1)避免半包问题或对接收做粘包处理发送频率很快时丢包TCP发送缓冲区溢出WiFi链路不稳定增加发送间隔使用sent标志或队列机制减少不必要的数据量断电重启后不能自动恢复未实现重连逻辑或重连逻辑阻塞了主循环把重连放到loop()里用标志位或状态机控制避免在重连期间阻塞网络任务以上排查表是浓缩的干货收藏价值很高。初次做TCP的时候遇到“能连接但时而掉线”的情况多半是因为没有做心跳或没有检测socket的断开状态。这往往是隐藏最深的坑因为在局域网环境里WiFi短暂断开又连上很常见但TCP连接不会自动感知WiFi的二次连接状态。4.3 实测过程与代码迭代记录来说说我自己实测时的一次真实迭代过程这个经验很典型。最初我写完代码直接上板串口监视器打印出WiFi已连接TCP服务器也收到了ESP32发来的“Hello, server!”一切都看起来很顺利。但跑了大约两分钟后我把电脑端服务器关掉重启了一次发现ESP32就像“傻”了一样再也不主动连接了。原因很简单在我的初版代码里client.connected()并不是立刻返回false的。当服务器端主动断开连接时这个调用可能在本地网络栈里还认为连接存在直到下一次写操作client.println触发底层错误后才会真正感知。也就是说只靠client.connected()并不能及时发现断线需要配合写入结果判断或者心跳超时机制。我随后把loop()里的发送逻辑改成了if (client.connected()) { if (!client.println(data)) { Serial.println(Send failed, reconnecting...); client.stop(); connectToServer(); } }通过检查println的返回值来判断数据是否真正写入了底层的socket。如果写入失败立即停止连接并重建。这种改法虽然简单粗暴但效果立竿见影。在后续的持续运行测试中这个改造让设备在服务器重启、路由器断电、笔记本休眠唤醒等极端场景下都能在5秒内自动恢复连接。另一个值得提的点是串口监视器波特率。ESP32的默认调试波特率是115200但网络调试助手里看不到ESP32的打印信息两者容易混淆。我在调试时习惯用串口监视器看ESP32端日志用网络调试助手看TCP链路上的数据两边独立能快速定位问题到底出在“链路”上还是“数据格式”上。4.4 从TCP到更远的扩展方向把这段TCP链路彻底搞定之后你会发现整个思维体系一下子打开了。最简单的扩展方向之一是接入现有物联网云平台。无论是阿里云物联网平台、腾讯云IoT还是巴法云它们其实都暴露了一个MQTT接入地址和端口。MQTT协议在传输层上跑的就是TCP底层逻辑和我们现在建立的这条“长连接”没有本质区别。你只需要把上面的WiFiClient替换成MQTT库里的PubSubClient在回调函数里解析云平台下发的JSON指令一个“云智能插座”或者“环境监测站”就基本成型了。如果不想完IP地址直连也可以考虑用HTTPClient库发起REST API请求将传感器数据POST到一台云服务器上这也是很多入门级“物联网数据可视化大屏”项目的常用方案。本质上就是换一种应用层协议传输层仍然是TCP。更进一步如果你对局域网内多设备协作感兴趣可以考虑在电脑端部署一个TCP服务端让多块ESP32同时连接、各自上报数据。这时你需要处理的是“多客户端并发”本质是在服务端为每个连接开一个线程或使用异步IO而ESP32端的代码几乎不用改动。这也是后面做一个家用多节点环境监测网络的基础。5. 一些想特别嘱咐的话这三个月的系列做下来我最大的感触是物联网开发里真正难的不是某个具体函数怎么调用而是你有没有建立一套“链路思维”。拿到一个设备先想明白它的数据从哪来、要经过哪些网络节点、最终到哪里去中间每一跳的可靠性怎么样连接断了怎么恢复数据格式怎么定义——这些问题想清楚了剩下的就是用代码把它们逐个落实。在这个ESP32 TCP通信实验里我们落实了从WiFi入网到TCP建连、从数据上报到指令下发、从心跳保活到断线重连的完整链路。这套东西放到任何实际项目中都不过时。我自己在做前期验证的时候也习惯用这种方式先搭一个最原始、最可控的通信Demo再逐步换成云平台或者更复杂的协议栈这样可以大大减少后期定位问题的复杂度。最后再说一个小技巧调试的时候尽量别把ESP32的串口监视器和电脑端TCP服务器的“发送”按钮同时快速操作。因为串口打印本身会占据一些执行时间和TCP收发混在一起很容易让你误判数据延迟的原因。把它们分开测试——先只测ESP32发、服务器收再测服务器发、ESP32收最后再双向一起跑。分步验证才是工程调试的正确姿势。