As we’ve discussed in our two previous posts, buttons and switches on FPGAs can be very useful to work with. They can be very useful tools when debugging as well. The problem is that these forms of inputs often “bounce”, and produce multiple edges when only one is desired. This can be a problem when you wish to depend or rely upon these inputs as being a dependable part of a debugging solution–before you’ve managed to ensure their dependability.

Making contact bounce particularly problematic is the fact that it is difficult to simulate, since the bounces tend to appear quite random.

Our first post on this topic looked into how this sort of bouncing manifested, showing several examples and traces of contact bounces. The pictures weren’t very pretty, as many buttons from several different boards and even a keypad all manifested this problem.

Our second post discussed how to create the digital logic necessary to debounce a user input.

We then created some simple test points that could be used to measure button press duration and the number of times a particular button press bounced.

Today, our discussion is going to focus on how to connect these two components to the wishbone bus present within our debugging-bus testing logic. We’re also going to add a compressed wishbone scope to the design in order to get the traces out of our design such as were presented in the initial post.

Instantiating the two components

Over the course of the last two posts, we’ve created two components that we would now like to test: a debouncer, and an unbounced module. We are going to test these two components by pressing a button, and then reading the results produced by these two components as registers on a bus. Once read, we’ll use the bus to reset these components.

We’ll start by assuming we have NBTNS buttons, in a vector named i_button coming into our top level design. This vector, together with our system clock, can then be used as inputs to these two modules, as in:

debouncer	#(NBTNS) thedebouncer(i_clk, i_button,
				debounced, debounce_dbg);
unbounced	#(NBTNS) theunbouncer(i_clk, unbounce_reset,
				i_button, transitions, max_clock);

The result of the debouncer is a NBTNS-bit vector debounced. This vector is a copy of i_button, save only that it has been both synchronized to the clock and any bounces have been removed.

The two results of the unbounced vector are two 31-bit vectors: transitions which holds the number of transitions, and max_clock which holds the number of clock ticks from the first transition to the last.

The other two items are the unbounce_reset and the debounce_dbg. The unbounce_reset line clears the counters. We’ll hook that up in the next section. The debounce_dbg lines, however, we’ll come back to when we discuss hooking the debouncer up to the wishbone scope.

Connecting the components to the debugging bus

We’d like to hook these two peripherals to our wishbone debugging bus, testbus.v. In particular, we’d like to be able to read these three values from an external UART. Since reading the values requires no logic handshake with the modules, they can be hooked up just as simply as any of the other simple registers within the testbus.v.

To hook this new peripheral up, just assign it an address among our simple data peripheral’s address map:

always @(posedge i_clk)
	case(wb_addr[3:0])
	...
	4'h6:    smpl_data <= transitions;
	4'h7:    smpl_data <= max_clock;
	4'h8:    smpl_data <= { {(32-NBTNS){1'b0}}, debounced };
	...
	defult: smpl_data <= 32'h0;
	endcase

Don’t forget when using these addresses, that each address references a 32-bit quantity, and hence they need to be multiplied by four to get the offset into the address map. Hence, address offsets of 24, 28, and 32 from the simple data peripheral now contain our new values. Given that we placed our simple data peripheral at an address offset of 0x2040, that means these new registers will have final addresses 0x2058, 0x205c, and 0x2060.

One other change is required to the bus, we need to create our reset signal, unbounce_reset. We’re going to set this reset signal up so that any time we write to either of these registers, either the transitions or the max_clock register, we reset our counters:

assign	unbounce_reset = (wb_stb)&&(wb_we)&&(smpl_sel)
				&&(wb_addr[3:1]==3'b011);

That’s it! That’s all the changes that are necessary in order to connect these two new peripherals to our bus. Gosh, if it’s that easy, why not try your own peripherals?

Before moving to the next section, there’s one other difference. If you compare this new top level design with the initial top level design, you’ll notice that the block RAM has been removed. This was to provide extra block RAM space for the wishbone scope—space that was never really needed in the end.

Switching to a compressed wishbone scope

There are three RTL steps to setting up a wishbone scope within your design.

The first step is to create a trigger. You can find the trigger I created in the debouncer.

always @(posedge i_clk)
	trigger <= (!ztimer)&&(!different)&&(!(|i_in))
			&&(timer[(LGWAIT-1):2]==0)&&(timer[1]);

This trigger is designed to stop the scope any time the debouncing logic is complete, and the button’s logic value(s) has returned to zero. In practice, it didn’t work very well for testing the keypad, since the keypad uses negative logic (a button release sends the signal high, not low). The end result was that I often needed to trigger the scope manually from the debugging bus, but we’ll come back to this in a moment.

The next step in connecting the scope is to select the data lines you wish to record. Here, let’s connect to our synchronized inputs, the debounced result, as well as the ztimer and trigger logic from within the design. Because buttons take place over a very long period of time, we’ll need to use the compressed wishbone scope. That’s going to limit our debugging width to 31-bits instead of 32. (The last bit is used for run length encoding.)

//	debug[31] is used for RLE encoding, and thus not available to us here
assign	debug[30] = ztimer;
assign	debug[29] = trigger;
assign	debug[28] = 1'b0;
// Bits 27:14 are assigned to the output of the debouncer
assign	debug[27 :(14+NIN)] = 0;
assign	debug[(14+NIN-1):14] = o_debounced;
// Bottom 14 bits are assigned to our synchronized input(s)
assign	debug[13 :NIN] = 0;
assign	debug[(NIN-1):0] = r_in;

assign	o_debug = debug;

While this approach limits the number of inputs to 13-bits in a 14-bit field, the logic within the actual debouncer code makes it possible to record up to 14-bits, and to process any number of buttons up to an arbitrary width.

The final change is to adjust the scope itself. Switching from an uncompressed wishbone scope to a compressed wishbone scope is almost as easy as changing wbscope to wbscopc. Indeed, the two share an almost identical interface standard. The only more substantial change beyond changing the name is adjusting the input data width from 32-bits down to the 31-bits of the compressed scope. We’ll also pull the scope_trigger from its bit position within the debug vector coming from the debouncer code.

	assign	scope_trigger = debounce_dbg[29];
	wire	[30:0]	debug_data;
	assign	debug_data    = debounce_dbg;
	wbscopc	#(5'd9) thescope(i_clk, 1'b1, scope_trigger, debug_data,

Setting up the design for this purpose is really just that easy.

Adjusting the register definition files

Finally, I adjusted the regdefs.h and regdefs.cpp files to reflect that there are two new registers in our design, giving them names that I can use to access them both from my own C code (regdefs.h), as well as from the wbregs command line (regdefs.cpp).

The first change was to regdefs.h, where we needed to define our three new registers:

#define	R_TRANSITIONS	0x00002058
#define	R_MAXCLOCKS	0x0000205c
#define	R_DEBOUNCED	0x00002060

You may remember from our discussion above that these are the addresses of these three new registers within our simple data peripheral’s address space.

I then gave these registers user-friendly names by modifying regdefs.cpp:

{ R_DEBOUNCED	  ,	"DEBOUNCED"	},
{ R_TRANSITIONS	  ,	"BOUNCES"	},
{ R_TRANSITIONS	  ,	"TRANSITIONS"	},
{ R_MAXCLOCKS	  ,	"DURATION"	},
{ R_MAXCLOCKS	  ,	"MAXCLOCKS"	},

This only modifies a structure providing a mapping from register values to user register names.

The final change was to create a piece of C++ code to read from the wishbone scope. You can see what this new scope code looks like here, although how to develop this code is discussed in general here. Build and run this new btnscope program, and you’ll have a VCD file that you can then use with GTKWave to view traces showing how your own buttons bounce–just like the traces I presented when we first started.

That’s it! We can now grab two registers from our button press, and read their results via wbregs duration and wbregs bounces. We can also reset our count by writing to either of these registers via something like wbregs duration 0. Using btnscope, we can also read and create VCD files.

Scripts to access the design

After using this design over and over several times, I got tired of typing the same commands over and over. As a result, I wrote two scripts to make things easier.

The first script, getinfo.sh, simple reads the two registers and dumps them into a file record for later analysis. It also manually triggers the wishbone scope in case it hadn’t triggered yet.

#!/bin/bash

wbregs bounces  | tee -a btndata.txt
wbregs duration | tee -a btndata.txt
wbregs scope 0x88000020

The second script, reset.sh, resets the logic for another button press. Specifically, it resets the scope (i.e. writes to it with the high data clear), and then attempts to write to the bounce count. As with any write to either of the unbounced registers, this will set the reset line to the unbounced module for one clock, and thus reset the counters. Finally, just to be certain that the two registers actually cleared as desired, they are read after reset.

#!/bin/bash

wbregs scope 0x020 # Reset the scope
wbregs bounces 0   # Reset the unbounced code
wbregs bounces     # Verify that the two counts have returned to zero
wbregs duration

Conclusion

That’s it! You now have all the information you need to either build and measure your own debouncer, or to just measure the bounces on your own device. Further, all of the debouncing logic, design, and software files have been placed into their own GitHub repo for your own reference.