
简介基于STM32F103与ESP8266通过HTTP协议接入中移OneNet云平台的物联网实战项目适合具有一定单片机基础、希望掌握设备联网与云平台交互的开发者。工程能够主动上报继电器状态及温湿度数据同时接收平台下发的控制指令并执行动作覆盖设备入云、数据双向通信、断电重连等核心环节。资源包共175个文件包含42个h头文件、40个c源文件以及编译生成的o、hex、map等中间与烧录文件并配套KEIL工程文件整体约6.45MB代码基于KEIL开发已在STM32F103C8T6上运行同系列其他型号可自行调整芯片型号与Flash容量后使用。目前已有2270人学习下载。工程源码结构清晰可直接导入KEIL查看串口、定时器、ADC、GPIO等模块实现也可重点研读ESP8266联网与HTTP协议解析部分便于迁移至自研物联网产品也可作为毕业设计参考。1. 一块 STM32 板子把继电器和温湿度推上 OneNetHTTP 这条路并不难走很多人一提到 ESP8266 上云第一反应就是上 MQTT其实在你还没有搭建自己的 Broker、业务逻辑还没拆成主题模型之前HTTP 协议接入 OneNet 反而是最容易验证设备链路的方式。这套基于 STM32F103C8T6 ESP8266 的工程走的就是串口2 接 ESP8266上报继电器状态和温湿度到中移 OneNet 云平台同时用 HTTP 轮询方式接收平台下发的控制指令WEB 端和 APP 端的按钮可以直接驱动单片机引脚。项目已在 C8T6 上跑通KEIL 工程直接打开换同系列 F103 只需调整芯片型号和 Flash 容量。适合正在做智能网关、环境监控、设备远程控制的工程师也适合第一次把 AT 指令串成完整业务链的人作为迁移基线。2. ESP8266 与 STM32 接线、AT 固件验证和 KEIL 工程适配2.1 引脚分配与电平匹配STM32F103C8T6 的串口2 默认映射在 PA2 和 PA3PA2 为 USART2_TXPA3 为 USART2_RX。ESP8266 的 UART 接口必须与 STM32 的 UART 做交叉连接也就是模块的 TXD 接 STM32 的 RX模块的 RXD 接 STM32 的 TX。信号ESP8266 引脚STM32F103C8T6 引脚说明电源VCC3.3V峰值电流超过 300mA不能用 STM32 板载 LDO 直接带地GNDGND必须共地否则串口电平无法形成回路模块发送UTXDPA3 / USART2_RX模块回传给单片机的数据模块接收URXDPA2 / USART2_TX单片机发给模块的 AT 指令使能CH_PD / EN3.3V必须拉高悬空会导致模块不启动ESP8266 开发板与 STM32 通信时最容易栽的坑有三个。第一串口电平不是 5V直接把 STM32 的 5V 引脚拉出去接模块会烧射频前端STM32 引脚虽然多数是 3.3V 容忍但 USART 输出电平也以 3.3V 为上限所以两边都要默认 3.3V TTL。第二模块供电要独立ESP8266 在 Wi-Fi 发包瞬间电流会到 300mA 以上STM32F103 的板载稳压器往往只够 MCU 自己用我一般会用一个 AMS1117-3.3 分开供电电感靠近模块电源脚。第三CH_PD 不能省量过不少模块在不接 CH_PD 时能发 AT 却不连网。2.2 先用串口助手验证 ESP8266 AT 固件不要一上来就把 STM32 和 ESP8266 焊死先找一个 USB-TTL 把模块单独接到 PC用串口助手发送下面的指令确认固件版本和 Wi-Fi 连接能力。ATCWMODE1 OK ATCWJAPyour_ssid,your_password WIFI CONNECTED OK第一行把模块设置成 Station 模式第二行连接家庭路由器。如果返回WIFI CONNECTED说明模块射频部分正常如果返回ERROR或WIFI DISCONNECT先查密码是否含特殊字符注意 AT 指令里双引号必须原样发送串口助手不要开“发送新行”之外的多余回车。这个步骤相当于给后续 STM32 联调定了一个基准模块本身没问题问题才可能出在单片机串口配置或代码时序上。固件选择方面建议使用支持ATCIPSTART、ATCIPSEND、ATCIPMUX的标准 AT 固件版本号并不是越高越好老版本固件对Connection头的处理更宽容。如果你用的是 NodeMCU 板子板载 USB-TTL 与模块 UART 走的是同一组引脚接 STM32 时必须把板载 USB 口断开否则两边驱动同时拉电平会直接导致通信乱码。2.3 KEIL 工程芯片型号和调试器选择代码使用 KEIL MDK 打开工程文件后第一件事不是编译而是确认 Target 选项里的芯片型号。当前工程默认配置是 STM32F103C8T6对应 64K Flash 和 20K RAM。如果你手里是 STM32F103RCT6 这类 256K Flash 的芯片需要在 Options for Target - Device 里选到对应型号同时注意启动文件也要匹配。工程里包含的 stm32f10x_flash.c、stm32f10x_usart.c、stm32f10x_tim.c 等文件说明它走的是标准外设库不是 HAL 库所以预定义宏STM32F10X_MD和STM32F10X_HD必须与芯片型号对应。C8T6 属于中等密度用 MDRCT6 属于高密度用 HD这个宏错了即使编译通过也可能出现外设时钟异常。调试器那里同样要分清有的下载器是 J-LINK有的是 ST-Link在 Options for Target - Debug 下拉框里选择对应驱动然后到 Settings 里确认能识别到目标芯片。下载前检查 Flash Download 选项卡C8T6 的 Program Size 填 64K如果烧录时提示No STM32 Target Found先查接线 SWDIO、SWCLK、GND 是否连接再看板子是否被别的程序占用串口导致供电被拉低。3. OneNet HTTP 接入原理APIKey、数据点上报与命令轮询3.1 先理清 OneNet 平台上的三个关键参数OneNet 旧版多协议接入的 HTTP 接口地址是api.heclouds.com80443 对应 HTTPS80 对应 HTTP。设备端一般走 80 端口因为 ESP8266 的 AT 固件处理 HTTPS 需要额外配置证书HTTP 在局域网到公网链路足够满足多数控制场景。接入前必须拿到三个参数。device_id 是在平台创建设备后生成的一串数字标识具体的物理设备api-key 是项目级或设备级的鉴权密钥相当于设备的访问令牌数据流 datastream id 是你自己定义的业务字段名比如温度叫temp湿度叫hum继电器状态叫relay。OneNet 控制台生成 APIKey 的位置在「多协议接入」产品下的设备管理页创建完产品后进入设备列表设备详情页里就能查看或重置 api-key这个值要保存到单片机代码的宏定义里。参数获取位置用途device_id设备列表标识具体设备api-key设备详情页HTTP 请求头鉴权datastream id自定义平台端显示的字段名如果你把这三个参数放到 HTTP 请求的不同位置很容易理解 OneNet 的模型URL 里放设备 IDHeader 里放 APIKeyBody 里放数据流字段。平台不关心你是 STM32 还是别的单片机只认这个 HTTP 报文结构。3.2 上报数据点的 HTTP 报文结构设备上报温湿度时实际发出的 HTTP 请求长这样POST /devices/12345678/datapoints HTTP/1.1 Host: api.heclouds.com api-key: AbCdEf1234567890 Content-Length: 108 Connection: close {datastreams:[{id:temp,datapoints:[{value:26.5}]},{id:hum,datapoints:[{value:60}]}]}注意Content-Length必须和 JSON Body 的字节数完全一致多一个空格或少一个换行都会导致 OneNet 返回 400。很多人在 PC 上测试时用工具自动填充长度没问题一旦转到单片机手动拼字符串就会因为sprintf里中文字符或转义符计算错误而反复失败。我一般先在 PC 上用 Python 或 Postman 把这条请求调通再复制到 C 代码里固定好 Body 格式。上报 JSON 里每个 datastream 是一个独立对象id对应平台上下发控制的数据流名称datapoints是数组可以一次携带多个时间点数据。如果只上报当前值一个元素就够了。函数里需要拼接的部分是value因为温湿度是浮点数C 语言里建议用整数放大十倍来传输比如温度 26.5 传输为 265平台端显示时再除以 10这样避免浮点格式化在 MCU 上占用过多 Flash。3.3 命令下发轮询与 ACKOneNet 的 HTTP 接入没有像 MQTT 那样维持长连接平台下发控制指令是通过「命令」机制实现的。设备需要主动向平台发起命令查询请求平台把排队的指令放到响应体里设备解析后执行再回一个确认。轮询接口结构如下GET /cmds?device_id12345678qos0 HTTP/1.1 Host: api.heclouds.com api-key: AbCdEf1234567890响应示例{errno:0,data:{cmd_uuid:a1b2c3d4,data:relay_on}}拿到cmd_uuid和data字段后设备执行继电器动作然后调用 ACK 接口告诉平台这条命令已经被消费POST /cmds/a1b2c3d4/ack HTTP/1.1 Host: api.heclouds.com api-key: AbCdEf1234567890 Content-Length: 2 {}这里qos0表示平台不重发指令适合继电器这种重复执行也无害的控制逻辑如果控制的是步进电机这类必须精确执行的设备建议用qos1平台会等到 ACK 才把命令从队列里删掉。命令轮询的频率不建议太高一秒一次足够平台端建立命令到设备响应一般有几十到几百毫秒延迟轮询太快只会让 ESP8266 一直处于发包状态影响上报稳定性。3.4 AT 指令下的 HTTP 连接复用每次上报都执行一遍ATCIPSTART建立 TCP 连接OneNet 服务器端会很快断开空闲链接导致频繁握手。更稳的做法是在 ESP8266 上建立长连接后复用HTTP 请求头里用Connection: keep-alive发完一个请求后不要主动断开 TCP等下一次上报继续用同一个链路 ID 发。AT 指令流程是ATCIPSTARTTCP,api.heclouds.com,80 CONNECT OK ATCIPSEND POST /devices/12345678/datapoints HTTP/1.1 ... SEND OK这条链路不会一直存在OneNet 网关一般会在几十秒无数据后关闭连接所以代码里要记录最后一次成功发送的时间超过 30 秒没有交互时就主动ATCIPCLOSE然后重新CIPSTART。判断连接是否失效最直接的方式是看CIPSEND后是否返回SEND OK如果返回ERROR或CLOSED说明链路已经断了必须重建。4. STM32 端代码实现状态机、串口缓冲与指令解析4.1 串口异步接收与缓冲区设计ESP8266 的 AT 固件返回数据是流式的一条 TCP 数据可能分好几包到达因此不能用简单的阻塞等待接收完成。工程里用串口2 的中断把字节逐个放进环形缓冲区主循环再从缓冲区里查找OK、SEND OK、IPD这些关键字。#define RX_BUF_MAX 512 static char rx_buf[RX_BUF_MAX]; static volatile uint16_t rx_wr 0; static volatile uint16_t rx_rd 0; void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { rx_buf[rx_wr] (char)USART_ReceiveData(USART2); rx_wr (rx_wr 1) % RX_BUF_MAX; } }rx_wr是写入指针rx_rd是读取指针两个指针相等时缓冲区为空。中断里只做写操作主循环负责读和处理这样即使 AT 响应一次性来了两百字节也不会丢。缓冲区大小 512 字节覆盖一次 HTTP 响应绰绰有余OneNet 的 JSON 响应一般不超过 256 字节。如果发现指令偶尔丢失优先把RX_BUF_MAX增大到 1024同时检查USART_Config里是否开启USART_IT_RXNE。4.2 主流程状态机整个 STM32 端业务逻辑用状态机实现避免了在阻塞等待 AT 响应时卡死整个主循环。核心状态包括 Wi-Fi 连接、TCP 连接、上报数据、等待响应、下发指令查询。typedef enum { ST_WIFI_CONNECT, ST_TCP_CONNECT, ST_SEND_REPORT, ST_WAIT_REPORT_RESP, ST_GET_CMD, ST_WAIT_CMD_RESP } app_state_t; static app_state_t state ST_WIFI_CONNECT; while (1) { switch (state) { case ST_WIFI_CONNECT: if (esp8266_cmd_wait_ok(ATCWJAP\ssid\,\pwd\, 8000)) { state ST_TCP_CONNECT; } else { esp8266_cmd_wait_ok(ATCWJAP\ssid\,\pwd\, 8000); } break; case ST_TCP_CONNECT: if (esp8266_cmd_wait_ok(ATCIPSTART\TCP\,\api.heclouds.com\,80, 8000)) { state ST_SEND_REPORT; } break; case ST_SEND_REPORT: build_report_request(http_buf); esp8266_send_data(http_buf); state ST_WAIT_REPORT_RESP; break; case ST_WAIT_REPORT_RESP: if (esp8266_find(SEND OK)) state ST_GET_CMD; break; case ST_GET_CMD: build_cmd_request(http_buf); esp8266_send_data(http_buf); state ST_WAIT_CMD_RESP; break; case ST_WAIT_CMD_RESP: if (esp8266_find(relay_on)) { GPIO_SetBits(GPIOC, GPIO_Pin_13); } else if (esp8266_find(relay_off)) { GPIO_ResetBits(GPIOC, GPIO_Pin_13); } state ST_TCP_CONNECT; break; } }esp8266_cmd_wait_ok的作用是发送一条 AT 指令并在超时时间内等待返回OK超时则返回 0。这个函数内部会不断消费环形缓冲区。esp8266_find是在缓冲区里搜索特征字符串搜索成功后自动清空缓冲区避免上一次响应残留影响下一次解析。状态机每执行完一次循环就完成了一轮“上报温湿度 拉取一次控制指令”的完整交互链路没有关闭第二次就会复用上一次的 TCP 连接。4.3 拼接 HTTP 请求和解析控制指令build_report_request函数负责把设备 ID、APIKey、温湿度值拼成合法的 HTTP 报文。这里不用第三方 HTTP 库纯字符串拼接就够了也方便你看清楚整个请求的组成。static void build_report_request(char *buf) { char body[160]; uint16_t len; snprintf(body, sizeof(body), {\datastreams\:[{\id\:\temp\,\datapoints\:[{\value\:%d}]}, {\id\:\hum\,\datapoints\:[{\value\:%d}]}]}, temp_x10, hum_x10); len strlen(body); snprintf(buf, 320, POST /devices/%s/datapoints HTTP/1.1\r\n Host: api.heclouds.com\r\n api-key: %s\r\n Content-Length: %d\r\n Connection: keep-alive\r\n \r\n %s, DEVICE_ID, API_KEY, len, body); }temp_x10和hum_x10是放大了十倍的整数温湿度值snprintf里用%d输出避免在 MCU 上直接格式化浮点数。snprintf的返回值是应该写入的字符数这里没用它算Content-Length而是单独strlen(body)因为 JSON 里有双引号snprintf返回值在极端情况下可能与实际写入长度不一致。解析下发指令时不需要引入完整的 JSON 解析器因为 OneNet 命令响应体结构固定直接搜data:后面的字符串即可。用strstr找到目标位置再移动指针逐个比对指令名。char *p strstr(rx_buf, \data\:\); if (p ! NULL) { p 8; // 跳过 data: if (strncmp(p, relay_on, 8) 0) { relay_on(); } else if (strncmp(p, relay_off, 9) 0) { relay_off(); } }这段逻辑依赖平台的响应格式OneNet 返回的 JSON 里data字段位于data:relay_onp 8刚好跳到指令字符串开头。这种方式比逐个字符解析 JSON 省内存也更容易在调试时通过串口日志确认匹配位置。缺点是如果平台升级响应格式需要同步调整偏移量所以代码里最好用宏定义这个偏移值并加一条printf输出原始响应。4.4 换芯片型号时要改哪些位置项目输入明确说当前代码在 STM32F103C8T6 运行其他 F103 型号只要改 KEIL 里的芯片型号和 Flash 容量。具体来说要动四个地方。第一Device 选项卡选择实际型号C8T6 是 64K FlashRCT6 是 256K Flash选的型号决定了编译器默认的存储布局。第二预定义宏STM32F10X_MD与STM32F10X_HD这是标准外设库的密度分类C8 和 CB 用 MDRC、RD、RE 用 HD宏和芯片不匹配时芯片外设时钟配置函数会走错分支。第三启动文件工程文件列表里startup_stm32f10x_md.s替换为startup_stm32f10x_hd.s这个文件定义了堆栈大小和中断向量表不能混用。第四Flash Download 里的 Programming Size保持与芯片一致曾遇到有人把 CBC 代码下进 RE 芯片程序能跑但写 Flash 时行为异常就是这个参数不对。改完这四个点重新编译先点亮板上 LED 确认内核时钟正常再联调 ESP8266。5. 联调排错从串口日志到上线后的稳定性技巧5.1 分层打印日志联调时把串口1 作为调试口STM32 的所有状态切换都用printf输出到串口1串口2 上透传 ESP8266 的原始返回。分层的价值在于快速定位问题属于 MCU 层、AT 指令层还是 HTTP 协议层。例如串口1 打印[TCP] connecting...串口2 能看到CONNECT说明 AT 指令执行成功如果串口1 已经发送ATCIPSEND但串口2 没有任何输出问题在串口2 的发送引脚或模块电源。[1] ATCIPSTART TCP 80... [2] CONNECT [1] SEND REPORT len108 [2] SEND OK [2] IPD,180:HTTP/1.1 200 OK...照着这个格式观察只要出现CTA/ERROR就直接锁定到具体指令。调试时建议关闭一级缓存优化避免调试器优化掉关键变量。5.2 高频异常处理对照现象可能原因处理方式上报返回 400JSON 格式或 Content-Length 不对用 PC 端工具先调通再把报文头粘贴到 C 代码电源指示灯正常但不连 Wi-Fi模块固件异常或 CH_PD 未拉高重新刷 AT 固件检查 EN 引脚电压ESP8266 偶发返回busy上一次指令未处理完每次发送 AT 指令前等待OK加大指令间隔下发指令不执行轮询 GET 命令返回为空或命令已被消费在平台手动下发并打印IPD的完整内容确认 data 字段运行几小时后不上报TCP 连接被服务器关闭未重新连接记录最后成功发包时间超时执行ATCIPCLOSE重建链路注意 OneNet 平台侧对没有数据上报的设备也有状态判断不要在代码里只连不上报。用一个定时器让继电器状态变化时立即触发上报比单纯周期性上报更能还原平台端设备实时性。5.3 增加模块级复位兜底HTTP 接入最怕 ESP8266 固件内部死机AT 指令没有任何响应时仅靠软件重发解决不了问题。把 ESP8266 的 RST 引脚接到 STM32 的一个 GPIO在状态机里维护一个连续无响应计数器超过五次就拉低 RST 100ms 再拉高强制模块冷启动然后重新执行ATCWMODE1和ATCIPSTART。这比执行一万次重发都有效也为之后换成 NB 模组连接 OneNet 保留了同样的外设复位逻辑只是 AT 指令集从 ATCIPSTART 换成 CoAP 或 UDP 的配置命令。最后提醒一点所有控制指令解析完成后立即消费平台端 ACK 消息的超时时间建议设置 3 秒超过则直接丢弃这条命令防止堆积造成后续继电器动作错乱。本文还有配套的精品资源点击获取