Register Patterns
Open any RTL and the same few register shapes appear over and over. All six here are the enable-and-reset structure of the last article with a different expression for the next value, and the counters and shift registers are the ones from sections 3.4 and 3.5 written down. Once you have built each, you recognise it on sight in someone else’s code.
Up counter
module up_counter #(
parameter N = 8
) (
input wire clk, rst_n, en,
output reg [N-1:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) count <= {N{1'b0}};
else if (en) count <= count + 1'b1;
end
endmodule
count == {N{1'b1}} is the cycle before it wraps, which is the terminal count of section 3.5.
Up/down counter
always @(posedge clk or negedge rst_n) begin
if (!rst_n) count <= {N{1'b0}};
else if (en) begin
if (up) count <= count + 1'b1;
else count <= count - 1'b1;
end
end
A mux between plus one and minus one.
Modulo-N counter
always @(posedge clk or negedge rst_n) begin
if (!rst_n) count <= {W{1'b0}};
else if (en) begin
if (count == M - 1) count <= {W{1'b0}};
else count <= count + 1'b1;
end
end
With M a parameter this is a decade counter, a seconds counter, or anything that wraps somewhere other than a power of two. It is the clear-on-N-1 counter of section 3.5, and M - 1, not M, for the reason given there.
Serial in, parallel out
always @(posedge clk or negedge rst_n) begin
if (!rst_n) shift_reg <= {N{1'b0}};
else shift_reg <= {shift_reg[N-2:0], serial_in};
end
The concatenation drops the top bit, moves the rest up one, and puts the new bit at the bottom. After $N$ edges the word is in the register with every bit available. This is the receiver of every UART and SPI port.
Parallel in, serial out
always @(posedge clk) begin
if (load)
shift_reg <= data_in;
else if (shift_en)
shift_reg <= {shift_reg[N-2:0], 1'b0};
end
assign serial_out = shift_reg[N-1];
Load a word at once, then push it out a bit per edge from the top. The transmitter.
Saturating accumulator
An ordinary accumulator wraps on overflow, and in audio or fixed-point work a wrap turns a loud signal into a click. A saturating one sticks at the maximum. Compute the sum one bit wider and clamp on the carry:
wire [N:0] sum = {1'b0, acc} + {1'b0, data_in};
always @(posedge clk) begin
if (add_en)
acc <= sum[N] ? {N{1'b1}} : sum[N-1:0];
end
The extra bit is the carry out of the addition, as in the saturating adder of section 4.3; if it is set the true sum was too big and the register takes all ones.
A testbench for the shift register
`timescale 1ns / 1ps
`default_nettype none
module sipo_shift_reg #(
parameter N = 8
) (
input wire clk, rst_n, serial_in,
output reg [N-1:0] shift_reg
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) shift_reg <= {N{1'b0}};
else shift_reg <= {shift_reg[N-2:0], serial_in};
end
endmodule
module tb_sipo_shift_reg;
parameter N = 8;
reg clk = 0, rst_n, serial_in;
wire [N-1:0] shift_reg;
sipo_shift_reg #(.N(N)) dut (.clk(clk), .rst_n(rst_n), .serial_in(serial_in), .shift_reg(shift_reg));
always #5 clk = ~clk;
reg [N-1:0] test_byte = 8'b1011_0010;
integer i;
initial begin
$dumpfile("tb.vcd");
$dumpvars(0, tb_sipo_shift_reg);
rst_n = 0; serial_in = 0;
@(negedge clk); rst_n = 1;
for (i = N - 1; i >= 0; i = i - 1) begin // most significant bit first
serial_in = test_byte[i];
@(negedge clk);
end
if (shift_reg !== test_byte)
$display("FAIL: shift_reg=%b (expected %b)", shift_reg, test_byte);
else
$display("PASS: shift_reg=%b after %0d cycles", shift_reg, N);
$finish;
end
endmodule
Feed the byte in most-significant-bit first over eight edges, and the register should hold the byte. That is the standard check for any shift register before it goes into something bigger.
Six patterns and one idea: the enable-and-reset register, with the next value being an increment, a decrement, a wrap, a shifted-in bit or a clamped sum. They cover most of the sequential logic in most designs. The next article arranges the same flip-flops to solve a timing problem rather than a storage one: pipelining.