Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions drivers/led_strip/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ zephyr_library_sources_ifdef(CONFIG_APA102_STRIP apa102.c)
zephyr_library_sources_ifdef(CONFIG_LPD880X_STRIP lpd880x.c)
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_GPIO ws2812_gpio.c)
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_SPI ws2812_spi.c)
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_UART ws2812_uart.c)
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_I2S ws2812_i2s.c)
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_RPI_PICO_PIO ws2812_rpi_pico_pio.c)
zephyr_library_sources_ifdef(CONFIG_TLC5971_STRIP tlc5971.c)
Expand Down
20 changes: 16 additions & 4 deletions drivers/led_strip/Kconfig.ws2812
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ config WS2812_STRIP_SPI
depends on DT_HAS_WORLDSEMI_WS2812_SPI_ENABLED
select SPI if $(dt_compat_on_bus,$(DT_COMPAT_WORLDSEMI_WS2812_SPI),spi)
help
Enable driver for WS2812 (and compatibles) LED strip using SPI.
Enable driver for WS2812 (and compatible) LED strips using SPI.
The SPI driver is portable, but requires significantly more
memory (1 byte of overhead per bit of pixel data).

Expand All @@ -28,13 +28,25 @@ config WS2812_STRIP_SPI_FORCE_NOCACHE

endif

config WS2812_STRIP_UART
bool "WS2812 LED strip UART driver"
default y
depends on DT_HAS_WORLDSEMI_WS2812_UART_ENABLED
select SERIAL if $(dt_compat_on_bus,$(DT_COMPAT_WORLDSEMI_WS2812_UART),uart)
select UART_ASYNC_API
select SERIAL_SUPPORT_ASYNC
help
Enable driver for WS2812 (and compatible) LED strips using UART.
This method requires a high-speed UART and carefully crafted
byte frames to meet the strict WS2812 timing protocol.

config WS2812_STRIP_I2S
bool "WS2812 LED strip I2S driver"
default y
depends on DT_HAS_WORLDSEMI_WS2812_I2S_ENABLED
select I2S if $(dt_compat_on_bus,$(DT_COMPAT_WORLDSEMI_WS2812_I2S),i2s)
help
Enable driver for WS2812 (and compatibles) LED strip using I2S.
Enable driver for WS2812 (and compatible) LED strips using I2S.
Uses the I2S peripheral, memory usage is 4 bytes per color,
times the number of pixels. A few more for the start and end
delay. The reset delay has a coarse resolution of ~20us.
Expand All @@ -48,7 +60,7 @@ config WS2812_STRIP_GPIO
depends on (SOC_SERIES_NRF91X || SOC_SERIES_NRF51X || SOC_SERIES_NRF52X || SOC_SERIES_NRF53X)
select LED_STRIP_RGB_SCRATCH
help
Enable driver for WS2812 (and compatibles) LED strip directly
Enable driver for WS2812 (and compatible) LED strips directly
controlling with GPIO. The GPIO driver does bit-banging with inline
assembly, and is not available on all SoCs.

Expand Down Expand Up @@ -109,5 +121,5 @@ config WS2812_STRIP_RPI_PICO_PIO
depends on DT_HAS_WORLDSEMI_WS2812_RPI_PICO_PIO_ENABLED
select PICOSDK_USE_PIO
help
Enable driver for WS2812 (and compatibles) LED strip using
Enable driver for WS2812 (and compatible) LED strips using
the Raspberry Pi Pico's PIO.
336 changes: 336 additions & 0 deletions drivers/led_strip/ws2812_uart.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
/*
* Copyright (c) 2025 Google LLC
*
* SPDX-License-Identifier: Apache-2.0
*/

/*
* @file
* @brief WS2812 LED strip driver using a UART peripheral
*
* This driver uses a UART's asynchronous API to generate the precise,
* high-speed signal required by WS2812 and compatible LEDs.
*
* The driver encodes each WS2812 data bit ('1' or '0') into a multi-bit
* "symbol" (e.g., 110 for '1', 100 for '0'). It then employs a frame-aware
* packing strategy to transmit these symbols efficiently.
*
* Signal Inversion:
* The WS2812 protocol requires an idle-low signal. This is achieved by
* inverting the UART's TX output (requiring the "tx-invert" devicetree
* property).
*
* A standard UART frame:
* d0 d1 d2 d3 d4 d5 d6
* ___ __ __ __ __ __ __ __ __ ...
* |__|__|__|__|__|__|__|__|
* Start Bit (low) ^ ^ Stop Bit (high)
*
* An inverted UART frame:
* d0 d1 d2 d3 d4 d5 d6
* __ __ __ __ __ __ __ __
* ___| |__|__|__|__|__|__|__|__ ...
* Start Bit (high) ^ ^ Stop Bit (low)
*
* Frame-Aware Packing:
* The driver reuses the UART's hardware-generated start and stop bits as part
* of the on-wire symbol.
* - The symbol's MSB ('1') maps to the inverted UART start bit.
* - The inner bits are packed into the UART data payload.
* - The symbol's LSB ('0') maps to the inverted UART stop bit.
*
* Configuration Constraint:
* This packing scheme imposes a constraint: the total number of bits in a
* UART frame (1 start + N data + 1 stop) must be an integer multiple of the
* symbol's length (`bits-per-symbol`). For example, if `data-bits` is set to 7,
* the 9-bit total frame size (1 + 7 + 1) is compatible with a `bits-per-symbol`
* of 3.
*
* Example: The WS2812 data stream `101` sent as symbols `110`, `100`, `110`
* and packed into one 9-bit UART frame (1 start + 7 data + 1 stop):
* d0 d1 d2 d3 d4 d5 d6
* __ __ __ __ __
* ___| |__| |__ __| |__ ...
* Start Bit (high) ^ ^ Stop Bit (low)
*/

#define DT_DRV_COMPAT worldsemi_ws2812_uart

#include <string.h>
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/led_strip.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/dt-bindings/led/led.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/util.h>

#define LOG_LEVEL CONFIG_LED_STRIP_LOG_LEVEL
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(ws2812_uart);

/* Each color channel is represented by 8 bits. */
#define BITS_PER_COLOR_CHANNEL 8

/*
* Helper macros to get UART frame configuration from the parent UART's devicetree node.
*/
#define DT_UART_NODE(inst) DT_INST_PARENT(inst)
#define DT_UART_DATA_BITS(inst) DT_PROP_OR(DT_UART_NODE(inst), data_bits, 8)
/* Only UART_CFG_STOP_BITS_1 or 1 is supported. Other values will fail the BUILD_ASSERT below. */
#define DT_UART_STOP_BITS(inst) DT_ENUM_IDX_OR(DT_UART_NODE(inst), stop_bits, 1)
#define DT_UART_HAS_PARITY(inst) (DT_ENUM_IDX(DT_UART_NODE(inst), parity) != \
UART_CFG_PARITY_NONE)
#define DT_UART_HAS_TX_INVERT(inst) (DT_PROP_OR(DT_UART_NODE(inst), tx_invert, 0) != 0)

/* The total number of bits for one UART frame transmission (start + data + parity + stop). */
#define UART_FRAME_BITS_FROM_DT(inst) \
(1 + DT_UART_DATA_BITS(inst) + DT_UART_HAS_PARITY(inst) + DT_UART_STOP_BITS(inst))

/* Calculate the buffer size needed. */
#define WS2812_UART_CALC_BUFSZ(num_px, num_colors, bits_symbol, bits_frame) \
DIV_ROUND_UP((num_px) * (num_colors) * BITS_PER_COLOR_CHANNEL * (bits_symbol), \
(bits_frame))

struct ws2812_uart_cfg {
const struct device *uart_dev;
uint8_t *px_buf;
uint16_t one_symbol;
uint16_t zero_symbol;
uint8_t bits_per_symbol;
uint8_t num_colors;
const uint8_t *color_mapping;
size_t length;
uint16_t reset_delay;
uint8_t uart_frame_bits;
};

struct ws2812_uart_data {
struct k_mutex lock;
struct k_sem tx_done_sem;
};

/*
* Serializes an 8-bit color value into the UART buffer. This function takes
* an 8-bit color value, expands each of its 8 bits into the appropriate symbol
* pattern, and packs the resulting stream into UART data payloads.
*/
static inline void ws2812_uart_ser(uint8_t color, const struct ws2812_uart_cfg *cfg,
uint8_t *frame_bit_pos, uint8_t **buf)
{
for (int i = BITS_PER_COLOR_CHANNEL - 1; i >= 0; i--) {
uint16_t pattern = (color & BIT(i)) ? cfg->one_symbol : cfg->zero_symbol;

for (int p = cfg->bits_per_symbol - 1; p >= 0; p--) {
uint8_t pos = *frame_bit_pos;
/* Start and stop bits are always handled by hardware and skipped. */
bool is_hw_bit = (pos == 0) || (pos == (cfg->uart_frame_bits - 1));

/*
* With an inverted signal, a high pulse ('1') is made by sending
* a low level ('0'). We clear the bit as the buffer is pre-filled.
*/
if (!is_hw_bit && (pattern & BIT(p))) {
/* Map frame position to data position (no start bit). */
**buf &= ~BIT(pos - 1);
}

(*frame_bit_pos)++;
if (*frame_bit_pos >= cfg->uart_frame_bits) {
(*buf)++;
*frame_bit_pos = 0;
}
}
}
}

/*
* Callback for UART ASYNC API events.
*/
static void ws2812_uart_callback(const struct device *dev, struct uart_event *evt, void *user_data)
{
struct k_sem *tx_done_sem = user_data;

if (evt->type == UART_TX_DONE) {
k_sem_give(tx_done_sem);
}
}

/*
* Latch current color values on strip and reset its state machines.
*/
static inline void ws2812_reset_delay(uint16_t delay)
{
k_usleep(delay);
}

static int ws2812_strip_update_rgb(const struct device *dev, struct led_rgb *pixels,
size_t num_pixels)
{
const struct ws2812_uart_cfg *cfg = dev->config;
struct ws2812_uart_data *data = dev->data;
const size_t buf_len = WS2812_UART_CALC_BUFSZ(num_pixels, cfg->num_colors,
cfg->bits_per_symbol, cfg->uart_frame_bits);
uint8_t *px_buf = cfg->px_buf;
uint8_t *current_buf = px_buf;
uint8_t frame_bit_pos = 0;
int ret;

/* Lock the driver to ensure thread-safe access to the buffer and UART */
k_mutex_lock(&data->lock, K_FOREVER);
Comment on lines +179 to +180
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be a semaphore?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good question. While a binary semaphore can be used to implement mutual exclusion, a mutex is more appropriate here. A mutex clearly states that the code is protecting a shared resource (px_buf), whereas a semaphore is typically for signaling (the ISR signaling the thread). Moreover, a mutex provides priority inversion protection that a semaphore does not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is priority inversion needed here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sempahores are normally preferred for driver code, few reasons for that, Andy goes in more details in https://youtu.be/p8OgqVQxklo?si=iaVjKvUwvntmViGn&t=1304 but yeah if you don't need priority inversion semaphore seems to be the way to go, also since most core code do not use mutexes the code has a chance of being compiled out if we keep not using it in drivers.

Copy link
Contributor Author

@waihongtam waihongtam Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you both for the feedback, and especially for the link to Andy's talk. It provided great context on Zephyr's common practices.

I understand the general preference for semaphores in Zephyr drivers, as they are a smaller and faster implementation when priority inversion protection is not required.

My choice to use a mutex here is a trade-off that prioritizes robustness over performance gains. I particularly aim to support the following scenario:

  • A low-priority thread (e.g., a workqueue thread) handles a continuous LED animation, like a breathing effect, by periodically calling update_rgb().
  • A high-priority thread handles commands from an external host (the application processor) that must immediately overwrite the LED state for factory testing or to indicate a critical status. This thread also calls update_rgb().

In this situation, the high-priority thread could be blocked if the low-priority thread is preempted by a medium-priority task while holding the lock. A mutex with priority inheritance solves this problem transparently, making the driver more robust.

Given that we can't predict how developers will integrate this driver, I feel the safer approach is better suited for a general-purpose driver.


/* memset to 0xFF is correct for inverted signal logic */
memset(px_buf, 0xFF, buf_len);

/*
* Convert pixel data into a packed bitstream for the UART.
* Each color bit is expanded into a pattern of `bits_per_symbol`.
*/
for (size_t i = 0; i < num_pixels; i++) {
for (uint8_t j = 0; j < cfg->num_colors; j++) {
uint8_t pixel_val;

switch (cfg->color_mapping[j]) {
/* White channel is not supported by LED strip API. */
case LED_COLOR_ID_WHITE:
pixel_val = 0;
break;
case LED_COLOR_ID_RED:
pixel_val = pixels[i].r;
break;
case LED_COLOR_ID_GREEN:
pixel_val = pixels[i].g;
break;
case LED_COLOR_ID_BLUE:
pixel_val = pixels[i].b;
break;
default:
LOG_ERR("Invalid color mapping");
k_mutex_unlock(&data->lock);
return -EINVAL;
}

ws2812_uart_ser(pixel_val, cfg, &frame_bit_pos, &current_buf);
}
}

/*
* Start the non-blocking transfer. The uart_tx function will return
* immediately. The callback will signal completion via the semaphore.
*/
ret = uart_tx(cfg->uart_dev, px_buf, buf_len, SYS_FOREVER_US);
if (ret) {
k_mutex_unlock(&data->lock);
return ret;
}

/* Wait for the transfer to complete. */
k_sem_take(&data->tx_done_sem, K_FOREVER);

/* Latch the data and reset the strip */
ws2812_reset_delay(cfg->reset_delay);
k_mutex_unlock(&data->lock);

return 0;
}

static size_t ws2812_strip_length(const struct device *dev)
{
const struct ws2812_uart_cfg *cfg = dev->config;

return cfg->length;
}

static int ws2812_uart_init(const struct device *dev)
{
const struct ws2812_uart_cfg *cfg = dev->config;
struct ws2812_uart_data *data = dev->data;
int ret;

if (!device_is_ready(cfg->uart_dev)) {
LOG_ERR("%s: UART device %s not ready", dev->name, cfg->uart_dev->name);
return -ENODEV;
}

for (int i = 0; i < cfg->num_colors; i++) {
switch (cfg->color_mapping[i]) {
case LED_COLOR_ID_WHITE:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not supported as above, then should return an error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current behavior is consistent with other ws2812 drivers.

Due to an API limitation, the update_rgb() API doesn't support the WHITE color, as the argument pixels is an array of RGB colors, not RGBW. For WS2812 devices that support a dedicated WHITE channel, the API will set the WHITE channel to a value of 0. An error is not returned because the update_rgb() API still successfully performs its intended task.

case LED_COLOR_ID_RED:
case LED_COLOR_ID_GREEN:
case LED_COLOR_ID_BLUE:
break;
default:
LOG_ERR("%s: invalid channel to color mapping.", dev->name);
return -EINVAL;
}
}

k_mutex_init(&data->lock);
k_sem_init(&data->tx_done_sem, 0, 1);

ret = uart_callback_set(cfg->uart_dev, ws2812_uart_callback, &data->tx_done_sem);
if (ret) {
LOG_ERR("Failed to set UART callback: %d", ret);
return ret;
}

return 0;
}

static const struct led_strip_driver_api ws2812_uart_api = {
.update_rgb = ws2812_strip_update_rgb,
.length = ws2812_strip_length,
};

#define WS2812_NUM_PIXELS(idx) (DT_INST_PROP(idx, chain_length))
#define WS2812_NUM_COLORS(idx) (DT_INST_PROP_LEN(idx, color_mapping))
#define WS2812_UART_BITS_PER_SYMBOL(idx) (DT_INST_PROP(idx, bits_per_symbol))
#define WS2812_UART_BUFSZ(idx) \
WS2812_UART_CALC_BUFSZ(WS2812_NUM_PIXELS(idx), WS2812_NUM_COLORS(idx), \
WS2812_UART_BITS_PER_SYMBOL(idx), UART_FRAME_BITS_FROM_DT(idx))

#define WS2812_UART_CHECK(idx) \
BUILD_ASSERT(!DT_UART_HAS_PARITY(idx), \
"The UART peripheral must be configured with parity disabled."); \
BUILD_ASSERT(DT_UART_STOP_BITS(idx) == 1, \
"The UART peripheral's stop-bits property must be set to 1."); \
BUILD_ASSERT(DT_UART_HAS_TX_INVERT(idx), \
"The UART peripheral must be configured with tx-invert."); \
BUILD_ASSERT((UART_FRAME_BITS_FROM_DT(idx) % WS2812_UART_BITS_PER_SYMBOL(idx)) == 0, \
"Total UART frame bits must be a multiple of bits-per-symbol."); \
BUILD_ASSERT(WS2812_UART_BITS_PER_SYMBOL(idx) <= 10, \
"bits-per-symbol cannot be greater than 10."); \
BUILD_ASSERT(WS2812_UART_BITS_PER_SYMBOL(idx) >= 3, \
"bits-per-symbol must be at least 3."); \
BUILD_ASSERT( \
(DT_INST_PROP(idx, one_symbol) & BIT(WS2812_UART_BITS_PER_SYMBOL(idx) - 1)) && \
(DT_INST_PROP(idx, zero_symbol) & \
BIT(WS2812_UART_BITS_PER_SYMBOL(idx) - 1)), \
"Symbol's MSB must be 1 (the start bit may be reused)."); \
BUILD_ASSERT(!((DT_INST_PROP(idx, one_symbol) & BIT(0)) || \
(DT_INST_PROP(idx, zero_symbol) & BIT(0))), \
"Symbol's LSB must be 0 (the stop bit may be reused).")

#define WS2812_UART_DEVICE(idx) \
WS2812_UART_CHECK(idx); \
static uint8_t ws2812_uart_##idx##_px_buf[WS2812_UART_BUFSZ(idx)]; \
static struct ws2812_uart_data ws2812_uart_##idx##_data; \
static const uint8_t ws2812_uart_##idx##_color_mapping[] = \
DT_INST_PROP(idx, color_mapping); \
static const struct ws2812_uart_cfg ws2812_uart_##idx##_cfg = { \
.uart_dev = DEVICE_DT_GET(DT_INST_PARENT(idx)), \
.px_buf = ws2812_uart_##idx##_px_buf, \
.one_symbol = DT_INST_PROP(idx, one_symbol), \
.zero_symbol = DT_INST_PROP(idx, zero_symbol), \
.bits_per_symbol = WS2812_UART_BITS_PER_SYMBOL(idx), \
.num_colors = WS2812_NUM_COLORS(idx), \
.color_mapping = ws2812_uart_##idx##_color_mapping, \
.length = WS2812_NUM_PIXELS(idx), \
.reset_delay = DT_INST_PROP(idx, reset_delay), \
.uart_frame_bits = UART_FRAME_BITS_FROM_DT(idx), \
}; \
DEVICE_DT_INST_DEFINE(idx, ws2812_uart_init, NULL, &ws2812_uart_##idx##_data, \
&ws2812_uart_##idx##_cfg, POST_KERNEL, \
CONFIG_LED_STRIP_INIT_PRIORITY, &ws2812_uart_api);

DT_INST_FOREACH_STATUS_OKAY(WS2812_UART_DEVICE)
Loading