2026/9/3 3:09:28

STM32+DHT11通过NB-IoT上传OneNET云平台完整指南

STM32+DHT11通过NB-IoT上传OneNET云平台完整指南 简介一套面向STM32与NB-IoT物联网开发的实战项目资料以STM32F103C8T6为主控通过BC260Y模块以MQTT协议将DHT11采集的温湿度数据上报至OneNET云端适合嵌入式初学者及物联网应用开发者学习参考。资源共151个文件压缩包3.42MB包含37个h头文件与36个c源文件覆盖外设驱动、协议解析及主逻辑uvprojx/uvoptx为Keil工程文件可一键打开编译hex/axf为生成的可执行文件与调试映像另有.d、.crf、.o等编译中间文件及txt说明、sct链接脚本等便于理解编译过程与工程结构。已有1433人学习下载。通过该工程可掌握STM32串口及CubeMX外设配置、BC260Y的AT指令接入、MQTT报文封装与OneNET设备连接流程同时了解DHT11单总线时序读取是一份贴近实际、可运行的物联网上云参考方案。 前阵子给一个农业环境监测的小项目做网关端设备要求把温湿度数据实时传到云平台。现场条件很现实大棚里没有WiFi拉网线更不现实最后定下的方案是STM32F103C8T6采集DHT11温湿度通过BC260Y这颗NB-IoT模组走蜂窝网络直接把数据上报到OneNET云平台。整套链路从传感器到云端一共就四段DHT11 - STM32 - BC260Y - OneNET。这篇文章把我从硬件接线、DHT11时序、BC260Y的AT指令到OneNET平台配置的完整过程整理出来。重点不是贴一份能跑的源码就完事而是把每一步为什么这么设计、哪些地方容易踩坑说清楚。如果你手上正好在做NB-IoT相关的东西或者准备用STM32接DHT11上报云平台这篇可以直接当参考。1. 数据链路总览从DHT11到OneNET的四个环节1.1 为什么选NB-IoT而不是WiFi或4G做物联网项目先定通信方式这个决定会影响后面所有硬件设计。大棚、粮仓、管道井这类场景最大的问题是现场没有稳定的有线网络和WiFi而且很多设备部署在相对封闭的环境里2G/4G信号未必好。NB-IoT的优势就在这里覆盖广、穿透强、单个设备流量小一年下来流量费几乎可以忽略。BC260Y是移远一颗比较有代表性的NB-IoT模组尺寸很小支持Band 3/5/8国内主流运营商网络基本都能用。我是直接用BC260Y-CN版本配中国移动的物联网卡。如果你手里是电信或联通的NB卡也一样能注册上只是APN可能不同后面会提到。1.2 硬件连接与电平匹配板子选的是常见的STM32F103C8T6最小系统板PA0接DHT11数据线PA2和PA3复用为USART2接BC260Y的串口。先说结论再给接线表STM32的VCC接3.3VDHT11的VCC接3.3VDATA引脚必须外接一个4.7kΩ上拉电阻到3.3V。DHT11本身是单总线协议数据引脚是开漏输出没有上拉电阻读取会非常不稳定。BC260Y的供电建议直接用独立的3.8V/2A电源不要跟STM32共用一颗LDO。NB模组在发起网络附着或发送数据瞬间电流会冲到1.5A以上共用电源会导致模组频繁重启。STM32F103C8T6BC260YDHT113.3V-VCCGNDGNDGNDPA0-DATAPA2 (USART2_TX)UART_RX-PA3 (USART2_RX)UART_TX-这里有一个特别容易被忽略的点BC260Y的UART电平是1.8V而STM32的UART电平是3.3V。如果直接把两边的串口连起来低电平还不算大问题但高电平会出现识别不了的情况尤其BC260Y发送给STM32的TX信号只有1.8V高电平STM32的GPIO输入高电平阈值一般需要0.7×VDD约2.31V直接连大概率误码。我在这块吃过亏刚开始用杜邦线直连串口打印出来的AT返回偶尔是乱码后来加了一颗TXS0104EPWR电平转换芯片才稳定。如果不想加芯片也可以把BC260Y的VDDIO引脚接到1.8V同时把STM32的UART RX引脚用分压电阻从3.3V降到1.8V左右但这样做比较麻烦不如直接电平转换省心。2. DHT11单总线驱动最容易出错的时序部分2.1 一次采集的握手过程DHT11看着简单实际对时序比较敏感。它用的单总线协议总线上只有一根数据线所有通信由主机发起。一次完整读取分为几个阶段主机先把总线拉低持续时间必须大于18ms用来发送起始信号。我习惯拉低20ms余量更足。释放总线并拉高20到40us等待DHT11响应。DHT11检测到起始信号后会先把总线拉低80us再拉高80us这个就是它的响应信号。响应结束之后DHT11开始输出40bit数据8bit湿度整数、8bit湿度小数、8bit温度整数、8bit温度小数、8bit校验和。数据位的区分靠高电平持续时间。每一位都是先拉低50us然后拉高。如果高电平持续约26到28us代表逻辑0如果高电平持续约70us代表逻辑1。所以读取时等低电平结束之后延时40us再判断引脚电平如果还是高就是1如果是低就是0。用标准库或HAL库都能写但要注意一点DHT11对时序要求挺严起始信号那20ms低电平期间任务里一定不能有高优先级的抢占中断长时间打断否则DHT11可能不响应。2.2 HAL库下实现us级延时HAL_Delay只能做到毫秒级而DHT11采样过程中需要微秒级延时所以得自己写一个us延时函数。最稳妥的办法是用内核调试寄存器DWT也就是Cortex-M3内核里的Data Watchpoint and Trace单元。void delay_us(uint32_t us) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; DWT-CYCCNT 0; uint32_t start DWT-CYCCNT; uint32_t cycles (SystemCoreClock / 1000000U) * us; while ((DWT-CYCCNT - start) cycles); }SystemCoreClock在STM32F103上通常是72MHz所以1us对应72个内核时钟周期。这个延时函数在DHT11读取中非常合适没有中断上下文切换问题。要注意的是如果开了其他中断并且中断里调用了同样基于DWT的延时可能互相干扰不过一般场景不会出现。也可以直接用定时器来做微秒延时比如TIM2作为us定时器但是DHT11的读取对时间连续性要求高进出中断反而会增加不确定性所以我个人更推荐DWT方案。2.3 数据解析与校验代码完整读取函数大概长这样uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature) { uint8_t data[5] {0}; GPIO_InitTypeDef GPIO_InitStruct {0}; // 配置PA0为输出拉低20ms HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); HAL_Delay(20); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); delay_us(30); // 配置PA0为输入等待DHT11拉低响应 GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_SET) return 1; // 无响应 while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET); while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_SET); for (int i 0; i 40; i) { while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET); delay_us(40); if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_SET) { data[i / 8] (data[i / 8] 1) | 0x01; while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_SET); } else { data[i / 8] (data[i / 8] 1); } } // 配置回输出方便下次发送起始信号 GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); if ((data[0] data[1] data[2] data[3]) data[4]) { *humidity data[0]; *temperature data[2]; return 0; } return 2; // 校验失败 }校验和是前四个字节相加取低8位这个很容易验证。如果连续读取失败先别怀疑代码用示波器看DHT11数据脚波形比盲调强得多。DHT11对供电稳定性也有要求3.3V供电下读取距离不要太远线长了波形会被拉垮。3. BC260Y接入OneNETAT指令与MQTT报文3.1 模组上电和网络附着BC260Y上电后先通过串口发一个AT测试模组是否正常。这里要注意波特率我用的115200但有些模块默认是9600或115200买回来的模组如果没配置过先试这几个常见值。接下来按顺序执行这些指令ATE0 CME ERROR: 3第一次执行ATE0可能报错不用慌多试一次或者先发AT再看回显。ATE0是关闭回显后面读写数据时不会乱。然后是SIM卡和网络状态ATCPIN? CPIN: READY ATCSQ CSQ: 22,0 ATCEREG? CEREG: 0,1 ATCGATT? CGATT: 1CSQ的第一个值表示信号强度范围0到3122以上基本稳定CEREG返回第二个数字1表示已注册到网络CGATT返回1表示分组域附着成功。如果CEREG一直返回0先检查天线是否接好再确认物联网卡是否开通了NB-IoT业务。NB-IoT卡一般不需要手动设APN如果SIM卡是通用物联网卡且无法附着可以尝试ATCGDCONT1,IP,cmiotcmiot是中国移动物联网常用的APN。电信卡有些地区用ctnb联通卡用cmiot或者woiot关键看运营商给你的APN参数。注意设置后要重新执行ATCGATT0再ATCGATT1让APN生效。3.2 配置MQTT三元组并建立连接BC260Y内置了MQTT协议栈不需要自己在MCU上跑MQTT客户端只需要用AT指令控制。它使用的是一套和移远4G模组类似的MQTT AT指令集。先在OneNET控制台拿到三个参数我这边创建产品后得到的是产品ID: 372865 APIKey: abc123def456ghi789 设备ID: dev_001OneNET的MQTT接入要求比较特殊连接时三元组可以按产品维度填也可以按设备维度填。以我用的产品维度来说服务器地址183.230.40.39端口1883ClientID产品IDUsername产品IDPasswordAPIKey于是对应的AT指令是ATQMTOPEN0,183.230.40.39,1883 OK ATQMTCONN0,372865,372865,abc123def456ghi789 OK如果平台要求设备维度则ClientID填设备ID、Password填设备APIKey具体以OneNET控制台给出的接入说明为准。连接成功后可以用ATQMTCONN?查询当前连接状态返回第二个参数为2表示已连接。这里要重点说一个容易炸的问题BC260Y如果固件版本较老可能不提供QMTOPEN这套MQTT指令只提供LwM2M指令比如ATQLWULDATA。出现这种情况时要么升级模组固件要么改用LwM2M方式接入OneNET。我手里的模组固件版本是BC260YCNAR01A08支持MQTT指令但如果你买的是纯LwM2M版本就得换方案。3.3 温湿度数据打包与上报MQTT建立连接之后发布消息使用ATQMTCPUB。OneNET旧版多协议接入里设备数据通过主题$dp上传payload格式是一段JSON。我参考OneNET文档拼出来的报文如下{datastreams:[{id:temp,datapoints:[{value:25.5}]},{id:humi,datapoints:[{value:60.2}]}]}通过AT指令发送时要先计算payload的字节长度。比如上面这段JSON我数出来是123字节实际以串口发送工具为准然后执行ATQMTCPUB0,0,0,0,$dp,123 发送完上面这一行BC260Y会返回这时候再把payload正文发出去等模组返回OK就说明发布成功。QoS我一般设0温度上报这类数据丢一帧影响不大但响应快、省流量。QoS设1的话需要确认NB-IoT链路时延高没必要。数据到了OneNET之后平台会根据JSON里的datastreams自动建数据流temp和humi就是两个数据流的名字。如果你想上报多个实时数据用一条消息把多个datastreams放在一起效率最高。4. OneNET平台侧配置产品、设备与数据流4.1 创建产品和获取鉴权参数打开OneNET控制台选择多协议接入。我用的是MQTT协议因为BC260Y内置的AT指令集直接就是MQTT改造成本最小。创建产品时要注意产品名称按实际项目填设备接入协议必须选MQTT否则后面拿不到MQTT三元组。创建完产品后记录产品ID和APIKey这两个就是MQTT连接时Username和Password的来源。如果是个人学习测试用免费的产品就够用了。不过OneNET平台现在也有新版和旧版入口旧版地址能不能注册取决于当前运营状态如果发现老地址进不去就用新版的OneNET Studio接入服务器地址会变成mqtts.heclouds.com这类新域名鉴权参数也会变但整体思路一致拿产品ID、设备ID、APIKey去填MQTT三元组。4.2 在平台上查看温湿度数据设备接上MQTT并成功发布$dp消息后在设备列表里可以看到设备状态变为在线。点进设备详情左侧数据流列表中会出现temp和humi两个数据流。如果没有出现说明MQTT虽然连接成功但发布的topic或者payload格式不对。我自己测试时出现过一次很隐蔽的问题ATQMTCPUB里的数据长度写错导致JSON被截断OneNET界面显示设备在线但数据流一直空。排查了半天最后用串口抓AT命令才发现len参数少算了两个字符。所以建议先把payload放到串口发送工具里测长度再填到AT指令中别手数。平台侧还有一个查看工具数据流展示页面支持按时间范围查询数据点能看到每次上报的值和时间戳确认数据正确后再接应用层逻辑别一上来就调告警和可视化。5. 联调中我踩过的四个坑5.1 BC260Y的串口电平不匹配上面已经提过BC260Y的UART电平是1.8VSTM32是3.3V。如果你没有加电平转换测试时可能偶尔能收到OK但发送稍微频繁一点就乱码。这是模组和MCU直连最常见的坑没有之一。我的建议是不要省这个元件。加一颗TXS0104EPWR或者用两个MOS管搭电平转换电路都行成本很低但能省掉大量排查时间。5.2 模组供电电流不够导致重启BC260Y最高发射功率时电流很大我用可调电源观察过瞬间大概1.8A左右。一开始我用STM32板载的3.3V LDO给模组供电结果每次ATCSQ能返回但一执行ATCGATT1或者MQTT连接模组就自动重启。排查过程很简单用示波器测量模组VBAT引脚电压发现附着瞬间电压从3.8V掉到2.7V一下直接触发低压复位。后来换成独立的Buck降压模块输出3.8V峰值电流2A以上问题消失。5.3 QMTCPUB发布后OneNET没数据这个问题卡了我一个下午。连接正常、主题也发了、模组返回OK但平台上数据流就是没有新数据。把AT指令和payload组合抓出来看发现我在OneNET里设置的datastreams上报格式不对。OneNET旧版MQTT要求payload必须是平台约定的JSON格式不能自己随便写字段。我一开始发的是{temp:25.5,humi:60.2}这种自描述格式OneNET解析不了必须用标准格式把数据类型包起来。改成datastreams结构后数据正常出现。5.4 DHT11和NB-IoT上报节奏冲突NB-IoT模组在上报数据时会占用串口而DHT11读取又需要连续时序。我第一版程序是每读取一次DHT11立刻通过BC260Y上报结果发现某些时刻DHT11读出来的值全是255或者校验错。原因其实很简单BC260Y和STM32串口交互是阻塞式的我在上报时用HAL_UART_Transmit死等串口发送期间没有及时切换DHT11的GPIO方向导致DHT11数据线上的时序被拉乱。处理方式是定时采集和定时上报分离。每10秒读取一次DHT11把温湿度缓存在全局变量里每30秒通过BC260Y上报一次。读取和上报不再互相抢占时间DHT11的错误率骤降。如果你也遇到DHT11偶发读错可以按这个思路改一轮大概率能解决。整套系统跑下来最让我感慨的不是代码逻辑而是很多问题出在硬件细节上。电平转换、电源余量、时序隔离这些都做好了MCU和模组之间才能真正稳定通信。文章里的代码和AT指令我尽量写得可直接用但实际项目里厂商平台参数可能微调建议以OneNET控制台、BC260Y AT指令手册和DHT11数据手册为准。本文还有配套的精品资源点击获取