STM32+NB-IoT光照度监控上云:BH1750到腾讯云小程序全链路实战
2026/9/16 16:49:37 网站建设 项目流程

简介:基于STM32与NB-IoT的光照度采集上传腾讯云项目,面向物联网初学者和嵌入式开发者,完整演示了从传感器数据采集、NB-IoT网络传输到云端存储及微信小程序展示的端到端流程,覆盖硬件、通信、云服务和移动端多技术栈。资源包共5个文件,整体约4.04MB,包含两份STM32工程源码(分别对应标准库与Pro版本)、项目简介与搭建步骤、善学坊官方学习指南以及开源许可证,便于对照不同开发环境选用。已有643人学习下载。通过该资源,读者可掌握BH1750或TSL2561等光照度传感器的数据读取与处理、NB-IoT模块的AT指令配置与数据上报、腾讯云API的接入与数据可视化;配套图文教程从零讲解项目背景、硬件接线、代码烧录和云端配置,能有效缩短环境搭建与排错周期。对于希望快速上手NB-IoT+STM32+云平台综合项目的人而言,这是一个高性价比的实践参考。

1. 一块开发板、一个云平台,把光照度链路打通到底

做物联网的同学都清楚,设备端采集数据从来不是难点,STM32 的 ADC 或者 I2C 接口读个 BH1750 光照度传感器是基本功;真正的分水岭在"数据怎么稳定、低成本地到达云端"。Wi-Fi 方案受制于路由器和功耗,2G/4G 模组在弱信号场景下心有余而力不足。这个项目给了一条更贴合实际部署的路径:用 NB-IoT 窄带蜂窝网络,把 STM32 采集的光照度数据直接推送到腾讯云,再通过微信小程序实时查看。NB-IoT 的优势在于覆盖深、穿透强、单连接功耗极低,适合分布广、数据量小、不要求高实时性的场景——光照度监控恰好命中这些特征。适合的人群也很明确:想从"裸机裸板"走向"端-管-云"完整闭环的嵌入式开发者,以及做智慧农业、仓储环境监测、城市照明管理的工程人员。开放源码里已经包含了标准库和 HAL 库两套 STM32 工程,还附带搭建文档,这也是我推荐直接拿它当起点、而不是自己去拼模块的原因。

2. NB-IoT 与 STM32 的取数链路:从传感器到模组串口

2.1 光照度传感器的两种接入方式,别一上来就写代码

这个项目里的"光照度"最终要变成比特流,但传感器和 STM32 之间怎么接线,直接决定了固件代码的结构。我拆过的同类项目里,BH1750 和 TSL2561 是最常见的两选一,它们的差别值得先讲清楚。

  • BH1750:I2C 接口,16 位分辨率,量程 1~65535 lx。不需要校准,上电就能读数。缺点是一次转换要等 120ms,不适合高速采样。
  • TSL2561:同样是 I2C,但支持积分时间和增益调节,暗光下表现更好。缺点是寄存器配置比 BH1750 复杂,新手容易把地址搞错。
  • 光敏电阻 + ADC:最省成本,但输出是非线性的,需要查表或指数拟合,精度远不如数字传感器。

我建议你跟项目源码保持一致,优先用 I2C 数字传感器。原因不只是精度,而是 STM32 的 I2C 外设天然适合这种低频、短距离的读取,代码里也不用做校准曲线。源码里如果看到I2C_ReadReg()这类函数,那就是走的这条路。

I2C 接入时的硬件细节:BH1750 的 ADDR 引脚接 GND 时地址是 0x23,接 VCC 时是 0x5C。STM32 的 PB6/PB7 或者 PB8/PB9 分别对应 I2C1/I2C2 的 SCL/SDA,接上 4.7kΩ 上拉电阻到 3.3V。这个上拉电阻不是可选项,漏极开路的总线没有它,通信会间歇性失败,而且问题极其隐蔽——示波器看波形全是正常的,但程序里就是读回0xFF

/* HAL 库 I2C 读取 BH1750 的示例 */ #define BH1750_ADDR 0x23 // ADDR 接 GND #define BH1750_POWER_ON 0x01 #define BH1750_ONE_H_MODE 0x20 // 一次高分辨率模式,1lx 精度 uint8_t bh1750_init(void) { uint8_t cmd = BH1750_POWER_ON; if (HAL_I2C_Master_Transmit(&hi2c1, BH1750_ADDR << 1, &cmd, 1, 100) != HAL_OK) return 0; cmd = BH1750_ONE_H_MODE; if (HAL_I2C_Master_Transmit(&hi2c1, BH1750_ADDR << 1, &cmd, 1, 100) != HAL_OK) return 0; return 1; } uint16_t bh1750_read_lux(void) { uint8_t buf[2]; if (HAL_I2C_Master_Receive(&hi2c1, BH1750_ADDR << 1, buf, 2, 100) != HAL_OK) return 0xFFFF; return (buf[0] << 8) | buf[1]; }

这段代码揭示了一个容易被忽视的点:BH1750 在POWER_ON之后必须等待大约 10ms 才能发送测量命令,否则芯片还在休眠状态,I2C 会 NACK。另外,HAL_I2C_Master_Transmit的最后一个参数是超时时间(ms),在调试时如果总线挂死,HAL 会在这里卡住然后返回超时错误,而不是死循环——这点比标准库的I2C_WaitEvent好用很多。实际项目里我会把超时时间从 100ms 缩短到 20ms,配合一个重试计数器,因为光照度读取本来就是个低频操作,没必要为单次失败等 100ms。

初始化完成后,芯片会返回确认信号。实际测量时,高分辨率模式需要 120ms 的转换时间,所以你调用bh1750_read_lux()之前,至少得留足这个间隔。源码工程里如果有定时器或者HAL_Delay,那这个时序就已经被考虑进去了。

2.2 NB-IoT 模组的 AT 指令状态机:串口发出去的不是字符串,是状态

STM32 采到光照度数据之后,接下来要交给 NB-IoT 模组上传。这里必须先建立一个认知:NB-IoT 模组本质上是一个"串口转网络"的黑盒,STM32 通过 UART 发送 AT 指令控制它。模组厂商不同(BC26、BC35-G、M5310 等),AT 指令集略有差异,但核心流程是通用的:检查模组状态、附着网络、建立连接、发送数据。

我在源码里看到NB-IoT.zip这个压缩包,里面应该包含了模组的驱动代码和 AT 指令的封装函数。调试串口助手时,我一般会分三步验证链路是否通畅:

  1. 上电后发送AT,期待返回OK,这能确认模组本身是否活着。
  2. 发送AT+CGATT?,返回值是1表示已附着到 NB-IoT 网络。
  3. 发送AT+CEREG?,返回值0,10,5表示已注册网络。

AT+CSQ返回的信号质量值(RSSI)又是一个判断指标,一般10~31之间都能正常工作,低于10说明信号弱,数据上报大概率会失败。这里有一个经验值:AT+CSQ的返回值是099时,不用继续往下调试了,先检查天线和 SIM 卡状态,比浪费时间改代码有效得多。

/* NB-IoT 发送数据到腾讯云的核心序列 */ static const char* AT_CMD[] = { "AT+CEREG?\r\n", // 检查网络注册状态 "AT+CSCON=0\r\n", // 关闭连接状态主动上报,节省流量 "AT+QMTCFG=\"recv/mode\",0,0,1\r\n", // 配置 TCP 接收模式 "AT+QMTOPEN=0,\"your-mqtt-host\",1883\r\n", // 打开 MQTT 连接 "AT+QMTCONN=0,\"clientId\",\"username\",\"password\"\r\n", // 鉴权连接 "AT+MIPLNOTIFY=0,0,0,0,0,0,1,0,\"supply_voltage\",1,1,1,1,3,\"123\"\r\n" };

这是一段高度抽象的序列,实际项目里每一条 AT 指令之间都必须等待模组返回OK或错误码,不能连续发送。原因在于 NB-IoT 模组的协议栈是单线程的,上一条指令还没处理完,下一条来了就直接丢弃。我用状态机来管理这个过程:SEND->WAIT_OK->CHECK_RESP->NEXT_CMD,每步设 5 秒超时,超时后重发一次,累计三次失败就重启模组。这是被现场环境逼出来的方案——弱网下模组偶发无响应太常见了,不加状态机,程序会卡死在串口等待里。

项目实际使用的是 MQTT 协议接入腾讯云,因为腾讯云 IoT 平台对 MQTT 的支持比较完善。模组上电后如果直接走 TCP 连接,连上后还需要自己处理心跳、重连、断线检测;MQTT 模组一般固件内置了这些逻辑,应用层代码省不少事。颗粒度再往下看,腾讯云 IoT 的 MQTT 连接需要三元组认证:ProductIDDeviceNameDeviceSecret。这个三元组在腾讯云控制台创建设备时自动生成,源码里大概率是写死的,但生产环境我建议烧录前用脚本批量生成,避免固件泄露导致设备被仿冒。

2.3 上报数据格式与 Topic 设计:云端的影子设备靠这个活着

数据从模组出去,不是随便甩个AT+MIPLNOTIFY就能被腾讯云正确解析的。NB-IoT 模组走的是 LwM2M 协议,而 LwM2M 基于 IPSO 对象模型,每个传感器数据都要映射到一个 Object ID 和 Resource ID 上。腾讯云 IoT 平台的规则是:设备端用AT+MIPLNOTIFY上报,平台端会在设备影子里的对应资源下更新 JSON。

光照度在 IPSO 模型里通常归属 Object ID3301(Light Control 或 Illuminance),Resource ID5700是实际测量值。如果你的模组固件支持 LwM2M,代码里需要先把"光照度"这个资源注册好,再上报数据。

char cmd[128]; snprintf(cmd, sizeof(cmd), "AT+MIPLNOTIFY=0,0,0,0,0,0,1,0," "\"illuminance\",%u,%u,%u,%u,%u,\"%u\"\r\n", RES_OBJ_ID, RES_INST_ID, RES_ATTRIB_READ | RES_ATTRIB_WRITE, TYPE_INT, 0, lux); HAL_UART_Transmit(&huart2, (uint8_t*)cmd, strlen(cmd), 1000);

这个指令各参数的含义依次是:消息 ID(0 表示自动生成)、Object ID、Object Instance ID、Resource ID、操作标志、数据格式。%u填充的lux是光照度值,转成十进制字符串,所以snprintf里的最后一个%u是数据,前面的数字是协议固定的。如果用的是腾讯云 IoT 的 MQTT 方式,上报 JSON 的话,那就走$thing/up/property/{ProductID}/{DeviceName}这个 Topic,数据格式是:

{ "method": "report", "clientToken": "client-123", "params": { "illuminance": 356 } }

两种协议二选一即可,源码如果用的是 LwM2M AT 指令,就按前者;如果模组固件是 MQTT 版,就按后者。数据格式选错,平台端会报{"code": 403, "message": "payload format error"},这个问题在调试时经常出现,先查 Topic 和方法名,再查 JSON 缩进,别怀疑是网络问题。

3. 从 CubeMX 到腾讯云:把这套工程完整跑起来

3.1 工程结构解析:标准库和 HAL 库各起什么作用

源码包里同时提供了源代码 for STM32 Std NB-IoT.zip源代码 for STM32 Pro NB-IoT.zip,这正好对应两种主流固件库。Std全称 Standard Peripheral Library,是意法半导体早期的标准外设库,代码直接操作寄存器,适合老工程师维护老项目,但 GPIO 配置要逐位设置,写起来繁琐;Pro全称 HAL (Hardware Abstraction Layer) 库,以HAL_GPIO_ReadPin这类函数为接口,CubeMX 图形化配置后自动生成初始化代码,可读性和可移植性都更强。

我的建议是:如果你还在用 Keil5 建裸机工程,就选 HAL 库版本,后面切芯片型号(比如从 F103 换到 L431)时,CubeMX 能直接改配置重新生成;标准库代码在芯片停产边缘的当下,新项目不值得投入。Keil 环境下安装 stm32 芯片包是第一步,直接在 Keil 的 Pack Installer 里搜STM32F1xx_DFP,安装完才能在 Device 列表里看到目标芯片。如果安装不上,去 Keil 官网手动下载 DFP 包,用Pack Installer -> File -> Import导入。

CubeMX 配置里几个关键的引脚复用要确认下:I2C1 的 PB6/PB7 给传感器,USART2 的 PA2/PA3 给 NB-IoT 模组,电源指示灯接 PC13(大多数 F103 开发板板载)。时钟树里把HCLK设为 72MHz,APB1 分频器设为/2,这样 I2C 的时钟源才不会是 36MHz 超频状态。I2C 速度选择Standard Mode(100kHz),别图快选Fast Mode(400kHz),BH1750 最大支持 400kHz,但 STM32 的 I2C 在某些版本上有总线错误 bug,跑低速稳得多。

3.2 编译下载与串口抓数:先让数据在本地可见

工程在 Keil5 里打开后,先Build一次,确认 0 Error 0 Warning,然后Options for Target -> Debug里选择ST-LinkJ-LinkSettings -> Flash Download勾选Reset and Run。下载完按复位键,串口助手连接 STM32 的 USART1(跟 NB-IoT 模组相连的那个口),波特率源码里是什么就设什么(通常是 115200)。此时你应该能看到模组返回的OK+QMTOPEN: 0,0

如果串口没有输出,先别怀疑代码,用示波器或万用表量 PB6/PB7 和 PA2/PA3 的电平。3.3V 的 USART 信号在串口模块的 TTL 端子里能直接测到跳变,如果一直是低电平或者悬空,多半是跳线帽没插对,或者是 USB 转串口的 GND 和板子 GND 没共地。这个问题我们现场叫"串口三大坑":没共地、Tx/Rx 接反、波特率不符。优先级高于任何代码逻辑。

光照度数据验证:拿手机闪光灯照一下传感器,串口里打印的 lux 值应该从几十跳到几百;用手捂住,数值应该掉到 10 以下或者接近 0。如果数值不变,检查 I2C 地址,大概率 BH1750 的 ADDR 引脚接法跟代码里的地址不一致。

3.3 腾讯云侧:创建设备、订阅 Topic、看数据流转

云端的配置决定了上报数据最终去哪。登录腾讯云控制台,搜索"物联网开发平台 IoT Explorer",新建项目,区域选离你设备最近的城市,产品品类选"智慧生活-家居安防-光照度传感器"。创建完产品后,在产品详情页找到ProductID(格式类似X1234567),然后创建设备,拿到DeviceNameDeviceSecret。这三个值填到工程源码中的iot_config.h里对应的宏定义处。注意腾讯云的DeviceSecret是设备端鉴权用的,如果走 MQTT 会自动生成usernamepassword,不要手工改。

Topic 的订阅规则要特别注意:设备端上报属性走$thing/up/property/{ProductID}/{DeviceName},云端下发指令走$thing/down/property/{ProductID}/{DeviceName}。在工程代码里,QMTCONN指令后要跟着QMTSUB订阅下行 Topic,这样小程序端通过云端 API 下发控制指令(比如调整上报频率阈值)时,设备能收到。

代码里我一般会把这个回调函数留出来:

void thing_down_callback(char *msg, uint16_t len) { // 解析 msg 中的 JSON,识别 method: "control" 或 "report_reply" // 如果包含 "illuminance_threshold",更新全局变量 threshold }

这个回调里面做的是把腾讯云下发的 JSON 转换成业务逻辑。比如云端下发了{"method":"control","params":{"threshold":500}},设备端就把阈值更新为 500,之后光照度低于 500 时上报,高于 500 时不上报。这种增量上报策略能显著降低功耗。

3.4 微信小程序端:数据从云端 API 到手机屏幕

源码包里没有直接给出独立的小程序工程,但腾讯云的 IoT Explorer 提供了小程序端 SDK,流程是可以在 30 分钟内跑通的。微信开发者工具里新建项目,导入腾讯连连小程序模板,然后在app.js里填入产品 ID 和密钥。注意这里的密钥是腾讯云 API 密钥(SecretId/SecretKey),不是设备密钥,在小程序端做签名时需要用到。它通过TC3-HMAC-SHA256算法做签名,公众提到"签名算法报错"十有八九是时间戳格式不对,必须是秒级 Unix 时间戳。

小程序端订阅设备属性的代码大致是:

const deviceClient = require('./device_client'); deviceClient.registerDevice({ productId: 'YOUR_PRODUCT_ID', deviceName: 'YOUR_DEVICE_NAME', onData: (payload) => { // payload.status 是设备上报的 JSON const lux = payload.status.reported.illuminance; this.setData({ lux: lux }); } });

这段代码的核心是onData回调——它监听云端下发的设备数据,一旦 STM32 上报光照度,腾讯云会通过 WebSocket 或 HTTP 长轮询把数据推给小程序。小程序端拿到illuminance字段后,渲染到进度条或数字组件上,就完成了"端-管-云-端"的闭环。如果小程序一直收不到数据,优先查云端的消息消费日志,而不是小程序代码——绝大多数链路断点发生在设备端没上报或 Topic 不对。

4. 功耗、稳定性与现场调优:把 NB-IoT 项目做“硬”

4.1 上报频次与 PSM/eDRX 的权衡

NB-IoT 的低功耗不是天然属性,而是靠 PSM (Power Saving Mode) 和 eDRX 两种机制省出来的。PSM 模式下设备大部分时间深度睡眠,只在唤醒后注册网络、上报数据、加密传输,然后再次进入 PSM。这个模式的开启指令通常是模组厂商自定义的 AT 指令序列,比如移远 BC26 是AT+CPSMS=1,,,"00000010","00000011",高新兴的模块类似但参数含义略不同。

上报频次的设置直接影响功耗和流量成本。光照度监控这个场景,如果用在室内照明控制,5 分钟上报一次足够;用在农业大棚,可能需要 1 分钟一次;如果是停车场照明管理,10 分钟都行。上报频次每减小一半,电池寿命差不多翻倍。这个项目如果用的是 3.6V 锂电池供电,配合 PSM,5 分钟一次上报大概能满足 8 个月以上的续航。

参数一栏里我把T3324(eDRX 周期)和T3412(TAU 定时器)都做了调整——不过实际场景里大部分 NB-IoT 模组默认参数就够了,不需要手动改。如果运营商网络用的是专网卡,PSM 参数会被网络强制覆盖,你设了也白设。

4.2 串口 AT 指令响应超时的三个修正方案

我在现场调试时遇到最多的问题是模组返回超时,表现为HAL_UART_Transmit发完 AT 后,收不到OK,程序就一直等下去。这个问题的根源不一定是模组挂了,而是网络附着阶段本来就慢——NB-IoT 从冷启动到附着网络的时间,实测 2 到 10 秒不等,而很多工程代码里的串口接收超时只有 1 秒。

第一个修正方案:将步进状态机里的等待超时从 1 秒提高到 5 秒。第二个方案:在 UART 接收中断里设置一个全局标志位,当收到\n时置位,主循环查询标志位。第三个方案更激进但最可靠——模组一个 AT 指令如果长时间无响应,直接拉低 RESET 引脚重启模组,初始化流程从头走一遍。前两个方案是治标,第三个方案是有效兜底。

代码层面的具体操作可以是这样的:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { rx_buffer[rx_index++] = rx_byte; if (rx_byte == '\n') { rx_complete_flag = 1; } HAL_UART_Receive_IT(&huart2, &rx_byte, 1); } }

这是用串口空闲中断收完一帧数据的方式,必须保证在回调里重新调用HAL_UART_Receive_IT开启下一个字节的中断接收,否则只进一次中断就停了。rx_buffer收到完整 AT 响应后,主循环里做字符串匹配,比如搜"+QMTCONN: 0,0"算连接成功,搜"ERROR"就算失败,重新发一次。注意 AT 响应的结尾是\r\nOK\r\n,所以匹配时要用strstr()而不是strcmp()

4.3 反例对比:为什么 Wi-Fi 方案在这个场景里处处吃亏

最后聊一个选择层面的问题。每个用 NB-IoT 的项目都会被人问:为什么不直接用 ESP8266 加 MQTT?我在这个项目里也反复权衡过:如果设备部署在有 Wi-Fi 的室内,ESP8266 确实成本低、上传速率高、调试方便。但光照度监控这类设备的位置选择,往往是在窗边、外墙、温室、铁皮房、隧道、道路照明——这些位置 Wi-Fi 覆盖要么没有,要么极不稳定。

NB-IoT 的覆盖增益是 20dB,比 Wi-Fi 能深入地下、墙后、偏远的农业园区。更重要的一点:NB-IoT 的模块功耗曲线更平滑。ESP8266 在连接 Wi-Fi 时瞬间电流能达到 300mA,而 NB-IoT 模组在空闲态只有几十微安,PSM 下甚至接近0。两者在 5 分钟上报一次的场景下,电池寿命差几倍不止。

不过也要承认 NB-IoT 的短板:上报延迟不可控。NB-IoT 网络的信道调度由基站决定,冷启动后第一条数据可能 10 秒后才被接收,这决定了它不适合做实时性要求高的业务(比如门锁开合)。光照度监控天然就是分钟级的数据需求,正好把这个短板绕了过去。选型没有最好,只有最合适——比如你在信号不稳定的移动场景里,NB-IoT 之后就走到了卫星和 LTE-M 的通道里,这是后话了。

从源码工程里实际验证一把:把implementation从 HAL 库切到标准库重新编译,你会看到工程里 USART 的初始化、中断处理函数、AT 指令拼接方式是两套完全不同的写法。这也是这个资源最大的价值——它同时让两代人在同一套设备板子上理解 NB-IoT 接入腾讯云的完整时序。当你把 PSM 配置、AT 状态机、云端三元组、小程序推送全部跑通的那一刻,往后做任何 NB-IoT 数据采集项目,你都能直接把架构和代码模板复用过去,只需要换成传感器和数据协议。

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

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

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

立即咨询