Debugging techniques
It’s significantly harder to debug a C program once it has been synthesized into logic: The compilation cycles are much longer, and there is no debugger one can attach to the program as it runs. No breakpoints, no stepping, no variable displays.
On the other hand, with Xillybus it’s possibly to print out “printf-style” debugging messages to the console in real time. There are three functions in the xilly_debug.c package for this purpose: xilly_puts(), xilly_decprint() and xilly_hexprint(), which are detailed below.
To have these debug messages displayed, the following (Linux shell) command should be issued before opening the data transport pipes:
$ cat /dev/xillybus_read_8
This will dump any debug message generated until it’s stopped with CTRL-C. Windows users may use the streamread application, which is part of the Windows pack for Xillybus.
Note: When the debugging functionality isn’t used, three lines in xillydemo.v must be commented out, as mentioned at the end of part V. When returning to use debugging, these lines must be restored, or no data will appear on the console.
Debugging functions
The debugging functions can be called from anywhere in the code, and not only from the wrapper function, as shown in the example.
The xilly_debug.c source file should be included in the project, and the following statement in every source file which calls the function:
#include "xilly_debug.h"
These are the three functions available:
- void xilly_puts(const char *str) — Prints a null-terminated string, just like printf() with no parameters. For example,
xilly_puts("Hello, world\n");
Note the line feed at the end of the string. In its absence, the following output remains on the same line (which may be desired).
- void xilly_hexprint(const uint32_t val, const int digits) – Prints the 32-bit unsigned integer given as the first argument as a hexadecimal number. The second argument is the minimal number of hex-digits (”nibbles”) to be printed (by adding zeros to the left). Legal values are between 0 and 8, inclusive. Other values will result in unpredictable behavior. Typical use:
xilly_hexprint(x, 8); // Like printf("%08x", x) xilly_hexprint(x, 1); // Like printf("%x", x)
- void xilly_decprint(const uint32_t val, const int digits) — Prints the 32-bit unsigned integer given as the first argument as a plain decimal number. The second argument is the minimal number of digits to be printed (by adding zeros to the left). Legal values are between 0 and 10, inclusive. Other values will result in unpredictable behavior. For example,
xilly_decprint(x, 1); // Like printf("%d", x)
There is no real printf() or sprintf() functions available for HLS, as these functions would waste by far more logic resources than needed to perform the desired task. The human readable debug output of a single value can therefore span three statements:
xilly_puts("The value is now"); xilly_decprint(thevalue, 1); xilly_puts("\n");
It does therefore make sense to encapsulate a sequence like this in a function or macro. Writing additional functions for other data formats is fairly straightforward.
Execution reordering
It’s important to remember that the debug messages may arrive sooner than they would in a classic C program execution: The HLS synthesizer attempts to parallelize the execution flow by evaluating each expression as soon as possible. Consequently, it's useless to put xilly_puts() calls to mark “I arrived here in the code” debugging messages: The output may arrive before reaching that point in the code, because the xilly_puts() can in theory be run sooner, as it doesn’t depend on any input data. When these functions actually fire off in a real execution depends on several factors, and the behavior isn’t repeatable from one synthesis to another.
The rule to keep in mind is: The HLS compiler assures that the output’s content is as expected from the C code, but it’s time of execution may change. So if a xilly_hexprint() call comes before a xilly_puts(), the latter will be executed only after the data needed for xilly_hexprint() is available, and the call is completed.
Consider, for example, the following function:
void xillybus_wrapper(int *in, int *out) { #pragma AP interface ap_fifo port=in #pragma AP interface ap_fifo port=out #pragma AP interface ap_ctrl_none port=return int x1; x1 = *in++; xilly_puts("Hello, world\n"); // When is this printed? xilly_puts("x1="); xilly_hexprint(x1, 1); xilly_puts("\n"); }
The call xilly_puts() that prints out “Hello, world” is after the assignment to x1 from the FIFO’s input. The question is: Will this output appear before any data has been sent the data pipe? Will the “x1=” part be printed out as well?
The answer is that it may, and it may not. The only thing that is certain, is that the xilly_hexprint() call will not take place before any data has been sent, because the printed output depends on the input from the data pipe. And nothing will happen, of course, until both the input and output pipes are opened, because the logic is held in reset until then.