亿迅智能制造网
工业4.0先进制造技术信息网站!
首页 | 制造技术 | 制造设备 | 工业物联网 | 工业材料 | 设备保养维修 | 工业编程 |
home  MfgRobots >> 亿迅智能制造网 >  >> Industrial programming >> VHDL

在块RAM中设计健壮的VHDL环形缓冲区FIFO

环形缓冲区是顺序编程语言中创建队列的常用结构,同样也可以在硬件中实现。本文将介绍如何用VHDL在块RAM中实现环形缓冲区FIFO。

实现FIFO时需要做许多设计决策:需要什么样的接口?资源是否受限?是否需要抗过读和过写能力?延迟是否可接受?这些都是设计FIFO时会考虑的问题。

网上有许多免费的FIFO实现,也有像Xilinx LogiCORE这样的FIFO生成器。但许多工程师仍倾向于自己实现FIFO。因为尽管它们都执行基本的入队和出队任务,但在细节上却可能有巨大差异。

环形缓冲区的工作原理

环形缓冲区是一种FIFO实现,它使用连续内存存储缓冲数据,且数据移动最少。新元素从写入时起保持在同一内存位置,直到被读取并从FIFO中移除。

使用两个计数器跟踪FIFO中数据的位置和数量。这些计数器指向数据存储内存空间起始处的偏移量。在VHDL中,这将是数组单元的索引。本文将这两个计数器称为指针。

这两个指针分别是头指针和尾指针。头指针始终指向将包含下一个写入数据的内存槽,而尾指针指向将从FIFO读取的下一个元素。虽然有其他变体,但我们将使用这种方式。

空状态

在块RAM中设计健壮的VHDL环形缓冲区FIFO

如果头指针和尾指针指向同一元素,表示FIFO为空。上图展示了一个8槽FIFO的例子,头尾指针均指向元素0,表明FIFO为空。这是环形缓冲区的初始状态。

注意,如果两个指针都在另一个索引(如3),FIFO仍为空。每次写入,头指针前移一位;每次读取,尾指针前移一位。

当任一指针到达最高索引时,下一次写入或读取会使指针回绕到最低索引。这就是环形缓冲区的优势:数据不动,只有指针移动。

头指针领先尾指针

在块RAM中设计健壮的VHDL环形缓冲区FIFO

上图显示同一环形缓冲区在五次写入后的状态。尾指针仍在槽0,头指针移至槽5。含数据的槽在图中标为浅蓝色。尾指针指向最老元素,头指针指向下一个空闲槽。

当头指针索引大于尾指针时,可通过头指针减尾指针计算环形缓冲区元素数。上例中得5个元素。

尾指针领先头指针

在块RAM中设计健壮的VHDL环形缓冲区FIFO

仅当头指针领先尾指针时,用头减尾才有效。上图中头指针在索引2,尾指针在索引5,直接相减得2-5=-3,无意义。

解决方法是用FIFO总槽数(本例为8)偏移头指针。计算变为(2+8)-5=5,即正确答案。

尾指针永远追赶头指针,这就是环形缓冲区的工作方式。一半时间尾指针索引大于头指针。数据存储在两者之间,如图中浅蓝色所示。

满状态

在块RAM中设计健壮的VHDL环形缓冲区FIFO

环形缓冲区满时,尾指针指向头指针后的下一个索引。这种方案导致无法使用所有槽存储数据,至少需保留一个空槽。上图显示环形缓冲区满的情况,空闲但不可用的槽标为黄色。

也可用专用的空/满信号指示满状态,这允许所有内存槽存数据,但需要额外寄存器和查找表(LUT)逻辑。因此我们采用保留一个空槽方案实现环形缓冲区FIFO,因为这仅浪费较便宜的块RAM。

环形缓冲区FIFO实现

定义FIFO的接口信号将限制环形缓冲区的可能实现方式。本例使用经典读/写使能和空/满/有效接口的变体。

输入侧有写数据总线,承载要推入FIFO的数据;还有写使能信号,置位时FIFO采样输入数据。

输出侧有FIFO控制的读数据和读有效信号,以及下游用户控制的读使能信号。

空和满控制信号属于经典FIFO接口,我们也使用它们。它们由FIFO控制,用于向读写方传达FIFO状态。

背压

等到FIFO空或满才采取行动的问题在于,接口逻辑没有反应时间。时序逻辑按时钟周期工作,时钟上升沿将设计中的事件有效地分离为时间步长。

在块RAM中设计健壮的VHDL环形缓冲区FIFO

解决方案是包含近空和近满信号,它们比原始信号早一个时钟周期。这给外部逻辑留出反应时间,即使在连续读写时也是如此。

在我们的实现中,这些前导信号命名为empty_next和full_next,只是因为我倾向于用后缀而非前缀命名。

在块RAM中设计健壮的VHDL环形缓冲区FIFO

实体声明

下图展示了我们环形缓冲区FIFO的实体。除端口中的输入输出信号外,还有两个泛型常量。RAM_WIDTH泛型定义输入输出字的位数,即每个内存槽包含的位数。

RAM_DEPTH泛型定义为环形缓冲区预留的槽数。因保留一个槽指示满状态,FIFO容量为RAM_DEPTH - 1。RAM_DEPTH常量应与目标FPGA上的RAM深度匹配。块RAM基元内未使用的RAM将被浪费,无法与FPGA中其他逻辑共享。

 entity ring_buffer is generic ( RAM_WIDTH : natural; RAM_DEPTH : natural ); port ( clk : in std_logic; rst : in std_logic; -- Write port wr_en : in std_logic; wr_data : in std_logic_vector(RAM_WIDTH - 1 downto 0); -- Read port rd_en : in std_logic; rd_valid : out std_logic; rd_data : out std_logic_vector(RAM_WIDTH - 1 downto 0); -- Flags empty : out std_logic; empty_next : out std_logic; full : out std_logic; full_next : out std_logic; -- The number of elements in the FIFO fill_count : out integer range RAM_DEPTH - 1 downto 0 ); end ring_buffer; 

除时钟和复位外,端口声明包含经典的数据/使能读写端口。上游和下游模块用它们向FIFO推入新数据、弹出最老元素。

rd_valid信号在rd_data端口包含有效数据时由FIFO置位。该事件在rd_en脉冲后一个时钟周期发生。文末将详述原因。

接着是FIFO设置的空/满标志。empty_next在剩余1或0个元素时置位,而empty仅在0个元素时激活。同理,full_next指示还能容纳1或0个元素,full仅在无法再容纳数据时置位。

最后是fill_count输出。这是一个整数,反映FIFO当前存储的元素数。我们内部会用到它,通过实体暴露出来基本无额外开销,用户实例化时可选择留空该信号。

声明区

在VHDL文件的声明区,我们将声明自定义类型、子类型、若干信号及供环形缓冲区模块内部使用的过程。

 type ram_type is array (0 to RAM_DEPTH - 1) of std_logic_vector(wr_data'range); signal ram : ram_type; subtype index_type is integer range ram_type'range; signal head : index_type; signal tail : index_type; signal empty_i : std_logic; signal full_i : std_logic; signal fill_count_i : integer range RAM_DEPTH - 1 downto 0; -- Increment and wrap procedure incr(signal index : inout index_type) is begin if index = index_type'high then index <= index_type'low; else index <= index + 1; end if; end procedure; 

首先声明新类型建模RAM。ram_type是向量数组,由泛型输入决定大小。下一行用该类型声明ram信号,保存环形缓冲区数据。

接下来声明index_type,整数子类型。其范围间接受RAM_DEPTH泛型控制。随后用该索引类型声明头、尾指针信号。

然后是实体信号的内部副本声明,基名相同但后缀_i表示内部使用。这种做法避免在实体信号上使用inout模式(虽效果相同但被视为不良风格)。

最后声明incr过程,接受index_type信号参数。该子程序用于头尾指针增量并回绕。头尾指针是整数子类型,通常不支持回绕行为,我们用过程解决此问题。

并发语句

架构体顶部声明并发语句。我习惯将这些单行信号赋值放在普通进程前,因它们易被忽略。并发语句实质是一种进程,详见:

如何在VHDL中创建并发语句

 -- Copy internal signals to output empty <= empty_i; full <= full_i; fill_count <= fill_count_i; -- Set the flags empty_i <= '1' when fill_count_i = 0 else '0'; empty_next <= '1' when fill_count_i <= 1 else '0'; full_i <= '1' when fill_count_i >= RAM_DEPTH - 1 else '0'; full_next <= '1' when fill_count_i >= RAM_DEPTH - 2 else '0'; 

第一块并发赋值将实体信号的内部版本复制到输出。这些行确保实体信号在同一时间跟随内部版本,但在仿真中有一个delta周期延迟。

第二块也是最后一块并发语句赋值输出标志,指示环形缓冲区的满/空状态。计算基于RAM_DEPTH泛型和fill_count信号。RAM深度是不变常量,因此标志仅随填充计数更新而变化。

更新头指针

头指针基本功能是外部写使能信号置位时增量。我们通过将head信号传给前述incr过程实现。

 PROC_HEAD : process(clk) begin if rising_edge(clk) then if rst = '1' then head <= 0; else if wr_en = '1' and full_i = '0' then incr(head); end if; end if; end if; end process; 

代码额外包含and full_i = '0'防止过写。若确定数据源绝不在FIFO满时写入,可省略此逻辑。无保护时,过写会导致环形缓冲区再次变空。

若FIFO满时头指针仍增量,头指针将指向尾指针同一元素,模块将“遗忘”所含数据,FIFO填充显示为空。

在增量头指针前评估full_i信号,仅会遗忘被覆盖的值。我认为这方案更好。但无论如何,若发生过写,表明模块外部有故障。

更新尾指针

尾指针增量方式类似,但以read_en输入为触发。同理,包含and empty_i = '0'防止过读。

 PROC_TAIL : process(clk) begin if rising_edge(clk) then if rst = '1' then tail <= 0; rd_valid <= '0'; else rd_valid <= '0'; if rd_en = '1' and empty_i = '0' then incr(tail); rd_valid <= '1'; end if; end if; end if; end process; 

此外,每次有效读取时脉冲rd_valid信号。读数据在rd_en置位后下一个时钟周期有效(若FIFO非空)。虽此信号非必需,但为方便保留。实例化模块时若留空,综合会优化掉rd_valid。

推断块RAM

要让综合工具推断块RAM,必须在无复位的同步进程中声明读写端口。我们每个时钟周期读写RAM,由控制信号处理数据使用。

 PROC_RAM : process(clk) begin if rising_edge(clk) then ram(head) <= wr_data; rd_data <= ram(tail); end if; end process; 

此进程不知下次写入何时发生,但无需知道。我们持续写入。当head因写入而增量时,开始写入下一槽,有效锁存写入的值。

更新填充计数

fill_count信号用于生成满/空信号,进而防止FIFO过写和过读。填充计数器由组合进程更新,敏感于头尾指针,但这些信号仅在时钟上升沿更新,因此填充计数也在时钟沿后立即变化。

 PROC_COUNT : process(head, tail) begin if head < tail then fill_count_i <= head - tail + RAM_DEPTH; else fill_count_i <= head - tail; end if; end process; 

填充计数即头指针减尾指针。若尾指针索引大于头指针,需加上RAM_DEPTH常量得当前环形缓冲区元素数。

环形缓冲区FIFO完整VHDL代码

 library ieee; use ieee.std_logic_1164.all; entity ring_buffer is generic ( RAM_WIDTH : natural; RAM_DEPTH : natural ); port ( clk : in std_logic; rst : in std_logic; -- Write port wr_en : in std_logic; wr_data : in std_logic_vector(RAM_WIDTH - 1 downto 0); -- Read port rd_en : in std_logic; rd_valid : out std_logic; rd_data : out std_logic_vector(RAM_WIDTH - 1 downto 0); -- Flags empty : out std_logic; empty_next : out std_logic; full : out std_logic; full_next : out std_logic; -- The number of elements in the FIFO fill_count : out integer range RAM_DEPTH - 1 downto 0 ); end ring_buffer; architecture rtl of ring_buffer is type ram_type is array (0 to RAM_DEPTH - 1) of std_logic_vector(wr_data'range); signal ram : ram_type; subtype index_type is integer range ram_type'range; signal head : index_type; signal tail : index_type; signal empty_i : std_logic; signal full_i : std_logic; signal fill_count_i : integer range RAM_DEPTH - 1 downto 0; -- Increment and wrap procedure incr(signal index : inout index_type) is begin if index = index_type'high then index <= index_type'low; else index <= index + 1; end if; end procedure; begin -- Copy internal signals to output empty <= empty_i; full <= full_i; fill_count <= fill_count_i; -- Set the flags empty_i <= '1' when fill_count_i = 0 else '0'; empty_next <= '1' when fill_count_i <= 1 else '0'; full_i <= '1' when fill_count_i >= RAM_DEPTH - 1 else '0'; full_next <= '1' when fill_count_i >= RAM_DEPTH - 2 else '0'; -- Update the head pointer in write PROC_HEAD : process(clk) begin if rising_edge(clk) then if rst = '1' then head <= 0; else if wr_en = '1' and full_i = '0' then incr(head); end if; end if; end if; end process; -- Update the tail pointer on read and pulse valid PROC_TAIL : process(clk) begin if rising_edge(clk) then if rst = '1' then tail <= 0; rd_valid <= '0'; else rd_valid <= '0'; if rd_en = '1' and empty_i = '0' then incr(tail); rd_valid <= '1'; end if; end if; end if; end process; -- Write to and read from the RAM PROC_RAM : process(clk) begin if rising_edge(clk) then ram(head) <= wr_data; rd_data <= ram(tail); end if; end process; -- Update the fill count PROC_COUNT : process(head, tail) begin if head < tail then fill_count_i <= head - tail + RAM_DEPTH; else fill_count_i <= head - tail; end if; end process; end architecture; 

上述代码展示了环形缓冲区FIFO的完整代码。填写下方表单可立即获取ModelSim工程文件及测试平台。

测试平台

FIFO在简单测试平台中实例化以演示工作原理。可通过下方表单下载测试平台源码及ModelSim工程。

泛型输入设为:

测试平台先复位FIFO。复位释放后,向FIFO写入顺序值(1-255)直到满。最后在测试完成前清空FIFO。

下图显示测试平台完整运行的波形。fill_count信号在波形中以模拟值显示,更直观展示FIFO填充电平。

在块RAM中设计健壮的VHDL环形缓冲区FIFO

仿真开始时头、尾指针和填充计数均为0。full信号置位时,头指针值为255,fill_count也为255。填充计数仅达255尽管RAM深度为256,因我们用保留一个空槽法区分满空,如前所述。

在停止写入转为读取的转折点,头指针值冻结,尾指针和填充计数开始递减。仿真结束时FIFO为空,头尾指针均为255,填充计数为0。

此测试平台仅供演示,不具备自检行为或验证FIFO输出正确性的逻辑。

下周文章将使用此模块深入探讨约束随机验证。这是一种不同于常用定向测试的策略。简言之,测试平台将与DUT(被测器件)进行随机交互,DUT行为需由单独测试平台进程验证。最后,当预定义事件发生一定次数后,测试完成。

点击阅读后续博文:
约束随机验证

在Vivado中综合

我在Xilinx Vivado中综合了环形缓冲区,因它是最流行的FPGA实现工具。但应适用于所有具有双端口块RAM的FPGA架构。

要将环形缓冲区作为独立模块实现,必须给泛型输入赋值。Vivado中通过Settings → General → Generics/Parameters菜单设置,如下图所示。

在块RAM中设计健壮的VHDL环形缓冲区FIFO

RAM_WIDTH设为16,同仿真。但RAM_DEPTH设为2048,因这是我选用的Xilinx Zynq架构中RAMB36E1基元的最大深度。选较低值仍使用同等数量块RAM;较高值会导致使用多个块RAM。

下图显示Vivado报告的实现后资源使用。环形缓冲区确实消耗一个块RAM和少量LUT及触发器。

在块RAM中设计健壮的VHDL环形缓冲区FIFO

省去valid信号

你可能会问:rd_en和rd_valid间一个时钟周期延迟真有必要吗?毕竟置位rd_en时数据已在rd_data上。能否直接使用该值,让环形缓冲区在读取时跳到下一元素?

在块RAM中设计健壮的VHDL环形缓冲区FIFO

严格说,valid信号非必需。我保留它仅为方便。关键是必须等到置位rd_en后的时钟周期,否则RAM没时间反应。

FPGA块RAM是全同步组件,读写都需要时钟沿。读写时钟可来自不同源,但必须有时钟沿。且RAM输出到下一级寄存器(触发器)间不能有逻辑,因为给RAM输出打拍的寄存器在块RAM基元内部。

在块RAM中设计健壮的VHDL环形缓冲区FIFO

上图展示数值从我们环形缓冲区的wr_data输入,经RAM传播,最终出现在rd_data输出的时序图。因每个信号在上升沿采样,从驱动写端口到出现在读端口需三个时钟周期。接收模块利用该数据还需额外一个周期。

降低延迟

有方法缓解此问题,但以额外FPGA资源为代价。试着从环形缓冲区读端口减少一个时钟周期延迟。下面代码片段将rd_data输出从同步进程改为敏感于ram和tail信号的组合进程。

 PROC_READ : process(ram, tail) begin rd_data <= ram(tail); end process; 

遗憾的是,此代码无法映射到块RAM,因为RAM输出与rd_data上第一个下游寄存器间可能有组合逻辑。

下图显示Vivado报告的资源使用。块RAM被LUTRAM取代——一种用LUT实现的分布式RAM。LUT用量从37飙升至947。查找表和触发器比块RAM昂贵,这正是我们有块RAM的原因。

在块RAM中设计健壮的VHDL环形缓冲区FIFO

用块RAM实现环形缓冲区FIFO有很多方法。或许用其他设计能省掉额外时钟周期,但会增加额外支持逻辑。对大多数应用,本文提出的环形缓冲区已足够。

更新:
如何使用AXI ready/valid握手在块RAM中创建环形缓冲区FIFO

下篇博文将用约束随机验证为环形缓冲区模块创建更好的测试平台。

点击阅读后续博文:
约束随机验证


VHDL

  1. VHDL 字符串列表创建:最佳实践与示例
  2. 基于 Tcl 驱动的 VHDL 密码锁模块测试平台
  3. 优雅结束 VHDL 测试平台仿真
  4. VHDL 实现 PWM 控制器:设计、仿真与 FPGA 演示
  5. VHDL 生成随机数:从均匀分布到 OSVVM
  6. 构建VHDL自检测试平台实战指南
  7. 用VHDL受保护类型和访问指针实现动态链表
  8. 利用进程内过程简化VHDL状态机设计
  9. 使用VHDL不纯函数提升FSM可读性与可维护性
  10. 掌握VHDL函数:高效设计实战指南
  11. 用VHDL设计有限状态机:红绿灯实战范例
  12. 在VHDL中使用过程:用可复用代码简化设计