先澄清一个容易混的点:这里的 BAS 是 Battery Service(电池服务),属于 GATT 体系下的标准服务。它的服务 UUID 是大家熟悉的 0x180F,1.1 版本在老版本只有一个电量百分比特征的基础上,把电池状态、能量、时间、健康、身份信息一整套特征补齐了。
目录
一、先立骨架:一个实例只伺候一块物理电池
二、六种部署形态:变的是物理边界,不变的是实例归属
2.1 形态一与形态二:各自独立的两套 Server
2.2 形态三:分布式设备共享一个 Server
2.3 形态四:可分离 + 分布式,热插拔问题的源头
2.4 形态五:后买的配件,实例按需开张
三、聚合组:多块电池怎么合成用户看到的一格电
四、Low 与 Critical:两级告警语义
五、读规范要先接受的几套规矩
5.1 情态动词就是法律效力
5.2 RFU 与 Prohibited:两种未知值,两种处理
5.3 需求表格与一致性
5.4 字节序:一律小端
六、电池拔插的两条完整消息流
6.1 场景一:拔下旧电池,插入一块不同的新电池
6.2 场景二:拔下的是同一块电池,又插了回来
6.3 三个工程上的坑
七、写在最后
八、测验
做过 BLE 设备的同学大概率都读过电量:搜服务、找 0x2A19、读一个 0 到 100 的字节。但真正做产品时会被一连串问题打脸:TWS 两只耳机在手机上为什么有时显示两个电池、有时三个(还带充电盒)?可拆卸笔记本的底座电池拔了,服务要不要销毁?后买的背夹电池怎么动态冒出来?这篇就围绕这些问题,把 Battery Service 的设计意图、六种典型部署形态、三条术语和热插拔消息流一次性串起来。
一、先立骨架:一个实例只伺候一块物理电池
规范对 Battery Service 的定义只有一句话,但信息密度很高:
The Battery Service exposes the Battery Level and other information for a battery (which may be composed of one or more cells) in the context of the battery's electrical connection to a device.
拆开看有三层意思。
第一,服务的描述对象是 battery,一块电池可以由一个或多个 cell(电芯)组成。也就是说,几节电芯串并联封装成一块电池包,对上只暴露一个服务实例,电芯不是单独建模的。
第二,所有信息都有一个大前提:in the context of the battery's electrical connection to a device,要结合电池与设备的电气连接关系来理解。电量是 0% 还是 100% 只是表层,这块电池此刻在不在位、谁在给谁供电、是充电还是放电。
第三,多电池就多实例,规范用词是 should:
If a device has several batteries to be monitored, then multiple instances of the Battery Service should be declared.
我个人的记忆模型是:把每个 BAS 实例想象成一块电池的专属档案袋,里面既有体检表(电量、电压、温度、循环次数),也有身份证(厂商、型号、序列号)。一块物理电池对应一个档案袋,设备上有几块电池,GATT 数据库里就挂几个档案袋。后面所有的部署图,本质上都是在回答档案袋挂在哪面墙上(哪个 GATT Server)、由谁来更新。
最简单的形态就是一部手机,一块电池,一个 GATT Server,一个服务实例,所有东西层层嵌套:

另外补两条服务声明层面的硬约束(在后面的服务定义章节给出,先记住):该服务没有任何其他 GATT 服务依赖,LE 和 BR/EDR 两种传输都可以跑;一个服务实例内部,每种特征最多只允许出现一个实例,想描述多块电池只能靠多开服务,不能在一个服务里堆两个 Battery Level。
二、六种部署形态:变的是物理边界,不变的是实例归属
规范用六张图把真实产品的形态枚举了一遍。建议按两个维度去读:物理上有几个设备、逻辑上有几个 GATT Server。
2.1 形态一与形态二:各自独立的两套 Server
下面左图是物理上完全分离的两个设备,典型代表是一对各自独立工作的左右耳塞;下图是两个可贴合(attachable)的设备,比如平板和键盘。注意它们的共同点:无论物理上贴不贴合,每个设备都保留自己的电池、自己的 BAS 实例和自己的 GATT Server。

对 Client 侧的含义是:你要分别建立两条 GATT 连接、做两次服务发现,谁也不代理谁。贴合动作不会把两台设备合并成一个 Server。
2.2 形态三:分布式设备共享一个 Server
同样是两只耳塞,换一种产品设计就完全不同:两只物理耳塞对外呈现为同一个 headset(distributed device),电池和实例仍然各有一个,但 GATT Server 只有一个,由其中一只耳塞(或充电盒/主耳)代管。

对比前一种形态可以体会到规范的分寸:Server 的边界不是由物理外壳决定的,而是由谁对外维护 GATT 数据库决定的。Client 只连一次,却能在一个 Server 里发现两个 BAS 实例。图中两只耳塞分别被标成 Aggregation Group 1 和 Group 2,这点先留个印象,第四节展开。
2.3 形态四:可分离 + 分布式,热插拔问题的源头
这是六张图里信息量最大的一张。一台可拆卸笔记本:显示屏部分有一块不可拆卸电池(Battery 1),底座部分塞了两块电池(Battery 2、3)。整机共享一个 GATT Server,但只有显示屏带蓝牙射频,所以底座那两块电池的状态是先通过设备内部链路汇总到显示屏,再由显示屏对外暴露的。

关键设计在这里:底座被拔下来之后,对应 Battery 2、3 的服务和特征并不会从 GATT 数据库里消失,而是继续保留,只是特征值会表明电池已被移除。规范在这里直接点名了:
When the base is detached, the services and characteristics for those batteries remain in the GATT database but their values will show the batteries as being removed. See Appendix A for an example message sequence illustrating the removal and insertion of a battery.
为什么不动态删服务?工程上很好理解:服务句柄是 Client 做发现、订阅、缓存的基础。如果热插拔一次就销毁重建一次句柄,Client 就得反复做服务发现,CCCD 订阅状态也全部丢失。让档案袋留在墙上、只改档案袋里的内容,是代价最小的方案。SPEC给的就是这个方案的标准时序,第六节细讲。
2.4 形态五:后买的配件,实例按需开张
最后一种形态是手机加一块后购买的背夹电池(phone battery sleeve)。手机出厂时只有一个实例;背夹接上后,手机通过一条内部的电池信息链路识别到它,再动态新建一个 BAS 实例代表背夹,图中用绿色虚线框表示这个事后开张的实例。

注意它和形态四的差别:形态四是电池物理移除但实例常驻(电池可拆卸、实例不销毁);形态五是整个配件一开始不存在,实例随配件接入而新增。这也是规范留给实现者的弹性:可拆卸电池走值表达,外挂配件走新增实例。
六张图浓缩成一张表:
|
形态 |
物理设备 |
GATT Server |
BAS 实例 |
典型产品 |
电池不在位时 |
|
基础设备 |
1 |
1 |
1 |
手机 |
不涉及 |
|
物理分离 |
2 |
2 |
2 |
独立双耳塞 |
各管各 |
|
可贴合 |
2 |
2 |
2 |
平板+键盘 |
各管各 |
|
分布式 |
2 |
1 |
2 |
呈现为一个headset的双耳 |
值表达 |
|
可分离分布式 |
多部件 |
1 |
3 |
可拆卸笔记本 |
实例保留,值显示removed |
|
外挂配件 |
2 |
1(代管) |
初始1,接入后2 |
手机+背夹电池 |
配件级新增实例 |
Client 侧的工程结论就一句话:永远不要假设对端只有一个 0x180F,发现服务时要枚举全部实例并按句柄范围分别订阅。
三、聚合组:多块电池怎么合成用户看到的一格电
引入一个概念,Battery Aggregation Group:
A Battery Aggregation Group represents a collection of one or more batteries that provide power to a device concurrently and in parallel.
关键词是 concurrently and in parallel,同一时刻并联供电的一组电池才能进同一个聚合组。它解决的是 UI 呈现问题:系统有三块电池,用户不想看三个百分比,只想看整机还剩多少电。规范给了两个非常具体的聚合算法示例。
能量口径的算法是把同组实例的可用能量求和、可用容量也求和,再相除:
剩余百分比 = Σ Available Energy / Σ Available Battery Capacity
续航时间口径则是取同组各实例 Time until Discharged 字段的最大值,作为整机预估剩余时间——并联供电时系统能撑多久取决于最能扛的那一块,而不是平均值。
用代码表达大概是这样:
/* 同一聚合组内多个 BAS 实例的汇总结果 */
struct bat_inst {
uint16_t avail_energy; /* Available Energy, 单位 mWh */
uint16_t avail_capacity; /* Available Battery Capacity, 单位 mAh */
uint32_t time_to_discharge; /* Time until Discharged, 单位 s */
};
uint8_t agg_group_percent(const struct bat_inst *g, size_t n)
{
uint32_t sum_e = 0, sum_c = 0;
for (size_t i = 0; i < n; i++) {
sum_e += g[i].avail_energy;
sum_c += g[i].avail_capacity;
}
if (sum_c == 0)
return 0;
/* 先乘 100 再除,避免整数截断把结果吃成 0 */
return (uint8_t)((sum_e * 100u) / sum_c);
}
uint32_t agg_group_remaining_seconds(const struct bat_inst *g, size_t n)
{
uint32_t max_t = 0;
for (size_t i = 0; i < n; i++)
if (g[i].time_to_discharge > max_t)
max_t = g[i].time_to_discharge;
return max_t;
}
回看形态四那张图,底座的两块电池同属 Aggregation Group 2,因为它们同时给底座供电;显示屏的电池单独占 Group 1。两只耳塞虽然共享 Server,却分到两个组——它们不并联,各供各的。聚合关系本身会通过 Battery Information 特征里的 Battery Aggregation Group 字段告诉 Client,UI 怎么分组、要不要加总,由上层应用自己决定,规范只提供素材。
四、Low 与 Critical:两级告警语义
规范对两个词给了严格定义,做低电策略时不能混:
Low: the device will still function normally, but immediate recharge or replacement of the battery is recommended.
Critical: a load powered by the battery might not function fully or correctly.
打个比方:Low 相当于油表亮灯,车还能正常开,但规范建议你立刻去加油或换电池;Critical 则是发动机已经开始喘振,由电池供电的负载可能无法完整或正确地工作,该保存数据、降频、通知用户了。一个是功能完好的提醒态,一个是功能即将受损的失效预警,两者在 Battery Critical Status、Battery Level Status 等特征里都有对应比特位,SPEC 1.1 版本正是为了把这种语义标准化,而不是让每家厂商自定义一个低电阈值。
五、读规范要先接受的几套规矩
5.1 情态动词就是法律效力
蓝牙规范用 shall、must、will、should、may、can 这几个词表达不同强度的要求,读的时候必须逐字较真:
|
用词 |
含义 |
落地理解 |
|
shall |
is required to |
强制要求,不合规就是不合格 |
|
must |
强制要求的必然推论,或不争的事实 |
没有商量余地 |
|
will |
it is true that |
事实陈述 |
|
should |
is recommended that |
推荐,不做要有理由 |
|
may |
is permitted to |
允许的可选项 |
|
can |
is able to |
能力/因果描述 |
前面那句多电池 should 多实例就是推荐而非强制;而字节序、服务 UUID 用的是 shall,没有例外。另外 note 只是提示,不能夹带新要求,当 note 的澄清和正文要求冲突时,以要求为准。规范还明确:图和正文不一致时,文字优先,图只是 visual aid,图里画的某一种实现方式不构成对实现的限制。
5.2 RFU 与 Prohibited:两种未知值,两种处理
预留字段 RFU(Reserved for Future Use,这个缩写和全称等价)的处理分三种情形,非常容易在面试里被绕:
1. 结构体里的 RFU 字段:发送方填 0;接收方必须忽略它,尤其不能因为它非零就拒绝整个结构。
2. 取值范围里被划为 RFU 的枚举值:发送方不得使用;接收方收到应当连同所在数据结构一起拒绝,除非规范明确说该场景下要忽略无法识别的值。
3. RFU 比特位:未分配比特发 0;收到为 1 的消息时按 0 继续处理(另有规定除外)。
Prohibited 只用于枚举型字段里那些未分配的值,原文要求三连:
These values shall never be used by an implementation, and any message received that includes a Prohibited value shall be ignored and shall not be processed and shall not be responded to.
收到带 Prohibited 值的消息:忽略、不处理、不回复,注意是不响应,连错误应答都不要回。而且 Prohibited 这个词从来不缩写。一句话区分:RFU 比特收到当 0、协议尽量向前兼容;Prohibited 值收到当垃圾、整包静默丢弃。
5.3 需求表格与一致性
后面所有特性表格用 M(强制)、O(可选)、X(排除)、N/A(不适用)、C.n(条件强制)标注,条件语句紧跟在表格下方。一致性原则叫 process-mandatory:只要你宣称符合本规范,所有标 M 的能力必须按规定方式支持;你自己选了要支持的 O 和 C 项,同样必须完整做到。不能只实现一半然后宣称支持。
5.4 字节序:一律小端
All characteristics used with this service shall be transmitted with the least significant octet first (i.e., little endian).
所有特征值一律最低有效字节在前。BAS 1.1 新增的字段大量是 uint16、uint24(电压、能量、容量、时间、温度),解析时不能想当然按网络字节序处理。一段典型的解析代码:
/* 特征值缓冲区中按小端取出多字节整数 */
static uint16_t rd_u16_le(const uint8_t *p)
{
return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
}
static uint32_t rd_u24_le(const uint8_t *p)
{
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16);
}
/* 例:解析 Battery Energy Status 中的电压与可用能量(字段前还有 flags) */
void parse_energy_status(const uint8_t *v)
{
uint16_t present_voltage_mv = rd_u16_le(v + 2); /* Present Voltage, 分辨率 1mV */
uint16_t avail_energy_mwh = rd_u16_le(v + 5); /* Available Energy, 分辨率 1mWh */
/* … */
(void)present_voltage_mv; (void)avail_energy_mwh;
}
六、电池拔插的两条完整消息流
形态四埋的钩子在这里收。规范用两张消息序列图(MSC)回答同一个问题:电池被物理拔出、再插入,空口到底发生了什么。前提设计是特征常驻 GATT 数据库,靠特征值本身表达电池在不在位:
…the characteristics remain in the GATT database but their values tell if the battery is present or not.
6.1 场景一:拔下旧电池,插入一块不同的新电池

逐步走一遍:
-
步骤 1 到 3,Client 分别给 Battery Information、Serial Number String、Battery Energy Status 三个特征的 CCCD(Client Characteristic Configuration Descriptor)写值,使能 indication。这是订阅动作,本质是对 CCCD 句柄写入 0x0002(小端两个字节):
/* ATT Write Request: 使能某个特征的 Indication */
uint8_t att_op = 0x12; /* Write Request */
uint16_t cccd_handle = 0x0142;/* 目标特征 CCCD 的句柄,按实际发现结果填入 */
uint8_t value[2] = { 0x02, 0x00 }; /* 0x0002 = Indication,小端 */
/* notification 对应 0x0001;indication 带确认,适合电池这种关键状态 */
-
步骤 4 是 Server 本地事件:电池被拔出,没有空口消息。
-
步骤 5,Server 主动上报 Battery Energy Status 的变化,其中 Battery Present 位为 No,宣告电池不在位。
– 步骤 6,本地事件:一块新的电池插入。
-
步骤 7、8,因为换了电池,身份证信息全部翻新,Battery Information 和 Serial Number String 各自上报一次变化。
-
步骤 9,再次上报 Battery Energy Status,Battery Present 位变回 Yes。
6.2 场景二:拔下的是同一块电池,又插了回来

时序几乎一样,唯一区别在步骤 7、8 被 alt / optional 组合框包住:插回来的还是同一块电池,身份信息没变,厂商、型号、序列号都一样,这两条 indication 就可以不发;但步骤 9 不能省,在位状态从 No 翻回 Yes 必须通知 Client。
这两张图放在一起,规范的意图非常清楚:上报内容按状态差异裁剪,身份没变就不重复报身份证,在位状态永远照报。
6.3 三个工程上的坑
第一,indication 是有确认的(Server 发完要等 Client 的 Confirmation Handle Value Indication),和不需要确认的 notification 不同。电池在位状态属于关键事件,用 indication 可以保证 Client 不丢事件;但如果固件里没处理好确认超时重传,连续快速拔插可能把指示队列堵死。
第二,热插拔期间不要销毁服务、不要改句柄。Client 不需要重新做服务发现,等指示即可;如果实现成动态删服务,不仅违背附录的示例模型,还会让订阅过的 CCCD 全部失效。
第三,电池不在位时,除在位标志外的其他字段是否可读、读出什么值,要以各特征自身行为定义为准,Client 端不要把 0 电量直接显示成空电——先判 Battery Present,再看数值,否则拔出电池的瞬间 UI 可能闪出一格 0%。
七、写在最后
用一条主线把全文串起来:一块物理电池对应一个 BAS 实例(电芯不单独建模),实例挂在哪个 GATT Server 取决于设备形态,电池物理不在位时实例常驻、用值表达,身份变化才触发身份类特征上报。再配上小端字节序、RFU/Prohibited 的处理纪律和聚合组的两条求和/取最大值算法,后面十二个具体特征的行为定义读起来就只是查字典了。
八、测验
Q1:一台 BLE 设备上最多/最少有几个 Battery Service?TWS 耳机在手机上可能看到几个电池服务?
参考答案:
没有数量上限的强制规定,一块需要监控的电池对应一个实例,多电池应声明多个实例(规范用词 should),单电池设备就是一个。TWS 耳机取决于产品形态:如果左右耳各自是独立 GATT Server(各自独立连接),手机通过两条链路各看到一个;如果双耳对外呈现为一个分布式设备、共享一个 GATT Server,则在一条链路的同一个 Server 下发现两个实例;若充电盒也纳入并由同一 Server 代管,还可能出现三个实例(左、右、盒)。每台 Server 内部每种特征只允许一个实例,扩展靠多开服务而不是重复特征。
Q2:可拆卸设备的电池被拔出时,GATT 数据库中的 Battery Service 及其特征是否应当删除?Client 如何感知拔插?
参考答案:
不删除。服务和特征常驻 GATT 数据库,由特征值表达电池是否在位。Client 事先通过写 CCCD 使能 Battery Energy Status 等特征的 indication;电池拔出后 Server 指示 Battery Energy Status 变化(Battery Present: No);插入新电池时依次指示 Battery Information、Serial Number String(身份更新)和 Battery Energy Status(Present: Yes);如果插回的是同一块电池,前两条身份类指示为可选,在位状态指示必发。Client 全程无需重新发现服务。
Q3:收到字段中 RFU 比特为 1、或枚举字段取到 Prohibited 值,分别怎么处理?BAS 特征的多字节字段按什么字节序传输?
参考答案:
RFU 发送方必须置 0;接收方对 RFU 字段应忽略、不得因此拒绝结构,对 RFU 比特按 0 继续解析;若收到的是取值范围内被划为 RFU 的枚举值,则应将该对象及所在数据结构判错拒绝(规范另有忽略规定的除外)。Prohibited 值任何实现不得发送,收到后整条消息忽略、不处理、不回复,且 Prohibited 无缩写形式。BAS 所有特征值一律小端传输(least significant octet first),多字节字段按小端解析。
网硕互联帮助中心







评论前必须登录!
注册