Skip to content

Example: Debug Probes

Julian Kemmerer edited this page Nov 15, 2020 · 5 revisions

wave

Say you are describing some hardware (ex. a DDR3 mem test)...you have a state machine and some related signals (ex. state, address, data) you want to probe for debug in hardware testing.

Your existing option is something like a MARK_DEBUG attribute in your HDL + going through your manufacturer specific 'chipscope/signal tap' flow - which I have not had much fun with personally - annoying every time I need to use it.

Otherwise, doing this without manufacturer support, maybe you are lucky enough to have setup some kind of debug structs/record ports where you easily can route debug signals around your design hierarchy for some existing debug ports you've planned up. Or worse you are adding new ports and connections all over your design hierarchy to get to these signals - boo.

Here is why this is easy in PipelineC:

To start, look to the ability to connect point-to-point wires from one point in the design hierarchy to other. With a single line of code (a macro), a design can connect a point to point wire from the signal you want to capture to some debug probe module/port (the compiler wires up and down across whatever design hierarchy exists for you).

    WIRE_WRITE(some_c_type_t, probe3, test_data) 
    // probe3=test_data 
    // some signal test_data of type some_c_type_t in the design

This lets you write a stand alone 'library module' for capture of debug signals probes.c. Each probe is exposed as a point to point wire from the probes module to wherever the user uses it. Here is how to 'use the library':

  1. Define the probe data type in a header file #define probe3_t some_c_type_t //probe3.h
  2. #include "probes.c" in the code you want to debug
  3. Add one or more WIRE_WRITE 's driving the probes you've defined

So what does this probes.c library module do for you? It samples your debug probe and streams out the data to a host pc (over UART for now). Here is a high level diagram of whats going on:

diagram

Ok so this lets you get a one time capture of some_c_type_t of data. Thats neat. But combined with a bit more logic its pretty powerful:

Say your some_c_type_t is defined like so from that memory test app example:

#define DEBUG_SAMPLES 32
typedef struct app_debug_t
{
  mem_test_state_t state[DEBUG_SAMPLES];
  uint32_t test_addr[DEBUG_SAMPLES];
  uint8_t test_data[DEBUG_SAMPLES];
}app_debug_t;

With a little FSM to shift data into those arrays inside the app_debug_t reg each clock cycle, you can capture samples over time (32 clocks here), and then one time sample that entire debug probe.

WIRE_WRITE(app_debug_t, probe0, debug_shift_reg) 

PipelineC comes with code generation support so the packing of app_debug_t to and from bytes is handled for you. That makes writing the supporting software C code very easy: write the UART port to identify the probe to read, then read back the probe bytes - converted to the struct form by generated code.

The simple debug_probes.c is such a little C program and plot_probes.py is a helper script that takes the probes printout and starts up GTKWave to view the probe data (see image at top of post). Let's you do a one liner capture and show waveforms:

./debug_probes 0 | plot_probes.py

A final note is that this 'capture logic' of shifting sampled data into registers over time - it can be whatever you want. Want to capture data only when your FSM changes state? Every 3rd cycle? Into block ram / fifos instead of shift regs? 'Advanced trigger and capture settings' you might configure in a manufacturer tool are just whatever logic you describe to store samples.