Implementing a Pulse Width Modulator (PWM) in Verilog

Introduction

Does the world need yet another Verilog implementation of the Pulse Width Modulator?

There are dozens of examples on the web with various degrees of complexity. In this post we will explore a moderately complex example. More importantly we will explore some of the design decisions that accompany the construction of Verilog modules. This includes parameterization of the PWM width, bounds for the duty cycle, and strict operation within a given clock boundary for stability. In addition, we will explore a pragmatic design philosophy. Instead of exploring “what” we will also attempt to explain “why” a particular set of design decisions was made.

PWM Operation

At its core, a digital PWM module is constructed from three blocks. As shown in Figure 1, we need a counter, a comparator, and a register to hold the desired duty cycle. The counter continually counts from zero to the modulus defined by the number of bits in the counter’s register. The comparator block then compares the count register’s value with the desired duty cycle. If the count is greater than or equal to D, the PWM output is set high otherwise it is low.

Figure 1 contains additional circuitry to bound the duty cycle to a given minimum and maximum value. This is a desirable feature in certain applications. For example, assume the PWM is used to control the upper MOSFET in a bridge circuit. A MOSFET in this position is typically powered by a bootstrap circuit. Extended operation at full duty cycle (always on) can discharge the bootstrap capacitor, causing undesirable operation as the MOSFET slides into a linear mode causing overheating. Consequently, it is desirable to place limits on the duty cycle.

This bounding is represented as a multiplexer. If d_in is between the min and max bounds, it is passed on. If not, either the min or max values are selected by the MUX. At the appropriate time the desired duty cycle value is locked in the register. To prevent glitches, the “appropriate time” occurs at the beginning of a cycle. For simplicity, this subtle synchronization detail is not shown in Figure 1.


Figure 1: Block diagram representation of the PWM.

The heart of the PWM is captured in these two lines of Verilog code.

    cnt <= cnt + 1'd1;
    PWM <= ((cnt + 1'd1) >= D)  ?  F  :  T;

The PWM output is synchronously set based on the comparison of count (cnt) and desired duty cycle (D) registers. With regards to the counter, there is a subtle Off By One (OB1) error that must be avoided. Notice that count is synchronously incremented by one as described on the first line. The count value is also used in the second line to make a comparison. Since we are using a non-blocking assignment (synchronous within the always @(posedge clk)), we must ensure that the comparison is in the same clock cycle as the counter increment. This is done by performing the comparison using the same cnt+1’d1. It’s helpful to think of this idea in terms of state and state next where cnt is the state and cnt+1’d1 is the next state.

The min and max bounding circuit features a few useful Verilog tricks. When the module is instantiated, the programmer specifies the min and max duty cycle as an integer percentage. This is used to construct two Verlog local parameters:

    localparam D_MIN = (2**B - 1) * D_MIN_PERCENT / 100;
    localparam D_MAX = (2**B - 1) * D_MAX_PERCENT / 100;

To better understand the operation, consider a situation where the programmer has instantiated the PWM with a 12-bit width and set the min and max to 5% and 90% respectively. Given the 12-bit width the maximum (full on) occurs when D is 4095. The 5% point is approximated as 204 and the 90% value is approximated as 3685. Note that we cannot use 2**B as base as it would be out of range when the max is set to 100 %. To see why, consider an 8-bit system. The number 2**8 is 256. In a modulo 256 system that equates to a zero. Therefore, we use 2**8 – 1.

As shown in Figure 1, the d_in, D_MIN, and D_MAX parameters may be considered the inputs to a MUX. The operation is described in this code. Observe that the output of the “MUX” is the D register.

     if (cnt == (2**B - 1)) begin            // ready D for the next full cycle
         if (d_in < D_MIN)      D <= D_MIN;
         else if (d_in > D_MAX) D <= D_MAX;
         else                   D <= d_in;
     end

There is a subtle but important aspect to this code with regards to when the D register may be updated. Keeping in mind our previous discussion of state and state next, we see that the next D is calculated as the modulus B counter rolls over. This is important as it prevent unexpected operation. It ensures that the new D value becomes active on the rising edge of the clock at the same moment the counter resets to zero.

The final code section we will explore is reset and enable section as shown below. Inspection reveals that a missing enable signal will halt the PWM and set its value low. The counter register is placed into a state preparing for the first rising edge of a clock when the module is once again enabled. As before, this “reset” number is 2**B – 1.

    if( !enable) begin          //*******************************************
        cnt <= 2**B - 1;        // For example in an 8-bit system, cnt = 255.
        PWM <= F;               // When enable is set, the system will
     end else begin             // start at zero on next rising edge
                                // of clock thereby initiating a full
                                // PWM cycle. This also allows for continuous
                                // update of D while in a disabled condition.
                                //*********************************************
        cnt <= cnt + 1'd1;
        PWM <= ((cnt + 1'd1) >= D) ? F : T;
    end

The testbench output presented in Figure 2 clearly shows the reset operation of the PWM. The PWM module’s enable line is asserted commanding enabling the start of the PWM process. For this specific 12-bit instantiation, we can see the counter identified as cnt[11:0], held a value of 2**B – 1 (which is 4095 in a 12-bit system). As the module is enabled, the counter immediately resets to zero. This behavior mirrors the previous discussion where we described how the D register is updated on a rollover of count.

Figure 2: Testbench output of the PWM module.

Design Philosophy

If you are reading this post, you almost certainly have knowledge of digital electronics and are likely new to the Verilog language. For this reason, I have placed detailed headers in the PWM.v file (attached below). I have also attempted to keep the code brief with a limited set of features for the module.
This design constrains the PWM to a power of two division of the clock (100 MHz for the Digilent Basys 3 FPGA board). This PWM is designed with a B-bit counter that naturally rolls over. As an example, recall that an 8-bit counter is a modulus 256 device. It rolls over from 255 to 0, which is 11111111 to 00000000. This limits the PWM frequency to a few discrete values as shown in the following table and defined by the equation:

f_{PWM} = \dfrac{100\ MHz}{2^B}

Where B is the instantiated bit-width of the PWM.

+---------+--------------+
|bit width| PWM frequency|
+---------+--------------+
|    7    |     781,250  |
|    8    |     390,625  |
|    9    |     195,313  |
|   10    |      97,656  |
|   11    |      48,828  |
|   12    |      24,414  |
|   13    |      12,207  |
|   14    |       6,104  |
|   15    |       3,052  |
|   16    |       1,526  |
|   17    |         763  |
|   18    |         381  |
|   19    |         191  |
|   20    |          95  |
|   21    |          48  |
|   22    |          24  |
|   23    |          12  |
|   24    |           6  |
+---------+--------------+

For more frequency and bit options, we certainly could modify the module by adding a prescaler. This would effectively divide each frequency in the table by, 2, 4, 8, 16, etc. We could also add modulo-N counter which would allow fine adjustments to the PWM frequency. You may choose to make these modifications to the PWM module. However, I would suggest that the design is good enough for many applications.

There is a broad range of frequencies that will accommodate most of your PWM needs from 1.5 kHz to 98 kHz 16 and 10-bits respectively. Some readers may rightly question the need to implement large registers for the lower PWM frequencies. They would be correct that such a design consumes FPGA fabric. However, remember that this design is for the beginner to intermediate Verilog programmer. You are unlikely to run out of fabric. If you do, you will be ready to modify this module to reduce size and target your specific PWM resolution and frequency needs.

Also don’t forget that the synthesis tools will do the yeoman’s work of trimming unused logic. For example, suppose you select a 24.4kHz PWM frequency (12-bits wide). Yet you only want to use an 8-bit wide driver. Use the concatenation operation as you instantiate the PWM. It would look like this:

   .d_in({my_drive_byte, 4’d0}),  // 24.4 kHz with an 8-bit resolution

In the background, the synthesis tools will keep the 12-bit clock counter. It will trim away all logic formally used to perform the comparison on the lower 4 bits. In effect, we have constructed a divide by 16 prescaler with minimal effort. The results are suggested in Figures 3 and 4 which show the before and after trimming when the lower 4 bits are disabled. Be sure to add the comments to remind yourself about this change. The only downside to this approach is the number of warnings generated by the synthesis tools.

On a related note, the synthesizer will take similar action for the MIN and Max duty cycle. If they are set to 0 and 100 respectively, the logic represented as the MUX in Figure 1 will be trimmed as it has no action; D is always assigned d_in.

Figure 3: VIVADO schematic for a full 12-bit instantiation.

Figure 4: Synthesizer trimmed schematic for 12-bit instantiation with 8-bit control as .d_in({sw, 4’d0}),

As a rule, build and test your logic so that it may be synthesized without generating warnings. Later, as you instantiate the design into a larger project pay close attention to the warnings and suppress those that you know for absolute certainty may be ignored.

Before departing this section, we should talk about clock boundaries. The PWM features a synchronous design with all elements operating on the positive edge of the system clock. A consequence of this design decision is the instantiation of many registers to hold the output values. These registers are responsible for synchronizing the device within the greater FPGA project. This may or may not be necessary for your design. However, as a rule, a synchronous design will provide an increased level of stability and more predictable operation.

Conclusion

The techniques described in this post will allow you to construct a simple Verilog based PWM. You are encouraged to construct and debug logic to the point where no warnings are generated. You are then encouraged to leverage the power of the synthesis tools to perform operations such as prescaling by using the Verilog concatenate operator.

We look forward to hearing from you as you design, build, troubleshoot microcontroller and digital logic-based systems. Your feedback is invaluable to us and our readers. Please share your comments and question about this material on this page or back on DigiKey’s primary TechForum page.

Best Wishes,

APDahlen

About this author

Aaron Dahlen, LCDR USCG (Ret.), serves as an application engineer at DigiKey. He has a unique electronics and automation foundation built over a 27-year military career as a technician and engineer which was further enhanced by 12 years of teaching (interwoven). With an MSEE degree from Minnesota State University, Mankato, Dahlen has taught in an ABET-accredited EE program, served as the program coordinator for an EET program, and taught component-level repair to military electronics technicians. Dahlen has returned to his Northern Minnesota home and thoroughly enjoys researching and writing articles such as this.

PWM.v

//******************************************************************************
//
// Module: Pulse Width Modulation
//
//  This RTL is subject to Term and Conditions associated with the 
//  DigiKey TechForum. Refer to:
//  https://www.digikey.com/en/terms-and-conditions?_ga=2.92992980.817147340.1696698685-139567995.1695066003
//
//  Should you find an error, please leave a comment in the forum space below. 
//  If you are able, please provide a recommendation for improvement.
//
//******************************************************************************
//       ______________________________________________
//      |                                              |
//      | PWM                                          |
//      |______________________________________________|
//      |                                              |
//      |    Parameters and defaults                   |
//      |        B = 12                                |
//      |        D_MIN_PERCENT = 0                     |
//      |        D_MAX_PERCENT = 95                    |
//      |                                              |
//  ----| enable                                       |
//  ==B=| d_in                                 PWM_out |----
//      |                                          cnt |==B=
//  ----| clk                                          |
//      |______________________________________________|
//
//** Description ***************************************************************
//
//  A Pulse Width Modulator (PWM). When enabled, the count advances on the 
//  rising edge of the system clock.
//
//** Sample Instantiation ******************************************************
//
//    PWM #(
//        .B(B),
//        .D_MIN_PERCENT(D_MIN_PERCENT),
//        .D_MAX_PERCENT(D_MAX_PERCENT)
//    )
//    PWM(
//        .clk(clk),
//        .enable(enable),
//        .d_in(d_in),
//        .PWM(PWM),
//        .cnt(cnt)
//    );
//
//** Parameters ****************************************************************
//
//  B: bit Width of PWM and associated registers.
//
//  D_MIN: Minimum duty cycle as an integer percentage (0 to 100).
//
//  D_MAX: Maximum duty cycle as an integer percentage (0 to 100).

//
//** Signal Inputs: ************************************************************
//
//  1) clk: High speed system clock (typically 100 MHz)
//
//  2) enable: Activates the PWM when logic high. PWM idles low when deactivated.
//
//  3) d_in: Is used to determine the duty cycle of the PWM. This input has a 
//     bit width defined by the B parameter.
//
//** Signal Outputs ************************************************************
//
//  1) PWM: Provides a Pulse Width Modulated signal. The frequency is 
//     determined as described in the comments.
//
//  2) cnt: Provides access to the PWM register. This is a PO2 implementation 
//     that will naturally overflow modulus 2^B.
//
//** Comments ******************************************************************
//
//  1) Given a 100 MHz clock and a 12-bit register width as defined by the "B" 
//     parameter, the PWM has a frequency of:
//
//          100 MHz
//         ----------   = 24.4 kHz
//            2^12
//
//  2) The PWM duty cycle input is buffered. The new duty cycle starts 
//     when the PWM count is zero.
//
//  3) TODO: Add guards ensuring that D_MAX > D_MIN.
//
//******************************************************************************

`define CHECK_PERCENT_RANGES

module PWM #(parameter 
    B = 12,                             // 24.4 kHz PWM assuming a 100 MHz clk
    D_MIN_PERCENT = 0,
    D_MAX_PERCENT = 95
)
(
    input wire clk,
    input wire enable,
    input wire [B - 1:0] d_in,
    output reg PWM,
    output reg [B - 1:0] cnt
);

//** CONSTANT DECLARATIONS *****************************************************

    /* General shortcuts */
        localparam T = 1'b1;
        localparam F = 1'b0;
   
    /* Convert percentage parameters to binary values */

        localparam D_MIN = (2**B - 1) * D_MIN_PERCENT / 100;
        localparam D_MAX = (2**B - 1) * D_MAX_PERCENT / 100;

//** SIGNAL DECLARATIONS *******************************************************

    reg[B-1:0] D;

//** Body **********************************************************************

    always @(posedge clk) begin

        if(!enable) begin  //*******************************************
            cnt <= 2**B - 1;        // When enable is released, the PWM will
            PWM <= F;               // start at zero on next rising edge of 
         end else begin             // clock thereby starting a full PWM cycle.
                                    // This also allows for continuous update of
                                    // D while in a disabled condition.
                                    // For example in an 8-bit system, cnt = 255.
                                    //*********************************************
            cnt <= cnt + 1'd1;
            PWM <= ((cnt + 1'd1) >= D) ? F : T;
        end

        if (cnt == (2**B - 1)) begin            // ready D for the next full cycle
            if (d_in < D_MIN)      D <= D_MIN;
            else if (d_in > D_MAX) D <= D_MAX;
            else                   D <= d_in;
        end

    end

endmodule

tb_PWM.v

//*************************************************************
//
// TESTBENCH for PWM
//
// Aaron Dahlen
//
// Description:
//
//    This simple testbench provides a stimulus to the PWM module.
//    The graphical signal timing diagram serves as the main
//    debugging tool. This testbench also provides limited
//    output to the test console including the count and
//    realtime information. It is up to programmer to interpret
//    this information based on the selected PWM duty cycle.
//
// Comments:
//
//  1) Set the various PWM parameters using the localparam
//     found in the CONSTANT DECLARATION section of this 
//     testbench.
//

//*************************************************************
module tb_PWM();

    /* Module Inputs */
        reg clk;
        reg enable;
        reg [11:0] d_in;

    /* Module Outputs */
        wire PWM;
        wire [11:0] cnt;

//** CONSTANT DECLARATION ************************************

   /* Local */
        localparam B = 12;
        localparam D_MIN_PERCENT = 0;
        localparam D_MAX_PERCENT = 90;

    /* Clock simulation */
        localparam clock_T_ns = 10;     // 100 MHz

    /* General shortcuts */
        localparam T = 1'b1;
        localparam F = 1'b0;


    /* Testbench Specific */


//** SYMBOLIC STATE DECLARATIONS ******************************

//** SIGNAL DECLARATIONS **************************************

     reg [31:0] i;

//** INSTANTIATE THE UNIT UNDER TEST (UUT)*********************


    PWM #(
        .B(B),
        .D_MIN_PERCENT(D_MIN_PERCENT),
        .D_MAX_PERCENT(D_MAX_PERCENT)
    )
    test_PWM(
        .clk(clk),
        .enable(enable),
        .d_in(d_in),
        .PWM(PWM),
        .cnt(cnt)
    );

//** ASSIGN STATEMENTS ****************************************

//** CLOCK ****************************************************

    always begin
        clk = T;
        #(clock_T_ns/2);
        clk = F;
        #(clock_T_ns/2);
    end

//** UUT Tests ************************************************ 

    initial begin

        initial_conditions();
        
    /* Begin tests */
        d_in = 50;

        delay_N_clocks(3);
        enable = T;
        delay_N_clocks(5000);

        d_in = 1500;
        delay_N_clocks(5000);

       // $monitor($realtime, " count = %d", cnt);

        $finish;
    end

//** Tasks **************************************************** 

    task initial_conditions(); begin
        repeat(5) @(posedge clk)
        enable = F;
        end
    endtask
    
    task delay_N_clocks(input integer N); begin
        repeat(N) @(posedge clk);
        end
    endtask
    
endmodule