ロバストなマイクロコントローラとFPGAのSPIインタフェースの実装:パート4 - ダブルバッファ

APDahlen Applications Engineer

この回は、マイクロコントローラ(uC)とフィールドプログラマブルゲートアレイ(FPGA)のインターフェースの探求を続けます。

  • Part 1では、大規模システムの開発を導くVerilogの設計思想を紹介しています。これは、クロック境界、ストローブ信号の使用、ダブルバッファの必要性などのレジスタ転送レベル(RTL)設計ガイドラインを紹介する重要な内容です。

  • Part 2では、SPIプロトコルを説明しています。選択されたプロトコルは802.3のEthernetフレームから適応されたもので、可変ペイロード長やデータの完全性の指標を提供する巡回冗長検査(CRC)などの概念があることを思い出してください。

  • Part 3では、uCからFPGAへのインターフェースの概要を示します。この記事の最も重要な部分は、便宜上、ここで図1として繰り返し表記したブロック図です。

今回は、ブロック図の右上に焦点を当てます。具体的には、拡張可能なダブルバッファの動作です。

図1: トップレベルFPGAのデータフローを示すブロック図

PWMの復習

ダブルバッファを紹介する前に、Verilog Pulse Width Modulator (PWM)の動作を簡単に説明します。ダブルバッファは、PWMなどのハードウェアモジュールへのアドレス指定可能なインターフェースとして最もよく見られるため、これは重要です。

PWMモジュールのトップレベルインターフェースは、このVerilogコードスニペットに示されています。モジュールがビット幅のパラメータを使用し、最小および最大デューティサイクルの制限を確立していることに注目してください。最後に、PWMモジュールにデューティ サイクルを設定するための[B - 1:0]入力ベクタがあることを確認します。入力が各PWMデューティサイクルの開始時に読み取られるという事実は示されていません。

module PWM #(parameter 
    B = 12,                             // implies a 24.4 kHz PWM assuming a 100 MHz system clock
    D_MIN_PERCENT = 0,
    D_MAX_PERCENT = 95
)
(
    input wire clk,
    input wire enable,
    input wire [B - 1:0] d_in,
    output reg PWM,
    output reg [B - 1:0] cnt
);

同時データプレゼンテーション

図1に示すように、PWMは、より大きなuCからFPGAへのSPIシステム内で動作するように設計されています。SPIはもともとバイト幅のデータ要素を処理することを思い出してください。これは、Bで定義されたデータ幅を使用して動作するPWMとはまったく対照的です。便宜上、PWMが16ビットのビット幅(B)でインスタンス化されたと仮定します。

システムがPWMの入力に関連するレジスタを更新する時、問題が発生します。適切な注意を払わないと、PWMがレジスタの更新の途中で読み取り操作を実行する可能性があります。その結果、ドライブバイトが1つが古いものと1つが新しいもので構成されることになります。これにより、1つのPWMサイクルの間、デューティサイクルが大幅にジャンプする可能性があります。LEDインジケータにPWMが使用されている場合、このことに気付かない可能性があります。より複雑なシステムでは、障害は強い衝撃に相当し、エラーが発生するタイミングと頻度によっては、システムが鳴ったり、不安定になったりする可能性があります。

解決策は、前の記事で説明したダブルバッファスキームを実装することです。詳細な説明は後で説明します。レジスタの1セットは、個々のバイトをキャプチャするために使用されます。Nバイトデータが全て収集されると、2番目の幅の広いレジスタが更新されます。この2番目のレジスタ(ダブルバッファ)は、代表的なPWMなどの他のモジュールを駆動するために使用されます。

ダブルバッファモジュール

ダブルバッファモジュールのブロック図は図2に示されています。内部的には4つの主要なセクションで構成されています。最も重要なのは出力レジスタです。この例では、幅が16ビットなので16ビットPWMの駆動に適しています。 出力レジスタは個々の8 ビットレジスタによって駆動され、この例ではLSBとMSBというラベルが付けられています。すべてのレジスタ更新がダブルバッファの制御セクションによって開始されることに注目してください。これは、すべての要素が100MHzプライマリクロックの刻みに応答する同期動作です。

図2: 個々の8 ビットバッファと出力バッファの関係を示すダブルバッファのブロック図

このコードリストに示すように、各ダブルバッファモジュールは特定のアドレスと特定のバイト幅でインスタンス化されることを理解することが重要です。16ビットのアドレス、8ビットのデータ、および書き込みストローブがすべてバッファのロードに関与することに注意してください。データ転送は、16ビットのアドレス入力がインスタンス化されたアドレスと一致すると開始されます。

module double_buffer #(
    parameter BYTE_WIDTH = 2,
    parameter BASE_ADDRESS = 16'h0200  // Starting address for the MSB
) (
    input wire clk,
    input wire [7:0] data,
    input wire [15:0] address,
    input wire write_strobe,
    output reg [((8 * BYTE_WIDTH) - 1): 0] double_buffer_out,
    output reg new_data_strobe
);

図1と2が示すように、このuCからFPGAへのインターフェースには、基礎となる8ビット転送プロセスがあります。パート2の記事で最初に紹介したコマンドフレームには、暗黙的な連続書き込み操作もあります。便宜上、コマンドフレームを図3として繰り返します。例として、PWMおよび関連するダブルバッファがアドレス0x0200でインスタンス化されたと仮定します。コマンドフレームの書き込みアドレスは0x0200に設定され、ペイロードの最初の2バイトには必要な16 ビットPWM値が保持されます。

図3: uCからFPGAへのSPIプロトコルの基礎を形成するコマンドフレームと応答フレーム

コマンドフレームが受信されて検証されると、MSG Writerブロック(図1を参照)は、PWMのダブルバッファを指すアドレス0x0200をアサートします。最初のペイロードバイトをデータバスに配置します。最後に1クロックサイクルの間、書き込みストローブをアサートします。これにより、図2に示すようにMSBがロードされます(ビッグエンディアン)。

連続書き込みを続けると、MSGライタはアドレスを進め、次のデータバイトをアサートし、書き込みストローブをパルス出力して、LSBをダブルバッファにロードします。このプロセスは、フレームのバイト長フィールドによって制御されるコマンドフレーム内のすべてのバイトに対して継続されます。本質的に、MSGライタは、関連するダブルバッファの長さを認識しません。アドレス、データ、書き込みストローブのアサートという3段階のプロセスのみに関係します。ダブルバッファモジュールがいつアドレス指定され、BYTE_WIDTHパラメータで指定された必要なバイト数をいつ受信完了したかを理解するのは、ダブルバッファ モジュールの役割です。

ダブルバッファのベースアドレスとバイト幅はインスタンス化時にわかるため、すべてのバイトがいつ受信完了したかを判断するのは簡単です。このPWMの例では、ダブルバッファが2までカウントし、ストローブを送信して出力レジスタをロードします。

技術的なヒント: データは、最上位バイト(MSB)または最下位バイト(LSB)から最初にアクセスされます。順序を表す用語は「エンディアン」です。MSBが最初にアクセスされる場合、システムはビッグエンディアンです。LSBが最初の場合、システムはリトルエンディアンです。この記事で説明されているダブルバッファと関連フレームはビッグエンディアンです。

ダブルバッファコード

ダブルバッファのVerilogコードはこのメモの最後に添付されています。このコードは、図2のブロック図に厳密に従っていますが、BYTE_WIDTHパラメータを変更することでNバイト幅に拡張することも可能です。

このコードの鍵は、Verilogのgenerate処理の使用です。Generate処理により、ハードウェアを繰り返し生成できることを思い出してください。ウィジェットを製造する工場のように動作します。ただし、この場合は、BYTE_WIDTHパラメータに等しいアセンブリの総数で8ビットレジスタを作成しています。これは、図4に示すVivado階層デザインウィンドウで確認できます。これらの「製造された」ブロックは、generateループで定義された連続した名前付けスキームとともに表示されます。

図4: ダブルバッファのインスタンス化で生成されたバイト幅レジスタが表示されます

生成された各9ビットレジスタに、対応するlocal_write_strobeが含まれていることを確認します。これは、関連する8ビットレジスタをロードするために「制御」セクションによって使用されるため、設計上の重要な側面です。

レジスタに加えて、generateループは各8ビットレジスタの出力が接続される8ビットベクタを作成します。これらのN×8ビットの束は連結されて、Nバイトの出力レジスタに渡されます。

コードの最後の部分は、Nバイトがいつ収集完了するかを決定します。次に、出力レジスタを更新し、new_data_strobeを送ります。

制御セクションには 3 つの基本機能があります。

  1. ベースアドレスがインスタンス化されたアドレスと一致した時に、モジュールをアクティブ化します。
  2. 「製造された」8ビットレジスタを指すカウンタを維持します。このカウンタは連続書き込みに不可欠です。
  3. 関連する8ビットレジスタをストローブします。
  4. N個の8ビットレジスタがいっぱいになると、出力バッファをストローブします。

技術的なヒント: ベクタとは、wire型の1次元配列のことです。例えば、「input wire [15:0] address」は、addressという名前の16ビットベクタを定義しています。

パート4のクロージング

本記事でのコードは確かに複雑ですが、Verilogのgenerate処理を使用すると、非常に柔軟な対応が可能になります。これにより、必要なバイト幅ごとに独立したモジュールを構築する必要がなくなります。

次回は、VerilogベースのCRCジェネレータの動作について説明します。その記事が完了すると、図3に示すフレームがFPGAによってどのように処理されるかが分かります。

ご意見やご提案をお待ちしております。高レベルのRTLシステム設計方法論に関するさらなる議論は特に歓迎致します。

ご健闘をお祈りします。

APDahlen

//**************************************************************************************************
//
// Module: Parameterized and Addressed Double Buffer
//
//  This RTL is subject to Terms and Conditions associated with the 
//  DigiKey TechForum. Refer to:
//  https://www.digikey.com/en/terms-and-conditions?_ga=2.92992980.817147340.1696698685-139567995.1695066003
//
//  Should you find an error, please leave a comment in the forum space below.
//  If you are able, please provide a recommendation for improvement.
//
//**************************************************************************************************
//           ______________________________________________
//          |                                              |
//          |   Module: double_buffer                      |
//          |                                              |
//          |    Parameters:                               |
//          |        BYTE_WIDTH = 4                        |
//          |        BASE_ADDRESS = 16'h0000               |
//          |______________________________________________|
//          |                                              |
//      ==8=| data                       double_buffer_out |=V==
//      ----| write_strobe                 new_data_strobe |----
//          |                                              |
//      ----| write_strobe                                 |
//          |                                              |
//      ----| clk                                          |
//          |______________________________________________|
//
//
//** Description ***********************************************************************************
//
//  This module works with a stream of parallel data. When the streamed address matches the 
//  parameterized address the module will store the byte width data into a buffer. The 
//  module than accepts consecutive data storing each byte in the associated buffer. When the
//  module has received BYTE_WIDTH bytes, the data are transferred to the output buffer. 
//   
//** Instantiation *********************************************************************************
//
//  double_buffer #(.BYTE_WIDTH(2), .BASE_ADDRESS(PWM_1_address) )
//    PWM_1_driver( 
//      .clk(clk), 
//      .data(MSG_writer_data), 
//      .address(MSG_writer_address),
//      .write_strobe(MSG_writer_strobe),
//      .double_buffer_out(PWM_1_drive),
//      .new_data_strobe(new_data_strobe)               // optional
//  );
//
//** Signal Inputs: ********************************************************************************
//
//  1) clk: High speed system clock (typically 100 MHz)
//
//  2) data: An 8-bit input. Data will be captured when address is within 
//     BASE_ADDRESS + BYTE_WIDTH (non-inclusive).  
//
//  3) address: A 16-bit input. The module will respond to BYTE_WIDTH consecutive address starting at 
//     BASE_ADDRESS extending to BASE_ADDRESS + BYTE_WIDTH (non-inclusive).  
//
//  4) write strobe: When pulsed, the data will be locked into the associated input buffers provided 
//     the address is within BASE_ADDRESS + BYTE_WIDTH (non-inclusive).  
//
//** Signal Outputs ********************************************************************************
//
//  1) double_buffer_out: This BYTE_WIDTH is the output buffer in this double buffer module. It is
//     updated in a single clock cycle after all the individual buffers have been updated. There 
//     is a one clock delay between the filling the last byte-wide buffer and a double buffer update.  
//
//  2) new_data_strobe: a pulse active for the same clock cycle in which the double_buffer is updated.
//
//** Comments **************************************************************************************
//
//  1) TODO, Consider eliminating the one clock cycle delay and eliminating the most significant 
//     byte input register. Use care for a single byte instantiation.
//
//**************************************************************************************************
 
module double_buffer #(
    parameter BYTE_WIDTH = 4,
    parameter BASE_ADDRESS = 16'h0000  // Starting address for the MSB
) (
    input wire clk,
    input wire [7:0] data,
    input wire [15:0] address,
    input wire write_strobe,
    output reg [((8 * BYTE_WIDTH) - 1): 0] double_buffer_out,
    output reg new_data_strobe
);
 
//** CONSTANT DECLARATIONS *************************************************************************
 
    /* General shortcuts */
        localparam T = 1'b1;
        localparam F = 1'b0;
 
//** Body *******************************************************************************************
 
wire [8*BYTE_WIDTH-1:0] concat_result;

reg delay_one_clk;
 
wire [7:0] buffer_data_out[BYTE_WIDTH-1:0]; // Array of 8-bit wires
 
// Generate N 8-bit registers

    generate
        genvar i;
        for (i = 0; i < BYTE_WIDTH; i=i+1) begin: gen_regs
 
        wire local_write_strobe = write_strobe && (address == BASE_ADDRESS + i); // Asserted only when the address matches
 
            local_8bit_reg register_inst (
                .clk(clk),
                .data(data),
                .write_strobe(local_write_strobe),
                .q(buffer_data_out[i])
            );
        end
    endgenerate
 
// Concatenate the outputs of the N 8-bit buffers dynamically

    generate
        genvar j;
        assign concat_result[7:0] = buffer_data_out[0];
        for (j = 1; j < BYTE_WIDTH; j=j+1) begin: gen_concat       // Note the use of the + operator with the loop starting at 1 not 0
            assign concat_result[j*8 +: 8] = buffer_data_out[j];  
        end
    endgenerate
 
 
    always @(posedge clk) begin           // Delay one clock for the double buffer operation to 
                                          // allow data to be clocked into the last of the 
                                          // individual registers.

        new_data_strobe <= F;             // Default 
        delay_one_clk <= F;   
 
        if (write_strobe && (address == (BASE_ADDRESS + BYTE_WIDTH - 1))) begin
            delay_one_clk <= T;
        end

        if (delay_one_clk)begin
            double_buffer_out <= concat_result;
            new_data_strobe <= T;
     end
    end
endmodule
 
module local_8bit_reg (
    input wire clk,
    input wire [7:0] data,
    input wire write_strobe,
    output reg [7:0] q
);
 
always @(posedge clk) begin
    if (write_strobe) begin
        q <= data;
    end
end
endmodule




オリジナル・ソース(English)