3. 硬件调试器概述
做嵌入式调试,第一件事是什么?
我个人习惯,拿到一块新板子,先不看原理图,先看它的内存映射表。说白了,就是搞清楚 CPU 眼里,每个地址对应的是什么玩意儿。是 Flash?是 SRAM?还是某个外设的寄存器?
ARM Cortex-M 系列在这方面做得非常规整。它把 4GB 的地址空间划分成了几个大区。你只要记住这几个区的边界,调试时就能少走很多弯路。
3.1 ARM Cortex-M 的 4GB 地址空间划分
Cortex-M 用的是 32 位地址总线,寻址范围 0x0000_0000 到 0xFFFF_FFFF。ARM 官方把这 4GB 空间分成了 8 个 512MB 的块。每个块有固定的用途。
| 地址范围 | 大小 | 用途 | 我常关注的点 |
|---|---|---|---|
| 0x0000_0000 - 0x1FFF_FFFF | 512MB | Code 区(Flash、系统存储器) | 中断向量表必须从这里开始 |
| 0x2000_0000 - 0x3FFF_FFFF | 512MB | SRAM 区 | 堆栈、变量、位带区域 |
| 0x4000_0000 - 0x5FFF_FFFF | 512MB | 外设区 | GPIO、UART、TIM 等寄存器 |
| 0x6000_0000 - 0x9FFF_FFFF | 1GB | 外部 RAM / 外部设备 | FSMC 扩展的 SDRAM 或 NOR Flash |
| 0xE000_0000 - 0xEFFF_FFFF | 256MB | 系统控制块 (SCB)、NVIC、MPU 等 | 调试寄存器、SysTick 都在这里 |
| 其余 | 剩余 | 保留或厂商自定义 | 一般碰不到,碰到了就是芯片选型有问题 |
嗯,这里要注意:Code 区和 SRAM 区虽然物理上是不同的介质,但在 CPU 看来,它们都是统一编址的。你可以在 Code 区执行指令,也可以在 SRAM 区执行指令——只要你的 MPU 允许这么做。
核心概念:统一编址。在 Cortex-M 里,访问 Flash、SRAM、外设寄存器,用的都是同一个 ldr 或 str 指令。区别只在于地址不同。这一点和 x86 的 IO 独立编址完全不同。
3.2 SRAM / Flash / 外设的具体地址范围
不同厂商的芯片,会在 ARM 规定的这些大区里,再划出具体的地址范围。我以最常用的 STM32F103 系列为例,给你拆开看看。
3.2.1 Flash 存储器(Code 区)
起始地址固定为 0x0800_0000。容量从 64KB 到 512KB 不等。芯片上电后,CPU 会从 0x0000_0000 取向量表,但实际物理上,0x0000_0000 是映射到 0x0800_0000 的。这叫别名映射。
调试技巧:我曾经遇到过一个问题:程序下载后跑不起来。用调试器读 0x0800_0000 处的数据,全是 0xFF。后来发现是 Flash 的编程电压没给够,导致写入失败。所以,调试时第一件事,先读一下 Flash 首地址,确认数据真的写进去了。
3.2.2 SRAM(SRAM 区)
起始地址 0x2000_0000。容量通常从 20KB 到 192KB 不等。这里存放全局变量、堆栈、以及动态分配的堆。
SRAM 的末尾地址是 0x2000_0000 + 容量 - 1。比如 64KB 的 SRAM,末尾就是 0x2000_FFFF。
3.2.3 外设寄存器(外设区)
起始地址 0x4000_0000。每个外设占用 1KB 或 2KB 的地址块。比如:
- GPIOA:
0x4001_0800 - USART1:
0x4001_3800 - TIM2:
0x4000_0000
你想想看,为什么每个外设只分配 1KB?因为寄存器数量很少,1KB 足够用了。剩下的地址空间是预留的,或者干脆就是空洞。调试时如果读到这些空洞,返回值通常是 0x0000_0000 或总线错误。
3.3 MPU(内存保护单元)基础
MPU 是什么?说白了,就是一个硬件级的保安。它让你能定义哪些内存区域是可读、可写、可执行的。一旦程序越界访问,MPU 会立刻触发一个异常(MemManage Fault)。
我刚开始用 MPU 时,觉得这东西是 RTOS 才需要的。直到有一次,一个野指针把关键配置数据给覆盖了,查了三天才找到原因。从那以后,我只要做稍微复杂点的项目,都会把 MPU 打开。
3.3.1 MPU 的核心概念
- Region(区域):MPU 最多支持 8 个或 16 个区域(取决于 Cortex-M 版本)。每个区域可以独立配置基址、大小、权限。
- 子区域(Sub-region):每个 Region 可以分成 8 个等份的子区域,可以单独使能或禁用。
- 权限属性:可读、可写、可执行、不可缓存等。
3.3.2 典型配置示例
假设我们有一个 STM32F4 项目,我想保护 Flash 区不被意外写入,同时禁止在 SRAM 区执行代码。MPU 配置大致如下:
// 伪代码:配置 MPU 区域
// Region 0: Flash (0x08000000, 1MB), 只读, 可执行
MPU->RNR = 0; // 选择 Region 0
MPU->RBAR = 0x08000000; // 基址
MPU->RASR = (0x13 << 1) | // 大小: 1MB (2^(13+1))
(0x1 << 28) | // 使能
(0x3 << 24) | // 全权访问 (Privileged & User)
(0x0 << 16); // 只读, 可执行
// Region 1: SRAM (0x20000000, 128KB), 可读写, 不可执行
MPU->RNR = 1;
MPU->RBAR = 0x20000000;
MPU->RASR = (0x10 << 1) | // 大小: 128KB
(0x1 << 28) |
(0x3 << 24) |
(0x1 << 16); // 可读写, 不可执行 (XN 位)
警告:配置 MPU 时,一定要先禁用全局中断,再修改 MPU 寄存器。否则,如果在配置过程中发生中断,而中断服务程序访问了尚未配置好的区域,会直接触发 HardFault。我吃过这个亏,板子直接死给你看。
3.4 内存映射与调试的关联
搞清楚了内存映射,调试器才能正确工作。你想想看:
- 调试器要下载程序,它得知道 Flash 的起始地址和大小。
- 调试器要设置断点,它得知道 SRAM 的地址范围,因为硬件断点数量有限,超过 6 个断点后,调试器会用 SRAM 里的 BKPT 指令来实现软件断点。
- 调试器要读取变量,它得知道变量是放在 SRAM 还是 Flash(常量)。
下面这张图,是我自己总结的 Cortex-M 内存映射与调试器交互的逻辑。你可以把它当作一个快速参考。
3.5 避坑指南:内存映射相关的常见问题
最后,分享几个我在项目中踩过的坑,希望能帮你省点时间。
坑 1:我曾经在调试时,发现某个全局变量值莫名其妙被改了。用调试器设了数据断点,发现是 DMA 写穿了。DMA 的目的地址设到了 SRAM 范围之外,写到了外设区。解决办法:给 DMA 的缓冲区加上 __attribute__((section(".sram_dma"))),强制放到安全区域。
坑 2:还有一次,程序在 SRAM 中运行(为了调试方便),结果一调用函数就 HardFault。查了半天,发现是 MPU 把 SRAM 设成了不可执行(XN)。我忘了自己之前配置过 MPU。解决办法:调试阶段,暂时把 SRAM 的 XN 位清掉。
坑 3:注意字节序。Cortex-M 默认是小端模式。但有些外设(比如某些以太网 MAC)会强制使用大端。如果你用调试器直接看内存,看到的数据可能是反的。我习惯在调试器里把内存显示模式改成「按 4 字节小端显示」,这样看着不累。