Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Pipelines and Timing Agility

Timing, be it the maximum frequency that your hardware reaches, or the number of clock cycles a module needs to output its result is an unavoidable aspect of hardware design, yet most HDLs give you very little help when reasoning about timing. The pipelining system in Spade gives you a way to reason about timing together with the compiler, often allowing you to refactor your code to accommodate new functionality or improve your \(f_{max}\) without much thought. That is what we will explore in this first part of the tutorial.

Agile hardware design is all about gradually building up a project piece by piece, and accommodating any changes to requirements that arise as you develop. With this in mind, we will start out with an existing project which, if you haven't done so already can be cloned from

git clone https://gitlab.com/spade-lang/agile-tutorial.git

Inside this repo is a game folder which is where we will do the rest of this part of the tutorial. First, run simulation to see what we are working with so far

cd game
swim build && swim cmd cargo run

The first time you run it, it may take some time so let's discuss what this will do in the meantime. swim build will first compile the Spade compiler, then compile the Spade code to Verilog which can be fed to simulators and synthesis tools. swim cmd cargo run then simulates the resulting code, and interprets the VGA signals that it the simulation generates to render the result in a window.

Once the compilation is done, it should show a purple box moving around in a circle over a background of stars and for this "sprint", we are going to make the graphics a bit nicer.

Exploring the Existing Code

Before we do that however, let us familiarize ourselves with the code in the project. All the code is in the src folder and most of the code is in main.spade, with a few utilities in other files that we will discuss if needed. There is also a main.rs file in there which contains the logic that is used to interpret the VGA signals that the simulation emits, and show them to you in a window.

At the top of main.spade are a few mod and use statements which specify which other files are part of the project, and import a few things from other libraries. The details are not important for this tutorial.

After the imports is the definition of a GameState struct which, as the name implies, stores the current state of the game.

struct GameState {

Currently, it has three fields recording the position and angle of the player. The type of these fields is Fp<26, 8> which is a FixedPpoint number with 26 bits in total, and 8 fractional bits. You can do most normal arithmetic operations on fixed point numbers, though due to language limitations, it is currently not done with +, - etc., but with methods such as .add and .sub.

The next block we encounter is an impl block which is used to add methods to the GameState struct.

impl GameState {

The first method, called next, is a pipeline with a latency of 1, and its job is to compute the next game state from the current game state.

pipeline(1) next(...)

Currently, it does some trigonometry to the new location and angle of the player.

The second method, called draw_player, is (predictably) responsible for drawing the player.

pipeline(0) draw_player(...)

It gets passed a pixel and its job is to return the Color of the player at that pixel. Since the player is not present everywhere it returns an Option<Color> so it can return None on the pixels where the player should not be drawn

The logic to draw the purple box that currently represents the player works like this. First, the coordinates of the player are converted to integers so we can compare them to the pixel coordinates. Then, the player coordinates are subtracted from the pixel coordinates. If the result of that subtraction on the x-axis is greater than 0, we are on the left side of the player, and if it is smaller than the player width, the coordinate is inside the player. With the same logic on the y-axis we get in_x and in_y which are both true if the player should be drawn on the current pixel.

The next block is responsible for converting a pixel to a color for the whole screen, not just the player.

pipeline(0) pixel_to_color(...)

Its implementation is relatively simple, it computes both the background color and player color, and selects which one to draw based on whether or not the player color is present.

The previous functions take a pixel coordinate and convert it to a color

pipeline(1) render_pipeline(...)

In VGA and HDMI, there are some additional signals that must be generated. The render_pipeline pipeline get passed all the VGA signals with pixel coordinates, uses the pixel_to_color pipeline to convert from pixels to colors, while passing along the rest of the control signals. The details of this unit are slightly out of scope for this tutorial. You will have to come in here to adjust some of the pipelining numbers, but you shouldn't have to make functional changes.

The rest of the file contains the top level module that ties everything together. If you are curious, feel free to glance through it, but it shouldn't require any changes in this tutorial.

entity top(...)

Task 1

Your first task is to make the "player sprite" a little bit more interesting than the current purple block. To do so, there is a texture pipeline available in sprite.spade. You pass it an x and a y coordinate, and it returns the color value of the texture at that point, if any. Since it is read from a memory, it has a latency of 1.

Update the drawing code to use this texture for the player

Hint: To convert between int and uint, use .to_uint()

Task 2

One big advantage of the pipelining abstraction is that it allows you to add register and move existing registers around without affecting what is being computed. This is especially useful when trying to reach a desired target frequency, so let's try that!

The current background of stars is a bit boring, but there is a more fancy one available in src/background.rs called fancy_background. Let's try using it instead of the existing stars by replacing the stars instantiation in main.rs

// Replace
let background_color = inst(0) background::stars(clk, pixel);
// With
let background_color = inst(0) background::fancy_background(clk, pixel);

If you run this as usual, it should look different. It will most likely also run noticeably slower in simulation, since it has to simulate a more complex circuit for every pixel. This is something we can live with, but there is a bigger problem which becomes apparent if you synthesize the circuit

# Run synthesis and place & route
swim pnr

it reveals that we're quite far from hitting our target frequency of 50 MHz

[INFO] Place and route maximum frequencies:
$glbnet$_e_1739[88]: 306.1 MHz (target: 250 MHz)
$glbnet$_e_1739[89]: 13.4 MHz (target: 50 MHz)

(Ignore the strange names, those are an annoying consequence of how the code for clock generation in this example is written.)

Your job now is to fix this and make sure we can reach the required 50 MHz. Start by looking at the timing report to try to work out which part of the design is taking the longest.

Once you're done, re-simulate the circuit, does it still work?

NOTE: The second 250 MHz clock sometimes also fails timing. Don't worry about this for now, it is only used for driving the serial HDMI signal and is unrelated to what you are doing. 250 MHz is on the edge of what the ECP5 FPGA we're targeting is capable of so the randomness in the PnR process means it fails occasionally.

Extra task 1

With us back to reasonable timing, we can go on to more fancy graphics. The game state has an angle in addition to the x and y coordinates. It would be helpful to the player if the ship is drawn at that angle so let's do that.

src/trig.spade contains implementations of sin and cos which you can use with trig::sin and trig::cos. Use these functions to rotate the player sprite according to its angle.