简介面向嵌入式初学者与物联网开发者资源包基于意法半导体的STM32F103C8T6微控制器和ESP8266 WiFi模块构成了一个以串口通讯控制无线模块的完整TCP服务器工程。它演示了如何通过AT指令集配置Wi-Fi模块、建立和关闭TCP连接并配合STM32的两组串口分别实现用户界面交互和与ESP8266的数据交换让开发者直观理解串口通讯在微控制器与无线模块协作中的关键作用对没有接触过无线模块的开发者而言能明显降低上手门槛。压缩包共244个文件、7.16MB包含C/H源码、Keil工程配置文件、编译中间文件以及PDF说明文档其中标准外设库中USART、GPIO、定时器等驱动配置均可直接查阅。工程还给出了ATCIPSTART建立连接、ATCIPSEND发送数据、ATCIPCLOSE关闭连接等常用指令的收发处理思路并针对连接断开、重试等异常场景提供了状态处理逻辑系统初始化、串口参数匹配、连接状态显示、数据收发处理等代码模块清晰可查便于在此基础上扩展远程控制与数据采集功能。目前已有2955人学习浏览该资源适合希望将STM32接入Wi-Fi网络并搭建TCP服务的小型项目实践者。 搞嵌入式这些年一直在跟串口、WiFi、TCP这几个词打交道。最近给一套采集设备做远程参数配置需求很直接硬件端得能被手机和PC主动连上下发配置、读取实时数据。选型的时候没多想就是STM32F103C8T6加ESP8266用AT指令把ESP8266配成TCP服务器。这套组合便宜、资料多、怎么折腾都不心疼。但真正把连接做稳、做到长时间不掉线远不是敲几条AT指令那么简单。这篇文章把我完整的接线、指令时序、状态机代码和实际调试踩过的坑都整理出来给正准备做同类项目的朋友一个参考。1. 为什么选这套组合STM32F103C8T6 ESP8266的服务端角色1.1 TCP服务器在这里解决的真实问题很多初学者一听到TCP服务器就觉得高深其实在这个项目里它解决的就是一个非常朴素的痛点设备端需要让别的设备主动来找它。手机App要连设备修改参数上位机要连设备读取数据如果设备只做客户端去连服务器那就得依赖一台额外部署的服务器局域网内反而绕远路。让ESP8266工作在AP模式并开启TCP Server手机直接连接设备发出的WiFi热点然后通过TCP协议连接到指定端口。这样STM32就相当于拥有一个可以被外部主动访问的网络入口。整个链路是STM32F103C8T6 ----串口---- ESP8266 ----WiFi---- 手机/PCSTM32和ESP8266之间只走AT指令ESP8266负责所有网络协议栈的处理对STM32来说TCP连接的建立、断开、数据收发全部被抽象成一串串可读的文本指令。这也是这套方案的生命力所在——不依赖特定操作系统不需要跑lwIP协议栈裸机工程就能搞定。1.2 方案边界能用和不能用的场景这套组合适合什么场景我先说结论适合并发量小、单包数据小、局域网内通信的场合。一个设备带两三个客户端每个客户端几秒钟交互一次传输几十到几百字节的数据ESP8266完全扛得住而且稳定性可以做得很好。不适合的场景也很明显如果要求高并发、大数据量持续传输、跨公网访问那ESP8266就不太合适了。WiFi模块的缓冲区有限串口波特率再高也受AT固件处理能力制约。实测下来单包超过1KB就比较容易出现分包或者丢帧需要自己处理组包逻辑。跨公网访问则需要云服务器中转或者内网穿透方案这不是本文讨论的重点。2. 硬件接线与固件准备翻车率高发区2.1 引脚分配与供电细节先看硬件连接。我用的STM32F103C8T6最小系统板板载串口一般是USART1PA9/PA10这个串口通常被USB转串口芯片占着用来打印调试信息。所以和ESP8266通信我建议单独用一个串口比如USART2PA2/PA3或USART3PB10/PB11避免调试信息和AT指令应答混在一起。具体接线如下STM32F103C8T6ESP8266模块说明PA2 (USART2_TX)RXDSTM32发送给ESP8266PA3 (USART2_RX)TXDESP8266发送给STM323.3VVCC给ESP8266供电GNDGND必须共地3.3VCH_PD (EN)使能引脚拉高—GPIO0悬空即可进入运行模式注意ESP8266工作时峰值电流能到300mA以上甚至更高。STM32最小系统板上的板载LDO一般只有几十到一百多毫安的能力直接给ESP8266供电模块会不断重启。最稳妥的做法是外接一个AMS1117-3.3稳压芯片或者用独立3.3V电源并且把电源地和STM32的地连在一起。这个坑我一开始踩得很惨后面在调试章节详细说。还有一个容易忽略的点ESP8266的TXD/RXD是3.3V电平STM32F103C8T6的IO口也是3.3V两者可以直接连接不需要电平转换。但如果你用的是5V的单片机那就必须加电平转换电路否则很容易烧模块。2.2 AT固件版本与初始化自检拿到ESP8266模块第一步不是接单片机而是先用USB转TTL模块单独测试一下确认固件版本和指令集。我用的是安信可ESP-12F模块出厂一般带AT固件。测试方式很简单USB转TTL的TX接模块RXRX接TX共地模块供电3.3V打开串口助手波特率设置115200。输入AT回车如果返回OK说明固件正常。然后输入ATGMR这个指令会返回固件版本号建议用1.7.4以后的版本或者官方较新的AT固件。老版本固件对TCP服务器模式的支持有差异有些指令参数格式都不一样。单独测试还有一个好处可以把常用的初始化指令事先跑通确认模块在AP模式下的行为。比如设置AP参数、开启CIPSERVER然后把测试结果记录下来方便后面单片机端联调时对照。3. TCP服务器AT指令时序连接建立的关键过程3.1 AP模式与STA模式的指令流差异要让ESP8266变成TCP服务器有两条路一条是让模块发WiFi热点AP模式另一条是让模块连接家里路由器STA模式客户端通过路由器访问设备。两条路的指令差别主要在WiFi连接部分TCP服务器部分是一样的。AP模式完整指令流AT // 测试返回OK ATE0 // 关闭回显减少串口干扰 ATCWMODE2 // 设置AP模式2表示仅AP ATCWSAPESP_AP,12345678,1,3 // 配置热点名、密码、信道1、WPA2加密 ATCIPMUX1 // 开启多连接这是CIPSERVER的前置条件 ATCIPSERVER1,8080 // 启动TCP服务器端口8080 ATCIFSR // 查询模块IP地址AP模式下通常是192.168.4.1这里最关键的是ATCIPMUX1。很多人在这一步栽跟头如果CIPMUX保持0单连接模式执行CIPSERVER会直接返回ERROR。因为TCP服务器天然要处理多个客户端连接AT固件规定服务器模式必须工作在多连接模式下。STA模式则是在CWMODE设置上不同ATCWMODE3 // APSTA双模式或者直接用ATCWMODE1 ATCWJAP你的路由器,密码STA模式下模块会从路由器获取IPATCIFSR会返回一个局域网IP。客户端连接这个IP加端口号即可访问设备。这里容易遇到一个问题如果路由器开启了AP隔离那么连接WiFi的设备之间不能互相访问即使在同一局域网也连不上。遇到这种情况去路由器后台关掉AP隔离即可。3.2 客户端连接后固件的行为当客户端成功连接上TCP服务器时ESP8266会主动向串口推送一条消息0,CONNECT含义是链路0link id 0与客户端建立了连接。如果再有第二个客户端连进来就会看到1,CONNECT。每个客户端会被分配一个独立的链路号范围是0到4也就是说ESP8266在TCP服务器模式下最大支持5个客户端。客户端发送数据过来时ESP8266推送的格式是IPD,0,5:hello拆开看IPD是固定头0是链路号5是数据长度冒号后面跟着的hello就是实际数据。这个格式是AT固件的标准推送格式STM32端解析数据帧就是围绕这个格式做文章。任何时刻想确认当前有几个客户端在线可以主动查询ATCIPSTATUS返回的包里会列出每个链路的状态比如CIPSTATUS:0,1,0,192.168.4.2,8080这里的字段含义是链路ID、连接状态1表示已连接、是否为远端关闭、远端IP、远端端口。这个查询指令在断线检测里非常有用后面会详细说明用法。3.3 发送数据的完整帧流程ESP8266向指定客户端发送数据不是直接把数据内容跟在指令后面就行而是有固定流程ATCIPSEND0,5第一个参数是链路号第二个参数是待发送数据的字节长度。此时ESP8266会返回一个提示符表示可以输入数据了。接下来输入5个字节的数据比如hello模块才会真正把数据发送出去然后返回SEND OK如果发送失败会返回SEND FAIL。这个细节在STM32端编程时非常关键因为符号出现之前你发送的任何字节都不会被模块当成数据处理而是被当成AT指令的一部分。所以完整的发送流程是一个先请求、等提示、再喂数据的三步交互过程。4. STM32串口驱动与协议状态机代码层面的核心设计4.1 串口接收缓冲与数据帧识别STM32通过串口接收ESP8266的数据问题在于ESP8266串口发过来的内容不是纯数据而是AT指令应答、系统消息、网络数据混在一起的流。可能上一帧是OK下一帧就是0,CONNECT紧接着又出现IPD,0,5:hello。所以单片机的串口接收不能简单地把字节存进缓冲区就完事必须设计一个能识别帧边界、能提取有效负载的解析逻辑。最基础的做法是用一个环形缓冲区加状态机。串口中断里只负责把字节丢进环形缓冲区主循环里逐个取出字节用状态机判断当前是在数据帧的头、长度、还是数据内容中。状态机的好处是不会因为一帧数据到达的时序发生错乱只要状态转移设计正确连续来多少帧都能稳定解析。4.2 AT命令发送与应答的封装思路发送AT指令时我建议做一个简单的封装避免在主循环里直接阻塞等待应答。思路是定义一个等待应答字符串的全局标记发送指令时记录当前指令ID然后主循环中每收到一行字符串就比较是否匹配期望的应答比如OK、ERROR、SEND OK、发送提示符等。这种方式比阻塞延时稳定得多因为AT指令的应答时间是动态的网络状态差的时候几十毫秒到几百毫秒都有可能。如果用HAL_Delay固定等200毫秒再判断慢的时候读取不到应答快的时候又浪费CPU时间。4.3 关键代码骨架我用HAL库写了一个简化的解析框架核心思路是逐字节状态机解析IPD帧同时保留AT应答的检测typedef struct { uint8_t state; // 当前状态 uint8_t conn_id; // 解析出的链路号 uint16_t data_len; // 解析出的数据长度 uint16_t cnt; // 当前已接收数据计数 } ipd_parser_t; typedef enum { IDLE, // 空闲 MATCH_HEAD, // 匹配 IPD, GET_ID, // 读取链路号 GET_LEN, // 读取长度 WAIT_COLON, // 等待冒号 READ_DATA // 读取数据 } parser_state_t; void parse_byte(uint8_t c, ipd_parser_t *p) { switch (p-state) { case IDLE: { static uint8_t match_idx 0; const char *head IPD; if (c head[match_idx]) { match_idx; if (match_idx 4) { match_idx 0; p-state GET_ID; } } else { match_idx 0; } break; } case GET_ID: if (c ,) { p-state GET_LEN; } else if (c 0 c 9) { p-conn_id c - 0; } break; case GET_LEN: if (c :) { p-cnt 0; p-state READ_DATA; } else if (c 0 c 9) { p-data_len p-data_len * 10 (c - 0); } break; case READ_DATA: // 这里把收到的字节存到对应的应用缓冲区 app_recv_data_buf[p-cnt] c; if (p-cnt p-data_len) { // 一帧数据接收完成交给业务逻辑处理 app_on_tcp_data(p-conn_id, app_recv_data_buf, p-cnt); p-data_len 0; p-state IDLE; } break; } }这个状态机的关键点在于完全靠字符驱动不依赖固定延时不受串口数据分包影响。即使一帧IPD,0,10:helloworld被拆成两次串口中断到达也能正确组帧。对于AT指令行我额外使用一个简单的行缓冲器遇到\n时认为一行结束然后把这一行与预设关键字比较void process_at_line(const char *line) { if (strstr(line, OK)) { at_cmd_ack ACK_OK; } else if (strstr(line, ERROR)) { at_cmd_ack ACK_ERROR; } else if (strstr(line, SEND OK)) { at_cmd_ack ACK_SEND_OK; } else if (strstr(line, CONNECT)) { // 新客户端接入 app_on_client_connect(); } else if (strstr(line, CLOSED)) { // 客户端断开 app_on_client_close(); } }在主循环中发送AT指令的流程做成非阻塞状态机。先发指令字符串然后等待at_cmd_ack被置位配套一个超时计数器超时未应答就报错重试。这样整个TCP通信链路跑起来后主循环还能腾出时间处理其他业务逻辑。5. 实际调试中踩过的坑从现象到根因5.1 WiFi模块反复重启不是代码问题项目刚开始调试时WiFi模块每隔十几秒就重启一次日志里偶发ready字样。一开始以为是代码问题查了AT指令配置、查了状态机都没有头绪。后来用万用表量了模块VCC引脚发现电压在3.1V到3.4V之间剧烈波动这才反应过来供电跟不上。改用外置AMS1117-3.3稳压芯片输入5V输出接ESP8266 VCC并加了一个470uF电解电容和一个100nF陶瓷电容。模块瞬间安静了连接也稳定了。这个坑真的非常经典尤其是用最小系统板直接引3.3V给ESP8266供电的时候。经验遇到WiFi模块反复重启、配网失败、长时间运行后掉线先测供电再看程序。电源不稳会导致一切玄学问题。5.2 IPD数据被截断数据量稍大时发现一个现象ESP8266推送IPD,0,20:...但STM32这边收到的数据经常只有十几个字节后面内容丢了。排查后定位到两个原因。第一串口接收缓冲区太小。我一开始用了一个64字节的数组ESP8266在115200波特率下每毫秒大约能传11.5字节如果主循环处理不及时还没来的及取走数据新数据就把缓冲区冲掉了。后面把环形缓冲加大到512字节问题缓解不少。第二没有考虑数据分片。TCP连接本质上是一个流式协议客户端发送的20字节可能在底层被拆成两个TCP包到达ESP8266模块会分别推送两条IPD帧或者一条帧长度和实际长度不一致。所以解析IPD时不能假设冒号后面的数据一次性到齐必须用状态机累积收到len个字节才认为帧结束。5.3 应答与数据混杂导致解析错乱在TCP服务器模式下AT指令的应答和网络数据是同一个串口返回的。比如客户端刚好在你发送ATCIPSTATUS的时候推送数据过来串口里就会同时出现CIPSTATUS:...和IPD,0,N:...。如果解析逻辑写得死就会把IPD帧的前半段当成AT应答处理导致数据错位。解决办法是为AT指令发送和IPD数据解析各设独立状态机。AT行解析器不管IPD帧IPD解析器也不管普通命令行。识别入口就是看一行开头的头几个字节是不是IPD如果是则把这个状态机待命进入IPD解析模式在数据读取完成前所有字节都交给IPD解析器不参与AT行匹配。这样两条解析链路互不干扰哪怕同时到来也能正确分流。5.4 客户端掉线后服务器继续保持连接手机或者上位机程序突然退出时TCP连接并不会立刻在ESP8266侧消失。如果客户端断开时没有正常发送FIN包或者网络链路异常ESP8266侧会一直维持着这个连接直到底层TCP超时。这会导致什么问题一个客户端反复重连几次后链路号被占满新的客户端再也连不进来。应对策略是在应用层主动检测。我的做法是服务端每隔一定时间比如3秒发送一个应用层心跳包同时维护一个最后收到客户端数据的时间戳。如果连续几个心跳周期都没有收到任何来自该链路的数据就主动通过ATCIPCLOSE0关闭这条链路释放连接资源。ATCIPCLOSE0 // 关闭链路0实测下来配合心跳机制断线后的链路回收从原来的十几分钟缩短到了几秒钟。这个机制在嵌入式TCP通信中几乎属于标配属于没有硬件TCP keepalive控制权时的最朴素方案。6. 让服务器连接更可靠的几条经验6.1 应用层心跳是保命手段ESP8266的AT固件本身对TCP keepalive的控制很弱你很难通过AT指令设置底层保活探测周期。所以在设计整个通信协议时一定要在应用层加入自定义心跳。心跳包尽量做小比如固定4字节帧头、命令字、序号、校验。客户端收到心跳后原样回复服务端来判断链路活性。如果两端都是自己做协议约定好就行。如果客户端是第三方工具或者现成App没法定制心跳那就退而求其次定期用ATCIPSTATUS轮询连接状态发现断开的链路就用ATCIPCLOSE主动回收。6.2 上位机或App端的注意事项TCP通信的问题往往出在两端。客户端如果不是自己写的先确认连接时使用的目的IP。AP模式下ESP8266发出来的热点通常子网是192.168.4.1手机连上热点后设备地址是固定的192.168.4.1端口就是你设置的8080。用Socket工具测试时很多朋友会把IP填成手机自己获取的IP那就永远连不上服务器。另外上位机收数据时要注意粘包和半包问题。TCP是字节流不是消息边界一帧IPD,0,5:hello解析出的5字节在客户端那边可能和其他数据一起到达。我自己用C#写上位机测试时就专门写了一个基于循环缓冲区的组帧函数每次收到网络流先入缓冲再按帧头帧尾提取完整帧。6.3 从单链路到多链路的扩展思路前面提到ESP8266服务器模式最多支持5个连接。在实际业务中一般会有一两个固定上位机长期保持连接另外几个是手机临时连接。设计时建议给每条链路一个状态表链路ID客户端IP是否在传输最后活跃时间应用状态0192.168.4.2否12:01:33已认证1—否12:03:10空闲这样在多客户端场景下STM32就能知道数据应该回给谁哪个链路超时了需要清理。如果业务逻辑简单也可以约定固定链路号对应的角色比如链路0只给上位机用链路1到4给临时手机连接。这样省去了动态分配和身份识别的复杂度对于单片机裸机程序来说尤其合适。这套方案我已经在几个正式项目里跑过了包括小型环境监测设备和简易门禁控制系统。整体体验是STM32F103C8T6的资源足够跑AT指令解析和TCP业务逻辑ESP8266在局域网环境下做TCP服务器完全够用关键是要把串口处理、状态机设计、掉线检测这三个环节做扎实。如果你也在做类似项目建议先在电脑上把AT指令流程完整走一遍再开始写单片机代码所有的坑在串口助手阶段就能提前暴露一半。后面想继续深入可以考虑用定时器加DMA方式接收串口数据进一步降低CPU占用也可以在应用层基础上扩展简单的帧校验和分包重传把嵌入式TCP通信做得更稳定。本文还有配套的精品资源点击获取