베릴로그로 PWM(Pulse Width Modulator, 펄스 폭 변조기) 구현하기

서론

우리는 PWM을 또 다르게 구현한 베릴로그가 여전히 필요할까요?

웹에는 다양한 복잡성을 가진 다른 수많은 PWM 예제가 있습니다. 본 게시글에서는 적당히 복잡한 예제를 살펴볼 것입니다. 더 중요한 것은, 베릴로그 모듈의 구축에 수반되는 일부 설계 결정 사항도 살펴볼 것입니다. 여기에는 PWM 폭의 매개변수화, 듀티 사이클의 한계, 그리고 주어진 클럭 경계 내에서의 엄격한 동작이 포함됩니다. 또한, 실용적인 설계 철학도 살펴볼 것입니다. "무언가"를 찾아보는 대신 “왜” 그런 특정 설계를 결정하였는지에 대해 설명하려 할 것입니다.

PWM 동작

디지털 PWM 모듈의 코어는 세 가지 블록으로 구성됩니다. 그림 1과 같이, 카운터, 비교기, 그리고 원하는 듀티 사이클을 저장할 레지스터가 필요합니다. 카운터는 0부터 시작하여 카운터 레지스터의 비트 수에 의해 정의된 모듈러스까지 계속해서 카운트합니다. 그런 다음 비교기 블록이 카운트 레지스터의 값과 원하는 듀티 사이클 값을 비교합니다. 카운트 값이 D보다 크거나 같으면 PWM 출력은 하이로 설정되고, 그렇지 않으면 로우로 설정됩니다.

그림 1에는 듀티 사이클을 지정된 최소 및 최대값으로 제한하는 추가 회로가 포함되어 있습니다. 특정 응용 분야에서는 필요한 기능입니다. 예를 들어, PWM을 사용하여 브리지 회로의 고측 MOSFET을 제어한다고 가정해 보겠습니다. 해당 위치의 MOSFET은 일반적으로 부트스트랩 회로에 의해 전원이 공급됩니다. 100%의 듀티 사이클(항상 온)로 장시간 동작하면 부트스트랩 커패시터를 방전시켜, MOSFET이 선형 모드로 들어가 과열이 발생함으로 인해 원치 않는 동작을 초래하게 됩니다. 따라서 듀티 사이클에 제한을 둘 필요가 있습니다.

이 제한은 멀티플렉서로 표현되었습니다. 만약 d_in이 최소값과 최대값 사이에 있다면, 값은 그대로 전달됩니다. 그렇지 않을 경우, MUX는 최소값 또는 최대값을 선택합니다. 적절한 시점에 원하는 듀티 사이클 값이 레지스터에 고정됩니다. 글리치(glitch)를 방지하기 위해, 이 "적절한 시점"은 (클럭)주기의 시작 시점에 발생합니다. 간소화를 위해, 이러한 세부적인 동기화 과정은 그림 1에 표시되지 않았습니다.


그림 1: PWM의 블록도 표현.

PWM의 핵심이 아래 두 줄의 베릴로그 코드에 담겨 있습니다.

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

PWM 출력은 카운트(cnt)와 원하는 듀티 사이클(D) 레지스터의 비교에 기반하여 동기화되어 설정됩니다. 카운터에 있어서, 미묘한 OB1(Off By One, 오프 바이 원) 오류는 피해야만 합니다. 코드의 첫 줄에서처럼 카운트는 동기화되어 1씩 증가합니다. 카운트 값은 또한 비교를 위해 두 번째 줄에서도 사용됩니다. 예제 코드는 논블로킹 할당(non-blocking assignment)을 사용해 (always @(posedge clk) 블록 내에서 동기적이기 때문에, 비교가 카운터 증가와 동일한 클록 사이클 내에서 이루어지도록 해야 합니다. 이는 동일한 cnt + 1’d1을 사용하여 비교를 함으로써 이루어집니다. 이 개념을 cnt가 (현재) 상태이고 cnt + 1’d1이 다음 상태인 상태와 다음 상태의 관점에서 생각하는 것이 도움이 될 것입니다.

최소 및 최대 제한 회로는 몇 가지 유용한 베릴로그 트릭을 포함하고 있습니다. 모듈이 인스턴스화될 때, 프로그래머는 최소 및 최대 듀티 사이클을 정수 퍼센트 값으로 지정합니다. 이는 두 개의 베릴로그 로컬 매개변수를 만드는 데 사용됩니다:

    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 모듈로 시스템에서 이는 0과 같습니다. 따라서 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 값이 계산된다는 것을 알 수 있습니다. 이는 예상치 못한 동작을 방지할 수 있기 때문에 중요합니다. 카운터가 0으로 리셋되는 순간 새로운 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)의 값을 유지하는 것을 볼 수 있습니다. 모듈이 활성화되면 카운터는 즉시 0으로 리셋됩니다. 이 동작은 카운트가 롤오버 될 때 D 레지스터가 업데이트되는 방식에 대한 이전 설명과 일치합니다.


그림 2: PWM 모듈의 테스트벤치 출력.

설계 철학

이 글을 읽고 있다면, 여러분은 거의 확실히 디지털 공학에 대한 지식이 있을 것이며, 베릴로그 언어에는 익숙하지 않을 가능성이 큽니다. 이러한 이유로, 저는 (아래에 첨부된) PWM.v 파일에 자세한 헤더를 추가하였습니다. 또한 최소한의 기능만으로 모듈의 코드를 간결하게 작성하도록 애썼습니다.

이 설계는 PWM을 클럭(Digilent Basys 3 FPGA 기판의 경우 100MHz)을 2의 제곱으로 나눈 값으로 제한합니다. 이 PWM은 당연히 롤오버 되는 B-비트 카운터로 설계되었습니다. 예를 들어, 8비트 카운터는 모듈로 256 장치입니다. 255에서 0으로, 즉 11111111에서 00000000으로 롤오버 됩니다. 이로 인해 PWM 주파수는 다음 표에 표시된 몇 가지 개별 값으로 제한되며, 이 값은 아래 식에 의해 정의됩니다:

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

여기서 B는 PWM의 인스턴스화된 비트 폭을 나타냅니다.

+---------+--------------+
|비트 폭|PWM 주파수|
+---------+--------------+
|    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 등으로 효율적으로 나눌 것입니다. 또한, PWM 주파수를 미세하게 조정할 수 있는 모듈로-N 카운터를 추가할 수도 있습니다. PWM 모듈을 이렇게 수정하도록 선택할 수도 있겠지만, 현재 설계는 대부분의 응용 분야에 충분히 적합하다고 판단됩니다.

각각 16비트와 10비트인 1.5kHz에서 98kHz에 이르는 넓은 범위의 주파수가 필요한 PWM 대부분을 수용할 수 있을 것입니다. 일부 독자들은 큰 레지스터 구현의 필요성에 마땅히 의문을 제기할 수 있습니다. 이러한 설계는 FPGA 리소스를 소모하기 때문에 맞는 말일 수도 있습니다. 그러나 이 설계는 초보자부터 중급 수준의 베릴로그 프로그래머를 위한 것이므로, 리소스가 쉽게 소모되지는 않을 것입니다. 만약 리소스가 부족하다면, 여러분은 모듈의 크기를 줄이고 특정 PWM 해상도와 필요한 주파수에 맞게 모듈을 수정할 준비가 되어 있을 것입니다.

또한, 합성도구는 사용하지 않는 로직을 꾸준히 제거할 것입니다. 예를 들어, (12비트 폭의) 24.4kHz PWM 주파수를 선택했다고 가정해 보겠습니다. 그렇지만 8비트 드라이버만 사용하고자 합니다. PWM을 인스턴스화할 때 결합(concatenation) 연산을 사용하십시오. 이는 다음과 같을 것입니다:

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

합성 도구는 백그라운드에서 12비트 클럭 카운터를 유지하지만, 하위 4비트의 비교 작업에 공식적으로 사용된 모든 로직을 제거할 것입니다. 사실상, 최소한의 노력으로 16으로 나누는 프리스케일러를 구현한 것입니다. 하위 4비트를 사용하지 않아 로직이 제거되기 전후의 결과가 그림 3과 그림 4에 있습니다. 이 변경 사항을 기억할 수 있도록 주석을 추가하는 것이 좋습니다. 이 접근 방식의 유일한 단점은 합성 도구가 생성하는 경고 메시지의 수입니다.

이와 관련하여, 합성기는 MIN 및 MAX 듀티 사이클에 대해서도 유사한 동작을 수행합니다. 만약 이들이 각각 0과 100으로 설정되면, 그림 1의 MUX로 표시된 로직은 아무런 동작을 하지 않기 때문에 제거될 것입니다; D는 항상 d_in에 할당됩니다.


그림 3: 완전한 12비트 인스턴스화에 대한 VIVADO 회로도.


그림 4: .d_in({sw, 4’d0})와 같이 8비트 제어가 사용된 12비트 인스턴스화에 대해 합성기가 다듬은 회로도.

일반적으로, 경고를 발생시키지 않고 합성할 수 있도록 로직을 설계하고 테스트하십시오. 나중에 설계를 더 큰 프로젝트에 인스턴스화할 때 경고를 주의 깊게 관찰하여 전적으로 확실히 무시해도 되는 경고는 억제하십시오.

이 부분을 마무리하기 전, 클럭 경계에 대해 이야기해야 합니다. PWM은 모든 구성요소가 시스템 클럭의 상승 에지에서 작동하는 동기식 설계를 특징으로 합니다. 이러한 설계 결정 사항의 결과로, 출력값을 저장하기 위해 많은 레지스터들이 인스턴스화됩니다. 이러한 레지스터들은 상위 FPGA 프로젝트 내에서 장치를 동기화하는 역할을 합니다. 여러분의 설계에 필요 없을 수도 있지만, 일반적으로 동기식 설계는 향상된 안정성과 보다 예측 가능한 동작을 제공합니다.

결론

이 게시글에 설명된 기술을 통해 간단한 베릴로그 기반 PWM을 설계할 수 있을 것입니다. 경고가 발생하지 않을 때까지 로직을 설계하고 디버그하시기 바랍니다. 그런 다음, 베릴로그 결합 연산자를 사용한 프리스케일링과 같은 작업을 수행하기 위해서는 합성 도구의 강력한 기능을 활용하시기 바랍니다.

마이크로컨트롤러 및 디지털 로직 기반 시스템을 설계, 구축, 그리고 문제 해결을 하는 과정에서 발생하는 여러분의 의견을 기다리겠습니다. 여러분의 피드백은 디지키 뿐만 아니라 다른 독자에게도 매우 소중합니다. 이 페이지 또는 디지키의 TechForum 페이지에 게시글에 대한 댓글과 질문을 공유해 주시기 바랍니다.

감사합니다,

APDahlen

저자 정보

미합중국 해안경비대(USCG) 소령(LCDR)으로 전역한 Aaron Dahlen은 DigiKey에서 애플리케이션 엔지니어로 근무하고 있습니다. 27년간의 군 복무 동안 기술자 및 엔지니어로서 쌓아온 그 만의 전자 및 자동화에 대한 지식은 12년간의 교단을 통해 (상호 연계되어) 더욱 향상되었습니다. 미네소타 주립대학, Mankato에서 전기공학 석사(MSEE) 학위를 받은 Dahlen은 ABET(Accreditation Board for Engineering and Technology, 미국 공학 기술 인증 위원회) 공인 전기공학 과정을 가르치고, EET(Electrical Engineering Technology, 전기공학 기술) 과정의 프로그램 조정관으로 일했으며, 군 전자 기술자에게 부품 수준의 수리에 대해 가르쳤습니다. 미네소타 주 북부의 집으로 돌아와 이런 류의 연구와 글쓰기를 즐기고 있습니다.

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


영문 원본: Implementing a Pulse Width Modulator (PWM) in Verilog