Featured image of post Controlling Neopixels with iceBreaker FPGA

Controlling Neopixels with iceBreaker FPGA

Building a Verilog Example to control Neopixels using an iceBreaker FPGA Board

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.

icebreaker FPGA and Neopixel PMod Board

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:

  1. Always high.
  2. High if binary 1, low if binary 0.
  3. High if binary 1, low if binary 0.
  4. 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.

Neopixel controlled with iceBreaker

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.

Built with Hugo
Theme Stack designed by Jimmy