引言
世界真的需要另一個脈衝寬度調變器的 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



