Skip to content

Commit 7e195a9

Browse files
waihongtamkartben
authored andcommitted
drivers: led_strip: Add UART-based driver for WS2812
This commit introduces a new driver for WS2812 and compatible LED strips that uses a UART peripheral. The driver generates the precise, high-speed signal required by the WS2812 protocol by encoding each data bit into a multi-bit "symbol" and using a frame-aware packing strategy for transmission: - Signal Inversion: The UART's TX line must be inverted (tx-invert) to create the protocol's required idle-low signal. A UART start bit then generates the initial high pulse of a WS2812 bit. - Frame-Aware Packing: The driver reuses the UART's hardware- generated start and stop bits as the first and last bits of the on-wire symbol. The inner bits of the symbol are packed into the UART data payload. This packing scheme imposes a configuration 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). Signed-off-by: Wai-Hong Tam <[email protected]>
1 parent ba20a37 commit 7e195a9

File tree

3 files changed

+353
-4
lines changed

3 files changed

+353
-4
lines changed

drivers/led_strip/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ zephyr_library_sources_ifdef(CONFIG_APA102_STRIP apa102.c)
66
zephyr_library_sources_ifdef(CONFIG_LPD880X_STRIP lpd880x.c)
77
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_GPIO ws2812_gpio.c)
88
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_SPI ws2812_spi.c)
9+
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_UART ws2812_uart.c)
910
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_I2S ws2812_i2s.c)
1011
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_RPI_PICO_PIO ws2812_rpi_pico_pio.c)
1112
zephyr_library_sources_ifdef(CONFIG_TLC5971_STRIP tlc5971.c)

drivers/led_strip/Kconfig.ws2812

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ config WS2812_STRIP_SPI
1313
depends on DT_HAS_WORLDSEMI_WS2812_SPI_ENABLED
1414
select SPI if $(dt_compat_on_bus,$(DT_COMPAT_WORLDSEMI_WS2812_SPI),spi)
1515
help
16-
Enable driver for WS2812 (and compatibles) LED strip using SPI.
16+
Enable driver for WS2812 (and compatible) LED strips using SPI.
1717
The SPI driver is portable, but requires significantly more
1818
memory (1 byte of overhead per bit of pixel data).
1919

@@ -28,13 +28,25 @@ config WS2812_STRIP_SPI_FORCE_NOCACHE
2828

2929
endif
3030

31+
config WS2812_STRIP_UART
32+
bool "WS2812 LED strip UART driver"
33+
default y
34+
depends on DT_HAS_WORLDSEMI_WS2812_UART_ENABLED
35+
select SERIAL if $(dt_compat_on_bus,$(DT_COMPAT_WORLDSEMI_WS2812_UART),uart)
36+
select UART_ASYNC_API
37+
select SERIAL_SUPPORT_ASYNC
38+
help
39+
Enable driver for WS2812 (and compatible) LED strips using UART.
40+
This method requires a high-speed UART and carefully crafted
41+
byte frames to meet the strict WS2812 timing protocol.
42+
3143
config WS2812_STRIP_I2S
3244
bool "WS2812 LED strip I2S driver"
3345
default y
3446
depends on DT_HAS_WORLDSEMI_WS2812_I2S_ENABLED
3547
select I2S if $(dt_compat_on_bus,$(DT_COMPAT_WORLDSEMI_WS2812_I2S),i2s)
3648
help
37-
Enable driver for WS2812 (and compatibles) LED strip using I2S.
49+
Enable driver for WS2812 (and compatible) LED strips using I2S.
3850
Uses the I2S peripheral, memory usage is 4 bytes per color,
3951
times the number of pixels. A few more for the start and end
4052
delay. The reset delay has a coarse resolution of ~20us.
@@ -47,7 +59,7 @@ config WS2812_STRIP_GPIO
4759
depends on DT_HAS_WORLDSEMI_WS2812_GPIO_ENABLED
4860
depends on (SOC_SERIES_NRF91X || SOC_SERIES_NRF51X || SOC_SERIES_NRF52X || SOC_SERIES_NRF53X)
4961
help
50-
Enable driver for WS2812 (and compatibles) LED strip directly
62+
Enable driver for WS2812 (and compatible) LED strips directly
5163
controlling with GPIO. The GPIO driver does bit-banging with inline
5264
assembly, and is not available on all SoCs.
5365

@@ -108,5 +120,5 @@ config WS2812_STRIP_RPI_PICO_PIO
108120
depends on DT_HAS_WORLDSEMI_WS2812_RPI_PICO_PIO_ENABLED
109121
select PICOSDK_USE_PIO
110122
help
111-
Enable driver for WS2812 (and compatibles) LED strip using
123+
Enable driver for WS2812 (and compatible) LED strips using
112124
the Raspberry Pi Pico's PIO.

drivers/led_strip/ws2812_uart.c

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
/*
2+
* Copyright (c) 2025 Google LLC
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
/*
8+
* @file
9+
* @brief WS2812 LED strip driver using a UART peripheral
10+
*
11+
* This driver uses a UART's asynchronous API to generate the precise,
12+
* high-speed signal required by WS2812 and compatible LEDs.
13+
*
14+
* The driver encodes each WS2812 data bit ('1' or '0') into a multi-bit
15+
* "symbol" (e.g., 110 for '1', 100 for '0'). It then employs a frame-aware
16+
* packing strategy to transmit these symbols efficiently.
17+
*
18+
* Signal Inversion:
19+
* The WS2812 protocol requires an idle-low signal. This is achieved by
20+
* inverting the UART's TX output (requiring the "tx-invert" devicetree
21+
* property).
22+
*
23+
* A standard UART frame:
24+
* d0 d1 d2 d3 d4 d5 d6
25+
* ___ __ __ __ __ __ __ __ __ ...
26+
* |__|__|__|__|__|__|__|__|
27+
* Start Bit (low) ^ ^ Stop Bit (high)
28+
*
29+
* An inverted UART frame:
30+
* d0 d1 d2 d3 d4 d5 d6
31+
* __ __ __ __ __ __ __ __
32+
* ___| |__|__|__|__|__|__|__|__ ...
33+
* Start Bit (high) ^ ^ Stop Bit (low)
34+
*
35+
* Frame-Aware Packing:
36+
* The driver reuses the UART's hardware-generated start and stop bits as part
37+
* of the on-wire symbol.
38+
* - The symbol's MSB ('1') maps to the inverted UART start bit.
39+
* - The inner bits are packed into the UART data payload.
40+
* - The symbol's LSB ('0') maps to the inverted UART stop bit.
41+
*
42+
* Configuration Constraint:
43+
* This packing scheme imposes a constraint: the total number of bits in a
44+
* UART frame (1 start + N data + 1 stop) must be an integer multiple of the
45+
* symbol's length (`bits-per-symbol`). For example, if `data-bits` is set to 7,
46+
* the 9-bit total frame size (1 + 7 + 1) is compatible with a `bits-per-symbol`
47+
* of 3.
48+
*
49+
* Example: The WS2812 data stream `101` sent as symbols `110`, `100`, `110`
50+
* and packed into one 9-bit UART frame (1 start + 7 data + 1 stop):
51+
* d0 d1 d2 d3 d4 d5 d6
52+
* __ __ __ __ __
53+
* ___| |__| |__ __| |__ ...
54+
* Start Bit (high) ^ ^ Stop Bit (low)
55+
*/
56+
57+
#define DT_DRV_COMPAT worldsemi_ws2812_uart
58+
59+
#include <string.h>
60+
#include <zephyr/device.h>
61+
#include <zephyr/devicetree.h>
62+
#include <zephyr/drivers/led_strip.h>
63+
#include <zephyr/drivers/uart.h>
64+
#include <zephyr/dt-bindings/led/led.h>
65+
#include <zephyr/kernel.h>
66+
#include <zephyr/sys/util.h>
67+
68+
#define LOG_LEVEL CONFIG_LED_STRIP_LOG_LEVEL
69+
#include <zephyr/logging/log.h>
70+
LOG_MODULE_REGISTER(ws2812_uart);
71+
72+
/* Each color channel is represented by 8 bits. */
73+
#define BITS_PER_COLOR_CHANNEL 8
74+
75+
/*
76+
* Helper macros to get UART frame configuration from the parent UART's devicetree node.
77+
*/
78+
#define DT_UART_NODE(inst) DT_INST_PARENT(inst)
79+
#define DT_UART_DATA_BITS(inst) DT_PROP_OR(DT_UART_NODE(inst), data_bits, 8)
80+
/* Only UART_CFG_STOP_BITS_1 or 1 is supported. Other values will fail the BUILD_ASSERT below. */
81+
#define DT_UART_STOP_BITS(inst) DT_ENUM_IDX_OR(DT_UART_NODE(inst), stop_bits, 1)
82+
#define DT_UART_HAS_PARITY(inst) (DT_ENUM_IDX(DT_UART_NODE(inst), parity) != \
83+
UART_CFG_PARITY_NONE)
84+
#define DT_UART_HAS_TX_INVERT(inst) (DT_PROP_OR(DT_UART_NODE(inst), tx_invert, 0) != 0)
85+
86+
/* The total number of bits for one UART frame transmission (start + data + parity + stop). */
87+
#define UART_FRAME_BITS_FROM_DT(inst) \
88+
(1 + DT_UART_DATA_BITS(inst) + DT_UART_HAS_PARITY(inst) + DT_UART_STOP_BITS(inst))
89+
90+
/* Calculate the buffer size needed. */
91+
#define WS2812_UART_CALC_BUFSZ(num_px, num_colors, bits_symbol, bits_frame) \
92+
DIV_ROUND_UP((num_px) * (num_colors) * BITS_PER_COLOR_CHANNEL * (bits_symbol), \
93+
(bits_frame))
94+
95+
struct ws2812_uart_cfg {
96+
const struct device *uart_dev;
97+
uint8_t *px_buf;
98+
uint16_t one_symbol;
99+
uint16_t zero_symbol;
100+
uint8_t bits_per_symbol;
101+
uint8_t num_colors;
102+
const uint8_t *color_mapping;
103+
size_t length;
104+
uint16_t reset_delay;
105+
uint8_t uart_frame_bits;
106+
};
107+
108+
struct ws2812_uart_data {
109+
struct k_mutex lock;
110+
struct k_sem tx_done_sem;
111+
};
112+
113+
/*
114+
* Serializes an 8-bit color value into the UART buffer. This function takes
115+
* an 8-bit color value, expands each of its 8 bits into the appropriate symbol
116+
* pattern, and packs the resulting stream into UART data payloads.
117+
*/
118+
static inline void ws2812_uart_ser(uint8_t color, const struct ws2812_uart_cfg *cfg,
119+
uint8_t *frame_bit_pos, uint8_t **buf)
120+
{
121+
for (int i = BITS_PER_COLOR_CHANNEL - 1; i >= 0; i--) {
122+
uint16_t pattern = (color & BIT(i)) ? cfg->one_symbol : cfg->zero_symbol;
123+
124+
for (int p = cfg->bits_per_symbol - 1; p >= 0; p--) {
125+
uint8_t pos = *frame_bit_pos;
126+
/* Start and stop bits are always handled by hardware and skipped. */
127+
bool is_hw_bit = (pos == 0) || (pos == (cfg->uart_frame_bits - 1));
128+
129+
/*
130+
* With an inverted signal, a high pulse ('1') is made by sending
131+
* a low level ('0'). We clear the bit as the buffer is pre-filled.
132+
*/
133+
if (!is_hw_bit && (pattern & BIT(p))) {
134+
/* Map frame position to data position (no start bit). */
135+
**buf &= ~BIT(pos - 1);
136+
}
137+
138+
(*frame_bit_pos)++;
139+
if (*frame_bit_pos >= cfg->uart_frame_bits) {
140+
(*buf)++;
141+
*frame_bit_pos = 0;
142+
}
143+
}
144+
}
145+
}
146+
147+
/*
148+
* Callback for UART ASYNC API events.
149+
*/
150+
static void ws2812_uart_callback(const struct device *dev, struct uart_event *evt, void *user_data)
151+
{
152+
struct k_sem *tx_done_sem = user_data;
153+
154+
if (evt->type == UART_TX_DONE) {
155+
k_sem_give(tx_done_sem);
156+
}
157+
}
158+
159+
/*
160+
* Latch current color values on strip and reset its state machines.
161+
*/
162+
static inline void ws2812_reset_delay(uint16_t delay)
163+
{
164+
k_usleep(delay);
165+
}
166+
167+
static int ws2812_strip_update_rgb(const struct device *dev, struct led_rgb *pixels,
168+
size_t num_pixels)
169+
{
170+
const struct ws2812_uart_cfg *cfg = dev->config;
171+
struct ws2812_uart_data *data = dev->data;
172+
const size_t buf_len = WS2812_UART_CALC_BUFSZ(num_pixels, cfg->num_colors,
173+
cfg->bits_per_symbol, cfg->uart_frame_bits);
174+
uint8_t *px_buf = cfg->px_buf;
175+
uint8_t *current_buf = px_buf;
176+
uint8_t frame_bit_pos = 0;
177+
int ret;
178+
179+
/* Lock the driver to ensure thread-safe access to the buffer and UART */
180+
k_mutex_lock(&data->lock, K_FOREVER);
181+
182+
/* memset to 0xFF is correct for inverted signal logic */
183+
memset(px_buf, 0xFF, buf_len);
184+
185+
/*
186+
* Convert pixel data into a packed bitstream for the UART.
187+
* Each color bit is expanded into a pattern of `bits_per_symbol`.
188+
*/
189+
for (size_t i = 0; i < num_pixels; i++) {
190+
for (uint8_t j = 0; j < cfg->num_colors; j++) {
191+
uint8_t pixel_val;
192+
193+
switch (cfg->color_mapping[j]) {
194+
/* White channel is not supported by LED strip API. */
195+
case LED_COLOR_ID_WHITE:
196+
pixel_val = 0;
197+
break;
198+
case LED_COLOR_ID_RED:
199+
pixel_val = pixels[i].r;
200+
break;
201+
case LED_COLOR_ID_GREEN:
202+
pixel_val = pixels[i].g;
203+
break;
204+
case LED_COLOR_ID_BLUE:
205+
pixel_val = pixels[i].b;
206+
break;
207+
default:
208+
LOG_ERR("Invalid color mapping");
209+
k_mutex_unlock(&data->lock);
210+
return -EINVAL;
211+
}
212+
213+
ws2812_uart_ser(pixel_val, cfg, &frame_bit_pos, &current_buf);
214+
}
215+
}
216+
217+
/*
218+
* Start the non-blocking transfer. The uart_tx function will return
219+
* immediately. The callback will signal completion via the semaphore.
220+
*/
221+
ret = uart_tx(cfg->uart_dev, px_buf, buf_len, SYS_FOREVER_US);
222+
if (ret) {
223+
k_mutex_unlock(&data->lock);
224+
return ret;
225+
}
226+
227+
/* Wait for the transfer to complete. */
228+
k_sem_take(&data->tx_done_sem, K_FOREVER);
229+
230+
/* Latch the data and reset the strip */
231+
ws2812_reset_delay(cfg->reset_delay);
232+
k_mutex_unlock(&data->lock);
233+
234+
return 0;
235+
}
236+
237+
static size_t ws2812_strip_length(const struct device *dev)
238+
{
239+
const struct ws2812_uart_cfg *cfg = dev->config;
240+
241+
return cfg->length;
242+
}
243+
244+
static int ws2812_uart_init(const struct device *dev)
245+
{
246+
const struct ws2812_uart_cfg *cfg = dev->config;
247+
struct ws2812_uart_data *data = dev->data;
248+
int ret;
249+
250+
if (!device_is_ready(cfg->uart_dev)) {
251+
LOG_ERR("%s: UART device %s not ready", dev->name, cfg->uart_dev->name);
252+
return -ENODEV;
253+
}
254+
255+
for (int i = 0; i < cfg->num_colors; i++) {
256+
switch (cfg->color_mapping[i]) {
257+
case LED_COLOR_ID_WHITE:
258+
case LED_COLOR_ID_RED:
259+
case LED_COLOR_ID_GREEN:
260+
case LED_COLOR_ID_BLUE:
261+
break;
262+
default:
263+
LOG_ERR("%s: invalid channel to color mapping.", dev->name);
264+
return -EINVAL;
265+
}
266+
}
267+
268+
k_mutex_init(&data->lock);
269+
k_sem_init(&data->tx_done_sem, 0, 1);
270+
271+
ret = uart_callback_set(cfg->uart_dev, ws2812_uart_callback, &data->tx_done_sem);
272+
if (ret) {
273+
LOG_ERR("Failed to set UART callback: %d", ret);
274+
return ret;
275+
}
276+
277+
return 0;
278+
}
279+
280+
static const struct led_strip_driver_api ws2812_uart_api = {
281+
.update_rgb = ws2812_strip_update_rgb,
282+
.length = ws2812_strip_length,
283+
};
284+
285+
#define WS2812_NUM_PIXELS(idx) (DT_INST_PROP(idx, chain_length))
286+
#define WS2812_NUM_COLORS(idx) (DT_INST_PROP_LEN(idx, color_mapping))
287+
#define WS2812_UART_BITS_PER_SYMBOL(idx) (DT_INST_PROP(idx, bits_per_symbol))
288+
#define WS2812_UART_BUFSZ(idx) \
289+
WS2812_UART_CALC_BUFSZ(WS2812_NUM_PIXELS(idx), WS2812_NUM_COLORS(idx), \
290+
WS2812_UART_BITS_PER_SYMBOL(idx), UART_FRAME_BITS_FROM_DT(idx))
291+
292+
#define WS2812_UART_CHECK(idx) \
293+
BUILD_ASSERT(!DT_UART_HAS_PARITY(idx), \
294+
"The UART peripheral must be configured with parity disabled."); \
295+
BUILD_ASSERT(DT_UART_STOP_BITS(idx) == 1, \
296+
"The UART peripheral's stop-bits property must be set to 1."); \
297+
BUILD_ASSERT(DT_UART_HAS_TX_INVERT(idx), \
298+
"The UART peripheral must be configured with tx-invert."); \
299+
BUILD_ASSERT((UART_FRAME_BITS_FROM_DT(idx) % WS2812_UART_BITS_PER_SYMBOL(idx)) == 0, \
300+
"Total UART frame bits must be a multiple of bits-per-symbol."); \
301+
BUILD_ASSERT(WS2812_UART_BITS_PER_SYMBOL(idx) <= 10, \
302+
"bits-per-symbol cannot be greater than 10."); \
303+
BUILD_ASSERT(WS2812_UART_BITS_PER_SYMBOL(idx) >= 3, \
304+
"bits-per-symbol must be at least 3."); \
305+
BUILD_ASSERT( \
306+
(DT_INST_PROP(idx, one_symbol) & BIT(WS2812_UART_BITS_PER_SYMBOL(idx) - 1)) && \
307+
(DT_INST_PROP(idx, zero_symbol) & \
308+
BIT(WS2812_UART_BITS_PER_SYMBOL(idx) - 1)), \
309+
"Symbol's MSB must be 1 (the start bit may be reused)."); \
310+
BUILD_ASSERT(!((DT_INST_PROP(idx, one_symbol) & BIT(0)) || \
311+
(DT_INST_PROP(idx, zero_symbol) & BIT(0))), \
312+
"Symbol's LSB must be 0 (the stop bit may be reused).")
313+
314+
#define WS2812_UART_DEVICE(idx) \
315+
WS2812_UART_CHECK(idx); \
316+
static uint8_t ws2812_uart_##idx##_px_buf[WS2812_UART_BUFSZ(idx)]; \
317+
static struct ws2812_uart_data ws2812_uart_##idx##_data; \
318+
static const uint8_t ws2812_uart_##idx##_color_mapping[] = \
319+
DT_INST_PROP(idx, color_mapping); \
320+
static const struct ws2812_uart_cfg ws2812_uart_##idx##_cfg = { \
321+
.uart_dev = DEVICE_DT_GET(DT_INST_PARENT(idx)), \
322+
.px_buf = ws2812_uart_##idx##_px_buf, \
323+
.one_symbol = DT_INST_PROP(idx, one_symbol), \
324+
.zero_symbol = DT_INST_PROP(idx, zero_symbol), \
325+
.bits_per_symbol = WS2812_UART_BITS_PER_SYMBOL(idx), \
326+
.num_colors = WS2812_NUM_COLORS(idx), \
327+
.color_mapping = ws2812_uart_##idx##_color_mapping, \
328+
.length = WS2812_NUM_PIXELS(idx), \
329+
.reset_delay = DT_INST_PROP(idx, reset_delay), \
330+
.uart_frame_bits = UART_FRAME_BITS_FROM_DT(idx), \
331+
}; \
332+
DEVICE_DT_INST_DEFINE(idx, ws2812_uart_init, NULL, &ws2812_uart_##idx##_data, \
333+
&ws2812_uart_##idx##_cfg, POST_KERNEL, \
334+
CONFIG_LED_STRIP_INIT_PRIORITY, &ws2812_uart_api);
335+
336+
DT_INST_FOREACH_STATUS_OKAY(WS2812_UART_DEVICE)

0 commit comments

Comments
 (0)