前阵子做了一个便携式温度监测仪的项目,核心器件用了U-blox的BLE模块,整机用纽扣电池供电,目标是在冷链运输场景下连续工作几个月。做完之后有不少朋友在问选型和实现上的细节,这里把整个项目的设计思路、硬件选型、固件实现和实测踩坑一次性整理出来,给正在做类似低功耗BLE产品的同学一个参考。
先说结论:U-blox BLE模块(我用的NINA-B1系列)在功耗、尺寸和开发效率上的综合表现比较均衡,配合Zephyr SDK做嵌入式开发,不需要额外MCU就能把温度采集、BLE通信、低功耗调度整套跑通。整机休眠电流做到了2.8uA左右,用一颗CR2477纽扣电池,10秒上报一次温度的工况下,理论续航可以到9个月以上。
1. 项目整体设计与方案选型
1.1 需求拆解:温度监测器到底要什么
做任何硬件项目之前,最好先把需求落到具体的量化指标上。这个温度监测器最开始的需求描述很模糊,就是"监测温度、蓝牙上报",但真正进入设计阶段,需要明确的点非常多:
- 测温范围:冷链场景通常要求-20℃到+60℃,但如果你要兼顾体温计等医疗场景,可能还要覆盖更宽范围
- 测温精度:普通环境监测±0.5℃就够,但如果是疫苗、血液制品运输,需要±0.2℃甚至更高
- 上报频率:是每秒都传,还是10秒、1分钟、10分钟采一次?这直接决定功耗量级
- 数据存储:蓝牙连接断开期间要不要本地缓存?缓存多少条?
- 电池寿命:客户要求一次性电池至少撑几个月?还是允许充电?
- 体积约束:纽扣电池还是有电池仓?PCB面积有没有限制?
我们这个项目定位是冷链运输箱内的温度记录贴片,要求是:测温范围-30℃到+70℃,精度±0.3℃(在-10℃到+50℃范围内),默认10秒采集一次温度,BLE广播/连接上报,CR2477纽扣电池供电,目标续航不低于6个月。
这个需求下来之后,方案其实就已经比较清晰了:集成BLE SoC的模块 + 高精度数字温度传感器 + 纽扣电池 + 低功耗固件调度。关键是选对BLE方案。
1.2 为什么选U-blox BLE模块而不是裸片方案
BLE方案的选择其实有几条路线:
第一种是直接用Nordic nRF52832/nRF52840裸芯片自己做射频设计。这种方式BOM成本最低,但射频匹配、天线设计、认证(FCC/CE)都要自己搞定,对于小团队或者快速验证的项目来说,射频短板是最容易卡壳的。
第二种是用国产或者日系的BLE SoC,比如Telink、Apollo等,成本很低,但生态和文档相对弱一些,低功耗调参的经验也不太好找。
第三种就是集成模块方案,比如U-blox、Laird(现在是Ezurio)、Murata、Silicon Labs的模块。模块把晶振、射频匹配、天线、DC-DC全部封装好了,你做PCB的时候只需要保证电源干净、天线区域净空,就能获得稳定的射频性能。蓝牙模块本身也通过了模块级的认证,可以省掉一大笔认证费用和时间。
我们的项目最终选了U-blox NINA-B1系列,主要考量是:
- 基于nRF52832 SoC,BLE 5.0,支持长距离编码模式,而且nRF52系列的生态成熟,低功耗特性经过大量验证
- 模块尺寸小,NINA-B111(内置天线版本)只有10x15x2.0mm,焊盘为LGA封装,适合小型化设计
- U-blox有完整的AT命令固件,同时支持Embedded模式(用SDK开发),开发方式灵活
- 工作电压范围1.7V-3.6V,可以直接吃纽扣电池电压,不需要额外升压
后来我对比过NINA-B3(基于nRF52840)和NORA-B1(nRF52833),B3主要是多出USB和更多Flash/内存,适合复杂应用;NORA-B1支持BLE 5.1,天线版本也全,但价格高一些。我们这个项目只需要连接和广播,nRF52832的性能完全够用,NINA-B1性价比最高。
1.3 模块选型细节:NINA-B1系列各版本怎么挑
NINA-B1系列内部有多个版本,主要区别在于天线和Flash容量。
| 型号 | 天线类型 | Flash/RAM | 适用场景 |
|---|---|---|---|
| NINA-B111 | PCB内置天线 | 512KB/64KB | 空间紧凑,外壳非金属 |
| NINA-B112 | 外置天线引脚 | 512KB/64KB | 需要外接天线,金属外壳 |
| NINA-B121 | PCB内置天线升级版 | 512KB/64KB | 与B111类似,但有所优化 |
如果你的产品外壳是全塑胶或者非金属,强烈建议选内置天线版本,省掉一个天线物料和装配工序。如果外壳有金属结构,或者设备装在金属机箱里,老老实实选外置天线版本,预留IPEX座或天线焊盘,留出调天线匹配的余量。
我们这里选的是NINA-B111,因为温度贴片是塑料外壳、空间极紧凑,内置天线最合适。注意内置天线版本对净空区和外壳材质有要求,具体我放在硬件设计部分详细说。
2. 硬件设计要点与传感器选型
2.1 温度传感器:NTC还是数字I2C?
温度监测器最核心的就是传感器选型。很多人第一反应是用NTC热敏电阻,便宜、简单、任何MCU都能读,但项目精度要求±0.3℃时,NTC方案的校准成本会高到让你怀疑人生。NTC的温度-电阻曲线是非线性的,需要查表或者用Steinhart-Hart方程计算,而且每个传感器个体差异大,批量生产时校准点不能少,否则一致性很难保证。
数字温度传感器是更省心的选择。我对比过几款:
| 传感器 | 接口 | 精度 | 功耗 | 备注 |
|---|---|---|---|---|
| TMP117 | I2C | ±0.1℃(-20~50℃) | 待机0.2uA,转换135uA/15ms | TI医疗级传感器 |
| SHT30 | I2C | ±0.2℃ | 待机0.2uA,转换~1.2mA/1ms | 温湿度一体 |
| BME280 | I2C/SPI | ±0.5℃ | 待机0.1uA | 温湿度气压一体 |
| MCP9808 | I2C | ±0.25℃ | 待机0.1uA,转换200uA | Microchip |
最终选了TMP117。理由很简单:它在-20℃到+50℃范围内能做到±0.1℃的精度,直接满足±0.3℃的需求,还留了余量;单次转换时间15ms,转换结束后自动回到待机模式,待机电流极低;I2C地址可通过引脚配置,如果未来要做多路温度监测也方便。另外一个隐藏优势是,TMP117内部有出厂校准,不需要你批量校准,这对生产端来说省掉了一大笔设备和人工成本。
使用TMP117时有几个细节注意:I2C上拉电阻一般选2.2k-4.7k,供电可以直接接VDD;如果传感器离模块远,上拉电阻要选小一些,但注意上拉电流不要过大;TMP117的地址引脚ADDR有三种状态(低、高、浮动),可以配置出4个地址,方便I2C总线上挂多颗。
2.2 供电方案:一颗纽扣电池怎么喂饱整个系统
温度监测器这种便携设备,供电设计是低功耗的根基。我们用的是CR2477纽扣电池,额定容量约1000mAh,但纽扣电池有一个特性容易被忽略:它的内阻比较大,在脉冲大电流(比如BLE广播时电流峰值十几毫安)情况下,输出电压会明显跌落。如果跌落太多,可能导致模块复位或者射频性能下降。
所以供电设计上要做几件事:
- 电源路径必须是电池 → 去耦电容 → 模块VBAT,去耦电容至少要100uF钽电容或者220uF电解电容并联0.1uF陶瓷电容。这个电容的作用相当于一个能量缓冲池,BLE在广播瞬间需要抽取十几毫安电流时,由电容来放电补充,避免电池电压被瞬间拉垮
- 模块VDD供电引脚(如果模块有内部DCDC,则VBAT直接输入;如果是外部供电架构,需要参考具体手册)要就近放去耦电容
- 传感器供电:TMP117的VDD可以直接连电池正极,因为它的待机电流极低。如果传感器工作电流较大,可以加一个MOS管开关,只在采样期间供电
实测下来,CR2477在初期内阻大约15-30Ω,在BLE广播脉冲(约15mA、持续几毫秒)下,压降大约0.2-0.4V,只要去耦电容足量,系统最低电压仍能保持在2.8V以上,NINA-B1的1.7V最低工作电压余量是足够的。
2.3 射频部分与天线布局:最容易翻车的地方
选了内置天线模块,射频设计会简单很多,但"简单"不等于"随意"。NINA-B111的数据手册里对内置天线的PCB布局有明确要求:天线下方和周围的PCB必须净空(不能铺铜),天线附近不能走地线、信号线和电源线,天线周围包裹的外壳也不能是金属材质,否则天线会被严重失谐,辐射效率掉一半都不奇怪。
我们第一版PCB就吃过这个亏:为了压缩板面积,在天线附近走了一根VCC走线,结果广播距离从设计的30米缩水到10米左右。后来重新改版,天线区域完全净空,周围1cm内不铺铜、不走线、不放置器件,距离立刻恢复正常。
如果产品外壳是金属的,那就必须换外置天线方案,或者设计天线延伸到外壳外部。常见做法是用外置FPC天线贴在塑料外壳内壁上,通过同轴线或弹簧针连接到模块。这种情况下一定要做天线匹配调试,至少要用网络分析仪看S11,频率范围至少要覆盖2.4-2.5GHz。
还有一个容易忽略的点:电池。纽扣电池本身是金属,如果电池正好贴在天线下方或者非常近,也会影响天线性能。设计时尽量让电池远离天线区域,或者在结构上保证两者之间有足够距离。
3. 固件开发与BLE通信实现
3.1 开发模式选择:AT命令还是Zephyr SDK?
U-blox NINA-B1支持两种开发模式:一种是AT命令模式,模块预烧了U-blox的AT固件,你只需要一个外部MCU通过UART发AT命令控制它;另一种是Embedded模式,用nRF Connect SDK(基于Zephyr RTOS)开发自己的固件,直接烧写到模块内部,不需要外部MCU。
对于这个温度监测器项目,我选了Embedded模式,理由很现实:
AT命令模式的最大好处是开发快、不需要了解BLE协议细节,但它需要额外一颗MCU来控制模块,整机功耗和BOM成本都会上升,而且外部MCU和模块之间还需要额外的通信开销。对于温度监测这种功能简单、但对功耗和体积要求高的设备,集成方案是更优解。
Embedded模式下,U-blox提供了基于Zephyr的board definition和示例工程,你可以在SDK里直接选NINA-B111作为target板子,写应用代码。Zephyr自带的BLE协议栈(SoftDevice Controller)非常成熟,低功耗管理、广播、连接、GATT服务这些都有现成API,不需要自己啃协议栈源码。
3.2 广播包与GATT服务设计
BLE通信设计要从产品功能反推。我们的温度监测器有两种工作模式:
- 广播模式:设备以一定间隔发出广播包,手机App可以扫描发现,读取里面的温度数据
- 连接模式:手机或网关主动连接设备,通过GATT服务读取历史温度数据、配置参数
广播包设计相对简单。传统广播包只放设备名称和厂商自定义数据。如果你想在广播包里直接携带温度数据,可以用厂商自定义字段,比如Company ID用0xFFFF(用于测试)或者申请自己的ID,后面的数据自定义格式:
[0x02, 0x01, 0x06] // Flags,表示LE General Discoverable Mode [0x03, 0x03, 0xF0, 0xFF] // Complete List of 16-bit Service UUIDs,或者用128-bit UUID [0x05, 0xFF, 0xAA, 0xAA, 0xYY, 0xYY] // Manufacturer Specific Data,放置温度数值注意广播数据最长只有31字节,你要权衡设备名长度、服务UUID数量和数据负载。如果把温度数据放在广播包里,手机扫描时不连接也能秒读温度,用户体验很好。
GATT服务设计方面,我们定义了一个自定义温度服务:
| 特征 | UUID | 属性 | 长度 | 说明 |
|---|---|---|---|---|
| Temperature Measurement | 自定义128-bit UUID | 读 / Notify | 4字节 | 温度值,IEEE 11073格式或int16,单位0.01℃ |
| Battery Level | 0x180F标准服务 | 读 / Notify | 1字节 | 电池电量百分比 |
| Device Info | 0x180A标准服务 | 读 | 可变 | 设备序列号、固件版本 |
| Sample Interval | 自定义128-bit UUID | 读 / 写 | 1字节 | 采样间隔配置(比如10秒~10分钟) |
温度值的数据格式我用了int16,单位0.01℃,这样-30.00℃到+70.00℃都能表示。Notify属性让设备可以主动推送温度变化给手机,手机不需要一直轮询。
3.3 传感器采集与低功耗调度:关键代码逻辑
在Zephyr里开发NINA-B1的固件,核心逻辑是这样一个无限循环:
- 进入System OFF或者深度睡眠模式(比如nRF52的System ON idle + 定时器唤醒)
- 定时器唤醒后,读取TMP117温度
- 更新GATT特征值,如果当前有BLE连接,直接Notify出去
- 更新广播包中的温度数据
- 重新进入睡眠
代码核心片段大概是这样:
#include <zephyr/kernel.h> #include <zephyr/device.h> #include <zephyr/drivers/sensor.h> #include <zephyr/bluetooth/bluetooth.h> #include <zephyr/bluetooth/gatt.h> static const struct device *tmp117_dev; static int16_t temp_value; static bool connected; /* 温度特征值更新回调 */ static ssize_t read_temp(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset) { return bt_gatt_attr_read(conn, attr, buf, len, offset, &temp_value, sizeof(temp_value)); } /* Notify温度特征 */ static void notify_temp(void) { struct bt_gatt_attr *attr = &my_service_attrs[1]; bt_gatt_notify(NULL, attr, &temp_value, sizeof(temp_value)); } void main(void) { int err; /* 初始化BLE协议栈 */ err = bt_enable(NULL); if (err) { printk("Bluetooth init failed (err %d)\n", err); return; } tmp117_dev = DEVICE_DT_GET(DT_NODELABEL(tmp117)); if (!device_is_ready(tmp117_dev)) { printk("TMP117 not ready\n"); return; } /* 启动广播 */ bt_le_adv_start(...); while (1) { /* 读取温度 */ struct sensor_value temp; sensor_sample_fetch(tmp117_dev); sensor_channel_get(tmp117_dev, SENSOR_CHAN_AMBIENT_TEMP, &temp); temp_value = (int16_t)(temp.val1 * 100 + temp.val2 / 10000); /* 更新广播包数据 */ update_adv_data(temp_value); /* 有连接则主动Notify */ if (connected) { notify_temp(); } /* 进入睡眠,等待下一个采样周期 */ k_sleep(K_SECONDS(sample_interval)); } }这段代码简化掉了错误处理和广播参数配置,但核心逻辑已经清楚了。重点在于:采样周期内的绝大部分时间,设备处于睡眠状态,电耗几乎为零。k_sleep时如果系统配置了PM(电源管理),Zephyr会自动进入System ON idle模式,电流只有微安级别。
3.4 连接参数与功耗的取舍:别让手机把功耗拖垮
BLE设备的功耗大头不是在广播上,而是在连接状态下。连接的功耗由连接间隔、从机延迟(slave latency)和超时时间共同决定。
连接间隔是主设备(手机)和从设备(模块)之间通信的频率。间隔越短,实时性越好,但从设备需要频繁唤醒收包,功耗越高。从机延迟允许从设备跳过若干个连接事件而不需要收包,这可以让设备在数据不变化时一直睡到下一个必须回复的事件。
我们项目的连接参数设置:
| 参数 | 值 | 说明 |
|---|---|---|
| Connection Interval Min | 30ms | 实际值由主设备决定 |
| Connection Interval Max | 50ms | 上限值 |
| Slave Latency | 4 | 可以跳过4个连接事件 |
| Supervision Timeout | 4000ms | 超过该时间没通信则连接断开 |
这样的配置下,设备在连接状态中大部分时间是睡眠的,只有每5个连接事件唤醒一次,功耗接近广播状态。注意,连接参数的最终决定权在主设备(手机),从设备只能通过更新连接参数请求来协商。Android和iOS的处理方式不太一样,Android默认会接受从设备的请求,iOS有时候会更保守,需要从设备在特定时机请求更新连接参数。
如果你做的是纯广播模式产品(不需要连接),那就简单很多,只要把广播间隔调大,功耗就直线下降。间隔100ms时平均电流约20uA,间隔1s时平均电流约5uA左右(具体取决于广播负载)。
4. 实测数据、功耗调优与问题排查
4.1 功耗实测记录与电池寿命计算
理论计算永远替代不了实测。我们用电流分析仪(Keysight N6705B或者开源方案用INA226电流传感器+树莓派)测了整机在各种状态下的电流。
实测平均电流数据:
| 状态 | 电流 | 持续时间 |
|---|---|---|
| 深度睡眠(无广播) | 2.8uA | 常态 |
| 广播(间隔1s,负载24字节) | 28uA | 广播期间 |
| 温度采集(TMP117转换) | 200uA | 15ms |
| BLE连接+Notify(间隔50ms,latency=4) | 35uA | 连接期间 |
基于以上数据,算一个典型场景:10秒采集一次并广播一次温度,不建立连接。平均电流大概是:
平均电流 = 广播平均电流(10秒内有约3次广播,每次约3-5ms,电流约15mA) + 采集平均电流(每次15ms x 200uA,占比很小) + 睡眠电流(占绝大部分时间) 估算公式: 广播每次消耗 = 15mA x 4ms / 1000 ≈ 60uC(微库仑) 10秒内3次广播 = 180uC 采集每次消耗 = 200uA x 15ms ≈ 3uC 10秒内1次采集 = 3uC 睡眠消耗(10秒) = 2.8uA x 10s = 28uC 总消耗(10秒) = 180 + 3 + 28 ≈ 211uC 平均电流 = 211uC / 10s ≈ 21.1uACR2477容量约1000mAh,按90%可用容量,理论续航:
1000mAh x 0.9 = 900mAh = 900000uAh 900000uAh / 21.1uA ≈ 42654小时 ≈ 1776天 ≈ 4.9年这个计算比客户要求的6个月目标高出很多,说明设计余量充足。但要注意,实际续航还会受温度、电池自放电(CR2477年自放电约1%-2%)、天线辐射效率等因素影响。如果你把上报频率提高到1秒一次,平均电流会跑到100uA以上,续航就降到一年以内了。所以在产品设计中,上报频率和电池寿命的权衡很重要,要根据实际业务需求来定,不是越快越好。
4.2 蓝牙扫描不到或者连接不稳:先查这四个地方
项目调试中遇到不少怪问题,这里分享最有价值的几个排查方向:
第一,先确认模块有没有跑起来。看广播有没有发出来,最简单是用手机装一个nRF Connect,扫描看看能不能看到设备。如果连nRF Connect都扫不到,基本是固件启动失败或者射频硬件问题。这时候看串口日志(NINA-B1板子会打印Zephyr的console)是最好的方式,代码里加打印,确认bt_enable和广播启动成功。
第二,供电电压跌落。前面说过,BLE广播瞬间电流很大,如果电池老化或者去耦电容不够,广播瞬间电压跌落会导致射频功放输出电压异常,直接表现是广播距离极短或者时有时无。用示波器抓VBAT波形,如果发现广播瞬间电压跌落超过0.3V,就得加大电容容量。另外注意,万用表量到的静态电压正常不代表动态电压正常,必须用示波器看动态波形。
第三,天线净空区违规。如果你的PCB上元件布局很挤,很容易无意中在天线附近走线、铺铜。哪怕只有一条细走线穿过净空区,也会影响天线效率。我们踩过的坑就是VCC走线。重新布局后信号强度从-80dBm提升到-65dBm(测试距离1米,实测值),效果非常明显。
第四,供电电源噪声。如果用USB供电或者开关电源供电,电源纹波会耦合到射频电路造成信号质量下降。排查方法是换成电池供电对比测试。项目里遇到手机扫描偶尔超时的问题,最后发现是实验室USB供电的纹波太大导致的,换成电池后问题消失。
4.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决建议 |
|---|---|---|---|
| 完全扫描不到设备 | 模块未启动、供电异常 | 看串口日志、查VBAT电压 | 确认固件正常运行,检查电源连接 |
| 广播有但距离极短 | 天线失谐、净空违规 | 检查PCB天线区域、测S11 | 确保净空区合规,重新布局 |
| 连接后频繁断开 | 连接超时参数太短、供电不稳定 | 查看断连原因码 | 调大Supervision Timeout,增强去耦 |
| 温度读数异常跳变 | I2C干扰、传感器供电不稳 | 示波器抓I2C波形 | 加强滤波,检查上拉电阻 |
| 功耗远高于理论值 | 未进入睡眠、外设漏电 | 逐个外设断开测试 | 检查GPIO配置,禁用未用外设 |
| 新固件烧录后无反应 | 烧录配置错误、SDK版本不匹配 | 检查烧录log | 核对U-blox官方SDK文档 |
需要注意的是,BLE相关的"玄学问题"多数都能归结为电源和天线两个物理层面的问题。先排除物理层,再看协议层,否则很容易在代码里白找半天。
5. 写在最后的一点体会
这个项目做下来,我最大的一个感受是:低功耗BLE产品,真正的核心技术不是BLE协议本身,而是系统级的能量预算管理。你能不能让设备在99%的时间里都睡得很深?能不能把每次唤醒的动作压缩到最短?这些决定了产品的续航上限。
U-blox NINA-B1 + Zephyr这套组合,让我在项目开发中少踩了很多射频的坑。模块化方案虽然BOM成本比裸芯片方案高几十块钱,但节约的时间和认证成本是实打实的。如果你也是小团队、项目周期紧、对射频经验不是特别自信,用模块方案绝对比头铁去啃裸芯片要划算。
最后分享一个小技巧:做功耗测试时,不要只看平均电流,要把"每个状态的电流-时间波形"记录下来,分析是哪一段占了主要能耗。有时候你会发现,一个不起眼的GPIO上拉电阻,可能比整个BLE协议栈还耗电。把波形拉出来,问题一目了然。