概要

在 Zynq 裸机开发过程中,程序写出来之后并不一定能一次运行正确。很多时候,程序无法实现预期功能,并不是因为语法错误,而是由于逻辑设计不合理、变量值异常、寄存器配置错误,或者程序流程与预期不一致。这时,掌握在线调试方法就显得尤为重要。本文结合 Xilinx SDK 开发环境,介绍 Zynq 裸机程序的基本调试思路,包括如何进入调试模式、如何设置断点、如何单步执行程序、如何查看局部变量与全局变量,以及如何通过 Memory 窗口观察 DDR 和外设寄存器内容,为后续独立分析和定位程序问题打下基础。

关键词

Zynq;裸机开发;SDK;Debug;断点;变量;Memory;在线调试

一、前言

在前面的内容中,我们已经学习了基于 SDK 硬件驱动库的编程方法,也通过 GPIO 实现了 LED 点亮和简单控制。
但是在真正的开发过程中,程序并不会总是一次就写对。

很多初学者都有过类似经历:

  • 程序可以编译通过,但运行现象不对;
  • LED 不亮,或者亮灭节奏不对;
  • 按键逻辑和预期不一致;
  • 串口没有输出;
  • 程序似乎“卡住了”,却不知道停在什么地方。

这类问题,单靠“猜”是很难解决的。
真正有效的方法,是进入调试状态,一步一步观察程序运行过程,查看变量值、分支走向和寄存器状态,从而找到问题所在。

这就是所谓的 Debug,也常被很多开发者称为在线调试或在线仿真。

对于 ARM 裸机开发来说,调试能力并不是附属技能,而是非常核心的开发能力。
如果说写代码是“搭系统”,那么调试代码就是“查问题”。一个只会照着例程抄程序、但不会调试程序的人,很难真正具备独立开发能力。

因此,这一篇就来系统整理 Zynq 裸机程序的基本调试方法。

二、什么是在线调试

所谓在线调试,就是在程序下载到开发板后,不是让它直接全速运行,而是让开发环境接管程序执行过程,从而实现:

  • 控制程序暂停和继续;
  • 单步执行每一行代码;
  • 设置断点,在指定位置自动停下;
  • 查看当前变量值;
  • 修改某些变量值;
  • 观察内存数据;
  • 查看外设寄存器状态。

对于 Zynq 来说,Xilinx SDK 已经提供了较为便捷的在线调试功能。
开发者不需要额外搭建复杂环境,只要在 SDK 中进入调试模式,就可以完成大部分基础调试工作。

可以把它简单理解为:

程序不再是“一跑到底”,而是可以被我们“暂停、观察、分析、继续”。

这对于分析裸机程序问题非常重要。

三、为什么裸机开发一定要学会调试

在裸机开发中,很多错误并不会明确报出来。
例如:

  • 某个 GPIO 方向没有配置对;
  • 某个循环条件写错了;
  • 某个寄存器写入值不正确;
  • 某个数组没有按预期存到 DDR;
  • 某个分支根本没有执行进去。

这些问题和普通 PC 软件编程不太一样。
普通应用程序出错时,可能会弹出异常信息,或者直接报错退出;但裸机程序很多时候只是“现象不对”,并不会告诉你具体哪里错了。

这时,调试的作用就体现出来了。

通过调试,我们可以回答下面这些关键问题:

  • 程序到底有没有执行到某一行?
  • 变量的值是不是和预期一样?
  • if 分支到底进了哪一边?
  • 某个函数是否真的被调用了?
  • DDR 中是否真的写入了想要的数据?
  • 外设寄存器当前值是不是对的?

所以说,在线调试对 ARM 裸机开发的意义,有点像 ModelSim 对 FPGA 逻辑设计的意义:

它不是可有可无的辅助工具,而是定位问题、分析问题的重要手段。

四、调试时主要关注哪些内容

在实际调试中,通常最常看的有四类信息:

  1. 变量(Variables)

用于查看当前函数中的局部变量,以及部分能够直接识别到的全局变量。

  1. 断点(Breakpoints)

用于让程序运行到指定代码行时自动停下,便于集中分析某一位置附近的逻辑。

  1. 表达式(Expressions)

用于手动添加想观察的变量、地址或表达式,便于持续监视其变化。

  1. 存储器(Memory)

用于查看某段 DDR 内存中的数据,或者直接查看某个外设寄存器地址上的内容。

这四类信息基本构成了裸机程序调试时最常用的观察窗口。

五、如何进入调试模式

在 SDK 中完成程序编译后,通常可以通过工具栏或右键菜单进入调试模式。
进入调试状态后,开发环境会自动连接处理器,并将程序加载到目标板运行环境中。

与普通“Run”不同,进入“Debug”后,程序不会简单地全速跑完,而是会在入口位置或者预设断点处停下来,等待开发者进一步控制。

进入调试模式后,SDK 的界面通常会切换到专门的 Debug 视图,此时可以看到:

  • 当前程序执行位置
  • 调用栈
  • 断点信息
  • 变量窗口
  • 内存窗口
  • 表达式窗口

对于初学者来说,第一次看到这些界面可能会觉得有点复杂,但其实只要抓住几个最常用的功能,就足够完成大部分基本调试任务。

六、单步调试的几种基本操作

进入调试模式之后,最常用的就是对程序执行流程进行控制。

  1. 单步运行

单步运行的意思是:

让程序执行当前这一行,然后停在下一行。

这适合用来观察每执行一步后,变量值发生了什么变化。
例如在一个 for 循环中,就可以通过单步执行来看循环变量 i 是如何变化的。

  1. 进入函数

当程序执行到某个函数调用语句时,如果选择“进入函数”,那么调试器会直接跳进这个函数内部,逐行观察函数内部代码执行过程。

这在分析驱动函数、自己封装的函数或者判断某个函数到底做了什么时,非常有用。

  1. 跳出函数

如果当前已经在一个函数内部,但不想继续一行一行往下跟,就可以使用“跳出函数”。
这样程序会把当前函数剩余部分执行完,然后停在调用该函数之后的位置。

  1. 全速运行

当你已经分析清楚某一段逻辑,不想再慢慢单步执行时,可以让程序恢复全速运行,直到遇到下一个断点,或者程序自然结束。

这在调试较长程序时能大幅节省时间。

七、断点的添加与使用

如果每次都从 main() 开始一行一行往下单步执行,会非常低效。
特别是程序前面有大量初始化代码,而你真正关心的是后面某个位置时,这种方式就很不实用了。

这时候就要用到断点。

  1. 什么是断点

断点可以理解成程序运行过程中的一个“拦截点”。
当程序执行到设置断点的那一行时,会自动停下来,等待开发者查看当前状态。

  1. 为什么要用断点

断点最大的作用,就是让程序:

平时全速运行,只在你关心的位置停下。

例如你想分析某个 if 分支是否会进入,或者想看某个循环跑到第几次时出现异常,就可以在对应位置设置断点。

  1. 断点的基本操作

调试时,常见的断点操作包括:

  • 添加断点
  • 取消断点
  • 暂时禁用断点
  • 查看当前所有断点

对于初学者来说,最实用的用法通常就是:

  • 在怀疑有问题的代码行打断点;
  • 全速运行程序;
  • 程序停下后查看变量与流程;
  • 根据结果继续单步或继续运行。

这样比从头慢慢单步要高效得多。

八、如何查看局部变量和全局变量

程序停在某一位置后,最先要看的通常就是变量。

  1. 局部变量

局部变量是当前函数内部定义的变量,例如:

int i;
int rev[32];

当程序执行到这些变量所在作用域时,调试器通常会自动在 Variables 窗口中显示它们的当前值。

通过查看局部变量,我们可以判断:

  • 循环变量是不是按预期递增;
  • 某个数组元素有没有被正确赋值;
  • 某个临时结果是不是异常;
  • 某个函数内部计算结果是否正确。
  1. 全局变量

全局变量不一定总会自动显示在局部变量窗口里,这时可以通过 Expressions 窗口手动添加需要观察的变量名。

全局变量的调试特别适合用来观察:

  • 程序状态标志位
  • 全局缓冲区
  • 中断标记
  • 外设控制状态

在某些情况下,调试器还允许直接修改变量值,这对于验证程序逻辑也很有帮助。例如你怀疑某个分支条件永远进不去,就可以尝试修改变量值,看程序行为是否改变。

九、Expressions 的作用

Expressions,也就是表达式窗口,是调试中非常好用但容易被忽视的功能。
它和 Variables 的区别在于:

  • Variables 更偏向于自动显示当前作用域中的变量;
  • Expressions 则允许你主动添加想观察的内容。

例如你可以添加:

  • 某个局部变量
  • 某个全局变量
  • 某个数组元素
  • 某个指针地址
  • 某个表达式结果

这对于长期监视某个关键变量特别有用。

举个例子,如果你正在调试 LED 闪烁程序,就可以把控制状态变量、计数变量、GPIO 输出变量都加入 Expressions 窗口中,这样每次单步执行时都能快速看到它们的变化,而不用反复展开查看。

十、如何查看 Memory 内容

在裸机程序调试中,Memory 窗口非常重要。
因为很多时候我们关心的并不只是普通变量,而是某一段固定地址中的内容。

Memory 窗口主要有两个典型用途:

  1. 查看 DDR 中的数据

比如在数据采集系统中,FPGA 可能会把采集到的数据通过 AXI 或 DMA 写入 PS 侧 DDR。
这时如果想知道数据到底有没有写进去、写进去后内容对不对,就可以直接在 Memory 窗口中输入对应 DDR 地址,查看那段内存中的真实数据。

  1. 查看外设寄存器值

从 CPU 的角度看,外设控制器也是映射到地址空间中的。
因此,外设寄存器本质上也可以像内存一样查看。

例如,如果你想知道 GPIO 控制寄存器的当前值,或者确认某个 UART 状态寄存器是否变化,就可以把相应寄存器地址加到 Memory 窗口中观察。

这一点非常重要,因为有时程序看起来“写了寄存器”,但实际上写入值并不对,或者某个寄存器值根本没变。
直接看 Memory,往往比单纯猜测更可靠。

十一、通过示例理解 Memory 调试方法

下面这个例程就是一个很典型的 DDR 调试示例:

#include "xparameters_ps.h"
#include "xil_io.h"

#define DDR_BASEARDDR      XPAR_DDR_MEM_BASEADDR + 0x10000000

int main()
{
    int i;
    int rev[32];

    for(i=0; i<32; i++)
    {
        Xil_Out32(DDR_BASEARDDR+i*4, i);
    }

    for(i=0; i<32; i++)
    {
        rev[i] = Xil_In32(DDR_BASEARDDR+i*4);
    }

    return 0;
}

这段程序做了两件事:

第一步,把 0 到 31 这 32 个整数依次写入 DDR 某段地址空间;
第二步,再把这 32 个地址中的内容读回来,存到数组 rev[32] 中。

  1. 这段程序为什么适合调试演示

因为它同时涉及:

  • 局部变量 i
  • 局部数组 rev
  • 固定地址的 DDR 空间
  • 写内存操作
  • 读内存操作

所以在调试时,我们可以从多个角度观察程序运行结果。

  1. 可以观察什么

调试这段程序时,可以重点观察:

  • i 在循环中是否按预期从 0 递增到 31;
  • rev[i] 中读回的数据是否和写入的一致;
  • DDR_BASEARDDR 起始地址对应的内存区域中,是否真的出现了 0、1、2、3……31 这些数据。

如果程序逻辑正确,那么最终应该看到:

  • DDR 中保存的是顺序递增数据;
  • rev 数组中也保存着相同内容。
  1. 这个例子说明了什么

这个例子很好地说明了一点:

调试不仅可以看“变量”,也可以直接看“地址空间中的数据”。

这对于后续分析 DDR 缓冲区、图像数据、采样数据、DMA 结果等场景非常有帮助。

十二、调试外设寄存器的思路

除了看 DDR,Memory 窗口还可以直接用于观察外设寄存器。

例如你正在调试 GPIO 点灯程序,就可以把 GPIO 控制器对应的基地址输入到 Memory 窗口里,然后重点观察:

  • 方向寄存器值是否正确
  • 输出使能寄存器值是否正确
  • 数据寄存器值是否发生变化

这样你就能把“程序行为”和“寄存器变化”对应起来。

举个简单例子:

  • 如果程序里调用了设置方向为输出的函数,但在 Memory 中看到方向寄存器那一位并没有被置位,那么说明程序并没有真正把配置写进去;
  • 如果方向已经正确,但数据寄存器始终没变化,那问题可能出在输出逻辑;
  • 如果寄存器都对,但 LED 还是不亮,那就要进一步考虑硬件连接、电平极性或引脚复用配置。

这就是在线调试相比单纯看代码的最大价值:

它能把“代码逻辑”和“硬件实际状态”联系起来。

十三、调试程序时的一般分析流程

很多初学者虽然知道怎么点调试按钮,但真到程序出问题时,还是不知道从哪开始看。
这里可以总结一个比较实用的思路。

  1. 先确认程序有没有跑到关键位置

最先做的,不是急着看一堆变量,而是先打断点确认程序是否真的执行到了你关心的那一段代码。

  1. 再看关键变量是否正常

如果程序执行到了目标位置,就重点看几个关键变量,例如:

  • 循环变量
  • 条件判断变量
  • 状态标志位
  • 输入输出数据

不要一开始就什么都看,重点盯几个最可能影响逻辑的变量。

  1. 再看分支走向是否符合预期

if、while、for 这些地方,最容易出现逻辑偏差。
要重点观察程序到底进了哪个分支,而不是只看自己“以为它该进哪里”。

  1. 最后结合 Memory 看寄存器或 DDR

如果变量和流程都基本正常,但现象还是不对,就要进一步看底层地址空间中的数据,确认:

  • DDR 是否写入成功
  • 寄存器是否真的被正确配置
  • 某些状态寄存器是否有预期变化

这个流程比“哪里不对就乱点一通”要有效得多。

十四、在线调试和 FPGA 仿真的类比

对于学过 FPGA 的同学来说,可以把 ARM 裸机的在线调试类比成软件侧的“仿真分析”。

在 FPGA 逻辑开发里,大家习惯用 ModelSim 去看波形、查时序、找问题;
而在 ARM 裸机开发里,SDK 的在线调试就承担了类似角色:

  • 观察程序一步步执行
  • 看变量如何变化
  • 看存储器中数据是否正确
  • 分析程序为什么没有实现预期功能

虽然形式不同,一个看波形,一个看代码执行过程,但目的其实是一样的:

都是在问题发生时,找到“程序实际做了什么”,而不是只凭主观猜测。

十五、初学者调试时容易出现的几个问题

  1. 只会全速运行,不会打断点

很多人程序一运行就直接点 Run,结果现象不对也不知道从哪看起。
正确做法是先找到关键位置打断点,让程序在最值得分析的地方停下来。

  1. 只看代码,不看变量

有些同学调试时还是盯着代码发呆,觉得“我写的应该没问题”。
但调试最重要的是看运行结果,而不是反复脑补代码逻辑。

  1. 只看变量,不看寄存器或内存

在裸机开发中,很多问题最终都要落到地址空间上。
如果只看 C 变量,不去确认寄存器值和 DDR 内容,有时会漏掉真正关键的信息。

  1. 不会结合现象来定位范围

例如 LED 不亮,不应该一上来就什么都看,而应该先想:

  • 程序跑到控制 LED 的那一行了吗?
  • GPIO 配置成功了吗?
  • 数据寄存器变了吗?
  • 板子的 LED 是高电平点亮还是低电平点亮?

调试不是盲目查看,而是带着问题逐步缩小范围。

十六、知识小结

本文围绕 Zynq 裸机程序调试方法,主要介绍了以下内容:

  • 在线调试是 ARM 裸机开发中非常重要的程序分析方法。
  • SDK 提供了便捷的 Debug 功能,可以让程序暂停、单步执行和全速运行。
  • 调试时最常关注的内容包括 Variables、Breakpoints、Expressions 和 Memory。
  • 断点可以让程序在指定位置自动停下,大大提高调试效率。
  • Variables 适合查看局部变量和部分全局变量,Expressions 适合主动添加需要长期观察的对象。
  • Memory 窗口既可以查看 DDR 中的数据,也可以查看外设寄存器值。
  • 通过观察固定地址空间的数据,可以更直观地分析程序是否真正完成了读写操作。
  • 调试程序时,建议按照“确认流程位置—查看关键变量—分析分支走向—检查 Memory 内容”的思路逐步定位问题。
  • 在线调试对 ARM 裸机开发的重要性,类似于 ModelSim 对 FPGA 逻辑设计的重要性。
Logo

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

更多推荐