Gowin IDE Tutorial: Debug FPGA Designs with the Gowin Analyzer Oscilloscope (GAO)

Learn how to use Gowin IDE's built-in GAO (Gowin Analyzer Oscilloscope) logic analyzer to capture and inspect internal FPGA signals directly on the FPGA without having to use an external oscilloscope.

Captured waveform in the GAO Analyzer Oscilloscope showing counter[7:0] and trig, with the trigger marker aligned to trig's rising edge

Preview of the Gowin Analyzer Oscilloscope

This is the third tutorial in my Gowin IDE series. Once your design is running on real hardware, you often want to see what is actually happening inside the FPGA, not just simulate it beforehand. That is exactly what `GAO`, the Gowin Analyzer Oscilloscope, is for.

GAO is an embedded logic analyzer core that Gowin IDE stitches into your design. It samples the internal signals you choose, on the same clock as your design, and streams the captured samples back over the same USB cable used for programming. No external logic analyzer or extra pins are needed.

In this tutorial I will show you how to create a GAO configuration, pick a trigger condition, capture some internal signals, and view the resulting waveform right inside Gowin IDE.

The only prerequisite is knowing how to create a new Gowin project and a Verilog source file. These steps were covered in the first tutorial.

The example design

For this tutorial I am using a tiny standalone project called GaoExample, targeting the same GW2A-LV18PG256C8/I7 device as before. The only source file is top.v, containing a free running 8 bit counter:

top.v
module top(
    input clk,
    output reg [7:0] counter
);

    always @(posedge clk) begin
        counter <= counter + 1;
    end
endmodule

This is deliberately simple: counter just keeps incrementing on every rising edge of clk. It gives us a signal with a completely predictable pattern, which makes it easy to confirm that GAO is actually capturing what we expect.

Constraints file

The only constraint needed is for the clock pin.

GaoExample.cst
IO_LOC "clk" H11;
IO_PORT "clk" IO_TYPE=LVCMOS33 PULL_MODE=NONE BANK_VCCIO=3.3;

Creating a GAO Config File

With the project synthesized at least once, click the New File icon and select GAO Config File from the list, then click OK.

New File dialog with GAO Config File selected in the file type list before clicking OK

The GAO Setting wizard opens. Leave the type as For RTL Design, since we are analyzing the RTL design directly rather than a post-synthesis netlist, and leave the mode as Standard. Click Next. Give the configuration file a name. I called mine GaoExample. Gowin IDE will save it with a .rao extension inside the project’s src folder. Click Next. The last step summarizes the settings. Check it and click Finish.

GAO Setting step selecting For RTL Design and Standard mode before clicking Next
GAO Configure File step naming the file GaoExample inside the project's src folder before clicking Next
Summary step showing RTL GAO, Standard mode and the GaoExample.rao path before clicking Finish

The GAO editor

The new GaoExample.rao file now shows up in the project tree, under a new GAO Config Files group. Double click it to open the GAO editor.

Project tree showing the newly created GaoExample.rao file under the GAO Config Files group

The GAO editor is organized per Core, and each core has two tabs. The Trigger Options tab has three panels: Trigger Ports on the left, where you pick which signals can be used to trigger a capture; Match Units in the middle, where you define the actual conditions to compare those signals against; and Expressions on the right, where you combine match units into the final trigger condition.

Trigger Options tab layout showing the Trigger Ports, Match Units and Expressions panels side by side

The Capture Options tab configures how sampling itself happens: the Sample Clock used to capture data, the Capture settings such as storage depth and trigger position, and the Capture Signals list of everything you actually want recorded in the waveform.

Capture Options tab layout showing the Sample Clock, Capture and Capture Signals panels

Setting up a trigger port

Selecting Trigger Port 0 reveals its empty signal list on the right.

Trigger Port 0 selected in the Trigger Ports list with its empty signal panel shown on the right

Click the green plus button to add a signal to this port.

Empty trigger port signal panel with the green plus button highlighted to add a new signal

This opens a signal browser, empty until you hit Search. Leaving the Name field blank and clicking Search lists every available signal, which is handy on small projects like this one. On a bigger design, typing a name first narrows that list down.

Select counter[7:0] from the column on the left, and move it to the right pane with the > button.

Empty signal browser dialog with a Name search field and two empty list panes
Signal browser filtering to counter[7:0] and clk, then moving counter[7:0] into the selected pane with the greater-than button

Back in the trigger port panel, counter[7:0] is now listed. Click OK to save it to the port.

Trigger port signal panel now containing counter[7:0] before clicking OK to save

Click the small arrow next to Trigger Port 0 any time to expand or collapse it and check which signal is assigned.

Trigger Ports list with Trigger Port 0 now populated, shown collapsed
Clicking the expand arrow next to Trigger Port 0 in the Trigger Ports list
Trigger Port 0 expanded to reveal counter[7:0] as its assigned signal

Configuring a Match Unit

Trigger ports on their own do not trigger anything. A Match Unit compares a trigger port against a value, and it is the match units that expressions are built from. Each match unit is configured through a Match Type and a Function dropdown, and there are six match types to pick from, each supporting a different set of comparison functions and bit values:

TypeBit ValuesMatching FunctionDescription
Basic0, 1, X==, !=General signal comparison; the cheapest one on resources.
Basic w/edges0, 1, X, R, F, B, N==, !=, jump detectionAdds jump (edge) detection to control on a signal transition.
Extended0, 1, X==, !=, >, >=, <, <=Ordered comparisons against an address or data value.
Extended w/edges0, 1, X, R, F, B, N==, !=, >, >=, <, <=, jump detectionOrdered comparisons plus jump detection.
Range0, 1, X==, !=, >, >=, <, <=, in/out of rangeWhether an address or data value falls inside or outside a range.
Range w/edges0, 1, X, R, F, B, N==, !=, >, >=, <, <=, in/out of range, jump detectionRange detection plus jump detection.
Bit values

  • 0 means low level.
  • 1 means high level.
  • X means either level.
  • R indicates a rising edge, 0 to 1.
  • F indicates a falling edge, 1 to 0.
  • B indicates either a rising or falling edge.
  • N means no level transition.

This tutorial only needs two of these: Basic, for a plain equality check, and later Basic w/edges, to catch a signal’s rising edge. Check the box next to M0 to enable it, then double-click on its row to open the Match Unit 0 dialog.

Match Units table with M0 checked and its row clicked, opening the Match Unit 0 dialog

The dialog opens with nothing selected yet. Pick Trigger Port 0 from the On Trigger Port dropdown.

With Trigger Port 0 selected, counter[7:0] appears on the left. Leave the function as == and enter 00000000 in binary as the value to match, so the unit fires the instant the counter wraps back to zero. Click OK.

Match Unit 0 dialog with the On Trigger Port dropdown empty, about to select a port
Match Unit 0 configured with Trigger Port 0, function == and binary value 00000000 before clicking OK

Building the trigger expression

Match units still need to be combined into an expression before they can actually trigger a capture. In the Expressions panel, leave Mode as Static and click into the empty expression box.

Expressions panel in Static mode with an empty expression box ready to be clicked

A small calculator style keypad opens, with a button for every match unit plus the logic operators needed to combine them.

Expression keypad with buttons for match units M0 through M15 and logic operators such as AND, OR and NOT

Since we only need M0, click its button and then OK.

Expression keypad with the M0 button selected before clicking OK

The expression M0 is now listed, meaning GAO will trigger exactly when match unit 0 evaluates true, that is, the moment counter[7:0] equals zero.

Expressions panel showing the single configured expression M0

Capture settings

Switch to the Capture Options tab. Click the ... button next to Clock, search for and select clk, and click OK. This is the clock GAO uses to sample every signal.

Capture Options tab selecting clk as the sample clock through the signal browser

Click Add in Capture Signals, search for counter, select counter[7:0], move it to the right pane, and click OK. This is what actually ends up in the waveform, separately from whatever you used for the trigger.

Capture Signals list adding counter[7:0] through the signal browser

The capture buffer holds 1024 samples by default. Setting Trigger Position to 30 tells GAO to keep 30 samples from before the trigger event and the rest from after it, which is useful to see what led up to the trigger, not just what followed it.

Capture settings with Trigger Position set to 30 out of a 1024 sample storage size

Saving and building

Save the .rao file. Notice the tab title carries an asterisk while there are unsaved changes, and loses it once saved.

GaoExample.rao tab with an asterisk in its title, indicating unsaved changes
GaoExample.rao tab without an asterisk, indicating the file has been saved

Click the Rerun All icon in the toolbar to synthesize and place & route the design again. This time, the GAO core gets stitched into the bitstream alongside your design.

Toolbar with the Rerun All icon highlighted to resynthesize the design with the GAO core included

Once both Synthesize and Place & Route finish with green checkmarks, the build is ready to program.

Process panel showing Synthesize and Place & Route both completed successfully with green checkmarks

Programming the GAO bitstream

Program the board as usual, but pay close attention to the FS File. Building with a GAO core produces its own bitstream, ao_0.fs, which includes the analyzer logic alongside your design.

Watch out for old bitstreams! (Don’t ask me how i know)

If you had already built the project once before creating the GAO configuration, impl/pnr will still contain the plain bitstream from that earlier build (GaoExample.fs in this case), since building with GAO does not delete it, it just adds ao_0.fs next to it.

That old file still programs and runs fine, it just has no GAO core in it, so double check FS File points at ao_0.fs and not a stale file left over from before you added GAO.

USB Cable Setting dialog with the FS File pointing at the GAO-instrumented ao_0.fs bitstream

Running a capture

Open the Gowin Analyzer Oscilloscope tool from the Tools menu.

Tools menu with Gowin Analyzer Oscilloscope highlighted

The Configuration tab also shows the trigger and match unit settings from the project file, so you can double check exp0: M0 is set up correctly before running. Click the Run button to arm the trigger and start waiting for a match.

Analyzer Oscilloscope Configuration tab showing the exp0: M0 trigger expression and M0 match unit, with Run highlighted

As soon as counter[7:0] reaches zero, the trigger fires and GAO streams the capture back. The core 0 tab shows counter[7:0] as a bus, printed in hex, with the orange marker at sample 30 showing exactly where the trigger landed, right where the value rolls over to 00.

Captured waveform of counter[7:0] as a hex bus, with the trigger marker at sample 30 where the value rolls over to 00

Expand counter[7:0] to see every individual bit as its own binary waveform. You can clearly see each bit toggling at half the rate of the one below it, exactly what you would expect from a binary counter.

counter[7:0] expanded into its individual bits, each shown as a separate binary waveform

Zooming out a little confirms the counter keeps incrementing cleanly, one step per clock cycle, well beyond the trigger point.

Zoomed out waveform view confirming counter[7:0] keeps incrementing by one every clock cycle

And that’s it. You now have a working internal logic analyzer for any Gowin FPGA design, without needing a single extra pin.

Advanced triggers

So far every trigger has fired on a static value, counter[7:0] == 0. GAO can also trigger on a signal’s edge rather than its level, which is a better fit for a one-shot event like a counter overflow. To try it, add a trig register to top.v that pulses for exactly one cycle every time counter wraps around:

top.v
module top(
    input clk,
    output reg [7:0] counter
);

    reg trig;

    always @(posedge clk) begin
        counter <= counter + 1;
        trig <= &counter;
    end
endmodule

Expand the box below if you want the full story of why the obvious approach doesn’t quite work.

Code explained: Why not just if (counter == 0)?

First idea on how to write the code might be:

top.v
module top(
    input clk,
    output reg [7:0] counter
);

    reg trig;
    
    always @(posedge clk) begin
        counter <= counter + 1;

        if (counter == 0)
            trig <= 1;
        else
            trig <= 0;
    end
endmodule

This looks reasonable, but it is wrong. Every signal on the right-hand side of a non-blocking assignment (<=) is read using its value from before this clock edge, and counter is no exception: the comparison counter == 0 still sees the old value, one cycle behind the counter <= counter + 1 that is scheduled to happen at the same edge. So by the time trig actually goes high, counter has already moved on to 1. The capture below still uses the original counter[7:0] == 0 trigger from earlier in this tutorial, but with trig also being recorded: the capture still lands right where counter hits zero, yet trig only rises one sample later.

Captured waveform showing trig rising one sample after the trigger point where counter equals zero, one cycle later than intended

Trigger is on the value when counter is 1, not 0

Comparing against the last value before the wrap fixes the timing:

always @(posedge clk) begin
    counter <= counter + 1;

    if (counter == 8'hFF)
        trig <= 1;
    else
        trig <= 0;
end

This works, but 8'hFF has to be updated by hand every time counter changes width. Comparing against the next value instead seems like it should avoid that problem entirely:

if ((counter + 1) == 0)

This does not work: counter + 1 and 0 are both evaluated at whatever width Verilog decides for the expression, which does not necessarily match counter’s width, so the comparison never wraps the way you would expect. Sizing every literal explicitly fixes it:

if ((counter + 8'd1) == 8'd0)

but that still needs to be edited by hand whenever the register’s width changes. The width-independent way to detect “all bits are 1” is the reduction-AND operator, &counter: it collapses every bit of counter down to a single bit that is 1 only when they are all 1. Since that is exactly the state right before the counter wraps to zero, it is exactly the condition we want:

8-bit:  11111111            -> &counter = 1
16-bit: 1111111111111111    -> &counter = 1
32-bit: all ones            -> &counter = 1
always @(posedge clk) begin
    counter <= counter + 1;

    if (&counter)
        trig <= 1;
    else
        trig <= 0;
end

And since trig is just being assigned the result of a boolean expression either way, the if can be dropped entirely:

top.v
module top(
    input clk,
    output reg [7:0] counter
);

    reg trig;

    always @(posedge clk) begin
        counter <= counter + 1;
        trig <= &counter;
    end
endmodule

This version keeps working no matter how wide counter ends up being, without touching anything besides its declaration.

Triggering on an edge

With trig added to the design, rebuild it (Rerun All, as before) and reopen GaoExample.rao. The old Trigger Port 0 still points at counter[7:0]; remove it by selecting the signal in the port’s list and clicking the red minus button.

Trigger Port 0 with counter[7:0] selected and the red minus button highlighted to remove it

Add trig to the now-empty port the same way as before: search for it, select it, and move it across.

Signal browser searching for trig and moving it into Trigger Port 0's signal list

Click OK to save trig as the port’s only signal.

Trigger Port 0's signal list now containing only trig before clicking OK

Open Match Unit 0 again. This time change Match Type to Basic w/edges, and instead of a static binary value, type R into trig’s value field to match its rising edge specifically, rather than just any moment it happens to be high.

Match Unit 0 dialog with Match Type set to Basic w/edges and trig's value set to R for a rising edge

The Trigger Options tab now reflects all of this: Trigger Port 0 carries trig, and M0 matches its rising edge, still combined into the same M0 expression as before.

Trigger Options tab showing Trigger Port 0 assigned to trig and Match Unit 0 set to Basic w/edges with value R

Switch to Capture Options and click Add again to also record trig in the waveform alongside counter[7:0], using the same search-and-select process as before. trig now shows up in the capture signal list too.

Capture Signals list with counter[7:0] already present and the Add button highlighted to add another signal
Capture Signals list now containing both counter[7:0] and trig

Save, rebuild, reprogram and run the capture again. This time the trigger fires exactly on trig’s rising edge, right as counter wraps around.

Captured waveform with the trigger marker aligned to trig's rising edge as counter wraps from FF back to 00

That’s working edge triggering. Instead of waiting for a signal to have a specific value, GAO can catch the exact instant it changes, which is often more powerful when looking for something.