FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

common_cells リポジトリに含まれる stream_モジュールの構成分析 (5. ストリーム・インタフェースのFork / Joinモジュール)

stream_fork.sv

1つの入力ストリームを複数の出力ストリームへ分配するモジュールである。 各入力ハンドシェイクに対して、すべての出力が正確に1回ハンドシェイクする。 入力はすべての出力がハンドシェイクした後にのみハンドシェイクするが、出力は同時にハンドシェイクする必要はない。

// Stream fork: Connects the input stream (ready-valid) handshake to *all* of `N_OUP` output stream
// handshakes. For each input stream handshake, every output stream handshakes exactly once. The
// input stream only handshakes when all output streams have handshaked, but the output streams do
// not have to handshake simultaneously.
//
// This module has no data ports because stream data does not need to be forked: the data of the
// input stream can just be applied at all output streams.

`include "common_cells/assertions.svh"

module stream_fork #(
    parameter int unsigned N_OUP = 0    // Synopsys DC requires a default value for parameters.
) (
    input  logic                clk_i,
    input  logic                rst_ni,
    input  logic                valid_i,
    output logic                ready_o,
    output logic [N_OUP-1:0]    valid_o,
    input  logic [N_OUP-1:0]    ready_i
);

    typedef enum logic {READY, WAIT} state_t;

    logic [N_OUP-1:0]   oup_ready,
                        all_ones;

    state_t inp_state_d, inp_state_q;

    // Input control FSM
    always_comb begin
        // ready_o     = 1'b0;
        inp_state_d = inp_state_q;

        unique case (inp_state_q)
            READY: begin
                if (valid_i) begin
                    if (valid_o == all_ones && ready_i == all_ones) begin
                        // If handshake on all outputs, handshake on input.
                        ready_o = 1'b1;
                    end else begin
                        ready_o = 1'b0;
                        // Otherwise, wait for inputs that did not handshake yet.
                        inp_state_d = WAIT;
                    end
                end else begin
                    ready_o = 1'b0;
                end
            end
            WAIT: begin
                if (valid_i && oup_ready == all_ones) begin
                    ready_o = 1'b1;
                    inp_state_d = READY;
                end else begin
                    ready_o = 1'b0;
                end
            end
            default: begin
                inp_state_d = READY;
                ready_o = 1'b0;
            end
        endcase
    end

    always_ff @(posedge clk_i, negedge rst_ni) begin
        if (!rst_ni) begin
            inp_state_q <= READY;
        end else begin
            inp_state_q <= inp_state_d;
        end
    end

    // Output control FSM
    for (genvar i = 0; i < N_OUP; i++) begin: gen_oup_state
        state_t oup_state_d, oup_state_q;

        always_comb begin
            oup_ready[i]    = 1'b1;
            valid_o[i]      = 1'b0;
            oup_state_d     = oup_state_q;

            unique case (oup_state_q)
                READY: begin
                    if (valid_i) begin
                        valid_o[i] = 1'b1;
                        if (ready_i[i]) begin   // Output handshake
                            if (!ready_o) begin     // No input handshake yet
                                oup_state_d = WAIT;
                            end
                        end else begin          // No output handshake
                            oup_ready[i] = 1'b0;
                        end
                    end
                end
                WAIT: begin
                    if (valid_i && ready_o) begin   // Input handshake
                        oup_state_d = READY;
                    end
                end
                default: begin
                    oup_state_d = READY;
                end
            endcase
        end

        always_ff @(posedge clk_i, negedge rst_ni) begin
            if (!rst_ni) begin
                oup_state_q <= READY;
            end else begin
                oup_state_q <= oup_state_d;
            end
        end
    end

    assign all_ones = '1;   // Synthesis fix for Vivado, which does not correctly compute the width
                            // of the '1 literal when assigned to a port of parametrized width.

`ifndef COMMON_CELLS_ASSERTS_OFF
    `ASSERT_INIT(n_oup_0, N_OUP >= 1, "Number of outputs must be at least 1!")
`endif

endmodule

ステートマシンの解析:

stream_forkは、入力側と各出力側の2種類のステートマシンで構成される。

1. 入力側ステートマシン (inp_state_q):

入力側はREADYWAITの2状態を持つ。

  • READY状態: 新しい入力ハンドシェイクを受け入れる準備ができている状態

    • valid_iがアサートされ、かつすべての出力が同時にハンドシェイクした場合(valid_o == all_ones && ready_i == all_ones)、入力のready_oをアサートして入力ハンドシェイクを完了し、状態はREADYのまま
    • valid_iがアサートされたが、すべての出力が同時にハンドシェイクしなかった場合、ready_o0にし、状態をWAITに遷移
    • valid_iがアサートされていない場合、ready_o0
  • WAIT状態: すべての出力がハンドシェイクするのを待っている状態

    • valid_iがアサートされており、かつすべての出力が準備できている場合(oup_ready == all_ones)、ready_oをアサートして入力ハンドシェイクを完了し、状態をREADYに戻す
    • それ以外の場合、ready_o0のままWAIT状態を維持

2. 出力側ステートマシン (oup_state_q[i]):

各出力は独立したREADYWAITの2状態を持つ。

  • READY状態: 出力がハンドシェイクを受け入れる準備ができている状態

    • valid_iがアサートされている場合、valid_o[i]をアサート
    • 出力がハンドシェイクした場合(ready_i[i] == 1):
      • 入力がまだハンドシェイクしていない場合(ready_o == 0)、状態をWAITに遷移(出力はハンドシェイク済みだが、入力のハンドシェイクを待つ)
      • 入力もハンドシェイクした場合、状態はREADYのまま
    • 出力がハンドシェイクしていない場合(ready_i[i] == 0)、oup_ready[i]0にして、出力の準備ができていないことを示す
  • WAIT状態: 入力のハンドシェイクを待っている状態

    • valid_iがアサートされており、かつ入力がハンドシェイクした場合(ready_o == 1)、状態をREADYに戻す
    • それ以外の場合、WAIT状態を維持

動作の流れ (例: N_OUP=3の場合):

タイムライン例:

Cycle 0: valid_i=1, ready_i={1,0,0}
  - 入力FSM: READY → WAIT (すべての出力が同時にハンドシェイクしていない)
  - 出力0: READY → WAIT (ハンドシェイクしたが、入力がまだ)
  - 出力1: READY (valid_o[1]=1, ready_i[1]=0なので oup_ready[1]=0)
  - 出力2: READY (valid_o[2]=1, ready_i[2]=0なので oup_ready[2]=0)
  - ready_o = 0

Cycle 1: valid_i=1, ready_i={1,1,0}
  - 入力FSM: WAIT (oup_ready != all_ones)
  - 出力0: WAIT (入力のハンドシェイク待ち)
  - 出力1: READY → WAIT (ハンドシェイクしたが、入力がまだ)
  - 出力2: READY (valid_o[2]=1, ready_i[2]=0なので oup_ready[2]=0)
  - ready_o = 0

Cycle 2: valid_i=1, ready_i={1,1,1}
  - 入力FSM: WAIT → READY (すべての出力が準備できた)
  - 出力0: WAIT → READY (入力がハンドシェイクした)
  - 出力1: WAIT → READY (入力がハンドシェイクした)
  - 出力2: READY → WAIT (ハンドシェイクしたが、入力がまだ)
  - ready_o = 1 (入力ハンドシェイク完了)

Cycle 3: valid_i=0
  - すべてのFSMがREADY状態に戻る

stream_fork_dynamic.sv

動的に選択可能なフォークモジュールである。別のストリームから提供されるビットマスクにより、どの出力ストリームにハンドシェイクするかを動的に決定する。

/// Dynamic stream fork: Connects the input stream (ready-valid) handshake to a combination of output
/// stream handshake.  The combination is determined dynamically through another stream, which
/// provides a bitmask for the fork.  For each input stream handshake, every output stream handshakes
/// exactly once. The input stream only handshakes when all output streams have handshaked, but the
/// output streams do not have to handshake simultaneously.
///
/// This module has no data ports because stream data does not need to be forked: the data of the
/// input stream can just be applied at all output streams.
module stream_fork_dynamic #(
  /// Number of output streams
  parameter int unsigned N_OUP = 32'd0 // Synopsys DC requires a default value for parameters.
) (
  /// Clock
  input  logic             clk_i,
  /// Asynchronous reset, active low
  input  logic             rst_ni,
  /// Input stream valid handshake,
  input  logic             valid_i,
  /// Input stream ready handshake
  output logic             ready_o,
  /// Selection mask for the output handshake
  input  logic [N_OUP-1:0] sel_i,
  /// Selection mask valid
  input  logic             sel_valid_i,
  /// Selection mask ready
  output logic             sel_ready_o,
  /// Output streams valid handshakes
  output logic [N_OUP-1:0] valid_o,
  /// Output streams ready handshakes
  input  logic [N_OUP-1:0] ready_i
);

  logic             int_inp_valid,  int_inp_ready;
  logic [N_OUP-1:0] int_oup_valid,  int_oup_ready;

  // Output handshaking
  for (genvar i = 0; i < N_OUP; i++) begin : gen_oups
    always_comb begin
      valid_o[i]       = 1'b0;
      int_oup_ready[i] = 1'b0;
      if (sel_valid_i) begin
        if (sel_i[i]) begin
          valid_o[i]       = int_oup_valid[i];
          int_oup_ready[i] = ready_i[i];
        end else begin
          int_oup_ready[i] = 1'b1;
        end
      end
    end
  end

  // Input handshaking
  always_comb begin
    int_inp_valid = 1'b0;
    ready_o       = 1'b0;
    sel_ready_o   = 1'b0;
    if (sel_valid_i) begin
      int_inp_valid = valid_i;
      ready_o       = int_inp_ready;
      sel_ready_o   = int_inp_ready;
    end
  end

  stream_fork #(
    .N_OUP  ( N_OUP )
  ) i_fork (
    .clk_i,
    .rst_ni,
    .valid_i ( int_inp_valid ),
    .ready_o ( int_inp_ready ),
    .valid_o ( int_oup_valid ),
    .ready_i ( int_oup_ready )
  );

`ifndef COMMON_CELLS_ASSERTS_OFF
  `ASSERT_INIT(n_oup_0, N_OUP >= 1, "N_OUP must be at least 1!")
`endif
endmodule

内部でstream_forkを使用し、選択マスクにより特定の出力のみにハンドシェイクを分配する。

stream_join.sv

複数の入力ストリームを1つの出力ストリームへ結合するモジュールである。 すべての入力がvalidの場合にのみ出力がvalidになる。

/// Stream join: Joins a parametrizable number of input streams (i.e., valid-ready handshaking with
/// dependency rules as in AXI4) to a single output stream.  The output handshake happens only once
/// all inputs are valid.  The data channel flows outside of this module.
module stream_join #(
  /// Number of input streams
  parameter int unsigned N_INP = 32'd0 // Synopsys DC requires a default value for parameters.
) (
  /// Input streams valid handshakes
  input  logic  [N_INP-1:0] inp_valid_i,
  /// Input streams ready handshakes
  output logic  [N_INP-1:0] inp_ready_o,
  /// Output stream valid handshake
  output logic              oup_valid_o,
  /// Output stream ready handshake
  input  logic              oup_ready_i
);

  stream_join_dynamic #(
    .N_INP(N_INP)
  ) i_stream_join_dynamic (
    .inp_valid_i(inp_valid_i),
    .inp_ready_o(inp_ready_o),
    .sel_i      ({N_INP{1'b1}}),
    .oup_valid_o(oup_valid_o),
    .oup_ready_i(oup_ready_i)
  );

endmodule

内部でstream_join_dynamicを使用し、すべての入力を選択するマスクを渡す。

stream_join_dynamic.sv

動的に選択可能なジョインモジュールである。 選択マスクにより、どの入力ストリームを結合するかを動的に決定する。

// Stream join dynamic: Joins a parametrizable number of input streams (i.e. valid-ready
// handshaking with dependency rules as in AXI4) to a single output stream. The subset of streams
// to join can be configured dynamically via `sel_i`. The output handshake happens only after
// there has been a handshake. The data channel flows outside of this module.
module stream_join_dynamic #(
  /// Number of input streams
  parameter int unsigned N_INP = 32'd0 // Synopsys DC requires a default value for parameters.
) (
  /// Input streams valid handshakes
  input  logic [N_INP-1:0] inp_valid_i,
  /// Input streams ready handshakes
  output logic [N_INP-1:0] inp_ready_o,
  /// Selection mask for the output handshake
  input  logic [N_INP-1:0] sel_i,
  /// Output stream valid handshake
  output logic             oup_valid_o,
  /// Output stream ready handshake
  input  logic             oup_ready_i
);

  // Corner case when `sel_i` is all 0s should not generate valid
  assign oup_valid_o = &(inp_valid_i | ~sel_i) && |sel_i;
  for (genvar i = 0; i < N_INP; i++) begin : gen_inp_ready
    assign inp_ready_o[i] = oup_valid_o & oup_ready_i & sel_i[i];
  end

`ifndef COMMON_CELLS_ASSERTS_OFF
  `ASSERT_INIT(n_inp_0, N_INP >= 1, "N_INP must be at least 1!")
`endif
endmodule

選択されたすべての入力がvalidの場合にのみ出力がvalidになり、選択された入力のみがready信号を受け取る。

stream_forkstream_joinのユースケース:

stream_forkstream_joinは必ずしもペアで使う必要はないが、組み合わせて使用する典型的なパターンがある。

1. ペアで使用する場合: 並列処理パイプライン

1つのストリームを複数の処理ユニットに分配し、処理後に結果を結合するパターン。

入力ストリーム
    │
    ├──> stream_fork ──┬──> 処理ユニット0 ──┐
    │                  ├──> 処理ユニット1 ──┤
    │                  └──> 処理ユニット2 ──┘
    │                                    │
    │                                    ├──> stream_join ──> 出力ストリーム
    │                                    │
    data_i ──────────────────────────────┘ (データは外部で接続)

使用例: - 画像処理: 1つの画像データを複数のフィルタ処理ユニットに分配し、結果を結合 - データ変換: 1つのデータストリームを複数の変換処理に分配し、変換結果を統合 - 検証・監視: 同じデータを複数の検証モジュールに送り、すべての検証が完了したら次のデータを処理

2. stream_forkのみを使用する場合: ブロードキャスト/モニタリング

1つのストリームを複数の宛先に同時に送信するパターン。データは同じものを複数の場所に送るだけ。

入力ストリーム
    │
    ├──> stream_fork ──┬──> モニター0 (ログ記録)
    │                  ├──> モニター1 (デバッグ)
    │                  └──> メイン処理パス
    │
    data_i ────────────┴──> (すべての出力に同じデータを接続)

使用例: - デバッグ: メインデータパスを変更せずに、デバッグ用のモニターを追加 - ログ記録: 同じデータを複数のログシステムに送信 - 冗長処理: 同じデータを複数のバックアップ処理ユニットに送信

3. stream_joinのみを使用する場合: マージ/統合

複数の独立したストリームを1つのストリームに統合するパターン。

ストリーム0 ──┐
ストリーム1 ──┼──> stream_join ──> 出力ストリーム
ストリーム2 ──┘

使用例: - マルチソース統合: 複数のデータソースからの結果を1つのストリームに統合 - パイプライン統合: 複数の並列パイプラインの結果を1つにまとめる - リクエスト統合: 複数のリクエストストリームを1つの処理パスに統合