Sequential Logic I

More Flexible Functions

The questions below are due on Wednesday September 11, 2024; 11:59:00 PM.
 
You are not logged in.

Please Log In for full access to the web site.
Note that this link will take you to an external site (https://shimmer.mit.edu) to authenticate, and then you will be redirected back to this page.

Motivation

Everything we've done so far has been with combinational logic. Consider the case of a simple adding module that takes two 16 bit numbers a and b and calculates their sum c. As a brief aside, since the two inputs each have values of 16 bits, we'll make the output 17 bits in size to accomodate any worst-case overflow that may occur.

Combinational Adder.

In SystemVerilog we could accomplish this in two different ways. First you could use an assign statement. The benefits are simplicity and brevity:

module adder_comb( input wire [15:0] a,
              input wire [15:0] b,
              output logic [16:0] c);
    assign c = a + b;
endmodule

You could alternatively do this using an always_comb block. The benefits are more expressiveness and flexibility:

module adder_comb( input wire [15:0] a,
              input wire [15:0] b,
              output logic [16:0] c);
    always_comb begin
        c = a + b;
    end
endmodule

In both cases, a circuit will get synthesized that combinationally adds a to b to yield c.

While we'd like to think that the calculation of "c = a + b" happens instantaneously, we know deep in our hearts that it actually doesn't. Upon the presentation of new inputs a and b, the output c will:

  • Be guaranteed to not change for at least the amount of time known as t_{cd}, or contamination delay, after the inputs have changed.
  • Not be guaranteed to have the new result until t_{pd}, or propagation delay, after both new inputs have appeared1

This is shown in the timing diagram below. The inputs change from 2 and 5 to 4 and 6. The output, which started at 7 since 2+5=7, remains at 7 for t_{cd} amount of time. After t_{cd} has elapsed, the output is no longer guaranteed to be 7.

Timing Diagram of Combinational Adder.

As to what happens in the period of time t_{cd} \lt t \lt t_{pd}? The output can be thought of as "unreliable". At some point in that window, the the value of c will transition from 7 ('b0111) to 10 ('b1010), but there's no telling when that will happen. And there's no telling how that will happen. This isn't like life or love where "the journey is what matters". We're engineers and we only care about hard, emotionless data, and that data is not guaranteed to be calculated until t_{pd} has elapsed. And as you can see in the plot above, once t_{pd} has passed, then and only then is a value of 10 show up at the output.

Some t_{cd} and t_{pd} Practice

Consider the circuit below made of very simple logic gates. Answer the questions below it thinking about how contamination delay (t_{cd}) and propagation delay (t_{pd}) will interact.

Mystery Logic Circuit. Note that if two lines cross, but there is no dot, there is no connection. Crossing wires with a dot indicate a connection.

Assume the following about the contamination delay t_{cd} and propagation delay t_{pd} of the above logic gates:

  • AND Gate: t_{cd}=30\text{ ps}, t_{pd}=60\text{ ps}
  • OR Gate: t_{cd}=55\text{ ps}, t_{pd}=75\text{ ps}
  • XOR Gate: t_{cd}=75\text{ ps}, t_{pd}=85\text{ ps}

Remember, t_{cd} is the minimum guaranteed time that the circuit will NOT change its output given a change to any of its inputs. t_{pd} is the maximum potential time before the output of a circuit will change given a change to any of its inputs.

What is the t_{cd} of the entire circuit? Answer in picoseconds.

What is the t_{pd} of the entire circuit? Answer in picoseconds.

Given the results above, how many times per second can this circuit accept a new set of inputs and provide an output? Hint: One second is 10^{12} \text{ ps} .

How many times per second can this circuit process a unique input?

Fixing the Problem

One thing that should be taken away from the exercises above is that the "thicker" your circuit becomes (meaning the more logic to traverse when going from input to output), the larger your t_{pd} will become. A larger t_{pd} ultimately means that a circuit can perform fewer calculations in an amount of time, which means our logic will be slower. This problem only gets worse and worse the more complicated your logic gets. Think about a modern computer. It is far more complicated than the five or so logic gates above. Can you imagine what its t_{pd} is to run a large language model or something or render a frame of your favorite video game? Since the thing driving the circuit needs to make sure to hold its inputs steady for at least the t_{pd} of the circuit this means that the driver will waste a lot of time holding values waiting for the circuit to "solve".

The solution is to break down and isolate portions of our combinational logic so that they can't influence one another until they have stabilized their outputs. These things that isolate the portions are known as registers or "flip flops". Specificaclly we'll use edge-triggered flip flops. When this is done, we call it "Sequential Logic"; all that really means is combintaional logic with edge-triggered flipflops in between.

The flip flops take care of the annoying task of "holding" inputs at steady values and free up the logic upstream from needing to do so. This therefore lets them be allowed to start calculating again.

As mentioned in class (as a brief review from 6.191), an edge-triggered flip flop has a truth-table that isn't defined solely from static values. In fact if you make a conventional "static" truth table for a D-type flip flop, it looks like the undefined mess shown below: (where the output Q is always based on the output Q...that's messed up and useless...a rock can do that).

Static Truth Table for D Flip Flop

However if you mess with a D flip flop more, you'll see that its outputs do change based on the input value D and the dynamic behavior of the clk signal. Specifically, the D flip flop will transfer the value of input D to output Q whenever clk transitions from a value of 0 to 1 aka "rising". Importantly, it does not perform the same way when clk falls from 1 to 0.

Dynamic Truth Table for D Flip Flop

Static Truth Table for D Flip Flop

Essentially, the flip-flop register stores a value D that can be modified at each rising edge of the clock cycle, and it stays at that value until D changes at a future rising edge.

A "registered" adder

Timing diagram for the registered adder above. Note the output is delayed a bit from what it would be if things were combinational only, but it does prevent the outside world from seeing the period of indecision from the propagation delay.

What this "buys" us is the ability to isolate output uncertainty from different chunks of logic. This therefore allows easy modularization of designs. If you can always present a valid result at your output, you can confidently hook up to other modules. In a lot of ways, flip-flops are the social media of digital design. Even though there may be deep, deep internal chaos going on inside a circuit, that isn't revealed to the outside world. You only see what's stored in the flip flop registers at each rising edge. So the circuit looks like it has its life together, but in reality it is a mess on the inside as various logic gates cycle through their t_{cd} and t_{pd} periods resolving outputs.

We write sequential logic in an always_ff block, which performs all the calculations in the block and updates the flip-flop registers at each rising edge of the clock cycle. We indicate when to perform the always_ff block by adding @(posedge clk), as shown below. This means that at each positive (rising) edge of the clock cycle with variable name clk, perform the block of logic.

module adder_seq (input wire clk,
              input wire [15:0] a,
              input wire [15:0] b,
              output logic [16:0] c);
    logic [16:0] cp;
    always_comb begin
        cp = a + b;
    end

    always_ff @(posedge clk)begin
        c <= cp;
    end
endmodule

Alternatively you can do it this way:

module adder_seq (input wire clk,
              input wire [15:0] a,
              input wire [15:0] b,
              output logic [16:0] c);
    //all in one, simpler (though requires discipline!)
    always_ff @(posedge clk)begin
        c <= a + b;
    end
endmodule

= vs. <=

In Verilog and SystemVerilog, within an always-type block (of which there are three that we use: always_comb, always_ff, and very rarely always_latch), lines of code are evaluated from top to bottom. When the end of the block is reached, the final result are the final signals. There are two ways to assign a variable:

  • Blocking statement, using = in an always block, will result in the line of code with = only being evaluated after the previous line has been evaluated.
  • Nonblocking statement, using <= in an always block, will result in every assignment being implemented in parallel once the end of the block is reached. For instance, an entire always_ff will be calculated in parallel during each clock cycle and updated on the rising edge of the clock, all at once. This means prior lines of code will not immediately impact how another line will get resolved.

To shed some light on how these two operators are different consider the Verilog below:

always_ff @(posedge clk)begin
    x = y || 1;
    z = x ^ u;
end

This will build the following circuit:

A blocking result

Whereas the following code using non-blocking assignments...

always_ff @(posedge clk)begin
    x <= y || 1;
    z <= x ^ u;
end

...will build the following circuit:

A blocking result

Which you can see is a completely different circuit.

Conclusion

Generally speaking we want the behavior of = when we are implementing combinational logic and we want the behavior of <= any time we're trying to make flip flop (sequential) logic. This early in your time in 6.205 these rules may seem weird, but just follow them. Do not put = in your always_ff blocks and do not put <= in your always_comb. This will avoid neeless synthesis and simulation headaches as time moves forward.

New and Old

If signals are being reliably held (aka "remembered") by the flip flops, there is the possibility that a signal's new value could be influenced by the signal's old value if we use a bit of signal feedback. For example, consider the very simple modification of the flip-flopped adder below. The current "count" is based on the previous count.

A SystemVerilog implementation of this design is implemented below:

module counter (  input wire clk_in,
                  output logic [15:0] count_out);
    logic [16:0] c;
    logic [15:0] count;

    adder_comb a1 (  .clk(clk_in),
                .a(1),
                .b(count),
                .c(c));

    always_ff @(posedge clk_in)begin
        count <= c[15:0];
    end

    assign count_out = count;
endmodule

Or perhaps more simply, and which really demonstrates the need for the <= type assignment:

module counter (  input wire clk_in,
                  output logic [15:0] count_out);

    always_ff @(posedge clk_in)begin
        count_out <= count_out + 1;
    end
endmodule

The module described above says that on every rising edge of the "clock" clk_in, count_out will increment by a value of 1 (in other words count_out will become count_out+1). Notice it uses the old value of count_out, or the value when entering the clock cycle, to update count_out for the next clock cycle.

Improved Counter

For an exercise (which we'll use in contents later in this week), make an improved version of the counter above. It has additional controls, including:

  • rst_in enables us to reset and/or freeze the counter at zero.
  • period_in is a value of the same size in bits as count_out and specifies how long the count period is. For example if period_in is set to 203, then count_out will run from 0 to 202 (203 cycles because of zero inclusive) before cycling back to 0 again. In other words, it allows our counter to have a user specified period rather than one that relies on simple overflow of our count_out variable.

In addition, this new counter, (we'll just call it counter still) should have a 32 bit size counter register rather than the 16 bit one we talked about in earlier examples. This will just make it more robust and flexible. We'll learn about parameters and related things in future weeks.

Once you're done, move on to the next part of this week's assignments :D


 
Footnotes

1What happens if a shows up after b? Well then you have to start the clock after the last event change occurs. (click to return to text)