Skip to content

WS2812B Addressable LED Driver Documentation

Overview

The WS2812B driver found in the driver/ folder of Embedded-Sharepoint is a thread-safe driver meant to run in a FreeRTOS task. The driver utilizes timers and a DMA channel to output the necessary PWM waveform to set the addressable LEDs to different colors. A mutex is used to ensure mutual exclusion of an addressable led strip, and a semaphore is included to indicate when a DMA transaction is done. The driver also supports multiple strings of addressable LEDs.

The struct below represents a string of LEDs,

typedef struct{
    uint8_t (*ledData)[NUMBER_PWM_DATA_ELEMENTS]; // Represents the colors contained in the strip: [LED][LEDNUM, G, R, B]
    uint16_t *pwmBuffer; // PWM bitstream of duty cycles, this is what is passed to DMA
    TIM_HandleTypeDef *timerHandle; // The timer handle used to generate PWM
    uint32_t channel;  // The channel associated with the pin's timer 
    uint8_t numberLeds; // the number of LEDs in the string
    SemaphoreHandle_t mutex; // protects multiple threads from writing to the handle
    StaticSemaphore_t mutexBuf; // static buffer for the mutex
    volatile uint8_t dmaActive; // indicates when a dma transmission is active
    SemaphoreHandle_t framePendingSem; // indicates that there's a new rgb frame to send
    StaticSemaphore_t framePendingBuf; // static buffer to store the semaphore
}ws2812b_handle_t;

Most functions in this driver return a ws2812b_status_t enum which stores the success status of that function.

typedef enum{
    WS2812B_OK, // WS2812B transaction completed successfully
    WS2812B_NULL_ERROR, // parameter is NULL
    WS2812B_ERROR, // an error occured
    WS2812B_BUSY // a shared resource is busy
}ws2812b_status_t;

CubeMX Setup

Before initializing the driver, you must configure your system clock, the PWM timer pin, and the DMA channel used.

Configure the system clock to 80mhz, instructions on how to do that can be found here in section 3.3.

Timer Configuration

Enable your timer and channel in PWM mode. Below I'm using Timer 4 channel 1, and configuring it for PWM generation WS2812B PWM Generation

Set the counter period to be 1000, the prescaler to be 0, and counter mode to be up. WS2812B Timer Configuration

DMA Configuration

  • Set the DMA request to be your timer and channel
  • Set direction to be Memory to Peripheral
  • Set data width to be Half word
  • Set mode to be Normal

WS2812B DMA Configuration

Generated code

Take the following generated functions from CubeMX:

  • SystemClock_Config
  • MX_DMA_Init
  • HAL_TIM_PWM_MspInit
  • MX_TIMX_Init <- the X refers to which timer you're using, the test uses TIM4, so it's MX_TIM4_Init
  • HAL_TIM_MspPostInit
  • HAL_TIM_PWM_MspDeInit

Priority Configuration

There is a max priority we can configure interrupts to be that use FreeRTOS functions. Make sure that your interrupts priority is >= this variable configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY.

Look at the call to the NVIC Set Priority function

  # sets the DMA1_Channel1_IRQ to be 4 + configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
  HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY + 4, 0);

Driver Initialization

The driver can be initialized by calling the below function:

ws2812b_status_t ws2812b_init(
    ws2812b_handle_t *ledHandler, 
    uint8_t ledData[][NUMBER_PWM_DATA_ELEMENTS], 
    uint16_t *pwmData,  // an EMPTY and non-NULL pointer to a 
    TIM_HandleTypeDef *timerHandle,  // the timer handle
    uint32_t channel, // the timer channel
    uint8_t numberLeds // Number of LEDs you want in your strip
)

It is recommended that this function is called in the context of an RTOS task after the scheduler has started. The system clock, timer, and dma must be configured before calling the ws2812b_init function.

Note that ledHandler, ledData, pwmData, timerHandle must be statically allocated by the user before it is passed into the init function, or else the driver will return WS2812B_NULL_ERROR.

On success the init function will return WS2812B_OK.

Callback Function

This driver makes use of the HAL_TIM_PWM_PulseFinishedCallback within the HAL. This interrupt is called after the PWM's duty cycle is set. To allow the user to use this interrupt for other things, this callback function is not stored in the driver, and instead it is the user's responsibility to call a specified callback function within the interrupt. The user can call ws2812b_TIM_PWM_PulseFinishedCallback in the interrupt service routine.

// the interrupt lives inside the ISR
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    // if the timer is the one used by the ws2812b driver
    if (htim->Instance == TIM4)
    {
        // call the hook function.
        ws2812b_TIM_PWM_PulseFinishedCallback(htim, &wsHandle, &xHigherPriorityTaskWoken);
    }
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

Note that since HAL_TIM_PWM_PulseFinishedCallback is an interrupt, you want to keep it short and bounded, and only use the ISR safe functions in FreeRTOS (they usually have a fromISR tag at the end of the function name).

You need to add the DMA IRQ handler to your code as well. The DMA channel IRQ handler depends on which DMA peripheral you're using.

void DMA1_Channel1_IRQHandler(){
    HAL_DMA_IRQHandler(&hdma_tim4_ch1);
}

Above I'm defining that if I have a channel 1 DMA interurpt, call the DMA IRQ handler for my timer and channel.

Setting Colors

After ws2812b_init returns successfully, the specified ws2812b_handle_t will contain all necessary information to set any addressable led.

Colors are encoded into the ws2812b_color_t struct, where the user can pass in any RGB value.

typedef struct{
    uint8_t red; // red value
    uint8_t green; // green value
    uint8_t blue; // blue value
}ws2812b_color_t;

In the ws2812b header file there are many macros with pre-encoded ws2812b_color_t with correct RGB values.

#define WS2812B_SOLID_GREEN         ((ws2812b_color_t){ .red = 0,   .green = 255,   .blue = 0 })
#define WS2812B_SOLID_RED           ((ws2812b_color_t){ .red = 255, .green = 0,     .blue = 0 })
#define WS2812B_SOLID_BLUE          ((ws2812b_color_t){ .red = 0,   .green = 0,     .blue = 255 })
#define WS2812B_SOLID_YELLOW        ((ws2812b_color_t){ .red = 255, .green = 255,   .blue = 0 })
#define WS2812B_SOLID_BURNT_ORANGE  ((ws2812b_color_t){ .red = 204, .green = 85,    .blue = 0 })
#define WS2812B_SOLID_PURPLE        ((ws2812b_color_t){ .red = 128, .green = 0,     .blue = 128 })
#define WS2812B_SOLID_OFF           ((ws2812b_color_t){ .red = 0,   .green = 0,     .blue = 0 })

There are many functions the user can call to set any led color


/**
 * @brief Sets the color for a specific led in the ws2812b strip
 * 
 * @param ledHandler    Pointer to the ws2812b handle.
 * @param led_num       The led number being set (0 indexed).
 * @param color         Struct containing RGB value to set the led too.
 * @param delay_ticks   Ticks to wait for data (0 = non-blocking, portMAX_DELAY = block until available).
 * @return ws2812b_status_t Returns WS2812B_OK on success, and returns any other value on failure
 */
ws2812b_status_t ws2812b_set_color(ws2812b_handle_t *ledHandler, uint8_t led_num,  ws2812b_color_t color, TickType_t delay_ticks);

/**
 * @brief Callback function that gets called in the TIM_PWM_PulseFinishedCallback function
 * 
 * @param ledHandler                Pointer to the ws2812b handle.
 * @param timerHandle               Pointer to the timer handle.
 * @param xHigherPriorityTaskWoken  Pointer to the highest priority task to be called next
 * @return none
 */
void ws2812b_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim, ws2812b_handle_t *ledHandler,  BaseType_t *xHigherPriorityTaskWoken);

/**
 * @brief Sets the color of all LEDs in a ws2812b strip
 * 
 * @param ledHandler    Pointer to the ws2812b handle.
 * @param color         Struct containing RGB value to set the led too.
 * @param delay_ticks   Ticks to wait for data (0 = non-blocking, portMAX_DELAY = block until available).
 * @return ws2812b_status_t Returns WS2812B_OK on success, and returns any other value on failure
 */
ws2812b_status_t ws2812b_set_all_leds(ws2812b_handle_t *ledHandler, ws2812b_color_t color, TickType_t delay_ticks);

/**
 * @brief Sets the color of a specified range of LEDs in a ws2812b strip
 * 
 * @param ledHandler    Pointer to the ws2812b handle.
 * @param start         Starting index of the led range to set (0 indexed).
 * @param end           Ending index of the led range to set (0 indexed).
 * @param color         Struct containing RGB value to set the led too.
 * @param delay_ticks   Ticks to wait for data (0 = non-blocking, portMAX_DELAY = block until available).
 * @return ws2812b_status_t Returns WS2812B_OK on success, and returns any other value on failure
 */
ws2812b_status_t ws2812b_set_led_range(ws2812b_handle_t *ledHandler, uint8_t start, uint8_t end, ws2812b_color_t color, TickType_t delay_ticks);

/**
 * @brief Loads an array of colors into the led strip
 * 
 * @param ledHandler    Pointer to the ws2812b handle.
 * @param color         An array of color structs that the led strip will be set too.
 * @param start         Starting index of the led range to set (0 indexed).
 * @param numColors     Number of elements in the colors array
 * @param delay_ticks   Ticks to wait for data (0 = non-blocking, portMAX_DELAY = block until available).
 * @return ws2812b_status_t Returns WS2812B_OK on success, and returns any other value on failure
 */
ws2812b_status_t ws2812b_load_colors(ws2812b_handle_t *ledHandler, const ws2812b_color_t colors[], uint8_t start, uint8_t numColors, TickType_t delay_ticks);

Examples

An example test for the LSOM can be found in here

Acknowledgements

Most of this driver is derived from this tutorial from ControllersTech.