Reusable cocotb drivers and test utilities for simulating video sources in FPGA testbenches. Supports parallel (HS/VS/DE/RGB) and AXI4-Stream video interfaces with built-in test pattern generation and image loading.
Included Drivers:
| Driver | Interface | Backpressure | Blanking |
|---|---|---|---|
ParallelVideoDriver |
HS / VS / DE / R / G / B | — | Full timing grid |
AXIStreamCameraDriver |
AXI4-Stream | Ignored (free-running) | Full timing grid |
AXIStreamVideoDriverBuffered |
AXI4-Stream | Waits for tready |
None |
Requires Python 3.10+, cocotb 2.0+, and a supported simulator (Verilator, XSIM, ModelSim, etc.).
VideoTiming is a dataclass that describes a complete video timing standard.
from cocotb_video_source import VideoTiming, TIMING_PRESETS
# Timing Preset
timing = TIMING_PRESETS["1080p60"]
# Custom Timing
timing = VideoTiming(
h_active=1280, h_fp=110, h_sync=40, h_bp=220,
v_active=720, v_fp=5, v_sync=5, v_bp=20,
pclk_hz=74_250_000.0,
name="720p60",
)Built-in presets: 480p60, 720p60, 1080p30, 1080p60, svga.
All drivers share the same async send/queue API via VideoDriverBase:
await drv.send_frame(frame) # send one frame, block until done
drv.queue_frame(frame) # fire-and-forget
drv.queue_frames([frame_a, frame_b]) # enqueue multiple
print(drv.frames_driven) # count of completed framesFrames are (H, W, 3) uint8 NumPy arrays in RGB order.
Drives hs, vs, de, r, g, b signals through the complete timing grid — active pixels, front porch, sync pulse, and back porch — one clock cycle at a time.
from cocotb_video_source.drivers.parallel import ParallelVideoDriver
drv = ParallelVideoDriver(dut.clk, dut, timing)
await drv.send_frame(frame)Signal names are resolved as dut.<prefix><name>, configurable with prefix=.
Emulates a free-running camera sensor. Drives the full h_total × v_total timing grid each frame. tvalid is asserted only during active pixels; blanking regions produce tvalid=0. tready is never sampled — the driver advances unconditionally every clock cycle.
from cocotb_video_source.drivers.axi_stream_camera import AXIStreamCameraDriver
drv = AXIStreamCameraDriver(dut.clk, dut, timing, data_width=32)
await drv.send_frame(frame)Streams packed pixel data over AXI4-Stream using pure backpressure flow control. The driver stalls on each beat until tready is asserted. No blanking cycles are inserted between lines; the interface is saturated whenever data is available.
Supports 1 or 2 pixels per clock via pixels_per_cycle (useful for 2ppc interfaces with 64-bit or 48-bit tdata).
from cocotb_video_source.drivers.axi_stream import AXIStreamVideoDriverBuffered
# 32-bit, 1 pixel per clock
drv = AXIStreamVideoDriverBuffered(dut.clk, dut, timing, data_width=32)
# 64-bit, 2 pixels per clock
drv = AXIStreamVideoDriverBuffered(dut.clk, dut, timing, data_width=64, pixels_per_cycle=2)
await drv.send_frame(frame)Both drivers assert tuser=1 on the first beat of each frame (start-of-frame) and tlast=1 on the last beat of each line.
Packs RGB pixel rows into tdata integer words. Used internally by both AXI-Stream drivers; also useful for writing custom monitors.
from cocotb_video_source import PixelPacker
packer = PixelPacker(data_width=32, byte_order="little")
beats = packer.pack_line(frame[row]) # list[int]Supported data widths: 24, 32, 48, 64. Byte order: "little" (R at LSB, Xilinx default) or "big".
Generates standard test frames as (H, W, 3) uint8 NumPy arrays.
from cocotb_video_source import PatternGenerator, VideoTiming
timing = VideoTiming(h_active=1920, v_active=1080, ...)
gen = PatternGenerator(timing)
frame = gen.color_bars() # SMPTE 75% colour bars
frame = gen.gradient_h() # horizontal luma ramp
frame = gen.gradient_v() # vertical luma ramp
frame = gen.checkerboard(32) # 32×32 pixel checkerboard
frame = gen.solid(255, 0, 0) # solid red
frame = gen.ramp() # linear pixel index ramp (integrity testing)
frame = gen.grid(h_period=64, v_period=64) # grid linesLoad real images from disk and resize them to match a VideoTiming.
from cocotb_video_source import load_and_resize
frame = load_and_resize("test_card.png", timing) # (H, W, 3) uint8Individual helpers: load_image(path) → array, resize_to_timing(image, timing) → resized array.
Use make_video_driver to select a driver by name — useful when parametrising testbenches.
from cocotb_video_source import make_video_driver
drv = make_video_driver("axi_stream_camera", dut.clk, dut, timing, data_width=32)
drv = make_video_driver("axi_stream_buffered", dut.clk, dut, timing, data_width=64, pixels_per_cycle=2)
drv = make_video_driver("parallel", dut.clk, dut, timing)import cocotb
from cocotb.clock import Clock
from cocotb.triggers import ClockCycles
from cocotb_video_source import VideoTiming, PatternGenerator
from cocotb_video_source.drivers.axi_stream_camera import AXIStreamCameraDriver
TIMING = VideoTiming(
h_active=1280, h_fp=110, h_sync=40, h_bp=220,
v_active=720, v_fp=5, v_sync=5, v_bp=20,
)
@cocotb.test()
async def test_video_pipeline(dut):
cocotb.start_soon(Clock(dut.clk, 13, unit="ns").start()) # ~74.25 MHz
frame = PatternGenerator(TIMING).color_bars()
drv = AXIStreamCameraDriver(dut.clk, dut, TIMING, data_width=32)
await drv.send_frame(frame)
# The frame occupies exactly h_total * v_total clock cycles
assert int(dut.frame_done.value) == 1import random
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge
from cocotb_video_source import VideoTiming, PatternGenerator
from cocotb_video_source.drivers.axi_stream import AXIStreamVideoDriverBuffered
TIMING = VideoTiming(h_active=640, h_fp=16, h_sync=96, h_bp=48,
v_active=480, v_fp=10, v_sync=2, v_bp=33)
@cocotb.test()
async def test_backpressure(dut):
cocotb.start_soon(Clock(dut.clk, 40, unit="ns").start())
async def random_tready():
rng = random.Random(0)
while True:
dut.tready_override.value = int(rng.random() < 0.5)
await RisingEdge(dut.clk)
cocotb.start_soon(random_tready())
frame = PatternGenerator(TIMING).checkerboard(cell=32)
drv = AXIStreamVideoDriverBuffered(dut.clk, dut, TIMING, data_width=32)
await drv.send_frame(frame)
assert int(dut.crc_ok.value) == 1import cocotb
from cocotb.clock import Clock
from cocotb_video_source import VideoTiming, load_and_resize
from cocotb_video_source.drivers.parallel import ParallelVideoDriver
TIMING = VideoTiming(h_active=800, h_fp=40, h_sync=128, h_bp=88,
v_active=600, v_fp=1, v_sync=4, v_bp=23)
@cocotb.test()
async def test_scaler_input(dut):
cocotb.start_soon(Clock(dut.clk, 25, unit="ns").start())
frame = load_and_resize("reference.png", TIMING)
drv = ParallelVideoDriver(dut.clk, dut, TIMING)
await drv.send_frame(frame)The repository includes Verilator-based cocotb simulation tests. From tests/dut_stubs/:
# Parallel driver
make TOPLEVEL=parallel_loopback COCOTB_TEST_MODULES=test_parallel_driver
# AXI-Stream camera driver
make TOPLEVEL=axis_camera_monitor COCOTB_TEST_MODULES=test_axis_camera_driver
# AXI-Stream buffered driver (32-bit)
make TOPLEVEL=axis_loopback COCOTB_TEST_MODULES=test_axis_driver
# AXI-Stream buffered driver (64-bit)
make TOPLEVEL=axis_loopback COCOTB_TEST_MODULES=test_axis_driver_64 DATA_WIDTH=64
# AXI-Stream buffered driver (2 pixels per clock, 64-bit)
make TOPLEVEL=axis_loopback COCOTB_TEST_MODULES=test_axis_driver_2ppc DATA_WIDTH=64
# AXI-Stream buffered driver (2 pixels per clock, 48-bit)
make TOPLEVEL=axis_loopback COCOTB_TEST_MODULES=test_axis_driver_2ppc_48 DATA_WIDTH=48
# Backpressure tests
make TOPLEVEL=axis_loopback COCOTB_TEST_MODULES=test_backpressure
# Pure Python unit tests (no simulator required)
pytest tests/test_timing.py tests/test_packer.py tests/test_patterns.py tests/test_image_loader.pyMIT