Altifigence Academy

55 / 63 · 概念

同步存储器与读写冲突约定

区分寄存读取的响应时刻与 read-first、write-first、no-change,分别设计 valid 和地址初始化的职责。

比地址更先要确定的是响应时刻

连接存储器时,除容量外,还必须规定读取延迟与冲突行为。这里设计一个 16 地址、每地址存八位的同步存储器,可在同一上升沿接受一次读与一次写。两个请求共用时钟,但地址独立。容量为 16×8=128 位,地址宽度为 log⁡216=4\log_2 16=4 位。

若读地址与 rd_en 在上升沿之前稳定,则该边沿后产生寄存后的 rd_data 和 rd_valid,称为一级寄存读取延迟。其他 always_ff 若在同一边沿读取这些输出,看到的是更新前值,因此消费响应的寄存器寄存器 存储多位状态的电路。本课程中的同步寄存器在时钟边沿存储指定输入。了解更多在下一个上升沿接收。应在波形上分别标记请求时刻与响应观察时刻。每个时钟都可接受新请求,因此一级延迟并不意味着吞吐率吞吐率 单位时间内接受或完成的工作数量。流水线即使不缩短单个任务的延迟,也可以提高吞吐率。了解更多只有每两周期一次。

同一地址同时读写时

假设边沿前 M[3]=8'h25,在读取地址 3 的同时写入 8'h9A。read-first 返回 25,并将存储值改为 9A;write-first 返回新值 9A;no-change 保持输出寄存器,因此不能保证其输出是地址 3 的旧值。这三者是不同的正常行为约定。AMD RAM 同步模式文档也区分这三种模式。

本例采用 read-first。用 M−M^{-} 表示边沿前数组、M+M^{+} 表示边沿后数组,则在不复位的边沿,对任意地址 aa 有:

M+[a]={wr_data,wr_en=1∧a=wr_addrM−[a],otherwiseM^{+}[a]=\begin{cases} \mathrm{wr\_data}, & \mathrm{wr\_en}=1\land a=\mathrm{wr\_addr}\\ M^{-}[a], & \text{otherwise} \end{cases}
rd_data+={M−[rd_addr],rd_en=1rd_data−,rd_en=0rd_valid+=rd_en\begin{aligned} \mathrm{rd\_data}^{+}&=\begin{cases} M^{-}[\mathrm{rd\_addr}], & \mathrm{rd\_en}=1\\ \mathrm{rd\_data}^{-}, & \mathrm{rd\_en}=0 \end{cases}\\ \mathrm{rd\_valid}^{+}&=\mathrm{rd\_en} \end{aligned}

即使发生冲突,读取仍使用 M−M^{-},而非 M+M^{+}。没有读请求时,rd_data 保持。

SystemVerilog
module sync_mem16x8 (
  input  logic       clk, rst,
  input  logic       wr_en, rd_en,
  input  logic [3:0] wr_addr, rd_addr,
  input  logic [7:0] wr_data,
  output logic [7:0] rd_data,
  output logic       rd_valid
);
  timeunit 1ns;
  timeprecision 1ps;
  logic [7:0] mem [0:15];

  always_ff @(posedge clk) begin
    if (rst) begin
      rd_valid <= 1'b0;
    end else begin
      rd_valid <= rd_en;
      if (wr_en)
        mem[wr_addr] <= wr_data;
      if (rd_en)
        rd_data <= mem[rd_addr];
    end
  end
endmodule

两条赋值的右侧都使用更新前值,非阻塞非阻塞赋值 时序 RTL 中以 <= 表示的赋值。先计算右侧,再预约更新,使同一边沿的各寄存器能够依据旧状态计算。了解更多赋值随后才生效。因此把写语句放在读语句前,也不会变成读取新值。若需要 write-first,可将读取表达式改为 (wr_en && wr_addr==rd_addr) ? wr_data : mem[rd_addr],明确表达意图。这条旁路仍保留读输出寄存器。

valid 与初始化是两种不同保证

rst 为同步高有效复位。上升沿 rst=1 时,忽略两个请求,只将 rd_valid 置 0,不清零数组和 rd_data。本约定不定义上电后从未写入地址的内容。仿真可能显示 X,实际硬件也不能假定某个特定值。

rd_valid=1 表示“当前周期是读请求的响应”,并不表示“该地址已初始化且值已知”。使用方应只读取先前写过的地址,或维护独立的逐地址初始化表。首次写入与 read-first 读取同一地址发生在同一边沿时,读出的旧内容仍未定义。还应以 valid 为使用条件,valid=0 时不将 rd_data 用于计算。“只读取已初始化地址”和“以 valid 判断响应”不能互相替代。

reset 不清除存储,因此已写数据在 RTL 语义上保留。但若系统规定复位后丢弃旧内容,外部初始化表也需相应重新管理。清除敏感信息的要求与控制复位是独立设计项。

每列记录一个上升沿上升沿 时钟从 0 变为 1 的瞬间。它不同于表示整个 CLK=1 区间的电平。了解更多的观察结果。输入取自边沿前,名称带 after 的状态取自更新后。列的排列表示采样顺序,并非描绘物理传播延迟传播延迟 从输入变化到输出稳定为正确值所需的时间。逻辑表达式等价与时间特性是两回事。了解更多。所有读写地址均为 3。E0 首次写入 25,E1 改写为 9A 并同时读取。data 中的破折号表示未定义值;valid=0 的总线不计作响应。数据以十六进制表示。

read-first 冲突时读取旧值
查看波形数据
波形数据:每个字符表示一个区间,点表示保持前一状态,p 表示一个时钟周期。
信号波形总线值
edge2345E0 → E1 → E2 → E3
wr_en1.0.
wr_data23..25 → 9A
rd_en01.0
M[3] after23..25 → 9A
rd_data after234.- → 25 → 9A
rd_valid after01.0

手动追踪四个边沿

E0 向地址 3 写入 25,rd_en=0。E1 向地址 3 写入 9A 并读取同一地址,E1 后 valid=1、data=25。E2 不写入,再次读取地址 3,得到 data=9A。E3 若 rd_en=0,则 valid=0,data 保持 9A。不能因为 E3 输出总线仍为 9A,就计为新响应,否则会把同一数据处理两次。

地址不同时,读写独立。例如读取地址 7 并写入地址 3,返回的是地址 7 的旧值。实际 FPGA 将此 RTL 映射到何种 RAM 资源,以及该资源如何处理同址冲突,需通过综合报告与器件文档确认。定义 RTL 语义与验证特定器件上的实现,是两个不同阶段。

自己试试

M[6] 已存有 8'h10。E0 读取地址 6,同时写入 8'h20;E1 不写入,再次读取;E2 令 rd_en=0。求 read-first 模式各边沿后的 data/valid。若改为 write-first,哪里会变化?从未写入地址的首次 read-first 冲突是否返回已定义值?

阅读解释

E0 后 data=10、valid=1,数组更新为 20。E1 后 data=20、valid=1。E2 保持 data=20,但 valid=0,不能计作响应。改为 write-first 后,只有 E0 的 data 变为 20。首次写入重叠的 read-first 读取返回的是写入前未初始化内容,因此不能指定某个值作为答案。valid=1 也不保证已初始化。

你的选择适用于此浏览器,可随时在页脚更改。