In the previous instalment of this blog, we looked at the solution architecture for driving Adafruit Neopixels. We created that solution within the Vivado block diagram (see “Adam Taylor’s MicroZed Chronicles Part 33: Driving Adafruit RGB NeoPixel LED arrays with the Zynq SoC.”) We also maximized the use of existing IP blocks in the ARM-based Zynq SoC so that we only need to create one new hardware module to drive the NeoPixels. That’s this topic of this blog post.

Our NeoPixel driver will exhibit the following behavior:

Read address 0 within the block RAM to determine the number of pixels in the array If the number of pixels is not 0: Read the next address to obtain the red, green, and blue byte values for the first pixel to form a 24-bit word value for the pixel. Determine which waveform (one or zero) to output for the most significant bit of the 24-bit value read from memory. Output that waveform to the NeoPixel string. Increment the word-position counter (e.g. msb-1, msb-2….) Determine which waveform to output for the next bit. Repeat steps 6 and 7 above until the all 24 bits have been output. Check if the correct number of 24-bit pixel values has been output. If more 24-bit pixel values remain, repeat steps 3 to 9 above after incrementing memory address for the next pixel. If the correct number of pixels has been output, wait for the reset time to allow the pixels to latch in the pixel values. Repeat steps 1 to 11.

This algorithm sequence requires two elements that should be familiar to most FPGA engineers:

A state machine to interface with the dual-port RAM, to sequence the output bits, and to keep track of the number of pixels to be output (see my article in Xcell Journal issue 81, “How to Implement State Machines in Your FPGA”)

A shift register to generate the NeoPixel waveforms

The Adafruit NeoPixel uses self-clocking waveforms with transitions in different places depending on whether the bit value to be output is a one or a zero. We’ll use a 25-bit shift register to hold and output the waveform pattern for each of the Adafruit Neopixel bits. I’ve defined two constants, which are used to pre-load the shift register with the required one or zero pattern depending on the bit to be output. (See the attached code and test bench).

The length of the waveform pattern constants and the size of the shift register depend on the shift register’s clock rate. In this case we’re driving the shift register with FCLK_CLK1 from the PS side of the Zynq device, which is set to 20 MHz. That gives us a 50nsec clock period, which is important because the NeoPixel bit timing requires 50nsec resolution as shown in the device’s timing diagram and table below.

The overall waveform periods for a one or a zero differ as well. The specification calls for periods of 1.15µsec for a zero and 1.3µsec for a one. However, the NeoPixel’s overall bit-time tolerance is ±300nsec, so we can use a 1.25µsec time period for both one and zero waveforms and still meet specified timing. The waveform representing a one will be short by 50nsec and the waveform for a zero will be long by 100nsec—well within specified tolerances.

A 1.25µsec bit time clocked out at 50nsec per clock tick means that we’ll need a 25-bit shift register, which matches the code snippet shown above. Because we can create any kind of hardware we want in the Zynq SoC’s PL (programmable logic), a 25-bit shift register is no harder to create than any other size shift register.

Once I completed the RTL for this simple I/O module, the next step was to verify the design. All experienced engineers know that verification can take longer the original design. In this case, I used modelsim to create a simple test bench that stimulated the RTL and allowed me to confirm the proper behavior of the module. In the next blog we will look at the verification strategy I will be using to test this demonstration.