
前阵子做了个洗手液余量监测器Hand Sanitizer Monitor核心选了 Nordic Semi 的 BLE SoC。起因很实际公司茶水间的自动洗手液机经常空瓶保洁阿姨一天跑三趟看液位忙起来还是免不了断档。疫情期间大家洗手频率高空瓶一两天没人管体验就很差。于是我在想能不能做一个低功耗、电池供电的小模块贴在洗手液机上实时把余量通过蓝牙发出去让管理员在手机上就能看到“该补液了”。这个项目做完后我觉得它很有代表性结构不复杂但把嵌入式、BLE 协议、低功耗设计、传感器选型串了个遍很适合做 IoT 入门到进阶的练手项目。这篇博文我会把从需求拆解、硬件选型、BLE 服务设计到固件实现、功耗调优、踩坑记录的完整过程都写出来。不管你是刚开始做 BLE 设备的开发者还是想在产品里加低功耗无线监测功能都可以参考这套思路。1. 先想清楚洗手液监测器到底要解决什么问题1.1 场景痛点说白了就是“空瓶没人管”我们先别急着聊芯片和协议做产品第一件事是把痛点看清楚。洗手液监测器的核心使用场景是公共区域比如写字楼卫生间、商场洗手台、医院门诊楼、餐厅门口。这些地方有几个共性人流量大、使用频繁一瓶 500ml 的洗手液可能两三天就见底。清洁人员负责的区域大没法频繁挨个检查每个洗手液机。一旦空瓶使用者的体验立刻变差尤其医疗机构里洗手是刚需。所以这个设备本质上是一个“余量传感器 无线报警器”。它不需要显示什么花哨信息也不需要控制电机之类的执行器核心任务就是定期测一次余量通过 BLE 把数据传出去在余量低于某个阈值时提醒补液。我把这个目标再拆一层它其实可以分解成三个子问题怎么判断余量还剩多少传感器方案。怎么把这个数据用低功耗的方式送出去BLE 通信方案。整机怎么在电池供电下扛几个月甚至一两年功耗管理方案。这三个问题是互相咬合的。传感器决定了功耗底线BLE 广播策略决定了射频功耗而整机功耗又直接决定了电池选型和安装维护成本。所以我建议朋友们做这类项目时先别急着画原理图先把这三个问题在纸上过一遍。1.2 把需求翻译成能落地的技术指标场景需求必须转换成可测量、可验证的工程指标否则后面没法验收。我给自己定了一张需求-指标对照表这也是我做项目时习惯先做的事需求工程指标设计选择能感知余量变化精度 ±10g能区分 50g 以上差值称重传感器 24 位 ADC低功耗、长续航目标电池寿命 12 个月nRF52832 深度休眠 周期唤醒无线监控室内穿墙有效距离 20mBLE 4.2/5.0 广播 GATT 连接免布线、免维护纽扣电池或两节 AAA 供电3V 电池供电整机平均电流 10μA提醒及时余量低于 15% 主动上报BLE 广播携带余量百分比App 推送部署简单不改装原洗手液机外部贴装模块独立供电这里想多说一句精度的问题。大家可能觉得称重上万分之一才叫准但在这个场景里500ml 洗手液瓶空瓶和满瓶的重量差大约 450g500g补液时机只需要判断“还有没有、剩多少”±10g 的精度完全够用了。这样我们的传感器和 ADC 选型就不必往航天级走成本能压下来。技术指标一定是从场景反推出来的不要为了精度而精度。我见过一些朋友做类似项目一上来就上 18 位以上的外部 ADC、精密基准源最后发现电池扛不住体积也做不到这就是典型的过度设计。2. 硬件选型把手头方案一个个过了一遍2.1 为什么锁定了 Nordic 的 BLE SoC硬件选型是这项目里最值得聊的部分。我需要一颗能满足“单芯片搞定 BLE 应用处理 低功耗”的 SoC最好生态成熟、资料全、踩坑少。当时我在 Nordic、TI、Dialog 几个主流方案里比了一圈最后选了 Nordic nRF52832。先说说 SoC 的概念。所谓 SoC就是片上系统把 MCU 核心、射频收发器、Flash、RAM、各类外设集成到一颗芯片里。对 BLE 设备来说SoC 方案最大的价值在于不需要外挂一颗 MCU 再来一颗蓝牙芯片一颗芯片既能跑业务逻辑又能做射频通信BOM 和体积都小很多。nRF52832 的核心参数是Cortex-M4F 内核主频 64MHz512KB Flash、64KB RAM支持 BLE 5.0 的广播扩展。这个配置放在洗手液监测器上绰绰有余固件逻辑再复杂几十倍也够跑。它还有 12 位 SAR ADC用来测电池电压、配合传感器做校准都方便省掉一颗外部 MCU。对比一下当时评估过的几个选择方案优势劣势结论Nordic nRF52832生态成熟、SoftDevice 协议栈稳定、资料多、功耗低价格比入门级略高选定均衡Nordic nRF52840完整 BLE 5.0支持 2M PHY、Coded PHY、USB体积偏大、价格更高后续增强版可以考虑Nordic nRF52810极低成本、资源够基础 BLEFlash/RAM 紧张扩展空间小量产降本时考虑TI CC2640协议栈也很成熟开发工具链相对重、资料分散备选Dialog DA14531功耗标杆、单价低资源极少适合纯透传不适合带传感器处理最终选 nRF52832 还有一个重要原因是开发体验。Nordic 的 nRF5 SDK 和后来的 nRF Connect SDK 都提供了完整的 BLE 服务示例、低功耗例程还有 nRF Connect 这个手机 App 可以直接看广播包、连 GATT 服务做调试。开发期省下来的时间比芯片贵出来那几块钱值多了。2.2 传感器方案对比称重、红外和电容我都评估过传感器是决定整个项目可靠性的关键我最初列了三个方案红外对射、电容式液位检测、称重传感器。我把它们放在一张表里对比方案原理优点缺点适不适合本项目红外对射在瓶身固定高度两侧放红外发射、接收管液位低于该高度光路变化结构简单、成本极低、功耗低只能检测“高于/低于”某点余量只有一个阈值适合极简报警但信息太少电容贴片电极贴在瓶外壁通过电容变化感知液位非接触、无机械结构受瓶身材料、液体介质、温度影响大标定麻烦可以做但稳定性难保证称重传感器应变片电桥称整瓶重量精度高、不受液体性质影响、不接触液体卫生需要机械结构承重、ADC 和功耗需专门设计选定方案我最后选了称重方案理由有两个。第一洗手液本身是凝胶或泡沫介电常数和流动性都不稳定电容和红外方案很容易被液体状态骗到称重是“硬物理量”瓶里有多少东西就是多少克简单直接。第二称重模块可以做成一个托板结构洗手液机直接放上去完全不需要改造原机器部署成本低。称重传感器有个必须注意的点应变片电桥本身是耗电大户。常见的 350Ω 全桥传感器在 5V 供电时静态电流就有 14mA 左右如果不处理光传感器就能把电池几天耗干。所以供电架构上必须把它做进可关断的电源域里这个我后面细说。2.3 供电架构把传感器这个大电流点彻底切掉整机供电采用一颗 CR2032 纽扣电池电压 3V容量约 220mAh。nRF52832 本身在 System ON 模式下的待机电流大约是 1.9μA这非常友好但传感器和 HX711 不能一直挂在电源上。我用一颗 P-MOSFET 做传感器电源开关GPIO 控制栅极只在测量瞬间给传感器和 HX711 供电测完立刻切断。这样整个外设电源域在 99% 的时间里是零电流状态待机功耗才会回到 nRF52832 本身的水平。电路上还有一个细节nRF52832 支持内置 DC-DC 模式需要外接一个 10mH 电感和滤波电容。DC-DC 模式能把峰值电流从 LDO 模式的 5mA 左右降到 4mA 上下虽然看着不多但对电池寿命有累积效应所以这个电路我保留了。参考 Nordic 官方参考设计的器件值即可不用自己调。3. BLE 数据链路广播、GATT 服务和上报策略设计3.1 广播和 GATT 连接两种模式怎么配合BLE 设备有两种工作模式广播Advertising和连接Connection。广播是单向的设备周期性地往空中发数据包任何扫描者都能收到连接是双向的两台设备建立链路后可以双向读写数据加密性更好。洗手液监测器需要同时利用这两种模式。平时设备处于广播状态把余量百分比、状态这些信息放在广播包里这样管理员用手机 App 在附近扫一扫就能看到所有设备的余量不需要逐个连接效率极高。当需要修改阈值、读取更详细数据、或者做固件升级时再通过 GATT 连接建立一条双向通道。这里有个经验不要把广播和连接割裂开设计。nRF5 SDK 里有个经典策略叫“可连接广播 暂停广播”就是在等待连接时广播一旦建立连接就停止广播连接断开后再恢复广播。这样既能让周围人扫到设备又不至于在连接期间浪费射频功耗。3.2 GATT 服务和特征定义的具体设计GATT 是 BLE 上层的数据库结构设备通过 Service服务和 Characteristic特征暴露数据。我把这个项目的 GATT 服务设计如下服务UUID特征属性数据格式自定义余量服务0xF001余量值 LevelRead Notifyuint16单位 g自定义余量服务0xF001低液位阈值 ThresholdRead Writeuint16单位 g自定义余量服务0xF001电池电压 BatteryVoltageRead Notifyuint16单位 mV电池服务 Battery Service0x180F电池电量 Battery LevelRead Notifyuint8单位 %设备信息服务0x180A型号 Model NumberReadstring自定义服务的 UUID 我用了 0xF001/Ox1 之类的短 UUID 做原型但正式产品建议用完整 128 位 UUID避免和别人的设备冲突。这里的核心特征有三个Level实时余量单位克。手机端直接读这个值就能知道剩多少。Threshold低液位阈值可写。管理员可以根据不同容量的洗手液瓶现场调整不用改固件。BatteryVoltage电池电压手机端判断电量状态低于 3.0V 就提醒换电池。连接参数我设置为连接间隔 30ms60msSlave Latency 8Supervision Timeout 4s。连接间隔长短会影响数据传输实时性Slave Latency 则决定了从机可以不监听多少个间隔这两个参数是节能的关键。因为我们的数据量很小根本不需要高频连接所以把 Slave Latency 拉到 8让从机多睡一会儿。3.3 广播数据中的余量信息怎么组织BLE 广播包最多 31 字节非常宝贵所以要精打细算。我的广播数据格式如下Flags 0x06 LE General Discoverable BR/EDR Not Supported Complete Local Name HSM-01 Manufacturer Specific Data Company ID 0x0059Nordic Type 0x01 Payload: 余量百分比(1B) 状态标志(1B) 电池电压(2B)这样一算广播包总共约 21 字节没有超限。余量百分比用一个字节表示 0100%状态标志位可以表示“正常/低液位/空瓶/低电量”电池电压用两个字节表示单位 mV。之所以把电池电压也放广播里是为了让管理员不连接也能在 App 里看到电量不连设备就能快速巡检所有洗手液机。上报策略上设备每 30 分钟测量一次测量后立即更新广播包内容。如果此时已有 GATT 连接就主动通过 Notify 把最新余量推给手机端。这样射频活动非常稀疏功耗自然就下来了。这个“周期性测量 变化才上报”的思路在很多低功耗 IoT 设备里都是通用套路。4. 固件落地与低功耗实测调优4.1 工程结构与主流程梳理固件我基于 nRF5 SDK 17.1.0 和 SoftDevice S132 协议栈来写IDE 用的是 SESSeger Embedded Studio。工程结构按照功能拆成几个模块main.c初始化与主循环。ble_app.c广播、GATT 服务、连接事件处理。sensor_hx711.c称重传感器数据读取与滤波。power_mgr.c传感器电源控制、RTC 周期唤醒、电池电压采集。board_config.h引脚、参数集中定义。主循环的骨架逻辑非常简洁int main(void) { ble_stack_init(); // SoftDevice 初始化 gap_params_init(); // 广播名称、连接参数 services_init(); // 注册自定义 GATT 服务 advertising_init(); // 初始化广播数据 sensor_init(); // 配置传感器引脚和电源控制引脚 rtc_wakeup_init(); // RTC 定时唤醒30 分钟一次 while (1) { sd_app_evt_wait(); // 进入低功耗等待事件唤醒 if (measure_flag) { measurement_t m; m.weight_g sensor_read_avg(32); // 称重32 次平均 m.voltage_mv battery_read_mv(); // 测电池电压 sensor_power_off(); // 立刻切断传感器电源 update_level_characteristic(m.weight_g); update_battery_characteristic(m.voltage_mv); update_adv_data(m); // 更新广播包 measure_flag 0; } } }sd_app_evt_wait()是 SoftDevice 提供的一个 API调用后 MCU 进入低功耗模式直到有事件发生才会唤醒。RTC 定时期满、蓝牙协议栈事件、外部中断都会触发唤醒。这个函数是 nRF52 低功耗应用的灵魂。有个小细节RTC 唤醒中断属于 App 中断优先级在中断回调里不要做耗时处理只设置measure_flag标志位回到主循环后再做完整测量。我当时第一次写的时候在中断里直接调sensor_read_avg()结果不仅测量期间中断频繁嵌套广播时序也被打乱了建议大家不要重蹈覆辙。4.2 用 HX711 读取称重传感器数据HX711 是 24 位高精度 ADC专为桥式传感器设计和称重传感器配合很成熟。它的驱动时序不复杂用 GPIO 模拟即可拉低 PD_SCK等待 DOUT 变低然后连续输出 24 个时钟脉冲在脉冲的下降沿读取 DOUT就能得到 24 位补码数据。读取核心代码大致如下uint32_t hx711_read_raw(void) { uint32_t value 0; // 等待 DOUT 变低表示数据就绪 while (nrf_gpio_pin_read(HX711_DOUT_PIN)) { } for (int i 0; i 24; i) { nrf_gpio_pin_set(HX711_SCK_PIN); nrf_delay_us(1); value (value 1) | (uint32_t)nrf_gpio_pin_read(HX711_DOUT_PIN); nrf_gpio_pin_clear(HX711_SCK_PIN); nrf_delay_us(1); } // 第 25 个脉冲选择通道 A、增益 128 nrf_gpio_pin_set(HX711_SCK_PIN); nrf_delay_us(1); nrf_gpio_pin_clear(HX711_SCK_PIN); return value; }读取的时候要注意一个坑HX711 的数据输出引脚 DOUT 和时钟引脚 SCK 是同步时序和 BLE 协议栈的中断可能产生竞争。如果读取过程被高优先级中断打断时序就乱了。所以我实际读取时用sd_nvic_critical_region_enter()和sd_nvic_critical_region_exit()把这段时序包裹起来防止 SoftDevice 的射频中断干扰。但临界区也不能包太久否则会影响蓝牙协议栈的实时性尤其是连接保持。在广播模式下还好连接模式下就要格外小心我一般把单次读取时间控制在 1ms 以内。称重本身的标定也很重要。我在校准时采用两点法记录空瓶状态下的 ADC 原始值raw_empty。记录满瓶状态下的 ADC 原始值raw_full。实际余量重量 (raw_now - raw_empty) / (raw_full - raw_empty) × 满瓶重量。为了缓冲传感器抖动我会连续读取 32 次去掉最大值和最小值剩下取平均值。实测下来这个方案在静止状态下重量读数稳定在 ±2g 以内完全满足 ±10g 的精度要求。4.3 低功耗参数实测与预算核算低功耗产品的最终指标要回到数字上。我在调优后用电流表实测了各个状态整理成一张功耗预算表状态实测电流持续时间备注System ON 待机RTC 运行约 2.2μA常驻软件定时唤醒传感器 HX711 测量约 12mA150ms含传感器电桥上电稳定时间BLE 广播3 包1s 间隔约 6mA30ms广播包很小很快结束BLE 连接保持约 8mA连接期间App 配置时发生时间短按 30 分钟测量一次计算一天最多 48 次每天的工作时间大约是48 ×150ms 30ms 8.64 秒。其余时间都处于 2.2μA 的待机状态。平均电流估算测量和广播带来的平均电流8.64s / 86400s × 12mA ≈ 1.2μA。待机平均电流约 2.2μA。加上电源管理芯片静态电流、PCB 漏电流整机平均电流大约在 46μA。CR2032 的可用容量按 220mAh 算理论寿命 220mAh / 5μA 44000 小时约等于 5 年。实际要考虑电池自放电、低温环境容量衰减保守估算也有 23 年。这个续航放在公区设备上非常理想基本做到了“装上就不管”。这里给一个调优心得低功耗的战场在待机时不在工作时。很多朋友上来就纠结广播电流是 5mA 还是 6mA但真正毁掉续航的往往是“没睡干净”——某个 GPIO 浮空、某个外设没断电、某个 LDO 静态电流大。我用的是 Nordic 的 Power Profiler Kit IIPPK2查看电流波形一眼就能看出哪里在偷偷漏电。5. 实战排坑这几个问题我折腾了很久5.1 扫码找不到设备项目刚开始调试时我用手机 App 扫描经常半天扫不到设备或者扫到了但广播数据显示不全。排查下来有两个原因一个是广播包超过了 31 字节。BLE 4.x 的广播信道包最大只有 31 字节如果我既放完整设备名又放厂商自定义数据很容易超。后来我把设备名改成短名称“HSM-01”把详细数据留在 GATT 服务里广播包只放最核心的余量百分比和状态问题就解决了。另一个原因是手机蓝牙缓存安卓手机尤其明显旧广播数据会缓存很久。解决方法是关掉蓝牙再开、清掉系统蓝牙缓存或者用 nRF Connect App 的刷新功能。排查这类问题我建议先抓原始广播包别靠手机系统设置里显示的信息判断。nRF Connect 或者安卓的 nRF Blinky 能看到 16 进制原始广播帧哪里多了少了、标志位对不对一目了然。5.2 HX711 数据漂移和电源纹波第一次联调时HX711 的读数在静止状态下还是会跳幅度有 ±10g 左右这个精度不够。我排查了一圈发现根因是传感器供电的电源纹波太大。HX711 对模拟供电很敏感如果供电轨上有开关噪声或者 LDO 纹波叠加ADC 结果会跟着抖。我原来的电路里传感器电源直接挂在 3.3V 数字 LDO 上数字部分一跑噪声就进来了。解决方法是传感器模拟电源加一级 LC 滤波用 10Ω 电阻和 10μF 电容构成 π 型滤波。HX711 的模拟电源和数字电源分离VFB 引脚参考电容靠近芯片放置。软件上做 32 次采样 排序去极值取平均。这两招下去读数稳定到 ±2g 以内余量检测彻底稳了。5.3 休眠电流为什么居高不下有段时间我把整机测量结果一合发现平均电流算出来应该 5μA但实际用万用表量却有 200 多 μA。这个差距太大了明显是哪里没睡干净。用 PPK2 看电流曲线后发现每隔一段时间就有一个小时长的脉冲显然有外设在周期醒来。我排查后找到两个真凶HX711 的 PD_SCK 引脚没有做下拉悬空状态导致 HX711 时不时进入异常状态。称重传感器的电源虽然用 MOSFET 切断了但 MOSFET 的栅极控制脚在休眠时被配置成浮空漏电从引脚流进来。解决方法是所有控制外设电源的 GPIO 在进入休眠前都强制设置为确定电平HX711 的 SCK 引脚在软件里拉低。GPIO 一旦浮空漏电路径就会变得不可控这是低功耗设计里最容易踩的坑。建议大家进入休眠前统一调用一个power_down_gpios()函数把所有 GPIO 都归档到明确的状态。另外nRF52832 有几个 GPIO 是复用为 NFC 功能的如果不用 NFC必须在UICR寄存器里禁用否则引脚会有额外漏电。这个细节在官方文档里写了但很容易被忽略。5.4 电量上报不准电池电压采集一开始我直接用 nRF52832 的内部 ADC 采 VDD结果数值偏低而且不同温度下偏差还不一样。原因是 VDD 本身还在变化电池供电时负载一上来电压就掉直接用 ADC 测 VDD 会受内部逻辑开关噪声影响。后来我改成用内部参考电压来测电池电压同时软件做 16 次采样取中位值再做一个两点校准校准后的电压误差控制在 ±30mV 以内。低电量判断阈值设在 3.0V低于这个值就通过广播状态位提示换电池。注意一点nRF52832 的 ADC 参考电压有一定温漂如果产品工作温度范围大最好在固件里加一条“电压-温度”修正表或者直接留出电压余量避免低温时误报低电。5.5 批量部署时的小工具和一些经验谈设备样机做完后我又写了一个 PC 端的小工具用于批量部署。用 C# 的 WinForms 结合 Windows.Devices.Bluetooth 接口在 Win10 自带蓝牙协议栈上实现扫描、连接、读写阈值特征和余量值。这个东西主要用来在生产时快速标定传感器效果很好。如果只是开发调试其实不用专门写上位机直接用 nRF Connect Desktop 或者 LightBlue 就能搞定我写小工具纯粹是因为生产线上要批量改阈值、批量记录标定数据。做这个项目最大的体会是低功耗 BLE 产品的核心设计不在射频而在“睡眠管理”。你把待机电流压下来整个产品的运维成本就下来了你把广播和 GATT 数据设计得清楚后续 App、网关、大数据平台都好对接。最后分享一个小技巧在做 PCB 时一定预留 PPK2 或万用表电流测试焊盘最好还能串一个 10Ω 采样电阻。调试功耗时不用每次飞线直接夹上 PPK2 就能看完整的电流波形省下的时间足够把整个项目多迭代一轮。