做BLE开发这些年,最常被新手问到的问题之一就是设备地址。扫描回调里返回的那6字节地址,到底是啥?是不是手机或开发板的MAC?为什么同一台设备,昨天扫出来是AA:BB:CC:DD:EE:FF,今天就变成了11:22:33:44:55:66?更迷惑的是,iPhone上拿到的还是一个UUID,压根不像地址。这篇文章就把BLE设备地址这件事彻底讲透,涉及ble通信协议里的链路层行为、ble连接过程中地址怎么传递,以及Android、iOS、uni-app、nRF52840抓包和ble mesh remote provisioning场景下地址的各种表现。无论你是刚接触ble蓝牙的新手,还是已经在ESP32、nRF52上写过固件的老手,这些内容都值得留个底。
1. BLE设备地址是什么:广播包和连接包里的“身份ID”
蓝牙低功耗工作在2.4GHz频段,这个频段被划分成40个射频通道,其中3个是广播通道(37、38、39),37个是数据通道。在BLE世界里,设备之间的发现、连接、数据收发,底层都靠链路层PDU在跑,而设备地址就是这些PDU里最核心的身份标识。
1.1 地址的48位格式与两个大类型
BLE设备地址长度固定是6字节,也就是48位,平时我们看到的“XX:XX:XX:XX:XX:XX”就是它的展现形式。但这个地址和你熟悉的以太网MAC地址有本质区别,它不一定是由厂商烧录的,也不一定是全球唯一的。
从协议栈层面看,BLE地址一共分两个大类:Public Device Address(公共设备地址)和 Random Device Address(随机设备地址)。凡是名字里带“Random”的,都意味着这个地址不是从IEEE申请来的固定OUI,而是设备自己生成的。判断一个地址是公共还是随机,不靠猜,而是看广播包或扫描请求里带的标志位:TxAdd和RxAdd。TxAdd=0表示发送方地址是Public类型,TxAdd=1表示发送方地址是Random类型。
在nRF52840这类芯片的开发中,很多人直接用SDK默认地址,那往往是静态随机地址。如果不理解地址类型,就容易出现“明明设备改了MAC,为什么扫描端还是显示老地址”这种让人摸不着头脑的问题。
1.2 广播扫描流程里的TxAdd和RxAdd到底做了什么
BLE设备之间的发现流程并不复杂。广播者周期性地在37、38、39三个广播通道上发送ADV_IND,这个包里面带着AdvA(广播者地址)和TxAdd标志。扫描者收到广播后如果想获取更多信息,可以回一个SCAN_REQ,里面包含ScanA(扫描者地址)和AdvA(目标广播者地址),同时通过RxAdd标志表明自己的地址类型。
这里最关键的一点是:每个链路层控制包里的地址字段都是6字节,但地址到底是Public还是Random,完全由头部标志位决定。所以抓包的时候,你不能只盯着地址的十六进制去猜,必须结合标志位来解析。
1.3 地址在连接建立时序里出现在哪个环节
完整的BLE连接过程可以简化为:广播设备发ADV_IND,扫描设备(此时变成发起者)发送CONNECT_REQ,双方进入连接状态。CONNECT_REQ这个包非常关键,里面同时携带了发起者地址(InitA)和广播者地址(AdvA),还有初始连接参数:连接间隔、从机延迟、超时时间、跳频增量等。
为了更直观,我把这个时序写出来,你在nRF52840上用抓包器分析时,看到的就是这个顺序:
广播者 扫描者/发起者 |<------------- ADV_IND (AdvA) ----------------| | | |-------------- SCAN_REQ (ScanA, AdvA) ------->| | | |<------------- SCAN_RSP (AdvData) ------------| | | |<------------- CONNECT_REQ (InitA, AdvA) -----| | | |============= 数据通道双向通信 ================|这个时序就是很多人常说的ble蓝牙建立时序图核心部分。注意看,从SCAN_REQ开始,双方就会把自己的地址放进包里,用来做链路层过滤和身份确认。如果其中一方是随机地址,连接建立后,每次重连时地址都有可能变化,这就引出了后面要讲的地址类型问题。
2. 四种地址类型逐一说清,别再傻傻当MAC用
很多人踩坑,根源是把BLE地址理解成了“蓝牙MAC”。实际上,BLE协议栈把地址分得还挺细,除了公共地址,随机地址里又拆成三种。这四种地址长得都一样,都是6字节,但行为和用途完全不同。
2.1 Public Device Address:厂商花钱买的“固定身份证”
Public Device Address才是真正意义上的全球唯一MAC地址。它的前3字节来自IEEE分配的OUI(组织唯一标识符),后3字节由厂商自己分配,组合起来就是一个48位的全球唯一地址。想用这种地址,厂商需要向IEEE缴费申请OUI。
实际产品里用Public Address的并不多,原因有两个:一是费用和合规成本比较高,二是它完全没有隐私性。只要设备用固定公共地址广播,任何人都能通过扫描器长期跟踪这台设备的行踪。所以现在的消费类蓝牙设备,尤其是手机、手表、耳机,普遍不会直接暴露Public Address。
2.2 Static Random Address:低成本产品的通用做法
Static Random Address,翻译成中文通常叫静态随机地址。它的两个最高位是11,其余46位由设备随机生成。这个地址一旦生成,在一次上电周期内保持不变,可以认为是一台设备在正式广播前的“自我命名”。
很多BLE模组出厂默认就是这个地址类型。比如ESP32、nRF52832跑默认例程的时候,扫描端看到的地址往往不是芯片烧录时的MAC,而是SDK在启动时生成的一个静态随机地址。需要注意,如果设备每次上电都重新生成,那对用户来说就是“每次连接都要重新找设备”;更好的做法是把随机地址保存到非易失性存储区,比如Flash或者NVDS,保证重启后地址不变。
2.3 Non-resolvable Private Address:临时变化的“马甲”
Non-resolvable Private Address,两个最高位是00,其余46位按一定周期轮换。这种地址的作用很简单:不让别人跟踪,但也不提供任何能被识别的信息。它适合那些不需要长期绑定、只是临时广播数据的场景。
比如一个温度传感器,每10分钟广播一次数据,每次广播用一个新的随机地址。扫描端即使收到了,也无法确认它是不是上次那台设备。这种隐蔽性是好事,但也意味着你不能靠它做白名单过滤,无法主动连接一台地址会变的设备。
2.4 Resolvable Private Address:带密钥的“变装”方案
Resolvable Private Address是隐私保护的主力,两个最高位是01。它的核心思路是:地址看起来在变,但拥有正确密钥(IRK,Identity Resolving Key,身份解析密钥)的接收方,可以通过密码学计算识别出设备身份。
日常使用中,iPhone和绝大多数Android手机对外广播时用的就是这种地址。你在手机蓝牙设置里看到的,和它实际发出的地址,并不是一回事。连接过的设备,因为存了对方的IRK,所以哪怕地址轮换了,系统依然能认出“这是同一台手机”。这也是为什么AirPods这类配件能和手机保持稳定连接,而不会因为地址变了就断连。
2.5 怎么快速辨认:看最高两位就够
虽然最可靠的判断方式是看链路层头部标志位,但如果你想从地址本身快速区分类型,看第一字节的最高两位就行:
| 地址类型 | 最高两位 | 是否变化 | 典型场景 |
|---|---|---|---|
| Public Device Address | 非11/01/00(通常00开头,但仍由OUI决定) | 固定 | 少数工业设备、开发板 |
| Static Random Address | 11 | 基本固定,可重启变化 | 模组、开发板默认 |
| Non-resolvable Private Address | 00 | 定期变化 | 临时广播、隐私广播 |
| Resolvable Private Address | 01 | 定期变化,可被IRK解析 | 手机、消费电子设备 |
我建议你在固件设计阶段就明确自己用哪种地址,不要只图省事用默认值。如果你的产品需要绑定用户,Resolvable Private Address加IRK管理是目前最稳妥的方案。
3. 可解析私有地址的原理:IRK到底怎么“认出”你
有一种情况很常见:你用手机连续扫同一个开发板,显示的地址每次都变,但点连接又能连上。这是因为手机上已经保存了对应设备的IRK,能在后台把这串不断变化的地址“翻译”回真实身份。这块机制很值得展开讲。
3.1 从一个例子开始:扫描端如何判断设备“没变”
假设一个蓝牙耳机使用Resolvable Private Address,每过15分钟切换一个新地址。它的手机App如果只靠扫描到的地址去匹配,那每次都要重新弹窗确认。实际上不是这样,手机在第一次配对后已经把耳机的IRK存进了系统蓝牙协议栈。之后每次收到广播,就算AdvA是一个从没见过的6字节地址,系统也会拿着所有已保存的IRK去尝试解析,只要匹配上,就认为“老熟人来了”。
3.2 生成和解析的关键公式
Resolvable Private Address的48位由两部分组成:高24位是哈希值,低24位是随机数prand。两个最高位固定为01。生成过程并不复杂:
1. 随机生成一个24位的prand,并把最高两位强制设为01; 2. 用IRK作为密钥,对prand执行AES-128加密; 3. 取加密结果,得到24位哈希值; 4. 地址 = 哈希值 || prand(哈希在高位,prand在低位)。接收方向上的解析就是反过来:
# 伪代码,仅示意逻辑,实际实现需注意字节序 def resolve_address(address, irk): prand = address & 0xFFFFFF expected_hash = aes128_encrypt(irk, prand) actual_hash = (address >> 24) & 0xFFFFFF return expected_hash == actual_hash如果哈希匹配,接收方就确认这个地址来自持有该IRK的设备。整个过程对上层应用是透明的,链路层直接处理完了。
3.3 白名单过滤为什么要靠地址解析
BLE协议支持白名单过滤,但白名单里不仅能放固定地址,还能放IRK。当扫描或广播开启“白名单+地址解析”模式时,控制器会在硬件层面直接判断:这个新地址解析后是不是白名单里的身份。这种做法比“扫到广播再逐层上报”效率高得多,适合对功耗敏感的低功耗产品。
在实际项目中,我踩过的一个坑是:把设备设置成Static Random Address后加入了白名单,结果设备重启后生成新地址,白名单立刻失效。后来改成用IRK白名单,问题才彻底解决。
4. 平台实战:Android、iOS、uni-app里地址怎么用
地址类型讲清楚后,回到开发场景。不同操作系统对BLE地址的暴露程度完全不一样,这里面的差异如果没摸清,很容易写出“在Android上正常、在iOS上连不上”的代码。
4.1 Android:BluetoothDevice.getAddress()的那些坑
Android的BluetoothDevice对象有getAddress()方法,看起来能拿到MAC地址,实际上有很多限制。Android 6开始,获取地址需要定位权限;Android 6以下系统,这个方法返回的通常是真实MAC地址;但从Android 12开始,系统进一步限制地址可见性,某些设备上这个方法会直接返回“02:00:00:00:00:00”,这是一个被系统屏蔽后的固定占位值。
更麻烦的是,如果对端外设使用的是随机地址,Android返回的就是当前扫描到的那个随机地址,而不是设备的固定身份。所以不要把getAddress()的结果当成业务主键去存数据库,否则下一次扫描可能就匹配不上了。
4.2 iOS:为什么peripheral.identifier不是MAC地址
iOS/ iPadOS的CoreBluetooth框架里,每个扫描到的peripheral只有一个identifier属性,类型是UUID,不是BLE MAC地址。这个UUID是系统根据设备的链路层身份(通常是IRK)和本机密钥计算出来的,同一台iPhone上稳定,但换一台iPhone、恢复系统设置、或者删除蓝牙配对记录后,它都可能变。
iOS没有公开API能直接读取BLE设备地址。你找不到“getMACAddress”之类的方法,因为Apple从隐私设计上就不打算把地址暴露给应用层。理解了这一点,就不会再纠结“为什么iOS拿不到MAC地址”了。
4.3 uni-app跨平台:能不能拿deviceId去createBLEConnection
热搜里有个非常具体的问题:uni-app在iOS上能不能根据蓝牙的deviceId建立连接?答案是能,但你要先搞清楚这个deviceId在不同平台是什么。uni-app的onBluetoothDeviceFound回调里有个deviceId字段,Android端通常是蓝牙MAC地址(或当前随机地址),iOS端就是peripheral.identifier这个UUID。拿这个deviceId传给uni.createBLEConnection,系统是认的,连接能建立。
但真正要注意的是:不要把这个deviceId当作永久ID去存。iOS重启、蓝牙重置、App卸载重装后,identifier可能变化。稳妥做法是每次启动重新扫描,根据设备名、广播数据里的自定义字段、ServiceUUID来综合判断目标设备,然后拿当次的deviceId去连接。
4.4 跨平台设备识别方案总结
如果你在做一个跨平台App,想稳定识别同一台BLE设备,建议按优先级这么选:
- 在广播数据里放入自定义的设备唯一ID字段,扫描端解析广播后拿到ID做业务识别;
- 使用iBeacon或Eddystone这类广播协议,里面携带的major/minor或namespace/instance信息可以当作设备标识;
- 对支持配对的设备,使用系统级的配对绑定机制,靠IRK识别身份;
- 尽量别依赖链路层MAC地址做长周期存储,地址类型和平台限制都会让你踩坑。
我会在设计BLE设备固件时,特意把设备序列号写进广播的厂商自定义数据段。这样App连不连得上,都先能通过广播内容识别设备,用户体验好很多。
5. 抓包实测:用nRF52840嗅探真实BLE地址
前面讲的都是纸上谈兵,实际调问题时,最直观的办法就是抓空中的BLE包来看。这里我以nRF52840为例,把从硬件准备到看地址字段的完整过程理一遍。
5.1 准备工作:刷sniffer固件、装Wireshark
nRF52840本身是一颗非常常用的BLE芯片,把它刷成Sniffer固件后,就能变成一个BLE抓包器。nRF官方提供了nRF Sniffer for Bluetooth LE这个工具,配合Wireshark使用。具体步骤是:用nRF Connect for Desktop或nrfutil把sniffer固件烧进nRF52840 dongle,然后在Wireshark里选择对应的串口捕获接口。
nRF Connect for Desktop里的“Bluetooth Low Energy”应用也可以直接查看设备,但如果你要看链路层细节,比如TxAdd、AdvA、InitA字段,还是Wireshark最合适。刷固件时注意选择与你芯片型号完全匹配的sniffer版本,用错固件会出现抓不到包或者乱码。
5.2 看ADV_IND包:TxAdd标志和AdvA字段怎么读
抓包后,在Wireshark里选中一个广播包,展开“Bluetooth LE Link Layer”协议树,你会看到类似这样的内容:
Bluetooth LE Link Layer Advertisement Type: ADV_IND Advertiser Address: 5a:xx:xx:xx:xx:xx Advertiser Address Type: Random (1)这里的关键就是Advertiser Address Type:如果是Random (1),说明TxAdd=1,地址是随机类型。你再多抓几个广播包,如果AdvA在变化,说明对端用的是Private Address;如果不变,可能是Static或Public。
我调试时有个习惯:先用抓包器确认设备地址类型,再去代码里查SDK配置。这样能迅速判断问题是出在固件配置,还是出在上层扫描逻辑。
5.3 观察CONNECT_REQ:发起者和应答者地址在哪
连接建立时抓一个CONNECT_REQ包,展开后能看到:
- InitA:发起者地址,通常是你的手机或开发板;
- InitA Type:发起者地址类型;
- AdvA:应答者(广播者)地址;
- AdvA Type:应答者地址类型。
这个包里的地址类型信息,能帮你确认双方在连接协商时用的是什么地址。有一次我排查一个“偶发连不上”的问题,抓包后发现手机作为发起者,每次Connection Request里的InitA都在变,对端固件又做了白名单过滤,导致白名单一直匹配不上。后来改成关闭白名单或改用IRK匹配,问题迎刃而解。
5.4 实际案例:同一台手机为什么每次广播地址都变
用抓包器对准一台iPhone,你会发现它发出的ADV_IND包里的AdvA地址每隔一小段时间就换一次,而且最高位总保持01。这就是Resolvable Private Address的典型表现。如果这台iPhone之前配对过某台设备,那台设备只要存储了iPhone的IRK,就能在新地址出现后仍然认出它。
开发时如果你想减少这种“地址乱跳”的干扰,可以在设备的调试固件里把地址固定成Static Random Address,抓包和日志匹配都会轻松很多。但产品化之前一定要记得改回带隐私保护的方案。
6. BLE Mesh场景下的地址问题:别把Unicast Address当MAC
蓝牙Mesh是近年很火的方向,尤其是工业照明、智能楼宇这类大规模组网场景。这里面的地址体系和经典BLE完全不一样,如果还拿“广播地址”的概念去套,会非常痛苦。
6.1 Mesh地址体系与链路层地址的区别
BLE Mesh网络里的设备不叫“从机”,而叫“节点”。每个节点在被配置(Provisioning)后,会获得一个16位的Unicast Address(单播地址),这是Mesh网络内的唯一标识。这个地址和链路层的BLE MAC地址没有任何映射关系,它只存在于Mesh协议层。
Mesh里还有Group Address(组播地址)和Virtual Address(虚拟地址)。比如一栋楼的照明系统,会把“3楼所有灯”配成一个Group Address,发一条消息就能同时控制一批设备。这种抽象是Mesh的优势,但也意味着你不能靠扫描到的BLE广播地址去反推它是Mesh里的哪个节点。
6.2 Remote Provisioning中怎么发现和绑定设备
热搜词里提到的ble mesh remote provisioning,指的是远程配置未入网设备的过程。普通Provisioning需要Provisioner和被配设备在物理距离内能互相扫描到,但Remote Provisioning允许一个已经入网的节点,通过Mesh网络转发Provisioning PDU,让Provisioner去配置另一个不在身边的设备。
这里被配置设备在链路上仍会发出Unprovisioned Device Beacon,里面携带的是一个128位的Device UUID,用来标识设备身份。所以做远程配置时,你关注的重点不是链路层6字节地址,而是Device UUID和Provisioning过程中分配的Unicast Address。
6.3 排查建议
远程配置连不上的时候,我一般会分几步排查:先确认代理节点有没有发出Mesh Beacon,再打开抓包工具看Unprovisioned Device Beacon里的Device UUID和广播信号强度,最后检查代理节点是否被授权执行Remote Provisioning操作。链路层地址在这种场景里只是“搬运工”,再怎么变都不影响Mesh层的身份识别。
7. 常见问题速查表与避坑清单
根据我过去几年的经验,把BLE地址常见的问题整理成一张速查表,方便你排查时快速定位:
| 问题 | 可能原因 | 解决思路 |
|---|---|---|
| 同一台设备地址一直在变 | 使用Private Resolvable/Non-resolvable地址 | 配对并存储IRK,或改用Static地址调试 |
| Android上拿不到真实MAC | Android 6+权限限制,Android 12+系统屏蔽 | 改从广播数据中的业务ID识别设备 |
| iOS上只有UUID没有MAC | CoreBluetooth设计如此 | 将peripheral.identifier作为会话级标识,不长期依赖 |
| uni-app iOS deviceId连接失败 | identifier变化或设备已不在广播 | 每次启动重新扫描,按广播内容匹配后连接 |
| 白名单过滤失效 | 白名单存的是固定地址,对端却用私有地址 | 白名单改用IRK匹配 |
| Mesh节点身份对不上 | 把Unicast Address当MAC用 | 用Mesh地址体系和Device UUID做节点标识 |
除了表格里的坑,还有几个经验可以分享:
固件里如果用Static Random Address,务必保存到掉电不丢失的区域。我见过太多产品重启后地址改变,导致用户手机里保存的老设备怎么都连不上。
如果你的外设需要被多个App扫描和识别,最好不要在扫描端做“只能连一个”的强绑定逻辑,BLE本身支持一对多广播,业务层的状态管理要清晰。
抓包分析时,优先用Wireshark看链路层字段,不要只看上层工具的“友好展示”。很多协议栈问题只有链路层才能暴露出来。
最后,产品量产前,建议把所有设备统一写入唯一的业务序列号。链路层地址只作为“信使”,业务识别全靠序列号,这样无论Android还是iOS、无论地址如何轮换,你的系统都不会乱。
做BLE开发这几年,我个人最深的体会是:设备地址只是发现阶段的“临时姓名”,千万别把它当成用户的“身份证号”。理解清楚地址类型、平台差异和Mesh特殊场景,你就能少走很多弯路。遇到问题的时候,不妨先抓个包,看看空中到底发生了什么,再决定要不要改代码。这个习惯帮我省下了大量排查时间,也推荐给你试试。