MicroLink + SEGGER RTT 打印踩坑:no find _SEGGER_RTT addr

MicroLink 读取 RTT 打印时提示找不到 _SEGGER_RTT 地址?本文记录完整的排查思路和解决方案,以及如何绕开 AC6 链接器 ZI/RW 混合限制

问题现象

用 MicroLink 调试 AT32F403A 时,想直接用 SEGGER RTT 做打印输出,工具却报了这么个错:

1
2
Addr = 0x20000000, wSize = 1024, Channel = 0
no find _SEGGER_RTT addr

明明是按 RTT 的标准流程配的,为什么找不到?


排查思路

第一步:理解 RTT 的工作原理

SEGGER RTT 的核心是一个叫做 _SEGGER_RTT 的控制块,里面存着 "SEGGER RTT" 标识字符串。调试器(或 MicroLink 这类工具)需要在 MCU 的内存里找到这个控制块,才能读取打印数据。

找控制块的方式有两种:

工具 扫描方式
J-Link 自动扫描整个 SRAM 范围,找到标识符就停
MicroLink 固定地址 0x20000000 开始扫描

问题根源就在这里 —— MicroLink 不会自己找,它只认 0x20000000。但 _SEGGER_RTT 控制块是链接器自由分配的,不一定恰好落在 SRAM 开头。

第二步:尝试固定地址

既然 MicroLink 只认 0x20000000,那把控制块固定放到那里不就行了?

第一反应是用 AC6 链接器的 __attribute__((at(0x20000000)))

1
2
__attribute__((at(0x20000000)))
SEGGER_RTT_CB_ALIGN(SEGGER_RTT_CB _SEGGER_RTT);

编译,报错:

1
main.o(.data) type RW incompatible with segger_rtt.o(...) type ZI

为什么? AC6 链接器对 ZI(零初始化)和 RW(有初值)数据在同一个执行区域有严格限制。_SEGGER_RTT 没给初值,链接器认为它是 ZI 类型,而 main.c 里其他数据是 RW 类型,混在一个区域就冲突了。

第三步:给初值试试

那把初值设成 0 总行了吧?

1
= {0};

还是报同样的错。原因是 AC6 对全零初值做了优化,仍然识别为 ZI。

第四步:scatter 文件方案

理论上可以在 scatter 文件里专门划一个 UNINIT 区域来放 RTT 控制块:

1
2
3
RTT_CB 0x20000000 UNINIT 0x00000500 {
    segger_rtt.o (.ARM.__AT_0x20000000)
}

这个方案确实可行,但 scatter 文件一碰就容易牵一发而动全身,能不动最好不动。


最终方案:非零初值 + at()

核心思路:让链接器把 RTT 相关变量识别为 RW 类型,这样就不会和 ZI 冲突。

SEGGER_RTT.c 中修改三个变量:

1
2
3
4
5
6
7
#define SEGGER_RTT_SECTION __attribute__((at(0x20000000)))

/* 非零初值使链接器识别为 RW,避免 ZI/RW 混合报错。
   acID[0]='\0' 保证 INIT() 宏能触发 _DoInit() */
SEGGER_RTT_SECTION SEGGER_RTT_CB_ALIGN(SEGGER_RTT_CB _SEGGER_RTT) = {{'\0', 1}};
SEGGER_RTT_SECTION SEGGER_RTT_BUFFER_ALIGN(static char _acUpBuffer  [BUFFER_SIZE_UP])   = {1};
SEGGER_RTT_SECTION SEGGER_RTT_BUFFER_ALIGN(static char _acDownBuffer[BUFFER_SIZE_DOWN]) = {1};

关键点逐个解释

写法 作用
at(0x20000000) AC6 链接器自动从该地址开始依次排列三个变量
{'\0', 1} acID[0]='\0' 满足 INIT() 触发 _DoInit() 的条件;acID[1]=1 非零 → RW 类型
{1} 数组首字节非零 → RW 类型,避开 ZI/RW 混合错误

为什么 acID[0] 必须是 '\0'

RTT 的 INIT() 宏里面有这样一段逻辑:

1
#define INIT()  do { if (_SEGGER_RTT.acID[0] == '\0') _DoInit(); } while (0)

如果 acID[0] 不是 '\0'_DoInit() 不会被调用,控制块就不会被正确初始化(包括写入 "SEGGER RTT" 标识)。所以虽然我们要给非零初值来骗过链接器,但第一个字节必须留 '\0',让 _DoInit() 有机会执行。

内存布局

1
2
3
0x20000000  ├─ _SEGGER_RTT   (~120 字节)
0x20000078  ├─ _acUpBuffer   (1024 字节)
0x20000478  └─ _acDownBuffer (16 字节)

总计约 1160 字节 固定在 SRAM 开头。AT32F403A 有 96KB SRAM,这点占用完全可以忽略。


踩坑过程一览

尝试 方案 结果 原因
__attribute__((section(".ARM.__at_0x20000000"))) ZI/RW 混合报错 ZI 类型与 RW 数据冲突
初值 = {0} 同上 AC6 优化全零初值为 ZI
⚠️ 修改 scatter 文件 可行但麻烦 不想碰链接器脚本
at() + 非零初值 成功 RW 类型,scatter 不用改

验证

修改后重新编译,下载程序,用 MicroLink 连接:

1
2
3
Addr = 0x20000000, wSize = 1024, Channel = 0
[OK] _SEGGER_RTT found at 0x20000000
Hello, RTT!

RTT 打印正常输出,问题解决。


总结

这个问题本质上是工具行为差异(J-Link 会扫描全 SRAM,MicroLink 只认固定地址)加上链接器特性(AC6 对 ZI/RW 混合很敏感)叠加出来的。

如果你也遇到类似情况,记住这几条:

  1. 先确认工具的扫描策略 —— 是自动扫描还是固定地址?
  2. AC6 链接器用 at() 固定地址时,一定要给非零初值 —— 否则会被优化成 ZI
  3. acID[0] 必须保持 '\0' —— 否则 INIT() 不会触发 _DoInit()
  4. 能不动 scatter 就不动 —— 用属性语法更干净

环境:AT32F403A + MDK-ARM (AC6) + MicroLink

世界是你们
使用 Hugo 构建
主题 StackJimmy 设计