在 Verilog 中實作脈衝寬度調變器(PWM)

引言

世界真的需要另一個脈衝寬度調變器的 Verilog 實作嗎?

網路上有幾十個例子,複雜度各不相同。本文將探討一個中等複雜度的例子。更重要的是,我們將探討 Verilog 模組建置過程中的一些設計決策。這包括 PWM 寬度的參數化、佔空比的界限,以及為了確保穩定性而在給定時脈範圍內嚴格運作。此外,我們也將探討一種務實的設計理念。我們不僅會探討「是什麼」,還會嘗試解釋「為什麼」做出特定的設計決策。

PWM 工作原理

數位 PWM 模組的核心由三個模組組成。如圖 1 所示,我們需要一個計數器、一個比較器和一個用於儲存所需佔空比的暫存器。計數器從零開始持續計數,直到達到由計數器暫存器位數定義的模數。然後,比較器模組將計數暫存器的值與所需的佔空比進行比較。如果計數大於或等於 D,則 PWM 輸出置高;否則,輸出置低。

圖 1 包含額外的電路,用於將佔空比限制在給定的最小值和最大值之間。這在某些應用中是一項理想的功能。例如,假設 PWM 用於控制橋式電路中的上部 MOSFET。該位置的 MOSFET 通常由自舉電路供電。長時間以全佔空比(始終導通)運作會導致自舉電容放電,使 MOSFET 進入線性模式,從而導致過熱,進而產生不良運作。因此,需要對佔空比進行限制。

這種限制由多工器 (MUX) 實現。如果 d_in 介於最小值和最大值之間,則將其傳遞。否則,MUX 將選擇最小值或最大值。在適當的時候,所需的佔空比值將被鎖定在暫存器中。為了防止毛刺,「適當的時候」發生在週期的開始。為簡單起見,圖 1 中未顯示此精細的同步細節。

圖 1:PWM 的方塊圖表示。

PWM 的核心功能由這兩行 Verilog 程式碼實作。

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

PWM 輸出根據計數比較 (cnt) 和所需佔空比 (D) 暫存器的比較來同步設定。關於計數器,必須避免一個微妙的「相差一」(OB1) 錯誤。請注意,計數同步遞增 1,如第一行所述。第二行也使用計數值來進行比較。由於我們使用的是非阻隔分配(在 always@(thoughtge clk)內同步),我們必須確保比較與計數器增量在相同的時脈週期中。這是透過使用相同的 cnt+1’d1 執行比較來完成的。從狀態和下一個狀態的角度來思考這個想法是有幫助的,其中 cnt 是狀態,cnt+1’d1 是下一個狀態。

最小和最大佔空比電路運用了一些實用的 Verilog 技巧。當模組實例化時,程式設計師需要將最小和最大佔空比指定為整數百分比。這用於建立兩個 Verilog 局部參數:

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

為了更好地理解其工作原理,請考慮這樣一種情況:程式設計師實例化了一個 12-bit 寬的 PWM,並將最小值和最大值分別設為 5% 和 90%。在 12-bit 寬下,當 D 值為 4095 時,PWM 達到最大值(完全導通)。 5% 的值近似為 204,90% 的值近似為 3685。注意,我們不能使用 2B 作為基數,因為當最大值設定為 100% 時,2B 的值超出了範圍。為了理解原因,考慮一個 8-bit 系統。28 等於 256。在模 256 系統中,256 等於 0。因此,我們使用 28 - 1。

如圖 1 所示,d_in、D_MIN 和 D_MAX 參數可以看作是多工器 (MUX) 的輸入。以下代碼描述了該操作。請注意,「MUX」的輸出是 D 暫存器。

     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

這段程式碼中有一個微妙但重要的細節,即何時更新 D 暫存器。回顧我們之前關於狀態和狀態的討論,我們可以看到,下一個 D 值是在模數 B 計數器溢位時計算的。這一點很重要,因為它能防止意外操作。它確保新的 D 值在時脈正緣生效,同時計數器重設為零。

我們將要探討的最後一個程式碼段是重設和使能部分,如下所示。檢查可知,缺少使能訊號將停止 PWM 並將其值設為低。當模組再次啟用時,計數器暫存器會進入準備迎接時脈第一個正緣的狀態。與之前一樣,這個「重置」值是 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

圖 2 所示的測試平台輸出清楚地顯示了 PWM 的重設操作。PWM 模組的致能線被置位,從而啟動 PWM 進程。對於這個特定的 12-bit 實例,我們可以看到計數器 cnt[11:0] 的值為 2**B – 1(在 12-bit 系統中為 4095)。模組致能後,計數器立即重設為零。這一行為與我們之前討論的計數溢位時 D 暫存器的更新方式相符。

圖 2:PWM 模組的測試平台輸出。

設計理念

如果您正在閱讀這篇文章,那麼您很可能具備數位電子學知識,並且可能對 Verilog 語言不太熟悉。因此,我在 PWM.v 檔案(附件如下)中提供了詳細的頭檔。此外,我還盡量保持程式碼簡潔,模組的功能也較為有限。

此設計將 PWM 頻率限制在時脈頻率的 2 的冪分頻點(對於 Digilent Basys 3 FPGA 開發板,時脈頻率為 100MHz)。此 PWM 採用 B-bit 計數器設計,該計數器本身會發生溢位。例如,一個 8-bit 計數器是模 256 裝置。它會從 255 溢出到 0,即從 11111111 溢出到 00000000。這會將 PWM 頻率限制在幾個離散值範圍內,如下表所示,並由以下公式定義:

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

其中 B 是 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  |
+---------+--------------+

為了獲得更多頻率和位數選項,我們當然可以透過增加預分頻器來修改模組。這將有效地將表格中的每個頻率分別除以 2、4、8、16 等。我們也可以加入模 N 計數器,以便對 PWM 頻率進行微調。您可以選擇對 PWM 模組進行這些修改。不過,我認為該設計對於許多應用來說已經足夠好了。

此模組的頻率範圍很廣,從 1.5kHz 到 98kHz,分別對應 16-bit 和 10-bit,足以滿足您的大部分 PWM 需求。一些讀者可能會質疑,對於較低的 PWM 頻率,是否需要使用大型寄存器。他們的說法是正確的,因為這樣的設計會佔用大量的 FPGA 空間。但是,請記住,該設計是為初級到中級 Verilog 程式設計師設計的。您不太可能遇到 FPGA 空間不足的問題。即使真的遇到了,您也可以修改模組以縮小尺寸,並滿足您特定的 PWM 解析度和頻率需求。

另外,別忘了綜合工具會自動完成精簡未使用邏輯的大量工作。例如,假設您選擇了 24.4kHz 的 PWM 頻率(12-bit 寬),但您只想使用 8-bit 寬的磁碟機。在實例化 PWM 時,可以使用級聯操作。程式碼如下所示:

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

在背景,綜合工具會保留 12-bit 時脈計數器。它會移除所有原本用於對低 4-bit 進行比較的邏輯。實際上,我們以最小的努力構建了一個 16 分頻預分頻器。圖 3 和圖 4 展示了禁用低 4-bit 後,調整前後的結果。請務必添加註釋以提醒自己注意此更改。這種方法的唯一缺點是綜合工具會產生大量警告。

另外,綜合器會對最小和最大佔空比採取類似的操作。如果它們分別設為 0 和 100,則圖 1 中表示為 MUX 的邏輯將被移除,因為它沒有實際作用;D 始終被賦值為 d_in。

圖 3:VIVADO 完整 12-bit 實例的原理圖。

圖 4:合成器簡化的原理圖,用於 12-bit 實例化,8-bit 控制為 .d_in({sw, 4’d0}),

通常情況下,應先建構並測試邏輯,確保其在綜合時不會產生警告。之後,在將設計實例化到更大的項目中時,請密切注意警告,並忽略那些可以完全確定可以忽略的警告。

在結束本節之前,我們應該討論時脈邊界。PWM 採用同步設計,所有元件都在系統時脈的正緣運作。這種設計決策導致需要實例化許多暫存器來保存輸出值。這些暫存器負責在更大的 FPGA 專案中同步裝置。這對於您的設計來說可能並非必要。但是,通常情況下,同步設計能夠提供更高的穩定性和更可預測的運作。

結論

本文介紹的技術將幫助您建立一個簡單的基於 Verilog 的 PWM 電路。我們鼓勵您建立並調試邏輯,直至不再出現任何警告。之後,我們鼓勵您利用綜合工具的強大功能,例如使用 Verilog 的 concatenate 運算子進行預分頻等操作。

我們期待您在設計、建構和調試基於微控制器和數位邏輯的系統時與我們分享您的經驗。您的回饋對我們和我們的讀者都至關重要。請在本頁面或 DigiKey 的主技術論壇頁面上分享您對本文的評論和問題。

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