Sequential Logic I
More Flexible Functions
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.
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
.
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.
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.
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} .
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).
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
.
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.
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 entirealways_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:
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:
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 ascount_out
and specifies how long the count period is. For example ifperiod_in
is set to 203, thencount_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 ourcount_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