绪论

本文将介绍一个完全用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总线特性包括:

  1. 两个时钟周期传输(非流水线,不同于AHB)

  2. 无需等待周期和回应信号

  3. 控制逻辑简单

  4. 只有四个控制信号

下图展示了APB总线上的一次标准的数据传输时序:

其他有关APB总线的介绍,将在下文AHB2APB Bridge模块的设计章节中涉及

1. 系统架构与功能

系统架构:

本项目的系统架构框图上图所示,

其中,蓝色部分为AHB总线以及AHB上的主机从机,粉色部分则是APB总线以及APB上的主机从机,

下面介绍一下总线上各个模块的功能和定义:

  1. 流水灯控制单元(Contro Unit)
    在AHB总线上,流水灯控制单元作为Master,
    控制单元发出的AHB总线信号将经过AHB2APB Bridge,成为APB信号,
    APB信号会对GPIO模块的寄存器进行配置,配置后的GPIO就可以对开发板上的按键状态进行观测,并对LED灯进行控制

  2. AHB转APB桥(AHB2APB Bridge)
    AHB2APB Bridge作为逻辑连接器,一方面是在AHB的Slave,另一方面也是APB的Master(APB总线有且只有这唯一1个Master)。
    弄清这个概念,就可以定义模块的输入输出的端口,以及需要满足的时序。
    由于AHB总线具有流水线特性,而APB没有,这就需要让AHB在合适的时候拉低HREADYout添加一个等待周期

  3. 通用输入输出模块(General-purpose Input/Output)
    GPIO模块作为APB总线上的Slave,具有一个APB Slave接口,
    除此之外,也会直接连接到FPGA开发板的按键外设和LED灯外设:
    GPIO模块的输入为Key[3:0],用于观测开发板上的按键状态,
    GPIO模块的输出为LED[3:0],用于控制开发板上LED灯的亮灭。
    由于本项目用到的FPGA开发板的LED外设为共阳极,因此当引脚输出低电平时,对应LED将被点亮;反之,LED将会熄灭。

项目中涉及到的外设在开发板上的位置如下图所示:

流水灯系统工作模式:

  1. 初始静止模式:所有LED灯熄灭
    上电后/按下reset后,进入该状态,所有LED灯熄灭,
    此时LED_mode = 4'b0000(LED_mode为Control Unit中流水灯工作模式寄存器,用以控制LED灯闪烁的逻辑)

  2. 工作模式0:普通流水灯模式
    初始静止模式同时按下KEY0~KEY3进入工作模式0,
    或在工作模式0~3下,按下FPGA开发板上按键KEY1进入工作模式0,
    此时LED_mode = 4'b0001,
    在工作模式0下,开发板上的LED灯将按照下图所示的方式,做有规律的周期性流动:

    LED0~LED4将依次闪烁,每个灯闪烁时长为1s,循环周期为4s

  3. 工作模式1:加速流水灯模式
    在工作模式0~3下,按下FPGA开发板上按键KEY2,进入该工作状态,
    此时LED_mode = 4'b0010,
    在工作模式1下,LED灯将加速流动,每个灯闪烁时长为0.5s,循环周期为2s:
    !

  4. 工作模式2:心跳模式
    在工作模式0~3下,按下FPGA开发板上按键KEY3,进入该工作状态,
    此时LED_mode = 4'b0100,
    该模式下LED等将模仿心跳的节奏进行闪烁(详见最后一章中FPGA运行视频)

  5. 工作模式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这个周期分别称为IDLESETUPENABLE

于是我们得到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],

  1. 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
  1. 反之,当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位引脚的输出电平均设定为低电平0(本开发板上的LED为共阳极)

CONFIG_2
对GPIO的4~7位引脚进行输出使能,点亮所有LED灯

READ_GPIO_DATA_RO
读GPIO的DATA_RO寄存器,根据寄存器[3:0]的值得知Key状态,
该状态是核心状态,
当配置完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,

采用了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的设计就介绍完了,下面我们将演示系统在FPGA开发板上的实际运行视频

5. FPGA验证

本项目所用开发板型号:XC7A35T-2FGG484I

上板验证视频:

(视频制作中,录制完会第一时间更新视频链接)

内容来源于网络如有侵权请私信删除

文章来源: 博客园

原文链接: https://www.cnblogs.com/sjtu-zsj-990702/p/17251396.html

你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!