Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 33

Parameterized N-bit Synchronous Up-Counter with Enable

Recommended resources

Hello! In our previous lesson, we designed a parameterized N-bit register with a load enable signal. This taught us how to store data and control when to store it, a fundamental skill in digital design.

Today, we'll build directly on that foundation to create another essential component: a synchronous up-counter. As promised, you'll see that a counter is simply a clever variation of the register we just built. Our goal is to implement a counter that is both flexible (N-bits wide, thanks to parameters) and controllable (it only counts when we tell it to, using an enable signal).

1. From Register to Counter: A Small but Powerful Change

Let's recall the register with a load enable. Its logic was: "If enabled, load new data; otherwise, hold the current data."

if (enable) q <= d;

A counter follows a very similar pattern, but with one key difference. Instead of loading external data d, it loads its own current value plus one.

if (enable) count <= count + 1;

That's it! Conceptually, a synchronous counter is just a register whose input is connected to the output of an adder that increments the register's current value. The enable signal controls whether the register loads this new, incremented value or simply holds its existing one.

The schematic below illustrates this exact structure for a 2-bit up-counter. You can see the register (RTL_REG_SYNC) holding the state, an adder (RTL_ADD) calculating the next state (count + 1), and a multiplexer (RTL_MUX) that, controlled by the enable signal, decides whether to load the new value or hold the old one.

2-bit Synchronous Up-Counter Schematic
A schematic of a 2-bit synchronous up-counter. The register's output (`out`) is fed into an adder, and the result is fed back to the register's input. The enable signal (`en`) controls whether this new value is loaded on the next clock edge.

2. Parameterizing Your Counter

Just like with our register, hard-coding a specific width (like 4-bit or 8-bit) is inflexible. We want to write one piece of code that can be used to create a counter of any size. We'll use the parameter keyword, which you're already familiar with from the last lesson.

The following resources walk through this process of creating a configurable counter. They are excellent references for seeing how parameters are used in a practical context.

Creating a Configurable Counter Circuit

This article from FPGA Coding clearly demonstrates how to transform a fixed-width counter into a configurable one.

Focus on the section titled "Now add configuration". Notice how the hard-coded [3:0] is replaced with [BITS - 1 : 0] after declaring the BITS parameter. This is the key technique for creating parameterized modules.

Verilog Parameters - Parameter Example

This example from ChipVerify shows a complete, parameterized counter module that includes an enable signal.

Read the section "Parameter Example" and examine the Verilog code for the counter. It uses a parameter N for width and an input en for enable, which is exactly what we need. Note: Both of these examples use an asynchronous reset (e.g., always @(posedge clk, posedge rst) or if (!rstn)). For consistency in our course, we will stick to the synchronous reset style (always @(posedge clk) with if (reset)) that we established in the previous lesson. The core logic for the enable and parameterization, however, is perfectly illustrated in these examples.


3. Implementing the N-bit Synchronous Counter in Verilog

Now, let's combine these concepts—the counter's incrementing logic, the enable signal, synchronous reset, and parameterization—into a single, clean Verilog module.

module sync_up_counter #(
    parameter N = 8  // Default width is 8 bits
) (
    // Inputs
    input wire clk,
    input wire reset,    // Synchronous, active-high reset
    input wire enable,   // Count enable

    // Output
    output reg [N-1:0] count
);

    // This block describes the sequential logic for the counter
    always @(posedge clk) begin
        if (reset) begin
            // On reset, force the counter to a known state (zero)
            count <= {N{1'b0}}; 
        end else if (enable) begin
            // If not reset and enabled, increment the count
            count <= count + 1;
        end
        // If reset is 0 and enable is 0, 'count' implicitly holds its value.
        // The synthesis tool infers a register with feedback.
    end

endmodule

Let's break down this code:

  • module sync_up_counter #(parameter N = 8): We define our module, making its width N configurable with a default of 8 bits.
  • output reg [N-1:0] count: The counter's output is an N-bit register, defined using our parameter N.
  • always @(posedge clk): This defines our synchronous logic block. As always for sequential logic, we use non-blocking assignments (<=).
  • if (reset): Our first priority is the synchronous reset. If reset is high on a rising clock edge, the counter is forced to 0. The {N{1'b0}} syntax creates an N-bit vector of zeros.
  • else if (enable): If we are not in a reset state, we then check the enable signal. If it's high, we perform the core counter action.
  • count <= count + 1;: This is the heart of the counter. The + operator instructs the synthesis tool to create an N-bit adder. The result of this addition becomes the value that the count register will latch on the next clock edge.
  • Implicit Hold: Just like in our register design, there is no final else statement. When reset is 0 and enable is 0, no assignment is made to count. In a clocked always block, this synthesizes to hardware that holds the register's current value—exactly the behavior we want.

4. Practice: Instantiating Your Counter

You've designed a generic N-bit counter. Let's practice using it.

Your task: Imagine you are designing a timer for a system that needs to count for 4096 cycles. Write the Verilog code to instantiate your sync_up_counter to create a counter suitable for this task.

  • The instance name should be timer_counter.
  • The required inputs are sys_clk, sys_rst, and timer_en.
  • The output wire should be named timer_val.

Think about the required bit-width N. What is the minimum number of bits needed to represent the value 4095 (which corresponds to 4096 cycles, from 0 to 4095)? (Hint: , ).

Click here for the solution

To count up to 4095, you need 12 bits, since . Therefore, you must override the parameter N to be 12.

// Wires to connect to the counter instance
wire sys_clk;
wire sys_rst;
wire timer_en;
wire [11:0] timer_val; // The output wire must match the counter's width

// Instantiate a 12-bit synchronous up-counter
sync_up_counter #(
    .N(12)  // Override the default parameter N=8 with 12
) timer_counter (
    .clk(sys_clk),
    .reset(sys_rst),
    .enable(timer_en),
    .count(timer_val)
);

Explanation:
By using #(.N(12)), we configure this specific instance of our generic counter to be 12 bits wide, overriding the default value of 8. This is the power of parameterized design in action.


Conclusion

You have successfully taken the register concept from the last lesson and extended it to create a parameterized synchronous counter. This is a significant step, as counters are used everywhere in digital systems—for controlling sequences, generating timing intervals, and managing program flow in processors.

Key Takeaways:

  • A synchronous up-counter is fundamentally a register combined with an adder.
  • The core logic is count <= count + 1, which is controlled by an enable signal.
  • The if (reset) ... else if (enable) ... structure creates a clear priority: reset is checked first, then enable.
  • If neither reset nor enable is active, the counter implicitly holds its value.
  • Using a parameter for the bit-width N makes the design reusable and robust.

In our next lesson, we will explore another powerful variation of the register: the shift register. Instead of incrementing a value, a shift register moves data laterally, which is crucial for tasks like serial communication and data manipulation.