
简介基于STM32与NB-IoT的光照度采集上传腾讯云项目面向物联网初学者和嵌入式开发者完整演示了从传感器数据采集、NB-IoT网络传输到云端存储及微信小程序展示的端到端流程覆盖硬件、通信、云服务和移动端多技术栈。资源包共5个文件整体约4.04MB包含两份STM32工程源码分别对应标准库与Pro版本、项目简介与搭建步骤、善学坊官方学习指南以及开源许可证便于对照不同开发环境选用。已有643人学习下载。通过该资源读者可掌握BH1750或TSL2561等光照度传感器的数据读取与处理、NB-IoT模块的AT指令配置与数据上报、腾讯云API的接入与数据可视化配套图文教程从零讲解项目背景、硬件接线、代码烧录和云端配置能有效缩短环境搭建与排错周期。对于希望快速上手NB-IoTSTM32云平台综合项目的人而言这是一个高性价比的实践参考。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 是最常见的两选一它们的差别值得先讲清楚。BH1750I2C 接口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 指令的封装函数。调试串口助手时我一般会分三步验证链路是否通畅上电后发送AT期待返回OK这能确认模组本身是否活着。发送ATCGATT?返回值是1表示已附着到 NB-IoT 网络。发送ATCEREG?返回值0,1或0,5表示已注册网络。ATCSQ返回的信号质量值RSSI又是一个判断指标一般10~31之间都能正常工作低于10说明信号弱数据上报大概率会失败。这里有一个经验值ATCSQ的返回值是0或99时不用继续往下调试了先检查天线和 SIM 卡状态比浪费时间改代码有效得多。/* NB-IoT 发送数据到腾讯云的核心序列 */ static const char* AT_CMD[] { ATCEREG?\r\n, // 检查网络注册状态 ATCSCON0\r\n, // 关闭连接状态主动上报节省流量 ATQMTCFG\recv/mode\,0,0,1\r\n, // 配置 TCP 接收模式 ATQMTOPEN0,\your-mqtt-host\,1883\r\n, // 打开 MQTT 连接 ATQMTCONN0,\clientId\,\username\,\password\\r\n, // 鉴权连接 ATMIPLNOTIFY0,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 连接需要三元组认证ProductID、DeviceName、DeviceSecret。这个三元组在腾讯云控制台创建设备时自动生成源码里大概率是写死的但生产环境我建议烧录前用脚本批量生成避免固件泄露导致设备被仿冒。2.3 上报数据格式与 Topic 设计云端的影子设备靠这个活着数据从模组出去不是随便甩个ATMIPLNOTIFY就能被腾讯云正确解析的。NB-IoT 模组走的是 LwM2M 协议而 LwM2M 基于 IPSO 对象模型每个传感器数据都要映射到一个 Object ID 和 Resource ID 上。腾讯云 IoT 平台的规则是设备端用ATMIPLNOTIFY上报平台端会在设备影子里的对应资源下更新 JSON。光照度在 IPSO 模型里通常归属 Object ID3301Light Control 或 IlluminanceResource ID5700是实际测量值。如果你的模组固件支持 LwM2M代码里需要先把光照度这个资源注册好再上报数据。char cmd[128]; snprintf(cmd, sizeof(cmd), ATMIPLNOTIFY0,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);这个指令各参数的含义依次是消息 ID0 表示自动生成、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设为 72MHzAPB1 分频器设为/2这样 I2C 的时钟源才不会是 36MHz 超频状态。I2C 速度选择Standard Mode100kHz别图快选Fast Mode400kHzBH1750 最大支持 400kHz但 STM32 的 I2C 在某些版本上有总线错误 bug跑低速稳得多。3.2 编译下载与串口抓数先让数据在本地可见工程在 Keil5 里打开后先Build一次确认 0 Error 0 Warning然后Options for Target - Debug里选择ST-Link或J-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然后创建设备拿到DeviceName和DeviceSecret。这三个值填到工程源码中的iot_config.h里对应的宏定义处。注意腾讯云的DeviceSecret是设备端鉴权用的如果走 MQTT 会自动生成username和password不要手工改。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 是ATCPSMS1,,,00000010,00000011高新兴的模块类似但参数含义略不同。上报频次的设置直接影响功耗和流量成本。光照度监控这个场景如果用在室内照明控制5 分钟上报一次足够用在农业大棚可能需要 1 分钟一次如果是停车场照明管理10 分钟都行。上报频次每减小一半电池寿命差不多翻倍。这个项目如果用的是 3.6V 锂电池供电配合 PSM5 分钟一次上报大概能满足 8 个月以上的续航。参数一栏里我把T3324eDRX 周期和T3412TAU 定时器都做了调整——不过实际场景里大部分 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 数据采集项目你都能直接把架构和代码模板复用过去只需要换成传感器和数据协议。本文还有配套的精品资源点击获取