第4章 变量与寄存器观察:Watch窗口、Memory窗口、Peripherals与System Viewer

调试的时候,最怕什么?

怕程序跑飞了,还不知道错在哪。怕变量值不对,却看不到。怕寄存器配错了,外设不干活。

这一章,我就带你把这几个窗口玩透。Watch窗口、Memory窗口、Peripherals窗口、System Viewer,这四个工具用好了,定位问题能快一倍。

4.1 Watch窗口:变量的“实时监控器”

Watch窗口,说白了就是给你盯着变量用的。程序跑起来,变量怎么变,它实时刷新。

4.1.1 添加变量到Watch

操作很简单:

  1. 在代码里选中变量名,右键 → Add to Watch。
  2. 或者直接在Watch窗口的空白行双击,手动输入变量名。

我个人习惯把关键变量都扔进去。比如电机控制里的目标速度、实际速度、PID误差值。一眼就能看出逻辑对不对。

小技巧:Watch窗口支持结构体展开。你添加一个结构体变量,点开“+”号,所有成员变量都看得清清楚楚。我调试CAN通信协议时,经常用这个功能检查报文结构体里的每个字段。

4.1.2 变量类型与格式

Watch窗口里,你可以右键变量,选择显示格式:

格式 适用场景
十进制 普通计数、延时参数
十六进制 寄存器值、地址、标志位
二进制 位操作、状态标志
浮点数 ADC采样值、PID输出

嗯,这里要注意:如果你看的是uint32_t类型的寄存器值,用十六进制最直观。看ADC采样结果,用十进制或浮点数。

4.1.3 实时更新与断点配合

Watch窗口有两种刷新模式:

  • 运行中刷新:程序全速跑,Watch窗口定期更新。适合观察趋势。
  • 断点刷新:程序停在断点处,Watch窗口显示那一刻的值。适合精确分析。

我曾经遇到一个Bug:电机转速忽快忽慢。我把速度变量加到Watch,全速跑着观察,发现目标速度没变,但实际速度在跳。后来在PID计算函数里设断点,Watch显示误差值突然变大——原来是传感器采样偶尔丢数据。你看,Watch窗口配合断点,问题一下就锁定了。

注意:全速运行时Watch刷新会占用CPU资源。如果程序对时序要求极高(比如PWM输出),建议只在断点模式下查看,或者减少Watch里的变量数量。

4.2 Memory窗口:直接看内存,不绕弯子

Watch窗口看变量,Memory窗口看内存。有时候变量值看起来不对,但内存里数据是好的——那可能是变量类型声明错了。反过来,内存数据乱了,那问题就大了。

4.2.1 打开Memory窗口

View → Memory Windows → Memory 1(或2、3、4)。你可以同时开4个窗口,看不同地址区域。

4.2.2 查看指定地址

在Address栏输入地址:

  • 0x20000000 — SRAM起始地址
  • 0x08000000 — Flash起始地址
  • &变量名 — 直接取变量地址

我调试时经常这么干:在Watch窗口看到变量值异常,立刻切到Memory窗口,输入&那个变量,看看内存里到底存了什么。有一次发现数组越界,就是靠这个办法——Watch里数组最后一个元素正常,但Memory里相邻地址的数据被覆盖了。

4.2.3 数据格式与显示

Memory窗口支持多种显示格式:

格式 说明
8位十六进制 每个字节显示2个十六进制字符
16位十六进制 每2个字节合并显示
32位十六进制 每4个字节合并显示
ASCII 右侧显示对应字符,适合看字符串

我个人习惯用32位十六进制看寄存器区域,用8位十六进制+ASCII看缓冲区数据。比如调试串口接收时,Memory窗口里能看到一帧完整的报文,哪个字节对了、哪个错了,一目了然。

核心思路:Watch窗口告诉你“变量值是什么”,Memory窗口告诉你“内存里实际存了什么”。两者对照,能发现很多隐藏问题。

4.3 Peripherals窗口:外设寄存器的“可视化面板”

Peripherals窗口,是Keil里我最喜欢的工具之一。它把外设寄存器按功能分组,用图形化界面展示。你不用去翻参考手册查寄存器地址,直接看这个窗口就行。

4.3.1 打开Peripherals窗口

View → Peripherals → 选择外设(比如GPIO、USART、TIM)。

举个例子,打开GPIOA的Peripherals窗口,你会看到:

  • 每个引脚的模式(输入、输出、复用、模拟)
  • 输出数据寄存器(ODR)的值
  • 输入数据寄存器(IDR)的值
  • 上下拉配置

而且,这些值会随着程序运行实时变化。你按下按键,对应引脚的IDR位立刻从1变成0。这种直观反馈,比看代码快多了。

4.3.2 修改寄存器值

Peripherals窗口里,有些寄存器可以直接修改。比如你想强制让某个GPIO引脚输出高电平,直接在ODR对应的位上点一下,改成1。程序下次运行到那里时,会读到这个新值。

但要注意:

警告:直接修改寄存器值,可能会破坏程序逻辑。比如你在定时器运行时改了预分频系数,计数器可能立刻溢出。我一般只在调试初期、确认硬件连接时用这个功能。正式调试时,还是靠代码控制。

4.3.3 实战案例:串口不发送数据

我记得有一次,串口怎么都不发数据。代码检查了好几遍,没问题。打开Peripherals窗口看USART寄存器:

  • CR1寄存器:UE位=1,使能了
  • CR1寄存器:TE位=1,发送使能了
  • DR寄存器:有数据待发送
  • SR寄存器:TXE位=0 —— 等等,TXE是0?

TXE位为0,表示发送数据寄存器非空,但数据还没发出去。再一看,波特率寄存器算出来的值不对。原来是时钟配置错了,USART的时钟频率比预期低了一半。波特率自然也不对。

你看,Peripherals窗口把寄存器状态全摆在你面前,问题根本藏不住。

4.4 System Viewer:系统级的外设总览

System Viewer,是Peripherals窗口的“升级版”。它把整个芯片的外设、中断、时钟树都列出来,以树形结构展示。

4.4.1 打开System Viewer

View → System Viewer。你会看到一个窗口,左侧是外设列表,右侧是详细信息。

4.4.2 主要功能

  • 外设寄存器查看:和Peripherals窗口类似,但信息更全。
  • 中断状态查看:哪些中断被使能了,哪些中断正在挂起,一目了然。
  • 时钟配置查看:HSE、HSI、PLL、系统时钟、外设时钟,当前值是多少。

我个人习惯在调试开始时,先打开System Viewer看一眼时钟树。确认系统时钟跑在目标频率上,外设时钟也正确使能了。这一步能避免很多低级错误。

4.4.3 与Peripherals窗口的对比

特性 Peripherals窗口 System Viewer
展示方式 图形化面板 树形结构
信息密度 中等 高
修改寄存器 支持 支持
中断查看 有限 完整
时钟查看 无 有

简单说:日常调试用Peripherals窗口,够快够直观。需要系统级分析时,切到System Viewer。

4.5 四个窗口的配合使用

这四个窗口不是孤立的。我调试时,经常这样配合:

  1. Watch窗口盯着关键变量,看逻辑对不对。
  2. Memory窗口验证内存数据,看有没有越界或覆盖。
  3. Peripherals窗口检查外设状态,看寄存器配没配对。
  4. System Viewer做系统级确认,看时钟和中断。

举个例子:调试I2C通信时,Watch窗口看发送缓冲区的数据,Memory窗口看接收缓冲区的内容,Peripherals窗口看I2C的状态寄存器(忙、应答、停止位),System Viewer确认I2C时钟是否使能。四个窗口一配合,问题基本跑不掉。

我的经验:不要只依赖一个窗口。Watch窗口说变量值不对,去Memory窗口看看是不是地址错了。Peripherals窗口说外设没反应,去System Viewer看看时钟开了没。交叉验证,才能快速定位。

4.6 本章知识结构图

下面这张图,帮你理清四个窗口的关系和适用场景:

调试窗口知识结构图 调试窗口知识结构图 Keil调试窗口 Watch窗口 Memory窗口 Peripherals窗口 System Viewer 功能 实时监控变量值 支持结构体展开 配合断点精确分析 功能 直接查看内存数据 支持多种格式显示 检测数组越界/覆盖 功能 外设寄存器可视化 实时状态反馈 支持直接修改 功能 系统级外设总览 中断状态查看 时钟配置确认 核心思路:交叉验证,快速定位 Watch + Memory + Peripherals + System Viewer 配合使用

4.7 避坑指南

最后,分享几个我踩过的坑:

  • Watch窗口变量不更新:检查一下是不是优化等级太高了。O0优化下变量肯定能看,O2优化下有些局部变量可能被优化掉。我建议调试时先用O0。
  • Memory窗口地址输错了:输入&变量名时,注意变量作用域。局部变量在函数退出后,地址可能被回收。用全局变量或静态变量更稳妥。
  • Peripherals窗口值不变:可能是外设时钟没使能。去System Viewer看一眼时钟树,确认外设时钟是打开的。
  • System Viewer里中断挂起但没响应:检查中断优先级分组。有时候优先级配错了,高优先级中断一直占着,低优先级中断永远进不去。

嗯,这些坑我都亲自踩过。现在调试时,这四个窗口已经成了我的标准配置。你也试试看,用熟了之后,定位问题的速度能快不少。