Having perviously got my Neopixel PCB working with my RP2040 Board, i’ve be wanting to spend some time building up a FPGA example, that has some of the same functionality. For this I wanted to build an example in Verilog that could be deployed to my icebreaker FPGA devboard, with its ice40 FPGA which as 5K LUTs is a great match for this task with it serial over USB link and PMod Connectors.
The data for the WS2812B LED’s, are sent over a single serial wire, the data is transmitted in a series of pulses, where the pulse length indicates if the bit is a One or zero. The length of the pulse is defined in the datasheet, and can be plotted bellow, where the length of the pulse is used to encode the bits:
Now to transmit the bits from our FPGA ideally we would recreate the waveform exactly as in the Datasheet, but this would require a high level or precision. If we look closely at the Datasheet we can find that the WS2812B actually has a reasonable tolarance to timing errors for the length of the pulses:
This means we can make an approximation of the pulse, as long as it is within these tolarances, so we can use a simple clock divider, to generate the pulses. Creating the clock that is suitable to run the state machine to control the LED’s is a simple case of, reducing the clock rate from 12 MHz:
// clock rate for calculating the clock divider
parameter clk_divider_count = clk_in_rate_hz/clk_pixel_rate_hz;
//clock divider for setting the rate that the state can change
always @(posedge clk) begin
if (counter == clk_divider_count)
begin
counter <= 0;
pixel_clk <= ~pixel_clk;
end
else
counter <= counter + 1'b1;
end
This means we can then start to think about building a state machine for generating the waveform using the icebreakers 12MHz clock and this clock divider which with a clock divider producing our clock, We can look to breakdown the waveform into 4 states based on the period of this clock:
- Always high.
- High if binary 1, low if binary 0.
- High if binary 1, low if binary 0.
- Always Low.
We can then start to build a picture of how this will work with wavedrom:
These 4 states can then be used in the statemachine, which includes an ‘IDLE’ state when a bit is not being transmitted. There are then the for states shown in the diagram 1-4, with my_value
storing all the RGB values in the order they will be transmitted.
//state machine for sending each value
always @(posedge pixel_clk) begin
case(state)
IDLE:
begin
if (data_ready == 1)
state <= STATE1;
count_bit <= 24;
end
STATE1:
begin
//first part, always high
data_out <= 1'b1;
state <= STATE2;
count_bit <= count_bit - 1;
end
STATE2:
begin
// high for a 1, low for 0
data_out <= my_value[count_bit];
state <= STATE3;
end
STATE3:
begin
// high for a 1, low for 0
data_out <= my_value[count_bit];
state <= STATE4;
end
STATE4:
begin
// always low
data_out <= 1'b0;
if(count_bit == 0)
state <= IDLE;
else
state <= STATE1;
end
endcase
We can then string a series of these bits one after another to start building up the 3 bytes, that made up the command that is sen to the WS2812B LED. There is one byte to represent the Red, green and Blue values that we are setting the individual LED’s to.
The functionality for all the writing to pixels is self contained in the writepixel function, this allows me to define separate strings of WS2812B LED’s that are connected to my FPGA. in my case these are on custom build PMod Boards, and the icebreaker board can support 3 of them.
writepixel writepixel(CLK ,valid,
r_value[pixel_count-1][7:0], //Red
g_value[pixel_count-1][7:0], //Green
b_value[pixel_count-1][7:0], //Blue
pmod1a[0],
busy);
As the code wrapped in this block only writes the values to a single LED, we need to control both writing to a series of LED’s and then the pause to reset the first LED. This is needed because the first LED takes and keeps the first RGB values, and then passes all the following bytes to the next LED. This is all handled in my top level design file, where we have a state machine that feeds the values in:
always @(posedge CLK) begin
if (counter[WIDTH-1] == 1'b1)
begin
if ((busy == 1'b0)&(valid == 1'b0))
begin
if (pixel_count > (10-1))
begin
pixel_count <= 0;
counter <= 0;
end
else
begin
valid <= 1'b1;
pixel_count <= pixel_count + 1'b1;
end
end
end
else
begin
if (~busy)
counter <= counter + 1'b1;
end
if (busy == 1'b1)
valid <= 1'b0;
end
This function handles the process of setting the pixel_count
sequentially, until reaching the end of the array, and then resetting. Rather than having a set delay, I’m using a counter, that when the first bit is 1
it then works though the output values before being reset after all 10 RGB have been transmitted.
These RGB values are static values for now, hard coded when we program the FPGA. Building this code for the FPGA, which is using the open source tools to deploy to the FPGA, is completed with Yoysy and Next-PNR, there is also some testing with is carried out using Verilator, I will do a separate write up on extending the testing using Verilator to generate some coverage statistics and allow the LED’s RGB values to be set using a serial connection to a PC.