简介:基于窄带物联网(NB-IoT)与STM32的光照度采集上云项目资源包,面向物联网应用开发初学者、嵌入式爱好者以及正在备战物联网实训或毕业设计的学员,也适合作为高校物联网课程的配套案例。项目以STM32为控制核心读取光照度传感器数据,经NB-IoT模块传输至腾讯云,再通过微信小程序展示实时数值,完整覆盖硬件采集、网络通信、云端接入和移动端呈现的链路。资源包共5个文件,集成约4.04MB,包含两套STM32工程源码(分别适配标准库与Pro版开发环境)、项目简介与搭建步骤HTML文档、善学坊官方学习指南HTML文档以及License许可说明;源码、配置和文档分层放置,既有可直接运行的固件,也有逐步搭建指引。当前已有643人浏览学习。通过该资源可系统掌握STM32的ADC采样与光照传感器驱动、NB-IoT模块的AT指令控制、腾讯云设备接入与数据流转、微信小程序API对接等关键环节,同时了解嵌入式、通信、云端和移动端协同开发的完整思路,便于后续扩展温湿度、GPS等更多传感器应用。
1. 用 NB-IoT 把光照度传到腾讯云,比 WiFi 方案多出来的三件事
光照度采集这种项目,最常见的第一版是用 STM32 加 ESP8266 走 WiFi,上云快、调试简单,但真放到室外绿化带、农业大棚或者厂房采光井里,WiFi 覆盖和功耗两个问题立刻浮现。NB-IoT 走运营商授权频谱,单点覆盖能穿两层楼,一个基站扛几万个终端,上报一条 100 字节的 JSON 报文,平均电流能做到微安到毫安级——这正是光照度这类低速率、小报文、长时间待机的数据最舒服的传输方式。下面按“方案选型、腾讯云平台配置、STM32 采集与上报、链路排错、低功耗落地”这条线,把基于 NB-IoT+STM32 实现采集光照度到腾讯云的一套可复现做法讲清楚,适合准备毕业设计、物联网竞赛或要快速出 NB-IoT 落地 demo 的嵌入式工程师。
2. NB-IoT + STM32 光照度采集系统的方案选型与数据链路
2.1 为什么光照度这类低速率小报文适合走 NB-IoT
2.1.1 NB-IoT 的特性边界
室内光照度采集的报文通常是“设备编号、时间戳、lux 值、信号强度”,一条不超过几十字节。NB-IoT 的设计目标就是这类业务:上行速率约 20-60kbps,下行更窄,但覆盖增益比 LTE 高 20dB,能钻到地下室和管道井里。3GPP 给 NB-IoT 定义了 PSM 和 eDRX 两种节电机制,设备可以长时间“睡觉”,睡醒后只上报一个数据包又睡回去。相比之下,WiFi 模组待机电流在毫安级,还要维护 TCP keepalive,在无人维护的现场很不划算。
需要注意边界:NB-IoT 不适合实时性要求高的双向控制,时延通常在 1-6 秒,命令下发是低频次的。如果后面要做自动补光灯的毫秒级控制,正确做法是让 STM32 本地判断,云端只做记录和远程策略调整。这个定位想清楚,后边腾讯云上的数据模板和 Topic 设计才不会跑偏。
2.1.2 与 Cat.1 和 LoRa 的取舍
如果数据量不大但要求实时响应,4G Cat.1 更合适;如果园区里自己布网关、不上运营商网络,LoRa 更合适,但要自己维护网关和频点合规问题。NB-IoT 的优势是直接用运营商存量基站,不用自建网关,一张卡一年流量很少也够用。选 NB-IoT 的场景一般是位置分散、数量多、没有本地 WiFi、又不希望自己维护网络设备的项目。这个逻辑也决定了腾讯云那边选产品时要按“设备-密钥”直接配,而不是按“网关-子设备”的拓扑配。
2.2 硬件选型:BH1750 传感器与 NB-IoT 模组的搭配
光照度采集传感器常用两种:光敏电阻加 ADC 电路,或者数字光照度传感器。光敏电阻成本低,但需要自己校准分压电阻和 ADC 参考电压,不同批次的器件一致性差,工程上标定很烦。BH1750 是 I2C 接口的数字传感器,量程 1-65535 lx,内部有光电二极管和 ADC,直接输出 lux 值,精度对农业补光和室内照明监测够用。I2C 只需要两根线,STM32 的硬件 I2C 或 GPIO 模拟 I2C 都能驱动。要注意 BH1750 的 ADDR 引脚电平决定设备地址是 0x23 还是 0x5C,多路采集时可以用不同地址挂同一条总线。
NB-IoT 模组常见的有移远 BC26/BC35-G、中移 M5310-A,以及国产的利尔达、有人物联网模组。选型主要看三件事:是否有支持 MQTT 的 AT 固件、PSM 功耗参数能不能通过 AT 指令配置、串口波特率是否方便直接接 STM32 的 USART。大部分模组默认波特率 9600 或 115200,在 STM32 初始化串口时先对上,调试阶段别一上来就开流控。拿到模组后先接 USB 转串口在电脑上验证一遍网络注册,指令会话大概是这样的:
AT OK AT+CSQ +CSQ: 22,99 OK AT+CEREG? +CEREG: 0,1 OK这里+CSQ: 22,99表示信号强度 22(0-31,越大越好),第二个参数 99 表示误码率未知,先不管。+CEREG: 0,1里第二个 1 表示已注册上网络。这两条能过,再谈连云。
2.3 数据上腾讯云的三种路径,MQTT 直连为什么是首选
这里有个关键决定:数据怎么从 NB-IoT 模组到达腾讯云。常见做法有三条路。
| 路径 | 组件 | 优点 | 缺点 |
|---|---|---|---|
| MQTT 直连 | 模组内置 MQTT AT 固件 + IoT Explorer | 链路最短,平台可管理 | 模组固件要支持 MQTT |
| UDP + 云网关 | 模组 UDP 上报 + 自建云转发 | 报文最省流量 | 要自己维护服务器并适配 |
| 运营商平台转发 | 通过运营商物联网平台转接到腾讯云 | 适合存量平台设备 | 多一跳,配置复杂 |
我一般建议优先走 MQTT 直连,因为它正好用上腾讯云 IoT Explorer 的设备管理、数据模板和规则引擎。设备端只要把 JSON 报文发到固定的 Topic,平台自动解析并显示在控制台,不用自己搭后端。对毕业设计和快速验证来说这是最稳妥的路径。后面章节的代码默认也是模组使用支持云平台接入的 MQTT AT 固件,如果手上的模组固件不带 MQTT,需要先用串口或 DFU 方式升级模组固件。
3. 腾讯云 IoT Explorer 的产品创建与 MQTT 连接参数
3.1 创建产品和设备时要确定的 4 个关键配置
在控制台选“物联网开发平台 IoT Explorer”,新建产品时,有四个配置会影响后面代码的写法。
| 配置项 | 取值示例 | 作用 |
|---|---|---|
| 节点类型 | 设备(不是网关) | 决定是否走子设备拓扑 |
| 数据协议 | MQTT | 与模组 AT 指令对接 |
| 认证方式 | 密钥认证 | 决定 MQTT 密码生成规则 |
| 产品名称 | stm32-lux-sensor | 建议拼音或英文,避免 URL 编码问题 |
节点类型选“设备”,不要选“网关”;数据协议选“MQTT”;认证方式选“密钥认证”。认证方式决定了设备侧最终连接时 MQTT 密码的生成规则,这是最容易让新手卡住的地方,下面专门讲。创建完产品后,在设备列表里点“添加设备”,会得到一个三元组:产品 ID、设备名称、设备密钥。产品 ID 是一串字母数字,设备名称是自己填的英文标识,设备密钥是 24 位左右的密文,这三样会被烧进 STM32 的代码里。
提示:设备密钥相当于设备在云端的登录口令,调试日志里不要再打印出来,调试完记得把相关打印关掉。
3.2 设备三元组与 MQTT 连接参数的对应关系
腾讯云 IoT Explorer 的 MQTT 连接参数不是随便填的。clientId 和 username 都是用产品 ID 加设备名拼接,password 是对时间戳用设备密钥做 HMAC-SHA1 生成的签名。常见做法是先取当前 UTC 时间戳(秒),把设备密钥作为 HMAC 的 key,时间戳作为消息内容,得到签名后拼成“时间戳;签名”。用 Python 可以这样生成签名:
import hmac import hashlib import time device_secret = "你的设备密钥" ts = str(int(time.time())) sign = hmac.new(device_secret.encode(), ts.encode(), hashlib.sha1).hexdigest() print("timestamp:", ts) print("password:", f"{ts};{sign}")这个脚本每次运行生成的都是新签名,原因是 timestamp 直接参与 HMAC 计算,云端用相同规则验签,设备端时间偏差超过一定范围会连接失败。所以设备侧也要维护一个能拿到当前 Unix 时间戳的时钟源,常见做法是上电时通过模组的取网络时间指令(不同模组指令名不同,如 AT+QLTS)同步一次。如果设备上没有 RTC 校准,直接把固定签名烧进去,只能在签名有效期内连接,时间一长必然掉线重连失败。
另外,不同版本控制台上的调试工具会直接生成一份 MQTT 连接参数供参考。第一次调不通时,把工具生成的参数和代码算出来的逐字符对比,比反复猜快得多。
3.3 Topic 设计:属性上报与事件上报的差别
IoT Explorer 的数据模板生成后,会有两类 Topic:属性上报$thing/up/property/{productId}/{deviceName},事件上报$thing/up/event/{productId}/{deviceName}。光照度建议用属性上报,因为属性会持续记录,可以直接在控制台看到实时曲线;事件适合告警,比如光照度持续低于阈值触发“光照不足”事件。
上报的报文格式是 JSON,method 取 report,params 里写字段名和值。字段名必须和数据模板里的标识符一致。
{"method":"report","clientToken":"123456","params":{"lux":852.3,"rssi":-71}}clientToken 是用于透传去重的字符串,可以用 STM32 的毫秒 tick 或自增计数代替。字段类型在数据模板里定义成浮点或整型,上报的值会做类型校验,类型不匹配会在平台侧解析失败,这个在第 5 章排错部分还会看到。
3.4 在腾讯云开发者后台确认设备在线
在控制台的设备详情页可以看到“在线/离线”。设备上线意味着 MQTT 连接成功,此时还没有任何业务数据,只代表云平台这个产品下对应的设备完成了认证。如果设备显示在线但你发的属性没有更新,去“设备调试”和“消息日志”页面查看上报记录,消息日志里能看到原始报文、解析结果和失败原因。这里建议养成习惯:每次改完代码先看消息日志,再回来看设备是否在线,不然容易被表面在线的假象带偏。
4. STM32 HAL 库读取 BH1750 并通过 NB-IoT 模组上报腾讯云
4.1 BH1750 的 I2C 读取与光照度换算
用 CubeMX 生成工程时,把 I2C1 设为 I2C 模式,速率选 100kHz 或 400kHz 都行,BH1750 最大支持 400kHz。下面这段用 HAL 库读取 BH1750 的代码比较常用:
// bh1750_driver.c #define BH1750_ADDR_W 0x46 // ADDR 接低电平时7位地址0x23,左移1位得到写地址 #define BH1750_ADDR_R 0x47 #define BH1750_PWR_ON 0x01 // 上电指令 #define BH1750_CONT_H 0x10 // 连续高分辨率模式,分辨率1lx uint16_t bh1750_read_lux(I2C_HandleTypeDef *hi2c) { uint8_t cmd = BH1750_PWR_ON; HAL_I2C_Master_Transmit(hi2c, BH1750_ADDR_W, &cmd, 1, 100); cmd = BH1750_CONT_H; HAL_I2C_Master_Transmit(hi2c, BH1750_ADDR_W, &cmd, 1, 100); HAL_Delay(180); // 高分辨率模式测量时间约120ms,留足余量 uint8_t buf[2]; HAL_I2C_Master_Receive(hi2c, BH1750_ADDR_R, buf, 2, 100); return (uint16_t)((buf[0] << 8) | buf[1]); }HAL_I2C_Master_Transmit 最后一个参数是超时时间 100ms,超过会返回 HAL_TIMEOUT,实际使用中最好检查返回值。测量结果是 16 位原始值,连续高分辨率模式下要除以 1.2 得到 lux。很多网上代码把原始值直接当 lux 用,会导致光照度整体偏大,实际采集时建议拿照度计或手机 App 对比校准。
4.2 NB-IoT 模组开机到连云的 AT 指令序列
模组上电后先做网络注册,再打开 MQTT 上下文,最后连接腾讯云。以移远 BC35-G/BC26 这类模组的 AT 指令为例,整个流程可以抽象成下面这张表:
| 阶段 | AT 指令 | 预期返回 | 说明 |
|---|---|---|---|
| 自检 | AT | OK | 串口通信正常 |
| 开射频 | AT+CFUN=1 | OK | 打开射频功能 |
| 附着网络 | AT+CGATT=1 | OK | 数据业务附着 |
| 查询注册 | AT+CEREG? | +CEREG: 0,1 | 第二位为1表示已注册 |
| 信号强度 | AT+CSQ | +CSQ: 25,99 | 第一位越大越好,99表示无信号 |
| 打开连接 | AT+QMTOPEN=0,"接入点域名",1883 | OK | 指向腾讯云 MQTT 接入点 |
| 建立连接 | AT+QMTCONN=0,"clientId","username","password" | OK | 三个参数按第 3 章规则生成 |
连接成功后,模组会返回+QMTCONN: 0,0。失败时返回错误码,常见的是 -3,表示 MQTT 层连接被拒,优先检查 password 签名和时间偏差。接入点域名在控制台“设备调试”页面能看到,常见是iot-mqtts.pvt.tencent.com,端口 1883。注意有些老固件要求填 IP 而不是域名,遇到解析失败先看固件版本。
4.3 在 STM32 里拼接 MQTT 报文并处理返回
连接建立后,上报一条属性报文只需要三步:拼接 Topic,拼接 JSON,发送。代码如下:
// mqtt_report.c char topic[128]; char payload[160]; char at_cmd[256]; snprintf(topic, sizeof(topic), "$thing/up/property/%s/%s", product_id, device_name); snprintf(payload, sizeof(payload), "{\"method\":\"report\",\"clientToken\":\"s%lu\",\"params\":{\"lux\":%.1f,\"rssi\":%d}}", (unsigned long)HAL_GetTick(), lux, rssi); snprintf(at_cmd, sizeof(at_cmd), "AT+QMTPUB=0,0,1,0,\"%s\",%d\r\n%s", topic, (int)strlen(payload), payload);QMTPUB 的第二个参数是 msgid,填 0 让模组自动生成;第三个参数 qos 填 1,表示至少一次,光照度上报丢一条可以接受,但 qos 太低不利于排查问题;第四个参数 retain 填 0,云端不需要保留旧值。整条 AT 指令通过 USART 发给模组,模组返回 OK 表示指令已接受,返回+QMTPUB: 0,0表示发布完成。注意不要在上一条 AT 响应返回前就发下一条,实际代码里可以用状态机或串口中断标志位来串行化。
4.4 用串口把上报链路完整打印出来
调试阶段常见的做法是把 USART2 接 STM32 的 printf 重定向,USART1 接 NB-IoT 模组,两条串口分开,避免 AT 响应和调试日志混在一起。串口调试时要打印的内容其实不多:模组每次 URC 响应、QMTPUB 完成标志、本地的 lux 原始值和换算后的值。printf 重定向用半主机模式或者直接写 ITM 都可以,MCU 资源紧张时优先用 ITM,不占串口。
光照度数据变化很快,上一秒 800 lx 下一秒被手挡住变成 50 lx,如果每次都上报会导致模组频繁发射,功耗和流量都受影响。常见做法是加上“变化超过阈值才上报”的本地判断,这条优化留到最后一章讲,它比改 PSM 参数更容易被忽略。
5. 从 SIM 卡到腾讯云日志:排查光照度不上云的顺序
5.1 第一步确认 NB-IoT 网络注册:AT+CEREG 和 AT+CSQ
上报失败先别急着改代码,先确认网络层是否就绪。AT+CEREG? 返回的第二位是注册状态:0 未注册、1 已注册、2 正在注册、3 被拒绝、5 已注册但漫游。看到 3 或 100 之类的错误码,优先怀疑 SIM 卡没有开通 NB-IoT 套餐,或者模组频段和当地基站不匹配。CSQ 返回第一项是信号强度,数值范围 0 到 31,10 以下基本别指望稳定连接,20 以上才算正常,99 表示没读到信号。
NB-IoT 测试最好的做法是准备一张已激活的测试卡,先插到 USB 转串口模块上用电脑手动敲指令,确认网络层和 MQTT 都能通,再接 STM32。这样能快速切分是设备问题还是网络问题。
提示:给设备换过 SIM 卡后,务必重新执行 AT+CFUN=1 和 AT+CGATT=1,某些模组不会自动重新附着。
5.2 第二步用 AT 指令自测 MQTT 与上报链路
模组接电脑时,还可以直接绕过 STM32 手动发一条测试报文。这个自测可以判定是模组侧 MQTT 问题还是 STM32 侧代码问题。常见的错误包括:AT+QMTCONN 返回错误码说明认证信息错了,返回 -3 基本可以断定是 clientId/username/password 拼法问题。另一点容易踩的是 Topic 里的产品 ID 和控制台显示的产品名称不是一回事,上报 Topic 里用的是产品 ID,不要混用。
发布时如果 QMTPUB 返回 ERROR,先检查 Topic 字符串是否完整。很多嵌入式代码把 snprintf 的目标缓冲区设成 64 字节,Topic 加 JSON 一长就截断了,返回的 ERROR 信息很误导人。把缓冲区放大到 160 字节以上,问题往往直接消失。
5.3 第三步在腾讯云控制台消息日志里定位问题
设备能连上但属性不更新,十有八九是数据模板字段名或 JSON 格式不一致。控制台“消息日志”里会显示每条上行消息的解析结果,解析失败时能看到字段级别的原因,比如“lux 字段类型不匹配”或“找不到产品 ID”。这里有个使用习惯:先在控制台“在线调试”发一条模拟数据,确认平台解析正常,再用设备发真实数据,这样就能知道是平台模板配置问题还是设备报文问题。
另外,clientToken 在线调试里是必填项,设备端用自增计数也能通过。若你看到消息日志里有数据但设备详情面板无曲线,检查数据模板里有没有把 lux 的“展示”开关打开,数据模板定义了字段但不展示,平台上就不会画曲线,这不是设备问题。
5.4 常见坑:芯片包、晶振电容与电平匹配
刚接触 STM32 工程的人最容易卡在开发环境。Keil5 安装 STM32 芯片包时,如果 Pack 版本和 CubeMX 生成的代码版本不一致,编译会报各种奇怪的未定义错误。建议先装齐对应系列的 Device Pack,再打开 CubeMX 生成的工程,别用旧工程硬改芯片型号。如果你用的是国产替代芯片,APM32 这类型号可以直接吃 STM32 的工程,但记得在魔术棒里改一下 Device 选项,否则烧录时报烧录算法错误。
硬件上还有两个细节容易忽略。第一是晶振电容,STM32 外部 8M 晶振的负载电容一般按晶振规格书选 18pF 到 22pF,但走线电容和芯片引脚电容会改变实际谐振频率,焊接后最好用示波器量一下系统时钟,万用表量不出来,错配会导致串口波特率偏差,模组 AT 指令时通时不通。第二是 NB-IoT 模组的串口电平,不少模组是 1.8V 电平,STM32 的 USART 引脚默认 3.3V,直接连接容易损坏模组或收不到数据,最稳妥的做法是加电平转换芯片,或者选用 3.3V 电平兼容的模组型号。如果调试时发现 HAL_Delay 偶尔卡死,多半是 SysTick 中断优先级被改动或调用位置在中断上下文里,把 I2C 和串口中断优先级理一遍再看。
6. 把光照度采集系统的待机功耗压下来的两个落地技巧
6.1 用 PSM 和 eDRX 控制 NB-IoT 射频的休眠时机
NB-IoT 待机功耗的大头在射频,不配置 PSM 时模组会保持监听,几十毫安级别的电流一直放。PSM(Power Saving Mode)允许设备在空闲一段时间后进入深度休眠,休眠期间网络侧认为设备不可达,但已注册的信息保留,醒来后不用重新附着,可以直接发数据。常见配置是用 AT+CPSMS 打开,并指定 T3324(活跃定时器)和 T3412(周期性注册更新定时器):
// T3324=60秒进入PSM,T3412=30分钟唤醒一次做周期性更新 AT+CPSMS=1,,,"00000101","00111000"这两个值都用 8 位二进制表示定时器编码,具体 bit 含义在模组 AT 手册里有表格,不同模组微有差异。eDRX 则可以看成轻量休眠,适合需要保持可达性的场景,用 AT+CEDRXS=1 配置。光照度采集通常一小时上报一次,PSM 比 eDRX 更合适,因为上报间隙不需要接收下行。
6.2 设备端加一层本地阈值过滤,比调参更值钱
STM32 上电后先读一次 BH1750,光照度只有变化超过设定阈值才组装 MQTT 报文,否则只更新本地缓存。阈值按场景设:室内照明监控可以设 20 lx,农业补光跟踪可以设当前值的 10%。判断代码很短,但对流量和功耗影响很明显:
float new_lux = bh1750_read_lux(&hi2c1); if (fabsf(new_lux - last_lux) > LUX_THRESHOLD) { report_to_tencent(new_lux); last_lux = new_lux; }上报完后调用 STM32 的 HAL_PWR_EnterSTOPMode 进入停止模式,并把 NB-IoT 模组设为 PSM 休眠,定时器唤醒后再走一遍采集流程。这一套组合下来,关键在“少发报文”和“早进 PSM”。可以先在本地用电流钳表量一下 PSM 状态的待机电流,再逐步加阈值过滤,往往能在一个上报周期内看到明显改善。
本文还有配套的精品资源,点击获取