记录一次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. 背景:我们要做什么

在给一块电表(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,全程毫秒级)

至此确认无误。


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. 可复用的六个手段

  1. 给每一步调用计时。 模态框的本质是"调用不返回",所以耗时异常就是最可靠的探测手段,
    不需要截屏或看窗口。我设的阈值是 3 秒(正常都在毫秒级)。

  2. 盯住"错误信息里变化的那个名字"。 唯一的器件名从 MIMXRT685_M33 变成
    R7S921040VCBG 的那一刻,结论就从"某处配错了"变成"它在遍历全库"。
    变量在变,说明你在看一个循环。

  3. 拿 DLL 的字符串表做"考古"。 错误消息在二进制里的邻居会告诉你它在哪条代码路径上。
    这条消息紧邻 Error in XML device description file → 立刻知道和"解析器件库"有关。
    同理,dump 出 Suppress 附近的字符串,就拿到了它的命令名表,
    于是能分清"哪些是我们以为存在其实不存在的命令"。

  4. A/B 对照 + 可还原。 把可疑文件改名(而不是删除)、立刻重跑、对比数字。
    28.75 s → 0.02 s 这种量级的差异,比任何推理都有说服力。

  5. 签名不明的 API 放进隔离进程试。 JLINK_SetErrorOutHandler 的实参约定查不到,
    猜错可能直接崩进程 —— 那就写个小脚本单独跑,崩了也不影响主程序。
    (结果它是对的,接上后 DLL 的报错文本会进日志,不再静默卡住。)

  6. 把"注意到的现象"记成表。 排查过程中我试过的手段、结果、以及被排除的假设
    都记在文档里。下一次遇到相似问题时,"这些路走过、不通、原因是××"比结论本身更省时间。


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. 如果重来一次

按这个顺序会快得多:

  1. 计时,定位到"哪一步卡" → 5 分钟,把范围从"整个工具"缩到"器件相关调用"。
  2. 看错误消息里的可变部分:同一个错误框里的器件名会不会变?
    会变 ⇒ 直接跳到"某处在遍历列表",省掉 H1/H2 两轮。
  3. 对比新旧版本安装目录的结构差异(哪个文件是"这个版本本来就该有的")。
    这一步能立刻纠正 H3 的错误归因。
  4. 查用户级配置目录(%APPDATA%\<厂商>\)—— 这是"重装也没用"类问题的第一嫌疑。
  5. 改名 A/B 验证,再决定删还是留。

一句话总结这次的核心:

报错里那个陌生的器件名不是在告诉你"配错了器件",
而是在告诉你"我在遍历一个很长的列表"。
顺着这个方向找到那份 2024 年的遗留库,28 秒就变成了 0.02 秒。

posted @ 2026-09-24 08:22  口嗨养生博  阅读(14)  评论(0)    收藏  举报