但凡在产线上待过的嵌入式工程师,都遇到过这种活儿:拿来一批板子,要把每一块的蓝牙MAC地址抄下来,录进MES系统或者贴成标签。最常见的做法是烧一个测试固件,跑起来后通过串口把地址打出来——听起来不难,但真到了量产或者返修环节,你就会发现这套流程又慢又容易错。客户退回的板子十有八九固件已经刷死,你根本没法让它跑打印程序。
我现在更习惯直接用调试器去读:J-Link连上SWD,脚本写好之后,不管Nordic还是Silicon Labs,不管芯片里有没有固件,都能把出厂写死的唯一地址抠出来。这篇文章就把我常用的两种方式完整讲一遍——SEGGER官方的JLink Commander命令行,和基于Python的PyLink(pylink-square封装库)——覆盖Nordic nRF52的FICR寄存器和Silicon Labs EFR32的Device Info页两条技术路线,再加上我写产线批量读取脚本时踩过的坑。如果你想在产线上快速采集蓝牙MAC,或者手里有一块没法启动的板子想查出它的身份,这篇文章可以直接照着抄。
1. 为什么会有"读MAC地址"这个需求:产线录入、返修定位、身份追溯
1.1 三种最常见的现实场景
先说实话,绝大多数人第一次想从调试器读MAC,都不是为了炫技,而是被逼的。我归纳下来基本是下面三种情况。
第一,产线信息采集。每一台出厂的蓝牙设备都要有唯一的MAC,这个地址要写进MES、贴成条码、或者和序列号绑定。如果等固件跑起来再通过串口回传,产线每个工位要多花几秒到几十秒不等,而且串口线的插拔、波特率的配置、上位机协议的解析都是麻烦事。用J-Link直接读,等于把"读身份信息"这件事从应用层下沉到了调试链路,速度快,也不需要目标板上有可运行的固件。
第二,返修和售后。板子退回来说连不上手机,或者根本开不了机。这时候你没法跑测试固件,但只要SWD口还在、芯片没被锁,J-Link就能把出厂信息读出来,快速确认这块板子的MAC、批次、测试记录。我甚至干过一件事:一批板子标签贴错,客户拿到的实物和系统里登记的MAC对不上,最后只能用J-Link一块一块读出来重新建账。
第三,同硬件平台做多型号。外壳不同、固件不同,但PCB一样,这个时候系统里想把"身份标识"和"外观版本"关联起来,就得从芯片底层拿唯一ID,而MAC就是最现成的标识。
1.2 "从代码里读"和"从调试器读"的本质区别
很多人会问:MAC地址在固件里明明能读,为什么要绕一圈用调试器?关键在于依赖关系。
从代码里读MAC,依赖的是"CPU能跑、程序没崩、串口/通信通道还在"这一整条链路。产线上只要有一个环节出问题,你就读不到。而从调试器读,走的是ARM Cortex-M的调试接口(SWD/JTAG),J-Link通过DAP直接访问总线地址空间,不需要CPU执行你的指令。所以哪怕Flash是空的、应用程序起不来、甚至bootloader坏了,只要调试口活着,FICR/DI页这些出厂信息都能读。
但有一个前提条件必须说清楚:前提是调试口没有被安全机制锁住。Nordic有APPROTECT,Silicon Labs有debug lock/ACL(访问控制),一旦项目里把这些打开了,J-Link虽然能识别到内核,但读内存会失败或者读到全FF。这个我在后面踩坑章节专门讲。
1.3 MAC到底是谁写进芯片的
这块不搞清楚,后面读出来你也不知道该怎么用。Nordic的方案里,每个nRF51/nRF52芯片出厂时,芯片内部FICR(Factory Information Configuration Registers,出厂信息配置寄存器)区域就被写好了48位的设备地址,用户程序只能读,不能改。也就是说,哪怕你用的是完全空白的芯片,地址也已经存在了。
Silicon Labs的EFR32系列类似,芯片出厂时在Device Info页(DI页)写了一个EUI标识,后面做蓝牙地址时从EUI派生。和Nordic不同的是,Silicon Labs允许用户在量产时通过MFG区覆盖默认地址,所以会出现"读到的出厂EUI"和"设备实际广播地址"不一致的情况。这一点在做生产管理时要心里有数,后面第五章细说。
2. 动手前的接线和识别:SWD引脚定义与板载调试器的区别
2.1 SWD四根线搞定
先讲接线。J-Link调试口支持JTAG和SWD两种协议,读MAC这种活儿用SWD就够了。SWD最少只需要四条线:SWDIO(数据)、SWCLK(时钟)、GND(地)、VTref(参考电压检测)。有些场合RESET线也建议接上,尤其是目标板跑在低功耗或者调试口被复用的时候,按住复位再连接能提高成功率。
常见的2.54mm间距6-Pin SWD座定义如下:
| Pin | 名称 | 方向 | 说明 |
|---|---|---|---|
| 1 | VTref | 输入 | J-Link用它检测目标板电压,不供电 |
| 2 | SWDIO | 双向 | 数据线,连接目标板SWDIO/TMS |
| 3 | GND | - | 必须和目标板共地 |
| 4 | SWCLK | 输出 | 时钟线,连接目标板SWCLK/TCK |
| 5 | RESET | 输出 | 可选,建议接上 |
| 6 | SWO | 输出 | 可选,调试串口/ITM用,读MAC用不到 |
2.2 开发板、评估板和自制板的接线差异
Nordic的nRF52 DK、nRF5340 DK这类开发板很有意思,板上自带了一个J-Link调试器,你插上USB线,PC端枚举出来的就是一个J-Link设备,其实不用额外接SWD线。USB口同时提供了虚拟串口(CDC),很多新手容易把"J-Link的USB"和"目标板串口"搞混,实际它们都是同一个USB口分出来的多个复合设备。
Silicon Labs的WSTK无线开发套件也自带调试器,而且这个调试器本身是J-Link内核的。你既可以用官方的Simplicity Studio/Simplicity Commander,也可以直接用SEGGER的JLink Commander连接,两者不冲突。WSTK板上Debug Connector要用配套的线连到目标板,注意别插反。
如果你用的是自制板,通常板上会预留SWD 4-Pin或6-Pin座子,直接对应上面的表格接线。这里我强烈建议:RESET线别省,特别是EFR32这类上电后默认跑bootloader的芯片,J-Link连接过程需要控制复位线。
2.3 VTref、VCC和GND:最容易搞错的三根线
接线里面最经典的坑,是把J-Link的Pin1(VTref)当成供电脚。VTref是"电压参考",它只是检测目标板的电平,以便J-Link知道IO阈值该用多少,它不负责给板子供电。如果你在小板上看到VTref显示0V,不用怀疑,就是目标板没上电或者共地没接好。
还有一点,有些J-Link转接板上会额外标一个VCC脚,那是真的对外供电的。给目标板供电之前,先确认目标板需要的电流,J-Link的LDO能力很有限(通常几十到一百毫安级别),带一个蓝牙SoC跑起来绰绰有余,但你要是带了传感器矩阵、屏幕、电机,分分钟把调试器拉崩。产线上能用外部电源就尽量用外部电源,省得出各种玄学问题。
3. 工具链准备:J-Link驱动、JLink Commander和PyLink的安装
3.1 安装SEGGER J-Link软件包
第一步永远是装驱动和软件包。去SEGGER官网下载J-Link Software and Documentation Pack,这个包跨平台,Windows、Linux、macOS都提供。安装完成后,Windows下会有JLink.exe(命令行)、JLinkConfig.exe(配置工具)等;Linux下对应的是JLinkExe和JLinkConfig。
版本选择上有个经验:如果你是正版J-Link(EDU、BASE、PRO等),直接装最新版放心用。如果你手上是市面上买的兼容版本,升级驱动前先掂量一下,新版DLL有时候会检测固件版本,兼容版升级后可能出现"cannot open device"或者提示固件需要更新,而更新固件不是所有兼容版都能成功的。我见过不少同事手痒点了升级,结果调试器变砖,只能重新刷固件。产线工位上的电脑,驱动版本一旦稳定就别乱动。
Windows 11用户注意,老版本驱动在新系统上签名可能会出问题,直接装SEGGER官网当前最新版,基本没有兼容性问题。
3.2 JLink Commander核心命令速查
JLink Commander是SEGGER自带的命令行工具,调试器连接、内存读写、Flash编程都能干。读MAC地址只需要下面几个命令:
| 命令 | 示例 | 作用 |
|---|---|---|
| device | device nRF52840_xxAA | 选择目标芯片型号 |
| si | si SWD | 设置调试接口为SWD |
| speed | speed 4000 | 设置SWD时钟,单位kHz |
| connect | connect | 建立连接 |
| mem8 | mem8 0x100000A4 8 | 按字节读内存 |
| mem32 | mem32 0x100000A4 2 | 按32位字读内存 |
| reset | reset | 复位目标板 |
| exit | exit | 退出 |
设备型号的填写是个小门槛。JLink的device参数不是随便填个芯片系列就行的,它决定J-Link怎么初始化目标。Nordic这边常用的设备名有nRF52832_xxAA、nRF52833_xxAA、nRF52840_xxAA这样带后缀的完整型号;Silicon Labs那边可以用EFR32BG22、EFR32BG13、EFR32MG21这类系列名,你也可以填完整型号(比如EFR32BG22C224F512IM40)。填错了最常见的结果是连接时报错或者读出来的内存全是FF。如果你不清楚芯片具体变体,先看芯片丝印,一般会印出完整型号。
3.3 PyLink(pylink-square)解决了什么问题
命令行虽然能用,但在产线自动化场景下,你总不能让工人去敲命令吧?这时候PyLink就派上用场了。PyLink这里指的是GitHub上开源的pylink-square库,它是SEGGER J-Link DLL的Python封装,PyPI直接安装:
pip install pylink-square装完之后,你写Python脚本就能控制J-Link:打开DLL、设置SWD接口、连接指定芯片、读写内存、控制复位,全部搞定。它最大的价值是能把"读MAC"这件事变成一个可以被产线程序调用的函数,还能把结果直接写进CSV、JSON或者MES系统的API。
先给一个最简连接测试,确保你的环境是通的:
from pylink import JLink, Swd jlink = JLink() jlink.open() jlink.set_tif(interface=Swd()) # 设置SWD接口 jlink.set_speed(4000) # 4MHz jlink.connect('nRF52840_xxAA') # 设备名 print('已连接,目标电压:', jlink.target_voltage()) jlink.close()如果这一步报错,优先检查三件事:SEGGER驱动装没装、J-Link的USB驱动识别没有、目标板有没有上电。pylink-square打开DLL时如果找不到DLL,Windows下通常是因为没装SEGGER软件包,Linux下需要把libjlinkarm.so所在目录加到LD_LIBRARY_PATH。
4. Nordic nRF52:从FICR寄存器挖出出厂地址
4.1 FICR寄存器地图
Nordic把出厂信息统一放在FICR区域。nRF52系列FICR基地址是0x10000000,其中设备相关的三个寄存器:
| 寄存器 | 地址 | 说明 |
|---|---|---|
| DEVICEADDR[0] | 0x100000A4 | 48位设备地址的低32位 |
| DEVICEADDR[1] | 0x100000A8 | 48位设备地址的高16位(仅低16位有效) |
| DEVICEADDRTYPE | 0x100000AC | 地址类型指示 |
DEVICEADDRTYPE最低位的含义很关键:为0时表示这是Public Address(公共地址),为1时表示应该按Random Static Address(随机静态地址)来使用。这个位会直接影响你后面把地址录进系统时的标注方式,后面踩坑章节再展开。
nRF53(比如nRF5340)和nRF52不一样,FICR基址在0x00FF0000,寄存器偏移也要查对应Product Specification,别直接把nRF52的地址套上去。
4.2 用JLink Commander读取
先上命令行。插好J-Link和目标板后,运行JLink.exe,依次输入:
device nRF52840_xxAA si SWD speed 4000 connect mem32 0x100000A4 2 mem32 0x100000AC 1 exitconnect之后J-Link会提示连接成功,并打印目标电压和内核类型。mem32命令一次读出两个32位字,也就是DEVICEADDR[0]和DEVICEADDR[1]。假设输出是这样的:
100000A4 = 5A53873C 100000A8 = 00002E7A 100000AC = 00000001先读到的两个32位值,devaddr[0]=0x5A53873C 是低32位,devaddr[1]=0x00002E7A 的高16位是0x2E7A。那么这个芯片的48位地址原始值就是:
device_addr = (0x2E7A << 32) | 0x5A53873C = 0x2E7A5A53873C但是注意,JLink显示的32位值在小端环境下,内存里字节顺序是反着排的。实际出现在内存里的8个字节是3C 87 53 5A 7A 2E 00 00(前4字节是0x100000A4开始的DEVICEADDR[0],后4字节是0x100000A8开始的DEVICEADDR[1]),这8个字节的前6个字节才是BLE地址的直接体现。怎么理解?我下一节专门讲。
4.3 用PyLink读FICR并解析
命令行适合单块板子手动看,要批量还得用PyLink。读取代码很简单:
from pylink import JLink, Swd jlink = JLink() jlink.open() jlink.set_tif(interface=Swd()) jlink.set_speed(4000) jlink.connect('nRF52840_xxAA') addr0 = jlink.memory_read32(0x100000A4, 1)[0] addr1 = jlink.memory_read32(0x100000A8, 1)[0] addr_type = jlink.memory_read32(0x100000AC, 1)[0] # 拼成48位值 device_addr = (addr1 & 0xFFFF) << 32 | addr0 # 转成6字节数组(内存顺序,即BLE协议栈的byte数组顺序) raw = device_addr.to_bytes(6, 'little') # 人眼习惯的大端显示 mac_str = ':'.join(f'{b:02X}' for b in reversed(raw)) print('原始48位值 : 0x%012X' % device_addr) print('内存字节 : %s' % ' '.join(f'{b:02X}' for b in raw)) print('MAC字符串 : %s' % mac_str) print('地址类型 : %s' % ('Random Static' if addr_type & 1 else 'Public')) jlink.close()输出示例:
原始48位值 : 0x2E7A5A53873C 内存字节 : 3C 87 53 5A 7A 2E MAC字符串 : 2E:7A:5A:53:87:3C 地址类型 : Random Static这里的memory_read32返回32位整数列表。如果你的pylink版本较新,也可以用memory_read8接口直接按字节读,更直观:
raw = bytes(jlink.memory_read8(0x100000A4, 8)) # 直接读8字节4.4 字节序:读出来的数据为什么要倒过来
这是读Nordic MAC最容易翻车的地方,我必须多花点篇幅。BLE设备地址本身是一个48位数,但无论在寄存器里还是空中报文里,都是"低字节在前"(little-endian)传输的。Nordic的FICR把地址按这个顺序存进内存:你从0x100000A4开始按字节读,读到的第一个字节就是BLE地址的最低字节,第六个字节才是最高字节。
如果你直接把读到的6字节按顺序用冒号拼出来,得到的是"低字节在前"的地址,也就是BLE扫描工具里看到的那个顺序。但大家平时写MAC、贴标签、录数据库,习惯的是类似Wi-Fi MAC那种"高字节在前"的大端写法。所以必须把字节反一遍。
判断你有没有反过来的方法很简单:找一块已经通过手机蓝牙扫描过的板子,nRF Connect里能看到它广告时的地址,把这个地址和脚本输出比一比就知道了。我刚做这个工具的时候,第一次就是没反转,导致录进去的500台数据全部对不上,后来写了个校验函数才救回来。
5. Silicon Labs EFR32:Device Info页里的EUI与蓝牙地址换算
5.1 DI页长什么样
Silicon Labs的EFR32系列在Flash高端有一块专门存出厂信息的区域,叫Device Info页(DI页)。绝大多数EFR32芯片(包括xG1、xG13、xG14、xG21、xG22等常见系列)DI页基地址是0x0FE08100,具体字段在不同参考手册里略有差异,但下面几个是通用的:
| 偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0x000 | 8字节 | UNIQUE_EUI_0_7 | 8字节EUI(类似EUI-64) |
| 0x008 | 8字节 | UNIQUE_EUI_48 | EUI-48(低6字节用于蓝牙地址) |
| 0x010 | - | MFG_INFO | 厂商信息字段 |
这些字段是出厂就写好的,用户程序只读。和Nordic的FICR一样,DI页不需要Flash里有任何用户代码就能通过J-Link读出来。
5.2 用JLink Commander读取EUI
连接流程和Nordic一样,设备名换成Silicon Labs的型号,比如EFR32BG22:
device EFR32BG22 si SWD speed 4000 connect mem8 0x0FE08100 8 mem8 0x0FE08108 8 exitmem8按字节显示,读出来的就是8字节EUI和8字节EUI-48字段。假设读出来是这样:
0FE08100 = 00 0B 57 34 56 78 9A BC 0FE08108 = 00 0B 57 34 56 78 9A BC这只是个例子,每块芯片的EUI都不同,前缀才代表厂商。
5.3 用PyLink读取并换算成蓝牙MAC
Python读取DI页的代码和Nordic那边几乎一样,只是地址变了:
from pylink import JLink, Swd jlink = JLink() jlink.open() jlink.set_tif(interface=Swd()) jlink.set_speed(4000) jlink.connect('EFR32BG22') # 读DI页:EUI-64 和 EUI-48 eui_64 = bytes(jlink.memory_read8(0x0FE08100, 8)) eui_48 = bytes(jlink.memory_read8(0x0FE08108, 8)) print('EUI-64 : %s' % ' '.join(f'{b:02X}' for b in eui_64)) print('EUI-48 : %s' % ' '.join(f'{b:02X}' for b in eui_48)) jlink.close()但这里有个必须注意的换算问题:DI页里的EUI字节顺序,和蓝牙广播时使用的地址字节顺序并不总是一致。Silicon Labs的蓝牙协议栈在生成默认公共地址时,会对EUI做字节反转和位处理(比如把本地区管理位设置正确)。更省事的办法是:直接跑一次官方蓝牙示例,把sl_bt_system_get_bt_address打出来的地址和J-Link读到的原始字节放到一起对比一次,你就知道这块芯片的换算关系了。一般情况下,把EUI-48的低6字节反转后,就是广播地址的常规写法。
提醒一下:现在很多EFR32项目在量产时会通过MFG token自定义蓝牙地址,一旦固件里调用过设置地址的接口,或者MFG区被写入自定义数据,设备实际广播的MAC就和出厂EUI无关了。这时候J-Link从DI页读到的EUI只能作为"芯片身份标识",不能作为"产品MAC"。做产线系统设计的时候,最好一开始就明确:到底以EUI为准还是以固件运行为准,避免两套数据打架。
5.4 WSTK上的特殊注意点
如果你用的是Silicon Labs官方的WSTK开发板,板上调试器默认的默认固件是兼容J-Link的,理论上JLink Commander直接认。但有个细节:WSTK的调试器经常同时被Simplicity Studio占用,两个工具抢同一个调试器资源会导致连接不上。遇到这种情况,要么关掉Studio,要么在设备管理器里确认调试器没有被占用。
6. 一个脚本通吃两家芯片:PyLink批量读取实践
6.1 脚本功能设计
读单个芯片的代码我们上面已经写了,但实际生产环境的需求往往更刁钻:一会儿来一批Nordic,一会儿来一批Silicon Labs;工人不想操作命令行;结果要么存CSV要么推送MES;有时候一个工位还插着好几个J-Link。所以我建议把读取逻辑封装成一个通用脚本,用参数区分厂商。
脚本设计几个关键点:
- 通过--vendor参数选择Nordic还是Silicon Labs,设备名和寄存器地址跟着切换
- 通过--serial指定J-Link的序列号,多调试器环境不打架
- 支持--format csv,方便直接进Excel或者MES
- 每次读完自动复位目标板,让下一道工序的设备处于干净状态
6.2 完整代码
import argparse import csv import time from pylink import JLink, Swd # 芯片配置 CHIP_CONFIG = { 'nordic': { 'device': 'nRF52840_xxAA', 'addr_regs': [0x100000A4, 0x100000A8], 'addr_type_reg': 0x100000AC, }, 'silabs': { 'device': 'EFR32BG22', 'eui_64_addr': 0x0FE08100, 'eui_48_addr': 0x0FE08108, }, } def read_nordic_mac(jlink): addr0 = jlink.memory_read32(CHIP_CONFIG['nordic']['addr_regs'][0], 1)[0] addr1 = jlink.memory_read32(CHIP_CONFIG['nordic']['addr_regs'][1], 1)[0] addr_type = jlink.memory_read32(CHIP_CONFIG['nordic']['addr_type_reg'], 1)[0] raw = ((addr1 & 0xFFFF) << 32 | addr0).to_bytes(6, 'little') mac = ':'.join(f'{b:02X}' for b in reversed(raw)) atype = 'Random Static' if (addr_type & 1) else 'Public' return mac, atype def read_silabs_mac(jlink): eui_48 = bytes(jlink.memory_read8(CHIP_CONFIG['silabs']['eui_48_addr'], 8)) # 按常见换算:取EUI-48低6字节,反转显示 mac = ':'.join(f'{b:02X}' for b in reversed(eui_48[:6])) return mac, 'EUI-derived' def read_mac(vendor, serial_no=None): jlink = JLink() jlink.open(serial_no=serial_no) jlink.set_tif(interface=Swd()) jlink.set_speed(4000) if vendor == 'nordic': jlink.connect(CHIP_CONFIG['nordic']['device']) mac, atype = read_nordic_mac(jlink) else: jlink.connect(CHIP_CONFIG['silabs']['device']) mac, atype = read_silabs_mac(jlink) jlink.reset() time.sleep(0.2) jlink.close() return mac, atype def main(): parser = argparse.ArgumentParser(description='通过J-Link读取蓝牙MAC') parser.add_argument('--vendor', choices=['nordic', 'silabs'], required=True) parser.add_argument('--serial', type=int, default=None, help='J-Link序列号') parser.add_argument('--format', default='text', choices=['text', 'csv']) parser.add_argument('--output', default=None, help='CSV输出文件') args = parser.parse_args() mac, atype = read_mac(args.vendor, args.serial) if args.format == 'csv': import sys if args.output: with open(args.output, 'a', newline='') as f: csv.writer(f).writerow([mac, atype, time.strftime('%Y-%m-%d %H:%M:%S')]) else: csv.writer(sys.stdout).writerow([mac, atype]) else: print(f'MAC: {mac} 类型: {atype}') if __name__ == '__main__': main()6.3 产线执行节奏与注意事项
脚本跑一次的时间很短:连接大概几百毫秒,读内存几个字节是微秒级的事,加上复位和退出,单块板子两秒内绝对能完成。真正拖时间的是人工放板、插线、等待稳定。所以产线工位常见做法是做成"放好板子按一下回车"的交互,而不是让工人盯着命令行输出。
另外一个容易忽略的点:脚本结束时的复位动作很有必要。你不复位,目标板可能停在调试暂停状态,下一道工序如果要用串口或者烧录器,就会发现设备行为异常。我一开始没加这行reset,产线反馈"烧完MAC之后板子必须重新上电才能工作",排查半天才知道是调试器退出时把芯片挂住了。
6.4 多J-Link并行
一条产线往往不止一个工位。如果你在一台电脑上接了多个J-Link,pylink默认打开的是第一个找到的设备,这就不行了。解决办法是给每个工位固定一个J-Link,把序列号写死在脚本或者配置里。
怎么查序列号?运行JLink.exe,启动横幅里就会打印序列号;或者在JLinkConfig工具里看。然后在上面代码里通过jlink.open(serial_no=xxxx)指定即可。
注意:序列号不要靠猜,每个J-Link的唯一序列号在调试器外壳标签上也有印,产线固定工位后最好录入配置文件,避免因为插错调试器导致整个工位读错数据。
7. 实战踩坑:连接失败、字节序反了、地址类型分不清
7.1 Target voltage not found
这是J-Link最常见的报错。看到这个提示,先别怀疑调试器坏了,按顺序排查:
- 目标板有没有上电?J-Link只检测VTref,不给板子供电。
- VTref线(Pin1)有没有接到目标板的电源脚?
- GND共地了没有?共地是SWD能通信的前提。
- 杜邦线是不是接触不良?产线上插拔久了,母头会松,换一根就好。
我见过最离谱的一次,是新来的同事把VTref接到了SWCLK上,报错信息一模一样,折腾了好半天。
7.2 Cannot connect to target
能显示目标电压但连不上内核,问题多半出在下面几个方向:
- SWDIO和SWCLK接反了。这个听起来低级,但换了非标座子、用了不同颜色的线之后,非常容易发生。
- 目标板在上电瞬间被低功耗代码拉进睡眠,J-Link来不及握手。对症的办法是按住RESET,启动连接的同时放开。这也是前面强调RESET线要接的原因。
- 代码里把SWD引脚复用成了GPIO。很多量产固件为了省功耗会关闭调试口,遇到这种板子只能靠复位窗口期连接,或者在固件里留一个不关闭SWD的测试版本。
- 芯片安全锁被打开。Nordic写入了APPROTECT,或者Silicon Labs的debug lock被置位,J-Link连不上或者能连但读不到内容。
这里必须提醒:不要轻易为了读MAC就去执行全片擦除解锁。解锁等于清空整个Flash,如果板子里的固件没有备份,你就把它变成砖了。正规做法是找研发要一个"带调试许可"的固件,或者走芯片厂商的解锁流程。
7.3 字节序踩坑实录
字节序这个坑,我前面详细讲过,但值得再强调一次,因为它太致命了。有一次批量读Nordic的板子,我直接在脚本里把raw按顺序拼成了MAC,看起来也挺像那么回事,每块板子地址还都不一样。结果第二天产线反馈,贴到包装上的MAC和手机扫描到的不一致——不是完全对不上,而是每个字节前后颠倒了。
从那以后我定了一条规矩:任何新写的读取脚本,第一件事不是批量跑,而是拿三块已知设备做对照。手机nRF Connect里能看到广播的MAC,脚本读出来的MAC要能和它对应上。两边都正确的标准是"字符完全一致,且前缀符合芯片厂商的OUI"。
7.4 Random Static还是Public
这个问题在Nordic平台尤其要注意。FICR的DEVICEADDRTYPE告诉你怎么解释地址:如果芯片出厂时被配置成Random Static Address,那么业务系统里如果把它当Public Address用,手机上看到的地址类型是带"随机"标志的,两个系统里存的MAC虽然字节一样,但语义不同。如果你们的后台系统做了地址类型校验,就会出现"MAC在黑名单但设备还能连"这类奇怪问题。
我在读脚本里把地址类型一并输出,就是为了避免这种歧义。至少在现场,工人和测试人员能一眼看出这块板子是公共地址还是随机地址,减少沟通成本。
7.5 其他容易忽略的细节
最后补充几个小细节,都是实操里总结的:
- 读完后一定要用reset让芯片正常复位,否则调试器退出后目标板可能停在挂起状态。
- 尽量用外部电源给目标板供电,J-Link的供电能力有限,别为了省一根线引入奇怪的故障。
- 产线工位的J-Link驱动版本要保持一致,别让不同班次的电脑驱动版本差太多,否则连接稳定性会有差异。
- 如果目标板和J-Link距离超过20厘米,SWD时钟降到1000kHz以下更稳,杜邦线长了信号完整性问题会冒出来。
最后再说一个实际体会:这套"调试器直读MAC"的方案,真正落地之后最大的收益不是省了多少时间,而是让产线的数据链路变干净了。以前靠串口打印,固件一更新,输出的格式可能就变,解析脚本跟着改,烦不胜烦。现在从芯片出厂信息里读,跟固件版本无关、跟应用逻辑无关,只要调试口在,数据格式就是固定的。如果你正准备搭产线的MAC采集工具,我建议一开始就把Nordic和Silicon Labs这两家的读取逻辑都做进去,别等到换产线时再补脚本。