在本教學中,我們將探索如何在Verilog中實現硬體PWM,並瞭解Verilog代碼的模組化如何使我們能夠根據需要在Mojo中配置盡可能多的硬體PWM電路。
以下是您需要遵照實施的所有內容:
我們將在嵌入式Micro提供的Mojo Base Project的基礎上,為該項目構建我們自己的Verilog代碼。以這些項目為基礎來構建是很有説明的,因為設備規格和ISE中的其他初始化工作都已經為我們設置好了!如果需要,您可以通過對專案目錄中的.xise專案檔案進行重命名來更改項目名稱。我將其命名為“MojoPWM.xise”。
通常,我們首先會在UCF中表明名稱和引腳編號,用於與Mojo上的I/O引腳建立的不同連接。但是,對於本項目,我們將使用板載LED,訊號名稱和引腳連接都已經指定好了。因此,此處不需要額外的聲明。我們將從創建一個新的Verilog模組開始,該模組將指定我們硬體PWM的行為。並非將代碼直接放入mojo_top模組中,我們將創建一個獨立的模組以利用模組化設計。如果我們要創建不同的硬體PWM電路來運行不同的LED,則只需要創建該種獨立PWM模組的新實例即可,無需複製和粘貼大量代碼。
按右鍵左側“Hierarchy”視窗中的任意位置,然後點擊名為“New Source…”的選項,在“Source Type”的選項清單中,選擇“Verilog Module”,並將檔命名為“PWM.v”,以此將創建出一個新的Verilog檔,該檔具有用於PWM模組的框架。
在真正開始編寫代碼之前,我們先來討論一下我們的PWM實現方法。如前所述,我們用該硬體生成的訊號本身是週期性的,也就是說訊號值與時間有關。因此,我們必須根據不間斷的滴答時鐘訊號來指定其行為。這個時鐘訊號已經作為系統輸入包含在mojo_top模組中了,為方波訊號,圖形如下所示:
我們的硬體操作可總結如下:
我們選擇值255作為最大計數器值,因為這是8位元數值中可以存儲的最大值(11111111)。如果從該值增加1,則計數器將回復到00000000,因為會溢出。要瞭解有關二進位計算和整數的二進位表示的更多資訊,請點擊下面附錄中的連結進行查看!
這是一個時序圖,表示我們隨內部時鐘訊號的硬體操作:
我們將以輸入和輸出訊號清單作為開始,進行對PWM模組的Verilog描述:
input clk,
input rst,
input[7:0] duty,
output sig_drv
您可能已經從它們的名稱中看出來了,這四個訊號分別為時鐘、重定、占空比值和PWM輸出訊號。
接下來,我們需要限定輸出訊號sig_drv的資料類型。Verilog有兩種資料類型,線網(wire)和寄存器(reg)。雖然這兩種類型之間的差異對我們的應用程式來說是很微小的,但是有一個主要區別需要注意,就是當我們就像在本項目中這樣使用always塊時,只能寫regs而不能寫wires。我們隨後會討論always塊及其相關操作。如果訊號清單中的訊號沒有被限定為wire 或 reg,Verilog將默認其聲明為wire類型。在這種情況下,我們需要通過模組訊號清單之後的以下行將sig_drv描述為reg:
reg sig_drv;
我們還將使用如上所述的8位元數目器,並且通過always塊對其進行設置。因此,我們需要聲明一個8位元大小的計數器,如下所示:
reg[7:0] counter;
您可能已經注意到了,像許多其他程式設計語言一樣,Verilog是0索引的,這意味著計數總是從0開始的。因此,一個8位元數目器中的位元索引值為數位0到7。
接下來,我們對8位元數目器的向上計數和輸出訊號的驅動的邏輯進行描述。我們可以使用always來完成!always塊是一種Verilog結構,使用者可以指定僅在always塊的觸發條件被滿足時才會進行的操作。一個always塊的基本結構如下:
always @ (…)
begin
…
end
在“@”符號後的括弧內,使用者需要指定觸發條件,該條件將決定何時執行塊內的邏輯。在我們的專案中,需要兩個always塊:
always @(*)
begin
end
always @(posedge clk or posedge rst)
begin
end
在第一個塊中,觸發條件為“*” ,這意味著只要專案中的任何訊號發生變化,該塊內的邏輯就會被執行。硬體工程師可能將此塊稱為組合邏輯,該邏輯始終將輸出值定義為輸入值的某些函數。在此塊中,我們將放入在所有時刻都起作用的邏輯,而不是每個時鐘週期只執行一次的邏輯,如輸出訊號的驅動。
如前所述,輸出訊號為高電平驅動還是低電平驅動取決於計數器相比於占空比的大小。可以通過以下代碼行中的always @ (*) 塊實現此功能:
if (duty > counter)
begin
sig_drv = 1’b1;
end
else
begin
sig_drv = 1’b0;
end
sig_drv訊號的寬度為1位元(只有0和1兩種值…一個位),因此我們在給它分配的值前加上字元“1’b”。從上面的代碼中我們可以看到,當占空比大於計數器值時,sig_drv線被驅動為1(高),否則被驅動為0(低)。
在第二個塊中,觸發條件為posedge clk 或 posedge rst。這意味著當時鐘訊號從低電平變為高電平或復位線從低電平變為高電平時,該塊內的邏輯被執行,且在每個時鐘週期內僅執行一次。我們將使用該always塊來指定每執行一個時鐘週期時的計數器增加。可以使用此塊中的以下代碼行完成此操作:
if (rst)
begin
counter <= 8’b0;
end
else
begin
counter <= counter + 1;
end
if語句的第一段指定了當復位線變為高電平時,必須將8位元數目器復位為全零。第二段的else條件下指定了如果復位線不是高電平,則計數器的值會被增加。
我們可以看到分配給計數器的值取決於其先前的值。硬體工程師將這種類型的邏輯稱為順序模型,因為輸出既是輸入的函數,也是過去狀態值的函數。
關於該代碼最後需要說明的是“<=” 運算子,即非阻塞設定運算子,用於將值賦給計數器變數。當我們像往常一樣使用“=”運算子(即阻塞設定運算子)來給訊號賦值時,其實我們已經默許Verilog對編寫的代碼自上而下來執行。換句話說,如果我們連續編寫了兩個“=”設定陳述式,那麼第一個賦值操作將會在第二個賦值操作開始之前完成執行。這其實在某些邏輯上是必要的,因為我們可能會使用以此方式分配的值進行後續計算。
但是,在基於高速時鐘訊號的順序邏輯中,我們實際上希望所有的值的分配都在某種程度上並行發生(如果它們彼此獨立的話),這樣我們就不會將程式延遲到與下一個時鐘邊沿發生衝突的程度。在本程式中,我們沒有在每個時鐘邊沿上指定執行多種任務。但是,如果我們需要這樣做的話,使用非阻塞設定運算子可以完成這項操作。
完整的PWM模組應如下所示: