Altifigence Academy

55 / 63 · 概念

同期メモリと読み書き競合の契約

レジスタ付き読み出しの応答時点と read-first・write-first・no-change を区別し、valid とアドレス初期化の役割を分けて設計する。

アドレスより先に決めること:応答の時点

メモリを接続するときは、容量だけでなく読み出しの遅延と競合時の動作も決める。ここでは、16 個のアドレスにそれぞれ 8 ビットを格納し、一つの立ち上がりエッジで読み出し一つと書き込み一つを受け付ける同期メモリを作る。二つの要求は同じクロックを使うが、アドレスは独立している。容量は 16×8=128 ビット、アドレス幅は log⁡216=4\log_2 16=4 ビットである。

読み出しアドレスと rd_en を立ち上がりエッジ前に安定させると、そのエッジ後にレジスタ出力 rd_data と rd_valid が得られる。これをレジスタ付き読み出し 1 段分の遅延と呼ぶ。別の always_ff が同じエッジでこの出力を読むと更新前の値を見るので、応答を消費するレジスタレジスタ 複数ビットの状態を記憶する回路です。このコースの同期式レジスタは、クロックエッジで定められた入力を記憶します。 詳しく見るは次の立ち上がりエッジで受け取る。要求時点と応答の観測時点は波形に分けて記す。新しい要求を毎クロック受け付けられるため、1 段の遅延がスループットスループット 単位時間に受け入れる、または完了する処理の数です。パイプラインは個々の処理の遅延を短くしなくても、スループットを高められます。 詳しく見る 1/2 を意味するわけではない。

同じアドレスを読み書きすると

エッジ直前に 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^{+} をエッジ後の配列とすると、reset でないエッジでは任意のアドレス 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 はアクティブ High の同期 reset である。立ち上がりエッジで rst=1 なら二つの要求を無視し、rd_valid だけを 0 にする。配列と rd_data を 0 で埋めることはない。電源投入後に一度も書かれていないアドレスの内容は、この契約では定義しない。シミュレータでは X が見える場合があり、実際のハードウェアでも特定の数値を仮定できない。

rd_valid=1 は「読み出し要求への応答サイクル」を意味し、「アドレスが初期化済みで値が既知」を意味しない。利用側は、書き込み済みのアドレスだけを読むか、アドレスごとの初期化表を別途持つ必要がある。最初の書き込みと同じエッジでそのアドレスを read-first で読んでも、以前の内容は依然として未定義である。valid=0 のときに rd_data を計算へ使わない valid ガードも必要になる。初期化済みアドレスだけを読む条件と valid ガードは、互いの代わりにはならない。

reset は記憶内容を消さないため、すでに書いた値は RTL 上では残る。ただし、システムが reset 後に以前の内容を破棄する方針なら、外部の初期化表もそれに合わせて管理する。機密情報の消去は、制御 reset とは別の設計要件である。

各列は一つの立ち上がりエッジ立上りエッジ クロックが 0 から 1 に変わる瞬間です。CLK=1 の区間全体を指すレベルとは区別します。 詳しく見るの観測記録である。入力はエッジ直前、名前に「更新後」が付く状態は更新直後の値を示す。列の整列はサンプル順を示し、物理的な伝搬遅延伝搬遅延 入力が変わってから、出力が正しい値に安定するまでの時間です。論理式の等価性と時間特性は別の性質です。 詳しく見るを描いたものではない。読み書きのアドレスはすべて 3 である。E0 で初めて 25 を書き、E1 で 9A に上書きしながら読む。data のダッシュは未定義値を表し、valid=0 のバスは応答として数えない。データ表記は 16 進数である。

read-first 競合で以前の値を読む
波形データを表示
波形データ:各文字は一つの区間、ドットは前の状態の保持、p はクロック 1 周期を表します。
信号波形バスの値
エッジ2345E0 → E1 → E2 → E3
wr_en1.0.
wr_data23..25 → 9A
rd_en01.0
更新後 M[3]23..25 → 9A
更新後 rd_data234.- → 25 → 9A
更新後 rd_valid01.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 も初期化を保証しない。

選択はこのブラウザに適用されます。フッターからいつでも変更できます。