實現穩健的微控制器到 FPGA SPI 介面:第 4 部分 - 雙緩衝器 (Double Buffer)

本貼文繼續探討微控制器(uC)到 FPGA 介面。

第 1 部分:介紹了指導大型系統開發的 Verilog 設計概念。介紹暫存器傳輸電平(RTL)設計準則的關鍵部分,如時脈邊界、頻閃器的使用和雙緩衝區的必要性。

第 2 部分:介紹了 SPI 協定。回想一下,所選的協定改編自 802.3 乙太網路框架,具有可變有效載荷長度和循環冗餘校驗(CRC)等概念,以提供資料完整性的度量。

第 3 部分:介紹了 uC 到 FPGA 介面的高階視圖。 文中最重要的部分是為方便起見,此處重複的框圖(如圖 1 所示)。

本部分重點介紹方塊圖的右上角。具體來說就是可擴展雙緩衝區的操作。

圖 1:顯示 FPGA 資料流的頂級 FPGA 方塊圖。

PWM 的回顧

在介紹雙緩衝器之前,我們將先簡單探討 Verilog PWM 的操作。這很重要,因為雙緩衝器最好被視為硬體模組(例如 PWM)的可尋址介面。

此 Verilog 程式碼片段描述了 PWM 模組的頂層介面。觀察此模組使用位寬參數並建立最小和最大佔空比的限制。最後,觀察 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-bit 的位元寬度 (B) 進行實例化。

當系統更新與 PWM 輸入相關的暫存器時,就會出現問題。 如果沒有適當注意,PWM 可能會在更新過程中執行讀取操作。 結果是驅動器位元組被一舊一新分割。這可能導致佔空比顯著跳躍並持續單一 PWM 週期。如果 PWM 用於 LED 指示器,這一點可能會被忽略。在更複雜的系統中,故障相當於強烈脈衝,可能導致系統響鈴或變得不穩定,這取決於錯誤發生的時間和頻率。

解決方案是實作前面文章中提到的雙緩衝區方案,隨後將進行深入討論。一組暫存器用於捕獲各個位元組。當收集到完整的 N-byte 資料時,會更新第二個更寬的暫存器。然後,第二個暫存器(雙緩衝器)用於驅動其他模組,例如代表性 PWM。

雙緩衝模組

雙緩衝模組的方塊圖如圖 2 所示。在內部,它由四個主要部分組成。最重要的是輸出暫存器。在本例中,它是 16-bit 寬度,適合驅動 16-bit PWM。輸出暫存器由單獨的 8-bit 暫存器驅動,在本例中它們被標記為 LSB 和 MSB。觀察到所有暫存器更新都是由雙緩衝器的控制部分發起的。這是一種同步操作,其中所有元件都響應主 100MHz 時脈的滴答聲。

圖 2:雙緩衝器方塊圖,顯示各個 8-bit 緩衝器和輸出緩衝器之間的關係。

重要的是要理解每個雙緩衝模組都是使用特定位址和特定位元組寬度進行實例化的,如本程式碼清單所示。 請注意,16-bit 位址、8-bit 資料和寫入頻閃都與載入緩衝區有關。 當 16-bit 位址輸入與實例化位址相符時,資料傳輸開始。

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-bit 傳輸過程。在第 2 篇文章中首次介紹的命令框架中也隱含了連續寫入操作。為了方便起見,這裡重複了指令框架,如圖 3 所示。作為一個例子,我們假設 PWM 和相關的雙緩衝區是用位址 0x0200 實例化的。命令框架的寫入位址將設定為 0x0200,有效負載的前兩個位元組將保存所需的 16-bit PWM 值。

image

圖 3:構成 uC 至 FPGA SPI 協定基礎的命令和回應框架。

當命令訊框被接收並驗證時,MSG Write 模組(參見圖 1)將聲明位址 0x0200,該位址指向 PWM 的雙緩衝區。它將把第一個有效負載位元組放到資料匯流排上。最後,它將在一個時脈週期內斷言寫選通。這將載入 MSB,如圖 2(大端)所示。

繼續連續寫入,MSG 寫入器推進位址,斷言下一個資料位元組,然後發出寫入傳入脈衝,從而將 LSB 載入到雙緩衝區中。對於命令框架中的每個位元組,此過程都會繼續,由框架的位元組長度字段控制。

本質上,訊息編寫器不了解關聯的雙緩衝區的長度。它只涉及斷言地址、資料和寫入頻閃的三步驟過程。 雙緩衝模組負責了解何時對它們進行尋址以及何時接收到 BYTE_WIDTH 參數指定的所需位元組數。

由於雙緩衝區的基底位址和位元組寬度在實例化時已知,因此很容易確定何時接收到所有位元組。在此 PWM 範例中,雙緩衝器計數到 2,然後傳送選通脈衝以載入輸出暫存器。

技術提示:可以先存取最高有效位元組(MSB) 或最低有效位元組(LSB)。描述順序的術語是「endian」。如果 MSB 先出現,則系統為大端位元組序。如果 LSB 在前,則系統為小尾數法。本文所述的雙緩衝區和相關框架是大端字節序。

雙緩衝程式碼

雙緩衝區的 Verilog 程式碼附在本筆記的最後。 該程式碼緊密遵循圖 2 方塊圖,並理解它可以擴展到 N 位元組寬度。 這是透過更改 BYTE_WIDTH 參數來實現的。

該程式碼的關鍵是 Verilog 產生運算子的使用。 回想一下,生成功能允許迭代生成硬體。它的運作就像一個製造小部件的工廠。除此之外,在本例中,我們正在製作 8-bit 暫存器,其元件總數等於 BYTE_WIDTH 參數。我們可以在此處捕獲的 Vivado 分層設計視窗中看到這一點,如圖 4 所示。這些「製造」區塊以其在生成循環中定義的連續命名方案出現。

image

圖 4:在雙緩衝區實例化中可以看到產生的位元組寬度暫存器。

觀察到每個產生的 9-bit 暫存器都包含一個對應的 local_write_strobe。這是一個重要的設計方面,因為「控制」部分使用它來載入相關的 8-bit 暫存器。

除了暫存器之外,產生循環還產生一個 8-bit 向量,每個 8-bit 暫存器的輸出都連接到該向量。然後將這些 N × 8-bit 包連接起來並傳遞到 N-byte 輸出暫存器。

程式碼的最後部分確定何時收集 N-byte。然後它更新輸出暫存器並傳送一個new_data_strobe。

控制部分有三個基本功能:

  1. 當基底位址與實例化位址匹配時啟動模組。
  2. 維持一個計數器以指向「製造」的 8-bit 暫存器。 此計數器對於連續寫入至關重要。
  3. 頻閃相關的 8-bit 暫存器。
  4. 當 N 個 8-bit 暫存器已滿時頻閃輸出緩衝器。

技術提示:向量是一維導線數組。一個例子是「輸入線[15:0]位址」,它定義了一個名為位址的16-bit 向量。

第 4 部分結束

雖然這段程式碼確實很複雜,但 Verilog 產生運算子提供了很大的靈活性。它消除了為每個所需位元組寬度建立獨立模組的需要。

在下一部分中,我們將探討基於 Verilog 的 CRC 產生器的操作。當這篇文章完成後,您將看到 FPGA 如何處理圖 3 所示的框架。

歡迎您提出意見和建議。 特別歡迎有關高級 RTL 系統設計方法的進一步討論。

//**************************************************************************************************
//
// 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