Verilogでパルス幅変調器(PWM)を実装する


APDahlen Applications Engineer

はじめに

世界は、さらにもう1つのVerilogによるパルス幅変調器(PWM)の実装が必要なのでしょうか?

ウェブ上にはさまざまな複雑さを持つ数多くの例が存在します。本記事では中程度の複雑さを持つ例を検討します。さらに重要な点として、Verilogモジュール構築に伴う設計上の判断についても考察します。これには、PWMのパルス幅のパラメータ化、デューティサイクルの範囲制限、安定性確保のための所定のクロック境界内での厳密な動作が含まれます。また、実用的な設計哲学についても探求します。単に「何を」設計するかではなく、特定の設計決定が「なぜ」なされたかについての説明も試みます。

PWMの動作

デジタルPWMモジュールは、その中核において3つのブロックで構成されています。図1に示すように、カウンタ、コンパレータ、そしてデューティサイクル設定値を保持するレジスタが必要となります。カウンタは、ゼロから、カウンタレジスタのビット数で定義される最大値まで、連続的にカウントします。次に、コンパレータブロックがカウントレジスタの値とデューティサイクル設定値を比較します。カウント値がD(デューティサイクル設定値)以上であればPWM出力はハイレベルに設定され、そうでない場合はローレベルとなります。

図1には、デューティサイクルを所定の最小値および最大値に制限するための追加回路が含まれています。これは特定の用途において望ましい機能です。例えば、ブリッジ回路においてPWMが上側MOSFETの制御に使用されると仮定します。この位置のMOSFETは通常、ブートストラップ回路によってゲート駆動されます。フルデューティサイクル(常時オン)の状態で長時間動作するとブートストラップコンデンサが放電してしまい、MOSFETがリニア動作領域に移行して過熱を引き起こすなど、望ましくない動作が生じる可能性があります。そのため、デューティサイクルに制限を設けることが望ましいです。

この制限処理はマルチプレクサ(MUX)として表わされます。d_inが最小値と最大値の範囲内にある場合は、そのまま通過します。範囲外の場合、MUXによって最小値または最大値のいずれかが選択されます。適切なタイミングで、デューティサイクル設定値がレジスタに保持されます。グリッチを防止するため、「適切なタイミング」はサイクルの開始時に設定されています。簡略化のため、このような微妙な同期の詳細は図1には示されていません。

図1: PWMを示すブロック図

PWMの核心は、以下の2行のVerilogコードに凝縮されています。

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

PWM出力は、カウント値(cnt)とデューティサイクル設定値(D)レジスタの比較に基づき、同期して設定されます。カウンタに関しては、回避すべき微妙なoff-by-one(OB1)エラーが存在します。最初の行で示されているように、カウント値は同期して1ずつインクリメントされることにご注意ください。また、2行目ではこのカウント値を用いて比較が行われます。ノンブロッキング代入(always @(posedge clk) 内で同期動作)を使用しているため、比較がカウンタのインクリメントと同一のクロックサイクル内で行われるようにする必要があります。これを実現するために、比較にはcnt+1’d1という同じ値を用いています。この考え方は、cntを現在の状態、cnt+1’d1を次の状態と捉えると理解しやすくなります。

最小値および最大値の制限回路には、いくつかの有用なVerilogのテクニックが盛り込まれています。モジュールをインスタンス化する際、プログラマーは最小および最大デューティサイクルを整数のパーセンテージで指定します。これは、2つのVerilogローカルパラメータを構築するために用いられます。

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

動作をよりよく理解するために、プログラマーがPWMを12ビット幅でインスタンス化し、最小値と最大値をそれぞれ5%と90%に設定した場合を想定します。12ビット幅では、最大値(フルオン)はDが4095のときに発生します。5%の値はおよそ204に、90%の値はおよそ3685にそれぞれ近似されます。なお、最大値を100%に設定した場合、範囲外となるため、2**Bを基準値として使用することはできない点にご注意ください。その理由を理解するため、8ビットのシステムを例に考えてみます。2**8は256となりますが、モジュロ256カウンタ(8 ビットで値が0~255の範囲で循環するカウンタ)では、これはゼロに相当します。したがって、2**8 – 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レジスタが更新されるタイミングに関して、微妙ですが重要な側面があります。先ほどの現在の状態と次の状態の議論を踏まえると、モジュロBビットカウンタがロールオーバーするタイミングで次のDの値が計算されることがわかります。これは予期しない動作を防ぐうえで重要です。これにより、新しい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ビットインスタンス化の例では、cnt[11:0]として示されているカウンタが2**B – 1(12ビットシステムでは4095)の値を保持していることが確認できます。モジュールがイネーブルされると、カウンタは直ちにゼロにリセットされます。この挙動は、カウンタのロールオーバー時にDレジスタが更新される仕組みについて説明した先の議論と対応しています。

図2: PWMモジュールのテストベンチ出力

設計の方針

この投稿をお読みの方は、ほぼ間違いなくデジタルエレクトロニクスの知識をお持ちであり、おそらくVerilog言語にはまだ不慣れである可能性が高いと考えられます。そのため、PWM.vファイル(下記に添付)には詳細なヘッダコメントを付けました。また、このモジュールの機能を限定し、コードをできるだけ簡潔に保つよう努めています。
この設計では、PWMはクロックを2のべき乗で分周したものに制限しています(Digilent Basys 3 FPGAボードの場合、100MHz)。このPWMは、自然にロールオーバーするBビットのカウンタを用いて設計されています。例として、8ビットカウンタはモジュロ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モジュールに対してこれらの変更を加えることも可能です。ただし、多くの用途においては、この設計で十分であると考えられます。

このPWMでは、1.5kHz(16ビット)から98kHz(10ビット)までの広範な周波数範囲をカバーしており、ほとんどのPWM用途に対応できます。一部の読者は、低いPWM周波数用に大規模なレジスタを実装する必要性について疑問をお持ちかもしれません。そのような設計がFPGAリソースを消費するというご指摘はごもっともです。ただし、この設計は初心者から中級者向けのVerilogプログラマーを対象としていることにご留意ください。FPGAリソースが不足することはまずないでしょう。万が一不足した場合でも、このモジュールを修正してサイズを縮小し、特定のPWM分解能や周波数要件に合わせて調整する準備が整っているはずです。

また、合成ツールが未使用ロジックの削減という重要な役割を担うことも忘れないでください。例えば、24.4kHzのPWM周波数(12ビット幅)を選択した場合でも、8ビット幅のドライバのみを使用したい場合があるでしょう。その際は、PWMをインスタンス化する際に連接演算子をご利用ください。具体的には以下のようになります。

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

バックグラウンドでは、合成ツールが12ビットクロックカウンタを維持します。一方、下位4ビットの比較に用いられていた論理回路がすべて削除されます。結果として、最小限の労力で16分周プリスケーラを構築したことになります。図3および図4には、下位4ビットを無効化した際の、削除の前後の結果が示されています。この変更についてご自身で確認できるよう、必ずコメントを追加してください。この手法の唯一の欠点は、合成ツールによって多数の警告が生成される点です。

関連する話題として、最小(MIN)および最大(MAX)デューティサイクルに対しても、合成ツールは同様の処理を行います。これらがそれぞれ0と100に設定されている場合、図1に示されている MUX として表現されたロジックは、実質的に動作を持たないため削除されます。これは、Dには常にd_inが代入されるためです。

図3: フル12ビットインスタンス化のためのVIVADO回路図

図4: d_in({sw, 4’d0}) のように8 ビット制御の12 ビットでインスタンス化を行った場合の、合成ツールによる削除後の回路図

原則として、警告を発生させることなく合成できるようにロジックをビルドし、テストしてください。その後、設計をより大きなプロジェクトにインスタンス化する際には、警告内容に十分注意を払い、確実に無視してよいとわかっているものは、無視するするようにしてください。

このセクションを終える前に、クロック境界について触れておく必要があります。このPWMは同期設計を採用しており、すべての要素がシステムクロックの立ち上がりエッジで動作します。この設計方針の結果として、出力値を保持するために多数のレジスタがインスタンス化されています。これらのレジスタは、より大規模なFPGAプロジェクト全体の中でデバイスを同期化させる役割を担っています。このような構成が設計によっては必ずしも必要でない場合もありますが、一般的には、同期設計は安定性が高く、より予測しやすい動作をもたらします。

まとめ

この投稿で説明した手法により、VerilogをベースとしたシンプルなPWMを構築することができます。警告が発生しない状態になるまでロジックを構築しデバッグすることをお勧めします。その後、Verilogの連接演算子を用いてプリスケーリングなどの処理を行うことで、合成ツールの持つ能力を活用することをお勧めします。

マイクロコントローラやデジタルロジックを用いたシステムの設計、ビルド、トラブルシューティングに取り組まれる中で、ぜひ皆さまのご意見をお聞かせください。皆さまからのフィードバックは、私たちにとっても読者の皆さまにとっても非常に貴重なものです。本資料に関するご意見やご質問は、このページ、またはDigiKeyのメインTechForumページにお寄せください。

ご健闘をお祈りします。

APDahlen

著者について

Aaron Dahlen氏、LCDR USCG(退役)は、DigiKeyでアプリケーションエンジニアを務めています。彼は、技術者およびエンジニアとしての27年間の軍役を通じて構築されたユニークなエレクトロニクスおよびオートメーションのベースを持っており、これは12年間教壇に立ったことによってさらに強化されました(経験と知識の融合)。ミネソタ州立大学Mankato校でMSEEの学位を取得したDahlen氏は、ABET認定EEプログラムで教鞭をとり、EETプログラムのプログラムコーディネーターを務め、軍の電子技術者にコンポーネントレベルの修理を教えてきました。彼はミネソタ州北部の自宅に戻り、このような記事のリサーチや執筆を楽しんでいます。

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




オリジナル・ソース(English)