Skip to content

Commit 4ca9062

Browse files
committed
feat(export): add a cocotb testbench
Include an example testbench that validates the sync signals and captures three frames into PNG files. based on @htfab's work at https://github.com/htfab/ttsky-vga-example/blob/main/test/test.py
1 parent 30fbe0d commit 4ca9062

File tree

2 files changed

+130
-0
lines changed

2 files changed

+130
-0
lines changed

src/examples/export/test/test.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import cocotb
2+
from cocotb.clock import Clock
3+
from cocotb.triggers import ClockCycles
4+
5+
import os
6+
import glob
7+
import itertools
8+
from PIL import Image, ImageChops
9+
10+
11+
@cocotb.test()
12+
async def test_project(dut):
13+
14+
# Set clock period to 40 ns (25 MHz)
15+
CLOCK_PERIOD = 40
16+
17+
# Set VGA timing parameters matching hvsync_generator.v
18+
H_DISPLAY = 640
19+
H_FRONT = 16
20+
H_SYNC = 96
21+
H_BACK = 48
22+
V_DISPLAY = 480
23+
V_FRONT = 10
24+
V_SYNC = 2
25+
V_BACK = 33
26+
27+
# Number of frames to capture
28+
CAPTURE_FRAMES = 3
29+
30+
# Derived constants
31+
H_SYNC_START = H_DISPLAY + H_FRONT
32+
H_SYNC_END = H_SYNC_START + H_SYNC
33+
H_TOTAL = H_SYNC_END + H_BACK
34+
V_SYNC_START = V_DISPLAY + V_FRONT
35+
V_SYNC_END = V_SYNC_START + V_SYNC
36+
V_TOTAL = V_SYNC_END + V_BACK
37+
38+
# Palette mapping uo_out values to RGB color
39+
palette = [0] * 256
40+
for r1, r0, g1, g0, b1, b0 in itertools.product(range(2), repeat=6):
41+
red = 170*r1 + 85*r0
42+
green = 170*g1 + 85*g0
43+
blue = 170*b1 + 85*b0
44+
palette[b0<<6|g0<<5|r0<<4|b1<<2|g1<<1|r1<<0] = bytes((red, green, blue))
45+
46+
# Set up the clock
47+
clock = Clock(dut.clk, CLOCK_PERIOD, units="ns")
48+
cocotb.start_soon(clock.start())
49+
50+
# Reset the design
51+
dut.ena.value = 1
52+
dut.ui_in.value = 0
53+
dut.uio_in.value = 0
54+
dut.rst_n.value = 0
55+
await ClockCycles(dut.clk, 10)
56+
dut.rst_n.value = 1
57+
await ClockCycles(dut.clk, 2)
58+
59+
# Define some functions for capturing lines & frames
60+
61+
async def check_line(expected_vsync):
62+
for i in range(H_TOTAL):
63+
hsync = dut.uo_out[7].value.integer
64+
vsync = dut.uo_out[3].value.integer
65+
assert hsync == (1 if H_SYNC_START <= i < H_SYNC_END else 0), "Unexpected hsync pattern"
66+
assert vsync == expected_vsync, "Unexpected vsync pattern"
67+
await ClockCycles(dut.clk, 1)
68+
69+
async def capture_line(framebuffer, offset):
70+
for i in range(H_TOTAL):
71+
hsync = dut.uo_out[7].value.integer
72+
vsync = dut.uo_out[3].value.integer
73+
assert hsync == (1 if H_SYNC_START <= i < H_SYNC_END else 0), "Unexpected hsync pattern"
74+
assert vsync == 0, "Unexpected vsync pattern"
75+
if i < H_DISPLAY:
76+
framebuffer[offset+3*i:offset+3*i+3] = palette[dut.uo_out.value.integer]
77+
await ClockCycles(dut.clk, 1)
78+
79+
async def skip_frame(frame_num):
80+
dut._log.info(f"Skipping frame {frame_num}")
81+
await ClockCycles(dut.clk, H_TOTAL*V_TOTAL)
82+
83+
async def capture_frame(frame_num, check_sync=True):
84+
framebuffer = bytearray(V_DISPLAY*H_DISPLAY*3)
85+
for j in range(V_DISPLAY):
86+
dut._log.info(f"Frame {frame_num}, line {j} (display)")
87+
line = await capture_line(framebuffer, 3*j*H_DISPLAY)
88+
if check_sync:
89+
for j in range(j, j+V_FRONT):
90+
dut._log.info(f"Frame {frame_num}, line {j} (front porch)")
91+
await check_line(0)
92+
for j in range(j, j+V_SYNC):
93+
dut._log.info(f"Frame {frame_num}, line {j} (sync pulse)")
94+
await check_line(1)
95+
for j in range(j, j+V_BACK):
96+
dut._log.info(f"Frame {frame_num}, line {j} (back porch)")
97+
await check_line(0)
98+
else:
99+
dut._log.info(f"Frame {frame_num}, skipping non-display lines")
100+
await ClockCycles(dut.clk, H_TOTAL*(V_TOTAL-V_DISPLAY))
101+
frame = Image.frombytes('RGB', (H_DISPLAY, V_DISPLAY), framebuffer)
102+
return frame
103+
104+
# Start capturing
105+
106+
os.makedirs("output", exist_ok=True)
107+
108+
for i in range(CAPTURE_FRAMES):
109+
frame = await capture_frame(i)
110+
frame.save(f"output/frame{i}.png")
111+
112+
113+
@cocotb.test()
114+
async def compare_reference(dut):
115+
116+
for img in glob.glob("output/frame*.png"):
117+
basename = img.removeprefix("output/")
118+
dut._log.info(f"Comparing {basename} to reference image")
119+
frame = Image.open(img)
120+
ref = Image.open(f"reference/{basename}")
121+
diff = ImageChops.difference(frame, ref)
122+
if diff.getbbox() is not None:
123+
diff.save(f"output/diff_{basename}")
124+
assert False, f"Rendered {basename} differs from reference image"

src/exportProject.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { downloadZip } from 'client-zip';
22
import { Project } from './examples/Project';
3+
import testPy from './examples/export/test/test.py?raw';
34

45
const infoYaml = (topModule: string) =>
56
`
@@ -75,6 +76,11 @@ export async function exportProject(project: Project) {
7576
date: currentTime,
7677
input: infoYaml(project.topModule),
7778
},
79+
{
80+
name: 'test/test.py',
81+
date: currentTime,
82+
input: testPy,
83+
},
7884
...Object.entries(project.sources).map(([name, content]) => ({
7985
name: 'src/' + name,
8086
date: currentTime,

0 commit comments

Comments
 (0)