Tutorial: WS2812B / NeoPixel RGB LED Programming#


What is WS2812?#

RGB LEDs are optoelectronic devices that integrate three light-emitting diodes (LEDs) of Red, Green, and Blue (RGB) colors into a single package. Typical RGB LEDs have four pins:

  • A common pin (either common cathode or common anode)
  • Three pins for driving the RGB LEDs

Modern RGB LEDs often include built-in control circuitry, which can be programmed to display different RGB colors and brightness levels.

The WS2812, WS2812B (an improved version of the WS2812), and SK6812 are popular examples of programmable RGB LED ICs, typically available in an SMD5050 package. They can be programmed using a single digital signal and can be cascaded using the DIN and DOUT pins for input and output, respectively.

Figure: WS2812 vs. WS2812B Module

Figure: Single-Pixel WS2812B Modules

Figure: Multi-Pixel WS2812B Modules

Notes:

  • WS2812 devices typically operate at 5V, but some variants can also work at 3.3V with reduced brightness.
  • WS2812-based RGB LED modules are also known under the brand name NeoPixel by Adafruit Industries.
  • When using a 5V WS2812B device and the DIN data signal is at 3.3V logic level, a logic level shifter (from 3.3V to 5V) is required (e.g., SN74HCT245).

 


WS2812 Programming#

To set the color of an RGB LED in a WS2812 module, 3 bytes (or 24 bits) of data are required per RGB LED (or pixel). A value of 0 represents the lowest brightness level, and 255 represents the highest brightness level for each color component. The WS2812 module uses the DIN pin to receive data from another device, such as a microcontroller. Data is shifted in a bit-serial manner.

When multiple WS2812 LEDs are connected in a chain, data bits are fed into the DIN pin of the first module and propagated through its DOUT pin to the next module in a daisy-chained (cascading) configuration. Each LED (or pixel) requires 24 bits to define its RGB color. Therefore, for LEDs, a total of bits, or bytes, must be sent. The first 24 bits (or three bytes) shifted in will be used by the first RGB LED in the chain and are removed; the remaining data is shifted out through the DOUT pin to the next LED.

 


Data Transmission Protocol#

Questions - How does the WS2812 module determine whether each bit is a 0 or a 1? - How is the boundary of each bit defined?

WorldSemi, the company that manufactures the WS2812B chip, has defined a data transmission protocol. According to the WS2812 & WS2812B datasheets, each bit is encoded using pulse-width modulation (PWM) in the time domain, with durations measured in microseconds. The duration of the High and Low voltage levels within each bit determines whether it represents a 0 or a 1.

Figure: WS2812B Bit Timing

Pulse Timing Definitions#

  • T1H: Duration of High signal for bit 1
  • T1L: Duration of Low signal for bit 1
  • T0H: Duration of High signal for bit 0
  • T0L: Duration of Low signal for bit 0
  • TH + TL: Total duration of one bit (bit time)
  • Reset: A Low signal duration (>= 50 µs) used to signal the end of a transmission frame.

WS2812 Timing Parameters

Bit Value High Time (T_H) Low Time (T_L)
0 0.40 µs ± 150 ns 0.85 µs ± 150 ns
1 0.80 µs ± 150 ns 0.45 µs ± 150 ns

WS2812B Timing Parameters

Bit Value High Time (T_H) Low Time (T_L)
0 0.35 µs ± 150 ns 0.90 µs ± 150 ns
1 0.90 µs ± 150 ns 0.35 µs ± 150 ns
  • Total Bit Time (T_H + T_L): 1.25 µs ± 600 ns
  • Reset Signal: ≥ 50 µs of Low level

Based on a bit time of 1.25 µs, the effective data transmission frequency is approximately: 1 / 1.25 µs ≈ 800 kHz

Data Format and Bit Order

  • Bit order within each byte is MSB first (Most Significant Bit first).
  • The byte order sent to the WS2812B follows GRB format:
    • First byte: Green (G)
    • Second byte: Red (R)
    • Third byte: Blue (B)

VHDL Coding: WS2812B Driver#

The ws2812b_driver module provided below is used to shift out 24-bit data to a WS2812B module. When the control signal WR (Write Strobe) is high, the 24-bit data on the DATA pins is written into an internal register of the module. The BUSY signal changes from 0 to 1, and the data is shifted out through the DOUT pin. After all 24 bits are sent, the BUSY signal changes from 1 back to 0.

-- File: ws2812b_driver.vhd
LIBRARY IEEE;
USE IEEE.STD_LOGIC_1164.ALL;
USE IEEE.NUMERIC_STD.ALL;

ENTITY ws2812b_driver IS
    GENERIC (
        CLK_HZ : INTEGER := 50_000_000 -- Input clock frequency
    );
    PORT (
        CLK   : IN STD_LOGIC; -- Clock input
        RST_N : IN STD_LOGIC; -- Async reset active low
        WR    : IN STD_LOGIC; -- Write strobe input
        DATA  : IN STD_LOGIC_VECTOR(23 DOWNTO 0); -- 24-bit GRB color data
        BUSY  : OUT STD_LOGIC; -- High when sending data
        DOUT  : OUT STD_LOGIC -- Serial data output to WS2812
    );
END ws2812b_driver;

ARCHITECTURE behavioral OF ws2812b_driver IS
    -- Timing constants
    CONSTANT CLK_TICK_US : INTEGER := CLK_HZ / INTEGER(1e6);
    CONSTANT T0H : INTEGER := (CLK_TICK_US * 400/1000); -- '0' bit high time 
    CONSTANT T0L : INTEGER := (CLK_TICK_US * 850/1000); -- '0' bit  low time
    CONSTANT T1H : INTEGER := (CLK_TICK_US * 800/1000); -- '1' bit high time
    CONSTANT T1L : INTEGER := (CLK_TICK_US * 450/1000); -- '1' bit  low time

    -- FSM states
    TYPE state_type IS (IDLE, LOAD, SEND_BIT_H, SEND_BIT_L);
    SIGNAL state : state_type := IDLE;

    -- Internal signals
    SIGNAL data_reg  : STD_LOGIC_VECTOR(23 DOWNTO 0) := (OTHERS => '0');
    SIGNAL bit_index : INTEGER RANGE 0 TO 23 := 23;
    SIGNAL clk_counter : INTEGER;
    SIGNAL busy_reg : STD_LOGIC := '0';
    SIGNAL dout_reg : STD_LOGIC := '0';

    SIGNAL cnt_t_high : INTEGER := 0;
    SIGNAL cnt_t_low  : INTEGER := 0;

BEGIN

    -- output signal assignments
    BUSY <= busy_reg;
    DOUT <= dout_reg;

    PROCESS (CLK, RST_N)
    BEGIN
        IF RST_N = '0' THEN
            state <= IDLE;
            busy_reg <= '0';
            dout_reg <= '0';
            clk_counter <= 0;
            bit_index <= 23;
            data_reg <= (OTHERS => '0');
            cnt_t_high <= 0;
            cnt_t_low <= 0;

        ELSIF rising_edge(CLK) THEN

            CASE state IS
                WHEN IDLE =>
                    busy_reg <= '0';
                    dout_reg <= '0';
                    clk_counter <= 0;
                    bit_index <= 23;
                    IF WR = '1' THEN
                        data_reg <= DATA;
                        busy_reg <= '1';
                        state <= LOAD;
                    END IF;

                WHEN LOAD =>
                    IF data_reg(23) = '1' THEN -- MSB timing
                        cnt_t_high <= T1H;
                        cnt_t_low <= T1L;
                    ELSE
                        cnt_t_high <= T0H;
                        cnt_t_low <= T0L;
                    END IF;
                    clk_counter <= 0;
                    dout_reg <= '1';
                    state <= SEND_BIT_H;

                WHEN SEND_BIT_H =>
                    clk_counter <= clk_counter + 1;
                    IF clk_counter = cnt_t_high THEN
                        dout_reg <= '0';
                        clk_counter <= 0;
                        state <= SEND_BIT_L;
                    END IF;

                WHEN SEND_BIT_L =>
                    clk_counter <= clk_counter + 1;
                    IF clk_counter = cnt_t_low THEN
                        IF bit_index = 0 THEN -- LSB sent
                            dout_reg <= '0';
                            clk_counter <= 0;
                            busy_reg <= '0';
                            state <= IDLE;
                        ELSE
                            -- Shift data left by 1 (MSB first)
                            data_reg <= data_reg(22 DOWNTO 0) & '0';
                            bit_index <= bit_index - 1;
                            -- Setup timings for next bit
                            IF data_reg(22) = '1' THEN
                                cnt_t_high <= T1H;
                                cnt_t_low <= T1L;
                            ELSE
                                cnt_t_high <= T0H;
                                cnt_t_low <= T0L;
                            END IF;
                            clk_counter <= 0;
                            dout_reg <= '1';
                            state <= SEND_BIT_H;
                        END IF;
                    END IF;

                WHEN OTHERS =>
                    state <= IDLE;

            END CASE;
        END IF;
    END PROCESS;

END behavioral;

 

The following module (ws2812b_driver_demo) shows how to instantiate the the ws2812b_driver driver and can be used to drive a single-pixel WS2812B module. There are a set of predefined color bits (24 bits for each color) which will be applied to the WS2812B in sequence.

LIBRARY IEEE;
USE IEEE.STD_LOGIC_1164.ALL;
USE IEEE.NUMERIC_STD.ALL;

ENTITY ws2812b_driver_demo IS
    PORT (
        CLK   : IN STD_LOGIC; -- 50MHZ clock
        RST_N : IN STD_LOGIC;
        DOUT  : OUT STD_LOGIC
    );
END ws2812b_driver_demo;

ARCHITECTURE rtl OF ws2812b_driver_demo IS

    COMPONENT ws2812b_driver
        GENERIC (
            CLK_HZ : INTEGER := 50_000_000
        );
        PORT (
            CLK   : IN STD_LOGIC;
            RST_N : IN STD_LOGIC;
            WR    : IN STD_LOGIC;
            DATA  : IN STD_LOGIC_VECTOR(23 DOWNTO 0);
            BUSY  : OUT STD_LOGIC;
            DOUT  : OUT STD_LOGIC
        );
    END COMPONENT;

    --------------------------------------------------------------------
    -- Color ROM (8 test colors: GRB format)
    --------------------------------------------------------------------
    TYPE rom_type IS ARRAY (0 TO 7) OF STD_LOGIC_VECTOR(23 DOWNTO 0);
    CONSTANT color_rom : rom_type := (
        x"00FF00",
        x"FF0000",
        x"0000FF",
        x"FFFF00",
        x"00FFFF",
        x"FF00FF",
        x"FFFFFF",
        x"000000"
    );
    -- FSM states
    TYPE state_type IS (IDLE, WRITE_DATA, WAIT_BUSY);
    SIGNAL state : state_type := IDLE;

    SIGNAL rom_index : INTEGER RANGE 0 TO 7 := 0;
    SIGNAL wr_strobe : STD_LOGIC := '0';
    SIGNAL busy : STD_LOGIC;
    SIGNAL delay_cnt : INTEGER := 0;
    SIGNAL data : STD_LOGIC_VECTOR(23 DOWNTO 0);
    CONSTANT CLK_HZ : INTEGER := 50e6;
    CONSTANT DELAY_MAX : INTEGER := CLK_HZ/2; -- use a smaller value for simulation

BEGIN

    data <= color_rom(rom_index);

    --------------------------------------------------------------------
    -- DUT: WS2812B Driver Instance
    --------------------------------------------------------------------
    inst_driver : ws2812b_driver
    GENERIC MAP(
        CLK_HZ => CLK_HZ
    )
    PORT MAP(
        CLK   => CLK,
        RST_N => RST_N,
        WR    => wr_strobe,
        DATA  => data,
        BUSY  => busy,
        DOUT  => DOUT
    );

    --------------------------------------------------------------------
    -- FSM Controller to loop through color_rom
    --------------------------------------------------------------------
    PROCESS (CLK, RST_N)
    BEGIN
        IF RST_N = '0' THEN
            wr_strobe <= '0';
            rom_index <= 0;
            state <= IDLE;
            delay_cnt <= 0;

        ELSIF rising_edge(CLK) THEN
            CASE state IS
                WHEN IDLE => -- Idle: wait until not busy
                    IF busy = '0' THEN
                        wr_strobe <= '1';
                        state <= WRITE_DATA;
                    END IF;

                WHEN WRITE_DATA => -- Issue write strobe
                    IF busy = '1' THEN
                        wr_strobe <= '0';
                        state <= WAIT_BUSY;
                    END IF;

                WHEN WAIT_BUSY => -- Wait for delay before next color
                    IF busy = '0' THEN
                        IF delay_cnt < DELAY_MAX THEN
                            delay_cnt <= delay_cnt + 1;
                        ELSE
                            delay_cnt <= 0;
                            rom_index <= (rom_index + 1) MOD 8;
                            state <= IDLE;
                        END IF;
                    END IF;

                WHEN OTHERS =>
                    state <= IDLE;
            END CASE;
        END IF;
    END PROCESS;

END rtl;

 


VHDL Simulation#

The following VHDL testbench (ts_ws2812b_driver_demo) is provided for simulation.

-- File: ts_ws2812b_driver_demo.vhd

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

entity tb_ws2812b_driver_demo is
end tb_ws2812b_driver_demo;

architecture sim of tb_ws2812b_driver_demo is
    -- Clock period definition
    constant CLK_PERIOD : time := 20 ns; -- 50 MHz

    -- DUT signals
    signal t_CLK    : std_logic := '0';
    signal t_RST_N  : std_logic := '0';
    signal t_DOUT   : std_logic;

    -- DUT instance
    component ws2812b_driver_demo
        port (
            CLK   : in  std_logic;
            RST_N : in  std_logic;
            DOUT  : out std_logic
        );
    end component;

begin
    -- Instantiate the DUT
    uut: ws2812b_driver_demo
        port map (
            CLK   => t_CLK,
            RST_N => t_RST_N,
            DOUT  => t_DOUT );

    -- Clock generation
    clk_process : process
    begin
        while true loop
            t_CLK <= '0';
            wait for CLK_PERIOD / 2;
            t_CLK <= '1';
            wait for CLK_PERIOD / 2;
        end loop;
    end process;

    -- Reset and simulation control
    stim_proc : process
    begin
        -- Apply reset
        t_RST_N <= '0';
        wait for 100 ns;
        t_RST_N <= '1';
        wait;
    end process;

end sim;

-- ghdl -a ws2812b_driver.vhd ws2812b_driver_demo.vhd tb_ws2812b_driver_demo.vhd 
-- ghdl -e tb_ws2812b_driver_demo
-- ghdl -r tb_ws2812b_driver_demo --vcd=waveform.vcd --stop-time=10ms

Figure: Simulation waveforms

 


Output Signal Measurement#

The VHDL source code has been compiled, and the resulting bitstream was loaded onto the MAX10-Lite FPGA board. The waveform of the output signal, measured by a digital oscilloscope, is shown below.

Figure: MAX10-Lite FPGA board + WS2812 module

Figure: Waveforms of the output signal

Figure: Waveforms of the output signal (bit value = 0)

Figure: Waveforms of the output signal (bit value = 1)

 


This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Created: 2025-07-02 | Last Updated: 2025-07-02