You have written a beautiful RTL architecture. Your ADC SPI master is strictly timed, your clock domains are crossed safely with Gray-coded FIFOs, and your AXI-Stream pipeline handles backpressure perfectly.
But if your first instinct is to synthesize this code and flash it directly to your FPGA, you are making a critical mistake. Debugging timing failures or dropped data packets on live silicon is a slow, painful process.
Before you ever touch a physical board, you must prove your architecture works in simulation. However, testing a mixed-signal pipeline presents a unique challenge: FPGA simulators (like Vivado Simulator or ModelSim) are entirely digital. They only understand 1, 0, X (unknown), and Z (high impedance).
So, how do you verify an ADC controller that expects a continuous analog sine wave?
Bridging the Gap: Behavioral Data Injection (Panels 1 & 2)
You cannot pipe an analog waveform into a digital simulator. Instead, you must bridge the gap using software.
The standard industry approach is to pre-calculate the analog behavior and inject it into your simulation memory. You can write a simple Python script to generate a mathematical sine wave, quantize it to your ADC's resolution (e.g., 12 or 16 bits), and save it as a column of hexadecimal values in a text file.
Once you have this .hex file, Verilog has a built-in system task designed exactly for this: $readmemh (Read Memory Hex).
You instantiate an array in your Verilog testbench and load the Python-generated data directly into it. Now, your testbench has a perfect digital representation of what the analog world will eventually look like.

Emulating the ADC Hardware
With the data loaded into memory, you can write a behavioral model of the ADC itself. This is not RTL meant for synthesis; this is a piece of simulation code designed to mimic the physical chip.
When your FPGA's SPI master pulls the Chip Select (cs_n) low and starts toggling the clock (sclk), this behavioral block will shift out the simulated data from your memory array, bit by bit, mimicking the ADC's datasheet timing perfectly.
Here is what that behavioral data injection looks like in a Verilog testbench:
module tb_adc_acquisition_pipeline;
// Testbench Signals
reg clk;
reg rst_n;
reg [15:0] simulated_analog_memory [0:1023]; // Array to hold 1024 samples
integer sample_index = 0;
// SPI Signals mimicking the physical interface
wire cs_n;
wire sclk;
reg sdata; // Master In, Slave Out (MISO)
// 1. Load the Python-generated analog data into simulation memory
initial begin
$readmemh("simulated_sine_wave.hex", simulated_analog_memory);
end
// 2. Behavioral ADC Model: Shift out data on the falling edge of SCLK
always @(negedge sclk or posedge cs_n) begin
if (cs_n) begin
sdata <= 1'bz; // High impedance when unselected
end else begin
// Drive the serial data line just like the real hardware
sdata <= simulated_analog_memory[sample_index][15]; // Drive MSB
simulated_analog_memory[sample_index] <= simulated_analog_memory[sample_index] << 1; // Shift left
end
end
// (UUT Instantiation and Clock Generation go here)
endmoduleThe Self-Checking Architecture (Panel 3)
Injecting the data is only half the battle. Once your RTL pipeline processes that data, you need to verify the output.
Many junior engineers verify their designs by opening the waveform viewer and manually scrolling through thousands of clock cycles, squinting at transitions to see if they look correct. This is not verification; it is guesswork.
Professional testbenches are self-checking. You should build a "Scoreboard" process at the end of your testbench. Because your testbench knows exactly what data it injected at the front end, it knows exactly what data should emerge from the AXI-Stream interface at the back end.
The testbench should automatically compare the output against the expected values. If it detects a mismatch, it prints a $display("ERROR") message with the exact timestamp. If the entire file processes correctly, it prints a clean [PASS].
Conclusion
Writing RTL is the act of designing hardware, but writing a testbench is the act of proving it works. By simulating analog environments through data injection and building self-checking architectures, you eliminate the guesswork of mixed-signal design. You can confidently synthesize your bitstream knowing that when you finally power on the physical board, the digital pipeline will perform exactly as engineered.
This article officially wraps up our four-part series on mixed-signal RTL architecture. If you have been following along—from the initial ADC SPI interface, through the Clock Domain Crossing (CDC) boundaries and AXI4-Stream routing, to this final verification stage—you now understand the mechanics of a complete, robust data pipeline.
In fact, this entire series was a structural breakdown of the architecture I engineered for my Optical FPGA Data Acquisition system. If you want to see how all these concepts (the SPI master, Gray-coded Async FIFOs, AXI backpressure, and Python-driven testbenches) wire together in a complete, synthesis-ready project, you can explore the full Verilog source code on my GitHub:
🔗 Optical FPGA Data Acquisition Repository