STM32+BC260Y+DHT11温湿度上报OneNET全流程实战
简介:一套面向STM32与NB-IoT物联网初学者的完整工程实践源码,围绕意法半导体STM32F103C8T6微控制器、中国移动BC260Y NB-IoT模块与低成本DHT11温湿度传感器,完整演示如何通过MQTT协议将采集到的温湿度数据上报至OneNET云端,实现从传感器采集、MCU处理到NB-IoT无线传输的端到端数据链路。压缩包内共151个文件、3.42MB,以C语言源代码(.c/.h)为主体,辅以Keil工程配置(uvprojx/uvoptx)、编译链接产物(axf/hex/map)、依赖与配置辅助文件(.d/.crf/.ini)及少量说明文档(txt/htm),文件类型覆盖源码阅读、工程编译、烧录与运行验证等多个环节,目录划分清晰,便于直接打开工程学习。目前已有1433人学习下载。工程覆盖MCU串口驱动、BC260Y模块AT指令交互、DHT11单总线时序读取等关键代码,同时涉及OneNET设备接入、MQTT发布/订阅、低带宽高可靠性传输机制等物联网核心知识点;读者可对照硬件连接与代码注释梳理完整数据链路,并在此基础上快速移植到自己的温湿度监测、智能家居或环境采集项目中,是嵌入式与物联网入门及课程设计颇具参考价值的实战资料。 最近整理项目笔记,把手头这套基于STM32+BC260Y+DHT11上报温湿度到OneNET的流程完整梳理了一遍。这套结构在物联网毕设、产品原型和环境监测节点里非常常见:单片机采集传感器数据,NB-IoT模组负责联网,最终把数据送到云平台。链路看似简单,但从硬件接线、传感器时序、模组AT指令到MQTT报文,每一步都有不少细节,很多朋友卡在中间某一段,往往是因为只看了孤立教程,没有把整条链路串起来。这篇文章就以实际项目为背景,从方案选型、电路设计、驱动代码、云平台配置到问题排查,完整走一遍流程,适合正在做NB-IoT数据上报、用OneNET做毕业设计或工业采集节点的开发者参考。
1. 整体方案选型与核心链路拆解
1.1 为什么选BC260Y而不是WiFi模组
这个项目里很多人会问:既然要上云,为什么不用ESP8266这种WiFi模块,成本低、资料也多。核心原因是应用场景。这套方案针对的是农业大棚、消防通道、仓库、户外管井这类没有稳定WiFi覆盖、或者不方便布线的位置。BC260Y是移远的NB-IoT模组,工作在授权频谱,一张SIM卡就能接入运营商网络,覆盖距离远、穿墙能力强、单点功耗低,非常适合分散式低速率传感器节点。
从成本角度看,NB-IoT模组比WiFi模块贵,但比4G Cat.1模组便宜,而且功耗优势明显。BC260Y支持PSM和eDRX两种低功耗模式,上报完数据后能深度睡眠,用锂亚电池供电跑一年以上是常见操作。对于不需要实时长连接、每隔几分钟上报一次温湿度的场景,这是比WiFi更合理的方案。
1.2 数据从传感器到云端的完整路径
整条数据链路由四段组成:
- DHT11采集温湿度,通过单总线协议把40bit数据发给STM32
- STM32做主控,把原始温度、湿度值解析出来,缓存到变量中
- BC260Y通过串口与STM32通信,STM32发AT指令让模组附着网络、建立MQTT连接
- OneNET平台上创建产品、设备和数据流,BC260Y按MQTT协议把JSON格式的温湿度数据发布到平台上
这里需要先想清楚:BC260Y不跑应用代码,它只负责“传输管道”。所有业务的逻辑都放在STM32里,包括传感器读取、数据格式化、AT指令流程控制、异常重试。选择这样的分工是因为NB-IoT模组的定位就是“可靠的无线modem”,把数据塞给它,它负责送到云端,业务复杂度集中在MCU侧,开发调试更方便。
2. 硬件准备与DHT11驱动编写
2.1 硬件清单与接线方式
这一套的硬件非常简洁,核心器件如下:
| 器件 | 型号/规格 | 数量 |
|---|---|---|
| 主控板 | STM32F103C8T6最小系统板 | 1 |
| NB-IoT模组 | 移远BC260Y-CN(含NB SIM卡) | 1 |
| 温湿度传感器 | DHT11(蓝色模块) | 1 |
| 电平转换模块 | 3.3V与1.8V双向电平转换(视模组评估板而定) | 可选 |
| 稳压/供电 | 3.6V锂电池或USB供电 | 1 |
| 天线 | NB-IoT外置天线 | 1 |
接线也很直接:DHT11的DATA引脚接STM32任意普通GPIO,比如PA0,VCC接3.3V,GND共地。BC260Y模组的UART_TX接STM32的USART2_RX,UART_RX接USART2_TX,电平匹配问题放在后面说。
注意:BC260Y的IO电平是1.8V,STM32的IO电平是3.3V。如果是用移远的评估板或者已经集成电平转换的模组底板,可以直接对接;如果是自己画的板子直连模组,建议在UART线上加电平转换芯片(比如TXB0104或分立MOS管方案),否则长时间运行容易出现随机乱码。
2.2 DHT11时序原理:单总线怎么读
DHT11是单总线传感器,一根数据线既要主机发命令,又要从机回数据,时序必须严格卡微秒级别。熟悉I2C的人上手很快,但DHT11更简单,没有地址和寄存器概念,每次通信就一次完整的数据交换。
完整时序分四步:
- 主机拉低总线,保持至少18ms,然后释放并延时20~40us,这代表“请求采集”
- DHT11检测到起始信号后,拉低总线80us响应,再拉高80us,表示准备发送数据
- DHT11连续发送40bit数据,高位在前,每位以50us低电平开始,后续高电平长度决定该bit是0还是1
- 数据格式是8bit湿度整数+8bit湿度小数+8bit温度整数+8bit温度小数+8bit校验和
判断bit的逻辑很关键:26~28us的高电平表示“0”,70us左右的高电平表示“1”。很多驱动写不好,就是因为在读取时只用简单的延时判断,没有处理好GPIO输入输出方向切换。
2.3 HAL库下的DHT11驱动代码
工程用STM32CubeMX生成,选择F103C8T6,时钟配置为内部或外部晶振均可,主要用HAL_GPIO操作。DHT11引脚配置为开漏输出,外部接4.7k上拉电阻到3.3V,这样在读取时可以直接切换为浮空输入模式,省去反复改GPIO配置的麻烦。
微秒级延时是DHT11驱动的关键,HAL_Delay只支持毫秒,必须自己实现us延时。我习惯用DWT寄存器,不需要额外定时器:
void delay_us(uint32_t us) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; DWT->CYCCNT = 0; uint32_t target = us * (SystemCoreClock / 1000000U); while (DWT->CYCCNT < target); }读取一个字节的核心代码:
static uint8_t DHT11_ReadByte(void) { uint8_t val = 0; for (int i = 0; i < 8; i++) { while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET); delay_us(40); if (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) { val = (val << 1) | 0x01; while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET); } else { val = val << 1; } } return val; }完整读取函数需要先拉低总线18ms,然后释放、延时20us左右,再检测响应信号。DHT11从机上电后需要一个稳定时间,所以驱动里最好加一个1秒钟的初始化延时,否则首次上电读数据容易失败。
3. BC260Y初始化与OneNET的MQTT接入
3.1 BC260Y上电与网络注册操作
BC260Y用AT指令控制,STM32通过串口发送带\r\n结尾的AT命令,并等待模组返回OK或特定响应。上电后第一步是确认模组工作正常,发AT返回OK;然后发AT+CGMM查看模组型号。
NB-IoT网络注册是整个项目里最容易卡住的地方,因为它比WiFi连路由慢得多。WiFi模块上电几秒就能拿到IP,NB-IoT模组上电后搜索小区、鉴权、附着网络,整个过程可能耗时10到60秒,在地下室或信号弱的地方更久。
网络注册操作分两步:
AT+CGATT=1 // 打开网络附着 AT+CEREG? // 查询网络注册状态,返回 +CEREG: 0,1 表示注册成功其中+CEREG: 0,1里的“1”代表已注册到网络,“5”代表已注册并 roaming。如果返回0或2,说明还没找到网络。注册成功后再查IP地址,用AT+CGPADDR=1,能返回一个IP就说明网络通道已经建立。
3.2 OneNET平台侧准备
接下来是OneNET平台配置。登录OneNET后,在控制台创建产品,选择“多协议接入”中的MQTT协议。这里要提醒一句:OneNET经过几次改版,新用户可能进入的是新版OneNET Studio界面,界面逻辑和老版差异很大。老版“多协议接入”适合本项目的经典接入方式:设备ID+APIKey,topic使用$dp直接上报数据流。
创建完产品后,在产品下添加设备,记录三个关键参数:设备ID、APIKey、产品ID。老版MQTT接入里,设备鉴权只需要设备ID和APIKey,连接服务器地址是183.230.40.40,端口6002。
如果是新版OneNET,MQTT接入域名是mqtts.heclouds.com:1883,鉴权方式使用token签名,需要计算res、et、sign等参数,流程相对复杂。建议做毕设或快速原型时,优先找“多协议接入”老版入口,整体开发成本低很多。
3.3 MQTT登录与数据上报报文
BC260Y自带MQTT协议栈,不需要在STM32上移植MQTT客户端,只需通过AT指令配置即可。
MQTT连接分两步,先打开网络:
AT+QMTOPEN=0,"183.230.40.40",6002返回OK后再查是否真正打开成功,会看到+QMTOPEN: 0,0,第二个0表示成功。然后连接OneNET:
AT+QMTCONN=0,"设备ID","设备ID","APIKey"这里容易懵:MQTT的clientId、username、password到底填什么?OneNET老版MQTT的规则是:clientId填设备ID,username也填设备ID,password填APIKey。和普通MQTT服务器(username是账号、password是密码)完全不同,直接用设备ID当用户名容易混淆,但平台就是这么要求的。
连接成功会返回+QMTCONN: 0,0,0。然后发布数据:
AT+QMTPUB=0,0,0,0,"$dp",28回车后模组会返回>,此时输入payload内容并补一个十六进制的0x1A作为结束。topic是$dp,payload是JSON格式:
{"datastreams":[{"id":"temp","datapoints":[{"value":26}]},{"id":"humi","datapoints":[{"value":58}]}]}发布成功返回+QMTPUB: 0,0,0。整个流程里,topic和JSON格式必须严格正确,尤其JSON里键名不能带单引号,value必须是数字不能是字符串,否则平台不报错但数据就是进不来。
4. 主程序整合与数据采集状态机
4.1 子模块初始化的顺序细节
STM32上电后,初始化顺序会影响整个系统的稳定性。我的习惯是:先初始化时钟和串口,再初始化GPIO和传感器,最后才操作BC260Y模组。原因很简单,NB-IoT模组启动时间最长,如果一开始就阻塞等待模组响应,会耽误传感器准备。
实际流程是:
- 初始化HAL库、系统时钟
- 初始化调试串口(用于打印日志)与BC260Y通信串口
- 初始化DHT11引脚,并延时1秒让传感器稳定
- 向BC260Y发送
AT,等待模组就绪 - 配置APN、附着网络、建立MQTT连接
- 进入主循环,定时读取DHT11并上报
BC260Y建议在首次上电时等待模组返回“RDY”或者通过AT指令轮询就绪状态。有些固件上电后会主动上报乱码,不一定是故障,串口调试时别紧张,先忽略这些主动消息,直到能够稳定响应OK。
4.2 数据上报周期与功耗设计
NB-IoT模组的功耗特性决定了数据上报周期不能设计得太随意。BC260Y的PSM模式有个特点:数据上报完成后,模组进入深睡,此时网络侧会有一段时间不主动下发数据;如果频繁在PSM和活跃态之间切换,反而更耗电。
我的项目里设置的是每5分钟上报一次温湿度,这比较适合环境监测场景。主循环用HAL_Delay或者定时器中断计时,采集前先唤醒模组、保持连接,然后读取DHT11、拼接JSON、发布数据,发布完成之后如果暂时不需要下行控制,就进入睡眠等待下一个周期。
需要特别注意的是DHT11本身的采样周期。DHT11数据手册明确要求两次读取间隔不得小于1秒,即便驱动代码再快也不能突破这个限制。我在调试时曾经在这个问题上栽过跟头:把读取间隔压缩到200ms,结果数据经常跳变或校验失败,后来查资料才发现DHT11内部的AD转换需要1秒左右完成。
4.3 主循环核心代码结构
主程序的核心逻辑可以整理成一个简易状态机,避免程序阻塞在某一个环节:
while (1) { switch (state) { case STATE_READ_SENSOR: if (DHT11_Read(&temperature, &humidity) == DHT11_OK) { state = STATE_UPLOAD; } else { state = STATE_RETRY; } break; case STATE_UPLOAD: snprintf(payload, sizeof(payload), "{\"datastreams\":[{\"id\":\"temp\",\"datapoints\":[{\"value\":%d}]},{\"id\":\"humi\",\"datapoints\":[{\"value\":%d}]}]}", temperature, humidity); if (send_qmtpub(payload) == SUCCESS) { state = STATE_SLEEP; } else { state = STATE_RETRY; } break; case STATE_SLEEP: HAL_Delay(300000); state = STATE_READ_SENSOR; break; case STATE_RETRY: reconnect_nbiot(); state = STATE_READ_SENSOR; break; default: state = STATE_READ_SENSOR; break; } }状态机相比纯顺序执行有个很大优势:如果模组网络暂时掉线,不需要整个程序卡死,可以在重试状态里做网络重连,同时把传感器数据保留在缓存里,等网络恢复后补报,而不是直接丢数据。
5. 常见问题与排查技巧实录
5.1 问题排查速查表
做这个项目时,大家最常卡住的问题集中在网络注册和MQTT连接两个环节。这里整理一个排查速查表,按现象直接定位:
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| 串口发AT无响应 | 波特率不对、模组未上电、串口TX/RX接反 | 确认BC260Y默认波特率,检查串口引脚 |
AT+CEREG?返回0或2 | SIM卡未插好、天线松动、信号弱 | 重新插卡,装外置天线,到窗边测试 |
AT+CGATT=1返回ERROR | APN配置错误、卡未激活 | 配置AT+CGDCONT=1,"IP","cmiot" |
QMTOPEN成功但QMTCONN失败 | 设备ID或APIKey错误 | 核对OneNET设备参数,区分大小写 |
QMTCONN成功但发布失败 | 主题错误、MQTT连接失效 | 确认使用$dp主题并检查返回码 |
| DHT11校验和错误 | 上拉电阻缺失、接线过长、采集间隔过短 | 加4.7k上拉,缩短杜邦线,确保间隔大于1秒 |
| 平台数据流无数据 | JSON格式错误、数据流名不一致 | 先用串口工具手工发布payload验证 |
5.2 我在实际调试中踩过的坑
第一个坑是发送AT+QMTPUB时payload长度算错。length字段指的是整个JSON字符串的字节数,不包含引号和结束符。有次我多算了一个字节,模组一直没反应,平台端也收不到数据。后来用串口工具反复试才发现长度必须和实际字符串完全一致,差一个字节都不行。解决办法很简单:在STM32代码里用strlen(payload)动态计算长度,不要手写。
第二个坑是模组的主动上报消息干扰了程序解析。BC260Y在某些固件版本上电后会主动上报^MODE: 2、^SYSSTART等URC消息,如果STM32只发AT指令却不处理这些主动消息,接收缓冲区会堆积,导致后续指令的响应和URC消息混在一起。代码里做AT响应解析时,要跳过以^开头的行,或者发送指令前先清空接收缓冲区。
第三个坑是电平匹配问题。早期用杜邦线直接连接STM32和BC260Y评估板,当时评估板已经做了电平转换,没问题;但后来换成裸模组,把3.3V信号直接怼到1.8V IO上,短时间能工作,长时间跑下来偶尔会通信失败。改成电平转换模块后问题消失。所以自己画板子时,务必看清模组硬件手册的IO电平要求。
最后提一个关于天线的小建议:NB-IoT频段比WiFi低,波长更长,天线的尺寸和走线对信号影响很大。用裸模组时如果不用外置天线,靠着PCB天线或者一段飞线,信号强度会有明显差异,表现为网络注册慢、连接不稳定。建议无论做原型还是量产,天线部分都按模组厂商的参考设计来做,别省这个成本。
这套方案整体跑通之后,后面如果要扩展,可以在OneNET上增加数据流、报警规则,或者在STM32端接更多传感器,比如土壤湿度、光照强度,代码框架不用大改,只要把JSON payload里的数据流增加字段就行。
本文还有配套的精品资源,点击获取
