Keil5 调试按钮别乱点:从这张 Debug 页面开始,把 Run、Step、Watch、Registers 讲明白

有个粉丝问了一个问题:

他用 STM32F103 标准库手动新建 Keil5 工程,程序能正常运行,但是进入 Debug 后,Watch1 里的全局变量一直灰色,显示:

cannot evaluate

他还把 Keil 的调试界面截图发了过来。

这张图非常适合拿来讲 Keil5 调试。因为很多新手不是不会写代码,而是进入 Debug 页面以后,不知道这些按钮分别干什么:

  • RST 是复位还是重新下载?

  • 黄色箭头停在哪一行是什么意思?

  • 红点是断点,那灰色块又是什么?

  • RunStopStep IntoStep Over 到底该怎么用?

  • RegistersCall Stack + LocalsMemoryWatch 各看什么?

  • 为什么程序在跑,变量却 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()NVICGPIO_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 后程序没有停,先查:

  1. 程序有没有真的执行到这一行;

  2. 当前代码是不是最新编译下载的;

  3. Debug Information 有没有勾;

  4. 优化等级是不是太高;

  5. 这行代码是不是被优化掉或根本没链接进去;

  6. 当前调试连接是否正常。

标准库工程手动搭建时,最常见的是:

你以为自己在调这个 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(),这里不需要手动再调一次。

然后:

  1. Clean Targets;

  2. Rebuild all target files;

  3. 进入 Debug;

  4. Run 一下;

  5. Stop/Halt;

  6. 从 Symbols Window 搜 g_watch_u8

  7. 拖到 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 就乱点按钮。

可以按这个流程:

  1. Rebuild,确认 0 Error。

  2. Download 或直接进入 Debug 自动下载。

  3. 进入 Debug。

  4. 点 RST,让 CPU 复位到初始状态。

  5. 在关键代码行左侧点红点打断点。

  6. 点 Run,让程序跑到断点。

  7. 停住后先看 Call Stack + Locals

  8. 再看 Watch 里的全局变量。

  9. 如果 Watch 不行,用 Symbols 找变量。

  10. 如果变量地址明确,再用 Memory 看 RAM。

  11. 如果源码对不上,再看 Disassembly 和 R15(PC)

  12. 单步时优先用 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 优先不要怀疑标准库文件。

建议你先这样查:

  1. 进入 Debug 后先 Run,再 Stop/Halt,停住 CPU 后看变量。

  2. 打开 Symbols Window,搜索你要看的变量名。

  3. 如果你 Watch 里填的是 uchar,确认它是不是 typedef unsigned char uchar;。如果是,它是类型名,不是变量名,应该填真实变量名。

  4. 写一个 volatile unsigned char g_watch_u8 的最小测试变量,确认 Watch 链路是否正常。

  5. Clean + Rebuild 后重新进入 Debug,看 Command 窗口加载的 .axf 是否是最新的。

  6. 如果 Symbols 能看到变量,但 Watch 不能显示,直接从 Symbols 拖到 Watch。

  7. 如果 Halt 后能看、Run 时不能看,说明主要是实时刷新问题,用断点或 Halt 看变量更稳。

调试这件事,按钮不用一次全记住。先掌握四个动作就够了:

打断点 -> Run 到断点 -> Step Over 看流程 -> Halt 后看变量

剩下的 Registers、Memory、Disassembly,是你遇到“源码看不明白、变量看不到、程序跑飞”时再拿出来兜底的工具。

你们平时用 Keil 调试,最常卡住的是 Watch 看不到变量,还是断点打不上?

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐