
做BLE开发的人十有八九都被“MAC地址”坑过。手机扫描出来一个地址打印出来看着像48位的设备地址拿去绑设备重启后却变了同一个设备在协议栈启动前后不一样抓包软件里明明标着 Public Device Address和手机系统里显示的又对不上。这些困惑基本都源自同一个问题BLE 并不像 Wi-Fi 那样只有一张静态的 MAC 地址它把设备地址分成了好几类Public Device Address、Random Device Address、Non-resolvable Private Address 这些名词听着像在背协议规范实际上直接决定了你的设备能不能被发现、连接之后会不会掉线、绑定关系还能不能对上。这篇文章就把这几个地址类型讲透顺便把开发中常见的“MAC 变了”、“平台显示不一致”问题一起解决了。1. 先搞清楚BLE里的“MAC地址”为什么这么难懂1.1 蓝牙地址不只是“一张网卡一个号”接触过以太网或者 Wi-Fi 的朋友对这个“MAC 地址”一般会有一个刻板印象设备出厂时烧录一个全球唯一的地址比如路由器标签上印的那一串 12 位十六进制数这辈子基本不变。但到了 Bluetooth Low Energy这套经验直接失效。BLE 的链路层地址也是 48 位结构上跟 MAC 地址完全一致都是XX:XX:XX:XX:XX:XX这种形式但它的语义远比网卡地址复杂。为什么因为 BLE 的广播和连接机制天然需要“身份”这个概念。网卡地址唯一是因为我们通过它来寻址设备身份和物理地址是绑定的。而 BLE 的应用场景既包含长期连接的传感器也包含大量一次性的广播数据包还有注重隐私的可穿戴设备。如果所有设备都用一成不变的固定地址那任何人都可以扫描你、跟踪你、记录你每天去过哪里、用过哪些设备。所以蓝牙规范在经典蓝牙的 Public Address 之外增加了一整套随机地址体系让设备在不暴露身份的情况下依然能正常通信。这样一来BLE 的“MAC 地址”就不再是一个固定不变的值而是一个会变、可以配置、还能加密解析的地址系统。很多开发者在第一天接触 BLE 时都会拿着手机扫描工具看到设备地址还分 Public 和 Random一脸懵。没关系先记住一句话BLE 地址本质是个 48 位二进制数地址类型由最高的两个 bit 决定不同 bit 组合代表不同含义后面所有内容都围绕这个展开。1.2 地址家族全景Public 和 Random 两大分支蓝牙核心规范Bluetooth Core Spec里BLE 的设备地址分成两大类第一类只有一种叫 Public Device Address第二类叫 Random Device AddressRandom 下面又分三种Static Random Address、Non-resolvable Private Address、Resolvable Private Address。很多人看到这个分类会问Non-resolvable Private Address 好像是单独一个概念标题里为什么把 Random Device Address 和它并列因为 Random Device Address 是一个大类而 Non-resolvable Private Address 是其中的一种具体实现。它们不是同一层级的概念。实际开发中你看到的地址类型字段大概率就是这么标的地址类型最高两位 bit是否固定能否解析身份典型用途Public Device Address00固定直接可识别出厂设备、Beacon、经典蓝牙Static Random Address01可固定直接可识别无 OUI 厂商的设备、模组地址Non-resolvable Private Address10定期变化不可解析匿名广播、周期性扫描Resolvable Private Address11定期变化可解析需 IRK隐私保护连接、防追踪这张表基本就是全文的地图。前两类是人畜无害的“显式身份”后两类是让你又爱又恨的“隐私地址”。下面逐个展开。2. Public Device Address唯一不变的一类2.1 组成结构公司标识加上厂商分配Public Device Address 的逻辑最好懂它要求设备厂商向蓝牙技术联盟Bluetooth SIG申请一个 Company Identifier这个标识一共 24 位通常你看到的 MAC 地址前缀00:0C:29、50:65:83这类就是公司标识。剩下的 24 位由厂商自己分配可以是流水号可以是芯片唯一 ID只要整个 48 位组合不冲突就行。这样的地址就是典型的固定地址和以太网卡一样出厂烧录后一般不改变。很多人会把蓝牙的 Public Device Address 和 Wi-Fi 的 MAC 地址完全混为一谈实际上它们确实长得一模一样连最高两位 bit 都是 00。这也造成一个常见误判如果我只拿蓝牙设备的地址前三位去查 OUI 数据库查到某个厂商就以为这个设备一定是该厂商生产的。这个判断在 Public Device Address 下基本成立但对于后面的 Random Address 完全不适用。注意BLE 的 Public Device Address 可以由芯片内部一次编程区域来存也可以是软件配置到 Flash 里但从蓝牙链路层视角它没有义务对外证明自己“真的来自那个 OEM”。2.2 实际用在哪怎么查Public Device Address 最常见的用途是设备出厂时需要有一个稳定身份、又不打算做复杂配对绑定的场景。比如 iBeacon 采集终端、防丢器、部分智能家居传感器都会直接用 Public Device Address 作为自己的唯一标识。上位机扫描时拿这个地址做成白名单只要广播包里的地址匹配就认为找到了目标设备。查这类地址的方式很简单。Linux 上可以用btmgmt或者hcitool lescan扫描广播设备Android 端可以在开发者选项里打开“蓝牙 HCI 信息收集日志”再用 Wireshark 打开日志文件就能看到完整的广播数据和地址类型。这里有一个很容易踩的坑很多芯片出厂烧录的地址是写在一次性 Fuse 或者寄存器里的如果你同时用多个同型号开发板它们的 Public Device Address 可能非常相似甚至前面的 24 位完全相同。如果你在代码里写死了“地址前缀等于XX” 就认定是自己设备那同厂商的不同设备会全部命中反而找不到真正的目标。2.3 OUI 前缀不能迷信看到热搜里有“000c 29开头的mac地址都是虚拟机吗”这个问题顺便说一句这类问题在 BLE 开发里也很典型。00:0C:29是某虚拟化平台申请的公司标识出现在以太网虚拟网卡里非常常见但假如你在分层网络环境里看到一个随机设备地址正好也是00:0C:29开头那基本可以断定是有人在软件层面伪造或者借用了这个 OUI。同理BLE 设备如果宣称自己的 Public Device Address 是00:0C:29:xx:xx:xx实际是某厂商的虚拟化平台 OUI也不能说明它一定来自该平台只是用户手动配置了一个类似的地址。真正要确认身份还是要结合广播数据、配对密钥、服务 UUID 等综合判断。前缀只能缩小范围不能当成铁证。3. Random Device Address一类地址两种命运3.1 Static Random Address没申请到 OUI 的“固定地址”Static Random Address 是 Random Device Address 里最接近“固定地址”的一种。生成规则是整个地址 48 位里最高两位固定为 01剩余的 46 位随机生成但不能全是 0 也不能全是 1。它不需要向蓝牙技术联盟申请任何厂商都可以自己生成只要不跟同一批设备里的其他地址冲突就行。这个特性对小厂商和做模组的团队特别友好。因为申请一套 OUI 要花钱走流程很多小团队做产品根本不需要全球唯一的前缀只要在我这一批设备里不冲突、重启后稳定不变就行。于是 Static Random Address 成了最常见的“低成本身份方案”。很多国产蓝牙芯片出厂时烧录的地址就是这种 Static Random Address它的特征也很明显地址的最高两位是 01转换成十六进制后地址的最左边两位大概率是4x到7x这个区间因为最高字节的最高两位已经是 01 了。Static Random Address 有一个重要的行为特点它“可以”变但“通常”不变。规范里说设备上电时可以重新生成也可以沿用原先的值。如果 SDK 的默认行为是每次启动都重新随机生成你看到的设备地址就会“每次开机都变”。这就是搜索词里“杰理701芯片mac地址为什么会改变”的一类典型原因不是芯片坏了而是这个地址本身就是 Random Device Address 里的 Static Random固件没把它固化下来每次初始化都重新随机了一次。解决办法也很简单在协议栈初始化时把上次生成的地址保存到 Flash下次启动读回来重新设置或者直接写死一个固定地址。3.2 Non-resolvable Private Address不想被认出来的“路人甲”Non-resolvable Private Address 这个名字有点劝退但它的思想很简单我不要实名甚至连“可解析的假名”都不要我就是个路人甲。它的最高两位固定为 10剩下的 46 位随机生成定时更换。因为随机部分不含任何可识别的身份信息收到这个地址的对端设备没有任何办法通过计算或者查表知道这个地址背后是哪个设备。那这种地址有什么用最常见的场景是设备只是周期性地向外发广播不需要被连接也不希望被追踪。比如一个商场里的客流统计信标它每隔几百毫秒广播一次每次地址都换路过的人无法把它和特定商场、特定品牌联系起来更无法判断“这个信标是不是我之前见过的那个”。在这种情况下Non-resolvable Private Address 就是最合适的匿名方式。我自己在开发环境里见过很多新手犯的错是把 Non-resolvable Private Address 当成了设备标识存到后台数据库里。结果就是设备每换一次地址服务器后台就多出一条“新设备”记录。这个问题在设备长时间运行、周期性广播时特别严重。所以遇到“设备数量越积越多”“同一物理设备被重复入库”这类现象第一反应应该去查地址类型看是不是 Privacy 相关的地址在捣乱。3.3 Non-resolvable 能不能发起连接这个问题值得单独说。Non-resolvable Private Address 不是不能用来发起连接而是在连接建立后对端设备无法通过地址识别你。假设你的传感器用 NRP 地址去连接手机手机收到连接请求时扫描到的地址一直在变化它根本不知道这是已经配对过的设备还是陌生设备。如果你们之间又没有绑定关系手机大概率会直接忽略或者进入普通未配对流程。反过来如果手机用白名单过滤只允许“已知设备”连接而你的地址每过一段时间就变了那白名单也形同虚设。所以 Non-resolvable Private Address 更适合广播、扫描这些“不需要长期身份”的场景。真正常用的连接型隐私方案是下面要讲的 Resolvable Private Address。4. Resolvable Private Address既隐藏又能认出来4.1 核心原理用 IRK 加密生成地址标题虽然只提到了 Public、Random、Non-resolvable 三类但实际开发里Resolvable Private Address 才是隐私功能的主角。它解决了一个看似矛盾的问题设备不想让别人通过固定地址跟踪我但同时我家手机要是能认出我来。RPA 的最高两位是 11地址的组成分两部分24 位的 prand随机部分和 24 位的 hash哈希部分。生成过程大致是设备自己保存一个 128 位的密钥叫 IRKIdentity Resolving Key类似于设备的“私钥”。每次需要生成新地址时先用随机数生成 prand再对 prand 做哈希运算取结果前 24 位作为 hash最后拼成一个 48 位地址。对端设备怎么认出来前提是它在之前的配对过程中已经拿到了你这个设备的 IRK。它在扫描到地址后会把地址拆开取出 prand用同样的哈希算法算一次看算出来的结果是不是跟地址里的 hash 部分一致。如果一致说明这个地址就是用我手上这把钥匙生成的那这台设备就是之前跟我配对过的设备。这就是“Resolvable”的含义地址看起来是随机的对外保密但持有 IRK 的对端可以识别出来。4.2 连接之后怎么保证不掉线这里有一个容易混淆的点RPA 的地址是定期变化的那设备正在连接的时候地址变化会不会导致断连答案是不会。BLE 的连接链路在建立之后链路层地址已经固定作用在当前连接上中高层不再依赖地址去寻址。地址变化发生在每次广播、扫描、重新连接时。也就是说设备在断网外广播时用地址 A过一会儿换地址 B已经建立的连接不受影响但如果它断开后想重新连接到手机就必须用新的 RPA手机再通过 IRK 解析出来才能知道“哦还是原来那台设备”。这也是为什么你在 iOS 和 Android 上看到的蓝牙设备 ID 呈现方式完全不同。iOS 出于隐私保护不暴露底层链路层地址给上层的是一个 UUIDAndroid 在某些版本里直接暴露 MAC 地址但地址可能是随机的。如果你在开发上层应用直接拿系统返回的 MAC 地址当设备唯一标识在 RPA 环境下会非常痛苦。正确做法是先做 Bonding配对绑定把 IRK 和 Identity Address 保存下来后续靠绑定信息识别设备而不是靠扫描到的瞬时地址。4.3 三种随机地址的边界什么时候该用谁实操经验里我用一句话帮助团队做选型你希望设备“稳定可见”就用 Static Random你希望设备“彻底匿名”就用 Non-resolvable Private你希望设备“既匿名又能被信任方识别”就用 Resolvable Private。需要注意Static Random Address 和 Non-resolvable Private Address 之间有一道分界线后不能随便混用。有人为了省事把 Non-resolvable 地址也当成“会变的随机地址”在程序里直接保存下来用于回连结果设备重启后地址变了回连失败。而 Static Random 地址虽然用户可能没给它烧录一个漂亮的 OUI但只要你在 SDK 里固定下来它实际上是可以长期稳定工作的。我自己做低功耗外设时如果没有严格的防追踪需求倾向使用固化后的 Static Random Address省心也方便用手机扫描工具调设备。5. 实操如何查看与配置 BLE 地址类型5.1 抓包软件里的地址类型怎么看排查问题永远要先看事实。最简单的方式是用抓包工具。PC 端可以用 nRF52840 Dongle 配合 Wireshark 的 nRF Sniffer 插件抓空中的广播包和连接包。打开广播包后展开 Bluetooth LE Link Layer 字段能看到AdvA: xx:xx:xx:xx:xx:xx紧接着下面就会标注Address Type: Public或者Random如果是 Random还会继续细分 Static、Non-resolvable、Resolvable。很多工程师抓包之后只盯着地址本身忽略地址类型字段这是不对的。同样是4A:C5:...这样的地址如果类型是 Static Random你固化下来没问题如果类型是 Resolvable Private你固化下来就是给自己挖坑。抓包看到的现象一定要和地址类型绑定起来看。如果手头没有专业抓包硬件手机也能凑合。Android 手机在开发者选项里打开“蓝牙 HCI 信息收集日志”再用 Bug Report 或日志导出功能拿到 HCI 日志放到 Wireshark 里同样能看地址类型。iOS 的隐私限制比较严不容易直接拿到链路层日志但如果自己用 CoreBluetooth 开发可以从系统返回的CBPeripheral.identifier变化频率侧面判断对方是否在使用隐私地址。5.2 手动解析地址类型的小工具很多团队喜欢把抓包文件导出后自己写脚本统计“哪些设备在广播”。这时候如果你不理解地址类型统计结果一定是错的。我写过一个非常简单的 Python 函数用于在事后分析时按地址类型归类def parse_ble_addr_type(addr_int: int) - str: # addr_int: 48 位的链路层地址 top_two (addr_int 46) 0b11 if top_two 0b00: return Public Device Address if top_two 0b01: return Static Random Address if top_two 0b10: return Non-resolvable Private Address if top_two 0b11: return Resolvable Private Address return Unknown这里核心逻辑就是提取最高两位 bit。需要注意的是市面上不同抓包脚本展示地址时字节序处理不统一。有的把地址按大端字符串输出有的按蓝牙传输序输出导致你从字符串里取最高字节时容易取反。建议先从数据源里拿到原始 48 位整数值再做解析而不是对显示字符串盲猜。在我实际处理过的日志里因为工具字节序问题把 RPA 判断成 Public 的例子有两三次每次排查都要花掉半天时间。如果你要在开发板上配置 Static Random Address可以根据自己 SDK 的接口来处理。例如很多协议栈里都提供类似set_random_address(addr)的接口你只需要准备一个合法的 48 位地址传入。这里也写一个生成合法 Static Random 地址的参考逻辑import secrets def generate_static_random_addr() - int: val secrets.randbits(48) val ~(0b11 46) # 先清空最高两位 val | (0b01 46) # 最高两位置为 01 # 规范要求剩余 46 位不能全 0 或全 1随机情况下概率极低 return val把生成的整数转成 6 字节存进 Flash下次启动直接读回来设置就能固定住这个“伪静态地址”。注意不同芯片的 API 对字节序要求不一样传参前要看清楚 SDK 里地址数组是[LSB, ..., MSB]还是反过来这是最容易踩到的地方。5.3 平台层统一识别方案做应用层开发的同学最头疼的就是“MAC 地址在 iOS 和 Android 上拿到的完全不一样”。这里直接给结论iOS 从CBPeripheral.identifier拿到的是一个 UUID这个 UUID 在系统重新启动、设备卸载 App 后可能变化Android 6 以后获取蓝牙地址需要定位权限Android 8 以后很多设备返回的是随机地址不是 Public Device Address。所以两个平台都不建议直接拿“MAC 字符串”作为设备唯一 Key。更可靠的做法是用 BLE 的配对绑定机制扫描到设备后发起配对配对成功系统会保存 Bonding 信息绑定后的 Identity Address 才会稳定下来。Android 侧可以通过BluetoothDevice.getBondState()判断绑定状态配合getAddress()使用但要注意这个地址是否来自绑定信息。iOS 侧则是依赖系统的CBPeripheralManager和蓝牙配对数据库。总结成经验法则能依赖绑定成功后拿到的 Identity Address就不要依赖扫描阶段的瞬时地址。6. 常见问题与排查实录6.1 设备 MAC 地址为什么一直在变这个问题是被问得最多也是最容易自己解决的。先抓包或者用手机扫描日志确认地址类型。如果是 Non-resolvable Private Address那它本来就会周期变你要做匿名广播就别把这个当设备 ID如果是 Static Random Address但每次重启都变说明固件初始化时没有固化地址需要把地址保存到 Flash如果是 Resolvable Private Address变化是正常的但需要在配对后保存 IRK 和 Identity Address否则无法回连。我遇到过一个实际案例一批传感器使用某个国产芯片初始固件里没有做地址固化每次 OTA 重启后地址都变。后台服务器每 5 分钟收到一条“新设备上线”的消息实际只有 30 台设备后台却录了上千条记录。最后定位到原因就是芯片 SDK 默认会调用一次随机地址生成而不是读取保留寄存器。给协议栈打了一个补丁把首次生成的 Static Random Address 写入 Flash问题立刻解决。6.2 为什么应用层拿到的 “MAC” 和抓包看到的不一样这个也不少见。手机系统拿到的地址和空中抓包看到的链路层地址有时会不一样。原因有两个一个是系统做了地址随机化扫描阶段用随机地址连接成功后换成身份地址另一个是抓包工具和手机日志导出的时间点不一致刚好跨越了地址更新周期。尤其在 iOS 平台上App 根本拿不到链路层地址看到的 UUID 是系统映射出来的不代表实际空中的地址。所以排查时建议把“空中地址”“系统接口地址”“上层应用 UUID”这三个概念区分开。它们可能相同也可能不同但都对。不要一发现不一致就认为抓包工具坏了或者系统出 Bug。经验是凡是要对设备做长期识别都优先依赖配对绑定之后的 Identity Address或者设备广播数据里自定义的设备序列号而不是依赖链路层 MAC。6.3 地址套路排查速查表现象可能原因处理思路同一设备扫描地址周期变化使用了 Non-resolvable / Resolvable Private确认场景是否需要隐私保护每次重启地址都变Static Random 未固化将地址存入 Flash 或配置为固定值手机 App 显示 MAC 与抓包不同系统层随机/隐私策略以绑定后的 Identity Address 为准白名单过滤失效地址变化后未匹配改用 IRK / 绑定信息过滤设备数量重复入库把隐私地址当设备 ID使用广播数据中的自定义序列号前缀和厂商对不上手动配置或伪 OUI结合服务 UUID 等综合判断把这张表存下来下次遇到地址相关的问题先按现象对号入座能省去大量周旋时间。6.4 再说一点关于绑定和隐私的坑最后提醒一件事如果你的设备支持配对绑定并且开启了隐私功能一定不要只保存对端的瞬时地址。正确做法是在配对完成事件里读取 Identity Address 和 IRK保存到 Flash。后续不管对端换多少次 RPA你都能通过 IRK 解析出来。很多低功耗手环项目设备在手机侧表现为“连接一次后下次扫描不到”十有八九就是因为只存了瞬时地址没有存绑定信息。我在实际项目里见过最隐蔽的问题某个模组默认开启了“Privacy 1.2”广播地址每 15 分钟换一次但调试工具的过滤规则还写死了 Public Address结果现场调试时设备时远时近、时有时无排查了整整一天最后把抓包里面的地址类型字段一展开真相大白。从那以后我把“先看地址类型”写进了团队调试规范任何蓝牙连接疑难杂症第一件事就是抓包第二件事就是看 AdvA 的地址类型。这个习惯比记忆任何 API 都管用。