记录一次jlink 弹窗问题
一个 J-Link 弹窗引出的一小时:从「No loader specified」到 %APPDATA% 里的 2024 年遗留
现象一句话:每次用 J-Link 的 RTT 读调试输出,都会弹一个框
J-Link V9.80 Error / Device: MIMXRT685_M33: Flash bank 0x08000000: No loader specified,
并卡住 9~37 秒等人点确定。
最终定位:%APPDATA%\SEGGER\下遗留了一份 2024-11-14 的旧版用户级器件库,
新版 DLL 仍然加载它,而它引用的几百个 flash loader 文件早已不存在。
改名后device命令从 28.75 s → 0.02 s。
目录
- 0. 背景:我们要做什么
- 1. 先把时序量出来:9 秒的停滞是哪一步
- 2. 技术细节:直接调 JLinkARM.dll 读 RTT
- 3. 排查:五个假设,四次排除
- 4. 根因:新旧版 J-Link 的架构差异
- 5. 修复与收益
- 6. 可复用的六个手段
- 7. 踩坑清单
- 8. 如果重来一次
0. 背景:我们要做什么
在给一块电表(HT7x3x 主控)做 SPI 协议联调,需要一个能实时看电表侧调试打印的通道。
工程里已经集成了 SEGGER RTT(printf_* 系列走 RTT 到 J-Link),所以最自然的做法是
在主机侧脚本里读 RTT 输出,和 AT 命令的时间线对齐。
需求很朴素:读 RTT 输出。不烧写、不下载、不调试会话 —— 只是读内存里的一个环形缓冲。
为此写了一个直接用 ctypes 调 JLinkARM.dll 的小工具。然后弹窗就来了。
1. 先把时序量出来:9 秒的停滞是哪一步
排查的第一步不是猜,而是给每一步调用计时。
正常来说 J-Link DLL 的调用都是毫秒级。所以只要给 JLINK_ExecCommand 之类的调用套上
秒表、超过 3 秒就告警,异常立刻自己跳出来:
t0 = time.time()
r = self.f_exec(cmd.encode(), out, len(out))
dt = time.time() - t0
if dt >= 3.0:
print('⚠ "%s" 耗时 %.1fs —— 正常应是毫秒级,极可能弹了对话框在等人点' % (cmd, dt))
第一次跑出来是这样:
07:44:35 JLINK_Open OK (ret=0) <- 0.24s,正常
07:45:05 JLINK_Halt -> (卡了 9 秒) <- 异常!
07:45:06 JLINK_Reset -> -1 <- 复位失败
关键信号:JLINK_Open 快、Halt/Reset 卡。
"只有需要器件信息的调用才卡"——这个分界直接把范围缩小到"器件/flash 相关路径"。
(模态框阻塞的特征就是:调用一直不返回,直到有人点掉它。所以耗时异常 = 很可能在弹框,
这是个非常实用的判据,不用去截屏。)
2. 技术细节:直接调 JLinkARM.dll 读 RTT
顺带记录这段,因为博客里这类"直接调 DLL"的完整例子不多。
2.1 用到的接口
int JLINK_Open(void);
int JLINK_Close(void);
int JLINK_RTTERMINAL_Control(int Config, void* p); // 0=START 1=STOP 2=GETDESC
int JLINK_RTTERMINAL_Read(int BufferIndex, char* buf, int size);
int JLINK_ExecCommand(const char* cmd, char* out, int outsize);
int JLINK_ResetNoHalt(void);
流程:JLINK_Open() → ExecCommand("SetRTTAddr 0x…") → RTTERMINAL_Control(START) →
循环 RTTERMINAL_Read(0, buf, n)。
2.2 坑一:32 位 / 64 位 DLL
SEGGER 安装目录里有两个 DLL,同名不同位数:
JLinkARM.dll 32 位 (PE Machine 0x14C)
JLink_x64.dll 64 位 (PE Machine 0x8664) <- 64 位 Python 必须用这个
用错了报 OSError: [WinError 193] %1 不是有效的 Win32 应用程序。
这个错误信息完全没提"位数",第一次见容易懵。
2.3 坑二:RTT 控制块地址从 .map 里取
RTT 需要在目标 RAM 里找到那个控制块。J-Link 支持自动搜索,但对非 SEGGER
器件库里的芯片,DLL 不知道 RAM 范围,自动搜索靠不住。更好的办法是显式指定地址。
本工程把控制块放在专用 .rtt 段,IAR 的 .map 里直接能看到:
.rtt uninit 0x2001'e000 4 0x858 SEGGER_RTT.o
于是写个函数从 .map 里取符号 _SEGGER_RTT 的地址:
m = re.search(r"^\s*_SEGGER_RTT\s+0x([0-9a-fA-F']+)", txt, re.M)
addr = int(m.group(1).replace("'", ''), 16) # 注意 IAR 用单引号分组地址!
注意
0x2001'e000里的单引号是 IAR 的千位分隔符,不处理的话int()会失败。
每次编译地址都会变,所以每次现取,不要写死。
2.4 坑三:RTTERMINAL_Read 会重复返回整块缓冲
这个行为很反直觉:它不是"返回新增数据",而是把整块缓冲从起点返回,
而且随着目标继续写而增长。结果就是同一段开机横幅被打印了 15~19 次。
"相同块去重"抓不住它(每次长度都在变),正确做法是前缀增量:
if data == last_chunk:
continue # 纯重复
if data.startswith(last_chunk):
data = data[len(last_chunk):] # 只取新增
last_chunk = last_chunk + data
else:
last_chunk = data # 缓冲回绕/重来
3. 排查:五个假设,四次排除
H1 工程里配了错器件?
弹窗里写着 MIMXRT685_M33(NXP 的 i.MX RT685,Cortex-M33)—— 跟我们这块表
(HT7x3x,Cortex-M4)毫无关系。第一反应:某个配置文件里配错了。
于是把能找到的配置全查一遍:
39 个 .jlink 设置文件 -> 只有 Cortex-M4 / Cortex-M3 / ARM7,没有 MIMXRT685
注册表 HKCU\Software\SEGGER\J-Link -> 只有 InstallPath / CurrentVersion / 一堆 Log 开关
JLinkRTTViewerSettings.ini -> Device="CORTEX-M4"
本工程 .jlink -> Device="Cortex-M4"
排除。 而且工程自己的配置是对的。
H2 DLL 记着"上次用的器件"?
JLinkDLL.ini 里确实有个 [Device] 段,但那是 MRU 列表(最近用过的器件名),
第一项就是 CORTEX-M4。没有"当前活动器件"这样的键。
排除。
H3 安装目录缺 Devices\ loader 树?——这条误导了我一轮
查安装目录时发现:
C:\Program Files\SEGGER\JLink_V794i\Devices\ 只有 Vango\ ,3 个 .FLM
而 JLinkDevices.xml 里每条 <FlashBankInfo Loader="Devices/<厂商>/…/*.FLM">
引用的文件都不存在。看上去因果完美:缺 loader → 报 "No loader specified"。
我还找了个"佐证":DLL 的字符串表里,这条错误消息紧邻
Error in XML device description file: …,看着就是"解析器件库"的代码路径。
于是基于这个判断做了一次修改:把出问题那个器件的两条 <FlashBankInfo> 删掉。
结果:
弹窗换成了另一个器件:
R7S921040VCBG_SPIBSC_OctaFlash: Flash bank 0x20000000
这个"名字会变"的现象是整场排查的决定性线索:说明 DLL 不是在为"某个被选中的器件"
报错,而是在遍历全库、逐个报告缺失。删条目治不了本,因为坏条目有几百条。
修正后的认识:
- "安装目录缺
Devices\"这个观察本身没错,但不是病因; - 后来装了 V9.8 才明白:新版安装目录本来就没有
Devices\(见 §4)。
H4 用命令把弹窗压下去?
从 DLL 二进制里 dump 出了它的命令名表(搜 Suppress 附近的字符串),发现一堆看起来
很对症的开关,逐个试:
| 命令 | 结果 |
|---|---|
SuppressInfo |
ERROR: Unknown command(它是 JLink.exe 的命令行开关,不是 DLL 命令) |
SuppressGUI 1 |
返回 0(接受)但弹窗照旧 |
SetBatchMode 1 |
返回 0,无效 |
HideDeviceSelection 1 |
返回 0,无效 |
DisableInfoWinFlashDL |
返回 0,无效 |
SetMSGBoxTimeout 1000 |
V9.8 里 Unknown command(旧版有,新版删了) |
JLinkDevicesXMLPath "<空库目录>" |
命令被接受,总耗时 20.2 s → 13.8 s,但第一条 device 仍要 13 s 且仍弹框 |
全部无效。 但这些实验不是白做的 —— JLinkDevicesXMLPath 那条"只降了一部分时间"
很有信息量:它改的是安装级搜索路径,说明还有另一处器件库在被加载。
H5 %APPDATA% 下的遗留库 —— 就是它
顺着 H4 的线索,仔细看 %APPDATA%\SEGGER\:
JLinkDevices.xml 160,383 B 2024-11-14 <- 旧格式的器件库
JLinkDevices\ 目录(同一份老库的副本)
Devices\ 只剩 Vango\,3 个 .FLM
而 V9.8 的安装目录里既没有 JLinkDevices.xml 也没有 Devices\ —— 它的器件库和
loader 已经内嵌进 26 MB 的 DLL 里了(V7.94i 的 DLL 只有 21 MB)。
抽查那份老库引用的 loader,无一存在:
Devices/ATMEL/SAMA5D2/SAMA5D2XPLAINED_QSPI.elf 用户级=False V9.8=False
Devices/Cypress/PSoC5/Cypress_PSoc5_EEPROM.elf 用户级=False V9.8=False
Devices/ClouderSemi/CR600/CR600.FLM 用户级=False V9.8=False
决定性验证:把这两项改名(可还原),重跑:
device Cortex-M4 -> 0.02 s (改名前 28.75 s,约 1400 倍)
总耗时 5.6 s(= 5 s 读窗口 + 0.6 s,全程毫秒级)
至此确认无误。
4. 根因:新旧版 J-Link 的架构差异
| V7.94i | V9.8 | |
|---|---|---|
安装目录 JLinkDevices.xml |
有(160 KB,外部库) | 没有 |
安装目录 Devices\ |
有 | 没有 |
JLinkARM.dll 体积 |
21 MB | 26 MB(库与 loader 内嵌) |
SEGGER 在新版里把器件库和 flash loader 打包进 DLL 了,不再需要外部目录。
但用户级器件库的位置没变:DLL 仍会去 %APPDATA%\SEGGER\ 找用户自定义器件
(这是给用户添加自制器件定义用的)。于是:
V9.8 DLL 启动
└─ 加载内嵌器件库(正常,没有问题)
└─ 加载用户级器件库 ← 2024 年 V7.94i 时代留下的那份 160KB 老库
└─ 逐条校验 <FlashBankInfo Loader="Devices/...">
└─ loader 文件全都不存在
└─ 遍历全库逐个报 "No loader specified" ← 弹框 + 卡 ~28 秒
为什么重装 V9.8 也没用:重装只换安装目录,%APPDATA% 里那份用户级老库纹丝不动。
5. 修复与收益
$app = "$env:APPDATA\SEGGER"
Rename-Item "$app\JLinkDevices.xml" 'JLinkDevices.xml.disabled' -Force
Rename-Item "$app\JLinkDevices" 'JLinkDevices.disabled' -Force
# 确认没问题后再删
(我用了改名而不是删除,方便万一有问题能立刻还原。)
收益不止我们这条链路:JLink Commander / J-Flash / RTT Viewer 等所有 SEGGER 工具
都会一起恢复正常,因为它们同样会遍历那份坏库。
工具里还固化了一个体检:每次运行检查这两项是否存在,存在就打印警告和修法,
并且只在此时才拦截会碰器件库的操作(--reset),环境干净后不再有任何拦截。
6. 可复用的六个手段
-
给每一步调用计时。 模态框的本质是"调用不返回",所以耗时异常就是最可靠的探测手段,
不需要截屏或看窗口。我设的阈值是 3 秒(正常都在毫秒级)。 -
盯住"错误信息里变化的那个名字"。 唯一的器件名从
MIMXRT685_M33变成
R7S921040VCBG的那一刻,结论就从"某处配错了"变成"它在遍历全库"。
变量在变,说明你在看一个循环。 -
拿 DLL 的字符串表做"考古"。 错误消息在二进制里的邻居会告诉你它在哪条代码路径上。
这条消息紧邻Error in XML device description file→ 立刻知道和"解析器件库"有关。
同理,dump 出Suppress附近的字符串,就拿到了它的命令名表,
于是能分清"哪些是我们以为存在其实不存在的命令"。 -
A/B 对照 + 可还原。 把可疑文件改名(而不是删除)、立刻重跑、对比数字。
28.75 s → 0.02 s 这种量级的差异,比任何推理都有说服力。 -
签名不明的 API 放进隔离进程试。
JLINK_SetErrorOutHandler的实参约定查不到,
猜错可能直接崩进程 —— 那就写个小脚本单独跑,崩了也不影响主程序。
(结果它是对的,接上后 DLL 的报错文本会进日志,不再静默卡住。) -
把"注意到的现象"记成表。 排查过程中我试过的手段、结果、以及被排除的假设
都记在文档里。下一次遇到相似问题时,"这些路走过、不通、原因是××"比结论本身更省时间。
7. 踩坑清单
WinError 193⇒ 十有八九是 32/64 位 DLL 用错,不是文件损坏。- IAR
.map里的地址带单引号分组(0x2001'e000),直接int()会失败。 JLINK_RTTERMINAL_Read重复返回整块缓冲,要按前缀增量去重。SuppressInfo是 JLink.exe 的命令行开关,不是 DLL 命令(Unknown command)。- "安装目录缺 Devices 树"是误导:新版 J-Link 本来就没有它。别看到"缺文件"就往
"文件缺失导致报错"上归因 —— 先确认这个文件在当前版本里是否本来就该存在。 - 重装软件不会清理
%APPDATA%下的用户级配置。跨大版本升级时,
这些"用户自定义"目录里的老格式文件可能被新版本继续加载。 - 用"弹窗自动关闭器"能绕过卡顿,但会掩盖证据。排查阶段它让我差点止步于
"问题解决了"(其实只是没人点了)。所以它现在是--dismiss-dialogs手动开关,
默认关闭。
8. 如果重来一次
按这个顺序会快得多:
- 计时,定位到"哪一步卡" → 5 分钟,把范围从"整个工具"缩到"器件相关调用"。
- 看错误消息里的可变部分:同一个错误框里的器件名会不会变?
会变 ⇒ 直接跳到"某处在遍历列表",省掉 H1/H2 两轮。 - 对比新旧版本安装目录的结构差异(哪个文件是"这个版本本来就该有的")。
这一步能立刻纠正 H3 的错误归因。 - 查用户级配置目录(
%APPDATA%\<厂商>\)—— 这是"重装也没用"类问题的第一嫌疑。 - 改名 A/B 验证,再决定删还是留。
一句话总结这次的核心:
报错里那个陌生的器件名不是在告诉你"配错了器件",
而是在告诉你"我在遍历一个很长的列表"。
顺着这个方向找到那份 2024 年的遗留库,28 秒就变成了 0.02 秒。
浙公网安备 33010602011771号