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.
Passing an array around
The most familiar 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. However, this is quite a difficult interface to implement in an FPGA. 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.
This interface would look something like
entity ws2812<#N>(clk: clock, rst: bool, to_write: [Color; N]) -> bool {
// ...
}
"Function" to write single LED
Another option we might be tempted to try is to have an interface where you
"call" a function to set a specific LED. This is difficult to do in practice
however. In spade, one does not "call" a function, instead you instantiate a
block of hardware. One might work around that by passing something like an
Option<(Index, Color)>
to the driver, which updates the specified LED.
However, this is still not without flaws. First, we can't update a single LED, we need to send colors to all the LEDs before it too, so we'd need to store what the color of the other LEDs are. Second, it takes time to transmit the control signals, so one couldn't send new colors at any rate, the module must be ready to transmit before receiving the next command. This is technically solvable, but there are better options for this particular interface.
Letting the driver read from memory
Passing a reference is slightly more doable in an FPGA. Here, we might give the LED driver a read port to a memory from which it can read the color values at its own pace. This is certainly an option for us to use here, though spade currently doesn't have great support for passing read ports to memories around. Until that is mitigated, we'll look for other options
This might look something like this, but the MemoryPort is not currently supported in spade
entity ws2812<#N>(clk: clock, rst: bool, mem: MemoryPort<Color>, start_addr: int<20>) -> bool { // ... }
For those unfamiliar, the
#NumLjds
syntax means that the entity is generic over an integer calledNumLeds
.In current spade, one would have to write it as
struct Ws2812Out { signal: bool, read_addr: int<20>, } entity ws2812<#NumLeds>(clk: clock, rst: bool, memory_out: Color, start_addr: int<20>) -> Ws2812Out { // ... }
which decouples the
read_addr
frommemory_out
, and does not make clear the read delay between them.
Driver owned memory
Another, more spade- and FPGA friendly option is to have the driver itself own a memory where it stores the colors to write, and expose a write port to that memory for instanciators to write new values. This might look as follows:
entity ws2812<#NumLeds>(clk: clock, rst: bool, write_cmd: Option<int<20, Color>>) -> bool {
// ...
}
Just in time output
Finally, an interface which might be unfamiliar coming from the software world is to have the user generate the color on the fly, i.e. the user provides a translation from LED index to LED color. This is quite a nice setup as it doesn't intrinsically require any memory; if color selection is simple, it can be made on the fly. This interface is best demonstrated graphically
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.