Tutorial 7: Counting the Seconds, the Human Edition

In Tutorial 6 we made a counter, but it counted in hexadecimal– not exactly human-friendly! In this tutorial, we’ll count make a counter that counts seconds the way a stopwatch does. That is, counting up from zero to 59, and then wrapping.

Counting in Decimal, the Hardware Way

If you had to do this assignment in software, it would be pretty simple: just use division and modulo operators to get the decimal digits, then send those out. But we have a problem: those operators are actually very expensive in hardware. In the hardware world we can do something much simpler– just count in decimal. In order to do that, we’ll create a module that represents a decimal digit. Old-school computers used to do this all the time. It’s called BCD or Binary Coded Decimal. HP actually made a line of personal computers in the early 1980s called the Series 80 that used BCD instead of binary. Most handheld calculators do this as well. BCD has the advantage that it does rounding exactly the same way that humans do, so you get results that are more sensible to people. Ironically, it isn’t used much these days because it’s more complicated and takes more hardware than doing calculations in binary.

Anyway, here is my module which implements a BCD digit:

`timescale 1ns/1ns
module bcd_digit
  #(
    parameter modulus = 10
    )
  (
   input clk,
   input carry_in,
   output reg [$clog2(modulus)-1:0] digit,
   output carry_out
   );

  initial digit = 0;

  assign carry_out = carry_in && digit == modulus-1;

  always @(posedge clk)
    if (carry_in)
      if (carry_out)
	digit <= 0;
      else
	digit <= digit+1;

endmodule

It’s quite simple, really. First, there is a parameter modulus which indicates where the digit should wrap. There is a carry_in input which says to increment the digit, the digit output, and a carry_out output which can be used to connect to the next more significant digit. Since we use a parameter for the modulus, we can’t correctly use a fixed width for the digit output. The number of bits required for the job depends on the value for modulus. So, we use the built-in $clog2 function to take the log base 2 of modulus. This function basically returns the number of bits required to hold the value of its input.

Change the binary counter in the top module for two BCD digits like this.

  wire [7:0] sec_count;
  wire ones_carry_out; 	    
  bcd_digit #(10) ones_digit
    (
     .clk(clk),
     .carry_in(sec_pulse),
     .digit(sec_count[3:0]),
     .carry_out(ones_carry_out)
     );
  
  bcd_digit #(6) tens_digit
    (
     .clk(clk),
     .carry_in(ones_carry_out),
     .digit(sec_count[6:4]),
     .carry_out()
     );
  assign sec_count[7] = 0;

Notice how we only pass three bits to the tens digit, since it has a maximum value of 5. We need to also set bit seven of digit so that it is defined.

Changing the Test

If we simulate, we’re obviously going to have a bunch of failures. We need to modify our model to match our new behavior. Here’s my code for the rewritten model. In this case, we can go ahead and use division and modulo operators. The model is software, so those are OK to use.

`timescale 1ns/1ns
module model
  #(
    parameter ms_limit = 100000
    )
  (
   input clk,
   output [7:0] seconds
   );
  
  integer counter = 0;
  always @(posedge clk)
    counter <= counter+1;

  reg [7:0] seconds_value;
  always @(posedge clk)
    seconds_value <= (counter / (ms_limit * 1000))%60;

  assign seconds[3:0] = seconds_value % 10;
  assign seconds[7:4] = seconds_value / 10;

endmodule

Running the Simulation

If we go ahead and run the simulation, we get the following:


run all
Simulation complete at time 14000000.000000ns.
*** Simulation PASSED 0/2800002
$finish called at time : 14 ms : File "/home/pete/tutorial7/tutorial7.srcs/sim_1/new/bench.v" Line 89

In other words, it works, and the test passes. This illustrates an important point. Our test now uses a model of the correct behavior, and the model tells the test what the expected results are. This way, we don’t have to change the test bench when the behavior of the RTL changes.

Now, I’m not completely happy with the simulation, because it does not check that the count wraps from 59 back to zero. Let’s change the wait statement to wait for that to happen. I simply had it wait for the count to go back to 1 after it waited for the count to go to 20.

Adding More Digits

That was too easy. Lets add digits for tenths and hundredths of seconds, as well as minutes and hours. This means lots of digit values, so it’s best to keep them in a wide bus. First, we’re going to introduce the Verilog notion of a localparam. This is like a parameter value, but it can’t be overriden in the module instantiation. The localparam declaration can appear anywhere before its use. Here is my definition of the number of digits we need:

  localparam num_digits = 3 + 2 + 2 + 2;

We need three millisecond digits, two second digits, two minute digits, and two hour digits. Next, we declare the bus which will hold the digits and the bus that holds the carry values.

  wire [num_digits*4-1:0] time_digits;
  wire [num_digits-1:0] carry;
  assign carry[0] = ms_pulse;

Here, we also assign the least significant bit of the carry signal to be the same as the ms_pulse signal.

Then we just instantiate a bunch of digits:

  bcd_digit #(10) digit0	// 1 ms
    (
     .clk(clk),
     .carry_in(carry[0]),
     .digit(time_digits[3:0]),
     .carry_out(carry[1])
     );

  bcd_digit #(10) digit1	// 10 ms
    (
     .clk(clk),
     .carry_in(carry[1]),
     .digit(time_digits[7:4]),
     .carry_out(carry[2])
     );

  bcd_digit #(10) digit2	// 100 ms
    (
     .clk(clk),
     .carry_in(carry[2]),
     .digit(time_digits[11:8]),
     .carry_out(carry[3])
     );

  bcd_digit #(10) digit3	// 1 s
    (
     .clk(clk),
     .carry_in(carry[3]),
     .digit(time_digits[15:12]),
     .carry_out(carry[4])
     );

  bcd_digit #(6) digit4		// 10 s
    (
     .clk(clk),
     .carry_in(carry[4]),
     .digit(time_digits[18:16]),
     .carry_out(carry[5])
     );
  assign time_digits[19] = 0;

  bcd_digit #(10) digit5	// 1 min
    (
     .clk(clk),
     .carry_in(carry[5]),
     .digit(time_digits[23:20]),
     .carry_out(carry[6])
     );

  bcd_digit #(6) digit6		// 10 min
    (
     .clk(clk),
     .carry_in(carry[6]),
     .digit(time_digits[26:24]),
     .carry_out(carry[7])
     );
  assign time_digits[27] = 0;

  bcd_digit #(10) digit7	// 1 hour
    (
     .clk(clk),
     .carry_in(carry[7]),
     .digit(time_digits[31:28]),
     .carry_out(carry[8])
     );

  bcd_digit #(10) digit8	// 10 hour
    (
     .clk(clk),
     .carry_in(carry[8]),
     .digit(time_digits[35:32]),
     .carry_out()
     );

All that is left is to set the digits that we want to look at:

  assign digit = ssdcat_pre ? time_digits[19:16] : time_digits[15:12];

If you simulate this, everything works just the same as before and the test passes. However, we won’t be able to see the other digits.

Looking at the Other Digits

How can we look at all those digits now if we only have a two digit display? My solution is to use the switches to indicate which pair of digits you want to see. To do this, we’ll use another case statement. Change the declaration of the digit signal to be a reg, then add the following always block to set the digit signal based on the switch values. Here is my always block.

  always @(*)
    case(1'b1)
      switch[3]: digit = ssdcat_pre ? time_digits[35:32] : time_digits[31:28];
      switch[2]: digit = ssdcat_pre ? time_digits[27:24] : time_digits[23:20];
      switch[1]: digit = ssdcat_pre ? time_digits[19:16] : time_digits[15:12];
      switch[0]: digit = ssdcat_pre ? time_digits[11: 8] : time_digits[7 :4 ];
      default:   digit = ssdcat_pre ? time_digits[19:16] : time_digits[15:12];
    endcase

Note that this case statement looks a little backwards. Rather than a variable in the case statement, we just have a constant 1-bit one. Then, in the place where we normally put the value, we have a variable. This is a pretty common Verilog idiom. What we’ve coded is a case for when switch 3 is set. If it is set, then the other switch values don’t matter. So if both switch 3 and 2 are set, then the time will show the same as if switch 3 were the only one set. Verilog just looks down the list to see the first one that matches and then selects that. In hardware we refer to this as a “priority encoding”.

If we simulate this, we would get lots of error. Remember that we set our switch values at random? This will cause the digit outputs to go haywire. If we want our simulation to pass, we’d better exclude the low four bits of switch from turning on. We can do that by modifying the random assignment to the switch values like this:

  always @(posedge clk)
    switch <= $random & 8'hf0;

6 thoughts on “Tutorial 7: Counting the Seconds, the Human Edition

  1. Pingback: Lab 7: counting second, human edition – tranminhhai

  2. Hi Pete,

    Could you please share the source file in the lab? I found in the following code:
    bcd_digit #(10) ones_digit
    (
    .clk(clk),
    .carry_in(sec_pulse),
    .digit(sec_count[3:0]),
    .carry_out(ones_carry_out)
    );

    we must use a reg for sec_pulse to add a delay to the test bench for time sync, if we use the same code from the tutorial 6.

    Many Thanks

  3. Hi Pete, what was your reason for using the delays #(10) and #(6) in the top module bcd_digit instantiations again? I’m learning a ton working through your tutorials. Thanks so much for putting these together.

  4. Hi Pete, what was the reason for the #(10) and #(6) delays in the top bcd_digit instantiations? Just trying to make sure I’m following. Thanks so much for putting these tutorials together. Im learning a ton!

  5. Those aren’t delays. It’s a parameter which tells the counter digit when it should wrap. The 10 in the first says to count to 9 and then wrap to zero. In the second one it tells it to count to 5 and then wrap to zero.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.