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

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

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

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.