ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32+ESP8266+Android:从串口到手机屏幕的TCP透传方案

STM32+ESP8266+Android:从串口到手机屏幕的TCP透传方案 简介面向嵌入式与Android开发者这套源码工程完整实现了STM32经ESP8266 WiFi模块与Android手机应用的双向数据传输可解决物联网设备端到端无线交互需求。包内共2826个文件大小33.86MB覆盖C/H固件源码、Java应用代码、XML界面、JSON配置、Gradle工程及APK/class/dex等编译产物工程结构完整便于对照学习。已有2269人学习下载代码包含STM32串口驱动、ESP8266 AT指令控制、Android Socket服务端与数据解析等关键路径。既可作为毕业设计或课程设计的完整参考也能为智能家居、工业数据采集等场景提供一套可扩展的通信框架。1. 从串口到手机屏幕一块STM32怎么和Android App把数据聊起来把STM32采集的数据送到手机App上显示很多开发者第一反应是上云、走MQTT、用现成物联网平台。但如果你只是做实验室台架、样机联调、或者产品原型阶段的数据回传绕一圈云平台反而增加延迟和依赖。更直接的做法是让ESP8266以TCP客户端身份接入局域网Android端开一个ServerSocket监听两边点对点传数据。这个方案最麻烦的不是TCP本身而是串口侧ESP8266的AT指令回复和用户数据混在同一根UART上一旦波特率、回显、换行符处理不好STM32发出去的命令就像打进了黑洞。这篇把STM32 ESP8266 Android的完整链路拆开讲从串口收发、AT指令时序到Android端Socket接收每一步给出可直接抄的参数配置和代码适合正在做嵌入式联网、又不想过早引入云平台的开发者。2. 链路架构与选型STM32主控、ESP8266透传、Android TCP Server的职责划分2.1 为什么选ESP8266做WiFi桥接而不是直连4G模块很多项目里数据量并不大可能是几路传感器读数、几个状态标志位每秒最多几百字节。这种量级用4G模块属于杀鸡用牛刀模组贵、流量卡管理麻烦而且AT指令集各家不统一。ESP8266的优势在成本几块钱、功耗可控、AT指令生态成熟而且市面上大量NodeMCU开发板可以直接用Arduino IDE做快速验证。如果手里正好有NodeMCU注意它的管脚定义和裸芯片不一样——板载USB转串口芯片已经把UART0引出来了但GPIO2等引脚在启动时有电平要求不能随意接外设这是用Arduino IDE开发时最容易踩的坑。另一个选型理由是ESP8266支持透传模式。所谓透传就是芯片进入CIPMODE1之后串口收到的所有字节自动打包成TCP报文发出去不需要每条数据前再加ATCIPSEND指令。这样STM32端的代码可以复用原有的串口发送逻辑只是在初始化阶段多做一次透传配置。相较之下如果使用逐条ATCIPSEND方式STM32每次发送都需要拼指令、等回复实时性差且代码耦合度高。2.2 数据流每一段的协议边界整条链路可以拆成三段STM32与ESP8266之间的UART、ESP8266与Android之间的TCP、Android内部从Socket到UI线程的数据传递。每一段都有自己的协议边界混在一起处理是后面排错困难的总根源。第一段UART上跑的是AT指令和用户数据的混合流。初始化阶段STM32向ESP8266发送AT指令此时总线上只有控制指令进入透传模式后总线上的数据全部是业务数据。因此STM32端的串口接收逻辑需要区分两个阶段配置阶段按行解析AT回复透传阶段按自定义帧解析业务数据。第二段TCP上跑的是裸数据。如果不做应用层协议接收端无法判断一帧数据从哪里开始、到哪里结束。常见做法是每帧数据以\r\n结尾接收端按行读取。但TCP是流协议底层可能粘包Android端读取时要用BufferedReader.readLine()这类按行解析的封装而不是一次read()就认为收到完整一帧。第三段是Android内部的线程切换。Socket监听必须在子线程收到数据后要通过Handler或runOnUiThread切回主线程更新TextView。很多新手直接在Socket线程里操作UI控件导致崩溃这个属于Android基础问题但在这条链路里尤其容易触发因为数据是持续高频到达的。2.3 关键参数表参数推荐值说明UART波特率115200ESP8266默认且需与STM32的USART配置一致数据位/停止位/校验8/N/1最常见的串口配置TCP端口8080Android端ServerSocket监听端口避开常用端口服务器IPAndroid设备WiFi局域网IP不能是127.0.0.1App内通过WiFiManager获取透传模式CIPMODE1进入透传后免去逐条CIPSEND提示Android手机开热点给ESP8266连接时热点IP一般是192.168.43.x网段ESP8266作为DHCP客户端获取地址但连接的目标IP始终是手机的热点IP这个要在代码里写死或者做成可配置项。3. STM32端实现HAL库下的串口收发与AT指令时序3.1 串口初始化与波特率匹配STM32端使用STM32CubeIDE生成初始化代码时USART2用于和ESP8266通信是常见接法。PA2为TX、PA3为RX与ESP8266的RXD、TXD交叉连接ESP8266的CH_PD引脚接3.3V使能GPIO0保持悬空或高电平进入运行模式。注意STM32和ESP8266都是3.3V逻辑电平可以直接相连如果用5V单片机和它通信则必须加电平转换。/* USART2 初始化参数 */ huart2.Instance USART2; huart2.Init.BaudRate 115200; huart2.Init.WordLength UART_WORDLENGTH_8B; huart2.Init.StopBits UART_STOPBITS_1; huart2.Init.Parity UART_PARITY_NONE; huart2.Init.Mode UART_MODE_TX_RX; HAL_UART_Init(huart2);这里有一个容易忽略的问题HAL_UART_Init只配置了串口参数引脚复用映射由HAL_UART_MspInit里的HAL_GPIO_Init完成不要在初始化流程里漏掉。另外如果后续调试发现收不到ESP8266的回复先查ESP8266的供电——它启动瞬间电流可以达到300mA以上用STM32开发板的3.3V输出常常拉垮表现为AT指令无响应或返回乱码。建议ESP8266单独用AMS1117-3.3供电共地即可。3.2 发送AT指令并解析回复的轮询与状态机ESP8266的AT指令回复以\r\n结尾典型的成功回复是OK\r\n。STM32端配置阶段的做法是发一条指令等待回复超时重试。这里需要用轮询加超时机制而不是HAL_UART_Transmit之后就干等因为AT指令偶尔会因为WiFi环境问题延迟返回。uint8_t Esp8266_SendCmd(char *cmd, char *ack, uint16_t timeout_ms) { uint8_t buf[128] {0}; uint16_t len 0; HAL_UART_Transmit(huart2, (uint8_t*)cmd, strlen(cmd), 200); uint32_t tick HAL_GetTick(); while (HAL_GetTick() - tick timeout_ms) { if (HAL_UART_Receive(huart2, buf len, 1, 50) HAL_OK) { len; if (len sizeof(buf)) break; buf[len] \0; if (strstr((char*)buf, ack)) return 1; } } return 0; /* 超时未收到预期回复 */ }这段代码的逻辑是先把指令发出去然后每次尝试读取1字节把收到的内容拼进缓冲区每拼入一个字节就用strstr查一下期望的回复字符串是否出现。ack参数可以传OK或ERROR等关键字。timeout_ms字段控制总超时时间防止某条指令长时间无响应卡死整个流程。使用这个函数时的调用顺序是AT、ATE0关回显、ATCWMODE1Station模式、ATCWJAPSSID,PASSWORD连路由器、ATCIPSTARTTCP,192.168.1.100,8080建TCP连接、ATCIPMODE1进透传、ATCIPSEND开始透传。其中CWJAP的等待时间要放大到5~10秒因为WiFi关联有扫描和认证过程。这里注意如果之前开启了回显ack匹配OK依然有效但缓冲区里会混入指令本身的回显内容不影响最终判断。3.3 把传感器数据封装成Android能识别的格式业务数据建议使用JSON格式STM32端用cJSON库序列化Android端用org.json解析两边都省事。比如采集温度和湿度cJSON *root cJSON_CreateObject(); cJSON_AddNumberToObject(root, t, temperature); cJSON_AddNumberToObject(root, h, humidity); cJSON_AddNumberToObject(root, id, device_id); char *json_str cJSON_PrintUnformatted(root); /* 追加换行符作为帧结束标记 */ char frame[128]; sprintf(frame, %s\r\n, json_str); HAL_UART_Transmit(huart2, (uint8_t*)frame, strlen(frame), 100); cJSON_Delete(root); free(json_str);最后追加\r\n是关键一步。Android端如果用BufferedReader.readLine()它默认读到一个\n就返回\r会被剥掉所以这里加\n就够了。但如果你后续要自己解析原始字节流那么\r\n作为帧边界更稳妥因为可以明确区分字段内的换行和帧尾的换行。设备ID字段用来区分多台设备如果只有一个设备可以省略但建议保留后续做多设备管理时不用改协议。4. ESP8266配置与Android端Socket接收4.1 ESP8266客户端模式的AT配置序列ESP8266在这个项目里作为TCP客户端主动去连接Android手机上的ServerSocket。完整配置序列如下我一般用串口助手调试一遍确认每一条都返回OK后再把序列固化到STM32代码里AT ATE0 ATCWMODE1 ATCWJAPYourSSID,YourPassword ATCIPSTARTTCP,192.168.1.100,8080 ATCIPMODE1 ATCIPSENDCIPSTART里的IP地址是Android手机在当前WiFi局域网里拿到的IP端口8080必须在AndroidManifest里不需要特殊声明因为ServerSocket监听不涉及权限。CIPMODE1之后串口进入透传模式这条指令本身不返回OK而是直接返回提示符。等到CIPSEND再次返回之后所有串口数据直接被转发到TCP连接对端。注意透传模式下退出比较麻烦需要发送并且在前后各留1秒静默时间。所以如果STM32业务代码里需要临时切回配置模式比如重新配网要预留这个退出机制否则只能复位ESP8266。如果你用的是NodeMCU开发板而不是裸模块VIN和3V3引脚的带载能力会比裸模块好一些但仍然建议外置供电。不要指望USB口那点电流在WiFi长时间收发时稳住3.3V实测掉到2.9V以下就会出现TCP反复断开重连的现象症状很像代码问题实际是电源问题。4.2 Android APP的ServerSocket子线程实现Android端需要开一个子线程运行ServerSocket监听8080端口。核心代码private void startServer() { new Thread(() - { try { ServerSocket server new ServerSocket(8080); while (!Thread.currentThread().isInterrupted()) { Socket client server.accept(); BufferedReader reader new BufferedReader( new InputStreamReader(client.getInputStream())); String line; while ((line reader.readLine()) ! null) { JSONObject obj new JSONObject(line); final double temp obj.getDouble(t); final double humi obj.getDouble(h); runOnUiThread(() - { tvTemp.setText(温度: temp ℃); tvHumi.setText(湿度: humi %); }); } } } catch (Exception e) { Log.e(Server, socket error, e); } }).start(); }这段代码有几个关键点。第一accept()是阻塞的所以整个循环放在子线程。第二readLine()按行读取这是为什么STM32端发送的数据必须以\n结尾——因为readLine()遇到\n才返回。第三runOnUiThread把数据更新切回主线程避免跨线程操作UI。第四while内层循环保持读取直到连接断开这样ESP8266断线重连时不用重启App。有一点很容易被忽略Android 9及以上默认禁止明文TCP流量需要在AndroidManifest.xml里给主Activity加android:usesCleartextTraffictrue属性或者配置network_security_config.xml允许特定IP段的明文流量。否则Socket连接建立后收不到数据LogCat里会刷一堆CLEARTEXT communication not permitted警告。4.3 常见卡点回显污染、换行符、线程阻塞我在实际调试中遇到最多的三类问题列在这里。回显污染如果ESP8266的ATE0没执行成功每次发送AT指令后串口都会先把指令原文回显一遍然后才是回复内容。STM32端解析回复时strstr匹配OK倒是不受回显影响但如果你用精确比较比如strcmp就会一直失败。处理办法是初始化序列里第一条就是ATE0并在STM32端对这条指令的回复做特殊处理——有些固件对ATE0返回的信息是OK但也有的固件直接关闭回显不再返回任何内容所以超时有ERROR重试机制更稳。换行符不匹配ESP8266的AT指令回复是\r\n但不同固件版本有的也带\r不带\n或者只有\n。用strstr匹配时如果目标字符串包含\r源串只有\n就匹配不上。我通常把ack参数统一为纯关键字不带任何控制字符比如就传OK而不是OK\r\n。ServerSocket线程阻塞一个客户端断开后readLine()返回null内层循环退出回到外层的accept()等待新连接。但如果是ESP8266的TCP连接异常断开readLine()也可能抛出SocketException此时如果不捕获整个子线程会崩溃退出App再也收不到数据。所以代码里要捕获IOException并继续回到accept()而不是让异常直接中断线程。5. 排错与进阶抓串口日志、处理粘包与数据校验5.1 分层定位故障的位置整条链路四个节点——STM32、ESP8266、WiFi路由器、Android手机任何一个环节断了表象都是App收不到数据。我排错时通常按从下到上的顺序先用USB转TTL直接连ESP8266的串口用PC上的串口助手手动执行AT指令确认模块能连WiFi、能建TCP连接。这一步通不过问题在ESP8266配置或网络环境通过了再接上STM32用逻辑分析仪抓PA2引脚的波形确认STM32确实发出了数据。现象优先排查点手法AT无响应ESP8266供电万用表量3.3V启动瞬间压降AT有响应连不上WiFiSSID/密码错误手动ATCWJAP看返回码建TCP连接失败手机IP是否变化App内打印本机IP核对目标地址连上了但收不到数据帧格式不匹配抓串口日志看是否有\r\nApp偶发崩溃明文流量限制检查usesCleartextTrafficWiFi路由器这层是很多人容易忽略的隔段时间掉线问题。如果ESP8266长时间不发送数据部分路由器的NAT空闲超时只有60~120秒TCP连接会被悄悄断开但双方都不感知。处理办法是应用层心跳STM32每30秒发一个{type:ping}Android端收到后不更新UI只重置空闲计时器超过90秒没收到任何数据就关闭当前Socket重新accept能大幅减少假死状态。5.2 TCP粘包与拆包处理因为STM32端每帧数据都以\r\n结尾Android端readLine()天然解决了按行拆包的问题。但这个方案只适用于JSON这种自带清晰边界的文本协议。如果你传的是二进制传感器数据或大量浮点数组按行分割就不合适了因为二进制数据里可能随机出现\r\n字节序列。常见做法是定长帧或长度前缀。定长帧实现最简单但浪费带宽长度前缀更常用——每帧开头用4字节表示负载长度负载内容原样传输。DataInputStream in new DataInputStream(client.getInputStream()); byte[] lenBuf new byte[4]; while (true) { in.readFully(lenBuf); int len ByteBuffer.wrap(lenBuf).order(ByteOrder.BIG_ENDIAN).getInt(); byte[] payload new byte[len]; in.readFully(payload); /* 此时 payload 就是完整一帧不会粘包 */ }STM32端对应的发送逻辑是先发4字节大端长度再发负载。这种方式在局域网内开销很小但换来的是二进制数据也可以稳定传输。对于采集高频ADC波形或图片数据的场景这是更好的选择。5.3 进阶技巧帧头长度校验的传输协议如果项目要往产品方向走建议把协议升级成帧头长度校验的结构。帧头用固定魔数比如0xAA55长度字段表示帧体长度校验字段用CRC8或累加和Android端收到帧先校验再解析能过滤掉WiFi传输中的偶发误码。typedef struct { uint16_t magic; /* 0xAA55 */ uint16_t length; /* payload字节数 */ uint8_t payload[256]; uint8_t crc; /* payload累加和 */ } Frame;STM32端发送前计算累加和Android端收到后重算比对。这个改动表面看只是多了几个字节开销但实际意义在于WiFi链路的误码率虽然低但基于UDP的丢包和TCP的静默损坏都可能存在有了帧级校验至少能保证App上显示的数据不是脏数据。否则一个传感器读数偶尔跳变排查半天发现是传输层误码那就太冤枉了。CRC计算代码可以用查表法STM32的M0/M3内核上几百字节的表放Flash没压力计算耗时几微秒可以忽略不计。本文还有配套的精品资源点击获取
返回列表