1. 从一个被忽略的细节说起:为什么扫描响应包值得单独写一篇
做低功耗蓝牙开发的朋友,大概率都经历过这样一个阶段:设备能广播了,手机也能搜到,连接也正常,于是觉得"蓝牙这块搞定了"。但真正把产品推到测试环节,问题就来了——扫描列表里那串英文名字看着别扭,客户问能不能显示中文,你打开代码一看,广播包里的设备名是写死的英文,想改成中文,编译烧录,手机一扫,乱码。
这不是个例。我在接触TLSR_8258这颗芯片的早期,就卡在这个看似不起眼的地方。广播包(Advertising Packet)和扫描响应包(Scan Response Packet)是两回事,很多人第一次做蓝牙中文名,改的是广播包,结果发现要么长度不够放不下,要么显示出来是问号。扫描响应包才是那个"额外空间",它允许设备在被主动扫描时,再补发最多31字节的数据,设备名、厂商信息、自定义服务提示,都可以塞在这里。
这篇内容就是围绕TLSR_8258 平台上蓝牙中文扫描响应包的设置方法展开的。我会把广播包和扫描响应包的关系讲清楚,把中文编码在蓝牙协议里的处理逻辑拆开,然后给出在 TLSR_8258 SDK 上可复现的配置步骤,最后聊聊实测中容易踩的坑。适合正在用泰凌微这套 SDK 做蓝牙产品、又想把设备名做成中文的开发者,也适合刚接触 BLE 协议、想弄明白"扫描响应"到底怎么回事的新手。
需要提前说明的是,蓝牙规范里对设备名的编码并没有强制要求必须是 ASCII,但实际显示效果取决于手机端解析方式。中文能不能正常显示,核心在于编码格式和字节长度这两件事,而不是芯片本身不支持。下面我们一层层拆。
2. 广播包与扫描响应包:先把这俩的关系理清楚
2.1 广播包装不下的东西,扫描响应包来兜底
BLE 设备在广播状态下,会周期性地在三个广播信道(37、38、39)上发送广播包。一个广播包的有效载荷(Payload)最多 31 字节,这 31 字节里还要扣除设备地址、Flags 等固定字段,真正留给用户数据的空间非常有限。设备名如果比较长,比如"智能温湿度传感器-A01",光名字就占掉十几个字节,再加上其他服务 UUID,根本放不下。
扫描响应包就是为解决这个问题设计的。当手机(Central)收到广播包后,如果它处于主动扫描模式,会向设备发送一个 Scan Request,设备收到后回复一个 Scan Response,这个响应包同样有 31 字节的有效载荷空间。广播包负责"喊一嗓子"告诉别人我在这儿,扫描响应包负责"补充说明"我是谁、我能干什么。
在 TLSR_8258 的 SDK 里,这两个包是分开配置的。广播数据通过ble_controller_set_advertising_data之类的接口设置,扫描响应数据则通过对应的 scan response 接口设置。很多人只配了前者,没配后者,结果设备名显示不全或者干脆不显示,就是这个原因。
2.2 主动扫描和被动扫描,决定了扫描响应包会不会被发出去
这里有个容易被忽略的前提:扫描响应包只有在对方主动扫描时才会被请求和回复。如果手机端用的是被动扫描(Passive Scanning),它只收广播包,不发 Scan Request,那你的扫描响应包配得再漂亮也没用。
实际开发中,绝大多数手机系统(包括主流安卓和 iOS)在蓝牙搜索界面用的都是主动扫描,所以扫描响应包通常能被正常触发。但如果你自己写 Central 端程序做测试,就要注意把扫描模式设成主动,否则会误以为扫描响应包没生效。这个点在调试阶段特别容易让人绕弯路,我后面会专门讲怎么排查。
2.3 31 字节的账要算清楚,中文一个字占 3 字节
扫描响应包虽然有 31 字节,但里面还要放 AD Structure 的长度字段和类型字段。以设备名(Complete Local Name,AD Type 0x09)为例,结构是:1 字节长度 + 1 字节类型 + N 字节名字数据。也就是说,名字实际可用空间是 29 字节左右。
如果用 UTF-8 编码,一个中文字符占 3 字节,那 29 字节最多放 9 个中文字,还得留点余量。如果用 GBK 编码,一个中文占 2 字节,能放 14 个左右。这就是为什么编码选择直接决定了你能放多长的中文名。UTF-8 兼容性好但占空间,GBK 省空间但手机端解析不一定认。这个取舍后面会详细分析。
3. 中文编码在 BLE 设备名里的处理逻辑
3.1 蓝牙规范没规定设备名必须是什么编码
翻遍 Bluetooth Core Specification,关于 Local Name 的定义只说了它是 UTF-8 字符串(在较新的规范版本里明确提到 UTF-8),但早期很多设备和协议栈实现里,ASCII 是默认假设。这就导致了一个现实问题:规范说 UTF-8,但手机端显示时怎么解码,各家实现不一样。
安卓系统在解析设备名时,底层拿到的是字节数组,上层用 UTF-8 解码的情况比较普遍,所以 UTF-8 编码的中文名在多数安卓机上能正常显示。iOS 相对更严格地遵循 UTF-8,一般也没问题。但一些老旧的蓝牙调试 App 或者自定义的 Central 程序,可能直接用 ASCII 解码,那中文就会变成乱码。
3.2 UTF-8 和 GBK 的实测对比
我在 TLSR_8258 上分别用两种编码做过测试,结果如下表:
| 编码方式 | 单中文字节数 | 29字节可容纳字数 | 安卓显示 | iOS显示 | 部分调试App显示 |
|---|---|---|---|---|---|
| UTF-8 | 3 | 约9个 | 正常 | 正常 | 可能乱码 |
| GBK | 2 | 约14个 | 部分正常 | 可能乱码 | 视App而定 |
| Unicode转义 | 6 | 约4个 | 基本不支持 | 不支持 | 不支持 |
从实测看,UTF-8 是兼容性最好的选择,虽然占空间,但主流系统都认。GBK 虽然省空间,但在 iOS 上经常出问题,不建议作为首选。如果你的中文名确实很长,超过 9 个字,那就要考虑精简名字,或者把完整名字放到扫描响应包、把简称放到广播包里做配合。
3.3 字节序和数组定义:C 语言里怎么存中文
在 TLSR_8258 的 SDK 里,扫描响应数据通常定义成一个u8数组。中文不能直接写在字符串字面量里就完事,因为编译器对源文件编码的处理方式不同,有的 IDE 默认 GBK,有的默认 UTF-8,直接写中文可能导致实际字节和你预期的不一样。
稳妥的做法是手动查表或者用工具把中文转成十六进制字节数组,然后硬编码到数组里。比如"温度"两个字的 UTF-8 编码是E6 B8 A9 E5 BA A6,那数组里就写0xE6, 0xB8, 0xA9, 0xE5, 0xBA, 0xA6。这样不管源文件是什么编码,烧进去的字节都是确定的。
提示:不要依赖 IDE 的源文件编码设置来保证中文字节正确,手动转十六进制是最稳的方式。转换工具用在线的"UTF-8 编码转换"即可,注意选 UTF-8 而不是 GBK。
4. TLSR_8258 SDK 里扫描响应包的配置实操
4.1 找到扫描响应数据的设置入口
TLSR_8258 的 SDK(以泰凌微常见的 BLE SDK 为例)里,广播和扫描响应的配置一般集中在app_config.h或者专门的app_att.c、app.c文件里。你需要找的是类似scan_rsp_data或者ble_scan_rsp_data这样的数组定义,以及调用设置接口的地方。
典型的设置流程是这样的:先定义一个扫描响应数据数组,然后在广播初始化或者广播启动之前,调用设置扫描响应数据的接口,把这个数组和它的长度传进去。不同版本的 SDK 接口名可能略有差异,但逻辑是一致的——准备数据、设置数据、启动广播。
4.2 构造一个带中文设备名的扫描响应包
假设我们要把设备名设成"温湿度计",用 UTF-8 编码。先算字节:
- "温" = E6 B8 A9
- "湿" = E6 B9 BF
- "度" = E5 BA A6
- "计" = E8 AE A1
一共 12 字节。加上长度字段和类型字段,AD Structure 总长是 14 字节(长度字段值 = 1 类型 + 12 数据 = 13,即 0x0D)。
数组可以这样定义:
// 扫描响应数据:Complete Local Name = "温湿度计" u8 scan_rsp_data[] = { 0x0D, // 长度:类型1字节 + 数据12字节 0x09, // AD Type: Complete Local Name 0xE6, 0xB8, 0xA9, // 温 0xE6, 0xB9, 0xBF, // 湿 0xE5, 0xBA, 0xA6, // 度 0xE8, 0xAE, 0xA1 // 计 };然后在初始化时调用设置接口,把scan_rsp_data和它的长度(14)传进去。注意长度要传实际数组长度,不要传 31,否则会把后面的无效数据也发出去。
4.3 广播包和扫描响应包的分工建议
一个比较实用的做法是:广播包里放一个简短的英文标识或者缩写,扫描响应包里放完整的中文名。这样即使某些场景下扫描响应没被触发,广播包里也有个能识别的名字;而主动扫描时,用户能看到完整中文名。
比如广播包里放TH-01,扫描响应包里放"温湿度计-01"。广播包短,广播功耗和信道占用都小;扫描响应包只在被请求时发送,不影响常态广播。这个分工在低功耗产品里尤其重要,因为广播是周期性发生的,广播包越短,平均功耗越低。
5. 实测中那些让人抓头的坑
5.1 配了扫描响应包但手机不显示中文名
这是最常见的问题。排查顺序建议这样走:
- 确认手机是主动扫描。用 nRF Connect 这类工具时,看它有没有发 Scan Request。如果工具用的是被动扫描模式,扫描响应包根本不会被请求。
- 确认扫描响应数据真的被设置了。在设置接口调用后打个日志,或者用抓包工具看空口数据,确认 Scan Response 里有你配的字节。
- 确认编码是 UTF-8。如果用了 GBK,换 UTF-8 再试。
- 确认长度字段正确。长度字段写错会导致解析错位,后面的数据全乱。
我遇到过一种情况:扫描响应数据设置接口在广播启动之后才调用,结果没生效。设置扫描响应数据要在广播启动之前完成,顺序反了就不行。这个在 SDK 文档里不一定写得明显,但实测确实如此。
5.2 中文显示成问号或者方块
问号通常是编码不匹配,手机用 ASCII 解码了 UTF-8 字节。方块可能是字体问题,但蓝牙设备名一般走系统字体,较少出现。如果确认是 UTF-8 还显示问号,检查一下是不是长度字段把中文字节截断了——比如长度字段写小了,只发了半个中文字符的字节,解码自然失败。
5.3 设备名太长导致扫描响应包溢出
31 字节是硬上限,超了会被协议栈截断或者直接设置失败。如果你的中文名超过 9 个字(UTF-8),就要考虑精简。实在要放长名字,可以拆成两段:扫描响应包里放前半段,另外通过 GATT 服务里的 Device Name 特征值放完整名字。手机连上之后读 GATT 能拿到完整名,扫描列表里显示简称,这是个折中方案。
5.4 不同手机显示不一致
这个前面提过,根源在手机端解码实现差异。不要追求所有设备显示完全一致,把目标定在主流机型正常显示即可。如果产品对显示有强要求,建议在说明书或者 App 里做名字映射,扫描到设备的 MAC 或者广播里的英文标识后,由 App 显示对应的中文名,这样最可控。
6. 几个能直接抄的配置模板和调试技巧
6.1 常用中文设备名的 UTF-8 字节速查
| 中文名 | UTF-8 字节(十六进制) | 总字节数 |
|---|---|---|
| 温度计 | E6 B8 A9 E5 BA A6 E8 AE A1 | 9 |
| 湿度计 | E6 B9 BF E5 BA A6 E8 AE A1 | 9 |
| 传感器 | E4 BC A0 E6 84 9F E5 99 A8 | 9 |
| 智能灯 | E6 99 BA E8 83 BD E7 81 AF | 9 |
| 开关 | E5 BC 80 E5 85 B3 | 6 |
三个中文字刚好 9 字节,加上长度和类型字段共 11 字节,扫描响应包完全放得下,还能再塞点别的数据。
6.2 用抓包工具验证扫描响应包内容
调试阶段强烈建议用支持 BLE 抓包的设备(比如配合 Wireshark 的抓包方案)看一下空口数据。重点看 Scan Response 里的 AD Structure,确认:
- AD Type 是不是 0x09(Complete Local Name)或者 0x08(Shortened Local Name)
- 长度字段和实际数据是否匹配
- 中文字节是不是你预期的 UTF-8 序列
抓包能看到最底层的事实,比在手机界面上猜要高效得多。
6.3 一个容易忽略的细节:广播包和扫描响应包的类型要区分
广播数据里如果已经放了 Complete Local Name,扫描响应包里再放一次,有些手机会以广播包为准,扫描响应包里的名字反而不显示。建议广播包里放 Shortened Local Name(0x08)或者干脆不放名字,把 Complete Local Name(0x09)留给扫描响应包。这样职责清晰,显示也稳定。
6.4 低功耗场景下的取舍
如果产品是电池供电,广播间隔和广播包长度直接影响功耗。扫描响应包虽然只在被扫描时发送,但频繁被扫描也会增加功耗。在低功耗产品里,可以考虑扫描响应包里只放必要信息,名字尽量短,或者用英文缩写加 App 映射的方式,把中文显示交给 App 处理。这是我在实际项目里比较推荐的思路——芯片负责可靠通信,显示层的事情交给显示层。
7. 关于这套方法,我自己的几点体会
TLSR_8258 这颗芯片在 BLE 开发里算是性价比很高的选择,SDK 也相对完整,但文档对中文处理这块着墨不多,很多细节要靠自己试。我最初做中文设备名的时候,在编码和长度上反复折腾了好几轮,最后总结下来就是两条:UTF-8 编码、长度字段算准、设置顺序别搞反。把这三件事做对,中文扫描响应包基本就稳了。
另外,扫描响应包不只是放设备名,厂商自定义数据、服务提示、配网信息都可以放。理解了它的机制之后,你会发现这 31 字节是个很灵活的空间。后面如果有机会,可以再聊聊怎么用扫描响应包做快速配网或者设备识别,那是另一个有意思的方向。
如果你也在用 TLSR_8258 做蓝牙产品,遇到中文显示的问题,可以先按上面的排查顺序走一遍,大概率能定位到原因。实在搞不定,抓个包看看空口数据,真相往往就在那几行十六进制里。