Tutorial 3: Video cores (top.beamrace)
Warning
This tutorial is not finished yet.
‘Beamracing’ is a simple method for creating interesting video patterns, by calculating the color of each pixel right before it is needed. It’s especially well-suited to FPGAs as we can build custom logic just for computing the value of each pixel, at the full video clock rate.
In gateware/src/top/beamrace, we have 2 files:
top.py: example gateware for a few different beamracing bitstreams.
sim.cpp: a Verilator testbench for simulatingbeamracebitstreams. This one writes each frame as an image to a bitmap file for inspection
Simulation
If you go ahead and run:
$ pdm beamrace sim --core=balls
You will notice the simulation emits some images like frameXX.bmp. If you open one up, you will see an interesting pattern. These images have been calculated by the Balls() core present in top.py, in response to some simulated audio inputs.
Building and --modeline
As these simple beamrace cores do not include a CPU (part of the reason they synthesize so quickly!), they also do not support dynamic framebuffer resizing.
For this reason, you’ll want to supply a --modeline argument depending on what screen you want to use. For example, for Tiliqua’s round screen, you will want something like:
$ pdm beamrace build --core=balls --modeline=720x720p60r2
Structure
Each beamrace core shares the same input and output interface, so they can all share the same BeamRaceTop wrapper core (which handles all the auxiliary logic outside the actual pixel color calculations).
You will notice each pattern has attributes like:
i: In(BeamRaceInputs())
o: Out(BeamRaceOutputs())
These have the following structure:
class BeamRaceInputs(wiring.Signature):
"""
Inputs into a beamracing core, all in the 'dvi' domain (at the pixel clock).
"""
def __init__(self):
super().__init__({
# Video timing inputs
"hsync": Out(1),
"vsync": Out(1),
"de": Out(1),
"x": Out(signed(12)),
"y": Out(signed(12)),
# Audio samples (already synchronized to DVI domain)
"audio_in0": Out(signed(16)),
"audio_in1": Out(signed(16)),
"audio_in2": Out(signed(16)),
"audio_in3": Out(signed(16)),
})
class BeamRaceOutputs(wiring.Signature):
"""
Outputs from a beamracing core, all in the 'dvi' domain (at the pixel clock).
"""
def __init__(self):
super().__init__({
"r": Out(8),
"g": Out(8),
"b": Out(8),
})
In this fashion, each core already has the current audio sample and position in the frame, as well as timing signals, to use for pattern generation.
Note that all signals are already synchronized into the video domain, with the dvi domain already remapped to sync, so from the perspective of your own pattern-generating core, you can do everything in the sync domain.
TODO
Finish writing this.