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 for more information.


I made use of the PMOD connector footprint from 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;

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


        fill_bram : process(clk)
            if rising_edge(clk) then
                if vram_part = '0' then
                    write_addr <= 0 ;
                    write_addr <= size  ;
                end if;
                if colour_counter >= size then
                    colour_counter <= "000000000000000000000000";
                    vram_part <= not vram_part;
                    if colour_counter = 3 then   -- mark specific LED
                        vram(to_integer(colour_counter)+write_addr) <=     "111111110000000000000000";
                        if colour_counter(0) = '1' then -- generate alternating pattern
                            vram(to_integer(colour_counter)+write_addr) <= "000000000000000011111111";
                            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)
            if rising_edge(clk) then
                -- Counting clock edges
                clock_counter <= clock_counter + 1;
                if vram_part_tmp = '0' then
                    read_addr <= led_counter+size ;
                    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;
                        strip_1 <= '0';                    
                    end if;                
                    -- 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';
                        strip_1 <= '1';
                    end if;     
                end if;        
            end if;      
        end process write_leds;
end Behavioral;

Board design

FPGA links

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, original site is now down alas)

Leave Comment

Error Please check your entries!