0. Visual China
Visual China Contracted Photographer Horizon – Little Train1. Introduction: Why Use Buffers?Many FPGA beginners think that when processing video, the most straightforward idea is: the data from the camera can be directly sent to the HDMI display module, right?This “direct connection” method only works under an extreme coincidence: the output clock of the camera and the display clock of the HDMI must be completely locked and in phase. However, in the real world:
- The camera’s clock may be 24MHz.
-
The HDMI display clock may be 148.5MHz.
-
The camera outputs 30 frames of data per second, while the display refreshes 60 times per second.
If you force a direct connection, the consequence isscreen tearing: the upper half of the screen displays the Nth frame, while the lower half suddenly changes to the N+1th frame. It looks very ugly, like forcibly cutting and splicing two photos together.To solve this problem, we need a “reservoir”, which is—frame buffer. The most basic and classic buffer architecture isdouble buffering (Ping-Pong Buffer).2. What is “Ping-Pong Operation”?Ping-Pong means “back and forth”. We allocate two memory spaces in DDR3/DDR4, called Buffer A and Buffer B.
-
First Stage:
-
✍️ Writer is writing data into Buffer A.
-
👀 Reader is reading data from Buffer B.
-
(At this time, reading and writing do not interfere with each other)
-
Switching Moment:
-
When the writer has filled A, and the reader has also finished reading B.
-
Swap!
-
Second Stage:
-
✍️ Writer changes to write into Buffer B.
-
👀 Reader changes to read from Buffer A.
As long as this rhythm continues, the display will always read a complete image, and screen tearing will disappear.
3. Verilog Code Implementation
// double_buffer.vmodule double_buffer #( parameter ADDR_WIDTH = 2 ,parameter BUFFER_A_ADDR = 2'b01 ,parameter BUFFER_B_ADDR = 2'b10)( input clk_i ,input rstn_i // Control signals ,input frame_write_done ,input frame_read_done // Status outputs ,output reg [ADDR_WIDTH-1:0] current_write_addr ,output reg [ADDR_WIDTH-1:0] current_read_addr // Status indication ,output reg frame_available // Indicates whether a new frame is ready to switch); // Buffer index definitions localparam IDX_A = 1'b0; localparam IDX_B = 1'b1; // Internal pointers reg wr_idx; // 0 writes A, 1 writes B reg rd_idx; // 0 reads A, 1 reads B // Core logic: new frame ready flag // When a frame is written, set this flag to 1; only when read is complete and the flag is 1, will a swap occur reg new_frame_ready; // Address lookup function function [ADDR_WIDTH-1:0] get_addr(input idx); case(idx) IDX_A: get_addr = BUFFER_A_ADDR; IDX_B: get_addr = BUFFER_B_ADDR; endcase endfunction always @(posedge clk_i or negedge rstn_i) begin if (!rstn_i) begin wr_idx <= IDX_A; // Default write A rd_idx <= IDX_B; // Default read B new_frame_ready <= 1'b0; end else begin // ------------------------------------------------------- // Write logic // ------------------------------------------------------- if (frame_write_done) begin // Finished writing the current Buffer (Back Buffer) // Mark that new data is ready for swapping new_frame_ready <= 1'b1; // Note: In double buffering, the Writer cannot decide to switch Buffers by itself. // If the Writer is too fast, it must stay on the current wr_idx to continue writing (overwriting old data/dropping frames), // or external logic must pause the Writer. Here we keep wr_idx unchanged, waiting for the Reader to allow the swap. end // ------------------------------------------------------- // Read logic (the only moment a swap occurs) // ------------------------------------------------------- if (frame_read_done) begin if (new_frame_ready) begin // Only when: read is complete AND the background has written a new frame // can a Ping-Pong swap occur rd_idx <= ~rd_idx; // Read pointer flip wr_idx <= ~wr_idx; // Write pointer flip (Writer can finally go to the other side) new_frame_ready <= 1'b0; // Consumed the new frame, clear the flag end // Otherwise: Reader continues to read the current rd_idx (repeat frame/Hold) end end end // Output logic always @(*) begin current_write_addr = get_addr(wr_idx); current_read_addr = get_addr(rd_idx); frame_available = new_frame_ready; endendmodule
4. Simulation Testing
`timescale 1ns / 1psmodule tb_double_buffer; parameter ADDR_WIDTH = 2; parameter BUFFER_A_ADDR = 2'b01; parameter BUFFER_B_ADDR = 2'b10; reg clk, rstn; reg frame_write_done, frame_read_done; wire [ADDR_WIDTH-1:0] current_write_addr, current_read_addr; wire frame_available; // Instantiate double_buffer #( .ADDR_WIDTH(ADDR_WIDTH), .BUFFER_A_ADDR(BUFFER_A_ADDR), .BUFFER_B_ADDR(BUFFER_B_ADDR) ) dut ( .clk_i(clk), .rstn_i(rstn), .frame_write_done(frame_write_done), .frame_read_done(frame_read_done), .current_write_addr(current_write_addr), .current_read_addr(current_read_addr), .frame_available(frame_available) ); // Clock initial begin clk = 0; forever #5 clk = ~clk; end // Helper print task print_status(input [8*20:1] msg); $display("Time %0t | %s | WR: %s | RD: %s | Ready: %b", $time, msg, (current_write_addr==BUFFER_A_ADDR)?"A":"B", (current_read_addr==BUFFER_A_ADDR)?"A":"B", frame_available); endtask task tick_write; begin @(posedge clk); frame_write_done <= 1; @(posedge clk); frame_write_done <= 0; #1; end endtask task tick_read; begin @(posedge clk); frame_read_done <= 1; @(posedge clk); frame_read_done <= 0; #1; end endtask initial begin rstn = 0; frame_write_done = 0; frame_read_done = 0; #20; rstn = 1; #10; print_status("INIT "); // 1. Normal alternation $display("\n--- Case 1: Normal Ping-Pong ---"); tick_write(); // Write A complete print_status("Wr Done A "); // WR is still A, Waiting for read tick_read(); // Read B complete -> Swap occurs print_status("Rd Done B "); // Now WR:B, RD:A // 2. Writing too fast (Writer Blocked) $display("\n--- Case 2: Writer too fast (Stall/Overwrite) ---"); // Current state: WR writes B, RD reads A tick_write(); // Write B complete print_status("Wr Done B1"); // Ready=1, but cannot switch, because RD is still on A tick_write(); // Wrote B again (drop frame/overwrite) print_status("Wr Done B2"); // Pointer still hasn't moved! If it were triple buffering, the Writer would have already gone to C tick_read(); // RD finally finished reading A print_status("Rd Done A "); // Swap occurs: WR:A, RD:B // 3. Reading too fast (Reader Starvation) $display("\n--- Case 3: Reader too fast (Repeat) ---"); // Current state: WR writes A, RD reads B tick_read(); // Finished reading B, but Writer hasn't finished writing A print_status("Rd Done B1"); // No swap, repeat read B tick_read(); print_status("Rd Done B2"); // Still reading B tick_write(); // Writer finally finished writing A print_status("Wr Done A "); // Ready=1 tick_read(); // Next read complete print_status("Rd Done B3"); // Finally swapped #50 $stop; endendmodule
Three Scenarios:
- Read and write speeds are equal.
- Write speed is greater than read speed.
- Read speed is greater than write speed.
5. Little Secrets in the Code:new_frame_ready
You might ask, why not directly wr_idx <= ~wr_idx? Why use new_frame_ready?
This is to preventreading empty data.
If the reading speed is faster than the writing speed (for example, 60Hz read, 30Hz write).
-
Reader finishes reading Buffer B.
-
At this time, the Writer is still slowly writing Buffer A (only half written).
-
If a swap occurs at this time, the Reader will switch to read Buffer A.It will be a disaster, reading half-written data, causing screen tearing again.
With new_frame_ready, the Reader will obediently read Buffer B again (displaying a still image), until the Writer finishes writing A and raises a sign saying “I am ready”.
6. The Pitfall of Double Buffering
Does it seem perfect? However, in high-speed systems, Ping-Pong has a fatal weakness:strong coupling (Blocking).
Imagine:
-
Writer is writing very fast.
-
It finishes writing A and wants to write B.
-
But!Reader is still slowly reading B.
-
The Writer has nowhere to go! It cannot overwrite B, or the Reader will read incorrectly.
As a result: the Writer is forced to stop and wait (Stall).
For devices like cameras that cannot pause, this meansdata loss. For high-speed acquisition cards, this means FIFO overflow.
Is there a solution that allows the Writer to never wait for the Reader, to write whenever it wants, to fly whenever it wants?
Please stay tuned for the next article—The Magic of Triple Buffering.