尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32通过ESP8266 AT指令搭建嵌入式Web服务器的工程解析

STM32通过ESP8266 AT指令搭建嵌入式Web服务器的工程解析 简介基于STM32F103ZET与ESP8266的Web服务器工程包面向嵌入式开发者和物联网初学者解决通过UART连接ESP8266、配置Wi-Fi并实现HTTP网页服务器的实际问题。压缩包共85个文件包含36个C源文件、34个H头文件、两个启动汇编文件、hex固件、uvproj工程文件及多个网页代码txt工程含SYSTEM、CORE、USER等目录模块便于定位系统与用户代码另有HTTP详解、串口大数据量处理、登录与LED控制网页源码以及基于MINI STM32F103C8T6的参考例程可对照学习或直接移植。已有363人学习下载。资源围绕AT指令、AP/Station模式、HTTP请求解析与响应生成等关键点说明如何在STM32端处理GET/POST请求并搭配完整Keil工程便于编译烧录文中文档还可辅助排查串口传输丢包等问题。适合作为STM32Wi-Fi物联网入门实践也方便快速移植到其他平台。1. 资源包工程解读STM32F103ZET6 用串口 AT 指令驱动 ESP8266 搭 Web 服务器拿到STM32F103ZET_WIFI_web网页服务器.zip这个资源包时我第一反应是去查它到底是 lwIP 协议栈方案还是串口 AT 指令透传方案。打开文件结构扫了一遍SYSTEM、CORE、USER、HARDWARE四个目录加上wifi.c、webserve.c、usart1.c这几个关键源文件结论很清楚这是一个不跑 lwIP、完全靠 ESP8266 内置 TCP/IP 协议栈来完成 Web 服务的工程。STM32F103ZET6 的角色是主控负责通过 USART1 向 ESP8266 发 AT 指令、解析返回状态、拼接 HTTP 报文ESP8266 只干两件事——连 WiFi、收发 TCP 数据。这种方案的优势是大幅降低 Cortex-M3 端的资源和协议栈负担适合做传感器数据上报、远程开关控制这类轻量级 Web 应用。资源包里还带了HTTP详细解释.txt、登录网页代码.txt、登录成功网页代码.txt以及一个基于 STM32F103C8T6 MINI 板的同款例程作为对比。这篇文章会把工程里的初始化时序、HTTP 请求解析、网页数据组织三块关键代码逐步拆开并针对大数据量传输和粘包问题给出可落地的处理手段。2. 工程骨架与单元职责先搞清谁来维护 TCP 连接2.1 为什么选 AT 指令方案而不是 lwIPSTM32F103ZET6 有 64KB RAM其中可用堆空间约 40KB50KB。跑 lwIP 的 RAW API 虽然也能实现 HTTP Server但 TCP 报文缓冲、ARP 表、IP 分片重组这些都要在主控内存里划一块出来稍微上点并发连接内存就吃紧。而 ESP8266 内部自带完整的 TCP/IP 协议栈STM32 只需要通过 UART 发ATCIPSERVER1,80ESP8266 就能维护 TCP 监听和链路状态。这套方案的内存消耗集中在三块接收缓冲区、发送缓冲区、HTTP 响应报文的拼装缓冲。工程里将 USART1 接收缓冲设为 1024 字节发送缓冲设为 384 字节网页 HTML 源文件以const uint8_t数组的方式存储在 Flash 中不占 RAM。利用 ESP8266 的ATCIPSENDlength指令先告知数据长度、再发送报文正文ESP8266 会自动将 TCP 数据包封装发往客户端。2.2 标准库工程文件结构与编译产物工程采用正点原子标准库模板文件组织如下目录/文件职责关键技术点CORE/core_cm3.cCortex-M3 内核函数提供SysTick、NVIC配置函数CORE/startup_stm32f10x_hd.sZET6 启动文件高密度 Flash 芯片选用 HD 版本SYSTEM/delay/sys/usart延时、时钟、串口基础驱动delay_init、uart_initHARDWARE/wifi.cESP8266 指令封装ESP8266_Init、ESP8266_SendCmdHARDWARE/webserve.cWeb 核心处理HTTP 解析、响应生成USER/main.c主循环调度按键触发、状态灯控制OBJ/Template.hex编译产物可直接通过 FlyMcu 下载2.3 USART1 初始化与中断接收usart1.c里初始化了 115200 波特率的异步串口使能了接收中断并且在中断服务函数里做了环形缓冲或尾部指针累加。wifi.c依赖这个串口向 ESP8266 发送 AT 指令同时接收 ESP8266 返回的OK、ERROR、CONNECT、IPD等状态帧。void USART1_IRQHandler(void) { u8 res; if(USART1-SR (15)) { // 接收非空标志 RXNE res USART1-DR; // 读取数据硬件自动清 RXNE if(USART1_RX_STA 1024) { // 防止缓冲溢出最大 1024 字节 USART1_RX_BUF[USART1_RX_STA] res; } } }代码逻辑上接收中断里把 ESP8266 串口返回的字符逐个放进一个 1024 字节的全局数组USART1_RX_BUF中USART1_RX_STA记录当前收了多少字节。问题在于它没有按行号做定界HTTP 报文里可能连续到达数百字节单纯靠RXNE逐个收会频繁进中断导致main循环在 WiFi 数据密集时几乎被饿死。后文第 5 章会专门讲这个大数据量场景下的改进方案。3. ESP8266 初始化序列与 AP/Station 模式切换3.1 AT 指令握手流程ESP8266_Init函数做的事本质上是三条串口 AT 指令联动先通过ATCWMODE3把模块设为 APStation 双模式再通过ATCWJAPSSID,password让模块去连接路由器。如果路由器未就绪或者密码错误模块会返回FAIL成功则返回OK并且后续会周期性上报 IP 地址。这里有一个关键延时设计ATCWJAP最长可能需要 510 秒因此初始化代码中在发送该指令后需要做一个软件超时轮询而不是盲目等待 50ms。建议超时设成 10 秒以 200ms 为步进轮询 AT 返回。u8 ESP8266_Init(void) { u8 retry 0; ESP8266_SendCmd(AT\r\n, OK, 100); // 同步波特率并检查模块是否响应 delay_ms(500); ESP8266_SendCmd(ATCWMODE3\r\n, OK, 200); // 双模式AP Station delay_ms(500); while (retry 5) { if (ESP8266_SendCmd(ATCWJAP\ssid\,\password\\r\n, OK, 10000) 0) { break; // 连接成功退出重试 } retry; delay_ms(3000); // 路由器拒绝或密码错误时等待网络侧恢复 } ESP8266_SendCmd(ATCIPMUX1\r\n, OK, 200); // 开启多连接支持多个 TCP 客户端 ESP8266_SendCmd(ATCIPSERVER1,80\r\n, OK, 200); // 启动 TCP Server端口 80 return 0; }ESP8266_SendCmd的实现是清空接收缓冲发送 AT 指令字串然后阻塞读取串口返回在指定超时时间内查找目标关键字找到返回ESP8266_SEND_OK。这里有个容易踩坑的地方如果ATCIPMUX0单连接导致ATCIPSERVER返回 ERROR必须先把回显ATE0关掉再逐条排查否则返回的OK和ERROR会和新指令的响应帧交叉错位。还有一种情况是模块固件版本旧ATCWJAP在密码错误时它会先打印WIFI DISCONNECT再返回FAIL关键词搜索用的是FAIL此时帧里带WIFI DISCONNECT不影响匹配结果但我建议在调试时把完整响应帧打印到调试串口便于区分CWJAP:1和CWJAP:4这两种不同的错误码。3.2 AP 模式兜底当路由器不可用时ATCWMODE3的另一个好处是模块同时开启自身热点手机可以直接连接ESP8266开头的 AP默认 IP 为192.168.4.1。这在现场无路由器的场景下非常关键设备通电后用户连不上家中的 WiFi 时可以先连模块自身热点、在浏览器输入192.168.4.1访问配置页。wifi.c中应该预留一个ATCWSAP配置函数用于修改热点 SSID 和密钥。许多工程遗漏了这一步导致出厂后只能通过串口线重新配置路由器账号排查起来十分被动。3.3 TCP Server 与多连接管理ATCIPMUX1允许 ESP8266 同时维护 5 个 TCP 连接ATCIPSERVER1,80启动 Server 后模块会通过串口主动上报0,CONNECT、1,CONNECT这样的链路状态帧。STM32 端需要解析这条帧来判断新客户端接入。多连接模式下ATCIPSEND的格式变为ATCIPSENDlink_id,length发送响应到对应连接。资源包里的webserve.c也正是按照这个链路 ID 来组帧的必须和 HTTP 请求解析绑在一起处理不然多浏览器标签页同时访问时响应会乱串。4. HTTP 请求解析与响应生成搞清楚IPD后面的每一段4.1 从串口裸数据到 HTTP 请求头当 ESP8266 收到浏览器的 HTTP 请求时如果启用ATCIPDINFO1模块串口输出的帧格式如下IPD,0,415:GET / HTTP/1.1 Host: 192.168.1.100 Connection: keep-alive ...这里的0是 link_id415是本次收到的数据字节数冒号后的内容为请求报文。webserve.c中的WebServer_Handler函数接收这一帧数据并从GET、POST后提取 URL 路径。void WebServer_Handler(u8 *frame, u16 len) { u8 *url_start, *url_end; u16 url_len; if (strncmp((const char *)frame, IPD, 4) ! 0) { return; // 非数据帧忽略 } // 帧解析http_request 为 IPD,linkid,len: 之后的数据 u8 *http_request strchr((const char *)frame, :) 1; // 从 中提取 URL形如 / 或 /led_on url_start strchr((const char *)http_request, ); url_end strchr((const char *)(url_start 1), ); url_len url_end - url_start - 1; char url[32] {0}; strncpy(url, (const char *)(url_start 1), url_len); // 根据 URL 分发到不同控制动作 if (strcmp(url, /) 0) { SendHTML_MainPage(link_id); // 返回主页面 } else if (strcmp(url, /led_on) 0) { GPIO_SetBits(GPIOA, GPIO_Pin_8); SendHTML_ControlResult(link_id, LED ON); } else if (strcmp(url, /led_off) 0) { GPIO_ResetBits(GPIOA, GPIO_Pin_8); SendHTML_ControlResult(link_id, LED OFF); } }这个函数没有用sscanf而是手动遍历字符串。因为sscanf依赖 stdio 库一方面增加了代码体积另一方面在处理不定长 URL 时容易产生不可控的栈溢出风险。手动取指针的写法更安全但在做strncpy时要注意 URL 长度边界url缓冲区 32 字节而实际请求中 URL 可能包含后续参数比如/led_on?nametest所以url_end定位应该是?或则空格取较小值。4.2 HTML 页面存储与动态数据填充资源包里的webcode.h把用浏览器写好的 HTML 登录页和控制页源码转成了 C 语言十六进制数组这是工程化一个通用做法网页文件不烧录到外部 Flash而是编译进固件内部减少运行时 IO 读取。数组定义形式如下const unsigned char html_mainpage[] { 0x3C, 0x21, 0x44, 0x4F, 0x43, 0x54, 0x59, 0x50, // !DOCTYPE 0x45, 0x20, 0x68, 0x74, 0x6D, 0x6C, 0x3E, 0x0A, // E html\n // 后续字节省略 };发送时直接把这个数组送入 AT 指令数据区交给 ESP8266 发出。这里有一个工程细节ATCIPSEND0,len发送完数据后模块会返回SEND OK此时必须有明确握手再去发下一个请求否则连续高频刷新页面时会出现busy s...错误帧。同时单片机端发送缓冲只有 384 字节而 HTML 源文件往往超过 1KB因此webserve.c中的发送逻辑要分片每次从数组中取 256 字节调用ATCIPSEND分段发完再用下一个连接发剩余部分。4.3 204 状态与保活机制HTTP 1.1 默认开启 Keep-Alive浏览器不会因为响应结束就断开 TCP。这带来一个隐患客户端不关闭连接ESP8266 的 TCP 连接表会被占满后续新设备将无法接入。处理办法有两种一是在响应头中带Connection: close通知浏览器响应完成后关闭 TCP 链路代码实现为在SendHTML_MainPage的最后拼接字符串Connection: close\r\n。二是定期从串口读取0,CLOSED状态帧并清除对应的链路记录实现连接回收。资源包里的登录成功网页代码结构同时兼容这两种方式推荐第一种简单有效。5. 串口大数据量传输的过载问题拆包、定界与重传5.1 为什么RXNE逐字节接收会丢包ESP8266 模块在收到浏览器请求后会一次性把打包好的 HTTP 报文通过 UART 发出。115200 波特率下每毫秒约 11.5 字节一个 400 字节的请求需要约 35ms 传完。如果 STM32 主频 72MHz 且主循环中刚好在执行 Flash 擦写或delay_ms(500)串口中断依然能进但接收缓冲写入效率远比不过模块发送效率导致 USART 溢出标志ORE置位后DMA 停摆数据直接丢弃。典型现象是浏览器首次打开正常刷新后却是白屏报ERR_INCOMPLETE_CHUNKED_ENCODING。资源包里专门放了一个串口大数据量时的解决办法.txt这是本工程最有参考价值的一部分它给出的思路是改用 USART 空闲中断配合 DMA 接收同时把波特率提升到 460800 或 921600。usart1.c修改后如下void USART1_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); DMA_InitStructure.DMA_PeripheralBaseAddr (u32)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (u32)USART1_RX_BUF; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize 1024; // 环形上限 DMA_InitStructure.DMA_Mode DMA_Mode_Circular; // 循环采集 DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_Init(DMA_InitStructure); }开启空闲中断后每次 ESP8266 发送完一包数据串口总线空闲硬件置位IDLE标志此时从 DMA 当前计数器的值就能算出实际收了多少字节。逻辑上先关 DMA、读剩余计数、再重新填满 DMA接着处理这包完整数据。这个方案把 CPU 从每字节中断中释放出来大数据量下依然能保持稳定接收。工程原来的逐字节中断方式建议只用于 AT 指令交互阶段比如ATCWMODE这类短返回进入 HTTP 数据收发阶段后切换到 DMA 模式。5.2 粘包处理一条 TCP 流包含多个 HTTP 请求DMA 空闲中断对于单个数据包有效但无法避免 ESP8266 将多个请求连续上报到一个缓冲区。举个例子浏览器加载首页时会同时发/请求和/favicon.ico请求二者会在 ESP8266 内部被打包成一次 UART 传输最终进入 STM32 缓冲区后变成两段独立的GET报文首尾相接。解析时如果只处理第一个\r\n\r\n后剩余的字节不做二次分帧下一个请求就会被吞掉TCP 层表现为丢数据。实践中需要把缓冲区内每个IPD块单独拆出并按len字段跳过该块继续查找下一个IPDu16 frame_start 0; while (frame_start recv_cnt) { u8 *ipd_flag strstr((const char *)(rx_buf frame_start), IPD); if (ipd_flag NULL) break; u8 *len_pos strchr((const char *)ipd_flag, ,) 1; len_pos strchr((const char *)len_pos, ,) 1; u16 pkt_len atoi((const char *)len_pos); // 实际 HTTP 数据起始位置 u8 *http_data strchr((const char *)len_pos, :) 1; WebServer_Handler(http_data, pkt_len); // 跳到下一帧当前帧头 数据长度 结尾换行边界 frame_start (http_data - rx_buf) pkt_len 2; }这个while循环能在一个接收周期内处理完所有已到达的 HTTP 请求。要注意atoi函数解析len字段到遇到:为止数值位不能超过 5 位因为 TCP 单包上限就是 1460 字节超过则需要横向分片。另外一个实际调试技巧粘包发生时先把接收缓冲原样打印到调试串口观察相邻两个IPD之间有没有\r\n分隔符。部分 ESP8266 AT 固件在数据帧后面带\r\n而旧固件没有这会直接影响frame_start偏移量的设置所以这个偏移值必须根据实际固件版本做适配。5.3 看门狗喂狗位置与阻塞风险如果在循环里调用了delay_ms(3000)等待 ESP8266 响应独立看门狗 IWDG 极有可能超时复位。解决方法是把喂狗函数放在每个 AT 指令发送的轮询等待循环中而不是主循环末尾。更稳妥的方式是把ESP8266_SendCmd改造成分时状态机每次只等一个固定时隙没有返回则退出本轮让主循环有机会喂狗和检查超时计数。这个设计在正式产品中非常关键否则设备很容易在 WiFi 信号差的环境下陷入反复重启。6. 网页资源转 C 数组实现自动化脚本替换与手动兜底6.1 开发时如何快速更新网页每次在 Dreamweaver 或 VS Code 里改完 HTML如果都要手工转换成 C 数组效率非常低且极容易因为漏写换行符导致页面在浏览器里错版。常见做法是写一个 Python 小工具读取 HTML 文件并输出webcode.hwith open(index.html, rb) as f: data f.read() lines [const unsigned char html_mainpage[] {] for i, byte in enumerate(data): if i % 12 0: lines.append( ) lines.append(0x{:02X}, .format(byte)) if (i 1) % 12 0: lines.append(\r\n) lines.append(\r\n};) with open(webcode.h, w) as f: f.write(.join(lines))脚本输出后在 Keil 里重新编译把新固件烧进 STM32再浏览器访问设备 IP 即可验证效果。还有一个更便捷的变通如果你的工程支持外部 SPI Flash 存储可以把webcode.h生成一个 bin 文件直接在串口烧录工具里用 XMODEM 下载省去反复交叉编译的步骤。资源包里的LED关的网页.txt内容结构比较规整可以直接用这个脚本转换。6.2 验证 Web 服务器是否正常工作的三个层级这里提供一个分层验证方法排错时能快速缩小范围第一层用 PC 串口助手直接连接 ESP8266 模块绕过 STM32手动发ATCIPSERVER1,80然后 PC 浏览器访问设备 IP 能出现控制页面则问题定位在 STM32 解析端第二层在 STM32 工程里加串口打印用调试串口输出IPD帧的原始内容和解析后的url变量核对是否与你访问的地址一致第三层在 PC 上抓包过滤tcp.port 80看 STM32 是否有回包、回包里 TCP 序号是否连续。这套方法里最快捷的往往是第二层串口打印原始帧如果原始帧内容正常而 LED 没动作那就是webserve.c里的字符串比较分支出了问题。本文还有配套的精品资源点击获取
返回列表