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 called NumLeds.

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 from memory_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.