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

Driver interface

Now that we know how we should talk to the LEDs, we should also consider how we want the interface to our library to work. Here we have a few options with various trade-offs.

When coming from a software background, you may be tempted to define a function which when “called” sends the LED commands to update a specific LED, but this is easier said than done.

First, in hardware, we cannot “call” a function, we instantiate a piece of hardware that always exists, and does its job over time. Since the LED protocol requires sending all the LED values again to update one individual LED, our driver also needs to know the state of the full LED list, not just the one being updated.

Generally, we will have the driver continuously send update the LEDs, with some way for another module to configure the LED values.

The output of the whole system will be a single bool which we connect to the LEDs.

For the input, we have a few options:

Passing an array around

The most familiar way coming from a software world might be for the library to take a copy or a reference to an array containing the values to set the LEDs to.

This interface in Spade would look something like this

entity ws2812<#uint N>(clk: clock, rst: bool, led_values: [Color; N]) -> bool {
// ...
}

The #uint N indicates that this entity is generic over the number of LEDs

However, this is quite a difficult interface to implement in an FPGA since all signals we want to read have to be passed as physical wires. If we were to copy the LED values, we would need 24 bits per LED to be connected between the driver and user. Those bits would need individual wires, so the number of wires would quickly grow very large. Even at 1 bit per color channel it gets unwieldy:

+-------------------------------------------------+
| generator |
+-------------------------------------------------+
| | | | | | | | | | | | | | | | | |
r g b r g b r g b r g b r g b r g b
v v v v v v v v v v v v v v v v v v
+-------------------------------------------------+
| generator |
+-------------------------------------------------+

Letting the driver read from memory

Another option is to pass a memory port to the driver from which it can read one LED value every clock cycle. This allows another module to write the desired values to the memory while the driver reads the data. For this tutorial, the details are slightly out of scope, but it is still useful to know about it as a possibility.

This interface would look roughly like this:

struct port ReadPort {
addr: &uint<8>,
value: inv &Color,
}

entity ws2812<#uint N>(clk: clock, rst: bool, memory: ReadPort) -> bool {
// ...
}

Just in time output

While a memory based solutions work, they do require something to fill the memories with color values for each LED. In some cases, this is exactly what we want, but in other cases, such as when generating a pattern it may be better to generate the color for each LED as required by the application.

This leads to another possible implementation which is a bit different than what you may be used to in software. It consists of a two stage approach where we start off by generating “generic” control signals that only include an LED index instead of the concrete color to emit for each LED. Then, we pass that through a user-provided color selection/transformation function.

       Control
signals
|
v
+---------------+
| State machine |
+---------------+
|
v
+-------------------+
| User provided |
| color translation |
+-------------------+
|
v
+------------------+
| Output generator |
+------------------+
|
V
LED
Signals

Here, as driver implementors we are responsible for providing the state machine, whose output would be some signal which says “Right now, we should emit byte B of the color for LED N”. We’ll represent it by an enum

The color translator translates that into a known color, and the output generator generates the signals that actually drive the LED.

In some sense, this interface is the most general. Both the driver owned memory version, as well as the memory read port version can be implemented by plugging the read logic into the translation part. For that reason, we will implement this interface first.

With all that discussion on interfaces out of the way, it is finally time to start implementing things. The next section will introduce the finite state machine, a real work horse in any Spade project.