Debugging Your Hardware
There are a lot of things that can go wrong when building custom hardware. In the software world, tooling builds in debugging support automatically until you explicitly turn it all off. We don’t have that luxury in hardware so we must do it ourselves.
You have already seen testbenches, which are an important tool for testing the functional correctness of your code. However they can only test things at the software abstraction level. They can’t determine issues that come from your use of directives, or from the interfacing between the Arm CPUs and the custom hardware.
Debug Interfaces
Let’s imagine the following custom hardware IP core:
uint32 toplevel(uint32 *ram) {
#pragma HLS INTERFACE m_axi port=ram offset=slave bundle=MAXI
#pragma HLS INTERFACE s_axilite port=return bundle=AXILiteS
ram[2] = ram[1] + ram[0];
return 0;
}
This hardware simply adds two number in main memory together and stores the result in a third. If we run this and nothing happens, how do we know what is wrong? There are (at least) the following problems that we need to isolate in order to debug this:
Your code
Some problem in the tooling - a corrupted project etc.
You forgot to rebuild and reexport the hardware
The AXI Slave interface (IP core to CPU)
The AXI Master interface (IP core to main memory)
and probably a few other things
We can make the situation better by building in debug interfaces. Look at the following small modification.
uint32 toplevel(uint32 *ram, int version) {
#pragma HLS INTERFACE m_axi port=ram offset=slave bundle=MAXI
#pragma HLS INTERFACE s_axilite port=return bundle=AXILiteS
#pragma HLS INTERFACE s_axilite port=version bundle=AXILiteS
version = 42;
ram[2] = ram[1] + ram[0];
return 0;
}
We’ve added a version
parameter and bundled it into the AXI Slave interface. Now from the Arm CPU we can use the generated API call (which will be called XToplevel_Get_version()
) to read this, and if we get 42
then we know that our projects are set up correctly and that we have wired up the slave interface. We can also at any point change this value, rebuild everything, and reassure ourselves that we are running the latest version of everything.
Debug Modes
We can go further and add full debugging modes into our hardware.
uint32 toplevel(uint32 *ram, int version, int mode, int arg1, int arg2) {
#pragma HLS INTERFACE m_axi port=ram offset=slave bundle=MAXI
#pragma HLS INTERFACE s_axilite port=return bundle=AXILiteS
#pragma HLS INTERFACE s_axilite port=version bundle=AXILiteS
#pragma HLS INTERFACE s_axilite port=mode bundle=AXILiteS
#pragma HLS INTERFACE s_axilite port=arg1 bundle=AXILiteS
#pragma HLS INTERFACE s_axilite port=arg2 bundle=AXILiteS
version = 42;
switch(mode) {
//Do our normal thing
case 0:
ram[2] = ram[1] + ram[0];
return 0;
//Simple AXI Slave test
case 1:
return arg1 + arg2;
//AXI Master read tests
case 2:
return ram[0];
case 3:
return ram[arg1];
//AXI Master write tests
case 4:
ram[arg1] = arg2;
return 1;
}
}
Here, we have added alternate execution modes that let us test different bits of functionality, thereby narrowing down the problem. By setting mode
and the debug parameters arg1
and arg2
we can peek and poke main memory and thereby observe the effects from the CPU. If mode 1 is working, but mode 2, 3 and 4 aren’t, then maybe we wired up the slave interface correctly but not the master interface.
You can carry this concept forward into your actual hardware design. Instead of treating it as a single monolithic block, alternate execution modes and debugging registers can help a lot with figuring out why something isn’t doing what you expect.