From f313ea3c9be1e30da9a221e1f38b5af9733e03f6 Mon Sep 17 00:00:00 2001 From: Anuj Pathak Date: Thu, 15 Aug 2024 17:18:40 +0530 Subject: [PATCH 1/3] dts: bindings: led_strip: Added WS2812-UART dts Added worldsemi,ws2812-uart DT binding. Signed-off-by: Ayush Kothari Signed-off-by: Anuj Pathak --- .../led_strip/worldsemi,ws2812-uart.yaml | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 dts/bindings/led_strip/worldsemi,ws2812-uart.yaml diff --git a/dts/bindings/led_strip/worldsemi,ws2812-uart.yaml b/dts/bindings/led_strip/worldsemi,ws2812-uart.yaml new file mode 100644 index 0000000000000..f8667c5f689bf --- /dev/null +++ b/dts/bindings/led_strip/worldsemi,ws2812-uart.yaml @@ -0,0 +1,68 @@ +# Copyright (c) 2024 Croxel, Inc. +# SPDX-License-Identifier: Apache-2.0 + +description: | + Worldsemi WS2812 LED strip, UART binding + + Driver bindings for controlling a WS2812 or compatible LED + strip with a UART. + + The UART driver should be usable as long as a zephyr UART API driver is available for your + board and has tx-invert property available and enabled. + UART Speed and Data-bits should satisfy following requirements too. + - (data-bits + 2) should be integer multiple of frame-len. + - speed = frame-len * bit_interval + Please correlate above equations with MCU and Led Controller datasheet to find compatibility + + Data serialization explanation for WS2812: + 3 pulses/bits of UART are used to represent one bit of RGB data. During the development of this + driver, I was using an ESP32C3 MCU with its UART baud rate set to 2.5Mbits per sec. Hence, 1 + bit equals 400ns. For WS2812 timing details, please refer to its datasheet. + Link: https://cdn-shop.adafruit.com/datasheets/WS2812.pdf + + We need to carefully analyse the pulse timing requirements of ws2812 IC or any other IC in + order to construct LED's 0, LED's 1 frame, rgb-frame-per-uart-frame (Total number of RGB data + bits sent per UART transaction) and frame-len (bits to represent single rgb bit). + + For WS2812: ___ + LED's 0 frame-> 0x4 or 0b100 -> _| 1 |_0__0_ + ______ + LED's 1 frame-> 0x6 or 0b110 -> _| 1 1 |_0_ + + rgb-frame-per-uart-frame -> 3 (3 RGB data bits will be sent uart transaction) + frame-len -> 3 (3 UART bits needed to represent 1 RGB bit) + + Calculation will be done using these node properties to build a lookup table using C macros + automatically. And that lookup-table will be used to feed the actual UART buffer which + will be sent to WS2812 IC. + +compatible: "worldsemi,ws2812-uart" + +include: [uart-controller.yaml, ws2812.yaml] + +properties: + rgb-frame-per-uart-frame: + type: int + enum: [1, 2, 3] + required: true + description: | + Total number of RGB bits sent per UART transaction. + + zero-frame: + type: int + required: true + description: | + The UART frame value representing a LED's bit 0. + + one-frame: + type: int + required: true + description: | + The UART frame value representing a LED's bit 1. + + frame-len: + type: int + enum: [3, 4, 5, 7, 8, 9] + required: true + description: | + Number of bits to represent 1 RGB data bit. From 5b7540eda5e9630c31047509f3441b82ea260437 Mon Sep 17 00:00:00 2001 From: Anuj Pathak Date: Thu, 15 Aug 2024 17:20:46 +0530 Subject: [PATCH 2/3] drivers: led_strip: Added ws2812-uart driver - ws2812 or compatible led's can be driven using uart peripheral if internal or external tx-pin-inversion is used. We could send multiple pixel bits in one uart frame, making it ram efficient. Signed-off-by: Ayush Kothari Signed-off-by: Anuj Pathak --- drivers/led_strip/CMakeLists.txt | 8 + drivers/led_strip/Kconfig.ws2812 | 8 + drivers/led_strip/ws2812_uart.c | 317 +++++++++++++++++++++++++++++++ 3 files changed, 333 insertions(+) create mode 100644 drivers/led_strip/ws2812_uart.c diff --git a/drivers/led_strip/CMakeLists.txt b/drivers/led_strip/CMakeLists.txt index b293782e620fb..f5f99ec6918b3 100644 --- a/drivers/led_strip/CMakeLists.txt +++ b/drivers/led_strip/CMakeLists.txt @@ -6,7 +6,15 @@ 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) zephyr_library_sources_ifdef(CONFIG_TLC59731_STRIP tlc59731.c) + +if (CONFIG_WS2812_STRIP_UART) +message(WARNING "WS2812-UART driver depends on tx-pin-inversion. Please make sure \ +you either have it enabled in uart property (if supported by the driver) or \ +have external hardware setup in place to invert tx pin's logic level" +) +endif() diff --git a/drivers/led_strip/Kconfig.ws2812 b/drivers/led_strip/Kconfig.ws2812 index 5040515e8bc10..64f3dfbb5e81f 100644 --- a/drivers/led_strip/Kconfig.ws2812 +++ b/drivers/led_strip/Kconfig.ws2812 @@ -52,3 +52,11 @@ config WS2812_STRIP_RPI_PICO_PIO help Enable driver for WS2812 (and compatibles) LED strip using the RaspberryPi Pico's PIO. + +config WS2812_STRIP_UART + bool "WS2812 LED strip UART driver" + default y + depends on DT_HAS_WORLDSEMI_WS2812_UART_ENABLED + select UART if $(dt_compat_on_bus,$(DT_COMPAT_WORLDSEMI_WS2812_UART),uart) + help + Enable driver for WS2812 (and compatibles) LED strip using UART. diff --git a/drivers/led_strip/ws2812_uart.c b/drivers/led_strip/ws2812_uart.c new file mode 100644 index 0000000000000..35dc61af3cda1 --- /dev/null +++ b/drivers/led_strip/ws2812_uart.c @@ -0,0 +1,317 @@ +/* + * Copyright (c) 2024 Croxel, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#define DT_DRV_COMPAT worldsemi_ws2812_uart + +#include + +#include + +#define LOG_LEVEL CONFIG_LED_STRIP_LOG_LEVEL +#include +LOG_MODULE_REGISTER(ws2812_uart); + +#include +#include +#include +#include +#include +#include + +struct ws2812_uart_cfg { + const struct device *uart_dev; /* UART device */ + uint8_t *px_buf; + size_t length; + uint8_t num_colors; + const uint8_t *color_mapping; + const uint8_t *uart_lookup_table; + uint16_t reset_delay; + uint8_t repeat_count; +}; + +/* + * Serialize a 24-bit color channel value into an equivalent sequence + * of 8 bytes of UART frames. + */ +static inline size_t ws2812_uart_serialize(const struct device *dev, uint8_t *buf, uint8_t color[3]) +{ + size_t buff_indx = 0; + uint8_t serialized_val = 0; + const struct ws2812_uart_cfg *cfg = dev->config; + + /* Loop through each of the 3 color components (Red, Green, Blue) */ + for (uint8_t i = 0; i < 3; i++) { + /* Loop through each bit of the current color component */ + for (uint8_t j = 0; j < 8; j++) { + /* Shift the serialized value left by 1 bit to make room for the next bit, + * then OR it with the current bit of the color component. + */ + serialized_val = (serialized_val << 1) | ((color[i] >> (7 - j)) & 0x01); + + /* Check if we have serialized enough bits based on the repeat_count. + * If so, use the serialized value to look up the corresponding UART value + * from the lookup table and store it in the buffer. + */ + if (((i * 8) + j) % cfg->repeat_count == (cfg->repeat_count - 1)) { + buf[buff_indx++] = cfg->uart_lookup_table[serialized_val]; + + /* Now Reset the serialized value for the next set of bits */ + serialized_val = 0; + } + } + } + /* Return the number of bytes filled in the buffer */ + return buff_indx; +} + +static void ws2812_uart_tx(const struct ws2812_uart_cfg *cfg, uint8_t *tx, size_t len) +{ + for (size_t i = 0; i < len; i++) { + uart_poll_out(cfg->uart_dev, tx[i]); + } +} + +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; + + uint8_t *px_buf = cfg->px_buf; + size_t px_buf_len = cfg->length * cfg->num_colors * 8; + size_t uart_data_len = 0; + size_t i; + uint8_t color[3]; + + /* + * Convert pixel data into UART frames. Each frame has pixel data + * in color mapping on-wire format (e.g. GRB, GRBW, RGB, etc). + */ + + for (i = 0; i < num_pixels; i++) { + uint8_t j; + + for (j = 0; j < cfg->num_colors; j++) { + switch (cfg->color_mapping[j]) { + /* White channel is not supported by LED strip API. */ + case LED_COLOR_ID_WHITE: + color[j] = 0; + break; + case LED_COLOR_ID_RED: + color[j] = pixels[i].r; + break; + case LED_COLOR_ID_GREEN: + color[j] = pixels[i].g; + break; + case LED_COLOR_ID_BLUE: + color[j] = pixels[i].b; + break; + default: + return -EINVAL; + } + } + uart_data_len += ws2812_uart_serialize(dev, px_buf, &color[0]); + if (uart_data_len <= px_buf_len) { + px_buf = cfg->px_buf + uart_data_len; + } else { + return -ENOMEM; + } + } + ws2812_uart_tx(cfg, cfg->px_buf, uart_data_len); + k_usleep(cfg->reset_delay); + + return 0; +} + +static int ws2812_uart_init(const struct device *dev) +{ + const struct ws2812_uart_cfg *cfg = dev->config; + uint8_t i; + + if (!device_is_ready(cfg->uart_dev)) { + LOG_ERR("UART device not ready"); + return -ENODEV; + } + + for (i = 0; i < cfg->num_colors; i++) { + switch (cfg->color_mapping[i]) { + case LED_COLOR_ID_WHITE: + 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." + "Check the color-mapping DT property", + dev->name); + return -EINVAL; + } + } + + return 0; +} + +static size_t ws2812_strip_length(const struct device *dev) +{ + const struct ws2812_uart_cfg *cfg = dev->config; + + return cfg->length; +} + +static const struct led_strip_driver_api ws2812_uart_api = { + .update_rgb = ws2812_strip_update_rgb, + .length = ws2812_strip_length, +}; + +/* Lookup table formation macro definitions starts here*/ + +/* + * Reverses the bits in an 8-bit octet. + * For example, if the input octet is 0b10110010, the output will be 0b01001101. + */ +#define WS2812_UART_BYTE_BITREVERSE(octet) \ + ((((octet >> 0) & 1) << 7) | (((octet >> 1) & 1) << 6) | (((octet >> 2) & 1) << 5) | \ + (((octet >> 3) & 1) << 4) | (((octet >> 4) & 1) << 3) | (((octet >> 5) & 1) << 2) | \ + (((octet >> 6) & 1) << 1) | (((octet >> 7) & 1) << 0)) + +/* + * Extracts data bits from an inverted frame, removing the start and stop bits. + * Shifts the input frame right by 1 bit, and masks to keep only the relevant data bits. + */ +#define WS2812_PACKED_INVERTED_UART_WITHOUT_START_STOP(inv_pack_frame, data_bits) \ + (((inv_pack_frame) >> 1) & ((1 << (data_bits)) - 1)) + +/* + * Produces a UART byte for WS2812 LEDs by bit-reversing(because in UART transactions LSB is sent + * first) and packing the frame. Inverts the input frame(because we are inverting the tx-lines), + * extracts the relevant data bits, and right shifts the result. + */ +#define WS2812_PACKED_UART_BYTE(packed_frame, packed_frame_len) \ + (WS2812_UART_BYTE_BITREVERSE(WS2812_PACKED_INVERTED_UART_WITHOUT_START_STOP( \ + ~(packed_frame), packed_frame_len - 2)) >> \ + (8 - (packed_frame_len - 2))) + +/* + * Creates a 1x lookup table entry for WS2812 LEDs. + * The lookup table contains packed UART bytes for zero and one frames. + */ +#define WS2812_1X_LOOK_UP_TABLE_PREPARE(zero_frame, one_frame, frame_len) \ + { \ + WS2812_PACKED_UART_BYTE((zero_frame), (frame_len)), \ + WS2812_PACKED_UART_BYTE((one_frame), (frame_len)), \ + } + +/* + * Creates a 2x lookup table entry for WS2812 LEDs. + * The lookup table contains packed UART bytes for all combinations of zero and one frames. + */ +#define WS2812_2X_LOOK_UP_TABLE_PREPARE(zero_frame, one_frame, frame_len) \ + { \ + WS2812_PACKED_UART_BYTE(((zero_frame) << frame_len) | (zero_frame), \ + 2 * frame_len), \ + WS2812_PACKED_UART_BYTE(((zero_frame) << frame_len) | (one_frame), 2 * frame_len), \ + WS2812_PACKED_UART_BYTE(((one_frame) << frame_len) | (zero_frame), 2 * frame_len), \ + WS2812_PACKED_UART_BYTE(((one_frame) << frame_len) | (one_frame), 2 * frame_len), \ + } + +/* + * Creates a 3x lookup table entry for WS2812 LEDs. + * The lookup table contains packed UART bytes for all combinations of zero and one frames. + */ +#define WS2812_3X_LOOK_UP_TABLE_PREPARE(zero_frame, one_frame, frame_len) \ + { \ + WS2812_PACKED_UART_BYTE(((zero_frame) << 2 * frame_len) | \ + ((zero_frame) << frame_len) | (zero_frame), \ + 3 * frame_len), \ + WS2812_PACKED_UART_BYTE(((zero_frame) << 2 * frame_len) | \ + ((zero_frame) << frame_len) | (one_frame), \ + 3 * frame_len), \ + WS2812_PACKED_UART_BYTE(((zero_frame) << 2 * frame_len) | \ + ((one_frame) << frame_len) | (zero_frame), \ + 3 * frame_len), \ + WS2812_PACKED_UART_BYTE(((zero_frame) << 2 * frame_len) | \ + ((one_frame) << frame_len) | (one_frame), \ + 3 * frame_len), \ + WS2812_PACKED_UART_BYTE(((one_frame) << 2 * frame_len) | \ + ((zero_frame) << frame_len) | (zero_frame), \ + 3 * frame_len), \ + WS2812_PACKED_UART_BYTE(((one_frame) << 2 * frame_len) | \ + ((zero_frame) << frame_len) | (one_frame), \ + 3 * frame_len), \ + WS2812_PACKED_UART_BYTE(((one_frame) << 2 * frame_len) | \ + ((one_frame) << frame_len) | (zero_frame), \ + 3 * frame_len), \ + WS2812_PACKED_UART_BYTE(((one_frame) << 2 * frame_len) | \ + ((one_frame) << frame_len) | (one_frame), \ + 3 * frame_len), \ + } + +/* + * Macro to create a lookup table with repeated frames for WS2812 LEDs. + * This generates the appropriate lookup table based on the repeat count. + */ +#define WS2812_LOOK_UP_TABLE_LOOP(repeat_count, zero_frame, one_frame, frame_len) \ + WS2812_##repeat_count##X_LOOK_UP_TABLE_PREPARE((zero_frame) & ((1 << frame_len) - 1), \ + (one_frame) & ((1 << frame_len) - 1), \ + frame_len) + +/* + * Macro to prepare the lookup table for WS2812 LEDs. + * Generates the lookup table based on the repeat count, zero frame, one frame, and frame length. + */ +#define WS2812_LOOK_UP_TABLE_PREPARE(repeat_count, zero_frame, one_frame, frame_len) \ + WS2812_LOOK_UP_TABLE_LOOP(repeat_count, zero_frame, one_frame, frame_len) + +/* Lookup table formation macro definitions ends here*/ + +#define WS2812_UART_NUM_PIXELS(idx) (DT_INST_PROP(idx, chain_length)) +#define WS2812_UART_BUFSZ(idx, repeat) \ + ((WS2812_NUM_COLORS(idx) * 8 * WS2812_UART_NUM_PIXELS(idx)) / repeat) + +/* + * Retrieve the channel to color mapping (e.g. RGB, BGR, GRB, ...) from the + * "color-mapping" DT property. + */ +#define WS2812_COLOR_MAPPING(idx) \ + static const uint8_t ws2812_uart_##idx##_color_mapping[] = DT_INST_PROP(idx, color_mapping) + +#define WS2812_NUM_COLORS(idx) (DT_INST_PROP_LEN(idx, color_mapping)) + +/* Get the latch/reset delay from the "reset-delay" DT property. */ +#define WS2812_RESET_DELAY(idx) DT_INST_PROP(idx, reset_delay) + +#define UART_NODE(idx) DT_PARENT(DT_INST(idx, worldsemi_ws2812_uart)) + +#define WS2812_UART_DEVICE(idx) \ + \ + BUILD_ASSERT(DT_PROP(UART_NODE(idx), data_bits), "data-bits property missing"); \ + \ + BUILD_ASSERT(DT_PROP(UART_NODE(idx), data_bits) <= 8, "data-bits > 8 is not supported"); \ + \ + BUILD_ASSERT((2 + DT_PROP(UART_NODE(idx), data_bits)) % DT_INST_PROP(idx, frame_len) == 0, \ + "data-bits+2 should be a multiple of frame-len"); \ + \ + static uint8_t ws2812_uart_##idx##_px_buf[WS2812_UART_BUFSZ( \ + idx, DT_INST_PROP(idx, rgb_frame_per_uart_frame))]; \ + \ + WS2812_COLOR_MAPPING(idx); \ + static const uint8_t ws2812_uart_##idx##_lut[] = WS2812_LOOK_UP_TABLE_PREPARE( \ + DT_INST_PROP(idx, rgb_frame_per_uart_frame), DT_INST_PROP(idx, zero_frame), \ + DT_INST_PROP(idx, one_frame), DT_INST_PROP(idx, frame_len)); \ + \ + static const struct ws2812_uart_cfg ws2812_uart_##idx##_cfg = { \ + .uart_dev = DEVICE_DT_GET(DT_INST_BUS(idx)), \ + .px_buf = ws2812_uart_##idx##_px_buf, \ + .num_colors = WS2812_NUM_COLORS(idx), \ + .color_mapping = ws2812_uart_##idx##_color_mapping, \ + .reset_delay = WS2812_RESET_DELAY(idx), \ + .length = WS2812_UART_NUM_PIXELS(idx), \ + .uart_lookup_table = ws2812_uart_##idx##_lut, \ + .repeat_count = DT_INST_PROP(idx, rgb_frame_per_uart_frame), \ + }; \ + \ + DEVICE_DT_INST_DEFINE(idx, ws2812_uart_init, NULL, NULL, &ws2812_uart_##idx##_cfg, \ + POST_KERNEL, CONFIG_LED_STRIP_INIT_PRIORITY, &ws2812_uart_api); + +DT_INST_FOREACH_STATUS_OKAY(WS2812_UART_DEVICE) From f1d7650f645f557003ccd78267501009799bbda0 Mon Sep 17 00:00:00 2001 From: Anuj Pathak Date: Thu, 15 Aug 2024 17:23:16 +0530 Subject: [PATCH 3/3] samples: drivers: led_strip: extented to support ws2812-uart driver - added overlay file to test it on stamp_c3 board with on board led - added overlay file to test it on nucleo_u575 board with external led Signed-off-by: Ayush Kothari Signed-off-by: Anuj Pathak --- .../led_strip/boards/nucleo_u575zi_q.overlay | 35 +++++++++++++ .../drivers/led_strip/boards/stamp_c3.overlay | 50 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 samples/drivers/led_strip/boards/nucleo_u575zi_q.overlay create mode 100644 samples/drivers/led_strip/boards/stamp_c3.overlay diff --git a/samples/drivers/led_strip/boards/nucleo_u575zi_q.overlay b/samples/drivers/led_strip/boards/nucleo_u575zi_q.overlay new file mode 100644 index 0000000000000..6546291ca0fdf --- /dev/null +++ b/samples/drivers/led_strip/boards/nucleo_u575zi_q.overlay @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 Croxel, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include + +&usart3 { + status = "okay"; + current-speed = <7200000>; + pinctrl-0 = < &usart3_tx_pc10 &usart3_rx_pc11 >; + pinctrl-names = "default"; + data-bits = <7>; + tx-invert; + + led_strip: ws2812 { + compatible = "worldsemi,ws2812-uart"; + /* WS2812 */ + chain-length = <30>; /* arbitrary; change at will */ + color-mapping = ; + rgb-frame-per-uart-frame = <1>; + zero-frame = <0x1C0>; + one-frame = <0x1F8>; + frame-len = <9>; + }; +}; + +/ { + aliases { + led-strip = &led_strip; + }; +}; diff --git a/samples/drivers/led_strip/boards/stamp_c3.overlay b/samples/drivers/led_strip/boards/stamp_c3.overlay new file mode 100644 index 0000000000000..f48fa69da4647 --- /dev/null +++ b/samples/drivers/led_strip/boards/stamp_c3.overlay @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 Croxel, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include + +#include + +&uart0 { + status = "okay"; + current-speed = <2500000>; + pinctrl-0 = <&uart0_default>; + pinctrl-names = "default"; + data-bits = <7>; + tx-invert; + led_strip: ws2812 { + compatible = "worldsemi,ws2812-uart"; + + /* WS2812 */ + chain-length = <1>; /* arbitrary; change at will */ + color-mapping = ; + rgb-frame-per-uart-frame = <3>; + zero-frame = <0x4>; + one-frame = <0x6>; + frame-len = <3>; + }; +}; + +&pinctrl { + uart0_default: uart0_default { + group1 { + pinmux = ; + output-high; + }; + group2 { + pinmux = ; + bias-pull-up; + }; + }; +}; + +/ { + aliases { + led-strip = &led_strip; + }; +};