朋友最近做一款量产前的定制耳机,客户要求开机的蓝牙搜索名改成品牌名加型号,他在工程里搜了大半天“name”,改完字符串烧进去,手机一搜还是老名字。折腾一圈发现只改了经典蓝牙的名,BLE广播名没动,两个地方各管各的,等于白改。这个坑做蓝牙方案开发的应该都遇到过。
这篇就把“蓝牙名修改”这件事完整捋一遍:名字到底存在协议栈的哪几层、在国产蓝牙SoC方案的SDK里一般从哪个入口改、静态改和动态改分别怎么做、改完之后怎么验证,以及最容易踩的哪些坑。适合刚接触方案开发的工程师,也适合从单片机转过来、头一次碰蓝牙协议栈的朋友。
1. 先搞清楚蓝牙名在系统里不是“一个字符串”而是好几处数据
很多刚接触蓝牙开发的人以为,改设备名就是改一个全局字符串。实际拆开看,蓝牙设备在手机上的显示名至少可能来自两个完全不同的数据通道,而且这两条通道还都是独立维护的。
1.1 经典蓝牙和BLE:两套广播机制,名字各给各的
经典蓝牙(BR/EDR)在传统查询扫描阶段依靠EIR(Extended Inquiry Response)数据携带设备名,这段数据里同时可能存在“完整本地名”和“缩短本地名”。BLE这边则是通过广播通道的AD Type字段完成名字广播,比较常见的是0x09 Complete Local Name和0x08 Shortened Local Name,扫描响应包(Scan Response Data)里同样可以带名字。
问题就出在这:很多芯片是双模的,经典蓝牙名字和BLE名字在SDK里往往是两个变量、两个初始化函数,有的甚至由不同模块分别管理。你改了经典蓝牙的名字,BLE广播名没动,手机用BLE扫描时看到的还是旧名字;反过来也一样。前面说的定制耳机,改的就是这种“改了一半”的状态。
所以改名字之前必须先确认你的工程运行在哪种组合下:单模BLE、单模经典蓝牙,还是双模共存。这个决定你后面要动几处地方。
1.2 搜索时的显示名和连接后的设备名不是一回事
除了广播层,蓝牙规范里还有一种设备名保存在GATT服务的Device Name特征值(UUID是0x2A00)里。手机在扫描阶段显示的名字,主要来自广播包和扫描响应包;但连接建立之后,App也可以主动去读0x2A00这个特征值,把读到的内容作为设备名显示。
这意味着一个很容易被忽略的场景:你改了广播名,手机重新搜索能看到新名字;但如果某个App已经连接过这台设备并且缓存了0x2A00的旧值,进入连接状态后它仍然可能显示旧名字,除非你让GATT里的那个特征值也同步更新。
从开发视角看,一次完整的“改名”至少应该覆盖三个点:广播数据里的名字、扫描响应数据里的名字、GATT服务里0x2A00的值。具体到国产方案的SDK,有的把后两个绑定在一起,有的完全分开,要花点时间确认。
2. 在国产蓝牙SoC方案的SDK里,名字通常写在什么地方
这套方案的工程结构我很熟悉,几个系列下来,名字的入口基本可以归纳为三类:配置头文件里的宏定义、PC配置工具生成的参数Bin、运行时的动态改名额接口。实际用哪个,取决于你是开发调试阶段改,还是量产阶段让产线改,或者产品本身支持用户App自定义昵称。
2.1 第一步搜索:把配置头文件里的宏定义翻出来
拿到一个工程,最直接的线索是全局搜索这些关键词:BT_NAME、name、local_name、dev_name、BLE_NAME。通常名字定义会集中在某个配置头文件里,比如工程里的board_config.h或者app_config.h,典型写法类似这样:
#define PRODUCT_NAME_DEFAULT "STORY_01" // 广播初始化时引用 uint8_t *adv_name = (uint8_t *)PRODUCT_NAME_DEFAULT;有些SDK的宏不只是字符串本身,还会拼接一个长度,比如包装成BT_NAME_STR和BT_NAME_LEN两个宏。你在代码搜索时,如果只找字符串内容可能会漏掉长度宏,导致名字内容改了但长度没同步,广播包里的Name字段就是截断状态,手机上只显示前几个字符。
这里有个惯性操作要留意:很多工程师图省事,直接在宏定义里把名字换成中文,比如#define PRODUCT_NAME_DEFAULT "故事机"。如果SDK内部默认走UTF-8编码,一般来说没问题;但有些老旧代码库内部默认GBK或者ASCII,中文进去就是乱码。这个问题后面专门说。
2.2 量产不想改代码:配置工具和参数区怎么把名字写进去
到了量产阶段,这类方案通常不建议为不同客户各编一版固件,因为固件版本管理会爆炸。更通用的做法是,名字放在Flash的某个参数分区里,量产时通过PC端配置工具把名字、MAC地址、产品序列号等写进去。
配置工具生成的通常是一个包含固定偏移地址的Bin文件,烧录器会按照偏移写到Flash。芯片上电时,协议栈初始化流程里会先去这个参数区读取配置项,读到了就用参数区里的名字,没读到就走代码里的默认宏。
如果你的产品还要支持用户App自己改名字,那就要设计一个三层优先级:App写入的用户自定义名最优先,其次是从量产配置参数区读到的产品名,最后才是代码里的默认宏。实际工程里这个逻辑并不复杂,但对NV区读写的时序有要求:协议栈广播启动通常在系统初始化早期,如果你的NV读取函数依赖某些外设先初始化完,就可能出现“开机先广播默认名、过一会儿才变成自定义名”的怪现象。
2.3 静态宏、配置工具、动态API三条路线的选型建议
我整理了一个选型参考表,方便快速判断自己在哪个阶段该动哪条路线:
| 路线 | 适用阶段 | 优点 | 典型问题 |
|---|---|---|---|
| 代码宏定义 | 开发调试、固定产品名 | 最直接,改完编译即可 | 每个型号都得编一版固件,版本容易乱 |
| 配置工具写参数区 | 量产、多客户多品牌 | 同一份固件适配多个品牌名 | 需要管理参数Bin与固件的配对关系 |
| 动态API | 用户App可修改昵称 | 体验好,支持运行时改名 | 要处理NV存储、广播刷新、长度边界 |
| 混合使用 | 量产且支持自定义 | 兼顾出厂名和用户设置 | 读优先级与初始化时序要设计好 |
开发调试阶段最简单的方法就是改动宏定义,跑通基础验证;但产品如果要走产线,建议趁早切换到参数区方案,哪怕前期多花一点时间搭配置管理的流程,也比后面每个客户单独烧一版固件舒服得多。
3. 实战一个动态改名Demo:App自定义昵称并能重启保持
动态改名是蓝牙名修改里最有技术含量的一条路,这里用一个具体案例拆解:某儿童故事机项目,配套App里允许用户修改设备在手机上的显示名,修改后设备重启仍然保持新名字。
3.1 需求拆解:三个绕不开的问题
这类需求听起来简单,拆开其实有三个独立问题:
- 新名字写到哪:Flash里有可用的NV分区吗?读写接口符不符合这个小数据量的场景?
- 怎么让协议栈立刻生效:名字改完不是等重启,而是要求几秒内在手机上搜索就能看到新名字。
- 重启后怎么恢复:代码里默认宏还在,怎么让协议栈优先读NV里保存的用户名,而不是默认名。
这三个问题有一个没想清楚,就会做出“改名重启后丢设置”或者“重启后名字没变但广播乱码”的半成品。
分区规划上,我习惯把用户名字段放在一个独立地址,用简单的TLV结构存,而不是直接裸存字符串。原因是裸存字符串一旦长度变化,后面字段的偏移就要跟着动,不利于以后扩展。示意结构可以这样:
typedef struct { uint16_t magic; // 校验用魔数,防止读到无效数据 uint16_t len; uint8_t name[32]; } user_name_t;写NV前先读一次老数据,确认法magic没被破坏再决定是覆盖还是重建;读NV时如果magic不对,就走默认名。这套逻辑能挡住绝大多数因为Flash空数据、掉电写一半引起的异常。
3.2 关键流程:从App下发新名字到广播数据刷新
App把新名字通过自定义服务特征值写下来,设备端处理函数做三件事:先把数据拷贝到内存缓冲,再写入NV分区,最后调用协议栈的改名接口。伪代码结构大概是这样:
void on_user_name_write(uint8_t *buf, uint16_t len) { // 1. 合法性校验:长度不超过上限,不允许空名 if (len > NAME_MAX_LEN || len == 0) { return; } // 2. 写入NV区,带magic和长度 user_name_t user = {.magic = USER_NV_MAGIC, .len = len}; memcpy(user.name, buf, len); nvm_erase(USER_NAME_SECTOR); nvm_write(USER_NAME_ADDR, &user, sizeof(user)); // 3. 更新协议栈广播名 protocol_stack_set_name(buf, len); ble_adv_data_refresh(); }ble_adv_data_refresh()这一步很关键。有的SDK改名接口内部会重新构造广播包,但有的不会,你改了名字字符串不重新触发广播更新,手机上看到的还是旧名字。如果SDK没有提供专门的刷新接口,常见做法是用le_set_scan_rsp_data之类的接口把扫描响应数据重新设置一遍,同样能达到广播刷新的目的。
开机电初始化阶段,要把NV读取放在广播启动之前或紧跟其后:
void app_bt_init(void) { user_name_t user; if (nvm_read(USER_NAME_ADDR, &user, sizeof(user)) == OK && user.magic == USER_NV_MAGIC) { protocol_stack_set_name(user.name, user.len); } else { protocol_stack_set_name(PRODUCT_NAME_DEFAULT, strlen(PRODUCT_NAME_DEFAULT)); } ble_start_advertising(); }看起来简单,实际项目里很容易出现广播已经启动、名字才设置成功的情况。做法就是调整初始化顺序,让名字设置接口必须在ble_start_advertising()之前完成,否则丢广播包里的Name字段就成了必然。
3.3 调试验证:别拿系统蓝牙列表当唯一标准
动态改名之后怎么确认真的生效?我的标准流程是这样:
- 手机系统蓝牙设置里选中旧设备,点“忽略此设备”,把缓存清掉;
- 打开系统蓝牙搜索,看列表里显示的新名字;
- 用第三方BLE调试工具扫描,查看广播包的
Complete Local Name字段是不是新名字; - 连接设备后,读取GATT里
0x2A00特征值,确认连接后的设备名同步更新; - 给设备断电重启,再次扫描,确认NV里保存的新名字成功恢复。
第4步经常被漏掉。广播名和GATT设备名如果不同步,很多App连上后会立刻把GATT里读到的名字显示出来,造成“明明广播名是新名字,App里还是旧名字”的错觉。遇到这类现象,不要再怀疑广播包,直接去查0x2A00特征值的返回路径。
4. 改名这件事常见的几个坑和排错经验
动态改名和静态改名都踩过一些坑,这里按问题现象整理,每个都给出定位路径,方便你们遇到同类问题直接套用。
4.1 手机端显示旧名字,多半是缓存而不是固件问题
改完名字搜不到新名字,第一反应先清缓存。iOS和Android的蓝牙缓存机制不同,Android部分机型对已配对或曾经配对过的设备缓存很顽固,即使固件广播名已经改变,系统列表里仍显示旧名。操作上把设备从“已配对的设备”里移除,再重新扫描,基本能解决。
调试早期我经常花半天查代码,最后发现手机缓存没清,空耗时间。建议定一个规矩:每次烧录新固件后,第一件事就是忽略旧设备再扫描,别在旧缓存上猜问题。
4.2 中文名乱码,先查编码假设
蓝牙广播里的名字本质是一串字节,手机显示中文时默认按UTF-8解码。如果SDK内部封装时把UTF-8字符串又做了一次编码转换,或者工程源文件本身保存成了GBK,中文字节在广播包里就变成了无效UTF-8序列,手机上显示乱码。
排查方式是抓一段广播数据,把Name字段的十六进制拷贝出来,丢到在线UTF-8解码器里看能不能还原成中文。还原不了就说明编码链路上出了问题。实际项目中,我通常要求中文产品名先转成UTF-8字节数组再塞给协议栈,避免依赖编译器对源文件编码的隐式处理。
4.3 名字长度被截断,跟广播包容量有关
BLE传统广播模式下单个广播包的数据区最多31字节,里面装完UUID、厂商自定义数据之后,留给Name字段的空间并不多。如果完整本地名太长,要么用缩短本地名(Shortened Local Name)截断显示,要么把名字挪到扫描响应包里,扫描时手机通常会把广播包和扫描响应包拼起来看。
还有一个容易翻车的地方:缩写名在广播包里要带长度字节和类型字节,比如0x09后面才是名字内容,很多工程师手工拼包时把总长度算错一个字节,导致整个广播包被解析器丢弃,设备直接搜不到。我建议能用SDK自带接口设置名字就绝不手拼广播包。
4.4 双耳的从机名字不要单独乱改
TWS耳机这类主从结构产品,手机看到的永远是作为主机的那个耳机的广播名。从机在回连之前一般不广播独立名字,或者广播但被主机私有协议压制。如果贸然在从机固件里改了一个不同的名字,可能出现两只耳机在手机端显示两个不同设备名的现象。
配对状态下的名字同步逻辑,通常由主机通过私有GATT通道把名字数据同步给从机,两边共用一套名字存储。遇到类似项目,我建议把名字管理统一放在主机侧,从机只做数据接收,不要各自维护一套。
4.5 固件升级后名字重置成默认值,先查NV分区是否被覆盖
“用户改了昵称,升级固件之后恢复出厂名”是动态改名方案最常见的投诉。多数情况下不是代码逻辑丢数据,而是升级流程把参数区当作固件区重新擦写了。做固件升级兼容时,要确认升级脚本是否保留参数区,或者升级BootLoader是否重新分区了Flash布局。
如果升级前后Flash地址规划不一致,旧参数区的数据在新固件看来就是无效数据,magic校验不过,回到默认名是完全合理的表现。解决思路是升级兼容模块里做一次参数迁移,或者干脆把用户名备份到独立分区,升级不动那个分区。
这几个月做下来,我个人体会是,蓝牙名修改的难点从来不在“找到那个字符串”本身,而是在搞清楚这个名字要经过几层数据通道才能到达手机屏幕。改代码五分钟,排查“为什么没生效”可能五个小时。最后分享一个小技巧:调试时别只盯系统设置里的设备列表,准备一台装了BLE调试工具的安卓手机,直接看广播包原始数据,能帮你把缓存问题和固件问题彻底分开,效率会高很多。