ESP8266+OneNet云平台:STM32数据采集与可视化完整方案 简介面向嵌入式物联网开发者的RT-Thread ESP8266接入OneNET云平台完整工程包资源源自“RT-thread学习记录004”博文适合正在学习STM32、RT-Thread实时操作系统与物联网云平台对接的开发者。压缩包共2000个文件以C源文件1035个和C头文件1085个为核心覆盖RT-Thread内核、STM32外设驱动、ESP8266通信等关键实现同时包含HTML/Markdown说明文档、Python辅助脚本、sconscript与mk等构建配置文件便于阅读源码、调整配置和重新编译。资源包约17.94 MB目录结构基本沿用RT-Thread Studio标准工程模块划分清晰。目前已有1959人学习下载。借助该资源可快速搭建开发环境理解OneNET接入流程、AT指令交互、数据上云与设备管理实现配合博客讲解还能获得排错思路适合物联网方向学生和嵌入式开发者边看边练。1. 为什么是 ESP8266 加 OneNet从本地采集到云上可视化的最短路径串口屏上的数据滚动播放人一离开工位就什么都看不见了。这是很多单片机项目从实验室往现场走时撞上的第一堵墙我前后拆过的几个 RT-thread 工程最后都落在同一个解法上STM32L4 负责采集ESP8266 负责把数据送进 OneNet 云平台平台端打开网页就能拉出折线图。这个 rar 包里的东西并不花哨但把链路铺得很完整——RT-thread Studio 工程、ESP8266 的 AT 驱动、OneNet 接入用的 APIKey 和设备 ID连 cc936.c 这类 FatFs 编码页文件都一起打包了。对正在做物联网课程设计的学生、以及想给现有 STM32 设备加远程监控的工程师来说照着这条路走比从零看协议栈省事得多。2. RTOS 下 ESP8266 的驱动挂载AT 指令通道与读写保护2.1 为什么在 RT-thread 里把 ESP8266 当成“设备”来管裸机开发时直接对串口寄存器赋值再在 while 循环里死等 AT 返回这种方式在 RTOS 下马上出问题Wi-Fi 模块一条指令的返回可能要几十毫秒到几秒阻塞线程会让采集任务一起卡死。RT-thread 的设备框架把这个问题拆成两层底层是 UART 设备驱动上层是 AT 客户端组件中间用信号量通知数据到达。一般做法是先用 rt_device_find 拿到串口设备句柄再调用 rt_device_set_rx_indicate 注册接收回调。当 ESP8266 有数据返回时中断把数据搬进缓冲区同时释放信号量。采集线程等在信号量上而不是忙等串口这样 CPU 可以让给其他任务。互斥锁 at_lock 也要加上因为 AT 指令一问一答如果两个线程同时发指令返回包就串了。提示ESP8266 与 STM32 的接线上TXD 接 RXD、RXD 接 TXD、GND 必须共地3.3V 供电尽量单独走线不要和 STM32 的 3.3V 共用一路 LDOWi-Fi 发射瞬间的电流尖峰会拉低电压。static struct rt_mutex at_lock; static struct rt_semaphore at_resp_sem; static rt_err_t uart_rx_ind(rt_device_t dev, rt_size_t size) { rt_sem_release(at_resp_sem); return RT_EOK; }这段代码是接收通知机制的核心。uart_rx_ind 是接收回调每次串口收到数据就释放一次信号量读线程拿到信号量后从串口取数据。rt_sem_release 可以在中断上下文调用但要注意信号量计数值不能无限累加所以在发送下一条指令前要先复位信号量否则残留的计数值会让下一次等待立即返回读到的是上一次的旧数据。2.2 AT 指令封装等待、拼包、超时的通用套路无论发 AT 连 Wi-Fi 还是发 MQTT 指令都建议走同一个封装函数 esp8266_send_at避免每个业务函数重复拼包逻辑。这个函数接收命令字符串、期望的返回关键字和超时时间返回 RT_EOK 表示指令执行成功返回 -RT_ETIMEOUT 表示超时。static int esp8266_send_at(const char *cmd, const char *expect, int timeout) { char buf[256]; int len 0; rt_mutex_take(at_lock, RT_WAITING_FOREVER); rt_sem_control(at_resp_sem, RT_IPC_CMD_RESET, RT_NULL); rt_device_write(uart_dev, 0, cmd, strlen(cmd)); while (timeout-- 0) { if (rt_sem_take(at_resp_sem, 1000) RT_EOK) { len rt_device_read(uart_dev, 0, buf len, sizeof(buf) - len - 1); buf[len] \0; if (strstr(buf, expect) ! RT_NULL) { rt_mutex_release(at_lock); return RT_EOK; } } } rt_mutex_release(at_lock); return -RT_ETIMEOUT; }这里有两个容易踩的细节。第一是超时计数单位timeout 在外层循环递减内层 rt_sem_take 最长等 1000ms所以 timeout10 表示最多等 10 秒。第二是用 strstr 而不是 strcmp 匹配返回因为 AT 固件的应答经常带前缀和尾部比如 OK 前面有 \r\n后面也有换行精确匹配会失败。缓冲区 buf 只有 256 字节收到较长的 IP 地址列表时要注意 len 不能越界读取长度要留一个字节给字符串结束符。esp8266_send_at 封装好后联网三步走就可以写得很干净设置 Station 模式、连接 Wi-Fi、建立 TCP 连接。下面这张表是我调试 ESP8266 时常用的指令序列不同固件版本返回格式略有差异建议先串口助手跑一遍再写进代码。步骤指令期望返回说明1ATCWMODE1OK1 表示 Station 模式2ATCWJAPssid,passwordWIFI GOT IP连上路由器并拿到 IP3ATCIPSTARTTCP,183.230.40.39,80CONNECT OK连接 OneNet 的 HTTP 端口4ATCIPSEND长度准备发送数据长度是字节数5ATCIPCLOSEOK关闭当前连接注意第二行的期望返回是“WIFI GOT IP”而不是 OKESP8266 连上 Wi-Fi 后还要等 DHCP 分配地址这个提示出现才能说明链路真正通了。第 4 步 CIPSEND 的期望返回是大于号提示符AT 固件版本不同返回格式也不同如果发现匹配不上先用串口助手裸跑一遍指令看真实返回再决定 expect 参数填什么。2.3 上电时序先等模块就绪再发指令如果上电后立即发 AT大概率收到空返回或乱码因为 ESP8266 内部固件还没启动完成。我一般先拉低 EN 脚复位一次等待至少 1 秒再发送“AT\r\n”确认模块响应。这个简单动作能避免一半的“指令超时”假象。如果连续三次 AT 都无响应优先检查模块供电和串口接线而不是怀疑代码。Wi-Fi 模块瞬态电流较大用开发板自带 LDO 很容易掉电重启具体表现就是指令发出去后模块静默或返回乱码。3. OneNet 接入的前置配置APIKey、设备ID 与鉴权字段映射3.1 控制台里先创建设备再开始写代码代码侧接入 OneNet 需要三个东西设备 ID、APIKey、鉴权信息。APIKey 相当于访问密钥在 OneNet 控制台的产品详情页里生成设备 ID 是设备创建后自动分配的编号。鉴权信息是设备自己上报的身份标识通常用 MAC 地址或自定义字符串设备首次上报时云端用它来识别设备身份。在控制台的操作顺序一般是注册登录、创建产品、添加设备、生成 APIKey。设备创建后记录下设备 ID 和 APIKey这两个值一个用于 URL 路径一个用于 HTTP 请求头。下面的表格把控制台字段和代码里的宏定义对应起来避免填错位置。控制台字段示例值代码里的宏用途设备ID5987123456ONENET_DEVICE_IDURL 路径中的设备标识APIKey4Tk9vPbA7z0l3X...ONENET_API_KEYHTTP 请求头鉴权数据流IDtemp / humiDATASTREAM_ID区分不同折线图曲线3.2 常量组织别把 APIKey 写死在业务函数里把鉴权信息散落在各个函数里后面维护起来很痛。通常的做法是集中放到一个头文件或配置文件中。用宏定义虽然简单但不利于后续接入多台设备建议至少用结构体封装一下这样换设备时只改一处。typedef struct { char device_id[16]; char api_key[40]; char datastream[8]; } onenet_config_t; static const onenet_config_t onenet_cfg { .device_id 5987123456, .api_key 4Tk9vPbA7z0l3X..., .datastream temp, };这里的 device_id 是数字字符串不要转成 int 使用因为 URL 拼接时还是要字符串。api_key 长度按控制台实际显示填写多一位少一位都会导致鉴权失败。datastream 指的是云平台上的数据流名称OneNet 的机制是云端收到数据点时会根据 JSON 里的 id 自动创建数据流所以本地代码里的 datastream 字段可以提前规划好不要随便改改了就相当于换了条曲线。3.3 接入方式HTTP 还是 MQTT怎么选OneNet 支持多种设备接入协议。对于采集频率不高、几十秒上传一次的环境监测设备HTTP 接入足够实现最简单直接用 TCP 连接 80 端口发请求即可代码量小且容易调试。如果设备需要长时间在线、服务端要主动下发命令、或者要控制功耗MQTT 更合适通过 ATCIPSTART 建立 TCP 连接后在应用层跑 MQTT 协议心跳间隔要按模块功耗权衡。另外NB 模组接入 OneNet 的思路也是这两条只是底层的 TCP 链路换成了运营商网络APIKey 和设备 ID 的鉴权逻辑不变所以这套代码里的鉴权结构放在 NB 模组上同样能复用。4. STM32L4 采集与本地缓存HAL 外设、FatFs 和编码页的配合4.1 工程里的 HAL 文件对应哪些功能这个工程里出现 stm32l4xx_hal_adc.c、stm32l4xx_hal_tim.c、stm32l4xx_hal_i2c.c、stm32l4xx_hal_sd.c 和 stm32l4xx_hal_uart.c实际上就是采集链路的五个环节ADC 采集模拟量、TIM 定时触发采样、I2C 读取传感器寄存器、UART 与 ESP8266 通信、SD 卡负责离线存储。stm32l4xx_hal_tim.c 同时还要承担本地 RTC 校准或 LED 闪烁这类周期性任务。ff.c 是 FatFs 文件系统的核心实现cc936.c、cc949.c、cc950.c、cc932.c 是不同语言的代码页转换表。把 cc936.c 编进去之后FatFs 才支持简体中文文件名否则用 f_open 打开“数据.csv”这类中文路径会返回 FR_INVALID_NAME。cc949.c 是韩文、cc950.c 是繁体中文、cc932.c 是日文只做中文设备的话 cc936.c 就够了其他三个文件可以移除能省几十 KB 的 Flash 空间。4.2 用定时器触发 ADC 采样而不是 while 轮询如果直接在 while 循环里调用 HAL_ADC_Start 再读数据采样间隔会受代码执行时间影响占空比抖动不说CPU 还被占死。RT-thread 下通常的做法是 TIM 产生更新事件触发 ADC 转换ADC 完成后再由 DMA 把结果搬到内存整个过程不占用 CPU。#define ADC_BUF_LEN 16 static uint16_t adc_buf[ADC_BUF_LEN]; static volatile uint32_t adc_value 0; void adc_tim_dma_init(void) { HAL_TIM_Base_Start(htim2); HAL_ADC_Start_DMA(hadc1, adc_buf, ADC_BUF_LEN); } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { uint32_t sum 0; for (int i 0; i ADC_BUF_LEN; i) { sum adc_buf[i]; } adc_value sum / ADC_BUF_LEN; }这段代码里HAL_ADC_Start_DMA 的第三个参数是每次转换的样本数。回调里做软件平均滤波对工频干扰比较有效。DMA 模式要求 ADC 和 DMA 的中断优先级配置合适否则数据传输快于 CPU 处理会导致缓冲区覆盖。adc_value 用 volatile 修饰是因为它在中断里被修改主循环读取时要保证每次都从内存重新取值不要被编译器优化到寄存器里。提示如果想让采样频率精确可调直接改定时器的分频和重载值。以 80MHz 的 APB1 时钟为例分频设为 80、重载值设为 10000对应 100Hz 的触发频率。换算公式是 freq 80MHz / (prescaler 1) / (period 1)改参数时把两个值都减 1 是新手最容易漏的。4.3 断网时数据往哪放FatFs 写 CSV 的完整套路Wi-Fi 不稳定是常态不能因为一次断网就把采集数据丢掉。常见的设计是有网时直接上传断网时写入 SD 卡等网络恢复后再补传。FatFs 在 RT-thread 里的挂载路径一般是 /sdcard写文件前先 f_mount 挂载文件系统再 f_open 打开指定文件。FIL file; FRESULT res; res f_mount(fs, /sdcard, 1); if (res FR_OK) { res f_open(file, /sdcard/data.csv, FA_OPEN_ALWAYS | FA_WRITE); if (res FR_OK) { f_lseek(file, f_size(file)); f_printf(file, %s,%.1f,%.1f\r\n, time_str, temp, humi); f_close(file); } } f_mount(NULL, /sdcard, 0);f_mount 的第三个参数为 1 表示立刻挂载并扫描文件系统。f_open 的 FA_OPEN_ALWAYS 表示文件不存在就创建存在就打开配合 f_lseek 把写指针移到文件尾部这样一次采样追加一行不会覆盖历史数据。注意这里 f_printf 是 FatFs 内部实现的格式化输出函数不是 C 标准库的 printf浮点输出能力取决于 ffconf.h 里的配置如果打印结果全是 0去检查 ffconf.h 的浮点数支持宏是否开启。最后一行 f_mount(NULL, /sdcard, 0) 是卸载文件系统。如果你后期要拔卡必须先卸载否则 FatFs 缓冲区里的数据没有回写文件系统可能出现损坏。实际项目中不需要每次写完就卸载频繁挂载卸载会增加损耗按自己的读写频率决定即可。5. 数据点上传与云端折线图HTTP POST 请求的构造与应答解析5.1 数据流和数据点的模型要先想明白OneNet 的上云模型是“设备—数据流—数据点”。一个温湿度设备可以建温度、湿度两条数据流每条数据流由若干数据点组成。数据点的 value 是数值at 是时间戳。云端画折线图时就是把同一条数据流的点按时间排序画线所以一天下来曲线不对先查时间戳而不是上云方式。为了省事可以把多个数据流的数据点放在一次 HTTP 请求里提交。OneNet 要求的 JSON 结构是 datastreams 数组每个 stream 有自己的 idid 下面是 datapoints 数组。数据流没提前创建也没关系云端收到数据点后会自动建流这也是很多课程设计用 HTTP 接入图省事的原因。用 MQTTX 这类工具调试时能看到同样的 JSON 结构通过 MQTT 的 topic 上报本质都是往设备下的数据流里塞数据点。5.2 用 curl 验证一遍再写进嵌入式代码在 Linux 或者 Windows 的 Git Bash 里先用 curl 验证接口能省掉很多环境问题。我习惯先做一次成功请求再故意把 api-key 改错一次这样能看到正常返回和错误返回两种结构方便后面写解析逻辑。curl -X POST \ http://api.heclouds.com/devices/5987123456/datapoints?type3 \ -H api-key: 4Tk9vPbA7z0l3X... \ -H Content-Type: application/json \ -d { datastreams: [ { id: temp, datapoints: [ {value: 26.5} ] } ] }URL 里的 type3 表示 JSON 格式老版本还支持 type1 和 type2 的 key-value 格式新设备建议直接用 type3。返回的响应体是一个 JSONerrno 为 0 且 error 为 succ 表示成功错误时会返回对应的错误码。设备 ID 和 api-key 要替换成自己的注意 api-key 不是设备终端上的鉴权信息这两个字段在 HTTP 请求里的位置完全不同填反了会一直报权限错误。5.3 在 RT-thread 里用 webclient 组件发送RT-thread 的 webclient 组件提供了现成的 POST 接口配合 cJSON 库拼装 JSON 再发给 OneNet。发送完成后要立刻读响应因为 OneNet 返回的内容如果服务器端关闭了连接晚一步可能就 EOF拿到的响应体是空的。#include webclient.h #include cJSON.h cJSON *root cJSON_CreateObject(); cJSON *streams cJSON_AddArrayToObject(root, datastreams); cJSON *stream cJSON_CreateObject(); cJSON *points cJSON_AddArrayToObject(stream, datapoints); cJSON *point cJSON_CreateObject(); cJSON_AddNumberToObject(point, value, 26.5); cJSON_AddItemToArray(points, point); cJSON_AddItemToObject(stream, id, cJSON_CreateString(temp)); cJSON_AddItemToArray(streams, stream); cJSON_AddItemToObject(root, datastreams, streams); char *json_str cJSON_PrintUnformatted(root); cJSON_Delete(root);cJSON 的 AddNumberToObject 接受 double 类型温湿度数据直接传浮点变量即可。这里把 JSON 的构建和发送拆开先用 cJSON_PrintUnformatted 生成紧凑字符串检查长度后再通过 webclient_post 发送。新手经常犯的错误是 AddItemToArray 后忘了把 stream 加进 streams导致 JSON 里少一层结构服务端解析不了。每次中断上报前 cJSON_Delete 释放内存这个组件默认动态分配内存跑几天后内存碎片会越来越大建议用内存池或者定期重启任务。POST 请求的核心参数是 URL、header、body。webclient 的 header 里要带 api-key 和 Content-Type: application/json这个 header 拼错是 401 的重灾区。响应体里有 Content-Length 字段读取响应时按这个长度读不要指望服务端关闭连接来标识结束否则会阻塞在 recv 上直到超时。5.4 服务端主动下发命令的处理方式除了上传数据OneNet 还支持设备轮询取命令或 MQTT 推送命令。HTTP 方式下设备端每隔几秒去查询一次 cmds 接口拿到命令字符串后自行解析执行。轮询间隔不宜太短推荐 30 秒起步否则虽然不产生下行流量但 HTTP 请求本身的开销会把模块功耗拉高。MQTT 方式则通过订阅固定 topic命令实时性更好适合需要立刻响应下发的场景。判断用哪种方式的一个简单标准设备是否长时间在线在线时间长就走 MQTT短连接采集就走 HTTP 轮询。6. 联调收尾的几个坑AT 乱码、断线重连与时间戳漂移6.1 AT 返回乱码与指令超时的排错顺序拿到代码先不要急着改业务逻辑用串口助手把 ESP8266 单独拉出来发 AT 指令看原始返回。乱码的第一嫌疑是波特率不匹配AT 固件默认一般是 115200RT-thread 工程里 msh 控制台也可能用了 115200两者容易混淆。其次是接地模块与 STM32 之间 GND 没连会导致电平参考不一致出现随机错字节。用 ATGMR 查固件版本如果版本过低或者回显异常优先更新 AT 固件再继续调。AT 指令超时还有一个隐蔽原因代码里加了流控但硬件上没接 CTS/RTS 引脚模块等流控信号超时后直接丢弃了指令。6.2 断线重连用有限状态机管理连接状态Wi-Fi 断线是物联网设备的常态不要在采集循环里层层 if 嵌套判断重连。简单的办法是定义连接状态枚举根据状态决定下一步动作。状态机的好处是每个时刻只有一个动作在执行不会出现“正在重连”和“正在上报”同时跑的情况。typedef enum { LINK_DOWN, LINK_WIFI_CONNECTING, LINK_TCP_CONNECTING, LINK_ONLINE } link_state_t; static link_state_t link_state LINK_DOWN; void link_task(void) { int retry 0; while (1) { switch (link_state) { case LINK_DOWN: if (esp8266_send_at(ATCWJAP\ssid\,\pass\, WIFI GOT IP, 15) RT_EOK) { link_state LINK_TCP_CONNECTING; retry 0; } else if (retry 10) { rt_thread_mdelay(30000); } break; case LINK_TCP_CONNECTING: if (esp8266_send_at(ATCIPSTART\TCP\,\183.230.40.39\,80, CONNECT OK, 10) RT_EOK) { link_state LINK_ONLINE; } else { link_state LINK_DOWN; } break; case LINK_ONLINE: /* 定时上报数据失败则回到 LINK_DOWN */ break; } rt_thread_mdelay(500); } }retry 变量做退避重连超过 10 次后等待 30 秒再尝试避免 Wi-Fi 疯狂扫描导致模块发热。运行时如果发现某次上报返回“closed”把 link_state 拉回 LINK_DOWN让状态机重新走流程。注意状态机跳转后不要马上发下一条 AT模块内部切状态需要时间每次调用 esp8266_send_at 本身就有等待所以这个 500ms 的 mdelay 可以保证指令节奏稳定。6.3 时间戳单位不对折线图全乱OneNet 的数据点支持 at 字段不传时云端自动用服务器时间但一旦数据量大或需要本地记录补传时时间戳就得自己带。at 字段接受 ISO8601 字符串或者 Unix 时间戳注意时间戳精度是秒不是毫秒。RT-thread 的 time() 返回秒级时间但前提是系统时间已经同步很多板子上电后 RTC 是 1970 年不校准的话上报的数据点会被云端当作非法时间丢弃。如果不用 NTP简单的做法是用编译时间宏做初次校准再靠本地 RTC 维持。设备连上 Wi-Fi 后用 ATCIPSNTPCFG 命令配置 SNTP 服务器再通过 ATCIPSNTPTIME? 查询网络时间解析后写入芯片 RTC这样上传的数据点时间戳才能和云端对齐折线图的横轴才不会出现锯齿或整体偏移。云平台折线图乱成一团时先抓一帧上传的 JSON核对 at 字段是否落在当前时间附近而不是怀疑网络丢包这是定位时间类问题最快的路径。本文还有配套的精品资源点击获取