“Debugging” is the act of finding the root cause behind a system’s failure to perform as expected. In the case of digital systems, sometimes hardware circuits do not behave as expected, and sometimes software produces unexpected results. A hardware debugger lets you visualize and analyze any given signal in a circuit or system, and a software debugger lets you visualize and analyze any given register or memory location in a system. To use a hardware debugger (or “logic analyzer”), logic values (1’s and 0’s) are driven on relevant inputs, system is evolved by applying some number of clocks or other timing signals, and then relevant outputs are checked to see if they have attained the desired state. To use a software debugger, relevant memory locations are loaded with initial values, the system is evolved by applying some number of “steps” (or CPU clocks), and then relevant memory locations are examined to see if they contain the desired results. In both cases, the idea is to place some collection of the most basic “moving parts” into a known state, let the system evolve according to its design, and then examine another collection of moving parts to learn, in the most detailed sense, how the system changed.
Hardware debuggers are typically high-speed signal sampling devices, often called logic analyzers, that measure and record digital signals at a rate faster than they are expected to change (so, a 100MHz system would need a logic analyzer that samples at a minimum of 200MHz). The ZYNQ device includes an internal logic analyzer/hardware debugger that can examine the state of any signal in the FPGA, and it is invaluable in finding hardware bugs. It is available in the Vivado tool, but we will not explore its use here (you are encouraged to experiment with it on your own!).
Vitis includes a software debugger that you can use to step through your code, one instruction at a time, and watch as individual instructions change register and/or memory contents. By examining the effect of every single instruction, you can see exactly what your software is doing, and diagnose and fix any problem. For simpler programs, a software debugger lets you see exactly what is happening at every step, and that greatly adds to you understanding. As software projects get larger and more complex, software debuggers provide critical information about program operation, and it is fair to say their use is indispensable.
The screen shot below shows a typical “design view” of the Vitis workspace. The debugger tool can be launched by clicking on the “bug” icon next to the run icon. Note also you can toggle between the design and debug views using the buttons in the upper right.
Launching the debugger will start the debugger tool, switch to the Debug view, and highlight the first instruction. In the view below, the default window appearance has been altered a bit (the “memory” window was closed because no memory contents are relevant in this program, and the register and disassembly windows were made larger). In the debugger controls section, you can run the next instruction be clicking on one of the three “step” icons. The left “step into” icon causes the next instruction to be executed, including all branches into and out of subroutines (this is the “normal” step function). The middle “step over” icon also causes the next instruction to be executed, but it treats a subroutine call as a single instruction (the debugger executes the entire subroutine in one step, and then highlights the instruction immediately following the branch to await the next step command). The right “step return” can be used from inside a subroutine to cause the subroutine to finish, and return to the calling program.
In the register window, clicking on the “General Registers” column header will cause all registers to be displayed so you can monitor their content as the program progresses.
The disassembly window shows the instruction mnemonics with the actual immediate values calculated by the assembler – these mnemonics are directly translatable to machine codes, so these are the instructions that will be executed. For example, note the highlighted instruction in the source window (ldr r1,=LED_DATA). The same instruction is highlighted in the disassembly window, but now the instruction shows ldr r1, [pc, #128]. The assembler replaced the constant in the pseudoinstruction (=LED_DATA) with a pointer to an area of memory that contains the number to be loaded (0x41210000). The assembler placed that number 128 bytes past the current PC – if you scroll down in the disassembly window, you can see address location 0x0010060C (which is 128 bytes past the PC for the original instruction), and that 0x41210000 is stored at that location.