緒論 本文將介紹一個完全用Verilog HDL手寫的AMBA片上系統,項目的主題是設計一個基於AMBA匯流排的流水燈控制系統, 項目中所有數字電路邏輯都將通過Verilog進行RTL設計,不會調用成熟IP核, 然後利用Vivado平臺對RTL模型進行模擬、綜合與佈線,最後在FPGA開發板上進行板級驗 ...
緒論
本文將介紹一個完全用Verilog HDL手寫的AMBA片上系統,項目的主題是設計一個基於AMBA匯流排的流水燈控制系統,
項目中所有數字電路邏輯都將通過Verilog進行RTL設計,不會調用成熟IP核,
然後利用Vivado平臺對RTL模型進行模擬、綜合與佈線,最後在FPGA開發板上進行板級驗證。
AMBA是ARM公司推出的一種匯流排架構,目前已非常成熟,在行業內得到廣泛的應用,極具實際應用價值,
本項目涉及了AMBA架構中的AHB協議&APB協議,
系統包括AHB匯流排、APB匯流排兩個部分,
整個系統的基本架構如下圖所示:
總的來說,由AHB匯流排上的主機————流水燈控制單元(Control Unit),發出控制信號,
進而對APB匯流排上的從機GPIO模塊進行配置,實現對流水燈的流動方式的控制,
本項目是一個完整的AMBA匯流排系統,包括匯流排、主機、從機以及外設,
在各章節中,將會介紹如何利用Verilog語言對以下模塊進行RTL設計:
1. Control Unit模塊
2. AHB2APB Bridge模塊
3. GPIO模塊
希望本項目能給對AMBA匯流排架構感興趣的朋友們一些啟發與幫助。
下麵,先簡要介紹一下AHB/APB匯流排的基本特性和數據傳輸時序:
AHB匯流排協議簡介:
AHB匯流排,全稱為Advanced High performance Bus,屬於AMBA2.0規範中的一部分,
在SoC片上系統中,AHB主要用於一些高性能模塊之間的連接(如CPU、DMA、DSP等),
主要的特性包括:單時鐘邊沿操作、支持突發傳輸、支持多個主控制器、可配置匯流排寬度(32位~128位)、支持位元組、半字、字傳輸。
一個典型的AHB匯流排系統通常以下圖所示的方式進行多主機多從機互聯:
可以看出,Master(主機)這邊的地址信號HADDR和數據信號HWDATA都是廣播的,通過仲裁器arbiter給出的hselx來選擇對應的從機,
而Slave(從機)這邊的數據,則是通過Decoder來解碼給主機這邊的。
當HSELx選中對應的從設備後,就可以開始傳輸了,一個無等待傳輸的時序如下所示:
圖中可以看出,AHB匯流排具有流水線特性,地址周期和數據周期交替進行
APB匯流排協議簡介:
APB,全稱為Advanced Peripheral Bus,主要用於低帶寬的周邊外設之間的連接,例如UART、1284等,
APB匯流排架構不像AHB支持多個Master,在APB裡面唯一的Master就是APB橋。
APB匯流排特性包括:
-
兩個時鐘周期傳輸(非流水線,不同於AHB)
-
無需等待周期和回應信號
-
控制邏輯簡單
-
只有四個控制信號
下圖展示了APB匯流排上的一次標準的數據傳輸時序:
其他有關APB匯流排的介紹,將在下文AHB2APB Bridge模塊的設計章節中涉及
1. 系統架構與功能
系統架構:
本項目的系統架構框圖上圖所示,
其中,藍色部分為AHB匯流排以及AHB上的主機從機,粉色部分則是APB匯流排以及APB上的主機從機,
下麵介紹一下匯流排上各個模塊的功能和定義:
-
流水燈控制單元(Contro Unit)
在AHB匯流排上,流水燈控制單元作為Master,
控制單元發出的AHB匯流排信號將經過AHB2APB Bridge,成為APB信號,
APB信號會對GPIO模塊的寄存器進行配置,配置後的GPIO就可以對開發板上的按鍵狀態進行觀測,並對LED燈進行控制 -
AHB轉APB橋(AHB2APB Bridge)
AHB2APB Bridge作為邏輯連接器,一方面是在AHB的Slave,另一方面也是APB的Master(APB匯流排有且只有這唯一1個Master)。
弄清這個概念,就可以定義模塊的輸入輸出的埠,以及需要滿足的時序。
由於AHB匯流排具有流水線特性,而APB沒有,這就需要讓AHB在合適的時候拉低HREADYout添加一個等待周期 -
通用輸入輸出模塊(General-purpose Input/Output)
GPIO模塊作為APB匯流排上的Slave,具有一個APB Slave介面,
除此之外,也會直接連接到FPGA開發板的按鍵外設和LED燈外設:
GPIO模塊的輸入為Key[3:0],用於觀測開發板上的按鍵狀態,
GPIO模塊的輸出為LED[3:0],用於控制開發板上LED燈的亮滅。
由於本項目用到的FPGA開發板的LED外設為共陽極,因此當引腳輸出低電平時,對應LED將被點亮;反之,LED將會熄滅。
項目中涉及到的外設在開發板上的位置如下圖所示:
流水燈系統工作模式:
-
初始靜止模式:所有LED燈熄滅,
上電後/按下reset後,進入該狀態,所有LED燈熄滅,
此時LED_mode = 4'b0000(LED_mode為Control Unit中流水燈工作模式寄存器,用以控制LED燈閃爍的邏輯) -
工作模式0:普通流水燈模式
初始靜止模式同時按下KEY0~KEY3進入工作模式0,
或在工作模式0~3下,按下FPGA開發板上按鍵KEY1進入工作模式0,
此時LED_mode = 4'b0001,
在工作模式0下,開發板上的LED燈將按照下圖所示的方式,做有規律的周期性流動:
LED0~LED4將依次閃爍,每個燈閃爍時長為1s,迴圈周期為4s -
工作模式1:加速流水燈模式
在工作模式0~3下,按下FPGA開發板上按鍵KEY2,進入該工作狀態,
此時LED_mode = 4'b0010,
在工作模式1下,LED燈將加速流動,每個燈閃爍時長為0.5s,迴圈周期為2s:
! -
工作模式2:心跳模式
在工作模式0~3下,按下FPGA開發板上按鍵KEY3,進入該工作狀態,
此時LED_mode = 4'b0100,
該模式下LED等將模仿心跳的節奏進行閃爍(詳見最後一章中FPGA運行視頻) -
工作模式3:呼吸燈模式
在工作模式0~3下,按下FPGA開發板上按鍵KEY4,進入該工作狀態,
該模式下LED將周期性進行逐漸由亮到暗再到亮的變化(周期為4s,其中前2s由暗變數,後2由亮變暗),
此時LED_mode = 4'b1000,
由於FPGA開發板上的LED燈亮度無法直接配置,
該模式是通過調節LED燈的占空比來實現的,占空比越高,小燈的平均功率越大,我們人眼看到的亮度也就越高
2. AHB2APB Bridge模塊設計
首先介紹AHB2APB Bridge模塊的設計方法,
該模塊是AHB與APB匯流排之間的橋梁,負責兩種協議信號的相互轉換,
AHB2APB Bridge的模塊框圖和信號定義如下:
AHB2APB Bridge既是AHB匯流排上的一個Slave,也作為APB匯流排上唯一的Master,
其任務是將來自於AHB匯流排上的信號轉化為APB信號,實現AHB系統和APB的互聯,
那麼如何實現一個可以完成上述功能的Bridge模塊呢?
答案是通過一個有限狀態機(FSM,Finite State Machine)來進行控制,
看看一次典型的AHB到APB信號轉換時序圖(以Read為例):
在T1-T2期間,Bridge作為AHB匯流排上的Slave被HADDR選中,
在T2-T4兩個時鐘周期內,Bridge將AHB的HADDR上的地址Addr1放在APB匯流排的PADDR上,
其中T2-T3,PENABLE尚未被拉高,且HREADY會被Bridge拉低,因為APB的傳輸速度是低於AHB的,需要AHB匯流排上的Master進行等待,
而T3-T4,PENABLE被Bridge使能,數據信息被放上PRDATA並且由Bridge轉交給HRDATA,
我們將T1-T2,T2-T3,T3-T4這個周期分別稱為IDLE,SETUP和ENABLE,
於是我們得到AHB2APB Bridge的狀態機轉換示意圖:
註意:在T2-T4傳輸Data1後,APB匯流排又在T4-T6兩個周期內完成了Data2的傳輸,
這兩次傳輸之間並沒有經歷IDLE狀態,這表明當AHB匯流排上仍然有傳輸任務時,ENABLE狀態結束後會直接再次跳往SETUP狀態以準備下一個數據的傳輸,
AHB2APB Bridge模塊完整RTL如下:
module AHB2APB_bridge #(
//HRANS Parameters
parameter IDLE = 2'b00 ,
parameter BUSY = 2'b01 ,
parameter SEQ = 2'b10 ,
parameter NONSEQ = 2'b11 ,
//HRSP Parameters
parameter OKAY = 2'b00 ,
parameter ERROR = 2'b01 ,
parameter SPLIT = 2'b10 ,
parameter RETRY = 2'b11 ,
//bridge_state Parameters
parameter BRIDGE_IDLE = 2'b00,
parameter BRIDGE_SETUP = 2'b01,
parameter BRIDGE_ENABLE = 2'b10,
//ADDR Parameters
parameter ADDR_GPIO_0 = 32'h0000_0000,
parameter ADDR_GPIO_1 = 32'h0000_8000
) (
//------------ AHB ------------
//input
input iHCLK,
input iHRESETn,
input iHSEL,
input [1:0] iHTRANS,
input [3:0] iHSIZE ,
input [2:0] iHBURST,
input iHWRITE,
input [31:0] iHWDATA,
input [31:0] iHADDR ,
//output
output oHREADY,
output [1:0] oHRESP ,
output [31:0] oHRDATA,
//------------ APB ------------
//input
input [31:0] iPRDATA,
//output
output oPSEL0,
output oPSEL1,
output oPWRITE,
output oPENABLE,
output [31:0] oPADDR,
output [31:0] oPWDATA
);
/*————————————————————————————————————————————————————————————————————————*\
/ AHB Signal Register \
\*————————————————————————————————————————————————————————————————————————*/
reg iHWRITE_r ;
reg [31:0] iHADDR_r ;
reg [31:0] iHWDATA_r ;
always@( posedge iHCLK) begin
if(!iHRESETn) begin
iHWRITE_r <= 1'b0;
iHADDR_r <= 32'b0;
iHWDATA_r <= 32'b0;
end
else if( iHSEL && (bridge_state == BRIDGE_IDLE || bridge_state == BRIDGE_ENABLE))begin
iHWRITE_r <= iHWRITE; // ahb reg change when bridge_state is going to change:
iHADDR_r <= iHADDR ; // from IDLE to SETUP
iHWDATA_r <= iHWDATA; // from ENABLE to SETUP
end
end
/*————————————————————————————————————————————————————————————————————————*\
/ Bridge FSM \
\*————————————————————————————————————————————————————————————————————————*/
reg [1:0] bridge_state;
always @(posedge iHCLK ) begin
if (!iHRESETn) begin
bridge_state <= BRIDGE_IDLE;
end else begin
case ( bridge_state )
// IDLE
BRIDGE_IDLE: begin
if(iHSEL) begin
bridge_state <= BRIDGE_SETUP;
end
else begin
bridge_state <= BRIDGE_IDLE;
end
end
// SETUP
BRIDGE_SETUP: begin
bridge_state <= BRIDGE_ENABLE;
end
// ENABLE
BRIDGE_ENABLE: begin
if(iHSEL) begin
bridge_state <= BRIDGE_SETUP;
end
else begin
bridge_state <= BRIDGE_IDLE;
end
end
//DEFAULT
default: bridge_state <= BRIDGE_IDLE;
endcase
end
end
/*————————————————————————————————————————————————————————————————————————*\
/ AHB Slave Output \
\*————————————————————————————————————————————————————————————————————————*/
assign oHREADY = ( bridge_state == BRIDGE_SETUP ) ? 1'b0 : 1'b1;
assign oHRESP = OKAY ;
assign oHRDATA = iPRDATA;
/*————————————————————————————————————————————————————————————————————————*\
/ APB Master Output \
\*————————————————————————————————————————————————————————————————————————*/
assign oPSEL0 = ( iHADDR_r == ADDR_GPIO_0) ? 1'b1 : 1'b0;
assign oPSEL1 = ( iHADDR_r == ADDR_GPIO_1) ? 1'b1 : 1'b0;
assign oPWRITE = iHWRITE_r ;
assign oPENABLE = ( bridge_state == BRIDGE_ENABLE ) ? 1'b1 : 1'b0;
assign oPADDR = iHADDR_r;
assign oPWDATA = iHWDATA_r;
endmodule
3. GPIO模塊設計
GPIO(General Purpose I/O),是一種用於對器件的引腳做觀測或控制的外設,
在STM32、ZYNQ等開發中經常被使用到,相信大家並不陌生,
而在本項目中,我們將嘗試自己用Verilog語言,設計一個具有基本功能的GPIO外設,
該模塊具有一個APB Slave介面,
GPIO模塊會被掛在APB匯流排上,實現流水燈控制系統和開發板的外設(LED燈&按鍵)的交互,
模塊的信號定義如下表:
其中oGPIOout[3]連接開發板上LED4引腳,oGPIOout[2]連接LED3引腳,oGPIOout[1]連接LED2引腳,oGPIOout[0]連接LED1引腳,
iGPIOin[3:0]也是按照該順序,依次連接開發板上的4個按鍵:KEY4,KEY3,KEY2,KEY1。
GPIO模塊的功能實現,主要依靠四個32位寄存器:1.DATA_RO寄存器 2.DATA寄存器 3. DIRM寄存器 4. OEN寄存器
主機通過讀配置寫這些寄存器,就可以實現對外設的操作,
下麵分析這四個寄存器:
DATA_RO:
用來觀測GPIO引腳狀態,若引腳被配置成輸出模式,則該寄存器會反映驅動該引腳的電平的狀態。
DATA_RO是一個只讀寄存器,對該寄存器的寫操作是無效的
DATA:
當GPIO某一引腳被配置為輸出模式時,用來控制該引腳的輸出狀態
DIRM:
用來配置GPIO各個引腳的方向(做輸入or做輸出),
當DIRMP[x]==0,第x位引腳為輸入引腳,其輸出功能被disable
OEN:
當GPIO某一引腳被配置為輸出模式時,用來使能該引腳的輸出功能,
當OEN[x]==0時,第x位引腳的輸出功能被disable
為了避免在設計中引入雙向埠,我們為GPIO模塊賦予了一個32位輸入埠iGPIOin[31:0],一個32位輸出埠oGPIOout[31:0],
- 當reg_DIRM[i]=0,
對應i號引腳被DIRM寄存器配置為輸入埠,
oGPIOout[i]呈現高阻態1'bz,該位對應的引腳輸出功能實際上是不存在的,
iGPIOin[i]將寫入reg_DATA_RO[i],此時APB匯流排可以通過PRDATA[i]瞭解該引腳的輸入狀態:
reg [31:0] oGPIOout ;
always @(*) begin
for ( i=0 ; i<32 ; i=i+1 ) begin
if( reg_DIRM[i] & reg_OEN[i] ) begin //output mode
oGPIOout[i] = reg_DATA[i] ;
end else begin
oGPIOout[i] = 1'bz;
end
end
end
- 反之,當reg_DIRM[i]=1,
該引腳被DIRM寄存器配置為輸出埠,
oGPIOout[i]的值取決於reg_DATA[i]的情況,
iGPIOin[i]該位對應的引腳輸入功能實際上是不存在的,寫入reg_DATA_RO[i]的將是oGPIOout[i](即反映驅動該引腳的電平的狀態):
for ( i=0 ; i<32 ; i=i+1 ) begin
if ( reg_DIRM[i] ) begin
reg_DATA_RO[i] <= oGPIOout[i] ;// output mode
end else begin
reg_DATA_RO[i] <= iGPIOin[i] ;// input mode
end
end
GPIO的輸出邏輯如下圖所示,
只有DIRM寄存器被配置為輸出模式且OEN寄存器被配置為輸出使能狀態,oGPIOout才會反應DATA寄存器的狀態,否則為高阻態:
輸入引腳的邏輯則如圖中藍色部分所示,DIRM寄存器被配置為輸入模式時將寫入只讀寄存器DATA_RO,
否則DATA_RO取決於當前輸出模式的引腳的輸出狀態
GPIO模塊的完整RTL如下:
module APB_GPIO #(
//ADDR Parameters
parameter ADDR_GPIO = 32'h0000_0000,
parameter OFFSET_GPIO_DATA_RO = 4'h0,
parameter OFFSET_GPIO_DATA = 4'h4,
parameter OFFSET_GPIO_DIRM = 4'h8,
parameter OFFSET_GPIO_OEN = 4'hC
) (
// APB Signal
input iPCLK ,
input iPRESETn,
input iPSEL ,
input iPWRITE ,
input iPENABLE,
input [31:0] iPADDR ,
input [31:0] iPWDATA ,
output [31:0] oPRDATA ,
// I/O Signal
input [31:0] iGPIOin,
output [31:0] oGPIOout
);
/*————————————————————————————————————————————————————————————————————————*\
/ APB Signal Register \
\*————————————————————————————————————————————————————————————————————————*/
reg iPSELx_r ;
reg iPWRITE_r ;
reg [31:0] iPADDR_r ;
reg [31:0] iPWDATA_r ;
always@( posedge iPCLK) begin
if(!iPRESETn) begin
iPWRITE_r <= 1'b0;
iPADDR_r <= 16'b0;
end
else begin
iPWRITE_r <= iPWRITE;
iPWDATA_r <= iPWDATA;
iPADDR_r <= iPADDR;
end
end
/*————————————————————————————————————————————————————————————————————————*\
/ GPIO Register Declaration \
\*————————————————————————————————————————————————————————————————————————*/
// Read Only Data
reg [31:0] reg_DATA_RO;
// GPIO Data
reg [31:0] reg_DATA;
// Direction (in or out)
reg [31:0] reg_DIRM;
// Output Enable
reg [31:0] reg_OEN;
/*————————————————————————————————————————————————————————————————————————*\
/ Register Configuration \
\*————————————————————————————————————————————————————————————————————————*/
integer i;
//reg [31:0] GPIOin_r;
always @(posedge iPCLK ) begin
if( !iPRESETn ) begin
reg_DATA_RO <= 32'b0;
reg_DATA <= 32'b0;
reg_DIRM <= 32'b0;
reg_OEN <= 32'b0;
end
else begin
// reg_DATA, reg_DIRM, reg_OEN
if( iPENABLE && iPWRITE) begin
case ( iPADDR[3:0] )
OFFSET_GPIO_DATA_RO: begin end //DATA_RO is read only register
OFFSET_GPIO_DATA: begin
reg_DATA <= iPWDATA;
end
OFFSET_GPIO_DIRM: begin
reg_DIRM <= iPWDATA;
end
OFFSET_GPIO_OEN: begin
reg_OEN <= iPWDATA;
end
default: begin
reg_DATA <= reg_DATA ;
reg_DIRM <= reg_DIRM ;
reg_OEN <= reg_OEN ;
end
endcase
end
// DATA_RO
for ( i=0 ; i<32 ; i=i+1 ) begin
if ( reg_DIRM[i] ) begin
reg_DATA_RO[i] <= oGPIOout[i] ;// output mode
end else begin
reg_DATA_RO[i] <= iGPIOin[i] ;// input mode
end
end
end
end
/*————————————————————————————————————————————————————————————————————————*\
/ I/O \
\*————————————————————————————————————————————————————————————————————————*/
// iGPIOin -> GPIOin_r -> DATA_RO -> PRADATA
assign oPRDATA = reg_DATA_RO;
// reg_DATA -> GPIOout
reg [31:0] oGPIOout ;
always @(*) begin
for ( i=0 ; i<32 ; i=i+1 ) begin
if( reg_DIRM[i] & reg_OEN[i] ) begin //output mode
oGPIOout[i] = reg_DATA[i] ;
end else begin
oGPIOout[i] = 1'bz;
end
end
end
endmodule
4. Control Unit(流水燈控制單元)模塊設計
4.1 有限狀態機設計
我們的AMBA流水燈控制系統的核心:Control Unit,即控制單元,
Bridge模塊是APB上所有外設的Master,而CU模塊則是Bridge在AHB上的Master,
可以說是Master中的Master了,
Control Unit是信號傳輸的發起者,決定匯流排上是在讀還是在寫、也決定訪問的從機是哪個,
CU發出的控制信號包括HTRANS、HBURST、HSIZE、HADDR,數據信號包括HWDATA,
這些AHB信號如何生成均是由Control Unit負責的,
同時CU也需要接收HRDATA,並對其進行解讀判斷,
那麼這些AHB信號是如何生成的呢?
實際上,Control Unit模塊也是通過一個有限狀態機實現的:
下麵介紹一下該有限狀態機各個狀態的含義:
CONFIG_0~2:按下複位按鈕/上電後,對GPIO外設進行初始化配置,
其中,
CONFIG_0:
將GPIO的0~3位引腳配置為輸入模式,連接FPGA開發板的Key按鍵進行觀測
將GPIO的4~7位引腳配置為輸出模式,連接FPGA開發板的LED燈進行驅動控制
CONFIG_1:
將GPIO的4~7位引腳的輸出電平均設定為高電平1(本開發板上的LED為共陽極,引腳高電平=LED燈熄滅)
CONFIG_2:
對GPIO的4~7位引腳進行輸出使能
READ_DATA_RO:
讀GPIO的DATA_RO寄存器,根據寄存器[3:0]的值得知Key狀態,
該狀態是FSM的核心狀態,
當配置完GPIO的DIRM、OEN、DATA寄存器後,
我們會開始反覆進行讀DATA_RO,即觀測KEY的狀態,
然後根據KEY的值得到對應的LED_mode,
隨後會由該狀態跳向WRITE_DATA_0~3,對DATA寄存器進行配置,控制LED燈點亮or熄滅
WRITE_DATA_0~3:
配置不同的流水燈燈工作模式,模式由KEY狀態決定,
按下KEY1進入工作模式0,按下KEY2進入工作模式1,按下KEY3進入工作模式2,按下KEY4進入工作模式3,
在初始靜止狀態下,只有同時按下KEY0~3才能進入工作模式0,
此時按下某個單獨的KEY不會有任何反應的
隨著FSM狀態的改變,CU模塊將會給AHB匯流排發出不同的讀寫命令,從而實現對GPIO模塊的寄存器配置,
下表具體地整理了FSM狀態和CU模塊生成的AHB信號之間的對應關係:
上述FSM狀態轉換邏輯所對應的RTL:
reg [2:0] CU_state;
always @(posedge iHCLK ) begin
if ( !iHRESETn ) begin
CU_state <= CONFIG_0;
end else begin
case (CU_state)
// write GPIO_DIRM
CONFIG_0: begin
if ( iHREADY ) begin
CU_state <= CONFIG_1;
end else begin
CU_state <= CONFIG_0;
end
end
// write GPIO_DATA
CONFIG_1: begin
if ( iHREADY ) begin
CU_state <= CONFIG_2;
end else begin
CU_state <= CONFIG_1;
end
end
// write GPIO_OEN
CONFIG_2: begin
if ( iHREADY && iHRDATA[3:0] == 4'b0000) begin
CU_state <= READ_DATA_RO;
end else begin
CU_state <= CONFIG_2;
end
end
// read DATA_RO
READ_DATA_RO: begin
if ( iHREADY && iHRDATA[3:0] == 4'b1110) begin //key1 pressed
CU_state <= WRITE_DATA_0;
end else if ( iHREADY && iHRDATA[3:0] == 4'b1101) begin //key2 pressed
CU_state <= WRITE_DATA_1;
end else if ( iHREADY && iHRDATA[3:0] == 4'b1011) begin //key3 pressed
CU_state <= WRITE_DATA_2;
end else if ( iHREADY && iHRDATA[3:0] == 4'b0111) begin //key4 pressed
CU_state <= WRITE_DATA_3;
end else if ( iHREADY && led_mode[0] ) begin //keep mode0 if not pressed this moment
CU_state <= WRITE_DATA_0;
end else if ( iHREADY && led_mode[1] ) begin //keep mode1
CU_state <= WRITE_DATA_0;
end else if ( iHREADY && led_mode[2] ) begin //keep mode2
CU_state <= WRITE_DATA_1;
end else if ( iHREADY && led_mode[3] ) begin //keep mode3
CU_state <= WRITE_DATA_2;
end else begin
CU_state <= READ_DATA_RO; // Slave not ready || no key ever been pressed
end
end
// write DATA
WRITE_DATA_0 : begin
if ( iHREADY ) begin
CU_state <= READ_DATA_RO;
end else begin
CU_state <= CU_state;
end
end
// write DATA
WRITE_DATA_1 : begin
if ( iHREADY ) begin
CU_state <= READ_DATA_RO;
end else begin
CU_state <= CU_state;
end
end
// write DATA
WRITE_DATA_2 : begin
if ( iHREADY ) begin
CU_state <= READ_DATA_RO;
end else begin
CU_state <= CU_state;
end
end
WRITE_DATA_3 : begin
if ( iHREADY ) begin
CU_state <= READ_DATA_RO;
end else begin
CU_state <= CU_state;
end
end
default: CU_state <= CONFIG_0;
endcase
end
end
LED燈的亮滅改變,是通過配置GPIO內的寄存器實現的,
具體來說,是藉助匯流排信號HWDATA對GPIO的reg_DATA[7:4]寄存器進行配置,
在緒論章節中,我們介紹過LED_mode的編碼方式:
// LED_mode Parameters
parameter MODE0 = 4'b0001,
parameter MODE1 = 4'b0010,
parameter MODE2 = 4'b0100,
parameter MODE3 = 4'b1000,
LED_mode採用了One-hot編碼,這是為了對HWDATA進行assign賦值的時候,判斷條件寫起來簡便一些,
現在我們就可以設計出各種流水燈模式下的組合邏輯了,
各模式下的LED亮滅隨時間改變是通過計時器輔助的,
也就是下麵代碼中的timer,timer是一個32位寄存器:
reg [31:0] timer ;
always @ (posedge iHCLK or negedge iHRESETn) begin
if ( !iHRESETn )
timer <= 32'd0; // when the reset signal valid,time counter clearing
else if (timer == 32'd199_999_999) // 4 seconds count(50M*4-1=199999999)
timer <= 32'd0; // count done,clearing the time counter
else
timer <= timer + 1'b1; // timer counter = timer counter + 1
end
本質是將每一秒分成了50M幀(系統工作時鐘頻率為50MHz),
我們用assign語句給不同的幀下的HWDATA賦不同的值就行了,
首先是普通流水燈模式(工作模式0):
(註意,下麵的LED[3:0]在WRITE_DATA_0~3狀態下將會作為HWDATA[7:4]對GPIO_DATA寄存器進行配置)
assign LED[3:0] =
// mode0 普通流水燈模式
( led_mode[0] && timer >= 32'd149_999_999 ) ? 4'b0111 : // LED4亮
( led_mode[0] && timer >= 32'd99_999_999 ) ? 4'b1011 : // LED3亮
( led_mode[0] && timer >= 32'd49_999_999 ) ? 4'b1101 : // LED2亮
( led_mode[0] ) ? 4'b1110 : // LED1亮
....(其他模式)
這裡就體現出One-hot編碼的好處了,可以用( led_mode[0] )作為是否處於模式0的判定,
否則需要寫成( led_mode == MODE_0 ),
接下來是加速流水燈模式(工作模式1):
assign LED[3:0] =
// mode1 加速流水燈模式
( led_mode[1] && timer >= 32'd174_999_999 ) ? 4'b0111 : // LED4亮
( led_mode[1] && timer >= 32'd149_999_999 ) ? 4'b1011 : // LED3亮
( led_mode[1] && timer >= 32'd124_999_999 ) ? 4'b1101 : // LED2亮
( led_mode[1] && timer >= 32'd99_999_999 ) ? 4'b1110 : // LED1亮
( led_mode[1] && timer >= 32'd74_999_999 ) ? 4'b0111 : // LED4亮
( led_mode[1] && timer >= 32'd49_999_999 ) ? 4'b1011 : // LED3亮
( led_mode[1] && timer >= 32'd24_999_999 ) ? 4'b1101 : // LED2亮
( led_mode[1] ) ? 4'b1110 : // LED1亮
....(其他模式)
心跳模式(工作模式2):
assign LED[3:0] =
// mode2 心跳模式
( led_mode[2] && timer >= 32'd189_999_999 ) ? 4'b0000 : // 全亮
( led_mode[2] && timer >= 32'd179_999_999 ) ? 4'b1111 : // 全滅
( led_mode[2] && timer >= 32'd169_999_999 ) ? 4'b0000 : // 全亮
( led_mode[2] ) ? 4'b1111 : // 全滅
....(其他模式)
最後是呼吸燈模式(工作模式3),
該模式相對複雜一些,前面提到過是通過改變占空比控制LED燈亮度的,
占空比的改變是通過對timer的判斷實現的,
舉個例子:
assign LED[3:0] =
// 占空比 1/4 = 25%
( led_mode[3] && (timer >= 32'd89_999_999 ) && (timer[1:0] == 2'b00 ) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd89_999_999 ) ) ? 4'b1111 :
....(其他模式)
(timer[1:0] == 2'b00 )的判定,在timer的值每增加4的過程中,必定出現且只出現1次,
這樣在 (timer >= 32'd89_999_999 )的這段期間內,
LED燈的驅動引腳就會在25%的時間里處於低電平(點亮),剩下的75%時間里處於高電平(熄滅),
同理,(timer[2:0] == 3'b0 )就能得到1/8的占空比,也就是12.5%,
這樣就有:
assign LED[3:0] =
// mode3 呼吸燈模式
// 占空比 0%
( led_mode[3] && (timer >= 32'd189_999_999) ) ? 4'b1111 :
// 占空比 1/64 = 1.56%
( led_mode[3] && (timer >= 32'd169_999_999) && (timer[5:0] == 6'b000) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd169_999_999) ) ? 4'b1111 :
// 占空比 1/32 = 3.12%
( led_mode[3] && (timer >= 32'd149_999_999) && (timer[4:0] == 5'b000) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd149_999_999) ) ? 4'b1111 :
// 占空比 1/16 = 6.25%
( led_mode[3] && (timer >= 32'd129_999_999) && (timer[3:0] == 4'b000) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd129_999_999) ) ? 4'b1111 :
// 占空比 1/8 = 12.5%
( led_mode[3] && (timer >= 32'd109_999_999) && (timer[2:0] == 3'b000) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd109_999_999) ) ? 4'b1111 :
// 占空比 1/4 = 25%
( led_mode[3] && (timer >= 32'd89_999_999 ) && (timer[1:0] == 2'b00 ) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd89_999_999 ) ) ? 4'b1111 :
// 12.5%
( led_mode[3] && (timer >= 32'd69_999_999 ) && (timer[2:0] == 3'b000) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd69_999_999 ) ) ? 4'b1111 :
// 6.25%
( led_mode[3] && (timer >= 32'd49_999_999 ) && (timer[3:0] == 4'b000) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd49_999_999 ) ) ? 4'b1111 :
// 3.12%
( led_mode[3] && (timer >= 32'd29_999_999 ) && (timer[4:0] == 5'b000) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd29_999_999 ) ) ? 4'b1111 :
// 1.56%
( led_mode[3] && (timer >= 32'd9_999_999 ) && (timer[5:0] == 6'b000) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd9_999_999 ) ) ? 4'b1111 :
// 0%
( led_mode[3] ) ? 4'b1111 :
....(其他模式)
可以看出上述代碼實現了占空比從0% → 1.56% → 3.12% → 6.25% → 12.5% → 25% → 12.5% → 6.25% → 3.12% → 1.56% → 0%的等時間間隔變化,
由於開發板LED燈功率較高,在25%到100%之間的占空比,LED燈亮度都會很高,看不出太大變化,
因此我們將25%設定為了最高的占空比,對應LED燈最亮的時刻。
Control Unit模塊的完整RTL如下:
/*————————————————————————————————————————————————————————————————————————*\
/ Control Unit FSM \
\*————————————————————————————————————————————————————————————————————————*/
reg [2:0] CU_state;
always @(posedge iHCLK ) begin
if ( !iHRESETn ) begin
CU_state <= CONFIG_0;
end else begin
case (CU_state)
// write GPIO_DIRM
CONFIG_0: begin
if ( iHREADY ) begin
CU_state <= CONFIG_1;
end else begin
CU_state <= CONFIG_0;
end
end
// write GPIO_DATA
CONFIG_1: begin
if ( iHREADY ) begin
CU_state <= CONFIG_2;
end else begin
CU_state <= CONFIG_1;
end
end
// write GPIO_OEN
CONFIG_2: begin
if ( iHREADY && iHRDATA[3:0] == 4'b0000) begin
CU_state <= READ_DATA_RO;
end else begin
CU_state <= CONFIG_2;
end
end
// read DATA_RO
READ_DATA_RO: begin
if ( iHREADY && iHRDATA[3:0] == 4'b1110) begin //key1 pressed
CU_state <= WRITE_DATA_0;
end else if ( iHREADY && iHRDATA[3:0] == 4'b1101) begin //key2 pressed
CU_state <= WRITE_DATA_1;
end else if ( iHREADY && iHRDATA[3:0] == 4'b1011) begin //key3 pressed
CU_state <= WRITE_DATA_2;
end else if ( iHREADY && iHRDATA[3:0] == 4'b0111) begin //key4 pressed
CU_state <= WRITE_DATA_3;
end else if ( iHREADY && led_mode[0] ) begin //keep mode0 if not pressed this moment
CU_state <= WRITE_DATA_0;
end else if ( iHREADY && led_mode[1] ) begin //keep mode1
CU_state <= WRITE_DATA_1;
end else if ( iHREADY && led_mode[2] ) begin //keep mode2
CU_state <= WRITE_DATA_2;
end else if ( iHREADY && led_mode[3] ) begin //keep mode3
CU_state <= WRITE_DATA_3;
end else begin
CU_state <= READ_DATA_RO; // Slave not ready || no key ever been pressed
end
end
// write DATA
WRITE_DATA_0 : begin
if ( iHREADY ) begin
CU_state <= READ_DATA_RO;
end else begin
CU_state <= CU_state;
end
end
// write DATA
WRITE_DATA_1 : begin
if ( iHREADY ) begin
CU_state <= READ_DATA_RO;
end else begin
CU_state <= CU_state;
end
end
// write DATA
WRITE_DATA_2 : begin
if ( iHREADY ) begin
CU_state <= READ_DATA_RO;
end else begin
CU_state <= CU_state;
end
end
WRITE_DATA_3 : begin
if ( iHREADY ) begin
CU_state <= READ_DATA_RO;
end else begin
CU_state <= CU_state;
end
end
default: CU_state <= CONFIG_0;
endcase
end
end
/*————————————————————————————————————————————————————————————————————————*\
/ AHB Master Output \
\*————————————————————————————————————————————————————————————————————————*/
assign oHTRANS = NONSEQ;
assign oHBURST = SINGLE;
assign oHSIZE = DWORD;
assign oHADDR = ( CU_state == CONFIG_0 )? 32'h0000_0008 :
( CU_state == CONFIG_1 )? 32'h0000_000C :
( CU_state == CONFIG_2 )? 32'h0000_0004 :
( CU_state == READ_DATA_RO)? 32'h0000_0000 :
( CU_state == WRITE_DATA_0)? 32'h0000_0004 :
( CU_state == WRITE_DATA_1)? 32'h0000_0004 :
( CU_state == WRITE_DATA_2)? 32'h0000_0004 :
( CU_state == WRITE_DATA_3)? 32'h0000_0004 :
32'hz;
assign oHWRITE = ( CU_state == READ_DATA_RO )? 1'b0 : 1'b1;
assign oHWDATA = ( CU_state == CONFIG_0 )? 32'h0000_00F0 :
( CU_state == CONFIG_1 )? 32'h0000_00F0 :
( CU_state == CONFIG_2 )? 32'h0000_00F0 : // all LED turned OFF
( CU_state == WRITE_DATA_0 || CU_state == WRITE_DATA_1 ||
CU_state == WRITE_DATA_2 || CU_state == WRITE_DATA_3 ) ? {16'b0,LED[3:0],4'b0 } : 32'hz;
/*————————————————————————————————————————————————————————————————————————*\
/ LED Control \
\*————————————————————————————————————————————————————————————————————————*/
reg [3 :0] led_mode; //4 modes in all (one hot encoding)
reg [31:0] timer ;
wire [3 :0] LED ;
//------------ led_mode ------------
always @(posedge iHCLK ) begin
if ( !iHRESETn ) begin
led_mode <= 4'b0000;
end else begin
case (CU_state)
CONFIG_0: begin
led_mode <= 4'b0000;
end
CONFIG_1: begin
led_mode <= 4'b0000;
end
CONFIG_2: begin
if ( iHREADY && iHRDATA[3:0] == 4'b0000) begin
led_mode <= MODE0;
end else begin
led_mode <= 4'b0000;
end
end
READ_DATA_RO: begin
// key1 pressed mode -> MODE0
if( iHREADY && iHRDATA[3:0] == 4'b1110) begin
led_mode <= MODE0;
// key2 pressed mode -> MODE1
end else if( iHREADY && iHRDATA[3:0] == 4'b1101) begin
led_mode <= MODE1;
// key3 pressed mode -> MODE2
end else if( iHREADY && iHRDATA[3:0] == 4'b1011) begin
led_mode <= MODE2;
// key4 pressed mode -> MODE3
end else if( iHREADY && iHRDATA[3:0] == 4'b0111) begin
led_mode <= MODE3;
// no key pressed, mode kept
end else begin
led_mode <= led_mode;
end
end
WRITE_DATA_0 : begin
led_mode <= led_mode;
end
WRITE_DATA_1 : begin
led_mode <= led_mode;
end
WRITE_DATA_2 : begin
led_mode <= led_mode;
end
WRITE_DATA_3 : begin
led_mode <= led_mode;
end
default: begin
led_mode <= led_mode;
end
endcase
end
end
//------------ timer ------------
always @ (posedge iHCLK or negedge iHRESETn) begin
if ( !iHRESETn )
timer <= 32'd0; // when the reset signal valid,time counter clearing
else if (timer == 32'd199_999_999) // 4 seconds count(50M*4-1=199999999)
timer <= 32'd0; // count done,clearing the time counter
else
timer <= timer + 1'b1; // timer counter = timer counter + 1
end
//------------ led ------------
assign LED[3:0] =
// mode0 普通流水燈模式
( led_mode[0] && timer >= 32'd149_999_999 ) ? 4'b0111 : // LED4亮
( led_mode[0] && timer >= 32'd99_999_999 ) ? 4'b1011 : // LED3亮
( led_mode[0] && timer >= 32'd49_999_999 ) ? 4'b1101 : // LED2亮
( led_mode[0] ) ? 4'b1110 : // LED1亮
// mode1 加速流水燈模式
( led_mode[1] && timer >= 32'd174_999_999 ) ? 4'b0111 : // LED4亮
( led_mode[1] && timer >= 32'd149_999_999 ) ? 4'b1011 : // LED3亮
( led_mode[1] && timer >= 32'd124_999_999 ) ? 4'b1101 : // LED2亮
( led_mode[1] && timer >= 32'd99_999_999 ) ? 4'b1110 : // LED1亮
( led_mode[1] && timer >= 32'd74_999_999 ) ? 4'b0111 : // LED4亮
( led_mode[1] && timer >= 32'd49_999_999 ) ? 4'b1011 : // LED3亮
( led_mode[1] && timer >= 32'd24_999_999 ) ? 4'b1101 : // LED2亮
( led_mode[1] ) ? 4'b1110 : // LED1亮
// mode2 心跳模式
( led_mode[2] && timer >= 32'd189_999_999 ) ? 4'b0000 : // 全亮
( led_mode[2] && timer >= 32'd179_999_999 ) ? 4'b1111 : // 全滅
( led_mode[2] && timer >= 32'd169_999_999 ) ? 4'b0000 : // 全亮
( led_mode[2] ) ? 4'b1111 : // 全滅
// mode3 呼吸燈模式
// 占空比 0%
( led_mode[3] && (timer >= 32'd189_999_999) ) ? 4'b1111 :
// 占空比 1/64 = 1.56%
( led_mode[3] && (timer >= 32'd169_999_999) && (timer[5:0] == 6'b000) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd169_999_999) ) ? 4'b1111 :
// 占空比 1/32 = 3.12%
( led_mode[3] && (timer >= 32'd149_999_999) && (timer[4:0] == 5'b000) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd149_999_999) ) ? 4'b1111 :
// 占空比 1/16 = 6.25%
( led_mode[3] && (timer >= 32'd129_999_999) && (timer[3:0] == 4'b000) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd129_999_999) ) ? 4'b1111 :
// 占空比 1/8 = 12.5%
( led_mode[3] && (timer >= 32'd109_999_999) && (timer[2:0] == 3'b000) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd109_999_999) ) ? 4'b1111 :
// 占空比 1/4 = 25%
( led_mode[3] && (timer >= 32'd89_999_999 ) && (timer[1:0] == 2'b00 ) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd89_999_999 ) ) ? 4'b1111 :
// 12.5%
( led_mode[3] && (timer >= 32'd69_999_999 ) && (timer[2:0] == 3'b000) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd69_999_999 ) ) ? 4'b1111 :
// 6.25%
( led_mode[3] && (timer >= 32'd49_999_999 ) && (timer[3:0] == 4'b000) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd49_999_999 ) ) ? 4'b1111 :
// 3.12%
( led_mode[3] && (timer >= 32'd29_999_999 ) && (timer[4:0] == 5'b000) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd29_999_999 ) ) ? 4'b1111 :
// 1.56%
( led_mode[3] && (timer >= 32'd9_999_999 ) && (timer[5:0] == 6'b000) ) ? 4'b0000 :
( led_mode[3] && (timer >= 32'd9_999_999 ) ) ? 4'b1111 :
// 0%
( led_mode[3] ) ? 4'b1111 : 4'b1111 ;
endmodule
至此,Control Unit的設計就介紹完了,下麵我們將演示系統在FPGA開發板上的實際運行視頻
5. FPGA驗證
本項目所用開發板型號:XC7A35T-2FGG484I
上板驗證視頻:https://www.bilibili.com/video/BV1EM4y1U7QP/?vd_source=b96e62cf611b2bbbbacda9f1c4d9a394