A stopwatch is a good FPGA project that covers many basic, yet interesting areas of FPGA design. We will need display multiplexing for the multi-digit display, synchronous cascaded counter circuits to increment time registers for seconds and minutes, and a finite state machine to give us start, stop, and reset functionality.
I recently acquired a Basys 2 FPGA development board, which has an on-board 4 digit seven-segment display that lends itself nicely to keeping track of time in MM.SS format. Let’s build a stopwatch.
Before we begin to tackle the counting part of the stopwatch project let’s first consider how to control the 4-digit seven-segment display.
There are only 8 shared pins to control the 7 segments and dot for all 4 digits on the display, while 4 pins enable digits 0-3 at any time. By sequentially turning one digit on at a time while the other digits are off, at a high enough refresh rate, we can display different numbers for each digit at the “same time”. Multiplexing, as this is called, saves us on the number of pins needed to drive a device, but comes at the cost of having to design a multiplexing circuit.
The multiplexing code we will use for this part is referenced from FPGA Prototyping by Verilog Examples by Pong Chu.
module displayMux ( input wire clk, // clock and reset input lines input wire [3:0] hex3, hex2, hex1, hex0, // hex digits input wire [3:0] dp_in, // 4 dec pts output reg [3:0] an, // enable for 4 displays output reg [7:0] sseg // led segments ); // constant for refresh rate: 50 Mhz / 2^16 = 763 Hz // we will use 2 of the 2 MSB states to multiplex 3 displays localparam N = 18; // internal signals reg [N-1:0] q_reg; wire [N-1:0] q_next; reg [3:0] hex_in; reg dp; // counter register always @(posedge clk) q_reg <= q_next; // next state logic assign q_next = q_reg + 1; // 2 MSBs control 4-to-1 multiplexing (active low) always @* case (q_reg[N-1:N-2]) 2'b00: begin an = 4'b1110; hex_in = hex0; dp = dp_in[0]; end 2'b01: begin an = 4'b1101; hex_in = hex1; dp = dp_in[1]; end 2'b10: begin an = 4'b1011; hex_in = hex2; dp = dp_in[2]; end 2'b11: begin an = 4'b0111; hex_in = hex3; dp = dp_in[3]; end default: begin an = 4'b1111; dp = 1'b1; end endcase // hex to seven-segment circuit always @* begin case(hex_in) 4'h0: sseg[6:0] = 7'b0000001; 4'h1: sseg[6:0] = 7'b1001111; 4'h2: sseg[6:0] = 7'b0010010; 4'h3: sseg[6:0] = 7'b0000110; 4'h4: sseg[6:0] = 7'b1001100; 4'h5: sseg[6:0] = 7'b0100100; 4'h6: sseg[6:0] = 7'b0100000; 4'h7: sseg[6:0] = 7'b0001111; 4'h8: sseg[6:0] = 7'b0000000; 4'h9: sseg[6:0] = 7'b0000100; 4'ha: sseg[6:0] = 7'b0001000; 4'hb: sseg[6:0] = 7'b1100000; 4'hc: sseg[6:0] = 7'b0110001; 4'hd: sseg[6:0] = 7'b1000010; 4'he: sseg[6:0] = 7'b0110000; default: sseg[6:0] = 7'b0111000; // 4'hf endcase sseg[7] = dp; end endmodule
The circuit we will use lights up digits 0-F on the four digits of the display.
A decoder circuit takes the input of a 4 bit register and outputs the appropriate states of the common segment pins to represent the hexadecimal number on a seven-segment display.
An 18 bit counter register “q_reg” increments each positive edge of the clock, much like the counter we covered last time. The upper two bits of q_reg count from 0 to 3 with a frequency of 763 Hz, fast enough to multiplex the four digits of our display without any noticeable flickering.
A case statement uses the upper 2 bits of q_reg to turn on (enabled low) the appropriate digit enable pin and multiplex the routing of the separate digit registers to the common segment pins of the display. This separately routes numbers “hex0” to “hex3” to digits 0-3 of the display, sequentially with a frequency of 764 Hz. Thus we see each number being displayed on the appropriate digit.
Now that we have the ability to easily display numbers 0-9 on each digit of our display we can move on to the stopwatch circuit.
module stopwatch ( input wire clk, input wire go, stop, clr, output wire [3:0] d3, d2, d1, d0 );
The stopwatch circuit has 3 inputs that we will route to pushbuttons, which tell the stopwatch to go, stop, and clear. The output arrays d3-d0 will be used to route counter registers for the MM.SS time to the multiplexing display to show the elapsed time.
First lets consider the finite state machine that will control the user input portion of the stopwatch.
// declarations for FSM circuit reg state_reg, state_next; // register for current state and next state localparam off = 1'b0, // states on = 1'b1;
For the FSM there is a state register and next state logic register. We use two separate bits to represent the state of the FSM; off or on.
// state register always @(posedge clk, posedge clr) if(clr) state_reg <= 0; else state_reg <= state_next; // FSM next state logic always @* case(state_reg) off:begin if(go) state_next = on; else state_next = off; end on: begin if(stop) state_next = off; else state_next = on; end endcase
The state register updates on each positive edge of the clock, and is reset to 0 (off) when the clr input is asserted. The FSM next state logic controls the transition between on and off states. In the off state, if the go input is asserted, the next state is on, else the off state is retained. In the on state, if the stop input is asserted, the next state is off, else the on state is retained. We use the status of the state register to enable or disable the counting circuit.
The counting circuit uses synchronous cascaded counter registers to display incrementing time.
// declarations for counter circuit localparam divisor = 50000000; // number of clock cycles in 1 s, for mod-50M counter reg [26:0] sec_reg; // register for second counter wire [26:0] sec_next; // next state connection for second counter reg [3:0] d3_reg, d2_reg, d1_reg, d0_reg; // registers for decimal values displayed on 4 digit displays wire [3:0] d3_next, d2_next, d1_next, d0_next; // next state wiring for 4 digit displays wire d3_en, d2_en, d1_en, d0_en; // enable signals for display multiplexing wire sec_tick, d0_tick, d1_tick, d2_tick; // signals to enable next stage of counting // counter register always @(posedge clk) begin sec_reg <= sec_next; d0_reg <= d0_next; d1_reg <= d1_next; d2_reg <= d2_next; d3_reg <= d3_next; end // next state logic // 1 second tick generator : mod-50M assign sec_next = (clr || sec_reg == divisor && (state_reg == on)) ? 4'b0 : (state_reg == on) ? sec_reg + 1 : sec_reg; assign sec_tick = (sec_reg == divisor) ? 1'b1 : 1'b0; // second ones counter assign d0_en = sec_tick; assign d0_next = (clr || (d0_en && d0_reg == 9)) ? 4'b0 : d0_en ? d0_reg + 1 : d0_reg; assign d0_tick = (d0_reg == 9) ? 1'b1 : 1'b0; // second tenths counter assign d1_en = sec_tick & d0_tick; assign d1_next = (clr || (d1_en && d1_reg == 5)) ? 4'b0 : d1_en ? d1_reg + 1 : d1_reg; assign d1_tick = (d1_reg == 5) ? 1'b1 : 1'b0; // minute ones counter assign d2_en = sec_tick & d0_tick & d1_tick; assign d2_next = (clr || (d2_en && d2_reg == 9)) ? 4'b0 : d2_en ? d2_reg + 1 : d2_reg; assign d2_tick = (d2_reg == 9) ? 1'b1 : 1'b0; // minute tenths counter assign d3_en = sec_tick & d0_tick & d1_tick & d2_tick; assign d3_next = (clr || (d3_en && d3_reg == 9)) ? 4'b0 : d3_en ? d3_reg + 1 : d3_reg; // route digit registers to output assign d0 = d0_reg; assign d1 = d1_reg; assign d2 = d2_reg; assign d3 = d3_reg; endmodule
There is a base register, sec_reg, that increments with the positive edge of the clock. This register resets and sets a tick line high when the counter counts 1 second worth of clock cycles (sec_reg == divisor). This tick increments a register keeping a value of seconds “ones” for the display (MM.S<S>).
When the seconds “ones” registers reaches 9 and the base register ticks, the “ones” register ticks up, which enables a seconds “tenths” register (MM.<S>S) to go to its next state, signifying that ten seconds have elapsed.
When the “tenths” register reaches 5 and the “ones” and base registers tick, the “tenths” register ticks up to the minutes “ones” register (M<M>.SS), signifying that 60 seconds have elapsed.
When the minutes “ones” register reaches 9, and both seconds “tenths” and “ones” as well as base register tick, the minute “ones” register ticks up to the minutes “tenths” register (<M>M.SS), signifying that ten minutes have elapsed. The minute “tenths” register increments up to 9, where the next up tick it receives will effectively roll over the stopwatch. Thus this stopwatch can count up to 99 minutes and 59 seconds.
module stopwatch_unit ( input wire clk, go, stop, clr, // clock and reset input lines output wire [3:0] an, // enable for 4 displays output wire [7:0] sseg // led segments ); wire [3:0] d3, d2, d1, d0; stopwatch timer (.clk(clk), .go(go), .stop(stop), .clr(clr), .d3(d3), .d2(d2), .d1(d1), .d0(d0)); displayMux display_unit (.clk(clk), .hex3(d3), .hex2(d2), .hex1(d1), .hex0(d0), .dp_in(4'b1011), .an(an), .sseg(sseg)); endmodule
Finally we instantiate both of these circuits, routing the output of the stopwatch circuit to the input of the display multiplexing circuit.
# clock pins for Basys2 Board NET "clk" LOC = "B8"; # Bank = 0, Signal name = MCLK NET "clr" LOC = "M4"; # Bank = 0, Signal name = BTN2 NET "stop" LOC = "C11"; # Bank = 2, Signal name = BTN1 NET "go" LOC = "G12"; # Bank = 0, Signal name = BTN0 NET "an[3]" LOC = "K14"; # Bank = 1, Signal name = AN3 NET "an[2]" LOC = "M13"; # Bank = 1, Signal name = AN2 NET "an[1]" LOC = "J12"; # Bank = 1, Signal name = AN1 NET "an[0]" LOC = "F12"; # Bank = 1, Signal name = AN0 NET "sseg[6]" LOC = "L14"; # Bank = 1, Signal name = CA NET "sseg[5]" LOC = "H12"; # Bank = 1, Signal name = CB NET "sseg[4]" LOC = "N14"; # Bank = 1, Signal name = CC NET "sseg[3]" LOC = "N11"; # Bank = 2, Signal name = CD NET "sseg[2]" LOC = "P12"; # Bank = 2, Signal name = CE NET "sseg[1]" LOC = "L13"; # Bank = 1, Signal name = CF NET "sseg[0]" LOC = "M12"; # Bank = 1, Signal name = CG NET "sseg[7]" LOC = "N13"; # Bank = 1, Signal name = DP
For completeness, here is the user constraint file used for this circuit on the Basys 2 board.
If you are impatient and wish to test the circuit over the entire range of functional time output, you can make the divisor value smaller.
Appreciiate you blogging this
LikeLike