Keil5 调试按钮别乱点:从这张 Debug 页面开始,把 Run、Step、Watch、Registers 讲明白
Keil5 调试按钮别乱点:从这张 Debug 页面开始,把 Run、Step、Watch、Registers 讲明白
有个粉丝问了一个问题:
他用 STM32F103 标准库手动新建 Keil5 工程,程序能正常运行,但是进入 Debug 后,Watch1 里的全局变量一直灰色,显示:
cannot evaluate
他还把 Keil 的调试界面截图发了过来。
这张图非常适合拿来讲 Keil5 调试。因为很多新手不是不会写代码,而是进入 Debug 页面以后,不知道这些按钮分别干什么:
-
RST是复位还是重新下载? -
黄色箭头停在哪一行是什么意思?
-
红点是断点,那灰色块又是什么?
-
Run、Stop、Step Into、Step Over到底该怎么用? -
Registers、Call Stack + Locals、Memory、Watch各看什么? -
为什么程序在跑,变量却
cannot evaluate?
这篇就不写泛泛的“调试很重要”了。我们直接照着 Keil5 Debug 页面,把常用按钮和窗口讲一遍。
不同 Keil5 版本、不同工具栏布局里,图标位置可能会有一点差异。不要死记图标长相,重点记住它们属于哪一类:进入调试、运行控制、单步控制、断点控制、窗口观察。
先看这张 Debug 页面在告诉你什么

你这张截图里,Keil 已经进入 Debug 状态。
几个关键信息先看懂:
左侧:Registers 窗口
中间上方:Disassembly 反汇编窗口
中间下方:C 源码窗口 main.c
左下:Command 命令窗口
右下:Call Stack + Locals 窗口
底部状态栏:ST-Link Debugger
源码窗口里还有几个标记:
-
左边红点:断点。
-
当前停住的源码行:调试器当前 PC 附近的位置。
-
Registers里的R15 (PC):CPU 下一条要执行的指令地址。 -
R14 (LR):函数返回地址。 -
R13 (SP):当前栈指针。 -
xPSR:CPU 状态寄存器。
截图里 Call Stack + Locals 只有一个 main,说明当前调用栈很简单,程序停在 main() 里。
这时候先记住一句话:
Keil 调试不是只看 Watch。真正排问题,要同时看源码、寄存器、调用栈、内存和断点。
第一组按钮:进入和退出 Debug
最常用的是这个按钮:
Start/Stop Debug Session
它通常是一个带小虫子/放大镜感觉的 Debug 图标,也可以通过菜单进入:
Debug -> Start/Stop Debug Session
快捷键一般是:
Ctrl + F5
它做的事情是:
进入调试会话 / 退出调试会话
注意,它不是普通的“下载程序”按钮。
进入 Debug 后,Keil 会加载 .axf 调试文件,连接 ST-Link,然后让你能下断点、单步、看寄存器、看变量。
如果你只是点了 Flash -> Download,程序可以烧进去运行,但你还没有进入调试会话,自然看不到这些调试窗口。
第二组按钮:Reset、Run、Stop
截图左上调试工具栏里能看到 RST,这是:
Reset CPU
它的作用是让 CPU 复位。
在 STM32 上可以理解为:
让程序重新从启动入口跑
它不是重新编译,也不是重新下载。程序还是 Flash 里的那份,只是 CPU 复位后重新执行。
Run:让程序跑起来
Run 一般是黄色箭头或播放按钮,快捷键常见是:
F5
作用:
从当前位置继续运行,直到遇到断点、手动 Stop、异常或程序停住
比如你在 if (App_Key_Read() == APP_KEY_PRESSED) 这一行打了断点,点 Run 后,程序会一直跑,直到执行到这个断点停下来。
Stop / Halt:让 CPU 停下来
Stop 或 Halt 的作用是:
让正在运行的 CPU 暂停
这对 Watch 问题很关键。
很多变量在程序高速运行时,Watch 不一定能稳定实时刷新。你应该先这样做:
Run 一会儿 -> Stop/Halt -> 再看 Watch、Registers、Memory
如果 Halt 后变量能看,说明调试信息和符号大体没问题,只是运行时实时刷新不稳定。
如果 Halt 后还是 cannot evaluate,再查变量名、符号表和作用域。
第三组按钮:单步调试
Keil 调试工具栏里最容易让新手混乱的,就是几个 Step 按钮。
常用有 4 个:
|
按钮
|
常见名字
|
大概意思
|
适合场景
|
| — | — | — | — |
|
Step Into
|
单步进入
|
执行下一句,如果是函数就钻进去
|
想看函数内部怎么跑
|
|
Step Over
|
单步跳过
|
执行下一句,如果是函数就把函数当成一句跑完
|
只关心当前函数流程
|
|
Step Out
|
跳出函数
|
把当前函数跑完,停在调用它的地方
|
不想继续看当前函数内部
|
|
Run to Cursor
|
运行到光标
|
直接跑到光标所在行
|
想快速跳到某一行
|
Step Into:钻进函数
比如当前停在:
App_Key_Init();
你点 Step Into,Keil 会尝试进入 App_Key_Init() 函数内部。
适合你想看:
这个函数到底有没有执行
函数内部变量怎么变化
有没有跑到某个判断分支
但是新手不要每一行都 Step Into。因为你可能会一路钻进标准库、启动文件、甚至汇编里,最后不知道自己在哪。
Step Over:执行这一行,但不钻进去
如果你只是想确认:
App_Key_Init();
执行完以后程序能不能继续往下走,就用 Step Over。
它会把函数当成一整句执行完,然后停在下一行。
日常调试里,Step Over 比 Step Into 用得更多。
Step Out:从当前函数出来
如果你 Step Into 进了一个函数,发现里面不是你关心的问题,可以点:
Step Out
它会继续执行到当前函数返回,然后停在调用者那里。
比如你不小心进了 HAL_Delay() 或标准库底层函数,不想一行行走完,就用它出来。
Run to Cursor:跑到光标所在行
把光标放到你想停的位置,然后点:
Run to Cursor
Keil 会直接运行到这一行。
它很适合跳过一大段初始化代码。
比如你不想一步步走 SystemInit()、NVIC、GPIO_Init(),可以把光标放在 while(1) 里,直接 Run to Cursor。
Show Next Statement:回到当前执行位置
调试时你经常会翻文件、看别的函数,看着看着就忘了 CPU 现在停在哪。
这时用:
Show Next Statement
它会把编辑器跳回当前 PC 对应的源码位置。
这不是让程序运行,也不是改变 PC,只是把界面带回“CPU 现在停住的位置”。
第四组按钮:断点按钮
截图源码左边有一个红点,这就是断点。
断点的作用是:
程序运行到这里自动停下
常见断点按钮有几类。
Insert/Remove Breakpoint
作用:
在当前行设置或取消断点
快捷键常见是:
F9
你也可以直接在代码左侧灰色边栏点一下。
红点出现,表示这里有断点。
Enable/Disable Breakpoint
有时你不想删除断点,只是暂时不让它生效。
这时用:
Enable/Disable Breakpoint
断点还在,但暂时不会拦住程序。
Disable All Breakpoints
作用:
临时禁用所有断点
适合你怀疑程序被某个断点频繁打断,但又不想一个个删。
Kill All Breakpoints
作用:
删除所有断点
这个按钮要谨慎。
调了半天好不容易布好的断点,一点它就全没了。
断点没有停住时先查什么
如果你打了红点,点 Run 后程序没有停,先查:
-
程序有没有真的执行到这一行;
-
当前代码是不是最新编译下载的;
-
Debug Information有没有勾; -
优化等级是不是太高;
-
这行代码是不是被优化掉或根本没链接进去;
-
当前调试连接是否正常。
标准库工程手动搭建时,最常见的是:
你以为自己在调这个 main.c,其实 Keil Debug 加载的是旧 axf。
所以遇到断点不进,先 Clean + Rebuild,再重新进入 Debug。
第五组按钮:窗口按钮,别只盯着 Watch
Keil 调试窗口很多,新手常常只会看 Watch。其实不同窗口解决的问题不一样。
Registers:看 CPU 现场
你截图左侧打开的是 Registers。
这里能看到:
|
寄存器
|
含义
|
调试时怎么看
|
| — | — | — |
|
R0-R12
|
通用寄存器
|
函数参数、临时变量可能在这里
|
|
R13/SP
|
栈指针
|
栈是否跑飞、HardFault 时很重要
|
|
R14/LR
|
返回地址
|
看函数准备返回到哪里
|
|
R15/PC
|
当前执行地址
|
CPU 下一条指令在哪里
|
|
xPSR
|
状态寄存器
|
看异常状态、标志位
|
当源码窗口和你感觉不一致时,看 PC 最靠谱。
比如 R15 (PC) 在 0x080001C8,说明 CPU 当前执行位置就在 Flash 某个地址。对应源码能不能显示,要看调试信息和地址映射。
Disassembly:源码对不上时看汇编
你截图中间上方是 Disassembly。
它显示的是 CPU 实际执行的汇编指令。
什么时候看它?
-
断点停在奇怪位置;
-
源码行和执行位置对不上;
-
优化后单步跳来跳去;
-
进入 HardFault 后没有对应 C 源码;
-
想看某句 C 最后生成了什么指令。
新手不需要一开始就读懂所有汇编,但要知道:
Disassembly 是最后兜底窗口。源码不可信时,它告诉你 CPU 真正在执行什么。
Call Stack + Locals:看函数从哪来,局部变量是什么
你截图右下角是:
Call Stack + Locals
这里有两个用处。
第一,看调用栈。
比如程序停在 App_Key_Read(),它可能显示:
main
App_Key_Read
这能告诉你函数是从哪里一路调用过来的。
第二,看局部变量。
局部变量不一定适合手动写进 Watch。更推荐先看 Locals,因为它只显示当前作用域里的局部变量。
如果你在 Watch 里看局部变量显示 cannot evaluate,但在 Locals 里能看到,那说明问题不是变量不存在,而是 Watch 表达式作用域没解析对。
Memory:直接看 RAM 和外设寄存器地址
Memory 窗口可以直接输入地址。
比如全局变量在:
0x20000000
你可以在 Memory 里输入:
0x20000000
看这个地址上的值。
寄存器也可以看。
比如 STM32F103 的 GPIOC 基地址:
0x40011000
你在 Memory 里输入这个地址,就能看到 GPIOC 相关寄存器区域。
这对寄存器学习特别有用。你写:
GPIOC->ODR
或者手写:
*(
volatile
uint32_t
*)
0x4001100C
最后都可以在 Memory 里验证地址上的值有没有变化。
Watch:看你指定的变量或表达式
Watch 适合看:
-
全局变量;
-
静态变量;
-
当前作用域能解析的局部变量;
-
简单表达式;
-
外设寄存器表达式。
比如:
g_key_state
g_watch_u8
GPIOC->IDR
GPIOC->ODR
但 Watch 不是万能的。它依赖:
调试信息 + 符号表 + 作用域 + 目标内存可读
所以它显示 cannot evaluate,不一定是程序没运行。
Command:调试命令窗口
截图左下角是 Command。
这里可以输入 Keil 调试命令,也会显示一些加载信息。
比如你截图里有:
Load "key\\key.axf"
这句话很关键,它告诉你 Keil 当前加载的是哪个 .axf。
如果你怀疑自己调的不是最新程序,就看这里有没有加载到你刚刚编译的目标文件。
回到粉丝问题:Watch1 为什么 cannot evaluate
现在再看粉丝的问题,就清楚多了。
他的程序能跑,说明工程大体通了。Watch 灰色,优先按这条链路查:
先 Halt -> 看 Symbols -> 看 Watch 填的名字 -> 看作用域 -> 看 Memory -> 查 Debug Information
第一件事:确认 Watch 里填的是变量,不是类型
粉丝提到:
uchar
这里一定要确认:
uchar 是变量名,还是 typedef 出来的类型名?
很多老工程里会写:
typedef
unsigned
char
uchar;
如果是这样,uchar 只是类型名,没有运行时地址。
Watch 里填:
uchar
当然可能 cannot evaluate。
正确写法应该是看具体变量:
typedef
unsigned
char
uchar;
volatile
uchar g_key_value =
0
;
Watch 里填:
g_key_value
第二件事:用 Symbols Window 搜变量
进入 Debug 后打开:
View -> Symbols Window
搜索你要看的变量。
如果 Symbols 里搜不到,Watch 里也基本不用试。
常见原因是:
-
变量没有被编译进工程;
-
变量所在
.c文件没有加入 Keil 工程; -
没有勾
Debug Information; -
没有 Clean + Rebuild;
-
当前 Debug 加载的是旧
.axf; -
变量被优化或链接丢掉。
如果 Symbols 里搜得到,建议直接拖到 Watch。
不要自己手敲,尤其是 static 变量、局部变量、同名变量比较多的时候。
第三件事:先停住 CPU 再看
不要只在 Run 状态盯着 Watch。
先这样做:
Run -> Stop/Halt -> 看 Watch
如果 Halt 后能显示,说明符号没问题,只是运行时实时刷新问题。
如果 Halt 后也不能显示,再继续查名字和作用域。
View -> Periodic Window Update 只是让窗口运行时尝试刷新,它不能解决符号不存在和表达式错误。
第四件事:调试变量先写成 volatile
建议用下面这个最小例子验证 Watch:
#include "stm32f10x.h"
typedef
unsigned
char
uchar;
volatile
uchar g_watch_u8 =
0
;
volatile
unsigned
int
g_watch_u32 =
0
;
int main(void)
{
while
(
1
)
{
g_watch_u8++;
g_watch_u32++;
}
}
正常标准库启动文件会在进 main() 前调用 SystemInit(),这里不需要手动再调一次。
然后:
-
Clean Targets;
-
Rebuild all target files;
-
进入 Debug;
-
Run 一下;
-
Stop/Halt;
-
从 Symbols Window 搜
g_watch_u8; -
拖到 Watch1。
如果这个能显示,Keil 调试链路就是通的,原来的问题大概率是变量名、作用域、旧 axf 或变量没有进入符号表。
第五件事:确认 Debug Information 和 AXF
源码级调试靠 .axf,不是靠 .hex。
Create HEX File 是生成烧录文件。
Debug Information 才是 Watch、断点、源码级调试的关键。
建议按这个顺序做:
退出 Debug
Project -> Clean Targets
Project -> Rebuild all target files
重新进入 Debug
看 Command 窗口里 Load 的 axf 路径
你截图里 Command 窗口已经显示:
Load "key\\key.axf"
如果你发现加载的不是自己刚刚编译的工程,就先别查 Watch,先把目标文件路径和工程输出目录理顺。
标准库手建工程顺手检查这几个点
粉丝列出的文件基本方向是对的,但标准库工程还要注意:
启动文件和宏要匹配
startup_stm32f10x_md.s 对应 Medium-density。
常见 STM32F103C8/CB 可以用它。
同时宏定义:
STM32F10X_MD
要和启动文件匹配。
如果芯片是大容量,比如 STM32F103ZE,就要换成对应启动文件和宏。
用标准库函数就要加对应源文件
如果你调用了:
RCC_APB2PeriphClockCmd();
GPIO_Init();
NVIC_Init();
工程里就要加入:
stm32f10x_rcc.c
stm32f10x_gpio.c
misc.c
否则可能编译链接报错。
但注意,这通常不是 Watch cannot evaluate 的直接原因。标准库源文件少了,更多表现为函数未定义、外设不工作,而不是变量名无法求值。
Use MicroLIB 不是 Watch 的核心开关
Use MicroLIB 常影响 printf、库函数、代码体积等。
Watch 变量显示主要看:
Debug Information
Symbols
作用域
当前调试状态
目标内存访问
所以不要把所有调试问题都归到 MicroLIB 上。
一套推荐的 Keil 调试流程
以后调 STM32,不建议一进 Debug 就乱点按钮。
可以按这个流程:
-
Rebuild,确认 0 Error。 -
Download或直接进入 Debug 自动下载。 -
进入 Debug。
-
点
RST,让 CPU 复位到初始状态。 -
在关键代码行左侧点红点打断点。
-
点
Run,让程序跑到断点。 -
停住后先看
Call Stack + Locals。 -
再看
Watch里的全局变量。 -
如果 Watch 不行,用
Symbols找变量。 -
如果变量地址明确,再用
Memory看 RAM。 -
如果源码对不上,再看
Disassembly和R15(PC)。 -
单步时优先用
Step Over,需要看函数内部再用Step Into。
这个流程比“看变量不对就改代码”稳得多。
一张表记住常用按钮
|
按钮/窗口
|
作用
|
新手最常用场景
|
| — | — | — |
|
Start/Stop Debug Session
|
进入或退出调试
|
开始调试前必须进这里
|
|
RST / Reset CPU
|
CPU 复位
|
想从头重新跑程序
|
|
Run
|
继续运行
|
跑到断点或让程序正常执行
|
|
Stop / Halt
|
暂停 CPU
|
Run 后停下来观察变量
|
|
Step Into
|
单步进入函数
|
想看函数内部
|
|
Step Over
|
单步跳过函数
|
只看当前流程,不钻进函数
|
|
Step Out
|
跳出当前函数
|
不想继续看当前函数内部
|
|
Run to Cursor
|
跑到光标行
|
快速跳过一段初始化
|
|
Show Next Statement
|
回到当前执行行
|
翻代码后找不到 PC 位置
|
|
Insert/Remove Breakpoint
|
设置/取消断点
|
让程序跑到某行停下
|
|
Disable Breakpoint
|
暂时禁用断点
|
保留断点但先不触发
|
|
Kill All Breakpoints
|
删除所有断点
|
清理调试现场,谨慎使用
|
|
Registers
|
看 CPU 寄存器
|
PC/LR/SP、异常排查
|
|
Disassembly
|
看汇编
|
源码和执行位置对不上
|
|
Call Stack + Locals
|
看调用栈和局部变量
|
函数从哪来、局部变量是多少
|
|
Memory
|
按地址看内存/寄存器
|
直接看 RAM 或外设寄存器
|
|
Watch
|
看指定变量/表达式
|
看全局变量、状态变量
|
|
Command
|
看加载信息、输入命令
|
确认加载了哪个 .axf
|
最后给这个粉丝的直接回复
你这个现象,程序能运行,说明工程启动和下载大概率没问题。Watch1 cannot evaluate 优先不要怀疑标准库文件。
建议你先这样查:
-
进入 Debug 后先
Run,再Stop/Halt,停住 CPU 后看变量。 -
打开
Symbols Window,搜索你要看的变量名。 -
如果你 Watch 里填的是
uchar,确认它是不是typedef unsigned char uchar;。如果是,它是类型名,不是变量名,应该填真实变量名。 -
写一个
volatile unsigned char g_watch_u8的最小测试变量,确认 Watch 链路是否正常。 -
Clean + Rebuild 后重新进入 Debug,看 Command 窗口加载的
.axf是否是最新的。 -
如果 Symbols 能看到变量,但 Watch 不能显示,直接从 Symbols 拖到 Watch。
-
如果 Halt 后能看、Run 时不能看,说明主要是实时刷新问题,用断点或 Halt 看变量更稳。
调试这件事,按钮不用一次全记住。先掌握四个动作就够了:
打断点 -> Run 到断点 -> Step Over 看流程 -> Halt 后看变量
剩下的 Registers、Memory、Disassembly,是你遇到“源码看不明白、变量看不到、程序跑飞”时再拿出来兜底的工具。
你们平时用 Keil 调试,最常卡住的是 Watch 看不到变量,还是断点打不上?
更多推荐
所有评论(0)