尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32L151+BC20打造NB-IoT环境监测节点:从硬件到OneNET全记录

STM32L151+BC20打造NB-IoT环境监测节点:从硬件到OneNET全记录 做这个项目的起因很简单手里有一批STM32L151RCT6和BC20模组想搭一个真正能丢到野外跑几个月的环境监测节点。DHT11负责温湿度ADC采集一路电池电压和一路外部模拟量BC20同时搞定NB-IoT联网和GPS定位最后把数据送进OneNET物联网开放平台。这套组合听起来不复杂但把低功耗、单总线时序、云平台协议、GPS解析串到一起之后坑其实不少。这篇文章就是完整记录从硬件画图、驱动代码、平台配置到实际调试的全过程适合正在做NB-IoT环境监测、STM32OneNET接入、或者想用BC20做定位上报的开发者参考。1. 项目整体设计与硬件选型思路1.1 为什么选STM32L151RCT6 BC20STM32L151RCT6是ST的低功耗系列芯片Cortex-M3内核主频32MHzFlash有256KBRAM有32KB资源对这个小项目来说非常充裕。更关键的是它带了多路UART、12位ADC、多个定时器而且有完整的低功耗模式Stop模式电流可以做到微安级适合电池供电场景。我用它而不是F103系列主要就是看重低功耗和足够的串口数量一路给DHT11做单总线一路给BC20做AT指令通信还有一路可以留作调试打印。BC20是移远的NB-IoT模组它和普通NB-IoT模块最大的区别是内置了GPS/GNSS定位功能。这意味着一个模块就能解决“上云”和“定位”两件事不需要再外挂GPS模块。NB-IoT本身的优势是覆盖深、功耗低在野外或者地下车库这些Wi-Fi和4G信号不好的地方它依然能维持连接。结合STM32L151的低功耗特性这套系统才有可能真正长时间独立运行。1.2 DHT11在项目里的定位很多工程师一看到DHT11就摇头觉得精度差、稳定性一般。确实DHT11温湿度精度是±2℃和±5%RH跟SHT30这类传感器没法比。但在这个项目里我本来就不需要高精度只需要判断环境温度有没有突变、湿度是否超标DHT11完全够用。它的好处是便宜、数字接口、单总线协议简单驱动代码几十行就能搞定。如果是做产品级计量设备我不会选它但作为数据采集终端的第一版验证它非常合适。1.3 ADC具体采集什么信号ADC采集不是只采一路。这个系统里我规划了两路第一路采集电池电压通过两个电阻分压后接入ADC引脚。因为电池电压可能在3.6V到4.2V之间直接超过STM32的3.3V参考电压所以要分压。第二路预留做外部模拟量输入可以接土壤湿度传感器、光敏电阻、气压计变送器输出等。ADC配置为12位参考电压直接用VDDA采样精度对电池电压监测来说足够。1.4 OneNET平台的定位OneNET是中移动的物联网开放平台选择它有几个原因首先是国内服务器访问稳定其次是NB-IoT设备接入支持好再就是平台自带可视化工具可以快速把设备数据变成仪表盘。标题里强调“最新版本”是因为OneNET先后有过旧版和新版两个体系。新版控制台在产品定义、设备管理、数据流展示上更接近主流物联网平台的“物模型”思路接入方式也更灵活。项目中使用的是新版OneNET的MQTT接入这也是目前官方主推方式。2. 核心硬件连接与DHT11/ADC采集细节2.1 嘉立创画图时的DHT11原理图要点在嘉立创EDA里画DHT11电路元件库里直接搜DHT11能找到传感器件但要注意区分裸传感器和模块。我买的是裸传感器就是四引脚直插封装的那种引脚定义是VCC、DATA、NC、GND。原理图设计时重点不在DHT11本身而在数据线上的上拉电阻。DHT11的数据线是开漏输出必须接一个上拉电阻才能保证逻辑电平稳定。一般选4.7kΩ到10kΩ。我习惯用10kΩ因为STM32L151的IO口驱动能力不强10kΩ已经能保证上升沿足够快同时功耗也更低。如果用的是某宝买的DHT11模块模块上已经集成上拉电阻和滤波电容原理图直接引出三根线就行。电源方面DHT11供电范围是3.3V到5.5V所以我直接接STM32L151的3.3V。要注意的是在电池供电的低功耗系统里DHT11上电后本身会有几毫安的待机电流如果长时间不采集最好通过一个MOS管或三极管开关给它断电。原理图上可以预留一个P-MOS管用STM32的一个GPIO控制VCC。我第一版没加这个导致系统休眠时整体电流多了3mA后续改版才加上。嘉立创画图还有一个经验STM32L151的VDDA和VREF引脚一定不要悬空原理图上要加0.1μF和1μF的滤波电容模拟电源要单独走线。ADC的参考电压如果脏采样值会抖动得很厉害。另外DHT11和ADC走线不要太靠近BC20天线区域NB-IoT发射瞬间电流很大容易干扰模拟部分。2.2 DHT11时序与HAL库驱动编写DHT11的单总线时序看似简单实际调试时最容易翻车。完整通讯过程分三个阶段主机发送起始信号拉低数据线至少18ms然后拉高20~40us。DHT11响应传感器拉低80us再拉高80us。数据位40bit数据每一位开始都是50us低电平然后高电平长度决定是0还是1。高电平26~28us表示070us左右表示1。我最初用HAL_Delay函数做延时发现读出来的数据完全是乱的。原因很简单HAL_Delay基于SysTick最小单位是1ms根本满足不了us级延时的需求。后来我改用DWT计数器来做us延时。下面是完整的HAL库驱动代码适合STM32L151#include dht11.h #include stm32l1xx_hal.h #define DHT11_GPIO_PORT GPIOA #define DHT11_PIN GPIO_PIN_6 #define DHT11_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE() static void DWT_Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } static void DWT_Delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - start) ticks); } static void GPIO_Output_Mode(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DHT11_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStruct); } static void GPIO_Input_Mode(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DHT11_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStruct); } static uint8_t DHT11_ReadByte(void) { uint8_t data 0; for (int i 0; i 8; i) { while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_PIN) GPIO_PIN_RESET); DWT_Delay_us(40); if (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_PIN) GPIO_PIN_SET) { data (data 1) | 0x01; } else { data (data 1); } while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_PIN) GPIO_PIN_SET); } return data; } uint8_t DHT11_Read_TempHumidity(float *temp, float *hum) { uint8_t data[5] {0}; GPIO_Output_Mode(); HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_PIN, GPIO_PIN_RESET); DWT_Delay_us(20000); HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_PIN, GPIO_PIN_SET); DWT_Delay_us(30); GPIO_Input_Mode(); DWT_Delay_us(40); if (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_PIN) GPIO_PIN_RESET) { while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_PIN) GPIO_PIN_RESET); while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_PIN) GPIO_PIN_SET); for (int i 0; i 5; i) { data[i] DHT11_ReadByte(); } GPIO_Output_Mode(); HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_PIN, GPIO_PIN_SET); if ((data[0] data[1] data[2] data[3]) data[4]) { *hum (float)(data[0] data[1] / 10.0f); *temp (float)(data[2] data[3] / 10.0f); return 1; } } return 0; }这段代码有几个容易出错的地方起始信号拉低时间必须在18ms以上最好用20ms。如果时间不够DHT11不会响应。在等待响应和每一位数据时最好加超时保护。否则一旦DHT11损坏或没接好程序会卡死在while循环里整个系统都跑不了。我加超时的方法是检测一个时间戳超过200us就强制退出。每次读取完数据后GPIO要恢复成输出高电平这样保证DHT11不会处于异常状态。2.3 ADC配置与软件滤波ADC部分相对简单但采样结果的稳定性必须处理。STM32L151的ADC是12位我这里配置为单次转换触发方式用软件触发通道选择ADC_CHANNEL_0和ADC_CHANNEL_1。电池电压分压电路我用两个10kΩ电阻电池满电4.2V分压后是2.1V在ADC参考电压3.3V以内电池截止电压3.3V时分压后是1.65VADC能分辨得比较清楚。注意分压电阻值不能太小否则会有持续的漏电流。10kΩ对地有一个支路在低功耗模式下大约消耗0.3mA如果觉得大可以用两个1MΩ电阻但ADC输入阻抗匹配要留意。我这里选10kΩ是为了采样稳定。ADC驱动代码ADC_HandleTypeDef hadc1; void ADC_Init(void) { hadc1.Instance ADC1; hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV1; hadc1.Init.Resolution ADC_RESOLUTION_12B; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.ScanConvMode DISABLE; hadc1.Init.EOCSelection ADC_EOC_SINGLE_CONV; hadc1.Init.LowPowerAutoPowerOff DISABLE; hadc1.Init.LowPowerAutoWait DISABLE; HAL_ADC_Init(hadc1); } uint16_t ADC_ReadChannel(uint32_t channel) { ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel channel; sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_96CYCLES; HAL_ADC_ConfigChannel(hadc1, sConfig); HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, 100) HAL_OK) { return HAL_ADC_GetValue(hadc1); } return 0; }ADC滤波我采用了“中值滑动平均”两层滤波。先连续采11次排序后去掉最小3个和最大3个剩下5个取平均。这样做既能抑制瞬时尖峰又能平滑波动。代码如下uint16_t ADC_Filter(uint32_t channel) { uint16_t buf[11]; for (int i 0; i 11; i) { buf[i] ADC_ReadChannel(channel); } for (int i 0; i 10; i) { for (int j i 1; j 11; j) { if (buf[i] buf[j]) { uint16_t tmp buf[i]; buf[i] buf[j]; buf[j] tmp; } } } uint32_t sum 0; for (int i 3; i 8; i) { sum buf[i]; } return (uint16_t)(sum / 5); }实际测试下来ADC原始值最大跳变在二三十个LSB滤波后基本稳定在正负1个LSB。这个效果足够用了。3. BC20模块接入与GPS定位数据解析3.1 BC20开机、注册网络与SIM卡BC20虽然功能强大但使用起来还是要按AT指令一步步来。上电之前有几个硬件坑先要避开BC20的VBAT推荐电压是3.3V到4.3V不能直接接5V。我用3.7V锂电池供电电压范围内。NB-IoT发射时峰值电流可以达到1.5A到2A所以VBAT引脚旁边必须放一个大电容我放了两个100μF钽电容另外加一个1000μF电解电容是给整个系统用的。如果电容不够模块发射瞬间电压跌落会导致网络注册失败甚至重启。PWRKEY引脚需要拉低至少500ms模块才会开机。STM32用一个GPIO控制这个引脚开机后可以释放。接好硬件后第一步是AT指令握手AT OK然后查询SIM卡状态ATCPIN? CPIN: READY OK如果返回ERROR大概率是SIM卡没插好或者欠费。接下来查信号强度和网络注册ATCSQ CSQ: 18,0 OKCSQ第一个数值越大越好这里18相当于约-91dBm信号还行。再查网络附着ATCEREG? CEREG: 0,1 OK第二个参数为1表示已注册网络如果是0表示还在搜索或者没注册上。我调试时发现BC20在刚上电时要等几秒到十几秒才能完成附着程序里要做等待轮询不能上电后立刻发MQTT。3.2 GPS定位数据读取与解析BC20的GNSS功能用AT指令控制。先开启GPS接收ATQGPS1 OK定位成功后可以主动查询坐标ATQGPSLOC? QGPSLOC: 310324.000,3029.4471,N,11419.0459,E,1,4,1.2,26.2,0.0,0.0,0.0,,, OK这段数据的意思是UTC时间31分24秒注意前面没有小时实际格式里第一个字段是时分秒合并为hhmmss.sss这里是310324.000表示03:10:24纬度30°29.4471′N经度114°19.0459′E定位状态1卫星数4水平精度1.2米海拔26.2米。经纬度格式是“度分”格式需要转成十进制小数才能方便OneNET可视化地图显示。转换公式很简单十进制纬度 度 分 / 60比如3029.4471度是30分是29.4471十进制就是30 29.4471 / 60 30.490785。南纬和西经的同学要注意带上符号但国内场景一般就是N和E没有负号问题。在STM32侧我直接用strstr找到QGPSLOC:后的一整行再通过atoi和atof把逗号分隔的字段解析出来。这些解析函数在标准C库里都有不用自己造轮子。如果长时间定位失败有几个可能天线没接好模块在室内收不到卫星信号或者天线供电电路问题。BC20的GPS天线要用有源天线需要给天线馈电所以原理图上一定要有GPS_LNA供电电路。我第一版直接把天线接到模块的ANT引脚结果死活定位不到查了手册才知道有源天线需要VCC供电。3.3 通过MQTT将数据上报OneNETBC20内部集成了MQTT协议栈我们可以不自己在STM32里实现MQTT报文而是直接通过AT指令操作。主要指令是ATQMTCFGrecv/mode,0,0,1 ATQMTOPEN0,mqtt.heclouds.com,1883 ATQMTCONN0,设备ID,产品ID,鉴权信息 ATQMTPUB0,0,1,0,$sys/{产品ID}/{设备名}/thing/property/post,你的JSON数据需要注意的是OneNET新版平台的MQTT地址和端口由平台控制台动态生成。我一开始按网上旧教程填了老地址结果连不上。登录新版OneNET控制台后在产品“设备接入”页面能看到当前产品对应的MQTT接入地址和端口直接拷贝下来替换到AT指令里。MQTT上报的JSON格式必须和平台定义的物模型一致。比如我的设备属性定义如下属性名标识符数据类型温度temperaturefloat湿度humidityfloat电池电压battery_voltagefloat纬度latitudedouble经度longitudedouble那么实际上报payload就是{ temperature: 25.6, humidity: 68.3, battery_voltage: 3.86, latitude: 30.490785, longitude: 114.318431 }MCU拼接这个字符串需要注意浮点数转字符串的长度控制我直接用sprintf指定保留一位或两位小数避免过长。另外OneNET新版支持一次上报多条属性值也可以带设备时间戳字段具体看控制台提供的模板。我在BC20上电后做了一个简单的状态机初始化串口 - 开机 - 等待SIM卡就绪 - 等待网络注册 - 开启GPS - 连接MQTT - 循环采集和上报。这样即使某个环节失败也能在日志里快速定位。4. OneNET平台创建设备与数据流可视化4.1 新版OneNET创建产品与设备登录OneNET控制台后选择“产品开发”创建产品。产品名称填“环境监测终端”所属行业选“智能农业”或“智慧城市”联网方式选“NB-IoT”协议选“MQTT”。平台会自动生成产品ID。然后添加设备设备名称比如env_node_01。设备级安全建议使用“一机一密”模式平台会为每个设备生成唯一的鉴权信息这个信息要保存好后续BC20连接时会用到。新版OneNET和旧版一个很大区别是引入了物模型。你需要先定义产品的功能模型在“功能定义”页面添加刚才表格里那五个属性。定义好之后平台会给出标准Topic和标准数据格式MCU侧只需要按模板上报即可。4.2 数据流展示与可视化数据上报成功后在控制台的设备详情页就能看到实时数据历史数据会以数据流的形式保存。OneNET新版自带“数据可视化”模块可以直接新建可视化应用。我建的可视化应用包含了四个组件一个实时数值卡片显示最新温度。一个折线图显示电池电压降趋势。一个仪表盘显示湿度百分比。一个地图组件绑定经纬度属性显示设备位置。温度变化直接看折线图最直观。电池电压趋势图可以用来估算剩余电量。地图组件上能看到设备每隔一段时间上报的位置点实际测试时能明显看到轨迹点移动。OneNET的数据流查询接口还可以对接自己的网页或小程序如果后期要做多设备组网在管理后台配置好API权限就行。4.3 数据上报频率与存储周期上报频率不能乱设。NB-IoT的特点是低速率、低频次、低功耗。我测试时把上报周期设置为15分钟一次。如果每秒钟上报一次不仅浪费流量BC20频繁进入发射状态会导致模块发热严重电池也扛不住。OneNET平台侧会保留历史数据一定时间新版标准版默认保留时长大概30天。如果需要长期存储建议定期通过API把数据拉走存到本地数据库。这个项目里我暂时没做但已经在MCU端加上了“本地缓存待补传”的逻辑详情见下一节。5. 常见问题与排查技巧实录5.1 问题速查表下面这些是我在实际调试中踩过的坑整理成表格方便对照现象可能原因解决办法DHT11读回全0或校验失败起始信号时间太短拉低20ms以上DHT11偶尔卡死等待数据位时没有超时给每个while加超时退出ADC值波动大参考电压不稳、采样通道悬空增加滤波电容软件做中值平均ADC读数始终满量程外部信号电压超过VREF加分压电阻或限流电阻BC20 AT没响应电源供电不足、PWRKEY拉低时间不够加电容开机时序放在延时后BC20注册不上网络SIM卡松动、信号差、天线没接好检查CPIN和CSQ接近窗边测试MQTT连接失败平台地址端口填错、鉴权信息不对到OneNET控制台复制当前接入参数上报JSON解析失败payload格式和物模型不一致对照控制台标准模板修改GPS无法定位有源天线供电缺失、室内信号弱给天线馈电把天线放在窗外测试系统休眠电流大DHT11和分压电阻一直供电用MOS管给传感器断电改大分压电阻5.2 避坑心得与低功耗细节这里想分享几条常规文档里不会写出来的经验。第一BC20的MQTT连接成功后不是一直保持的。NB-IoT为了省电会进入PSM状态连接可能被基站断开。所以程序里不能假设连接永远有效每次上报前要检查MQTT连接状态如果断开了就重新连接。第二BC20和GPS同时工作时的功耗很大。如果系统要在野外跑几个月建议把定位频率和上报频率分开。比如每隔1小时才开启一次GPS定位成功后关掉GPS只保留NB-IoT上传。我实际测量过BC20连续GPS定位时电流在几十毫安NB-IoT发射瞬间上百毫安。不做低功耗策略的话两节18650电池也撑不住一周。第三OneNET新版的控制台文档更新比较快不同时期创建的产品Topic格式有可能不同。最稳妥的办法是登录控制台以后找到“设备接入”页面里的Topic模板和示例代码直接复制到项目里不要完全靠搜索引擎找的旧文章。我就是因为照搬了旧教程里的Broker地址和Topic浪费了一整天排查。第四串口调试要留好。BC20的AT日志和STM32上报日志分别放在两个串口。我用的是USART1连接BC20USART2留给Debug。代码里做了简单的printf重定向调试时在串口助手里能看到BC20返回的每一行AT响应。这样排查网络问题时不用在GPIO上猜状态。调试口在量产时一定关闭或降速因为调试打印本身也会耗流。第五在STM32本地加一个环形缓冲区的数据缓存。NB-IoT信号弱的时候一次上报可能失败。我的实现是把最近50条采集到的数据带时间戳存入STM32内部Flash的一个备份扇区每次上报成功后就清空对应区域。网络恢复后再从缓存里补传。这个功能对真实部署特别重要。我在野外测试时遇到过连续4个小时网络异常如果没做缓存这段时间的数据就全部丢失了。缓存区实现不需要复杂的文件系统。直接定义两个数组一个存状态一个存数据按照扇区擦写规则来管理。STM32L151的Flash有专门的数据区注意不要覆盖代码区即可。调用HAL_FLASHEx_Program写入前需要先擦除这个操作会稍微耗时但频率不高对系统影响可以忽略。6. 从第一版到稳定版的调试流程回顾整个项目从画图到云平台跑通前后大概花了两周。第一版硬件画得太随意DHT11数据线上拉电阻只放了1kΩ结果高电平被拉不到3.3V读数据经常失败。后来改成10kΩ问题就消失了。第二版加入了传感器电源开关休眠电流从5mA降到了0.2mA。第三版加了GPS天线馈电电路定位成功率才真正提升上来。软件侧的调试顺序也有讲究。我建议你先用USB转串口单独调BC20把AT指令全部跑通再把STM32的程序接进去。不要一开始就纠结OneNET平台先把BC20用串口助手手动发指令连上MQTT能收到平台数据确认无误后再写MCU侧代码。这样分阶段验证能省掉很多“两头黑”的问题。最后再分享一个小技巧OneNET新版平台的鉴权信息和Topic模板可以在控制台直接下载生成C代码示例虽然不是针对BC20的但数据结构可以拿来作参考。我自己的项目里就把平台生成的示例代码中的JSON组包函数移植到了STM32上稍微改一改浮点格式就行。这套方案稳定跑了一个多月中间经历了小区停电和平台维护数据断点续传功能也验证有效。如果你照着这个思路做遇到问题大概率就是硬件接触、NB信号、平台参数这三个方向逐个排查很快能定位。
返回列表