Lecture 1: Number Systems, Flip Flops, and Counters
Last Update: 2023-11-02
> Number Systems
In general, humans count in a number system called base 10 (often referred to as “decimal”). In this system, we start at zero and every time that we see another sheep jump over the fence, we simply “add one” to the current number. In decimal, this means that we count like this: 0, 1, 2, and so on until we get to 9. Once we see another sheep, the next number that we count is 10. After that, 11, 12, and so on. The point here is that in base 10, there are ten different symbols which are used per place value in the number. Once we’ve used up all the symbols, the count “carries over” to the next column to the left and we continue. (Remember that a number can have any number of leading zeros before the decimal point. We usually just don’t write them out in everyday usage. For example, 01 = 1)
What happens if we change the number base? What if instead of 10, we tried something like 2? We can follow the same logic as we did with decimal. In this new number system, binary, each column of the number can take a total of two values before we need to carry over to the next. Back to our sheep counting example, we would count sheep in binary as: 0, 1, 10, 11, 100, etc. Note that even though these numbers look like they are getting very large very quickly, since each column has a smaller representable range, this sequence is equivalent to the decimal sequence 0, 1, 2, 3, 4. (It may be helpful to see what’s happening by writing the leading 0’s in the binary numbers: 000, 001, 010, 011, 100)
An important observation at this point is that since each column allows the number base’s worth of possible values, this implies that each column going to the left is the next power of that base. For decimal:
- 1 = 10**0
- 10 = 10**1
- 100 = 10**2
- 1000 = 10**3
Whereas in binary, each column corresponds to a power of 2:
- 1 = 2**0
- 2 = 2**1
- 4 = 2**2
- 8 = 2**3
And so on.
These concepts can be applied to other number bases such as octal (base 8) and hexadecimal (base 16). Both of these bases are also very useful when working with computers. Each octal digit (0 to 7) corresponds to three binary digits and each hexadecimal (hex) digit corresponds to four binary digits. In the case of bases which exceed base 10, we typically use the alphabet to represent the extra symbols. In the case of hex, A to F represent decimal 10 to 15.
Boolean Algebra and Logic Gates
Boolean algebra is a special subset of algebra where every variable value may take the values false or true. You may notice that this conveniently maps to a 0 and 1 in our binary system. In this case, however, each value only takes on a single binary place since there are only two possible values. In this system, there are a few defined Boolean operations: “and,” “or,” and “not.” From these operations, we can construct other operations such as “nand,” “not,” “buffer,” “xor,” and “xnor.” Each Boolean operator takes a value on its left-hand side and its right-hand side (excluding “not” and the “buffer,” both of which only take in one value). They then return a value based on a set of pre-defined rules. For example, the “and” operator returns true if and only if both inputs are true; otherwise it returns false. A summary of each operation and its associated logic symbol is summarized below.
With the basic logic gates/operations covered, there are two more important operations. These come in the form of De Morgan’s laws. These allow us to change between “and” and “or” operations when dealing with negated signals. The first is that a “nand” gate with its inputs inverted forms “nor” gate. The second is that a “nor” gate with its inputs inverted forms a “nand” gate.
- ~A & ~B = ~(A | B)
- ~A | ~B = ~(A & B)
Finally, a quick note on notation. Throughout this text, I will be using the ‘~’ operator as a logical negation “not.” This is for two reasons: (1) this is the way that negation is written in HDLs such as Verilog, and (2) my static site generator doesn’t allow me to write a bar over expressions. In the wild, you may also come across an expression like X’ which is equivalent to saying “X not” or ~X.
> Adders [TODO]
An adder is a device that, unsurprisingly, adds two numbers. Let us first take a look at a basic example, 5 + 8.
Using the Boolean operations defined above, we can create something called a half adder. This takes in two single bit binary numbers and produces the sum of those numbers.
> Flip Flops [TODO]
Flip flops allow a circuit to hold a state. [gate-level construction, setup/hold time explanation]
> Counters [TODO]
We now have all the building blocks to start constructing sequential circuits. Perhaps the most intuitive next step is to combine the adder and some flip flops to create a counter.
> Verilog
Verilog is a hardware description language (HDL) which was originally designed for simulation of digital circuits. Its syntax borrows heavily from C since its original parser was based on that of the C compiler. As such, if you are familiar with a c-like language, its syntax might look vaguely familiar. Despite its similarity, you must always remember that the Verilog is describing a circuit and not (in general) a sequential program.
Verilog: AND gate
Let’s start with a very simple example: an “and” gate.
module and_gate (
input a, b,
output y
);
assign y = a & b;
endmodule
Listing 1: AND gate
Verilog breaks every circuit down into something called a “module.” Each module is like the circuit boxes we drew earlier for the adders and flip flops. A module has some input nets (wires) and some output nets. In the case of the above “and” gate, the module has two inputs: a and b. It also has one output: y. A module always starts with the keyword “module” followed by the name of the module (“and_gate”), the parameter list (to be shown later), the input/output list, the module body, and the keyword “endmodule.” Note that there is no semicolon after “endmodule.”
The body of this module consists of a single assignment to the y net. This is called a “continuous assignment” it means that any time that a value on the right hand side (RHS) of the equals sign changes, the expression is recomputed and the result stored in the lefthand expression. As such, this makes a “combinational” circuit, where there are no flip flops (i.e., it is purely logic gates with no feedback paths). Notice how the logic symbols shown earlier have a symbol or two in their center, these are the Verilog operators to perform each of those operations. In this case, the RHS of the statement performs the binary operation “&” which is a bitwise “and.” Thus, the output y is always set to the logical “and” of a and b.
Verilog: Flip Flop
Now, let’s try something more complicated: a flip flop.
module dff (
input d, clk,
output reg q,
output qn
);
assign qn = ~q;
always @(posedge clk) begin
q <= d;
end
endmodule
Listing 2: D Flip Flop
Analyzing the module like the “and” gate, we see that this module is named “dff” (D-Flip Flop), takes in two signals: “d” – data and “clk” – clock. It also has two outputs: “q” and “qn”. “But, wait!” you say, “isn’t ‘reg’ also declared as an output?” It certainly looks like it, but the key here is that “reg” is also a keyword. In this instance, we are explicitly setting the net type of the “q” net. Verilog has two (main) net types: “wire” and “reg.” By default, input and output nets are of type “wire,” which is why we have not declared a type for some nets. We could have just as well said “input wire d, clk” and gotten the same result for the input signals.
Then what do we gain from this “reg” net type? The key is how the net is assigned a value. For wire types, the net must be driven by either a continuous assignment (as we see for “qn”) or by the output of an instantiated module (more on this later). It cannot be driven by an assignment within an “initial” or “always” block. (These blocks as known as “procedural blocks.”) The “reg” type, then, fills this need. Despite its resemblance to the word “register,” a “reg” type does not have to be a register. In fact, it can represent the output of combinational logic, a latch, or a flip flop! In this case, the “reg” type is being used to model a flip flop’s behavior. This can be determined by analyzing the condition of the “always” block in which it is assigned.
The ”always” construct always (pun intended) starts with the “always” keyword, followed by “@(sensitivity-list)” where the sensitivity list is the list of events which the construct should wait for. When an event in the sensitivity list occurs, the block is “run.” In this case, the sensitivity list contains “posedge clk,” which indicates that the block should wait for the rising/positive edge of the “clk” signal then run. This gets us our rising edge triggered flip flop behavior.
Following the sensitivity list is the keyword “begin.” This is equivalent to the “{“ opening curly brace in a c-like language and only serves to start a block. The block is ended with the keyword “end.” Within the block is a single “nonblocking assignment” which uses the “<=” operator. In simulation, the value of “d” (the RHS) is copied to “q” (the LHS) when the block is run, specifically on the positive edge of the clock.
Aside: Simulator Scheduling
To understand nonblocking assignments, we first need to understand the simulation event scheduler. Like any numerical simulation tool, Verilog simulators break time up into individual time slices. Within each slice, there are a few key operations which are performed. While there are a number of event regions per time slice, we will concern ourselves with only the “active events” and the “NBA updates.”
Most of the action occurs in the active Active event region. Here, flow logic (such as “if/else”) are determined, continuous assignments are computed, blocking assignments are computed, and the RHS of nonblocking assignments are computed.
Continuous assignments operate by computing the value of the RHS and immediately assigning that value to the LHS.
Blocking assignments operate similarly to continuous assignments, except that the order they are executed is determined by the order they take within a begin/end block. For instance, take the code in Listing 3. Within the always block, there are two “blocking assignments.” First, “b = a” followed by “c = b.” Since these are nonblocking assignments, “b = a” is evaluated first and b’s value is updated. Then the simulator moves to the next statement, “c = b.” Since b has already been updated, c will now take the value of a. In effect, this module makes for a bit of a convoluted buffer from a to c.
module blocking_assign
(
input a,
output c
);
wire b;
// Can also use:
// always @(*)
always @(a) begin
b = a;
c = b;
end
endmodule
Listing 3: Blocking assignment example
Nonblocking assignments (NBA) are slightly more complex in how they are scheduled. During the Active event region, the RHS is evaluated but the LHS is not yet assigned. The simulator holds this “next state” value behind the scenes until the NBA update region is entered. This occurs when all the Active events which had been queued for this simulation time step have been processed. Once the NBA update region begins, the pending values for each nonblocking assignment are copied into the LHS.
Like the blocking assignments, the nonblocking assignments are performed in the order that they appear in a begin/end block. This means that the simulator will use the last value computed within a block (even if multiple assignments to the same LHS are present). What is not specified, however, is the ordering of NBAs in different begin/end blocks. As a result, the LHS net of a nonblocking assignment can only be assigned within a single “always” block. (The same goes for blocking assignments as well).
Keeping this two stage NBA update in mind, Listing 4 demonstrates the flip flop-like behavior of these assignments. Whenever the sensitivity list is matched (posedge clk), the begin/end block of the “always” construct is run. Since the assignments are nonblocking, the simulation does not wait until the assignment is complete to move to the next. As a result, b will take the value of a but c will take the old value of b. Thus, to get a to c, two clock cycles are required.
module nonblocking_assign
(
input a, clk,
output c
);
wire b;
always @(posedge clk) begin
b <= a;
c <= b;
end
endmodule
Listing 4: Nonblocking assignment example.
Two final notes about scheduling: (1) In reality, the scheduler has a lot more phases then the two discussed here. These are mostly used for verification constructs, so we have ignored them for the sake of simplicity. (2) Updates to LHS values which are used in the RHS expressions of other assignments can place the latter assignments into the event queue. If done (im)properly, an infinite loop is possible and the simulator will lock up.
If you are interested in really diving into the simulation events, do a search for “Verilog Language Reference Manual.” You should be able to find the ~800-1300 page pdf on the entire language specification. (You don’t have to read the whole thing!) It may even be useful if you design video games—Minecraft apparently uses a similar scheme for its Redstone scheduling.
Verilog: Counter
Finally, we combine all the above information into a single module: the binary counter.
module counter
(
input clk,
output [3:0] A
);
reg [3:0] count;
// Combinational logic (assuming A is a ‘reg’)
// Option 1:
// always @(*) begin
// A = count;
// end
//
// Option 2:
// always @(count) begin
// A = count;
// end
//
// Option 3:
assign A = count;
// Sequential logic
always @(posedge clk) begin
count <= count + 1;
end
endmodule
Listing 5: Simple 4-bit binary counter
Listing 5 shows our “counter” module. It has one input: “clk” and one output “A.” In this case, we specify a width for the “A” vector. Convention in Verilog is to order vector indices in descending order, the same way that bits are numbered in a binary value. In this case, “A” is defined as having bits “[3:0]” which totals 4 bits, starting from index 0. Similarly, the “count” vector has a width of 4 bits.
Next up is the assignment from the “count” vector to “A.” Commented out are two other ways that this continuous assignment could be modeled. Both always blocks make use of blocking assignments to model the combinational logic. Option 1involves the use of the “@(*)” sensitivity list. This is an “inferred” sensitivity list where the simulator or synthesizer guesses at what should be in the sensitivity list. Option 2 explicitly states “count” in the sensitivity list, meaning that whenever “count” changes value, the always block is run.
Finally is the “always” block which looks a lot like the DFF example from earlier. In this case, the new value of the count is computed in the same line as the nonblocking assignment. When a positive edge on the clock is detected, “count” takes on a value of 1 plus its current value.