STM32+ESP8266+HX711称重上云OneNET全链路开发指南
2026/9/16 15:56:31 网站建设 项目流程

简介:一套基于STM32的综合性物联网源程序,集成ESP8266 Wi-Fi模块、OneNet云平台与HX711重量传感器,面向嵌入式开发者和物联网学习者。工程实现了Wi-Fi连接管理、HX711重量数据采集与滤波处理、MQTT协议接入OneNet并实时上传,同时提供LCD显示、定时器、系统时钟等外设驱动示例,可灵活迁移到智能称重、健康监测、工业物料监控及高校实验教学等场景。压缩包共142个文件,以C源码(.c)与头文件(.h)为核心,包含HX711、SPI、MQTT等底层驱动,另附Keil工程文件(.uvprojx/.uvoptx)、编译输出(.axf/.hex/.map)及STM32CubeMX配置(.ioc),整体8.87MB,工程结构清晰。目前已有188人学习,既能作为毕设或课设的参考实现,也可当作快速上手STM32+ESP8266+云平台开发的调试范本。

1. 一套 stm32-esp8266-onenet-HX711 称重上云的完整链路

标题把整条数据链路的四个关键角色都列出来了:STM32 负责采集和业务逻辑,ESP8266 负责联网,OneNET 承载设备接入和数据展示,HX711 把电桥微伏级信号转成数字量。对很多从单片机转物联网的工程师来说,这四个模块单独跑通都不难,真正让项目停摆的是串口帧定义、MQTT 连接鉴权参数和 HX711 的校准细节这三处。这套方案适合做智能称重料斗、畜牧秤、垃圾桶满载检测这类需要周期上报重量数据的场景,也能直接延伸到仓储和产线计量。下面按数据流方向把每一步做成可直接复刻的模块,同时讲清楚参数为什么这么设,以及常见误用会踩在哪里。

2. HX711 称重采集与校准:STM32 侧时序、引脚和有效位数

2.1 HX711 与 STM32 的接线表和电源注意点

HX711 对外只有两个数字引脚:DOUT 数据输出和 SCK 时钟输入。它与 STM32 的连接不需要 SPI 外设,也不涉及地址配置,用两个普通 GPIO 模拟时序即可。以 STM32F103C8T6 为例,工程里惯用的是 PA0/PA1 这一组引脚,但需要注意 PA0 同时也是 WKUP,如果代码里开了待机模式唤醒,就会与 HX711 读取冲突。

HX711 引脚接 STM32F103C8T6说明
VCC3.3V模块内部已有稳压,3.3V 和 5V 都能工作
GNDGND需要与 STM32 共地
SCKPA1时钟输入,由 MCU 控制
DOUTPA0数据输出,空闲时为高电平

连接完成后,优先确认逻辑电平是否匹配。HX711 的数字引脚在高电平状态下若被拉到 VCC,而 STM32 GPIO 输出 3.3V,一般可以直接识别,但如果模块被 5V 供电且 SCK 内部没有钳位,长期工作有输入过压风险。稳妥做法是 HX711 也用 3.3V 供电,采样率和灵敏度都不会因此受影响。

对 HX711 有效位数要有合理预期:内部是 24bit Sigma-Delta ADC,实际有效位数受 RATE 引脚采样率、模拟电源纹波和 PCB 布局影响。RATE 接 GND 时采样率 10Hz,接 VCC 时 80Hz;10Hz 下典型有效位数约 20bit,80Hz 会掉到 17~18bit 左右。这类称重场景 10Hz 完全够用,刻意提高采样率换来的只是精度下降。

2.2 用 STM32 读取 HX711 的驱动代码与增益设置

下面给一段标准库风格的驱动代码,GPIO 映射到 PB0/PB1,方便和 2.1 里的 PA0/PA1 错开演示引脚改动时只需改宏。

// HX711 读取,PB0: SCK,PB1: DOUT #define HX711_SCK_H GPIOB->BSRR = GPIO_Pin_0 #define HX711_SCK_L GPIOB->BRR = GPIO_Pin_0 #define HX711_DOUT GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) static int32_t last_raw = 0; int32_t HX711_Read(void) { uint32_t raw = 0; uint32_t timeout = 100000; // 等待 DOUT 由高变低,表示转换完成 while (HX711_DOUT && timeout--); if (timeout == 0) { // 传感器断开或接线异常时返回上一次结果,避免主循环卡死 return last_raw; } // 读 24 位数据,MSB 在前 for (uint8_t i = 0; i < 24; i++) { HX711_SCK_H; delay_us(1); raw <<= 1; if (HX711_DOUT) raw++; HX711_SCK_L; delay_us(1); } // 第 25 个脉冲:通道 A,增益 128 HX711_SCK_H; delay_us(1); HX711_SCK_L; delay_us(1); // 24bit 补码转 int32 if (raw & 0x800000) { raw |= 0xFF000000; } last_raw = (int32_t)raw; return last_raw; }

逻辑说明:第 25 个脉冲的数量决定下一次转换的通道和增益,1 个脉冲是通道 A 增益 128,2 个是通道 B 增益 32,3 个是通道 A 增益 64。同一个程序里所有读取必须保持一致,否则标定斜率前后不统一。delay_us(1)要大于 HX711 要求的最小高/低电平宽度,按经验 1us 起步,SCK 频率不要超过 1MHz,否则偶发错位会很难复现。

参数说明:读函数是阻塞式的,10Hz 速率下最坏要等 100ms 的 DOUT 拉低。如果主循环里还有按键扫描或显示刷新,建议把 HX711 读取放到定时器节拍里,或者用“非阻塞等待 + 上一次结果”的结构,上面的timeoutlast_raw已经处理了传感器断开导致死循环的问题,实际项目里这两行不该省。

2.3 毛重、去皮与斜率:称重值换算和校准参数

读取到的是 raw 码,不是重量。设空秤时 raw 为zero,放上已知砝码后 raw 为raw_known,则斜率scale = (raw_known - zero) / mass_known,任意时刻重量weight = (raw - zero) / scale

单次读取波动大,称重计算前必须做滑动平均。采样次数太少精度差,太多则响应变慢,静态称重取 10 次,动态毛重监控取 5 次左右。

#define SAMPLE_N 10 int32_t HX711_Average(void) { int64_t sum = 0; for (int i = 0; i < SAMPLE_N; i++) { sum += HX711_Read(); } return (int32_t)(sum / SAMPLE_N); }

平均次数SAMPLE_N是 HX711 读函数被调用的次数,不是采样率。如果一次平均耗时 100ms×10 约 1 秒,对静态称重足够。想要更快就用 5 次,但零点波动会从 ±2g 放大到 ±5g 左右。

校准参数直接用一个带两个全局变量的函数维护:

float hx711_offset = 0; // 空秤 raw 值 float hx711_scale = 1000; // raw/kg 斜率,首次上电给默认值 void HX711_Calibrate(float known_kg) { hx711_offset = HX711_Average(); delay(500); // 此时秤台上要放好已知重量的砝码 float raw_now = HX711_Average(); hx711_scale = (raw_now - hx711_offset) / known_kg; }

提示:HX711 零点会随温度漂移,工业场景建议每次上电后让设备静置 2 分钟再做去皮,或者在上位机里保留一键归零入口。

3. STM32 与 ESP8266 串口通信:数据帧设计与避坑指南

3.1 为什么选 UART 直连而不是 AT 指令透传 MQTT

STM32 和 ESP8266 之间有两种主流接法。一种是 STM32 给 ESP8266 发 AT 指令,让 AT 固件自己跑透传或 MQTT;另一种是 ESP8266 刷 NodeMCU/Arduino 固件,自己连 Wi-Fi 和 OneNET,STM32 只把重量数据通过串口交给它。

常见做法是用第二种。AT 指令方式在 OneNET 场景下的痛点很明确:不同版本的 ESP8266 AT 固件对 MQTT 透传支持差异大,鉴权参数拼接、心跳超时、断线重连逻辑都依赖黑盒固件,出问题时串口日志只有ERROR,很难判断是哪一段的问题。而 Arduino IDE 开发 ESP8266 时,PubSubClient库是源码级可见的,OneNET 连接参数之间不匹配时可以直接在 Wi-Fi 连接和 MQTT 连接之间分段定位。

开发 ESP8266 侧的 NodeMCU 板卡,只需要在 Arduino IDE 中安装 esp8266 开发板包,选择 NodeMCU 或 Generic ESP8266 Module 编译。注意板载 LED 一般挂在 GPIO2,别把它当成状态灯后误以为程序没跑起来。

3.2 STM32 侧发送帧的格式与串口发送代码

串口通信最容易踩的坑是两边对帧边界理解不一致。直接用一句printf("W:%.2f\n", weight)开发最快,但 WiFi 抖动、串口电平噪声都会让字符串断成半行,ESP8266 侧解析字符串极其痛苦。工程里我一般用二进制帧,帧结构如下:

0x5A 0xA5 | len | type | payload | checksum len = type + payload 的字节数 type = 0x01(称重数据) payload = int32_t raw,int32_t weight_10g(单位 0.1g) checksum = 从 len 到 payload 末字节的异或

int32 统一用小端,STM32 和 ESP8266 都是小端 CPU,省掉字节序转换。STM32 侧串口发送函数:

void Send_WeightFrame(int32_t raw, int32_t weight_10g) { uint8_t buf[16]; buf[0] = 0x5A; buf[1] = 0xA5; buf[2] = 9; // type(1) + payload(8) buf[3] = 0x01; // 称重数据帧 memcpy(buf + 4, &raw, 4); memcpy(buf + 8, &weight_10g, 4); uint8_t sum = 0; for (int i = 2; i < 12; i++) { sum ^= buf[i]; } buf[12] = sum; for (int i = 0; i < 13; i++) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, buf[i]); } }

参数说明:波特率选 115200 8N1,这个速率下单帧 13 字节耗时约 1.1ms,对 10Hz 的 HX711 数据来说绰绰有余。weight_10g使用 0.1g 为单位的整数,避免浮点数在帧里造成解析不一致。校验用异或而非累加和,主要是代码表达更直观,出问题时能一眼看出边界在哪里。

3.3 ESP8266 侧接收解析状态机与校验处理

ESP8266 侧不能用while(Serial.available())读一帧就认为数据完整,串口数据到达时间不确定,WiFi 模块的中断和堆栈处理会把一帧拆成多个片段到达。必须用状态机逐字节消费。

static uint8_t rx_state = 0; static uint8_t rx_buf[32]; static uint8_t rx_len = 0; static uint8_t rx_cnt = 0; void handleSerial() { while (Serial.available()) { uint8_t b = Serial.read(); switch (rx_state) { case 0: if (b == 0x5A) rx_state = 1; break; case 1: if (b == 0xA5) rx_state = 2; else rx_state = 0; break; case 2: rx_len = b; if (rx_len + 2 > sizeof(rx_buf)) { // 长度异常,复位 rx_state = 0; break; } rx_cnt = 0; rx_buf[0] = b; rx_state = 3; break; case 3: rx_buf[++rx_cnt] = b; if (rx_cnt >= rx_len + 1) { // +1 为 checksum rx_state = 0; processFrame(rx_buf); } break; } } }

逻辑说明:状态机的核心是让每一帧从帧头开始重新对齐。case 3rx_len + 1表示 type、payload 和 checksum 的总长,processFrame内部先按同样的异或算法校验,校验不通过直接丢帧,不在串口里打印错误信息,避免打印过程再次干扰接收时序。

这个方案把串口协议和 MQTT 解耦,后续如果要换 LoRa 或 4G 模块,STM32 侧代码一行都不用改,只要 ESP8266 侧换一个网络驱动层。

3.4 串口参数与供电对通信稳定性的影响

串口波特率从 9600 改成 115200 后帧间隔变短,ESP8266 如果把Serial.print调试信息混在同一个串口里,会出现数据竞争。调试信息要单独走 SoftwareSerial,或者用Serial0之外的另一组引脚。

另一个被忽视的点是 ESP8266 发射瞬间电流可达 300mA,如果和 STM32、HX711 共用一个 LDO,电压跌落会让 STM32 串口的 TX 电平出现毛刺,帧校验偶尔失败。常见做法是 ESP8266 的 VCC 单独走一路电源,或者至少让 STM32 的 3.3V 与 ESP8266 的 3.3V 之间串一个磁珠和一个 100uF 电容,用示波器看串口波形时差距非常明显。

4. ESP8266 用 MQTT 上报 OneNET:APIKey、Topic 与数据点格式

4.1 OneNET 设备创建与 APIKey 生成流程

OneNET 的接入方式按产品类型分为多协议接入和基础套件等,老工程里最常见的是“多协议接入-MQTT”。操作路径是:登录开发者中心,创建一个产品,协议选择 MQTT;进入产品后添加一个设备,记录设备 ID;在产品概况或 APIKey 管理页面生成并复制 APIKey。

最容易搞混的是 APIKey、设备 ID、产品 ID 三个值的用途,它们在 MQTT 连接参数里各占一个位置:

OneNET 概念示例MQTT 连接参数位置
产品ID如 123456username
设备ID如 789012client_id
APIKey一段十六进制混合字符串password

新版 OneNET 物联网开放平台参数体系有差异,client_id用设备名称,password用 token,接入地址也不同。如果用的是新版,进入产品开发文档查看mqtts.heclouds.com对应的端口,再按上面的位置替换参数即可。

4.2 ESP8266 连接 OneNET MQTT 的代码和参数表

ESP8266 侧用PubSubClient库,连接代码的核心部分如下:

#include <ESP8266WiFi.h> #include <PubSubClient.h> const char* ssid = "your_wifi"; const char* password = "wifi_pass"; // 老版 OneNET MQTT 参数 const char* broker = "183.230.40.39"; const int port = 6002; const char* clientId = "789012"; // 设备ID const char* userName = "123456"; // 产品ID const char* passwd = "你的APIKey"; // 产品或设备对应的APIKey WiFiClient espClient; PubSubClient mqtt(espClient); void mqttReconnect() { while (!mqtt.connected()) { if (mqtt.connect(clientId, userName, passwd)) { // 连接成功后可以订阅下行命令 topic } else { delay(2000); } } }

参数说明:mqtt.connect的三个参数顺序是 clientId、username、password,颠倒顺序会被 OneNET 静默断开。OneNET 校验这三项,任何一个为空或填错都会导致连接失败,而且 Wi-Fi 本身是通的,现象是连上了又断、过一会儿又连。如果使用新版 OneNET,broker 改成mqtts.heclouds.com、port 改成 1883,client_id填设备名称,password填设备 token。

连接建立后,mqtt.loop()必须放在主loop()里周期性调用,否则底层 WiFiClient 无法维持 TCP 连接,运行几分钟后会掉线且不重连。

4.3 数据流命名、payload 与 OneNET 折线图绘制的对应关系

数据上报走发布主题。老版 OneNET MQTT 的上行主题是$dp,payload 是一个 JSON 数组,数组中的datastreams定义数据流名和数据点:

String buildPayload(float weight_kg) { String json = "[{\"datastreams\":[{\"id\":\"weight\",\"datapoints\":[{\"value\":"; json += String(weight_kg, 3); json += "}]}]}]"; return json; }

发布时直接调用:

String payload = buildPayload(weight_kg); mqtt.publish("$dp", payload.c_str());

对应关系说明:OneNET 的折线图在数据展示页是按数据流 ID 拉数的,图表显示的名称取决于平台中创建的数据流名称,需要与 payload 里的id完全一致。这里用英文字段weight,单位在创建数据流时填kg,这样字段和单位都对齐。payload 里的value必须是数字,不能带单位字符串,否则平台会把数据存成字符串类型,折线图无法绘制。

4.4 QoS、上报频率和断线重连参数

PubSubClient默认 publish 使用 QoS 0,OneNET 老版 MQTT 对 QoS 1 的兼容性有限,保持 QoS 0 并靠上报频率兜底是更稳的配置。上报频率需要结合场景和平台数据点配额:静态称重 5~15 秒一帧足够,如果按 100ms 上报,数据点数量会快速消耗配额;如果间隔超过 30 秒,折线图会变得稀疏,监控体验变差。

推荐做法是周期上报加阈值变化上报双触发:

if (abs(weight_kg - last_weight) >= 0.05 || millis() - last_send >= 15000) { mqtt.publish("$dp", buildPayload(weight_kg).c_str()); last_weight = weight_kg; last_send = millis(); }

参数说明:0.05是重量变化死区,单位 kg,静止时 15 秒一条数据,有人搬动物体时几乎实时上报。15000是最大上报周期,单位 ms,可以按平台配额放宽。断线重连的间隔用 2 秒,太短会对 OneNET 造成连接风暴,太长会导致离线恢复慢。

5. 整机联调与精度验证:云端校准下发和数据对拍方法

5.1 利用 OneNET 下行命令在线校准 HX711

烧录次数越少,批量交付越可控。传感器换规格、机械支撑结构调整都会让斜率变化,常见做法是把校准参数做成 OneNET 下行命令,不在代码里改。ESP8266 在连接成功后订阅命令主题,老版 OneNET 的通用格式是$creq/设备ID,具体以平台控制台主题说明为准。

回调里收到命令字符串后,把校准参数通过串口帧转发给 STM32,协议沿用第 3 章二进制帧,type 改0x02,payload 放两个 int32 类型的zeroscale。STM32 收到后直接更新全局校准变量,立即生效。这样产线上只需要把标准砝码放上去,在 OneNET 控制台发一条命令,不用开盖烧录。

需要注意首次上电时hx711_scale要有一个默认值,防止未校准状态重量计算异常。常见做法是把默认斜率写进 Flash,云端校准只做覆盖。

5.2 用串口日志、云端数据流和标准砝码做三级对拍

校准完成后,不要直接看 OneNET 折线图就说“准了”。我一般做三级对拍:第一级,STM32 的串口打印原始 raw 值和换算后的公斤数;第二级,ESP8266 的串口监视器打印它实际 publish 的 payload;第三级,OneNET 的“设备日志”或数据流查询看最后一条数据点。

三个位置的值应该一致。第一级准确、第二级不对,问题在串口帧解析或字节序;第二级准确、第三级不对,问题在 MQTT 连接参数或数据流 ID。整个链路都对齐才能进入精度评估。

对拍用的砝码建议选额定秤量一半左右的重量,比如量程 100kg 用 50kg 砝码,计量的线性误差最小。对拍记录要在设备上电 10 分钟后做,避开 HX711 刚上电时的温漂。

5.3 三个高频故障的定位与修复

读值一直为 0 且不变化。先量 HX711 的 VCC 和 DOUT 波形,SCK 上应有连续脉冲;再确认模块背面是否有跳线让 DOUT 被拉低,有些模块需要把 DOUT 上拉电阻焊上才能正常输出。

读数漂移,零点随时间缓慢变大。这是电桥温漂和模块内参考电压漂移叠加的结果,先把采样平均值从 10 提到 20,并在 HX711 模拟电源两端并一个 100uF 钽电容。如果仍然漂移,考虑在 STM32 侧每 10 分钟做一次空秤自动修正,但只修正偏移量,不要动斜率。

MQTT 连不上但 Wi-Fi 正常。检查 OneNET 三要素是否一一对应,特别容易犯的错误是拿同一个 APIKey 去连另一个产品的设备。老版 OneNET 的 APIKey 属于产品级,设备 ID 属于设备级,跨产品使用时会在 MQTT connect 阶段被拒。

最后提醒一个长期稳定性细节:HX711 的参考电压由 VAVDD 决定,VCC 电源纹波会直接折算成重量误差。给 ESP8266 和 HX711 分开供电,ESP8266 发射瞬间的电流尖峰会被墙在电源通路之外,再用示波器量 SCK/DOUT 上的噪声幅度,就能理解为什么模块单独滤波电源能省掉后面一大堆调参时间。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询