I created a PMOD module PCB using KiCAD, that enables connecting WS2812B lighting strips to an FPGA board with a PMOD interface. The board was assembled by JLCPCB.
This is my first project using an FPGA, I plan to soon implement an SPI interface with the FPGA, to accept colour pixels via SPI from a raspberry pi, to then drive the LEDs appropriately. I am making use of the original Zybo board which uses a Zynq FPGA, although I’m not using the ARM portion of this chip as I want to learn VHDL.
I am currently making use of BRAM to store the colour data.
I made use of the 74AHCT245 chip to convert PMOD signals from the FPGA from 3.3V logic to 5V logic. This chip is powered by the 5V supply from the WS2812B led strips.
The board will support up to 8 LED strips.
I made use of 8x 2.54mm pin headers, for allowing connection of the LED strips. I attached dupont sockets to JST SM wires I bought, via a crimper, which connect the strips themselves to my board.
As you can see below the schematic is very simple
The diodes are present so multiple 5V PSUs could be connected to the board, see https://www.edn.com/fundamentals-of-power-system-oring/ for more information.
I made use of the PMOD connector footprint from https://github.com/mithro/kicad-pmod so I got the correct pin numberings and silkscreen, as well as allowing me to position the 2x 6 pin header towards the edge of the board correctly 🙂
The following image shows the PMOD module connected to WS2812B lighting strips and my Zybo fpga board.
I had a number of problems when creating the VHDL, one of which was an abnormal termination with Vivado during synthesis. This is a bug with Vivado which is currently still present in 2020.1, to fix this I was told to add the following, synthesis was then able to complete 🙂
attribute dont_touch : string; attribute dont_touch of colour_counter : signal is "true";
I also ran into a bug with the time datatype where 32 bit floats are used rather than 64 bit, meaning precision is lost, the VHDL appeared to work ok in simulation but failed in implementation. I fixed this by making use of ‘real’ instead. See the xilinx forum for more information.
Also currently because I’m not using one of the Vivado 2020.1 supported OSs, I had to fake my /etc/os-release file to pretend to be Ubuntu 16.04, otherwise an exception would occur in the setup.
Originally I used 50uS for the refresh period, however I ran into issues with the display, after re-reading the datasheet it had to be greater than this value, I’m currently using 74uS. My calculations are based on the FPGA clock, which in this case is 125MHz.
library IEEE; use IEEE.NUMERIC_STD.ALL; use IEEE.STD_LOGIC_1164.ALL; use IEEE.MATH_REAL.ALL; use IEEE.MATH_REAL; entity lighting is port ( clk: in std_logic; -- 125MHz clock strip_1: out std_logic -- Output to first header pins on my PMOD board ); end lighting; architecture Behavioral of lighting is constant size: integer := 60 * 4; -- Number of LEDs constant tp_clock: real := 1.0 / 125.0e6; -- Get time period, for 125MHz clock type ram is array(0 to (size*2)-1) of unsigned(0 to 23); -- allocate BRAM for double buffering signal vram : ram := (others => "000000000000000000000000"); -- initialise signal led_bits: unsigned(0 to 23) := "000000000000000000000000"; -- each LED is 24 bits, we output the same bitstream to all LEDs signal long: integer := integer(0.85e-6 / tp_clock); -- long duration signal short: integer := integer(0.4e-6 / tp_clock); -- short duration signal refresh: integer := integer((74.0e-6) / tp_clock); -- refresh duration, when strip is driven low, MUST be ABOVE 50uS signal clock_counter: integer := 0; -- Counts clock pulses signal led_bit_counter: integer := 0; -- Counts the Nth bit of an LED's colour data (24 bits total) signal led_counter: integer := 0; -- LED number we're sending data for signal pulse: integer := 0; -- Keeps track if we are outputting a high signal spi_counter: integer := 0; -- Counts the bit from SPI for the Nth bit of a single LED signal spi_frame_counter: integer := 0; -- Data for the Nth LED signal spi_data: std_logic_vector(0 to 23) := "000000000000000000000000"; signal vram_part: bit := '0'; -- Writing SPI data, to first or second part of BRAM signal vram_part_tmp: bit := '0'; -- mirrors above, for choosing where to read from BRAM signal colour_counter: unsigned(23 downto 0) := "000000000000000000000000"; -- Counters address of BRAM for writing to attribute dont_touch : string; -- Hack so synthesis works attribute dont_touch of colour_counter : signal is "true"; -- Hack so synthesis works signal read_addr: integer := 0; -- Store address to read data from of double buffer signal write_addr: integer := 0; -- Store address to write data to in double buffer begin fill_bram : process(clk) begin if rising_edge(clk) then if vram_part = '0' then write_addr <= 0 ; else write_addr <= size ; end if; if colour_counter >= size then colour_counter <= "000000000000000000000000"; vram_part <= not vram_part; else if colour_counter = 3 then -- mark specific LED vram(to_integer(colour_counter)+write_addr) <= "111111110000000000000000"; else if colour_counter(0) = '1' then -- generate alternating pattern vram(to_integer(colour_counter)+write_addr) <= "000000000000000011111111"; else vram(to_integer(colour_counter)+write_addr) <= "000000001111111100000000"; end if; end if; colour_counter <= colour_counter + 1; end if; end if; end process fill_bram; write_leds: process(clk) begin if rising_edge(clk) then -- Counting clock edges clock_counter <= clock_counter + 1; if vram_part_tmp = '0' then read_addr <= led_counter+size ; else read_addr <= led_counter ; end if; led_bits <= vram(read_addr); -- Check if we've reached end of LED strip if led_counter = size then -- Check if we have finished refresh duration if clock_counter > refresh then clock_counter <= 0; led_counter <= 0; vram_part_tmp <= vram_part; else strip_1 <= '0'; end if; else -- Check if at the end of a WS2812B '0' or '1' if clock_counter > short+long then clock_counter <= 0; led_bit_counter <= led_bit_counter + 1; if led_bit_counter = 23 then led_bit_counter <= 0; led_counter <= led_counter + 1; end if; elsif led_bits(led_bit_counter) = '1' and clock_counter > long then strip_1 <= '0'; elsif led_bits(led_bit_counter) = '0' and clock_counter > short then strip_1 <= '0'; else strip_1 <= '1'; end if; end if; end if; end process write_leds; end Behavioral;
FPGA LUTs – I found this interesting regarding how the LUTs are formed in an FPGA for creating different logic operations.
Intel FPGA talk – I thought this introductory talk into fpgas was quite nice, covering the different logic elements etc.
Hamsterworks – Lots of really interesting VHDL projects (on archive.org, original site is now down alas)