Output Generation
First of all, the cause of the bug mentioned in the end of the last chapter was an incorrect equality check of the duration when transitioning between states. It should be
if duration >= trunc(t_ret -1) {
instead of
if duration >= t_ret {
Now that our state machine works, we have done most of the heavy lifting. We still need to translate our control signal into an actual LED output, which is what we’ll work on next.
First, we’ll define a type for storing colors as RGB values:
pub struct Color {
r: uint<8>,
g: uint<8>,
b: uint<8>,
}
Next, we will define a function to translate the OutputControl type returned by the state machine into control signals for the LEDs. This can only be done if the payload of the output control signals is a full color, i.e. OutputControl<Color>.
Recall that we will use the state machine to generateOutputControl<uint<N>>, let the user translate them intoOutputControl<Color>, then convert that to the final control signals.
The output is a single bool, the actual control signal to be passed to the LEDs. The interface will look like this
pub fn output_gen(control: OutputControl<Color>, timing: Timing) -> bool {
This conversion is relatively straight forward, the state machine gives us information about how long we’ve spent in each state, and we have to translate that into appropriate control signals. There is no state here, so we can do this with a function:
The OutputControl::Led has information about which of the 24 bits should be emitted. To translate that into a bit value, we’ll concatenate the color channels, and index the correct bit.
The full translation logic looks like this:
pub fn output_gen(control: OutputControl<Color>, timing: Timing) -> bool {
let t0h = timing.us0_4;
let t1h = timing.us0_8;
match control {
OutputControl::Ret => false,
OutputControl::Led$(payload: color, bit, duration) => {
let color_concat: uint<_> = (color.g `concat` color.r `concat` color.b);
let val = color_concat.to_bits()[trunc(23 - bit)];
let step_time = if val {t1h} else {t0h};
if duration > step_time {
false
}
else {
true
}
}
}
}
Testing
Again, it is good practice to test the module. Testing it is very similar to the state machine, except here we don’t have a clock. Instead, we’ll set a signal value, advance the simulation by a tiny time step, and assert the output. Here is an example of the test bench. Feel free to extend it with more tests that you think are reasonable. Here it might also be helpful to define some helper functions which check that a specific input gives a specific waveform, for example.
# top=output_gen
from spade import SpadeExt
import cocotb
from cocotb.triggers import Timer
@cocotb.test()
async def one_at_bit_0(dut):
s = SpadeExt(dut)
s.i.timing = """Timing$(
us280: 2800,
us0_4: 40,
us0_8: 80,
us0_45: 45,
us0_85: 85,
us1_25: 125,
)"""
# Sending 1 @ bit 0, time 0
s.i.control = "OutputControl::Led(Color$(g: 0b1000_0000, r: 0, b: 0), 0, 0)"
await Timer(1, units='ps')
s.o.assert_eq(True)
# Sending 1 @ bit 0, time 40
s.i.control = "OutputControl::Led(Color$(g: 0b1000_0000, r: 0, b: 0), 0, 40)"
await Timer(1, units='ps')
s.o.assert_eq(True)
# Sending 1 @ bit 0, time 80
s.i.control = "OutputControl::Led(Color$(g: 0b1000_0000, r: 0, b: 0), 0, 79)"
await Timer(1, units='ps')
s.o.assert_eq(True)
# Sending 1 @ bit 0, time 81
s.i.control = "OutputControl::Led(Color$(g: 0b1000_0000, r: 0, b: 0), 0, 81)"
await Timer(1, units='ps')
s.o.assert_eq(False)
With our tests now passing, we can finally run the code in hardware, which we will discuss in the next and final section of this chapter.