☰
使用J-Link和pylink直接读取Nordic与EFR32的MAC地址
2026/10/5 8:09:46 网站建设 项目流程

做蓝牙开发这几年,我发现自己被问得最多的一类问题不是怎么配对、怎么广播,而是——“我这个板子的MAC地址是多少?”设备还没烧固件、不开机、甚至程序压根没写,就急着要那6字节的蓝牙MAC。一般人的第一反应是跑例程,把地址打印出来。但产测、备货、写标签、绑定SN的时候,哪有时间等固件跑起来。正确做法是拿调试器直接读芯片里的出厂信息区,J-Link搞定Nordic,配合pylink脚本把流程自动化,Silicon Labs的EFR32系列也一样能读到。这篇文章就把我实际操作的整个思路、寄存器地址和避坑点都捋清楚,给做产测导入、固件开发、工装软件的朋友一个直接能抄的方案。

这个内容不是只讲“在哪敲一条命令”,还会说明为什么地址要倒着读、为什么有的芯片读出来是8个字节,以及连接不上、读出来全是F这些常见问题到底出在哪。无论你手头是nRF52832、nRF52840,还是EFR32BG22、EFR32MG21,只要支持SWD接口、没有被调试锁死,都可以用这套方法快速拿到MAC地址。

1. 项目背景:什么场景下需要调试器直接读MAC

1.1 产测、贴标、绑定SN时的刚需

单台设备看固件日志拿MAC很容易,但一上产线就完全不是一回事。流水线上的板子往往是空片,或者只烧了Bootloader,主程序还没下载,这时候你想通过串口打印MAC是根本做不到的。更常见的场景是组装厂需要在产品外壳或包装上贴MAC标签,标签数据是提前从PCBA板子上读出来的,总不能每块板子都先烧一套带测试固件的程序,读完再擦掉,那样效率太低,还容易把固件版本搞混。

我见过不少工厂的处理方式:用一个支持J-Link的工装,把板子的SWD测试点夹住,脚本自动读取芯片出厂MAC,然后写入产测系统,生成标签和测试记录,整个过程1到2秒一块板。这种方案的前提就是能够通过调试口直接访问芯片内部的出厂信息区,而不是依赖应用层固件。所以,掌握用J-Link直接读MAC的方法,从开发调试到工厂导入都用得上。

1.2 MAC地址在芯片里到底存哪儿

不同芯片厂商存储出厂MAC的位置完全不一样,这也是很多人一开始找不到地址的原因。Nordic的nRF51、nRF52系列把蓝牙MAC放在FICR(Factory Information Configuration Registers)区域,这个区域是芯片出厂时烧录的一次性配置区,用户程序无法修改,非常稳定。MAC相关的寄存器有两个:DEVICEADDRTYPE和DEVICEADDR。DEVICEADDR的地址是0x100000A8,长度48位(6字节),就是蓝牙设备地址本身;DEVICEADDRTYPE地址是0x100000A4,表示这个地址的类型,比如公共地址还是随机静态地址。

Silicon Labs的EFR32系列则不同,它把EUI-48(也就是MAC)放在Flash信息页(Info Page)的Device Info结构体里,我的实际经验里经常从0x0FE08130这个偏移开始读,长度为6字节。不同系列的具体偏移略有差异,但通常就在0x0FE08130附近,读不到的时候可以把整个信息页拉出来人工比对,几秒钟就能定位。

1.3 J-Link和pylink为什么能读到这些数据

J-Link通过SWD(Serial Wire Debug)接口访问芯片内部的调试访问端口,进而读取系统总线上的任意内存映射地址。FICR和Info Page都映射在芯片的地址空间中,所以调试器可以直接读。这里的关键点是:读取过程不需要CPU运行任何程序,芯片上电后SWD接口就是可用的,除非启用了调试锁。

pylink是SEGGER官方提供的Python绑定库,它封装了J-Link的DLL接口。你可以把pylink理解为:用Python代码来控制J-Link,就像你在J-Link Commander里手动敲命令一样。安装后只需要几行代码就能连接目标芯片、读取内存、控制CPU暂停和复位。用pylink而不是纯Commander的好处是容易做循环,一块板读完后自动读下一块,还能把结果直接写入Excel或数据库。

2. 开工前的准备:驱动、接线与工具链里的坑

2.1 J-Link驱动安装与版本核对

官方驱动从SEGGER官网下载,在Windows下安装后会自动包含J-Link Commander和DLL库,pylink就是靠这个DLL工作的。Win11下安装时需要注意:装完最好重新拔插一次J-Link,并且在设备管理器里确认枚举出来的是“J-Link”而不是带黄色感叹号的未知设备。如果驱动装不上,多半是系统强制驱动签名导致的,可以先试试以管理员身份运行安装包。

版本选择上,我建议直接装最新版,但要注意一个特别常见的坑:如果你用的是市面上某些非正规渠道的J-Link V9,新版驱动的DLL校验很严格,主动弹出“J-Link is a clone”之类提示。遇到这种情况,要么换正版工具,要么老老实实用旧版本驱动。如果是公司产线,我强烈建议采购正版J-Link或者Segger的批量授权,山寨工具在产线上出问题排查起来会让人崩溃。

另外,和你电脑上已有的工具链版本也会有冲突。比如你先装了Nordic的nrfjprog,它自带了一版J-Link驱动,之后你又装SEGGER新版驱动,两者可能会覆盖彼此的DLL。症状就是J-Link Commander能连,但nrfjprog报错;或者反过来。解决办法是全部重装同一版本的SEGGER驱动,再装nrfjprog时不要勾选“Include SEGGER J-Link driver”之类的选项。

2.2 SWD接口定义与线序核对

J-Link的接口有两种常见物理形态:一种是20pin JTAG排针,另一种是10pin 1.27mm SWD排针。实际读MAC只需要4根线:SWDIO、SWCLK、GND、VTref。VTref是从目标板采样回来的参考电压,用于电平匹配,一定要接,否则J-Link无法判断目标板电压。

20pin接口的常用定义如下:

Pin名称说明
1VTref目标板参考电压
2TMS / SWDIOSWD数据线
3GND地
4TCK / SWCLKSWD时钟线
6TDO / SWO可接,读MAC不需要
10nRESET复位信号,建议接

10pin接口则更紧凑,常见于Nordic DK板、Silicon Labs开发板以及很多量产的测试点设计。它的定义一般是这样:1脚VTref,2脚SWDIO,3脚GND,4脚SWCLK,5脚GND,6脚SWO,10脚nRESET。不同厂家的丝印可能略有差异,但万变不离其宗,只要保证SWDIO、SWCLK、地、参考电压四根线没错就行。

我踩过的一个坑是:目标板上有两个SWD接口,一个在板载调试器上,另一个是外部调试器的排针,两者接反了。如果你用板载J-Link,读的是板载调试器连接的MCU;你想用外部J-Link读板上的目标MCU时,记得先断开板载调试器的连接,否则总线有冲突。

2.3 pylink库安装与验证

pylink库用pip安装就行,推荐在虚拟环境里装,避免和系统Python环境互相污染。

pip install pylink

安装完成后,可以先用一小段代码验证J-Link是否被识别:

import pylink jlink = pylink.JLink() jlink.open() print("J-Link serial:", jlink.serial_number) jlink.close()

这段代码如果正常执行,说明驱动和pylink库都能工作。如果报错提示找不到DLL,检查一下SEGGER驱动是否安装,以及Python进程是32位还是64位,pylink需要和J-Link DLL的位数匹配。我自己有一次在Python 32位环境下跑,J-Link DLL是64位的,折腾了半天才反应过来,Python环境统一用64位能省掉大部分麻烦。

3. Nordic篇:nRF52系列读取MAC的完整操作

3.1 用J-Link Commander命令行直接读DEVICEADDR

最快速的方式是用J-Link Commander手动操作。连接nRF52832时,命令行这样写:

JLink.exe -device nRF52832_xxAA -if SWD -speed 4000 -autoconnect 1

不同芯片的型号字符串可以在SEGGER支持列表里查到,比如nRF52840是nRF52840_xxAA,nRF51822是nRF51822_xxAA。进入J-Link交互界面后,执行:

mem8 0x100000A8 6

这条命令的意思是:从0x100000A8地址开始,读取6个字节的内存。在我实际项目中,某块nRF52832板子的输出是:

50 7A 42 01 72 C8

这一串并不是最终要的MAC字符串,因为DEVICEADDR寄存器在FICR里是小端存放的,内存里读到的低地址字节其实是MAC的低位,必须倒过来读,真实MAC应该是:

C8:72:01:42:7A:50

这个倒序问题坑了非常多人。你只要记住一个原则:nRF52的FICR DEVICEADDR在内存里的字节顺序和打印出来的MAC完全相反。想验证的话,把读到的结果和你拿手机扫描到的板子蓝牙MAC对照一下,能对上就说明顺序对了。

3.2 用pylink脚本读取并自动转换字节序

手动敲命令适合临时验货,批量操作还是得靠脚本。下面这段是我在产测工装里实际用的代码,做了最小化精简:

import pylink from pylink.enums import JLinkInterfaces DEVICE_ADDR_REG = 0x100000A8 def read_nordic_mac(device="nRF52832_xxAA"): jlink = pylink.JLink() jlink.open() jlink.connect(device, interface=JLinkInterfaces.SWD, speed=4000) jlink.halt() raw = jlink.memory_read8(DEVICE_ADDR_REG, 6) mac_bytes = raw[::-1] # FICR小端,倒序才是真实MAC jlink.go() jlink.close() mac_str = ":".join(f"{b:02X}" for b in mac_bytes) return mac_str if __name__ == "__main__": print(read_nordic_mac())

这段代码有几个关键细节。halt()是为了让CPU停下来,避免调试访问和总线操作竞争;走之前调go(),让设备恢复执行,避免下一块板子上电后处于暂停状态。memory_read8返回字节列表,[::-1]实现倒序。如果你是Windows环境,连接速度4000kHz一般比较稳,如果遇到驱动能力弱的板子,降到1000kHz往往能解决问题。

3.3 和nrfjprog的结果对照验证

为了确认读出来的MAC是对的,我习惯再用Nordic官方工具nrfjprog交叉验证一遍:

nrfjprog --memrd 0x100000A8 --n 6

或者更直接,新版nrfjprog支持:

nrfjprog --devices

但那打印的是Device ID,不是MAC。最简单的还是读内存自己解析。nrfjprog读出来的原始字节同样需要倒序,所以不要以为换个工具就不用来回倒了。我之前测试过,同块板子用nrfjprog、J-Link Commander和pylink读出来的数据完全一致,倒序后和手机扫描到的MAC也一致。这样两边一比对,基本可以确认地址、读法和解析逻辑都正确。

4. Silicon Labs篇:EFR32系列读取MAC

4.1 EFR32的Device Info和EUI48在哪

Silicon Labs的EFR32系列芯片,包括BG22、BG13、MG21这些常见型号,出厂信息存放在Flash信息页(Info Page)的Device Info结构体中。EUI48就是这个结构体里的48位MAC地址字段,它通常代表芯片的全球唯一MAC。对EFR32系列,我的经验里取值地址从0x0FE08130开始,读6个字节即可。

需要注意,EFR32的MAC存储方式不像Nordic那样“简单粗暴地反序”,大多数型号内存中读出的字节顺序就是打印顺序,也就是你从0x0FE08130读到的第1个字节是MAC的最高位,最后一个字节是最低位。这一点和Nordic正好相反,所以换平台时千万不要套用同一个脚本逻辑。

4.2 用J-Link Commander操作EFR32

连接EFR32BG22时,命令行这样写:

JLink.exe -device EFR32BG22C224F512IM40 -if SWD -speed 4000 -autoconnect 1

进入交互界面后读取:

mem8 0x0FE08130 6

我手上的EFR32BG22模块读出来的输出是:

E8 94 F6 2C 15 68

这个直接就是MAC:E8:94:F6:2C:15:68。Silicon Labs和Nordic的字节序规则不一样,所以不是说所有芯片读出来都要反转,而是要看厂商具体实现。

如果0x0FE08130读出来感觉不太像有效MAC,或者全F全0,可以扩大范围读取:

mem8 0x0FE08100 128

把整个Device Info结构拉出来,人工找一下6字节的EUI48。通常它就在Unique ID后面,而且和芯片外壳丝印或出厂标签上的MAC一致,很好认。

4.3 pylink脚本读取EFR32并与Simplicity Commander对照

对应pylink脚本如下:

import pylink from pylink.enums import JLinkInterfaces EUI48_ADDR = 0x0FE08130 def read_efr32_mac(device="EFR32BG22C224F512IM40"): jlink = pylink.JLink() jlink.open() jlink.connect(device, interface=JLinkInterfaces.SWD, speed=4000) jlink.halt() mac_bytes = jlink.memory_read8(EUI48_ADDR, 6) jlink.go() jlink.close() mac_str = ":".join(f"{b:02X}" for b in mac_bytes) return mac_str if __name__ == "__main__": print(read_efr32_mac())

Silicon Labs官方也提供Simplicity Commander工具,可以直接在命令行确认MAC:

commander device info

输出的信息里包括Unique ID和MAC Address,用它对照pylink读出来的结果,一目了然。这两条路径我都试过,数据一致。如果你手头既装了Simplicity Studio又装了J-Link,注意Commander默认用的是Silicon Labs自带的驱动,和SEGGER J-Link互不冲突,可以直接并存。

5. 常见问题与排查技巧实录

5.1 连接不上目标芯片,卡在Cannot Connect

这个是遇到最多的问题。J-Link Commander报“Cannot connect to target”时,我一般按下面顺序排查:

先看VTref是否被检测到,如果J-Link软件界面显示目标电压为0,说明参考电压没接好,或者是目标板根本没上电。然后用万用表量一下SWDIO和SWCLK有没有被拉低,很多开发板上的按键、外设把这两个引脚复用了,导致调试口被占住。最后把速度往下调,4000kHz不行换1000kHz,再不行换100kHz,低速一般都能连上。

另外,目标芯片如果被启用了读写保护或者调试锁,SWD会被禁掉。Nordic里如果设置了APPROTECT,EFR32里如果设置了Secure Lock,调试器都进不去,这种情况只能先通过全片擦除或者厂商的解锁流程恢复。产线上如果碰到一致性的“连不上”,优先怀疑是不是上一道工序把锁开了。

5.2 读出来全是0xFF或者全是0x00

内存读出来全是F,基本是调试器访问的地址无效,或者芯片没有正确连接。先说地址无效:Nordic的FICR地址0x100000A8只对nRF51、nRF52系列有效,nRF53、nRF70的地址空间完全不同;EFR32的EUI48偏移也随型号变化,不要拿通用地址硬套。再有一个可能是芯片被保护后,内存读出来会被掩码成F或者0。

如果读出来全是0,一般是地址对但读的方式不对。比如用mem32去读6字节的MAC,读出来的32位数据和16位数据拼在一起,很容易搞错。统一用mem8按字节读,然后自己拼顺序,最稳妥。

5.3 读到的MAC和标签、实际广播地址对不上

这个现象其实要分两层看。第一层是字节序问题,Nordic需要倒序,这个前面已经反复强调。第二层是“MAC地址”这个概念本身有歧义:芯片出厂信息区里的EUI48或DEVICEADDR是硬件出厂地址,但BLE设备在运行时完全可以使用随机静态地址或私有地址,不一定广播真实的MAC。

比如nRF52的FICR里除了DEVICEADDR还有一个DEVICEADDRTYPE,如果地址类型是随机静态地址,那么设备每次上电可能动态生成地址。Silicon Labs的蓝牙协议栈也允许设备不采用EUI48作为公共地址,运行时的可发现地址可能是另一个值。所以你要是发现“读出来的和手机扫码看到的不一致”,先搞清楚你扫描到的是不是公共地址类型,再判断是否读取有误。

6. 顺手把它做成产测工具

6.1 一个完整的批量读取脚本

把Nordic和EFR32的逻辑整合到一个脚本里,是产测工装比较理想的形态。下面是一个简化版的可运行示例,支持通过命令行参数指定平台和数量:

import argparse import time import pylink from pylink.enums import JLinkInterfaces def make_reader(device, reg, reverse=False): def read(): j = pylink.JLink() j.open() j.connect(device, interface=JLinkInterfaces.SWD, speed=4000) j.halt() raw = j.memory_read8(reg, 6) j.go() j.close() data = raw[::-1] if reverse else raw return ":".join(f"{b:02X}" for b in data) return read if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--platform", choices=["nordic", "efr32"], required=True) parser.add_argument("--count", type=int, default=1) args = parser.parse_args() if args.platform == "nordic": reader = make_reader("nRF52832_xxAA", 0x100000A8, reverse=True) else: reader = make_reader("EFR32BG22C224F512IM40", 0x0FE08130, reverse=False) for i in range(args.count): mac = reader() print(f"{i + 1}\t{mac}") time.sleep(0.3)

这个脚本每读一块板子会等待300ms,让工装气动夹爪或操作员有时间换板子。实际产线里,我会把print替换成写入CSV或数据库,再关联一个SN二维码扫码枪输入,形成完整绑定链路。

6.2 后续扩展的一些思路

一次读一块还是不够快的话,可以同时挂多路J-Link到同一台电脑,pylink支持枚举当前连接的所有J-Link,循环打开、读取、关闭。每一路对应一个工位,产线节拍能再提一截。

除了MAC,同一个调试会话里还能一次性读芯片唯一ID、Flash大小、硬件版本,甚至校验固件烧录结果,这些都可以作为产测数据的一部分写进数据库,避免为了不同参数来回切换工具。配合CMake或者CI系统,甚至可以在固件编译完成后自动连接开发板烧录、读MAC、跑基础功能测试,整个流程下来能节省大量手工操作时间。

我个人在实际项目中的体会是:读MAC这件事,看起来只是“敲一条命令”,但真正落地到产线时要考虑字节序、器件型号差异、锁保护策略、数据怎么和SN绑定这些细节。先用J-Link Commander手动验证地址和顺序,再上pylink自动脚本,最后再迭代人机交互流程,是风险最小的推进路径。最后再分享一个小技巧:无论Nordic还是EFR32,首次在一款新芯片上操作时,都先读一块样片来验证地址和解析方式,如果你发现读出的6字节和外壳标签或官方工具输出一致,再批量套用脚本。毕竟不同批次、不同封装、不同固件烧录策略,都可能让你所谓的“通用地址”失效,留一手验证环节永远不亏。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询