蓝蓝夜 發表於 2023-3-30 23:05:00

「FPGA项目」—— 基于AMBA总线的流水灯控制系统

<h1 id="绪论">绪论</h1>
<p>本文将介绍一个完全用Verilog HDL手写的AMBA片上系统,</p>
<p>项目的主题是设计一个<strong>基于AMBA总线的流水灯控制系统</strong>,</p>
<p>项目中所有数字逻辑电路部分都不会通过调用成熟IP核的方式来实现,而是通过Verilog进行RTL设计,</p>
<p>然后利用Vivado平台对RTL模型进行仿真、综合与布线,</p>
<p>最后在FPGA开发板上进行板级验证,</p>
<p>开发板上的演示运行视频链接:https://www.bilibili.com/video/BV1EM4y1U7QP/?spm_id_from=333.999.top_right_bar_window_default_collection.content.click&amp;vd_source=b96e62cf611b2bbbbacda9f1c4d9a394</p>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202303/3146671-20230329235503648-207476459.png"></p>
<p>AMBA是ARM公司推出的一种总线架构,目前已非常成熟,在行业内得到广泛的应用,极具实际应用价值,</p>
<p>本项目涉及了AMBA架构中的<strong>AHB协议</strong>&<strong>APB协议</strong>,</p>
<p>系统包括AHB总线、APB总线两个部分,</p>
<p>整个系统的基本架构如下图所示:</p>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202303/3146671-20230330194216762-1395498640.png"></p>
<p>总的来说,由AHB总线上的主机————流水灯控制单元(Control Unit),发出控制信号,</p>
<p>进而对APB总线上的从机GPIO模块进行配置,实现对流水灯的流动方式的控制,</p>
<p>本项目是一个完整的AMBA总线系统,包括总线、主机、从机以及外设,</p>
<p>在各章节中,将会介绍如何利用Verilog语言对以下模块进行RTL设计:</p>
<p><strong>1. Control Unit模块</strong></p>
<p><strong>2. AHB2APB Bridge模块</strong></p>
<p><strong>3. GPIO模块</strong></p>
<p>希望本项目能给对AMBA总线架构感兴趣的朋友们一些启发与帮助。</p>
<p>下面,先简要介绍一下AHB/APB总线的基本特性和数据传输时序:</p>
<h2 id="ahb总线协议简介">AHB总线协议简介:</h2>
<p>AHB总线,全称为Advanced High performance Bus,属于AMBA2.0规范中的一部分,</p>
<p>在SoC片上系统中,AHB主要用于一些高性能模块之间的连接(如CPU、DMA、DSP等),</p>
<p>主要的特性包括:单时钟边沿操作、支持突发传输、支持多个主控制器、可配置总线宽度(32位~128位)、支持字节、半字、字传输。</p>
<p>一个典型的AHB总线系统通常以下图所示的方式进行多主机多从机互联:</p>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202303/3146671-20230327233120273-1753531789.png"></p>
<p>可以看出,Master(主机)这边的地址信号HADDR和数据信号HWDATA都是广播的,通过仲裁器arbiter给出的hselx来选择对应的从机,</p>
<p>而Slave(从机)这边的数据,则是通过Decoder来解码给主机这边的。</p>
<p>当HSELx选中对应的从设备后,就可以开始传输了,一个无等待传输的时序如下所示:</p>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202303/3146671-20230327234234762-1540959937.png"></p>
<p>图中可以看出,AHB总线具有流水线特性,地址周期和数据周期交替进行</p>
<h2 id="apb总线协议简介">APB总线协议简介:</h2>
<p>APB,全称为Advanced Peripheral Bus,主要用于低带宽的周边外设之间的连接,例如UART、1284等,</p>
<p>APB总线架构不像AHB支持多个Master,在APB里面唯一的Master就是APB桥。</p>
<p>APB总线特性包括:</p>
<ol>
<li>
<p>两个时钟周期传输(非流水线,不同于AHB)</p>
</li>
<li>
<p>无需等待周期和回应信号</p>
</li>
<li>
<p>控制逻辑简单</p>
</li>
<li>
<p>只有四个控制信号</p>
</li>
</ol>
<p>下图展示了APB总线上的一次标准的数据传输时序:</p>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202303/3146671-20230327233708587-855873884.png"></p>
<p>其他有关APB总线的介绍,将在下文AHB2APB Bridge模块的设计章节中涉及</p>
<h1 id="1-系统架构与功能">1. 系统架构与功能</h1>
<h2 id="11-系统架构">1.1 系统架构</h2>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202303/3146671-20230330193943786-1211806085.png"></p>
<p>本项目的系统架构框图上图所示,</p>
<p>其中,蓝色部分为AHB总线以及AHB上的主机从机,粉色部分则是APB总线以及APB上的主机从机,</p>
<p>下面介绍一下总线上各个模块的功能和定义:</p>
<ol>
<li>
<p><strong>流水灯控制单元(Contro Unit)</strong><br>
在AHB总线上,流水灯控制单元作为Master,<br>
控制单元发出的AHB总线信号将经过AHB2APB Bridge,成为APB信号,<br>
APB信号会对GPIO模块的寄存器进行配置,配置后的GPIO就可以对开发板上的按键状态进行观测,并对LED灯进行控制</p>
</li>
<li>
<p><strong>AHB转APB桥(AHB2APB Bridge)</strong><br>
AHB2APB Bridge作为逻辑连接器,一方面是在AHB的Slave,另一方面也是APB的Master(APB总线有且只有这唯一1个Master)。<br>
弄清这个概念,就可以定义模块的输入输出的端口,以及需要满足的时序。<br>
由于AHB总线具有流水线特性,而APB没有,<br>
因此需要Bridge在合适的时候拉低HREADY,添加一个AHB等待周期</p>
</li>
<li>
<p><strong>通用输入输出模块(General-purpose Input/Output)</strong><br>
GPIO模块作为APB总线上的Slave,具有一个APB Slave接口,<br>
除此之外,也会直接连接到FPGA开发板的按键外设和LED灯外设:<br>
GPIO模块的输入端口Key,用于观测开发板上的按键状态,<br>
GPIO模块的输出端口LED,用于控制开发板上LED灯的亮灭。<br>
由于本项目用到的FPGA开发板的LED外设为共阳极,因此当引脚输出低电平时,对应LED将被点亮;反之,LED将会熄灭。</p>
</li>
</ol>
<p>项目中涉及到的外设在开发板上的位置如下图所示:</p>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202303/3146671-20230330002343366-727715627.png"></p>
<h2 id="12-流水灯系统工作模式">1.2 流水灯系统工作模式</h2>
<ol>
<li>
<p><strong>初始静止模式:所有LED灯熄灭</strong>,<br>
上电后/按下reset后,进入该状态,所有LED灯熄灭,<br>
此时LED_mode = 4'b0000(LED_mode为Control Unit中流水灯工作模式寄存器,用以控制LED灯闪烁的逻辑)</p>
</li>
<li>
<p><strong>工作模式0:普通流水灯模式</strong><br>
<strong>初始静止模式同时按下KEY0~KEY3</strong>进入工作模式0,<br>
或在工作模式0~3下,按下FPGA开发板上按键KEY1进入工作模式0,<br>
此时LED_mode = 4'b0001,<br>
在工作模式0下,开发板上的LED灯将按照下图所示的方式,做有规律的周期性流动:<br>
<img src="https://img2023.cnblogs.com/blog/3146671/202303/3146671-20230330201051870-2003586566.png"><br>
LED0~LED4将依次闪烁,每个灯闪烁时长为1s,循环周期为4s</p>
</li>
<li>
<p><strong>工作模式1:加速流水灯模式</strong><br>
在工作模式0~3下,按下FPGA开发板上按键KEY2,进入该工作状态,<br>
此时LED_mode = 4'b0010,<br>
在工作模式1下,LED灯将加速流动,每个灯闪烁时长为0.5s,循环周期为2s:<br>
!<img src="https://img2023.cnblogs.com/blog/3146671/202303/3146671-20230330200956980-608301459.png"></p>
</li>
<li>
<p><strong>工作模式2:心跳模式</strong><br>
在工作模式0~3下,按下FPGA开发板上按键KEY3,进入该工作状态,<br>
此时LED_mode = 4'b0100,<br>
该模式下LED等将模仿心跳的节奏进行闪烁(详见最后一章中FPGA运行视频)</p>
</li>
<li>
<p><strong>工作模式3:呼吸灯模式</strong><br>
在工作模式0~3下,按下FPGA开发板上按键KEY4,进入该工作状态,<br>
该模式下LED将周期性进行<strong>逐渐由亮到暗再到亮</strong>的变化(周期为4s,其中前2s由暗变量,后2由亮变暗),<br>
此时LED_mode = 4'b1000,<br>
由于FPGA开发板上的LED灯亮度无法直接配置,<br>
该模式是通过<strong>调节LED灯的占空比</strong>来实现的,占空比越高,小灯的平均功率越大,我们人眼看到的亮度也就越高</p>
</li>
</ol>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202303/3146671-20230330201843794-529277667.png"></p>
<h1 id="2--ahb2apb-bridge模块设计">2.AHB2APB Bridge模块设计</h1>
<p>首先介绍AHB2APB Bridge模块的设计,</p>
<p>该模块是AHB与APB总线之间的桥梁,负责两种协议信号的相互转换,</p>
<p>AHB2APB Bridge的模块框图和信号定义如下:</p>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202303/3146671-20230330004218933-736673852.png"></p>
<p>AHB2APB Bridge既是AHB总线上的一个Slave,也作为APB总线上唯一的Master,</p>
<p>其任务是将来自于AHB总线上的信号转化为APB信号,实现AHB系统和APB的互联</p>
<h2 id="21-ahb2apb-bridge原理与特性">2.1 AHB2APB Bridge原理与特性</h2>
<p>AHB2APB Bridge模块的存在,是为了实现AHB主设备到APB从设备的连接(比如本项目中的CU和GPIO),</p>
<p>Bridge需要完成AHB协议和APB协议之间的转换,以及异步数据通信,</p>
<p>由于SoC系统的时钟多样性,<strong>AMBA总线并没有规范AHB和APB总线之间的时钟关系</strong>,</p>
<p>因此AHB2APB Bridge根据系统总线的时钟分布又可以分为两类:<strong>同步Bridge</strong>和<strong>异步Bridge</strong></p>
<p><strong>异步Bridge</strong></p>
<p>在很多场景中,为了充分利用AHB/APB协议各自特点,对于AHB总线系统、APB总线系统的时钟采用不同频率/不同相位,</p>
<p>对于这种情况,AHB2APB Bridge为异步Bridge,</p>
<p>异步Bridge可以在保证AHB设备的性能的前提下,尽可能降低APB上外设的功耗,</p>
<p>而代价则是跨时钟设计带来的额外面积和设计的复杂程度</p>
<p><strong>同步Bridge</strong></p>
<p>在本项目中,则采用同步Bridge,即AHB与APB时钟同频,同步Bridge具有以下优点:</p>
<ol>
<li>整个AMBA系统由单个时钟控制,<strong>有利于时序分析</strong></li>
<li>不涉及跨时钟域的信号同步处理,设计简单且<strong>综合后面积小</strong></li>
<li>系统运行稳定可靠</li>
</ol>
<h2 id="22-ahb2apb-bridge有限状态机">2.2 AHB2APB Bridge有限状态机</h2>
<p>那么如何实现一个可以完成上述功能的Bridge模块呢?</p>
<p>答案是通过一个<strong>有限状态机</strong>(FSM,Finite State Machine)来进行控制,</p>
<p>看看一次典型的AHB到APB信号转换时序图(以Read为例):</p>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202303/3146671-20230326195905831-1151608711.png"></p>
<p>在T1-T2期间,Bridge作为AHB总线上的Slave被HADDR选中,</p>
<p>在T2-T4两个时钟周期内,Bridge将AHB的HADDR上的地址Addr1放在APB总线的PADDR上,</p>
<p>其中T2-T3,PENABLE尚未被拉高,且HREADY会被Bridge拉低,因为APB的传输速度是低于AHB的,需要AHB总线上的Master进行等待,</p>
<p>而T3-T4,PENABLE被Bridge使能,数据信息被放上PRDATA并且由Bridge转交给HRDATA,</p>
<p>我们将T1-T2,T2-T3,T3-T4这个周期分别称为<strong>IDLE</strong>,<strong>SETUP</strong>和<strong>ENABLE</strong>,</p>
<p>于是我们得到AHB2APB Bridge的状态机转换示意图:</p>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202303/3146671-20230326194617888-601193517.png"></p>
<p>总结一下:</p>
<ol>
<li>
<p>对于SETUP,下一个周期必定跳往ENABLE,<br>
由于本项目用的是APB-Lite,没有PREADY信号,所有APB传输必须在两个周期完成</p>
</li>
<li>
<p>对于IDLE或ENABLE,若总线上有传输,则下一个周期为SETUP;若无,则下一个周期为IDLE<br>
(“有无传输”,意思是AHB端的HADDR是否选中APB上的设备)</p>
</li>
</ol>
<p>注意:在T2-T4传输Data1后,APB总线又在T4-T6两个周期内完成了Data2的传输,</p>
<p>这两次传输之间并没有经历IDLE状态,这表明当AHB总线上仍然有传输任务时,ENABLE状态直接跳往SETUP状态以准备下一个数据的传输,</p>
<p>由此我们得到AHB2APB Bridge的状态机:</p>
<pre><code>reg bridge_state;

    always @(posedge iHCLK ) begin
      if (!iHRESETn) begin
            bridge_state &lt;= BRIDGE_IDLE;
      end else begin
            case ( bridge_state )
                // IDLE
                BRIDGE_IDLE: begin
                  if(iHSEL) begin
                        bridge_state &lt;= BRIDGE_SETUP;
                  end
                  else begin
                        bridge_state &lt;= BRIDGE_IDLE;
                  end                     
                end
                // SETUP
                BRIDGE_SETUP: begin
                  bridge_state &lt;= BRIDGE_ENABLE;
                end
                // ENABLE
                BRIDGE_ENABLE: begin
                  if(iHSEL) begin
                        bridge_state &lt;= BRIDGE_SETUP;
                  end
                  else begin
                        bridge_state &lt;= BRIDGE_IDLE;
                  end
                end
                //DEFAULT
                default: bridge_state &lt;= BRIDGE_IDLE;
            endcase
      end
    end
</code></pre>
<h2 id="23-ahb与apb信号转换">2.3 AHB与APB信号转换</h2>
<p>完成上述状态机设计后,剩下的工作就非常简单了,我们需要把AHB总线上的信号放到APB总线上:</p>
<ol>
<li>
<p>APB上的控制信号(PWRITE,PADDR)与数据信号(PWDATA,PRDATA)都是直接从AHB那边的信号拿过来的,</p>
</li>
<li>
<p>PENABLE只在ENABLE状态下拉高,</p>
</li>
<li>
<p>PSELx取决于HADDR落在APB总线上哪个外设的地址空间范围内,在本设计中,APB Slave为APB GPIO模块</p>
</li>
</ol>
<p>于是可以得到AHB与APB信号之间的对应关系:</p>
<pre><code>    //AHB -&gt; APB
    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;
    //Bridge -&gt; AHB
    assign oHREADY = ( bridge_state == BRIDGE_SETUP ) ? 1'b0 : 1'b1;
    assign oHRESP= OKAY;
    assign oHRDATA = iPRDATA;
</code></pre>
<p>最后,</p>
<p>附上AHB2APB Bridge模块的完整RTL:</p>
<pre><code>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,
    inputiHTRANS,
    inputiHSIZE ,
    inputiHBURST,
    input         iHWRITE,
    input iHWDATA,
    input iHADDR ,
    //output
    output      oHREADY,
    output oHRESP ,
    output oHRDATA,
    //------------ APB ------------
    //input
    input iPRDATA,
    //output
    output oPSEL0,
    output oPSEL1,
    output oPWRITE,
    output oPENABLE,
    output oPADDR,
    output oPWDATA
);
    /*————————————————————————————————————————————————————————————————————————*\
    /                            AHB Signal Register                           \
    \*————————————————————————————————————————————————————————————————————————*/
    reg         iHWRITE_r ;
    reg iHADDR_r;
    reg iHWDATA_r ;

    always@( posedge iHCLK)begin
      if(!iHRESETn) begin
            iHWRITE_r &lt;= 1'b0;
            iHADDR_r&lt;= 32'b0;
            iHWDATA_r &lt;= 32'b0;
      end
      else if( iHSEL &amp;&amp; (bridge_state == BRIDGE_IDLE || bridge_state == BRIDGE_ENABLE))begin
            iHWRITE_r &lt;= iHWRITE; // ahb reg change when bridge_state is going to change:
            iHADDR_r&lt;= iHADDR ; // from IDLE   to SETUP                     
            iHWDATA_r &lt;= iHWDATA; // from ENABLE to SETUP
      end
    end
    /*————————————————————————————————————————————————————————————————————————*\
    /                                 Bridge FSM                               \
    \*————————————————————————————————————————————————————————————————————————*/
    reg bridge_state;

    always @(posedge iHCLK ) begin
      if (!iHRESETn) begin
            bridge_state &lt;= BRIDGE_IDLE;
      end else begin
            case ( bridge_state )
                // IDLE
                BRIDGE_IDLE: begin
                  if(iHSEL) begin
                        bridge_state &lt;= BRIDGE_SETUP;
                  end
                  else begin
                        bridge_state &lt;= BRIDGE_IDLE;
                  end                     
                end
                // SETUP
                BRIDGE_SETUP: begin
                  bridge_state &lt;= BRIDGE_ENABLE;
                end
                // ENABLE
                BRIDGE_ENABLE: begin
                  if(iHSEL) begin
                        bridge_state &lt;= BRIDGE_SETUP;
                  end
                  else begin
                        bridge_state &lt;= BRIDGE_IDLE;
                  end
                end
                //DEFAULT
                default: bridge_state &lt;= 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
</code></pre>
<h1 id="3-gpio模块设计">3. GPIO模块设计</h1>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202304/3146671-20230427193101963-76839650.png"></p>
<p>GPIO(General Purpose I/O),是一种用于<strong>对器件的引脚做观测或控制</strong>的外设,</p>
<p>在STM32、ZYNQ等开发中经常被使用到,相信大家并不陌生,</p>
<p>而在本项目中,我们将尝试自己用Verilog语言,设计一个具有基本功能的GPIO外设,</p>
<p>该模块具有一个APB Slave接口,</p>
<p>GPIO模块会被挂在APB总线上,实现流水灯控制系统和开发板的外设(LED灯&按键)的交互,</p>
<p>模块的信号定义如下表:</p>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202303/3146671-20230330204538423-1385907581.png"></p>
<p>其中oGPIOout连接开发板上LED4引脚,oGPIOout连接LED3引脚,oGPIOout连接LED2引脚,oGPIOout连接LED1引脚,</p>
<p>iGPIOin也是按照该顺序,依次连接开发板上的4个按键:KEY4,KEY3,KEY2,KEY1。</p>
<h2 id="31-gpio模块寄存器">3.1 GPIO模块寄存器</h2>
<p>GPIO模块的功能实现,<strong>主要依靠四个32位寄存器</strong>:1.DATA_RO寄存器 2.DATA寄存器 3. DIRM寄存器 4. OEN寄存器</p>
<p>主机通过配置/读写这些寄存器,就可以实现对外设的操作,</p>
<p>下面分析这四个寄存器:</p>
<ol>
<li>
<p>DATA_RO:<br>
用来观测GPIO引脚状态,若引脚被配置成输出模式,则该寄存器会反映驱动该引脚的电平的状态。<br>
DATA_RO是一个<strong>只读</strong>寄存器,对该寄存器的写操作是无效的</p>
</li>
<li>
<p>DATA:<br>
当GPIO某一引脚被配置为输出模式时,用来控制该引脚的输出状态</p>
</li>
<li>
<p>DIRM:<br>
用来配置GPIO各个引脚的方向(做输入or做输出),<br>
当DIRMP==0,第x位引脚为输入引脚,其输出功能被disable</p>
</li>
<li>
<p>OEN:<br>
当GPIO某一引脚被配置为输出模式时,用来使能该引脚的输出功能,<br>
当OEN==0时,第x位引脚的输出功能被disable</p>
</li>
</ol>
<p>为了避免在设计中引入双向端口,我们为GPIO模块赋予了一个32位输入端口iGPIOin,一个32位输出端口oGPIOout,</p>
<p>当<strong>reg_DIRM=0</strong>,<br>
对应i号引脚被DIRM寄存器配置为输入端口,<br>
oGPIOout呈现高阻态1'bz,该位对应的引脚输出功能实际上是不存在的,<br>
iGPIOin将写入reg_DATA_RO,此时APB总线可以通过PRDATA了解该引脚的输入状态:</p>
<pre><code>    reg oGPIOout ;
    always @(*) begin
      for ( i=0 ; i&lt;32 ; i=i+1 ) begin
            if( reg_DIRM &amp; reg_OEN ) begin //output mode
                oGPIOout = reg_DATA ;
            end else begin
                oGPIOout = 1'bz;
            end
      end
    end
</code></pre>
<p>反之,当<strong>reg_DIRM=1</strong>,<br>
该引脚被DIRM寄存器配置为输出端口,<br>
oGPIOout的值取决于reg_DATA的情况,<br>
iGPIOin该位对应的引脚输入功能实际上是不存在的,写入reg_DATA_RO的将是oGPIOout(即反映驱动该引脚的电平的状态):</p>
<pre><code>            for ( i=0 ; i&lt;32 ; i=i+1 ) begin
                if ( reg_DIRM ) begin
                  reg_DATA_RO &lt;= oGPIOout ;// output mode
                end else begin
                  reg_DATA_RO &lt;= iGPIOin ;// input mode      
                end
            end
</code></pre>
<h2 id="32-输出控制">3.2 输出控制</h2>
<p>对于每一位被配置成输出模式的GPIO引脚,<br>
其输出控制逻辑如下图所示:</p>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202304/3146671-20230427194159776-1229532500.png"></p>
<p>只有DIRM寄存器被配置为输出模式<strong>且</strong>OEN寄存器被配置为输出使能状态,<br>
oGPIOout才会反应DATA寄存器的状态,<strong>否则该输出引脚呈现高阻态</strong></p>
<h2 id="33-输入观测">3.3 输入观测</h2>
<p>而对于作为输出的GPIO引脚,其观测逻辑则在下图中用<strong>蓝色部分</strong>表示:</p>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202304/3146671-20230427194132637-2066706968.png"></p>
<p>DIRM寄存器被配置为输入模式时将写入只读寄存器DATA_RO,</p>
<p>否则DATA_RO反映该当前输出模式下的引脚的输出状态。</p>
<p>GPIO模块的完整RTL如下:</p>
<pre><code>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 iPADDR,
    input iPWDATA ,
    output oPRDATA ,

    // I/O Signal
    input iGPIOin,
    output oGPIOout

);

    /*————————————————————————————————————————————————————————————————————————*\
    /                            APB Signal Register                           \
    \*————————————————————————————————————————————————————————————————————————*/
    reg         iPSELx_r;
    reg         iPWRITE_r ;
    reg iPADDR_r;
    reg iPWDATA_r ;

    always@( posedge iPCLK)begin
      if(!iPRESETn) begin
            iPWRITE_r&lt;= 1'b0;
            iPADDR_r   &lt;= 16'b0;
      end
      else begin
            iPWRITE_r&lt;= iPWRITE;
            iPWDATA_r&lt;= iPWDATA;
            iPADDR_r   &lt;= iPADDR;
      end
    end

    /*————————————————————————————————————————————————————————————————————————*\
    /                           GPIO Register Declaration                      \
    \*————————————————————————————————————————————————————————————————————————*/
    // Read Only Data
    reg reg_DATA_RO;
    // GPIO Data
    reg reg_DATA;
    // Direction (in or out)
    reg reg_DIRM;
    // Output Enable
    reg reg_OEN;

    /*————————————————————————————————————————————————————————————————————————*\
    /                              Register Configuration                      \
    \*————————————————————————————————————————————————————————————————————————*/
    integer i;
    //reg GPIOin_r;

    always @(posedge iPCLK ) begin
      if( !iPRESETn ) begin
            reg_DATA_RO   &lt;= 32'b0;
            reg_DATA      &lt;= 32'b0;
            reg_DIRM      &lt;= 32'b0;
            reg_OEN       &lt;= 32'b0;
      end
      else begin
   
            // reg_DATA, reg_DIRM, reg_OEN
            if( iPENABLE &amp;&amp; iPWRITE) begin
                case ( iPADDR )
                  OFFSET_GPIO_DATA_RO: begin end    //DATA_RO is read only register            
                  OFFSET_GPIO_DATA:    begin
                        reg_DATA       &lt;= iPWDATA;
                  end
                  OFFSET_GPIO_DIRM:    begin
                        reg_DIRM &lt;= iPWDATA;
                  end
                  OFFSET_GPIO_OEN:   begin
                        reg_OEN &lt;= iPWDATA;
                  end
                  default:             begin
                        reg_DATA    &lt;= reg_DATA   ;
                        reg_DIRM    &lt;= reg_DIRM   ;
                        reg_OEN   &lt;= reg_OEN    ;
                  end
                endcase
            end

            // DATA_RO
            for ( i=0 ; i&lt;32 ; i=i+1 ) begin
                if ( reg_DIRM ) begin
                  reg_DATA_RO &lt;= oGPIOout ;// output mode
                end else begin
                  reg_DATA_RO &lt;= iGPIOin ;// input mode      
                end
            end   
       end
    end

    /*————————————————————————————————————————————————————————————————————————*\
    /                                     I/O                                  \
    \*————————————————————————————————————————————————————————————————————————*/
    // iGPIOin -&gt; GPIOin_r -&gt; DATA_RO -&gt; PRADATA
    assign oPRDATA = reg_DATA_RO;

    // reg_DATA -&gt; GPIOout
    reg oGPIOout ;
    always @(*) begin
      for ( i=0 ; i&lt;32 ; i=i+1 ) begin
            if( reg_DIRM &amp; reg_OEN ) begin //output mode
                oGPIOout = reg_DATA ;
            end else begin
                oGPIOout = 1'bz;
            end
      end
    end

endmodule
</code></pre>
<h1 id="4-control-unit流水灯控制单元模块设计">4. Control Unit(流水灯控制单元)模块设计</h1>
<h2 id="41-有限状态机设计">4.1 有限状态机设计</h2>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202303/3146671-20230330205904068-1322031463.png"></p>
<p>我们的AMBA流水灯控制系统的核心:Control Unit,即控制单元,</p>
<p>Bridge模块是APB上所有外设的Master,而CU模块则是Bridge在AHB上的Master,</p>
<p>可以说是Master中的Master了,</p>
<p>Control Unit是<strong>信号传输的发起者</strong>,决定总线上是在读还是在写、也决定访问的从机是哪个,</p>
<p>CU发出的控制信号包括HTRANS、HBURST、HSIZE、HADDR,数据信号包括HWDATA,</p>
<p>这些AHB信号如何生成均是由Control Unit负责的,</p>
<p>同时CU也需要接收HRDATA,并对其进行解读判断,</p>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202303/3146671-20230330210354707-63874788.png"></p>
<p>那么这些AHB信号是如何生成的呢?</p>
<p>实际上,Control Unit模块也是通过一个有限状态机实现的:</p>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202304/3146671-20230427195533480-836310543.png"></p>
<p>下面介绍一下该FSM的各个状态所代表的含义:</p>
<p><strong>1. CONFIG_0~2</strong>:按下复位按钮/上电后,对GPIO外设进行初始化配置,</p>
<p>其中,<strong>CONFIG_0</strong>:<br>
<strong>将GPIO的0~3位引脚配置为输入模式,连接FPGA开发板的Key按键进行观测</strong><br>
<strong>将GPIO的4~7位引脚配置为输出模式,连接FPGA开发板的LED灯进行驱动控制</strong></p>
<p><strong>CONFIG_1</strong>:<br>
将GPIO的4~7位引脚的输出电平均设定为高电平1(本开发板上的LED为共阳极,引脚高电平=LED灯熄灭)</p>
<p><strong>CONFIG_2</strong>:<br>
对GPIO的4~7位引脚进行输出使能</p>
<p><strong>2. READ_DATA_RO</strong>:<br>
读GPIO的DATA_RO寄存器,根据寄存器的值得知Key状态,<br>
该状态是FSM的核心状态,<br>
当配置完GPIO的DIRM、OEN、DATA寄存器后,<br>
我们会开始反复进行读DATA_RO,即观测KEY的状态,<br>
然后根据KEY的值得到对应的LED_mode,<br>
随后会由该状态跳向<strong>WRITE_DATA_0~3</strong>,对DATA寄存器进行配置,控制LED灯点亮or熄灭</p>
<p><strong>3. WRITE_DATA_0~3</strong>:<br>
配置不同的流水灯灯工作模式,模式由KEY状态决定,<br>
按下KEY1进入工作模式0,按下KEY2进入工作模式1,按下KEY3进入工作模式2,按下KEY4进入工作模式3,<br>
在初始静止状态下,只有同时按下KEY0~3才能进入工作模式0,<br>
此时按下某个单独的KEY不会有任何反应的</p>
<p>随着FSM状态的改变,CU模块将会给AHB总线发出不同的读写命令,从而实现对GPIO模块的寄存器配置,</p>
<p>下表具体地整理了<strong>FSM状态</strong>和<strong>CU模块生成的AHB信号</strong>之间的对应关系:</p>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202303/3146671-20230330224442213-1893482998.png"></p>
<p>上述FSM状态转换逻辑所对应的RTL:</p>
<pre><code>reg CU_state;

    always @(posedge iHCLK ) begin
    if ( !iHRESETn ) begin
      CU_state &lt;= CONFIG_0;
    end else begin
      case (CU_state)
            // write GPIO_DIRM
            CONFIG_0: begin
                if ( iHREADY ) begin
                  CU_state &lt;= CONFIG_1;
                end else begin
                  CU_state &lt;= CONFIG_0;
                end
            end
            // write GPIO_DATA
            CONFIG_1: begin
                if ( iHREADY ) begin
                  CU_state &lt;= CONFIG_2;
                end else begin
                  CU_state &lt;= CONFIG_1;
                end
            end
            // write GPIO_OEN
            CONFIG_2: begin
                if ( iHREADY &amp;&amp; iHRDATA == 4'b0000) begin
                  CU_state &lt;= READ_DATA_RO;
                end else begin
                  CU_state &lt;= CONFIG_2;
                end
            end
            // read DATA_RO
            READ_DATA_RO: begin
                if ( iHREADY          &amp;&amp; iHRDATA == 4'b1110) begin //key1 pressed
                  CU_state &lt;= WRITE_DATA_0;
                end else if ( iHREADY &amp;&amp; iHRDATA == 4'b1101) begin //key2 pressed
                  CU_state &lt;= WRITE_DATA_1;
                end else if ( iHREADY &amp;&amp; iHRDATA == 4'b1011) begin //key3 pressed
                  CU_state &lt;= WRITE_DATA_2;
                end else if ( iHREADY &amp;&amp; iHRDATA == 4'b0111) begin //key4 pressed
                  CU_state &lt;= WRITE_DATA_3;
                end else if ( iHREADY &amp;&amp; led_mode )            begin //keep mode0 if not pressed this moment
                  CU_state &lt;= WRITE_DATA_0;            
                end else if ( iHREADY &amp;&amp; led_mode )            begin //keep mode1
                  CU_state &lt;= WRITE_DATA_0;            
                end else if ( iHREADY &amp;&amp; led_mode )            begin //keep mode2
                  CU_state &lt;= WRITE_DATA_1;            
                end else if ( iHREADY &amp;&amp; led_mode )            begin //keep mode3
                  CU_state &lt;= WRITE_DATA_2;            
                end else begin
                  CU_state &lt;= READ_DATA_RO;   // Slave not ready || no key ever been pressed
                end                           
            end
            // write DATA
            WRITE_DATA_0 : begin
                if ( iHREADY ) begin
                  CU_state &lt;= READ_DATA_RO;
                end else begin
                  CU_state &lt;= CU_state;
                end
            end
            // write DATA
            WRITE_DATA_1 : begin
                if ( iHREADY ) begin
                  CU_state &lt;= READ_DATA_RO;
                end else begin
                  CU_state &lt;= CU_state;
                end
            end
            // write DATA
            WRITE_DATA_2 : begin
                if ( iHREADY ) begin
                  CU_state &lt;= READ_DATA_RO;
                end else begin
                  CU_state &lt;= CU_state;
                end
            end
            WRITE_DATA_3 : begin
                if ( iHREADY ) begin
                  CU_state &lt;= READ_DATA_RO;
                end else begin
                  CU_state &lt;= CU_state;
                end
            end
            default: CU_state &lt;= CONFIG_0;
      endcase
      end
    end
</code></pre>
<h2 id="42-led控制方法">4.2 LED控制方法</h2>
<p>LED灯的亮灭改变,是通过配置GPIO内的寄存器实现的,</p>
<p>具体来说,是借助总线信号HWDATA对GPIO的reg_DATA寄存器进行配置,</p>
<p>在绪论章节中,我们介绍过LED_mode的编码方式:</p>
<pre><code>    // LED_mode Parameters
    parameter    MODE0   = 4'b0001,
    parameter    MODE1   = 4'b0010,
    parameter    MODE2   = 4'b0100,
    parameter    MODE3   = 4'b1000,
</code></pre>
<p>LED_mode采用了One-hot编码,这是为了对HWDATA进行assign赋值的时候,判断条件写起来简便一些,</p>
<p>现在我们就可以设计出各种流水灯模式下的组合逻辑了,</p>
<p>各模式下的LED亮灭随时间改变是通过<strong>计时器</strong>辅助的,</p>
<p>也就是下面代码中的timer,timer是一个32位寄存器:</p>
<pre><code>regtimer   ;
always @ (posedge iHCLK or negedge iHRESETn)    begin
      if ( !iHRESETn )                           
            timer &lt;= 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 &lt;= 32'd0;                     // count done,clearing the time counter
      else
                  timer &lt;= timer + 1'b1;            // timer counter = timer counter + 1
    end
</code></pre>
<p>本质是将每一秒分成了50M帧(系统工作时钟频率为50MHz),</p>
<p>我们用assign语句给不同的帧下的HWDATA赋不同的值就行了,</p>
<p>首先是<strong>普通流水灯模式</strong>(工作模式0):<br>
(注意,下面的LED在WRITE_DATA_0~3状态下将会作为HWDATA对GPIO_DATA寄存器进行配置)</p>
<pre><code>assign LED =         
      // mode0 普通流水灯模式
      ( led_mode &amp;&amp; timer &gt;= 32'd149_999_999 ) ? 4'b0111 : // LED4亮
      ( led_mode &amp;&amp; timer &gt;= 32'd99_999_999) ? 4'b1011 : // LED3亮
      ( led_mode &amp;&amp; timer &gt;= 32'd49_999_999) ? 4'b1101 : // LED2亮
      ( led_mode                           ) ? 4'b1110 : // LED1亮
....(其他模式)
</code></pre>
<p>这里就体现出One-hot编码的好处了,可以用<strong>( led_mode )</strong>作为是否处于模式0的判定,<br>
否则需要写成<strong>( led_mode == MODE_0 )</strong>,</p>
<p>接下来是<strong>加速流水灯模式</strong>(工作模式1):</p>
<pre><code>assign LED =         
      // mode1 加速流水灯模式
      ( led_mode &amp;&amp; timer &gt;= 32'd174_999_999 ) ? 4'b0111 : // LED4亮
      ( led_mode &amp;&amp; timer &gt;= 32'd149_999_999 ) ? 4'b1011 : // LED3亮
      ( led_mode &amp;&amp; timer &gt;= 32'd124_999_999 ) ? 4'b1101 : // LED2亮
      ( led_mode &amp;&amp; timer &gt;= 32'd99_999_999) ? 4'b1110 : // LED1亮
      ( led_mode &amp;&amp; timer &gt;= 32'd74_999_999) ? 4'b0111 : // LED4亮
      ( led_mode &amp;&amp; timer &gt;= 32'd49_999_999) ? 4'b1011 : // LED3亮
      ( led_mode &amp;&amp; timer &gt;= 32'd24_999_999) ? 4'b1101 : // LED2亮
      ( led_mode                           ) ? 4'b1110 : // LED1亮   
....(其他模式)
</code></pre>
<p><strong>心跳模式</strong>(工作模式2):</p>
<pre><code>assign LED =   
      // mode2 心跳模式
      ( led_mode &amp;&amp; timer &gt;= 32'd189_999_999 ) ? 4'b0000 : // 全亮
      ( led_mode &amp;&amp; timer &gt;= 32'd179_999_999 ) ? 4'b1111 : // 全灭
      ( led_mode &amp;&amp; timer &gt;= 32'd169_999_999 ) ? 4'b0000 : // 全亮
      ( led_mode                           ) ? 4'b1111 : // 全灭
....(其他模式)
</code></pre>
<p>最后是<strong>呼吸灯模式</strong>(工作模式3),</p>
<p>该模式相对复杂一些,前面提到过是通过改变占空比控制LED灯亮度的,</p>
<p>占空比的改变是通过对timer的判断实现的,</p>
<p>举个例子:</p>
<pre><code>assign LED =
      // 占空比 1/4= 25%
      ( led_mode &amp;&amp; (timer &gt;= 32'd89_999_999 )&amp;&amp; (timer == 2'b00 )) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd89_999_999 )                           ) ? 4'b1111 :
....(其他模式)
</code></pre>
<p><strong>(timer == 2'b00 )</strong>的判定,在timer的值每增加4的过程中,<strong>必定出现且只出现1次</strong>,</p>
<p>这样在 <strong>(timer &gt;= 32'd89_999_999 )</strong>的这段期间内,</p>
<p>LED灯的驱动引脚就会在25%的时间里处于低电平(点亮),剩下的75%时间里处于高电平(熄灭),</p>
<p>同理,<strong>(timer == 3'b0 )</strong>就能得到1/8的占空比,也就是12.5%,</p>
<p>这样就有:</p>
<pre><code>assign LED =
      // mode3 呼吸灯模式
      // 占空比 0%
      ( led_mode &amp;&amp; (timer &gt;= 32'd189_999_999)                           ) ? 4'b1111 :
      // 占空比 1/64 = 1.56%
      ( led_mode &amp;&amp; (timer &gt;= 32'd169_999_999)&amp;&amp; (timer == 6'b000)) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd169_999_999)                           ) ? 4'b1111 :
      // 占空比 1/32 = 3.12%
      ( led_mode &amp;&amp; (timer &gt;= 32'd149_999_999)&amp;&amp; (timer == 5'b000)) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd149_999_999)                           ) ? 4'b1111 :
      // 占空比 1/16 = 6.25%
      ( led_mode &amp;&amp; (timer &gt;= 32'd129_999_999)&amp;&amp; (timer == 4'b000)) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd129_999_999)                           ) ? 4'b1111 :
      // 占空比 1/8= 12.5%
      ( led_mode &amp;&amp; (timer &gt;= 32'd109_999_999)&amp;&amp; (timer == 3'b000)) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd109_999_999)                           ) ? 4'b1111 :
      // 占空比 1/4= 25%
      ( led_mode &amp;&amp; (timer &gt;= 32'd89_999_999 )&amp;&amp; (timer == 2'b00 )) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd89_999_999 )                           ) ? 4'b1111 :
      // 12.5%
      ( led_mode &amp;&amp; (timer &gt;= 32'd69_999_999 )&amp;&amp; (timer == 3'b000)) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd69_999_999 )                           ) ? 4'b1111 :
      // 6.25%
      ( led_mode &amp;&amp; (timer &gt;= 32'd49_999_999 )&amp;&amp; (timer == 4'b000)) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd49_999_999 )                           ) ? 4'b1111 :
      // 3.12%
      ( led_mode &amp;&amp; (timer &gt;= 32'd29_999_999 )&amp;&amp; (timer == 5'b000)) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd29_999_999 )                           ) ? 4'b1111 :
      // 1.56%
      ( led_mode &amp;&amp; (timer &gt;= 32'd9_999_999)&amp;&amp; (timer == 6'b000)) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd9_999_999)                           ) ? 4'b1111 :
      // 0%
      ( led_mode                                                         ) ? 4'b1111 :
....(其他模式)
</code></pre>
<p>可以看出上述代码实现了占空比从0% → 1.56% → 3.12% → 6.25% → 12.5% → 25% → 12.5% → 6.25% → 3.12% → 1.56% → 0%的等时间间隔变化,</p>
<p>由于开发板LED灯功率较高,在25%到100%之间的占空比,LED灯亮度都会很高,看不出太大变化,</p>
<p>因此我们将25%设定为了最高的占空比,对应LED灯最亮的时刻。</p>
<p>Control Unit模块的完整RTL如下:</p>
<pre><code>module AHB_control_unit #(
    //AHB Parameters
    parameter    OKAY= 2'b00   ,
    parameter    ERROR = 2'b01   ,
    parameter    SPLIT = 2'b10   ,
    parameter    RETRY = 2'b11   ,
                                    parameter    IDLE   = 2'b00,
                                    parameter    BUSY   = 2'b01,
                                    parameter    SEQ    = 2'b10,
                                    parameter    NONSEQ = 2'b11,
    parameter    BYTE= 3'b000,
    parameter    WORD= 3'b001,
    parameter    DWORD = 3'b010,
                                    parameter    SINGLE = 3'b000 ,
                                    parameter    INCR   = 3'b001 ,
                                    parameter    WRAP4= 3'b010 ,
                                    parameter    INCR4= 3'b011 ,
                                    parameter    WARP8= 3'b100 ,
                                    parameter    INCR8= 3'b101 ,
                                    parameter    WARP16 = 3'b110 ,
                                    parameter    INCR16 = 3'b111 ,
    //Control Unit FSM Parameters
    parameter    CONFIG_0   = 3'b000,
    parameter    CONFIG_1   = 3'b001,
    parameter    CONFIG_2   = 3'b010,
    parameter    READ_DATA_RO = 3'b011,
    parameter    WRITE_DATA_0 = 3'b100,
    parameter    WRITE_DATA_1 = 3'b101,
    parameter    WRITE_DATA_2 = 3'b110,
    parameter    WRITE_DATA_3 = 3'b111,

    // LED_mode Parameters
    parameter    MODE0   = 4'b0001,
    parameter    MODE1   = 4'b0010,
    parameter    MODE2   = 4'b0100,
    parameter    MODE3   = 4'b1000,

    //Address 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

) (
    // Input from AHB Bus
    input         iHCLK,
    input         iHRESETn,
    input         iHREADY,
    inputiHRESP,
    input iHRDATA,

    // Output to AHB Bus
    output    oHTRANS,
    output    oHSIZE,
    output    oHBURST,
    output         oHWRITE,
    output oHADDR ,
    output oHWDATA

);
    /*————————————————————————————————————————————————————————————————————————*\
    /                           Control Unit FSM                               \
    \*————————————————————————————————————————————————————————————————————————*/
   
    reg CU_state;

    always @(posedge iHCLK ) begin
    if ( !iHRESETn ) begin
      CU_state &lt;= CONFIG_0;
    end else begin
      case (CU_state)
            // write GPIO_DIRM
            CONFIG_0: begin
                if ( iHREADY ) begin
                  CU_state &lt;= CONFIG_1;
                end else begin
                  CU_state &lt;= CONFIG_0;
                end
            end
            // write GPIO_DATA
            CONFIG_1: begin
                if ( iHREADY ) begin
                  CU_state &lt;= CONFIG_2;
                end else begin
                  CU_state &lt;= CONFIG_1;
                end
            end
            // write GPIO_OEN
            CONFIG_2: begin
                if ( iHREADY &amp;&amp; iHRDATA == 4'b0000) begin
                  CU_state &lt;= READ_DATA_RO;
                end else begin
                  CU_state &lt;= CONFIG_2;
                end
            end
            // read DATA_RO
            READ_DATA_RO: begin
                if ( iHREADY          &amp;&amp; iHRDATA == 4'b1110) begin //key1 pressed
                  CU_state &lt;= WRITE_DATA_0;
                end else if ( iHREADY &amp;&amp; iHRDATA == 4'b1101) begin //key2 pressed
                  CU_state &lt;= WRITE_DATA_1;
                end else if ( iHREADY &amp;&amp; iHRDATA == 4'b1011) begin //key3 pressed
                  CU_state &lt;= WRITE_DATA_2;
                end else if ( iHREADY &amp;&amp; iHRDATA == 4'b0111) begin //key4 pressed
                  CU_state &lt;= WRITE_DATA_3;
                end else if ( iHREADY &amp;&amp; led_mode )            begin //keep mode0 if not pressed this moment
                  CU_state &lt;= WRITE_DATA_0;            
                end else if ( iHREADY &amp;&amp; led_mode )            begin //keep mode1
                  CU_state &lt;= WRITE_DATA_1;            
                end else if ( iHREADY &amp;&amp; led_mode )            begin //keep mode2
                  CU_state &lt;= WRITE_DATA_2;            
                end else if ( iHREADY &amp;&amp; led_mode )            begin //keep mode3
                  CU_state &lt;= WRITE_DATA_3;            
                end else begin
                  CU_state &lt;= READ_DATA_RO;   // Slave not ready || no key ever been pressed
                end                           
            end
            // write DATA
            WRITE_DATA_0 : begin
                if ( iHREADY ) begin
                  CU_state &lt;= READ_DATA_RO;
                end else begin
                  CU_state &lt;= CU_state;
                end
            end
            // write DATA
            WRITE_DATA_1 : begin
                if ( iHREADY ) begin
                  CU_state &lt;= READ_DATA_RO;
                end else begin
                  CU_state &lt;= CU_state;
                end
            end
            // write DATA
            WRITE_DATA_2 : begin
                if ( iHREADY ) begin
                  CU_state &lt;= READ_DATA_RO;
                end else begin
                  CU_state &lt;= CU_state;
                end
            end
            WRITE_DATA_3 : begin
                if ( iHREADY ) begin
                  CU_state &lt;= READ_DATA_RO;
                end else begin
                  CU_state &lt;= CU_state;
                end
            end
            default: CU_state &lt;= 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,4'b0 } : 32'hz;

    /*————————————————————————————————————————————————————————————————————————*\
    /                              LED Control                                 \
    \*————————————————————————————————————————————————————————————————————————*/
   
    regled_mode; //4 modes in all (one hot encoding)
    regtimer   ;
    wire LED   ;
   
    //------------ led_mode ------------
    always @(posedge iHCLK ) begin
    if ( !iHRESETn ) begin
      led_mode &lt;= 4'b0000;
    end else begin
      case (CU_state)

            CONFIG_0: begin
                led_mode &lt;= 4'b0000;
            end

            CONFIG_1: begin
                led_mode &lt;= 4'b0000;
            end

            CONFIG_2: begin
                if ( iHREADY &amp;&amp; iHRDATA == 4'b0000) begin
                  led_mode &lt;= MODE0;
                end else begin
                  led_mode &lt;= 4'b0000;
                end
            end

            READ_DATA_RO: begin
                // key1 pressed mode -&gt; MODE0
                if( iHREADY          &amp;&amp; iHRDATA == 4'b1110) begin
                  led_mode &lt;= MODE0;
                // key2 pressed mode -&gt; MODE1
                end else if( iHREADY &amp;&amp; iHRDATA == 4'b1101) begin
                  led_mode &lt;= MODE1;
                // key3 pressed mode -&gt; MODE2
                end else if( iHREADY &amp;&amp; iHRDATA == 4'b1011) begin
                  led_mode &lt;= MODE2;
                // key4 pressed mode -&gt; MODE3
                end else if( iHREADY &amp;&amp; iHRDATA == 4'b0111) begin
                  led_mode &lt;= MODE3;
                // no key pressed, mode kept
                end else begin
                  led_mode &lt;= led_mode;
                end                     
            end

            WRITE_DATA_0 : begin
                led_mode &lt;= led_mode;
            end
            WRITE_DATA_1 : begin
                led_mode &lt;= led_mode;
            end
            WRITE_DATA_2 : begin
                led_mode &lt;= led_mode;
            end
            WRITE_DATA_3 : begin
                led_mode &lt;= led_mode;
            end
            default: begin
                led_mode &lt;= led_mode;
            end

      endcase
    end
    end

    //------------ timer ------------
    always @ (posedge iHCLK or negedge iHRESETn)    begin
      if ( !iHRESETn )                           
            timer &lt;= 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 &lt;= 32'd0;                     // count done,clearing the time counter
      else
                  timer &lt;= timer + 1'b1;            // timer counter = timer counter + 1
    end

    //------------ led ------------
    assign LED =         
      // mode0 普通流水灯模式
      ( led_mode &amp;&amp; timer &gt;= 32'd149_999_999 ) ? 4'b0111 : // LED4亮
      ( led_mode &amp;&amp; timer &gt;= 32'd99_999_999) ? 4'b1011 : // LED3亮
      ( led_mode &amp;&amp; timer &gt;= 32'd49_999_999) ? 4'b1101 : // LED2亮
      ( led_mode                           ) ? 4'b1110 : // LED1亮      
      // mode1 加速流水灯模式
      ( led_mode &amp;&amp; timer &gt;= 32'd174_999_999 ) ? 4'b0111 : // LED4亮
      ( led_mode &amp;&amp; timer &gt;= 32'd149_999_999 ) ? 4'b1011 : // LED3亮
      ( led_mode &amp;&amp; timer &gt;= 32'd124_999_999 ) ? 4'b1101 : // LED2亮
      ( led_mode &amp;&amp; timer &gt;= 32'd99_999_999) ? 4'b1110 : // LED1亮
      ( led_mode &amp;&amp; timer &gt;= 32'd74_999_999) ? 4'b0111 : // LED4亮
      ( led_mode &amp;&amp; timer &gt;= 32'd49_999_999) ? 4'b1011 : // LED3亮
      ( led_mode &amp;&amp; timer &gt;= 32'd24_999_999) ? 4'b1101 : // LED2亮
      ( led_mode                           ) ? 4'b1110 : // LED1亮
      // mode2 心跳模式
      ( led_mode &amp;&amp; timer &gt;= 32'd189_999_999 ) ? 4'b0000 : // 全亮
      ( led_mode &amp;&amp; timer &gt;= 32'd179_999_999 ) ? 4'b1111 : // 全灭
      ( led_mode &amp;&amp; timer &gt;= 32'd169_999_999 ) ? 4'b0000 : // 全亮
      ( led_mode                           ) ? 4'b1111 : // 全灭
      // mode3 呼吸灯模式
      // 占空比 0%
      ( led_mode &amp;&amp; (timer &gt;= 32'd189_999_999)                           ) ? 4'b1111 :
      // 占空比 1/64 = 1.56%
      ( led_mode &amp;&amp; (timer &gt;= 32'd169_999_999)&amp;&amp; (timer == 6'b000)) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd169_999_999)                           ) ? 4'b1111 :
      // 占空比 1/32 = 3.12%
      ( led_mode &amp;&amp; (timer &gt;= 32'd149_999_999)&amp;&amp; (timer == 5'b000)) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd149_999_999)                           ) ? 4'b1111 :
      // 占空比 1/16 = 6.25%
      ( led_mode &amp;&amp; (timer &gt;= 32'd129_999_999)&amp;&amp; (timer == 4'b000)) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd129_999_999)                           ) ? 4'b1111 :
      // 占空比 1/8= 12.5%
      ( led_mode &amp;&amp; (timer &gt;= 32'd109_999_999)&amp;&amp; (timer == 3'b000)) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd109_999_999)                           ) ? 4'b1111 :
      // 占空比 1/4= 25%
      ( led_mode &amp;&amp; (timer &gt;= 32'd89_999_999 )&amp;&amp; (timer == 2'b00 )) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd89_999_999 )                           ) ? 4'b1111 :
      // 12.5%
      ( led_mode &amp;&amp; (timer &gt;= 32'd69_999_999 )&amp;&amp; (timer == 3'b000)) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd69_999_999 )                           ) ? 4'b1111 :
      // 6.25%
      ( led_mode &amp;&amp; (timer &gt;= 32'd49_999_999 )&amp;&amp; (timer == 4'b000)) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd49_999_999 )                           ) ? 4'b1111 :
      // 3.12%
      ( led_mode &amp;&amp; (timer &gt;= 32'd29_999_999 )&amp;&amp; (timer == 5'b000)) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd29_999_999 )                           ) ? 4'b1111 :
      // 1.56%
      ( led_mode &amp;&amp; (timer &gt;= 32'd9_999_999)&amp;&amp; (timer == 6'b000)) ? 4'b0000 :
      ( led_mode &amp;&amp; (timer &gt;= 32'd9_999_999)                           ) ? 4'b1111 :
      // 0%
      ( led_mode                                                         ) ? 4'b1111 :4'b1111 ;
         

endmodule
</code></pre>
<p>上述方式是一种实现呼吸灯占空比调节的思路,</p>
<p>当然,并不是唯一的方法,这里再提供一种其他的设计思路,</p>
<p>有兴趣的读者可以研究一下如何用这种方式对呼吸灯模式的控制进行复现:</p>
<p>https://blog.csdn.net/spx1164376416/article/details/124935001?ops_request_misc=&amp;request_id=&amp;biz_id=102&amp;utm_term=PWM verilog&amp;utm_medium=distribute.pc_search_result.none-task-blog-2~</p>
<p>至此,Control Unit模块的设计思路就介绍完了,</p>
<p>在下面的第五章中,我们将演示系统在FPGA开发板上的实际运行视频</p>
<h1 id="5-fpga验证">5. FPGA验证</h1>
<p>本项目所用开发板型号:XC7A35T-2FGG484I</p>
<h2 id="51-动态仿真验证">5.1 动态仿真验证</h2>
<p>上板运行系统之前,先编写Testbench仿真观察一下波形:</p>
<ol>
<li>
<p>上电后CU对GPIO寄存器进行配置(CU模块状态机0-&gt;1-&gt;2)<br>
<img src="https://img2023.cnblogs.com/blog/3146671/202305/3146671-20230507223943204-977910011.png"></p>
</li>
<li>
<p>同时按下四个按键(FPGA_key=4'd0),进入流水灯模式,(CU模块状态机3&lt;-&gt;4)<br>
<img src="https://img2023.cnblogs.com/blog/3146671/202305/3146671-20230507224351051-40173121.png"></p>
</li>
<li>
<p>按下key1(FPGA_key=4'dd),进入加速流水灯模式<br>
<img src="https://img2023.cnblogs.com/blog/3146671/202305/3146671-20230507224602829-74783688.png"></p>
</li>
<li>
<p>其余几个模式同理,这里不一一展示了,附Testbench代码:</p>
</li>
</ol>
<details>
<summary>点击查看代码</summary>
<pre><code>module tb_SoC_top ();

    reg      HCLK    ;
    reg      HRESETn ;
    reg FPGA_Key;
    wire FPGA_LED;
   
    SoC_top u_test(
      .HCLK    (HCLK    ),
      .HRESETn (HRESETn ),
      .FPGA_Key(FPGA_Key),
      .FPGA_LED(FPGA_LED)
    );
   
    initial begin
            forever #10 HCLK = !HCLK; //50MHz
    end

    initial begin
      HCLK    = 0;
      HRESETn = 0;
      FPGA_Key = 4'b1111;

      #55
      HRESETn = 1;

      #1000
      FPGA_Key = 4'b0000;//key1~4 pressed
      #100
      FPGA_Key = 4'b1111;//key release

      #1000
      FPGA_Key = 4'b1110;   //key1 pressed
      #100
      FPGA_Key = 4'b1111;   //key release

      #1000
      FPGA_Key = 4'b1101;   //key2 pressed
      #100
      FPGA_Key = 4'b1111;   //key release

      #1000
      FPGA_Key = 4'b1011;   //key3 pressed
      #100
      FPGA_Key = 4'b1111;   //key release

      #1000
      FPGA_Key = 4'b1110;   //key3 pressed
      #100
      FPGA_Key = 4'b1111;   //key release
    end

endmodule
</code></pre>
</details>
<h2 id="52-fpga开发板实速运行">5.2 FPGA开发板实速运行</h2>
<p>上板验证视频:https://www.bilibili.com/video/BV1EM4y1U7QP/?vd_source=b96e62cf611b2bbbbacda9f1c4d9a394</p>
<p>本项目所用开发板型号:XC7A35T-2FGG484I</p>
<p>开发板资源使用情况如图:<br>
<img src="https://img2023.cnblogs.com/blog/3146671/202303/3146671-20230331161842655-1245246863.png"></p>
<p><img src="https://img2023.cnblogs.com/blog/3146671/202305/3146671-20230509132137829-1077028399.png"></p><br><br>
来源:https://www.cnblogs.com/sjtu-zsj-990702/p/17251396.html
頁: [1]
查看完整版本: 「FPGA项目」—— 基于AMBA总线的流水灯控制系统