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

如何使用就绪/有效握手在块 RAM 中创建 AXI FIFO

当我第一次必须创建逻辑来连接 AXI 模块时,我对 AXI 接口的特殊性有点恼火。代替常规的忙/有效、满/有效或空/有效控制信号,AXI 接口使用名为“就绪”和“有效”的两个控制信号。我的沮丧很快变成了敬畏。

AXI 接口具有内置流量控制,无需使用额外的控制信号。这些规则很容易理解,但在 FPGA 上实现 AXI 接口时必须考虑一些陷阱。本文将向您展示如何在 VHDL 中创建 AXI FIFO。

AXI 解决了延迟一个周期的问题

防止过度读取和覆盖是创建数据流接口时的常见问题。问题是当两个时钟逻辑模块进行通信时,每个模块只能以一个时钟周期延迟读取其对应模块的输出。

上图显示了顺序模块写入使用 write enable/full 的 FIFO 的时序图 信令方案。接口模块通过断言 wr_en 将数据写入 FIFO 信号。 FIFO 将断言 full 当其他数据元素没有空间时发出信号,提示数据源停止写入。

不幸的是,只要接口模块只使用时钟逻辑,它就无法及时停止。 FIFO 提高 full 标志正好在时钟的上升沿。同时,接口模块尝试写入下一个数据元素。它无法对 full 进行采样和反应 在为时已晚之前发出信号。

一种解决方案是包含一个额外的 almost_empty 信号,我们在 How to create a ring buffer FIFO in VHDL 教程中做到了这一点。 empty 之前的附加信号 信号,让接口模块有时间做出反应。

就绪/有效握手

AXI 协议在每个方向上仅使用两个控制信号实现流控制,一个称为 ready 和另一个 valid . ready 信号由接收器控制,逻辑 '1' 此信号上的值表示接收器已准备好接受新数据项。 valid 另一方面,信号由发送方控制。发送方应设置valid'1' 当数据总线上呈现的数据有效采样时。

重要的部分来了: 只有当 readyvalid'1' 在同一个时钟周期。接收者通知它何时准备好接受数据,发送者只需在有东西要传输时将数据放在那里。当双方同意,即发送方准备好发送而接收方准备接收时,就会发生传输。

上面的波形显示了一个数据项的示例事务。采样发生在时钟上升沿,时钟逻辑通常就是这种情况。

实施

有多种方法可以在 VHDL 中实现 AXI FIFO。它可以是移位寄存器,但我们将使用环形缓冲区结构,因为它是在块 RAM 中创建 FIFO 的最直接方法。您可以使用变量和信号在一个巨大的流程中创建所有功能,也可以将功能拆分为多个流程。

此实现对大多数必须更新的信号使用单独的进程。只有需要同步的进程对时钟敏感,其他的使用组合逻辑。

实体

实体声明包括一个通用端口,用于设置输入和输出字的宽度,以及在 RAM 中预留空间的插槽数。 FIFO 的容量等于 RAM 深度减一。一个槽始终保持为空,以区分 FIFO 是满还是空。

entity axi_fifo is
  generic (
    ram_width : natural;
    ram_depth : natural
  );
  port (
    clk : in std_logic;
    rst : in std_logic;

    -- AXI input interface
    in_ready : out std_logic;
    in_valid : in std_logic;
    in_data : in std_logic_vector(ram_width - 1 downto 0);

    -- AXI output interface
    out_ready : in std_logic;
    out_valid : out std_logic;
    out_data : out std_logic_vector(ram_width - 1 downto 0)
  );
end axi_fifo; 

端口声明中的前两个信号是时钟和复位输入。该实现采用同步复位,对时钟上升沿敏感。

有一个使用就绪/有效控制信号和通用宽度输入数据信号的 AXI 风格输入接口。最后是 AXI 输出接口,其信号与输入信号相似,只是方向相反。属于输入输出接口的信号以in_为前缀 或 out_ .

一个 AXI FIFO 的输出可以直接连接到另一个 AXI FIFO 的输入,接口完美地结合在一起。虽然,比堆叠它们更好的解决方案是增加 ram_depth 如果您想要更大的 FIFO,则为通用。

信号声明

VHDL 文件声明区域中的前两个语句声明 RAM 类型及其信号。 RAM 是根据通用输入动态调整大小的。

-- The FIFO is full when the RAM contains ram_depth - 1 elements
type ram_type is array (0 to ram_depth - 1)
  of std_logic_vector(in_data'range);
signal ram : ram_type;

第二个代码块声明了一个新的整数子类型和来自它的四个信号。 index_type 大小可以准确地表示 RAM 的深度。 head 信号始终指示将在下一次写操作中使用的 RAM 插槽。 tail 信号指向将在下一次读取操作中访问的插槽。 count 的值 信号总是等于当前存储在 FIFO 中的元素数量,而 count_p1 是延迟一个时钟周期的同一信号的副本。

-- Newest element at head, oldest element at tail
subtype index_type is natural range ram_type'range;
signal head : index_type;
signal tail : index_type;
signal count : index_type;
signal count_p1 : index_type;

然后是两个名为 in_ready_i 的信号 和 out_valid_i .这些只是实体输出 in_ready 的副本 和 out_valid . _i 后缀只是表示内部 ,这是我编码风格的一部分。

-- Internal versions of entity signals with mode "out"
signal in_ready_i : std_logic;
signal out_valid_i : std_logic;

最后,我们声明一个用于指示同时读取和写入的信号。我将在本文后面解释它的目的。

-- True the clock cycle after a simultaneous read and write
signal read_while_write_p1 : std_logic;

子程序

在信号之后,我们声明一个函数来增加我们自定义的 index_type . next_index 函数查看 readvalid 参数来确定是否有正在进行的读或读/写事务。如果是这种情况,索引将被递增或包装。如果不是,则返回不变的索引值。

function next_index(
  index : index_type;
  ready : std_logic;
  valid : std_logic) return index_type is
begin
  if ready = '1' and valid = '1' then
    if index = index_type'high then
      return index_type'low;
    else
      return index + 1;
    end if;
  end if;

  return index;
end function;

为了避免重复输入,我们创建了更新 head 的逻辑 和 tail 在一个过程中发出信号,而不是作为两个相同的过程。 update_index 程序接受时钟和复位信号,index_type的信号 , 一个 ready 信号和一个 valid 信号作为输入。

procedure index_proc(
  signal clk : in std_logic;
  signal rst : in std_logic;
  signal index : inout index_type;
  signal ready : in std_logic;
  signal valid : in std_logic) is
begin
    if rising_edge(clk) then
      if rst = '1' then
        index <= index_type'low;
      else
        index <= next_index(index, ready, valid);
      end if;
    end if;
end procedure;

这个完全同步的过程使用 next_index 更新index的函数 当模块未复位时发出信号。重置时,index 信号将被设置为它可以表示的最低值,因为 index_typeram_type 被宣布。我们本可以使用 0 作为重置值,但我尽量避免硬编码。

将内部信号复制到输出

这两个并发语句将输出信号的内部版本复制到实际输出。我们需要对内部副本进行操作,因为 VHDL 不允许我们以 out 模式读取实体信号 模块内部。另一种方法是声明 in_readyout_valid 使用模式 inout ,但大多数公司编码标准限制使用 inout 实体信号。

in_ready <= in_ready_i;
out_valid <= out_valid_i;

更新头部和尾部

我们已经讨论过 index_proc 用于更新 head 的过程 和 tail 信号。通过将适当的信号映射到该子程序的参数,我们得到了两个相同的过程,一个用于控制FIFO输入,一个用于输出。

-- Update head index on write
PROC_HEAD : index_proc(clk, rst, head, in_ready_i, in_valid);

-- Update tail index on read
PROC_TAIL : index_proc(clk, rst, tail, out_ready, out_valid_i);

由于 headtail 由复位逻辑设置为相同的值,FIFO 最初将为空。这就是这个环形缓冲区的工作原理,当两者都指向同一个索引时,这意味着 FIFO 是空的。

推断块 RAM

在大多数 FPGA 架构中,块 RAM 原语是完全同步的组件。这意味着,如果我们希望综合工具从我们的 VHDL 代码中推断出块 RAM,我们需要将读取和写入端口置于时钟进程内。此外,不能有与块 RAM 相关的复位值。

PROC_RAM : process(clk)
begin
  if rising_edge(clk) then
    ram(head) <= in_data;
    out_data <= ram(next_index(tail, out_ready, out_valid_i));
  end if;
end process;

没有读取启用写启用 在这里,这对于 AXI 来说太慢了。相反,我们不断地写入 head 指向的 RAM 插槽 指数。然后,当我们确定发生了写事务时,我们只需推进 head 锁定写入值。

同样,out_data 在每个时钟周期更新。 tail 当读取发生时,指针只是移动到下一个插槽。请注意,next_index 函数用于计算读取端口的地址。我们必须这样做以确保 RAM 在读取后反应足够快并开始输出下一个值。

计算FIFO中的元素个数

计算 RAM 中的元素数量只需减去 headtail .如果 head 已经包装好了,我们必须用 RAM 中的插槽总数来抵消它。我们可以通过 ram_depth 访问这些信息 来自通用输入的常量。

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

我们还需要跟踪 count 的先前值 信号。下面的过程创建了一个延迟一个时钟周期的版本。 _p1 后缀是表示这一点的命名约定。

PROC_COUNT_P1 : process(clk)
begin
  if rising_edge(clk) then
    if rst = '1' then
      count_p1 <= 0;
    else
      count_p1 <= count;
    end if;
  end if;
end process;

更新准备好了 输出

in_ready 信号应为 '1' 当此模块准备好接受另一个数据项时。只要 FIFO 未满就应该是这种情况,而这正是这个过程的逻辑所说的。

PROC_IN_READY : process(count)
begin
  if count < ram_depth - 1 then
    in_ready_i <= '1';
  else
    in_ready_i <= '0';
  end if;
end process;

检测同时读写

由于我将在下一节中解释的一个极端情况,我们需要能够识别同时读取和写入操作。每次在同一个时钟周期内有有效的读写事务,这个过程都会设置read_while_write_p1'1' 发出信号 在下一个时钟周期。

PROC_READ_WHILE_WRITE_P1: process(clk)
begin
  if rising_edge(clk) then
    if rst = '1' then
      read_while_write_p1 <= '0';

    else
      read_while_write_p1 <= '0';
      if in_ready_i = '1' and in_valid = '1' and
        out_ready = '1' and out_valid_i = '1' then
        read_while_write_p1 <= '1';
      end if;
    end if;
  end if;
end process;

更新有效 输出

out_valid 信号向下游模块指示 out_data 上呈现的数据 是有效的,可以随时采样。 out_data 信号直接来自 RAM 输出。实现 out_valid 由于 Block RAM 输入和输出之间存在额外的时钟周期延迟,因此信号有点棘手。

该逻辑是在一个组合过程中实现的,因此它可以对不断变化的输入信号做出无延迟的反应。该过程的第一行是设置 out_valid 的默认值 向 '1' 发出信号 .如果两个后续 If 语句均未触发,则这将是当前值。

PROC_OUT_VALID : process(count, count_p1, read_while_write_p1)
begin
  out_valid_i <= '1';

  -- If the RAM is empty or was empty in the prev cycle
  if count = 0 or count_p1 = 0 then
    out_valid_i <= '0';
  end if;

  -- If simultaneous read and write when almost empty
  if count = 1 and read_while_write_p1 = '1' then
    out_valid_i <= '0';
  end if;

end process;

第一个 If 语句检查 FIFO 是否为空或在前一个时钟周期中为空。很明显,当FIFO中有0个元素时,它是空的,但是我们还需要检查上一个时钟周期的FIFO的填充程度。

考虑下面的波形。最初,FIFO 是空的,如 count 所示 信号为 0 .然后,在第三个时钟周期发生写入。 RAM 插槽 0 在下一个时钟周期更新,但在 out_data 上出现数据之前需要一个额外的周期 输出。 or count_p1 = 0 的用途 声明是确保 out_valid 仍然是 '0' (以红色圈出)而值通过 RAM 传播。

最后一个 If 语句防止另一个极端情况。我们刚刚讨论了如何通过检查当前和以前的 FIFO 填充级别来处理空时写入的特殊情况。但是如果我们在 count 时同时执行读写操作会发生什么? 已经是 1 ?

下面的波形显示了这种情况。最初,FIFO 中存在一个数据项 D0。它已经存在了一段时间,所以 countcount_p10 .然后在第三个时钟周期同时进行读取和写入。一个项目离开 FIFO,一个新项目进入,使计数器保持不变。

在读取和写入的时刻,RAM 中没有准备好输出的下一个值,如果填充水平高于 1 就会出现。在输入值出现在输出上之前,我们必须等待两个时钟周期。如果没有任何附加信息,就无法检测到这种极端情况,以及 out_valid 的值 在下一个时钟周期(标记为纯红色)将错误地设置为 '1' .

这就是为什么我们需要 read_while_write_p1 信号。它检测到有同时读取和写入,我们可以通过设置 out_valid 来考虑这一点 到 '0' 在那个时钟周期内。

在 Vivado 中综合

要将设计实现为 Xilinx Vivado 中的独立模块,我们首先必须为通用输入赋值。这可以在 Vivado 中通过使用 Settings 来实现 → 一般泛型/参数 菜单,如下图所示。

已选择通用值以匹配作为目标器件的 Xilinx Zynq 架构中的 RAMB36E1 原语。实施后的资源使用情况如下图所示。 AXI FIFO 使用一个块 RAM 和少量 LUT 和触发器。

AXI 已准备就绪/有效

AXI 代表高级可扩展接口,它是 ARM 高级微控制器总线架构 (AMBA) 标准的一部分。 AXI 标准不仅仅是读取/有效握手。如果您想了解更多关于 AXI 的信息,我推荐这些资源以供进一步阅读:


VHDL

  1. 云及其如何改变 IT 世界
  2. 如何充分利用您的数据
  3. 如何使用 TEXTIO 从文件初始化 RAM
  4. 如何为使用 IoT 的 AI 做好准备
  5. 工业互联网如何改变资产管理
  6. 资产跟踪最佳实践:如何充分利用来之不易的资产数据
  7. 我们如何更好地了解物联网?
  8. 如何在餐厅业务中充分利用物联网
  9. 数据如何支持未来的供应链
  10. 如何使供应链数据可信
  11. AI 如何解决“脏”数据的问题
  12. 如何使用数据使您的维护供应链更有效