NB-IoT温湿度采集实战:STM32L152+BC26+LWM2M数据上云全流程
简介:本资源是一套面向嵌入式物联网开发者的STM32L152单片机实战项目例程,聚焦低功耗广域网场景,解决温湿度传感器数据通过NB-IoT模块(BC26)接入电信云/华为云IoT平台的核心问题,适用于高校课程设计、毕业设计及企业原型验证等中初级开发需求。压缩包共1119个文件,含311个C源码与258个头文件(构成完整KEIL标准库工程)、201个JavaScript脚本(用于平台侧解析或前端展示)、157个PNG图标资源(含LCD界面素材)、91个文本说明文档,以及调试配置类文件(如uvproj、ld、icf等),整体大小为20.32MB。已有78人学习下载,体现其在NB-IoT终端接入领域的实用参考价值。读者可直接复用ADC采集、串口AT指令驱动BC26、LWM2M协议栈轻量集成、云平台注册与数据上报等关键模块代码;所有外设引脚定义、通信时序注释详尽,配套清除编译残余的批处理脚本与SDIO/LCD/RTC等底层驱动文件,显著降低硬件适配与协议对接门槛。 搞这个项目之前,我一直觉得NB-IoT是“运营商的玩意儿”,离嵌入式开发很远。直到手头接了个环境监测需求:要在几间办公室角落放温湿度采集点,电池供电、不带网关、数据要能上云,最好在手机或电脑上随时能看到实时曲线。看了一圈,Wi-Fi在无路由的角落直接淘汰,LoRa又得自己搭网关和协议栈,最后把目光锁定在STM32L152 + BC26-NB-IoT + LWM2M这条链路上。
标题里那句“温湿度ADC数据”就是整条链路的起点:传感器输出模拟电压,STM32L152的ADC多通道采样,再经过DMA搬进内存,换算成温度和湿度数值;BC26模组负责走NB-IoT网络,用LWM2M协议把数据写到物联网平台的对应资源上。整个过程不算复杂,但涉及的环节很多:硬件供电、ADC精度、模组AT指令、平台侧设备建模、报错排查……我把实际跑通项目的完整过程整理在下面,希望能帮到正在折腾同类方案的人。
1. 选型这台戏:STM32L152、BC26和LWM2M到底是怎么凑到一起的
1.1 为什么绕开Wi-Fi和LoRa,选了NB-IoT
很多人一提到物联网数据传输,第一反应就是ESP8266接Wi-Fi,便宜又简单。但这个项目的第一约束是“现场没有稳定网络”:办公室角落、仓库货架附近,Wi-Fi信号基本是残废状态。拉网线又不现实,这时候NB-IoT的优势就体现出来了——只要运营商基站覆盖到位,模组直接进网,不需要用户侧配路由器,也不需要自己维护网关。
LoRa其实也在候选名单里,但问题同样明显:LoRa只解决了“最后一公里”的无线传输,数据从网关到云端还是得靠以太网、4G或Wi-Fi。等于我得自己搭一个网关设备,还要处理多节点接入、确认重传、服务器转发,工程量直接翻倍。NB-IoT把“设备到平台”这条完整通道都打包好了,我把数据交给模组,模组通过基站发给平台,平台再通过API或消息推送给我。对个人开发者或小团队来说,省掉网关和服务器这层,价值非常明显。
1.2 为什么是STM32L152而不是F1/F4
STM32L152是Cortex-M3内核的低功耗系列,主频不高(最高32MHz),但胜在低功耗模式做得细:STOP模式+RTC唤醒,典型电流能到3uA左右,对电池供电系统非常友好。F103虽然资料多、用的人多,但正常运行时电流就多出好几倍,STOP模式下的唤醒电流也没法看。这个项目要求电池供电跑半年以上,L152几乎是同价位里最稳妥的选择。
BC26模组也同样考虑了低功耗:它支持PSM(Power Saving Mode)和eDRX,数据上报完可以进入微安级别的休眠状态,配合L152的低功耗调度,整套设备的待机电流能做到很低。这也决定了后面的软件架构——不能用“轮询跑满主频”的思路,必须用状态机+定时唤醒的方式来设计。
1.3 LWM2M在这个链路里扮演什么角色
LWM2M全称Lightweight M2M,是OMA组织定义的设备管理协议,跑在CoAP/UDP之上,专门给资源受限的物联网设备用。这里得先理清一个概念:LWM2M不是物理层的通信方式,它更像“设备和平台之间的对话规则”。NB-IoT网络负责把数据包从基站传到核心网,但数据包怎么组织、平台怎么解析,就是LWM2M干的事。
LWM2M的核心模型是“对象-实例-资源”。举个实际例子:温度对象ID是3303,实例0,里面的5700资源就是“当前温度值”。我在MCU里执行“往3303/0/5700写入25.6”,BC26模组就会把它封装成CoAP请求发给平台,平台根据设备模型中定义好的对象ID去解析,数据就能落库了。华为云IoT平台、电信云CTWing都支持这套标准模型,所以用LWM2M还有个额外好处:换平台时,只要改服务器地址和鉴权参数,MCU侧代码几乎不用动。
整个系统的数据流向可以这样看:
- 传感器输出模拟电压
- STM32L152的ADC多通道扫描采样,DMA搬运到内存
- MCU换算成温度和湿度数值
- 通过UART把数值和对应的LWM2M资源路径发给BC26
- BC26封装CoAP报文,走NB-IoT基站上行
- 平台解析LWM2M资源,写入设备属性
- 应用侧通过平台API或消息推送拿到最终数据
2. 温湿度ADC采集链路:把模拟信号变成能上云的数值
2.1 传感器信号链设计:别一上来就调ADC,先看前端电路
标题里写的是“温湿度ADC数据”,说明温度和湿度都是通过ADC采样的模拟量。我做的是两个通道:温度用NTC热敏电阻加分压电阻,湿度用模拟输出的湿度传感器(比如HIH5030)。这种方案的优点是电路简单、成本低,MCU直接读ADC就行。
温度通道的原理是NTC阻值随温度变化,和固定电阻分压后,节点电压也随之变化。选型时注意两点:一是NTC的B值要选好,B值越大,温度分辨率越高;二是分压电阻的阻值要和NTC在目标温度区间的阻值接近,这样分压曲线最陡,灵敏度最高。我用的NTC是10k@25℃、B值3950,分压电阻取10k,在0~50℃范围内输出电压基本落在0.5V~2.8V,正好适配3.3V供电的ADC量程。
湿度通道用HIH5030这类电压输出型湿度传感器,输出范围是0.8V~3.3V对应0%~100%RH,线性度不错,校准也很简单,两三个湿度点就能拉出直线。这里有个电路细节:传感器输出阻抗偏高时,ADC内部采样电容充电时间不够,会导致采样值偏低。解决办法是把ADC采样时间调到最长(STM32L152 ADC采样时间可配置到最大档),或者在传感器输出和ADC引脚之间加一个运放缓冲。
2.2 STM32L152的ADC多通道扫描+DMA配置
STM32L152内置12位ADC,采样通道够用,关键是它支持扫描模式、连续转换和DMA。这套组合的价值在于:硬件自动按通道顺序转换,转换结果自动通过DMA搬进内存,CPU完全不需要干预。
我分配了三个通道:温度通道、湿度通道、芯片内部VREFINT通道。为什么要把VREFINT也采进来?因为电池供电时,参考电压会随供电电压波动,直接用3.3V当满量程算会引入误差。VREFINT是内部固定的基准电压,通过它反推当前实际的VDDA,换算出来的电压值更准。
用STM32CubeMX配置很直观,关键选项如下:
- ADC扫描模式:Enable
- ADC连续转换:Enable
- DMA连续请求:Enable
- DMA模式:Circular
- ADC采样时间:选最大档(如239.5周期)
- 转换通道数:3
配置好后,初始化代码会自动生成,DMA的中断回调里我置一个标志位:
volatile uint16_t adc_values[3]; volatile uint8_t adc_complete_flag = 0; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { adc_complete_flag = 1; } } uint8_t app_adc_read_all(uint16_t *temp_raw, uint16_t *humi_raw, uint16_t *vdda_raw) { adc_complete_flag = 0; HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_values, 3); // 等待转换完成,这里可以加超时保护 while (!adc_complete_flag) { // 可插入看门狗喂狗 } HAL_ADC_Stop_DMA(&hadc1); *temp_raw = adc_values[0]; *humi_raw = adc_values[1]; *vdda_raw = adc_values[2]; return 1; }实际使用中有一点要注意:DMA搬进数组的数值顺序和你配置通道的顺序一致,别把温度通道和湿度通道搞反了。另外,DMA模式下如果连续转换一直开着,功耗比较浪费,所以我是按需启动:唤醒后启动一次DMA转换,拿到结果马上关掉。
2.3 定时器触发采样和数字滤波:别把毛刺直接传给平台
这个项目的数据频率不高,10分钟上报一次,但每次上报前我会连续采32次,做滑动平均和中值滤波。为什么不一秒采一次存起来?低功耗系统能少运行一秒是一秒,集中采样反而能把ADC和CPU的运行时间压缩到最短。
定时器触发ADC还有另一个好处:可以精确控制采样时刻,避开设备射频发射瞬间的电源干扰。我的经验是,BC26发射时电源纹波比平时大很多,如果ADC在这时候采样,结果会被污染。所以软件流程是先采完ADC,再唤醒BC26上报数据,顺序不能反。
滤波代码不复杂,一个滑动平均加中值去毛刺:
#define FILTER_N 8 uint16_t app_adc_filter(uint16_t new_sample) { static uint16_t buf[FILTER_N]; static uint8_t idx = 0; static uint8_t cnt = 0; uint32_t sum = 0; uint8_t i; buf[idx] = new_sample; idx = (idx + 1) % FILTER_N; if (cnt < FILTER_N) cnt++; for (i = 0; i < cnt; i++) sum += buf[i]; return (uint16_t)(sum / cnt); }ADC原始码到温度的换算,我用查表+NTC公式相结合。NTC的阻值与温度关系可以用Steinhart-Hart方程描述,但如果不想算对数,直接做一张0~50℃的电压-温度对照表,线性插值即可,误差完全够用。湿度传感器线性度好,直接两段式线性换算就行。
3. BC26模组接入:从串口AT指令到LWM2M注册
3.1 硬件上电的坑:BC26的峰值电流能把MCU拉复位
BC26硬件连接不复杂:VCC接3.6~4.2V电源,UART的TX/RX和MCU交叉连接,PWRKEY拉低100毫秒以上开机,SIM卡座插NB卡,天线焊好。但供电这关我确实踩过坑——BC26在NB-IoT发射瞬间,电流峰值能到几百毫安甚至更高,如果电源线太细、电容太少,电压会瞬间跌破MCU的工作电压,整机直接复位。
解决方法是VBAT引脚附近加100uF钽电容+10uF陶瓷电容+100nF去耦电容,电源走线尽量短粗。用电池供电的话,最好在电池和负载之间并联一个大容量储能电容,让发射瞬间的电流从电容里走,而不是从电池内阻上硬拉。
L152和BC26的UART电平都是3.3V(BC26部分版本支持1.8V),可以直接相连。但如果BC26是1.8V版本,必须用电平转换芯片,不能直接用电阻分压凑合,否则模组识别不了高电平。
3.2 网络附着检查:SIM卡、信号和CEREG的返回值
模组上电后,第一件事是确认网络附着成功,否则后面LWM2M注册想都别想。我常用的检查顺序是:
AT # 返回OK,确认串口和模组正常 AT+CIMI # 返回SIM卡的IMSI,确认SIM卡能读 AT+CSQ # 查信号强度,返回值越大信号越好 AT+CEREG? # 查网络注册状态AT+CEREG?的返回值很多人看不懂,我把常用的列一下:
| 返回值 | 含义 | 能不能继续 |
|---|---|---|
| 0,0 | 未注册,正在搜索网络 | 继续等 |
| 0,1 | 已注册,本地网络 | 可以 |
| 0,3 | 注册被拒绝 | 检查SIM卡和APN |
| 0,5 | 已注册,漫游 | 可以 |
建议SIM卡选用运营商的正规NB-IoT卡,APN一般是默认配好的,但有的卡默认没配置,需要手动设置。在我的项目里,电信NB卡插上后AT+CEREG?能自动返回0,1,但我见过联通的卡要手动设APN才能附着,这个坑等遇到了再排查也来得及,关键是知道有这回事。
3.3 LWM2M配置和上报的核心AT指令
BC26内置了LWM2M协议栈,我不用在MCU侧实现CoAP和LWM2M,只需要通过AT指令配置服务器地址、开启注册、写入资源值。这里的配置思路是:
AT+QLWSCFG=0,"<平台CoAP接入地址>",5683 AT+QLWSREG=1,300 AT+QLWSDEV=3303,0,5700,25.6QLWSCFG是配置第0个LWM2M服务器的IP和端口,5683是CoAP的默认端口,如果平台要求DTLS加密,端口会变成5684,还需要配置PSK密钥。QLWSREG=1,300表示开启LWM2M注册,生命周期300秒,模组在周期内会主动向平台续注册,平台才能维持设备在线状态。QLWSDEV就是往指定对象资源写入数据:3303是温度对象,实例0,5700资源写温度值;3304是湿度对象,资源ID同样是5700。
温度、湿度各写一次之后,平台侧就能看到两条最新属性值。这里有个经验:如果平台和设备模型里没有定义3303/3304这些对象,平台会直接丢弃或报错,所以平台侧建模要和模组上报的资源路径一一对应,这个在下一节详细说。
4. 平台对接:设备建模、鉴权和数据上行
4.1 华为云IoT平台侧准备:先建模,设备才能“看得见”
华为云物联网平台(IoTDA)的对接,第一步不是写代码,而是去控制台创建产品、定义物模型。进入IoTDA控制台后:
- 创建产品,协议类型选LWM2M。
- 添加服务,比如“温湿度采集”。
- 在服务下添加属性:温度(float,只读)、湿度(float,只读)。
这里要注意,华为云IoT平台在LWM2M协议下,物模型中的属性会自动映射到指定的LWM2M对象资源。但如果你用的传感器数据上报路径是3303/0/5700这样的标准资源,华为云默认支持LWM2M标准对象,也能直接解析到设备影子中。为了保险起见,我在服务属性里特意设置了对应的LWM2M资源路径,这样平台能准确把上报值和属性关联起来。
设备注册时,设备标识符(device_id)我填的是BC26的IMEI,认证方式选密钥,填一个自己定的密钥。这个IMEI和密钥要记住,后面BC26侧可能要根据平台要求配置PSK,或至少保证上报的设备标识能和平台匹配。
4.2 从MCU buffer到平台显示:数据上行全流程对齐
平台侧准备完毕后,MCU侧整个上报流程大概是这样的:
- 唤醒后,
app_adc_read_all获取原始ADC值,换算成温度和湿度。 - 按固定格式组包,通过UART发送AT指令给BC26。
- 模组返回OK后,LWM2M模块内部把资源值封装成CoAP报文,走NB-IoT上行。
- 平台解析后写入设备属性。
代码里,上报函数我写得比较直白:
char temp_str[16]; char humi_str[16]; snprintf(temp_str, sizeof(temp_str), "%.1f", temp_value); snprintf(humi_str, sizeof(humi_str), "%.1f", humi_value); /* 上报温度 */ sprintf(cmd, "AT+QLWSDEV=3303,0,5700,%s\r\n", temp_str); uart_send_string(cmd); delay_ms(100); /* 上报湿度 */ sprintf(cmd, "AT+QLWSDEV=3304,0,5700,%s\r\n", humi_str); uart_send_string(cmd); delay_ms(100);需要注意,两条AT指令之间要给模组留足处理时间。我一开始连续发指令,第二条经常被丢弃,后来改成每发一条就等待模组返回OK,再发下一条,问题就消失了。
平台侧验证数据可以直接在控制台“设备详情”里看最新上报数据,或者开启消息跟踪,能看到上行消息的内容和时间。我建议把消息跟踪开着,联调时能直观看到模组有没有把数据送上来。
4.3 换成电信云CTWing时,哪些地方不一样
这个方案跑通后,我其实也简单验证过电信云CTWing的接入。协议栈还是LWM2M over CoAP,但有几个差异点:
- 接入地址和端口从电信云开发平台获取,不一定都是5683。
- 设备标识和鉴权需要在电信云平台先创建应用和产品,生成设备ID和注册码。
- 平台侧“数据上报”和“命令下发”的API结构不同,但LWM2M资源读写的基本逻辑一样。
所以标题里写“电信云或华为云物联网平台”,本质上不影响MCU侧核心代码,改的是平台配置和模组里的服务器地址。这也是我坚持用LWM2M而不是某平台私有协议的原因——通用性太好了。
5. 联调中的三个典型坑:从注册失败到数据丢失的完整排查链路
5.1 第一个坑:LWM2M注册总是不成功
我遇到的现象是:BC26的CEREG显示网络已注册,但平台侧设备一直显示“未激活”或“离线”。排查链路是这样的:
- 先用
AT+QLWSREG?查看注册状态,返回值如果是+QLWSREG: 0,说明LWM2M注册没开启或注册失败。 - 检查服务器地址配置:
AT+QLWSCFG?,确认IP和端口和平台控制台上的一致。端口填错是最高频的错误,5683和5684差一位,就是“不加密”和“加密”的天壤之别。 - 确认设备标识:BC26的IMEI,平台注册设备时填的设备标识必须是它。如果填错,平台找不到对应设备,注册请求直接被丢弃。
- 如果配了PSK,检查密钥是否一致。华为云IoT平台在部分接入方式下要求PSK匹配,密钥错一个字符都不行。
最后发现我的问题出在设备标识上:平台侧创建设备时填了自定义字符串,没填IMEI,导致模组注册报文里的Endpoint Name和设备不匹配。改回来后秒注册成功。
5.2 第二个坑:数据上报偶发丢失,平台端半小时没数据
这个坑比注册失败更隐蔽。设备在线,AT指令也返回OK,但平台侧数据就是断断续续。排查思路:
- 先怀疑BC26处在PSM或eDRX休眠窗口,数据包发出去后要等基站寻呼或下一个唤醒周期才能上行,导致延迟或超时。解决办法是临时把PSM关掉:
AT+CPSMS=0,再观察是否恢复正常。 - 如果关掉PSM就好了,说明是休眠策略和上报节奏的配合问题。我的处理是:上报前先发一条
AT+CEREG?确认网络状态,如果网络状态不对就不发,等下次周期重试。 - 还有一个容易忽略的点:MCU串口发完AT指令后,不要立刻进入低功耗模式,要等模组返回最终OK或错误码。否则串口数据可能只发了一半,模组还没处理完,MCU就睡了。
最终的解决办法是:上报流程设计成一个带确认的状态机,模组返回OK后再进入休眠;如果超时未响应,重新唤醒模组重试。
5.3 第三个坑:模组不定时自动重启,设备反复上下线
这个现象是最烦人的:设备运行几小时或几小时就“消失”一次,平台显示设备掉线,过几分钟又重新上线,看起来像模组复位了。
我用示波器抓BC26的VBAT波形,发现发射瞬间电压有瞬态跌落,幅度超过了模组电源跌落保护阈值。原因就是供电回路储能不够,NB-IoT射频发射脉冲电流太大,把电压拉崩了。加上BC26的天线阻抗匹配不理想,发射功耗进一步升高,问题就更加频繁。
解决办法:
- VBAT处电容加到总容量500uF以上。
- 电源走线加粗,减少串联电阻。
- 天线走线严格按照模组参考设计,长度和阻抗匹配别偷懒。
处理完后连续跑了72小时没再复现。这个坑给我的教训是:NB-IoT模组的上电瞬间和发射瞬间,电流变化剧烈,电源设计必须按峰值电流而不是平均电流来算。
6. 功耗预算、休眠策略和稳定运行的工程细节
6.1 平均功耗不是靠猜的,算一遍就清楚了
电池供电设备,先算功耗再定上报频率。我按10分钟上报一次来估算:
- MCU唤醒采集:100ms,平均电流5mA,耗电约0.5mAs。
- BC26发送数据:一次约3秒,平均电流120mA,耗电约360mAs。
- 休眠阶段:MCU STOP模式3uA + BC26 PSM模式3uA,加起来6uA,剩余597秒耗电约3.6mAs。
一个周期总耗电约364mAs,平均电流364/600 ≈ 0.6mA。一颗5000mAh的锂电池,理论工作时间5000/0.6 ≈ 8333小时 ≈ 347天,不到一年。如果换成30分钟上报一次,平均电流能降到0.2mA左右,理论工作时间超过两年。所以决定电池寿命的主要因素是上报频率,而不是低功耗模式下那几微安电流。这也是为什么我建议不要把采集频率定得太高——NB-IoT一次上报的功耗,顶得上休眠几十个小时。
6.2 运行调度:用状态机,别用定时器裸奔
低功耗设备和普通跑裸机while(1)的设备不一样,关键是不能让CPU闲等。我推荐直接上状态机,每个状态做一件事:
- SLEEP:MCU进入STOP模式,RTC定时唤醒。
- WAKEUP:RTC唤醒,初始化ADC、UART。
- SAMPLE:完成ADC采样、滤波、数值换算。
- REPORT:唤醒BC26,发送AT指令,等待OK。
- RECHECK:查CEREG,如果网络异常则重试。
- DONE:关闭外设,进入SLEEP。
状态之间的跳转由事件驱动,比如SAMPLE完成后自动切到REPORT,REPORT收到OK后自动切到DONE。用状态机的最大好处是:任何状态都能设置超时退出,不会因为某一步卡死导致整机功耗异常。
6.3 工程细节:日志、调试和稳定性
项目联调阶段,我习惯在UART上接一个USB转串口模块,打印所有AT指令和模组返回,方便复现问题。但正式部署时要关闭这些日志,因为printf这类阻塞输出在低功耗调度里非常碍事。
几个有价值的细节:
- 数据包格式:给上报的数据加帧头和CRC,虽然LWM2M和CoAP这类协议本身有校验,但MCU和模组之间的串口通信也需要校验,防止因为串口干扰导致发出去的数值已经是错的。
- 看门狗:独立看门狗在SLEEP模式下照样计数,会定时复位,所以要注意在唤醒期间喂狗,或者选用窗口看门狗配合STOP模式的策略。这个细节很容易被忽视,结果就是设备莫名其妙不断重启。
- SIM卡座:NB-IoT卡尽量用带锁扣的卡座,接触不良是导致“偶发注册失败”的元凶之一。
- 天线位置:天线不能紧贴MCU晶振和电源走线,否则射频干扰会影响ADC采样和模组接收灵敏度。
我实际调试中还发现,用printf打印浮点数会显著拉长运行时间,建议用整数定点数传输,或者乘10转成整数打印,这样串口传输快很多,也省功耗。
最后说一个个人觉得特别省力的技巧:在做平台侧数据验证时,我写了个简单的PC端串口小工具,模拟MCU给BC26发AT指令,先在PC上把模组和平台链路完全调通,再接MCU。这样把问题隔离成“模组-平台”和“MCU-模组”两段,出问题时不至于两头乱找。整套链路跑通之后,你会发现LWM2M其实并不神秘,它就是一个跑在CoAP上的标准资源模型,真正花时间的,永远是电源、信号和状态机这些底层细节。
本文还有配套的精品资源,点击获取
