4、变量与寄存器监控:Watch窗口、Local窗口、寄存器窗口、内存查看
调试的时候,最怕什么?
怕程序跑飞了,还不知道变量值对不对。怕寄存器配错了,外设死活不工作。我刚开始用RISC-V内核做项目那会儿,就吃过这个亏——明明代码逻辑看着没问题,但PWM就是不出波形。后来一查,才发现是某个控制寄存器的位没置对。
所以今天这章,咱们就把调试器里这几个监控窗口彻底搞明白。Watch窗口、Local窗口、寄存器窗口、内存查看,一个都不能少。
4.1 四个窗口的分工与协作
先看一张总览图,把这四个窗口的关系理清楚。
说白了,这四个窗口就是调试器的四双眼睛。Watch窗口让你盯住特定变量,Local窗口自动展示当前函数的局部变量,寄存器窗口看硬件状态,内存查看看原始数据。配合使用,基本没有查不出的问题。
4.2 Watch窗口:精准盯梢
Watch窗口是我用得最多的。它的核心作用就一个:把你关心的变量单独拎出来,实时看变化。
4.2.1 添加变量
在MounRiver Studio里,操作很简单:
- 在代码编辑区选中变量名,右键 → Add Watch
- 或者直接在Watch窗口空白处双击,手动输入变量名
- 支持表达式,比如
array[5]、*(pStruct + 2)
小技巧:我习惯把关键的状态机变量、错误标志、循环计数器都加到Watch窗口。这样单步执行时,一眼就能看到变化,不用来回翻代码。
4.2.2 变量类型与格式
Watch窗口会自动识别变量类型,但有时候我们需要看特定格式的数据。比如一个32位的寄存器值,你想看十六进制还是十进制?
| 数据类型 | 默认显示 | 可切换格式 |
|---|---|---|
| uint8_t / int8_t | 十进制 | 十六进制、二进制、字符 |
| uint16_t / int16_t | 十进制 | 十六进制、二进制 |
| uint32_t / int32_t | 十进制 | 十六进制、二进制、浮点 |
| float / double | 浮点 | 十六进制(原始字节) |
| 指针 | 十六进制地址 | 十进制偏移 |
右键点击变量,选择Format就能切换。我个人习惯:寄存器值看十六进制,计数器看十进制,状态标志看二进制(方便看每一位)。
4.2.3 数组与结构体的展开
Watch窗口支持展开查看数组元素和结构体成员。比如你有一个结构体:
typedef struct {
uint8_t id;
uint16_t count;
uint32_t timestamp;
uint8_t buffer[16];
} Packet_t;
Packet_t rxPacket;
在Watch窗口里添加 rxPacket,点击前面的三角展开,就能看到每个成员的值。数组 buffer[16] 也能展开看每个字节。
注意:展开大型数组(比如1024字节的缓冲区)时,调试器需要读取大量内存数据,可能会拖慢单步执行速度。我一般只展开需要的部分,或者用内存查看窗口代替。
4.3 Local窗口:自动捕获
Local窗口和Watch窗口最大的区别是什么?
Local窗口是自动的。你不需要手动添加任何变量,它会把当前函数的所有局部变量都列出来。进入一个函数,Local窗口自动刷新;退出函数,局部变量消失。
举个例子,你写了一个计算CRC的函数:
uint32_t calcCRC(uint8_t *data, uint16_t len) {
uint32_t crc = 0xFFFFFFFF;
uint16_t i;
for (i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 1) {
crc = (crc >> 1) ^ 0xEDB88320;
} else {
crc >>= 1;
}
}
}
return crc ^ 0xFFFFFFFF;
}
当你单步进入这个函数时,Local窗口会自动显示 data、len、crc、i、j 这些局部变量。每执行一步,值都在变——你就能清楚地看到CRC计算过程是否正确。
实用场景:我在调试SPI通信协议栈时,经常用Local窗口观察接收缓冲区的指针移动和字节计数。一旦发现计数不对,马上就能定位到是哪个循环出了问题。
4.4 寄存器窗口:硬件级透视
寄存器窗口是嵌入式调试的杀手锏。它让你直接看到CPU内部寄存器和外设寄存器的实时值。
4.4.1 CPU核心寄存器
CH32V203基于RISC-V内核,调试器会显示以下核心寄存器:
| 寄存器 | 名称 | 作用 |
|---|---|---|
| pc | 程序计数器 | 当前执行指令地址 |
| ra | 返回地址寄存器 | 函数返回地址 |
| sp | 栈指针 | 当前栈顶地址 |
| gp | 全局指针 | 全局数据访问基址 |
| tp | 线程指针 | 线程本地存储 |
| a0-a7 | 参数/返回值寄存器 | 函数参数传递和返回值 |
| s0-s11 | 保存寄存器 | 被调用者保存 |
| t0-t6 | 临时寄存器 | 调用者保存 |
我曾经遇到过一个栈溢出的问题,就是通过观察sp寄存器的值发现的。正常运行时sp在合理范围内波动,但有一次它突然跳到了非常低的地址——说明栈已经踩到全局变量区了。
4.4.2 外设寄存器
除了CPU寄存器,调试器还能查看外设寄存器。比如你想检查USART1是否配置正确:
// 在寄存器窗口找到 USART1 的寄存器组
// 查看 USART1->STATUS 寄存器的值
// 检查 TXE(发送空)和 TC(发送完成)位
寄存器窗口会把外设寄存器的每个位都解析出来,显示位名称和当前值。这比对着数据手册一个一个算快多了。
我的习惯:配置完一个外设后,先不急着跑功能,而是到寄存器窗口里核对一遍关键寄存器的值。比如定时器的预分频值、自动重装载值,确认和预期一致再往下走。这一步能省掉后面很多排查时间。
4.5 内存查看:原始数据直读
有时候Watch窗口不够用——比如你想看一大块连续内存的内容,或者DMA传输后的缓冲区数据。这时候就该内存查看窗口上场了。
4.5.1 基本操作
在MounRiver Studio中,打开Memory窗口,输入起始地址,就能看到从该地址开始的内存数据。支持以下显示格式:
- 1字节:每个地址显示一个字节
- 2字节:每两个字节组合显示(半字)
- 4字节:每四个字节组合显示(字)
- 4字节浮点:以单精度浮点数解释
- ASCII:右侧显示对应的ASCII字符
比如你想看一个256字节的DMA缓冲区:
// 假设 DMA 缓冲区地址为 0x20000100
// 在 Memory 窗口输入 0x20000100
// 选择 4 字节显示,长度设为 256
// 就能看到 64 个 32 位数据
4.5.2 地址计算与偏移
内存查看窗口支持地址偏移。比如你知道结构体在 0x20000050,想跳过前8个字节看后面的数据,可以直接输入 0x20000058。
注意:访问非法地址会导致调试器报错甚至断开连接。我曾经在输入地址时少打了一个0,结果访问到了未映射的内存区域,调试器直接卡死。所以输入地址一定要仔细核对。
4.6 实战技巧:组合使用
单个窗口各有局限,组合起来才是王道。我分享一个实际案例:
有一次调试CH32V203的I2C从机模式,主机发送的数据总是对不上。我是这么排查的:
- Watch窗口:添加接收缓冲区指针和已接收字节计数
- 寄存器窗口:查看I2C外设的状态寄存器和数据寄存器
- 内存查看:直接看接收缓冲区的原始数据
- Local窗口:进入中断服务函数,看局部变量
结果发现,中断里读取数据寄存器的时序不对,导致数据错位。如果只看Watch窗口的缓冲区内容,根本看不出问题——因为数据已经错了,但看起来是连续的。只有同时看寄存器窗口的数据寄存器值,才发现每次读取都少了一个字节。
总结一下:Watch窗口盯变量,Local窗口看局部,寄存器窗口查硬件,内存窗口读原始。四个窗口配合使用,没有查不出的Bug。