From b23ce48c1f4ceccacf20bffd6c8faa67a7fbd271 Mon Sep 17 00:00:00 2001 From: "Doug W." Date: Thu, 4 Dec 2025 13:13:40 -0500 Subject: [PATCH 1/5] ESP 5+ and RMT fix (#1) * Refactor RMT handling for ESP-IDF version compatibility and update LED strip driver initialization * Update header includes to use esp_idf_version.h for SDK version compatibility * Ensure esp_idf_version.h is included first in relevant source files for proper SDK version compatibility * Refactor LED strip driver for ESP-IDF 5.x compatibility by removing legacy RMT API code and simplifying initialization * Fix memory allocation for WS2812 LED strip by ensuring proper variable declaration --- nifs/atomvm_neopixel.c | 145 +++++++---------------- nifs/include/led_strip.h | 18 +-- nifs/led_strip_rmt_ws2812.c | 225 +++++++++++++++++++++++++++--------- 3 files changed, 210 insertions(+), 178 deletions(-) diff --git a/nifs/atomvm_neopixel.c b/nifs/atomvm_neopixel.c index 08f8460b..cc98cdf2 100644 --- a/nifs/atomvm_neopixel.c +++ b/nifs/atomvm_neopixel.c @@ -19,7 +19,6 @@ #include #include -#include #include #include #include @@ -31,18 +30,8 @@ #include "trace.h" #define TAG "atomvm_neopixel" -#define NO_ALLOC_FLAGS 0 -// References -// https://docs.espressif.com/projects/esp-idf/en/v3.3.4/api-reference/peripherals/rmt.html -// - -static const char *const led_strip_atom = "\x9" "led_strip"; -static const char *const channel_0_atom = "\x9" "channel_0"; -static const char *const channel_1_atom = "\x9" "channel_1"; -static const char *const channel_2_atom = "\x9" "channel_2"; -static const char *const channel_3_atom = "\x9" "channel_3"; -// 123456789ABCDEF01 +static const char *const led_strip_atom = "\x9" "led_strip"; static inline term ptr_to_binary(void *ptr, Context* ctx) @@ -61,25 +50,6 @@ static inline void *binary_to_ptr(term binary) } -static rmt_channel_t get_rmt_channel(Context *ctx, term channel) -{ - if (channel == globalcontext_make_atom(ctx->global, channel_0_atom)) { - return RMT_CHANNEL_1; - } else if (channel == globalcontext_make_atom(ctx->global, channel_1_atom)) { - return RMT_CHANNEL_2; - } else if (channel == globalcontext_make_atom(ctx->global, channel_2_atom)) { - return RMT_CHANNEL_2; - } else if (channel == globalcontext_make_atom(ctx->global, channel_3_atom)) { - return RMT_CHANNEL_3; -#if SOC_RMT_CHANNELS_PER_GROUP > 4 - // TODO -#endif - } else { - return RMT_CHANNEL_MAX; - } -} - - static term nif_init(Context *ctx, int argc, term argv[]) { UNUSED(argc); @@ -90,44 +60,28 @@ static term nif_init(Context *ctx, int argc, term argv[]) VALIDATE_VALUE(num_pixels, term_is_integer); term channel = argv[2]; VALIDATE_VALUE(channel, term_is_atom); - - rmt_channel_t rmt_channel = get_rmt_channel(ctx, channel); + // Note: channel argument is kept for API compatibility but ignored in ESP-IDF 5.x if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } else { - rmt_config_t config = RMT_DEFAULT_CONFIG_TX(term_to_int(pin), rmt_channel); - // set counter clock to 40MHz - config.clk_div = 2; - - esp_err_t err = rmt_config(&config); - if (err != ESP_OK) { - TRACE("Failed to initialize rmt config. err=%i\n", err); - term error_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(error_tuple, 0, ERROR_ATOM); - term_put_tuple_element(error_tuple, 1, term_from_int(err)); - return error_tuple; - } - err = rmt_driver_install(config.channel, 0, NO_ALLOC_FLAGS); - if (err != ESP_OK) { - TRACE("Failed to install rmt driver. err=%i\n", err); - term error_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(error_tuple, 0, ERROR_ATOM); - term_put_tuple_element(error_tuple, 1, term_from_int(err)); - return error_tuple; - } - led_strip_config_t strip_config = LED_STRIP_DEFAULT_CONFIG(term_to_int(num_pixels), (led_strip_dev_t) config.channel); - led_strip_t *strip = led_strip_new_rmt_ws2812(&strip_config); - if (!strip) { - TRACE("Failed to install WS2812 driver.\n"); - term error_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(error_tuple, 0, ERROR_ATOM); - term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(ctx->global, led_strip_atom)); - return error_tuple; - } - ESP_LOGI(TAG, "Installed WS2812 driver."); - return ptr_to_binary(strip, ctx); } + + led_strip_config_t strip_config = { + .max_leds = term_to_int(num_pixels), + .gpio_num = term_to_int(pin) + }; + + led_strip_t *strip = led_strip_new_rmt_ws2812(&strip_config); + if (!strip) { + TRACE("Failed to install WS2812 driver.\n"); + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(ctx->global, led_strip_atom)); + return error_tuple; + } + + ESP_LOGI(TAG, "Installed WS2812 driver."); + return ptr_to_binary(strip, ctx); } @@ -147,12 +101,11 @@ static term nif_clear(Context *ctx, int argc, term argv[]) TRACE("Failed to clear led strip. err=%i\n", err); if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } else { - term error_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(error_tuple, 0, ERROR_ATOM); - term_put_tuple_element(error_tuple, 1, term_from_int(err)); - return error_tuple; } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; } TRACE("Cleared led strip.\n"); return OK_ATOM; @@ -175,12 +128,11 @@ static term nif_refresh(Context *ctx, int argc, term argv[]) TRACE("Failed to refresh led strip. err=%i\n", err); if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } else { - term error_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(error_tuple, 0, ERROR_ATOM); - term_put_tuple_element(error_tuple, 1, term_from_int(err)); - return error_tuple; } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; } TRACE("Refreshed led strip.\n"); return OK_ATOM; @@ -210,12 +162,11 @@ static term nif_set_pixel_rgb(Context *ctx, int argc, term argv[]) TRACE("Failed to set pixel value on index %i (r=%i g=%i b=%i). err=%i\n", i, red, green, blue, err); if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } else { - term error_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(error_tuple, 0, ERROR_ATOM); - term_put_tuple_element(error_tuple, 1, term_from_int(err)); - return error_tuple; } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; } TRACE("Set pixel %i to r=%i g=%i b=%i\n", i, term_to_int(red), term_to_int(green), term_to_int(blue)); return OK_ATOM; @@ -250,12 +201,11 @@ static term nif_set_pixel_hsv(Context *ctx, int argc, term argv[]) TRACE("Failed to set pixel value on index %i (r=%i g=%i b=%i). err=%i\n", i, red, green, blue, err); if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } else { - term error_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(error_tuple, 0, ERROR_ATOM); - term_put_tuple_element(error_tuple, 1, term_from_int(err)); - return error_tuple; } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; } TRACE("Set pixel %i to r=%i g=%i b=%i\n", i, red, green, blue); return OK_ATOM; @@ -270,6 +220,7 @@ static term nif_tini(Context *ctx, int argc, term argv[]) VALIDATE_VALUE(handle, term_is_binary); term channel = argv[1]; VALIDATE_VALUE(channel, term_is_atom); + // Note: channel argument is kept for API compatibility but ignored in ESP-IDF 5.x led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); @@ -278,28 +229,14 @@ static term nif_tini(Context *ctx, int argc, term argv[]) TRACE("Failed to delete led strip. err=%i\n", err); if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } else { - term error_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(error_tuple, 0, ERROR_ATOM); - term_put_tuple_element(error_tuple, 1, term_from_int(err)); - return error_tuple; } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; } - rmt_channel_t rmt_channel = get_rmt_channel(ctx, channel); - err = rmt_driver_uninstall(term_to_int(rmt_channel)); - if (err != ESP_OK) { - TRACE("Failed to uninstall rmt driver. err=%i\n", err); - if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } else { - term error_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(error_tuple, 0, ERROR_ATOM); - term_put_tuple_element(error_tuple, 1, term_from_int(err)); - return error_tuple; - } - } - TRACE("LED strip niti'd\n"); + TRACE("LED strip tini'd\n"); return OK_ATOM; } diff --git a/nifs/include/led_strip.h b/nifs/include/led_strip.h index 64e86324..5e29f70b 100644 --- a/nifs/include/led_strip.h +++ b/nifs/include/led_strip.h @@ -25,12 +25,6 @@ extern "C" { */ typedef struct led_strip_s led_strip_t; -/** -* @brief LED Strip Device Type -* -*/ -typedef void *led_strip_dev_t; - /** * @brief Declare of LED Strip Type * @@ -99,19 +93,9 @@ struct led_strip_s { */ typedef struct { uint32_t max_leds; /*!< Maximum LEDs in a single strip */ - led_strip_dev_t dev; /*!< LED strip device (e.g. RMT channel, PWM channel, etc) */ + int gpio_num; /*!< GPIO number */ } led_strip_config_t; -/** - * @brief Default configuration for LED strip - * - */ -#define LED_STRIP_DEFAULT_CONFIG(number, dev_hdl) \ - { \ - .max_leds = number, \ - .dev = dev_hdl, \ - } - /** * @brief Install a new ws2812 driver (based on RMT peripheral) * diff --git a/nifs/led_strip_rmt_ws2812.c b/nifs/led_strip_rmt_ws2812.c index 9985f4f7..e8dd43e6 100644 --- a/nifs/led_strip_rmt_ws2812.c +++ b/nifs/led_strip_rmt_ws2812.c @@ -11,15 +11,18 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + #include #include #include #include "esp_log.h" #include "esp_attr.h" +#include "driver/rmt_tx.h" +#include "driver/rmt_encoder.h" #include "led_strip.h" -#include "driver/rmt.h" static const char *TAG = "ws2812"; + #define STRIP_CHECK(a, str, goto_tag, ret_value, ...) \ do \ { \ @@ -44,53 +47,134 @@ static uint32_t ws2812_t1l_ticks = 0; typedef struct { led_strip_t parent; - rmt_channel_t rmt_channel; + rmt_channel_handle_t rmt_chan; + rmt_encoder_handle_t rmt_encoder; uint32_t strip_len; uint8_t buffer[0]; } ws2812_t; -/** - * @brief Conver RGB data to RMT format. - * - * @note For WS2812, R,G,B each contains 256 different choices (i.e. uint8_t) - * - * @param[in] src: source data, to converted to RMT format - * @param[in] dest: place where to store the convert result - * @param[in] src_size: size of source data - * @param[in] wanted_num: number of RMT items that want to get - * @param[out] translated_size: number of source data that got converted - * @param[out] item_num: number of RMT items which are converted from source data - */ -static void IRAM_ATTR ws2812_rmt_adapter(const void *src, rmt_item32_t *dest, size_t src_size, - size_t wanted_num, size_t *translated_size, size_t *item_num) +// LED strip encoder +typedef struct { + rmt_encoder_t base; + rmt_encoder_t *bytes_encoder; + rmt_encoder_t *copy_encoder; + int state; + rmt_symbol_word_t reset_code; +} rmt_led_strip_encoder_t; + +static size_t rmt_encode_led_strip(rmt_encoder_t *encoder, rmt_channel_handle_t channel, + const void *primary_data, size_t data_size, rmt_encode_state_t *ret_state) { - if (src == NULL || dest == NULL) { - *translated_size = 0; - *item_num = 0; - return; + rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base); + rmt_encoder_handle_t bytes_encoder = led_encoder->bytes_encoder; + rmt_encoder_handle_t copy_encoder = led_encoder->copy_encoder; + rmt_encode_state_t session_state = RMT_ENCODING_RESET; + rmt_encode_state_t state = RMT_ENCODING_RESET; + size_t encoded_symbols = 0; + + switch (led_encoder->state) { + case 0: // send RGB data + encoded_symbols += bytes_encoder->encode(bytes_encoder, channel, primary_data, data_size, &session_state); + if (session_state & RMT_ENCODING_COMPLETE) { + led_encoder->state = 1; + } + if (session_state & RMT_ENCODING_MEM_FULL) { + state |= RMT_ENCODING_MEM_FULL; + goto out; + } + // fall-through + case 1: // send reset code + encoded_symbols += copy_encoder->encode(copy_encoder, channel, &led_encoder->reset_code, + sizeof(led_encoder->reset_code), &session_state); + if (session_state & RMT_ENCODING_COMPLETE) { + led_encoder->state = RMT_ENCODING_RESET; + state |= RMT_ENCODING_COMPLETE; + } + if (session_state & RMT_ENCODING_MEM_FULL) { + state |= RMT_ENCODING_MEM_FULL; + goto out; + } } - const rmt_item32_t bit0 = {{{ ws2812_t0h_ticks, 1, ws2812_t0l_ticks, 0 }}}; //Logical 0 - const rmt_item32_t bit1 = {{{ ws2812_t1h_ticks, 1, ws2812_t1l_ticks, 0 }}}; //Logical 1 - size_t size = 0; - size_t num = 0; - uint8_t *psrc = (uint8_t *)src; - rmt_item32_t *pdest = dest; - while (size < src_size && num < wanted_num) { - for (int i = 0; i < 8; i++) { - // MSB first - if (*psrc & (1 << (7 - i))) { - pdest->val = bit1.val; - } else { - pdest->val = bit0.val; - } - num++; - pdest++; +out: + *ret_state = state; + return encoded_symbols; +} + +static esp_err_t rmt_del_led_strip_encoder(rmt_encoder_t *encoder) +{ + rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base); + rmt_del_encoder(led_encoder->bytes_encoder); + rmt_del_encoder(led_encoder->copy_encoder); + free(led_encoder); + return ESP_OK; +} + +static esp_err_t rmt_led_strip_encoder_reset(rmt_encoder_t *encoder) +{ + rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base); + rmt_encoder_reset(led_encoder->bytes_encoder); + rmt_encoder_reset(led_encoder->copy_encoder); + led_encoder->state = RMT_ENCODING_RESET; + return ESP_OK; +} + +static esp_err_t rmt_new_led_strip_encoder(rmt_encoder_handle_t *ret_encoder) +{ + esp_err_t ret = ESP_OK; + rmt_led_strip_encoder_t *led_encoder = NULL; + + led_encoder = calloc(1, sizeof(rmt_led_strip_encoder_t)); + STRIP_CHECK(led_encoder, "allocate memory for led strip encoder failed", err, ESP_ERR_NO_MEM); + + led_encoder->base.encode = rmt_encode_led_strip; + led_encoder->base.del = rmt_del_led_strip_encoder; + led_encoder->base.reset = rmt_led_strip_encoder_reset; + + rmt_bytes_encoder_config_t bytes_encoder_config = { + .bit0 = { + .level0 = 1, + .duration0 = ws2812_t0h_ticks, + .level1 = 0, + .duration1 = ws2812_t0l_ticks, + }, + .bit1 = { + .level0 = 1, + .duration0 = ws2812_t1h_ticks, + .level1 = 0, + .duration1 = ws2812_t1l_ticks, + }, + .flags.msb_first = 1 + }; + + STRIP_CHECK(rmt_new_bytes_encoder(&bytes_encoder_config, &led_encoder->bytes_encoder) == ESP_OK, + "create bytes encoder failed", err, ESP_FAIL); + + rmt_copy_encoder_config_t copy_encoder_config = {}; + STRIP_CHECK(rmt_new_copy_encoder(©_encoder_config, &led_encoder->copy_encoder) == ESP_OK, + "create copy encoder failed", err, ESP_FAIL); + + uint32_t reset_ticks = WS2812_RESET_US * 40; // 40MHz resolution + led_encoder->reset_code = (rmt_symbol_word_t) { + .level0 = 0, + .duration0 = reset_ticks, + .level1 = 0, + .duration1 = reset_ticks, + }; + + *ret_encoder = &led_encoder->base; + return ESP_OK; + +err: + if (led_encoder) { + if (led_encoder->bytes_encoder) { + rmt_del_encoder(led_encoder->bytes_encoder); } - size++; - psrc++; + if (led_encoder->copy_encoder) { + rmt_del_encoder(led_encoder->copy_encoder); + } + free(led_encoder); } - *translated_size = size; - *item_num = num; + return ret; } static esp_err_t ws2812_set_pixel(led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue) @@ -98,8 +182,9 @@ static esp_err_t ws2812_set_pixel(led_strip_t *strip, uint32_t index, uint32_t r esp_err_t ret = ESP_OK; ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); STRIP_CHECK(index < ws2812->strip_len, "index out of the maximum number of leds", err, ESP_ERR_INVALID_ARG); + uint32_t start = index * 3; - // In thr order of GRB + // In the order of GRB ws2812->buffer[start + 0] = green & 0xFF; ws2812->buffer[start + 1] = red & 0xFF; ws2812->buffer[start + 2] = blue & 0xFF; @@ -112,9 +197,16 @@ static esp_err_t ws2812_refresh(led_strip_t *strip, uint32_t timeout_ms) { esp_err_t ret = ESP_OK; ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); - STRIP_CHECK(rmt_write_sample(ws2812->rmt_channel, ws2812->buffer, ws2812->strip_len * 3, true) == ESP_OK, + + rmt_transmit_config_t tx_config = { + .loop_count = 0, + }; + + STRIP_CHECK(rmt_transmit(ws2812->rmt_chan, ws2812->rmt_encoder, ws2812->buffer, ws2812->strip_len * 3, &tx_config) == ESP_OK, "transmit RMT samples failed", err, ESP_FAIL); - return rmt_wait_tx_done(ws2812->rmt_channel, pdMS_TO_TICKS(timeout_ms)); + STRIP_CHECK(rmt_tx_wait_all_done(ws2812->rmt_chan, timeout_ms) == ESP_OK, + "wait tx done failed", err, ESP_FAIL); + return ESP_OK; err: return ret; } @@ -122,7 +214,6 @@ static esp_err_t ws2812_refresh(led_strip_t *strip, uint32_t timeout_ms) static esp_err_t ws2812_clear(led_strip_t *strip, uint32_t timeout_ms) { ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); - // Write zero to turn off all leds memset(ws2812->buffer, 0, ws2812->strip_len * 3); return ws2812_refresh(strip, timeout_ms); } @@ -130,6 +221,14 @@ static esp_err_t ws2812_clear(led_strip_t *strip, uint32_t timeout_ms) static esp_err_t ws2812_del(led_strip_t *strip) { ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); + + if (ws2812->rmt_encoder) { + rmt_del_encoder(ws2812->rmt_encoder); + } + if (ws2812->rmt_chan) { + rmt_disable(ws2812->rmt_chan); + rmt_del_channel(ws2812->rmt_chan); + } free(ws2812); return ESP_OK; } @@ -137,29 +236,40 @@ static esp_err_t ws2812_del(led_strip_t *strip) led_strip_t *led_strip_new_rmt_ws2812(const led_strip_config_t *config) { led_strip_t *ret = NULL; + ws2812_t *ws2812 = NULL; + STRIP_CHECK(config, "configuration can't be null", err, NULL); - // 24 bits per led + // 24 bits per LED (3 bytes: G, R, B) uint32_t ws2812_size = sizeof(ws2812_t) + config->max_leds * 3; - ws2812_t *ws2812 = calloc(1, ws2812_size); + ws2812 = calloc(1, ws2812_size); STRIP_CHECK(ws2812, "request memory for ws2812 failed", err, NULL); - uint32_t counter_clk_hz = 0; - STRIP_CHECK(rmt_get_counter_clock((rmt_channel_t)config->dev, &counter_clk_hz) == ESP_OK, - "get rmt counter clock failed", err, NULL); - // ns -> ticks + rmt_tx_channel_config_t tx_chan_config = { + .clk_src = RMT_CLK_SRC_DEFAULT, + .gpio_num = (gpio_num_t)config->gpio_num, + .mem_block_symbols = 64, + .resolution_hz = 40000000, // 40MHz + .trans_queue_depth = 4, + }; + STRIP_CHECK(rmt_new_tx_channel(&tx_chan_config, &ws2812->rmt_chan) == ESP_OK, + "create RMT TX channel failed", err, NULL); + + // Calculate timing ticks (40MHz resolution) + uint32_t counter_clk_hz = 40000000; float ratio = (float)counter_clk_hz / 1e9; ws2812_t0h_ticks = (uint32_t)(ratio * WS2812_T0H_NS); ws2812_t0l_ticks = (uint32_t)(ratio * WS2812_T0L_NS); ws2812_t1h_ticks = (uint32_t)(ratio * WS2812_T1H_NS); ws2812_t1l_ticks = (uint32_t)(ratio * WS2812_T1L_NS); - // set ws2812 to rmt adapter - rmt_translator_init((rmt_channel_t)config->dev, ws2812_rmt_adapter); + STRIP_CHECK(rmt_new_led_strip_encoder(&ws2812->rmt_encoder) == ESP_OK, + "create led strip encoder failed", err, NULL); - ws2812->rmt_channel = (rmt_channel_t)config->dev; - ws2812->strip_len = config->max_leds; + STRIP_CHECK(rmt_enable(ws2812->rmt_chan) == ESP_OK, + "enable RMT TX channel failed", err, NULL); + ws2812->strip_len = config->max_leds; ws2812->parent.set_pixel = ws2812_set_pixel; ws2812->parent.refresh = ws2812_refresh; ws2812->parent.clear = ws2812_clear; @@ -167,19 +277,20 @@ led_strip_t *led_strip_new_rmt_ws2812(const led_strip_config_t *config) return &ws2812->parent; err: + if (ws2812) { + free(ws2812); + } return ret; } void led_strip_hsv2rgb(uint32_t h, uint32_t s, uint32_t v, uint32_t *r, uint32_t *g, uint32_t *b) { - h %= 360; // h -> [0,360] + h %= 360; uint32_t rgb_max = v * 2.55f; uint32_t rgb_min = rgb_max * (100 - s) / 100.0f; uint32_t i = h / 60; uint32_t diff = h % 60; - - // RGB adjustment amount by hue uint32_t rgb_adj = (rgb_max - rgb_min) * diff / 60; switch (i) { From 0eb17f8b6f2647a8ce4521f9c972a9623d43d50a Mon Sep 17 00:00:00 2001 From: "Doug W." Date: Thu, 4 Dec 2025 13:37:13 -0500 Subject: [PATCH 2/5] Brightness control (#2) * Add brightness control functions for NeoPixel strip * Refactor brightness handling in ws2812 LED strip driver --- nifs/atomvm_neopixel.c | 72 ++++++++++++++++++++++++++++++++++++- nifs/include/led_strip.h | 21 +++++++++++ nifs/led_strip_rmt_ws2812.c | 53 +++++++++++++++++++++++++-- src/neopixel.erl | 48 +++++++++++++++++++++++-- 4 files changed, 188 insertions(+), 6 deletions(-) diff --git a/nifs/atomvm_neopixel.c b/nifs/atomvm_neopixel.c index cc98cdf2..04402ac1 100644 --- a/nifs/atomvm_neopixel.c +++ b/nifs/atomvm_neopixel.c @@ -207,11 +207,63 @@ static term nif_set_pixel_hsv(Context *ctx, int argc, term argv[]) term_put_tuple_element(error_tuple, 1, term_from_int(err)); return error_tuple; } - TRACE("Set pixel %i to r=%i g=%i b=%i\n", i, red, green, blue); + TRACE("Set pixel %i to r=%i g=%i b=%i\n", i, term_to_int(red), term_to_int(green), term_to_int(blue)); return OK_ATOM; } +static term nif_set_brightness(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + term brightness = argv[1]; + VALIDATE_VALUE(brightness, term_is_integer); + + led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + + avm_int_t br = term_to_int(brightness); + if (br < 0 || br > 255) { + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, BADARG_ATOM); + return error_tuple; + } + + esp_err_t err = strip->set_brightness(strip, (uint8_t)br); + if (err != ESP_OK) { + TRACE("Failed to set brightness to %i. err=%i\n", br, err); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; + } + TRACE("Set brightness to %i\n", br); + return OK_ATOM; +} + + +static term nif_get_brightness(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + + led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + + uint8_t brightness = strip->get_brightness(strip); + return term_from_int(brightness); +} + + static term nif_tini(Context *ctx, int argc, term argv[]) { UNUSED(argc); @@ -266,6 +318,16 @@ static const struct Nif set_pixel_rgb_nif = .base.type = NIFFunctionType, .nif_ptr = nif_set_pixel_rgb }; +static const struct Nif set_brightness_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_set_brightness +}; +static const struct Nif get_brightness_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_get_brightness +}; static const struct Nif tini_nif = { .base.type = NIFFunctionType, @@ -305,6 +367,14 @@ const struct Nif *atomvm_neopixel_get_nif(const char *nifname) TRACE("Resolved platform nif %s ...\n", nifname); return &set_pixel_hsv_nif; } + if (strcmp("neopixel:nif_set_brightness/2", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &set_brightness_nif; + } + if (strcmp("neopixel:nif_get_brightness/1", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &get_brightness_nif; + } if (strcmp("neopixel:nif_tini/2", nifname) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &tini_nif; diff --git a/nifs/include/led_strip.h b/nifs/include/led_strip.h index 5e29f70b..e73c04eb 100644 --- a/nifs/include/led_strip.h +++ b/nifs/include/led_strip.h @@ -85,6 +85,26 @@ struct led_strip_s { * - ESP_FAIL: Free resources failed because error occurred */ esp_err_t (*del)(led_strip_t *strip); + + /** + * @brief Set global brightness for the strip + * + * @param strip: LED strip + * @param brightness: brightness value (0-255) + * + * @return + * - ESP_OK: Set brightness successfully + */ + esp_err_t (*set_brightness)(led_strip_t *strip, uint8_t brightness); + + /** + * @brief Get current global brightness + * + * @param strip: LED strip + * + * @return current brightness value (0-255) + */ + uint8_t (*get_brightness)(led_strip_t *strip); }; /** @@ -94,6 +114,7 @@ struct led_strip_s { typedef struct { uint32_t max_leds; /*!< Maximum LEDs in a single strip */ int gpio_num; /*!< GPIO number */ + uint8_t brightness; /*!< Global brightness (0-255), default 255 */ } led_strip_config_t; /** diff --git a/nifs/led_strip_rmt_ws2812.c b/nifs/led_strip_rmt_ws2812.c index e8dd43e6..3bafbdd3 100644 --- a/nifs/led_strip_rmt_ws2812.c +++ b/nifs/led_strip_rmt_ws2812.c @@ -50,7 +50,9 @@ typedef struct { rmt_channel_handle_t rmt_chan; rmt_encoder_handle_t rmt_encoder; uint32_t strip_len; - uint8_t buffer[0]; + uint8_t brightness; + uint8_t *out_buf; // Brightness-scaled output buffer + uint8_t buffer[0]; // Raw RGB values (flexible array member) } ws2812_t; // LED strip encoder @@ -183,6 +185,7 @@ static esp_err_t ws2812_set_pixel(led_strip_t *strip, uint32_t index, uint32_t r ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); STRIP_CHECK(index < ws2812->strip_len, "index out of the maximum number of leds", err, ESP_ERR_INVALID_ARG); + // Store raw RGB values - brightness applied at refresh time uint32_t start = index * 3; // In the order of GRB ws2812->buffer[start + 0] = green & 0xFF; @@ -197,12 +200,27 @@ static esp_err_t ws2812_refresh(led_strip_t *strip, uint32_t timeout_ms) { esp_err_t ret = ESP_OK; ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); + uint32_t buf_size = ws2812->strip_len * 3; + uint8_t *tx_buf; + + // Apply brightness scaling to output buffer + uint8_t br = ws2812->brightness; + if (br < 255) { + uint16_t scale = br + 1; + for (uint32_t i = 0; i < buf_size; i++) { + ws2812->out_buf[i] = (ws2812->buffer[i] * scale) >> 8; + } + tx_buf = ws2812->out_buf; + } else { + // Full brightness - transmit raw buffer directly (no copy needed) + tx_buf = ws2812->buffer; + } rmt_transmit_config_t tx_config = { .loop_count = 0, }; - STRIP_CHECK(rmt_transmit(ws2812->rmt_chan, ws2812->rmt_encoder, ws2812->buffer, ws2812->strip_len * 3, &tx_config) == ESP_OK, + STRIP_CHECK(rmt_transmit(ws2812->rmt_chan, ws2812->rmt_encoder, tx_buf, buf_size, &tx_config) == ESP_OK, "transmit RMT samples failed", err, ESP_FAIL); STRIP_CHECK(rmt_tx_wait_all_done(ws2812->rmt_chan, timeout_ms) == ESP_OK, "wait tx done failed", err, ESP_FAIL); @@ -218,6 +236,19 @@ static esp_err_t ws2812_clear(led_strip_t *strip, uint32_t timeout_ms) return ws2812_refresh(strip, timeout_ms); } +static esp_err_t ws2812_set_brightness(led_strip_t *strip, uint8_t brightness) +{ + ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); + ws2812->brightness = brightness; + return ESP_OK; +} + +static uint8_t ws2812_get_brightness(led_strip_t *strip) +{ + ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); + return ws2812->brightness; +} + static esp_err_t ws2812_del(led_strip_t *strip) { ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); @@ -229,6 +260,9 @@ static esp_err_t ws2812_del(led_strip_t *strip) rmt_disable(ws2812->rmt_chan); rmt_del_channel(ws2812->rmt_chan); } + if (ws2812->out_buf) { + free(ws2812->out_buf); + } free(ws2812); return ESP_OK; } @@ -237,13 +271,20 @@ led_strip_t *led_strip_new_rmt_ws2812(const led_strip_config_t *config) { led_strip_t *ret = NULL; ws2812_t *ws2812 = NULL; + uint8_t *out_buf = NULL; STRIP_CHECK(config, "configuration can't be null", err, NULL); // 24 bits per LED (3 bytes: G, R, B) - uint32_t ws2812_size = sizeof(ws2812_t) + config->max_leds * 3; + uint32_t buf_size = config->max_leds * 3; + uint32_t ws2812_size = sizeof(ws2812_t) + buf_size; ws2812 = calloc(1, ws2812_size); STRIP_CHECK(ws2812, "request memory for ws2812 failed", err, NULL); + + // Allocate output buffer for brightness-scaled data + out_buf = malloc(buf_size); + STRIP_CHECK(out_buf, "request memory for output buffer failed", err, NULL); + ws2812->out_buf = out_buf; rmt_tx_channel_config_t tx_chan_config = { .clk_src = RMT_CLK_SRC_DEFAULT, @@ -270,13 +311,19 @@ led_strip_t *led_strip_new_rmt_ws2812(const led_strip_config_t *config) "enable RMT TX channel failed", err, NULL); ws2812->strip_len = config->max_leds; + ws2812->brightness = config->brightness ? config->brightness : 255; ws2812->parent.set_pixel = ws2812_set_pixel; ws2812->parent.refresh = ws2812_refresh; ws2812->parent.clear = ws2812_clear; ws2812->parent.del = ws2812_del; + ws2812->parent.set_brightness = ws2812_set_brightness; + ws2812->parent.get_brightness = ws2812_get_brightness; return &ws2812->parent; err: + if (out_buf) { + free(out_buf); + } if (ws2812) { free(ws2812); } diff --git a/src/neopixel.erl b/src/neopixel.erl index 21f60dbd..49f6fe22 100644 --- a/src/neopixel.erl +++ b/src/neopixel.erl @@ -27,9 +27,11 @@ -module(neopixel). -export([ - start/2, start/3, stop/1, clear/1, set_pixel_rgb/5, set_pixel_hsv/5, refresh/1 + start/2, start/3, stop/1, clear/1, set_pixel_rgb/5, set_pixel_hsv/5, refresh/1, + set_brightness/2, get_brightness/1 ]). --export([nif_init/3, nif_clear/2, nif_refresh/2, nif_set_pixel_hsv/5, nif_set_pixel_rgb/5, nif_tini/2]). %% internal nif APIs +-export([nif_init/3, nif_clear/2, nif_refresh/2, nif_set_pixel_hsv/5, nif_set_pixel_rgb/5, nif_tini/2, + nif_set_brightness/2, nif_get_brightness/1]). %% internal nif APIs -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -behaviour(gen_server). @@ -41,6 +43,7 @@ -type channel() :: channel_0 | channel_1 | channel_2 | channel_3. -type color() :: 0..255. +-type brightness() :: 0..255. -type hue() :: 0..359. -type saturation() :: 0..100. -type value() :: 0..100. @@ -143,6 +146,35 @@ set_pixel_hsv(Neopixel, I, H, S, V) when is_pid(Neopixel), 0 =< H, H < 360, 0 =< set_pixel_hsv(_Neopixel, _I, _R, _G, _B) -> throw(badarg). +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param Brightness Brightness value (`0..255') +%% @returns ok | {error, Reason} +%% @doc Set global brightness for the strip. +%% +%% Brightness is applied when pixels are set. A value of 255 means full +%% brightness (no scaling), 128 means 50% brightness, 0 means off. +%% Note: You need to call refresh/1 and re-set pixels to see the effect. +%% @end +%%----------------------------------------------------------------------------- +-spec set_brightness(Neopixel::neopixel(), Brightness::brightness()) -> ok | {error, Reason::term()}. +set_brightness(Neopixel, Brightness) when is_pid(Neopixel), 0 =< Brightness, Brightness =< 255 -> + gen_server:call(Neopixel, {set_brightness, Brightness}); +set_brightness(_Neopixel, _Brightness) -> + throw(badarg). + +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @returns Brightness value (`0..255') +%% @doc Get current global brightness for the strip. +%% @end +%%----------------------------------------------------------------------------- +-spec get_brightness(Neopixel::neopixel()) -> brightness(). +get_brightness(Neopixel) when is_pid(Neopixel) -> + gen_server:call(Neopixel, get_brightness); +get_brightness(_Neopixel) -> + throw(badarg). + %% %% gen_server API %% @@ -168,6 +200,10 @@ handle_call({set_pixel_rgb, I, R, G, B}, _From, State) -> {reply, ?MODULE:nif_set_pixel_rgb(State#state.nif_handle, I, R, G, B), State}; handle_call({set_pixel_hsv, I, H, S, V}, _From, State) -> {reply, ?MODULE:nif_set_pixel_hsv(State#state.nif_handle, I, H, S, V), State}; +handle_call({set_brightness, Brightness}, _From, State) -> + {reply, ?MODULE:nif_set_brightness(State#state.nif_handle, Brightness), State}; +handle_call(get_brightness, _From, State) -> + {reply, ?MODULE:nif_get_brightness(State#state.nif_handle), State}; handle_call(Request, _From, State) -> {reply, {error, {unknown_request, Request}}, State}. @@ -240,6 +276,14 @@ nif_set_pixel_rgb(_NifHandle, _Index, _Red, _Green, _Blue) -> nif_set_pixel_hsv(_NifHandle, _Index, _Hue, _Saturation, _Value) -> throw(nif_error). +%% @hidden +nif_set_brightness(_NifHandle, _Brightness) -> + throw(nif_error). + +%% @hidden +nif_get_brightness(_NifHandle) -> + throw(nif_error). + %% @hidden nif_tini(_NifHandle, _Channel) -> throw(nif_error). From c8808b79baa18d1f330b63848851eb6cf737de66 Mon Sep 17 00:00:00 2001 From: "Doug W." Date: Thu, 4 Dec 2025 14:33:52 -0500 Subject: [PATCH 3/5] Rgbw (#3) * Add RGBW support for NeoPixel LED strips - Introduced `set_pixel_rgbw` function to handle RGBW pixel settings. - Updated NIF initialization to accept LED type (RGB/RGBW). - Enhanced LED strip configuration to include LED type. - Modified existing functions to accommodate RGBW data handling. * Refactor options type to support maps and proplists; add options normalization * log changes * Remove unnecessary logging for LED type and strip initialization in RGBW support * Enhance NeoPixel library documentation and add HSVW pixel control for SK6812 support --- README.md | 10 ++- markdown/neopixel.md | 103 ++++++++++++++++++++++++++-- nifs/atomvm_neopixel.c | 130 +++++++++++++++++++++++++++++++++++- nifs/include/led_strip.h | 33 ++++++++- nifs/led_strip_rmt_ws2812.c | 44 ++++++++++-- src/neopixel.erl | 110 ++++++++++++++++++++++++++---- 6 files changed, 396 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 7947a18b..ac1536f6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,14 @@ # AtomVM NeoPixel Library -This AtomVM Erlang library and Nif can be used to control WS2812 LED strips using the ESP32 SoC for any Erlang/Elixir programs targeted for AtomVM on the ESP32 platform. +This AtomVM Erlang library and Nif can be used to control WS2812 and SK6812 LED strips using the ESP32 SoC for any Erlang/Elixir programs targeted for AtomVM on the ESP32 platform. + +## Features + +- **RGB LED support** (WS2812, WS2812B) - 24-bit color +- **RGBW LED support** (SK6812) - 32-bit color with dedicated white channel +- **Global brightness control** - Hardware-efficient brightness scaling (0-255) +- **Multiple color spaces** - RGB, RGBW, HSV, and HSVW +- **ESP-IDF 5.x compatible** - Uses the new RMT driver API This Nif is included as an add-on to the AtomVM base image. In order to use this Nif in your AtomVM program, you must be able to build the AtomVM virtual machine, which in turn requires installation of the Espressif IDF SDK and tool chain. diff --git a/markdown/neopixel.md b/markdown/neopixel.md index 98316bfc..66111d9f 100644 --- a/markdown/neopixel.md +++ b/markdown/neopixel.md @@ -1,9 +1,16 @@ # Neopixel -This AtomVM Erlang library and Nif can be used to control WS2812 LED strips using the ESP32 SoC for any Erlang/Elixir programs targeted for AtomVM on the ESP32 platform. +This AtomVM Erlang library and Nif can be used to control WS2812 and SK6812 LED strips using the ESP32 SoC for any Erlang/Elixir programs targeted for AtomVM on the ESP32 platform. The AtomVM NeoPixel library is only supported on the ESP32 platform. +## Features + +- **RGB LED support** (WS2812, WS2812B) - 24-bit color (3 bytes per pixel) +- **RGBW LED support** (SK6812) - 32-bit color with dedicated white channel (4 bytes per pixel) +- **Global brightness control** - Efficient brightness scaling applied at refresh time +- **Multiple color spaces** - RGB, RGBW, and HSV + ## Build Instructions The AtomVM NeoPixel library is implemented as an AtomVM component, which includes some native C code that must be linked into the ESP32 AtomVM image. In order to build and deploy this client code, you must build an AtomVM binary image with this component compiled and linked into the image. @@ -16,9 +23,14 @@ Once the AtomVM image including this component has been flashed to your ESP32 de ## Programmer's Guide -The `atomvm_neopixel` library can be used to drive a strip of [WS2812](https://cdn-shop.adafruit.com/datasheets/WS2812.pdf) "Neopixel" LEDs. +The `atomvm_neopixel` library can be used to drive a strip of [WS2812](https://cdn-shop.adafruit.com/datasheets/WS2812.pdf) "Neopixel" LEDs or [SK6812](https://cdn-shop.adafruit.com/product-files/2757/p2757_SK6812RGBW_REV01.pdf) RGBW LEDs. + +Neopixel LED strips are individually addressable sets of "pixels". RGB strips (WS2812) contain 3 LEDs per pixel (red, green, blue), while RGBW strips (SK6812) contain 4 LEDs per pixel (red, green, blue, white). Each color channel can be configured using an 8-bit value (`0..255`). -Neopixel LED strips are individually addressable sets of "pixels", where each pixel contains 3 LEDs (red, blue, green). Each pixel can be configured using three 8-bit (`0..255`) values for red, green, and blue respectively, using an RGB color space. Alternatively, users may specify a hue (`0..359`), saturation (`0..100`), and value (`0..100`), using an HSV color space. +Colors can be specified using: +- **RGB** - Red, Green, Blue values (`0..255` each) +- **RGBW** - Red, Green, Blue, White values (`0..255` each) - for SK6812 strips only +- **HSV** - Hue (`0..359`), Saturation (`0..100`), Value (`0..100`) AtomVM programmers interface with the `atomvm_neopixel` API via the `neopixel` module, which provides operations for starting and stopping an Erlang process associated with a specified LED strip, and for setting values on each pixel in the strip. @@ -34,21 +46,27 @@ Use the `neopixel:start/2` function to initialize a neopixel instance. Specify {ok NeoPixel} = neopixel:start(Pin, NumPixels), ... -Use the `neopixel:start/3` function to initialize a neopixel instance with non-default options. Options are represented in an Erlang map. The permissible entries are encapsulated in the following table: +Use the `neopixel:start/3` function to initialize a neopixel instance with non-default options. Options can be specified as an Erlang map or a proplist (keyword list in Elixir). The permissible entries are encapsulated in the following table: | Key | Type | Default | Description | | ----- | ----- | ------| ----| +| `led_type` | `rgb \| rgbw` | `rgb` | LED strip type. Use `rgbw` for SK6812 RGBW strips. | | `timeout` | `non_neg_integer()` | 100 | Timeout (in milliseconds) used internally when communicating with the LED strip | -| `channel` | `channel_0\|channel_1\|channel_2\|channel_3` | `channel_0` | ESP RTC transmit channel to use. Use a different channel for each `neopixel` instance created. | +| `channel` | `channel_0 \| channel_1 \| channel_2 \| channel_3` | `channel_0` | Legacy option, ignored in ESP-IDF 5.x | -For example, +For example, to use an RGBW strip: %% erlang Pin = 18, NumPixels = 4, - {ok NeoPixel} = neopixel:start(Pin, NumPixels, #{channel => channel_1}), + {ok, NeoPixel} = neopixel:start(Pin, NumPixels, #{led_type => rgbw}), ... +Or in Elixir with a keyword list: + + # elixir + {:ok, neo_pixel} = :neopixel.start(18, 4, led_type: :rgbw) + The returned `NeoPixel` instance should be used for subsequent operations. Use the `neopixel:stop/1` function to stop a neopixel instance and free any resources in use by it. @@ -88,6 +106,33 @@ RGB values and their ranges are summarized in the following table: | green | `0..255` | Value of green LED | | blue | `0..255` | Value of blue LED | +#### RGBW Color Space (SK6812 only) + +For RGBW strips (SK6812), pixel colors can be set using the `neopixel:set_pixel_rgbw/6` function. This function is only available when the strip was initialized with `led_type => rgbw`. + +For example, to set the second pixel to red with 50% white: + + %% erlang + ok = neopixel:set_pixel_rgbw(NeoPixel, 1, 255, 0, 0, 128). + +Or in Elixir: + + # elixir + :ok = :neopixel.set_pixel_rgbw(neo_pixel, 1, 255, 0, 0, 128) + +RGBW values and their ranges are summarized in the following table: + +| Parameter | Range | Description | +| ----- | ----- | ------| +| red | `0..255` | Value of red LED | +| green | `0..255` | Value of green LED | +| blue | `0..255` | Value of blue LED | +| white | `0..255` | Value of white LED | + +> **Note:** Calling `set_pixel_rgbw/6` on an RGB strip will return `{error, not_supported}`. + +> **Note:** You can use `set_pixel_rgb/5` on RGBW strips - the white channel will be set to 0. + #### HSV color space Pixel colors can be set using the HSV color space via the `neopixel:set_pixel_hsv/5` function. Pixel indices are in the range `[0..NumPixels-1]`. @@ -107,6 +152,28 @@ HSV values and their ranges are summarized in the following table: | saturation | `0..100` | Color saturation, as a percentage, with 0 being all white, and 100 maximum color saturation. | | value | `0..100` | Value, as a percentage, with 0 being all dark, and 100 maximum brightness. | +#### HSVW color space (SK6812 only) + +For RGBW strips, use `neopixel:set_pixel_hsvw/6` to set a pixel using HSV values plus an independent white channel. The HSV values control the RGB LEDs while the white parameter controls the dedicated white LED. + + %% erlang + %% Set pixel 0 to orange (H=30) at full saturation and value, with 50% white + ok = neopixel:set_pixel_hsvw(NeoPixel, 0, 30, 100, 100, 128). + +Or in Elixir: + + # elixir + :ok = :neopixel.set_pixel_hsvw(neo_pixel, 0, 30, 100, 100, 128) + +| Parameter | Range | Description | +| ----- | ----- | ------| +| hue | `0..259` | Pixel hue (same as HSV) | +| saturation | `0..100` | Color saturation (same as HSV) | +| value | `0..100` | Value/brightness of RGB LEDs (same as HSV) | +| white | `0..255` | Value of the dedicated white LED | + +> **Note:** Calling `set_pixel_hsvw/6` on an RGB strip will return `{error, not_supported}`. + ### Refreshing pixels Use the `neopixel:refresh/1` function to ref refresh all the pixels in the LED strip. @@ -116,6 +183,28 @@ Use the `neopixel:refresh/1` function to ref refresh all the pixels in the LED s Refreshing the LED strip will manifest any changes made via any previous `set_pixel_*` operations (see above). +### Brightness Control + +Use the `neopixel:set_brightness/2` function to set the global brightness for the strip. Brightness is a value from 0 (off) to 255 (full brightness). + + %% erlang + ok = neopixel:set_brightness(NeoPixel, 128). %% 50% brightness + +Or in Elixir: + + # elixir + :ok = :neopixel.set_brightness(neo_pixel, 128) + +Brightness scaling is applied efficiently at refresh time, so: +- Original color values are preserved in memory +- Changing brightness and calling `refresh/1` immediately shows the effect +- No precision loss from repeated brightness changes + +Use the `neopixel:get_brightness/1` function to get the current brightness: + + %% erlang + Brightness = neopixel:get_brightness(NeoPixel). + ### API Reference To generate Reference API documentation in HTML, issue the rebar3 target diff --git a/nifs/atomvm_neopixel.c b/nifs/atomvm_neopixel.c index 04402ac1..881f823c 100644 --- a/nifs/atomvm_neopixel.c +++ b/nifs/atomvm_neopixel.c @@ -32,6 +32,8 @@ #define TAG "atomvm_neopixel" static const char *const led_strip_atom = "\x9" "led_strip"; +static const char *const rgbw_atom = "\x4" "rgbw"; +static const char *const not_supported_atom = "\xD" "not_supported"; static inline term ptr_to_binary(void *ptr, Context* ctx) @@ -60,15 +62,24 @@ static term nif_init(Context *ctx, int argc, term argv[]) VALIDATE_VALUE(num_pixels, term_is_integer); term channel = argv[2]; VALIDATE_VALUE(channel, term_is_atom); + term led_type_term = argv[3]; + VALIDATE_VALUE(led_type_term, term_is_atom); // Note: channel argument is kept for API compatibility but ignored in ESP-IDF 5.x if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } + // Determine LED type from atom + led_strip_type_t led_type = LED_STRIP_RGB; + if (globalcontext_is_term_equal_to_atom_string(ctx->global, led_type_term, rgbw_atom)) { + led_type = LED_STRIP_RGBW; + } + led_strip_config_t strip_config = { .max_leds = term_to_int(num_pixels), - .gpio_num = term_to_int(pin) + .gpio_num = term_to_int(pin), + .led_type = led_type }; led_strip_t *strip = led_strip_new_rmt_ws2812(&strip_config); @@ -212,6 +223,103 @@ static term nif_set_pixel_hsv(Context *ctx, int argc, term argv[]) } +static term nif_set_pixel_hsvw(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + term index = argv[1]; + VALIDATE_VALUE(index, term_is_integer); + term hue = argv[2]; + VALIDATE_VALUE(hue, term_is_integer); + term saturation = argv[3]; + VALIDATE_VALUE(saturation, term_is_integer); + term value = argv[4]; + VALIDATE_VALUE(value, term_is_integer); + term white = argv[5]; + VALIDATE_VALUE(white, term_is_integer); + + led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + + uint32_t red = 0; + uint32_t green = 0; + uint32_t blue = 0; + led_strip_hsv2rgb(term_to_int(hue), term_to_int(saturation), term_to_int(value), &red, &green, &blue); + + avm_int_t i = term_to_int(index); + esp_err_t err = strip->set_pixel_rgbw(strip, i, red, green, blue, term_to_int(white)); + if (err == ESP_ERR_NOT_SUPPORTED) { + TRACE("set_pixel_hsvw called on non-RGBW strip\n"); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(ctx->global, not_supported_atom)); + return error_tuple; + } + if (err != ESP_OK) { + TRACE("Failed to set pixel value on index %i. err=%i\n", i, err); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; + } + TRACE("Set pixel %i via HSVW\n", i); + return OK_ATOM; +} + + +static term nif_set_pixel_rgbw(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + term index = argv[1]; + VALIDATE_VALUE(index, term_is_integer); + term red = argv[2]; + VALIDATE_VALUE(red, term_is_integer); + term green = argv[3]; + VALIDATE_VALUE(green, term_is_integer); + term blue = argv[4]; + VALIDATE_VALUE(blue, term_is_integer); + term white = argv[5]; + VALIDATE_VALUE(white, term_is_integer); + + led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + + avm_int_t i = term_to_int(index); + esp_err_t err = strip->set_pixel_rgbw(strip, i, term_to_int(red), term_to_int(green), term_to_int(blue), term_to_int(white)); + if (err == ESP_ERR_NOT_SUPPORTED) { + TRACE("set_pixel_rgbw called on non-RGBW strip\n"); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(ctx->global, not_supported_atom)); + return error_tuple; + } + if (err != ESP_OK) { + TRACE("Failed to set pixel value on index %i (r=%i g=%i b=%i w=%i). err=%i\n", i, term_to_int(red), term_to_int(green), term_to_int(blue), term_to_int(white), err); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; + } + TRACE("Set pixel %i to r=%i g=%i b=%i w=%i\n", i, term_to_int(red), term_to_int(green), term_to_int(blue), term_to_int(white)); + return OK_ATOM; +} + + static term nif_set_brightness(Context *ctx, int argc, term argv[]) { UNUSED(argc); @@ -313,11 +421,21 @@ static const struct Nif set_pixel_hsv_nif = .base.type = NIFFunctionType, .nif_ptr = nif_set_pixel_hsv }; +static const struct Nif set_pixel_hsvw_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_set_pixel_hsvw +}; static const struct Nif set_pixel_rgb_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_set_pixel_rgb }; +static const struct Nif set_pixel_rgbw_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_set_pixel_rgbw +}; static const struct Nif set_brightness_nif = { .base.type = NIFFunctionType, @@ -347,7 +465,7 @@ void atomvm_neopixel_init(GlobalContext *global) const struct Nif *atomvm_neopixel_get_nif(const char *nifname) { TRACE("Locating nif %s ...", nifname); - if (strcmp("neopixel:nif_init/3", nifname) == 0) { + if (strcmp("neopixel:nif_init/4", nifname) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &init_nif; } @@ -363,10 +481,18 @@ const struct Nif *atomvm_neopixel_get_nif(const char *nifname) TRACE("Resolved platform nif %s ...\n", nifname); return &set_pixel_rgb_nif; } + if (strcmp("neopixel:nif_set_pixel_rgbw/6", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &set_pixel_rgbw_nif; + } if (strcmp("neopixel:nif_set_pixel_hsv/5", nifname) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &set_pixel_hsv_nif; } + if (strcmp("neopixel:nif_set_pixel_hsvw/6", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &set_pixel_hsvw_nif; + } if (strcmp("neopixel:nif_set_brightness/2", nifname) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &set_brightness_nif; diff --git a/nifs/include/led_strip.h b/nifs/include/led_strip.h index e73c04eb..d8cdac60 100644 --- a/nifs/include/led_strip.h +++ b/nifs/include/led_strip.h @@ -46,6 +46,24 @@ struct led_strip_s { */ esp_err_t (*set_pixel)(led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue); + /** + * @brief Set RGBW for a specific pixel (for RGBW strips like SK6812) + * + * @param strip: LED strip + * @param index: index of pixel to set + * @param red: red part of color + * @param green: green part of color + * @param blue: blue part of color + * @param white: white part of color + * + * @return + * - ESP_OK: Set RGBW for a specific pixel successfully + * - ESP_ERR_INVALID_ARG: Set RGBW failed because of invalid parameters + * - ESP_ERR_NOT_SUPPORTED: Strip is not RGBW type + * - ESP_FAIL: Set RGBW for a specific pixel failed because other error occurred + */ + esp_err_t (*set_pixel_rgbw)(led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white); + /** * @brief Refresh memory colors to LEDs * @@ -107,14 +125,23 @@ struct led_strip_s { uint8_t (*get_brightness)(led_strip_t *strip); }; +/** +* @brief LED Strip Type (RGB vs RGBW) +*/ +typedef enum { + LED_STRIP_RGB = 0, /*!< RGB LEDs (WS2812, WS2812B) - 3 bytes per pixel */ + LED_STRIP_RGBW = 1, /*!< RGBW LEDs (SK6812) - 4 bytes per pixel */ +} led_strip_type_t; + /** * @brief LED Strip Configuration Type * */ typedef struct { - uint32_t max_leds; /*!< Maximum LEDs in a single strip */ - int gpio_num; /*!< GPIO number */ - uint8_t brightness; /*!< Global brightness (0-255), default 255 */ + uint32_t max_leds; /*!< Maximum LEDs in a single strip */ + int gpio_num; /*!< GPIO number */ + uint8_t brightness; /*!< Global brightness (0-255), default 255 */ + led_strip_type_t led_type; /*!< LED type (RGB or RGBW), default RGB */ } led_strip_config_t; /** diff --git a/nifs/led_strip_rmt_ws2812.c b/nifs/led_strip_rmt_ws2812.c index 3bafbdd3..a511c107 100644 --- a/nifs/led_strip_rmt_ws2812.c +++ b/nifs/led_strip_rmt_ws2812.c @@ -51,8 +51,10 @@ typedef struct { rmt_encoder_handle_t rmt_encoder; uint32_t strip_len; uint8_t brightness; + uint8_t bytes_per_pixel; // 3 for RGB, 4 for RGBW + led_strip_type_t led_type; uint8_t *out_buf; // Brightness-scaled output buffer - uint8_t buffer[0]; // Raw RGB values (flexible array member) + uint8_t buffer[0]; // Raw RGB/RGBW values (flexible array member) } ws2812_t; // LED strip encoder @@ -186,11 +188,33 @@ static esp_err_t ws2812_set_pixel(led_strip_t *strip, uint32_t index, uint32_t r STRIP_CHECK(index < ws2812->strip_len, "index out of the maximum number of leds", err, ESP_ERR_INVALID_ARG); // Store raw RGB values - brightness applied at refresh time - uint32_t start = index * 3; - // In the order of GRB + uint32_t start = index * ws2812->bytes_per_pixel; + // In the order of GRB(W) ws2812->buffer[start + 0] = green & 0xFF; ws2812->buffer[start + 1] = red & 0xFF; ws2812->buffer[start + 2] = blue & 0xFF; + if (ws2812->bytes_per_pixel == 4) { + ws2812->buffer[start + 3] = 0; // White channel defaults to 0 for RGB calls + } + return ESP_OK; +err: + return ret; +} + +static esp_err_t ws2812_set_pixel_rgbw(led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white) +{ + esp_err_t ret = ESP_OK; + ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); + STRIP_CHECK(ws2812->led_type == LED_STRIP_RGBW, "set_pixel_rgbw called on non-RGBW strip", err, ESP_ERR_NOT_SUPPORTED); + STRIP_CHECK(index < ws2812->strip_len, "index out of the maximum number of leds", err, ESP_ERR_INVALID_ARG); + + // Store raw RGBW values - brightness applied at refresh time + uint32_t start = index * 4; + // In the order of GRBW + ws2812->buffer[start + 0] = green & 0xFF; + ws2812->buffer[start + 1] = red & 0xFF; + ws2812->buffer[start + 2] = blue & 0xFF; + ws2812->buffer[start + 3] = white & 0xFF; return ESP_OK; err: return ret; @@ -200,7 +224,7 @@ static esp_err_t ws2812_refresh(led_strip_t *strip, uint32_t timeout_ms) { esp_err_t ret = ESP_OK; ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); - uint32_t buf_size = ws2812->strip_len * 3; + uint32_t buf_size = ws2812->strip_len * ws2812->bytes_per_pixel; uint8_t *tx_buf; // Apply brightness scaling to output buffer @@ -232,7 +256,7 @@ static esp_err_t ws2812_refresh(led_strip_t *strip, uint32_t timeout_ms) static esp_err_t ws2812_clear(led_strip_t *strip, uint32_t timeout_ms) { ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); - memset(ws2812->buffer, 0, ws2812->strip_len * 3); + memset(ws2812->buffer, 0, ws2812->strip_len * ws2812->bytes_per_pixel); return ws2812_refresh(strip, timeout_ms); } @@ -275,8 +299,11 @@ led_strip_t *led_strip_new_rmt_ws2812(const led_strip_config_t *config) STRIP_CHECK(config, "configuration can't be null", err, NULL); - // 24 bits per LED (3 bytes: G, R, B) - uint32_t buf_size = config->max_leds * 3; + // Determine bytes per pixel based on LED type + uint8_t bytes_per_pixel = (config->led_type == LED_STRIP_RGBW) ? 4 : 3; + + // Allocate buffer based on LED type (3 bytes for RGB, 4 bytes for RGBW) + uint32_t buf_size = config->max_leds * bytes_per_pixel; uint32_t ws2812_size = sizeof(ws2812_t) + buf_size; ws2812 = calloc(1, ws2812_size); STRIP_CHECK(ws2812, "request memory for ws2812 failed", err, NULL); @@ -312,7 +339,10 @@ led_strip_t *led_strip_new_rmt_ws2812(const led_strip_config_t *config) ws2812->strip_len = config->max_leds; ws2812->brightness = config->brightness ? config->brightness : 255; + ws2812->bytes_per_pixel = bytes_per_pixel; + ws2812->led_type = config->led_type; ws2812->parent.set_pixel = ws2812_set_pixel; + ws2812->parent.set_pixel_rgbw = ws2812_set_pixel_rgbw; ws2812->parent.refresh = ws2812_refresh; ws2812->parent.clear = ws2812_clear; ws2812->parent.del = ws2812_del; diff --git a/src/neopixel.erl b/src/neopixel.erl index 49f6fe22..0354de3f 100644 --- a/src/neopixel.erl +++ b/src/neopixel.erl @@ -15,32 +15,43 @@ %% limitations under the License. %% %%----------------------------------------------------------------------------- -%% @doc WS2812 ("Neopixel") support. +%% @doc WS2812/SK6812 ("Neopixel") support. %% -%% Use this module to drive a strip of WS2812 "noepixel" LED strips. +%% Use this module to drive a strip of WS2812 or SK6812 "NeoPixel" LED strips. %% %% Each LED in a strip is individually addressable and can be configured in -%% 24-bit color, using either a Red-Green-Blue (RGB) or Hue-Saturation-Value (HSV) -%% color space. +%% 24-bit color (RGB) or 32-bit color (RGBW for SK6812), using either a +%% Red-Green-Blue (RGB/RGBW) or Hue-Saturation-Value (HSV) color space. +%% +%% Global brightness control is supported (0-255). +%% +%% Options: +%%
    +%%
  • `led_type' - `rgb' (default) or `rgbw' for SK6812 RGBW strips
  • +%%
  • `brightness' - Global brightness 0-255 (default 255)
  • +%%
  • `timeout' - Refresh timeout in ms (default 100)
  • +%%
  • `channel' - RMT channel (legacy, ignored in ESP-IDF 5.x)
  • +%%
%% @end %%----------------------------------------------------------------------------- -module(neopixel). -export([ - start/2, start/3, stop/1, clear/1, set_pixel_rgb/5, set_pixel_hsv/5, refresh/1, - set_brightness/2, get_brightness/1 + start/2, start/3, stop/1, clear/1, set_pixel_rgb/5, set_pixel_rgbw/6, set_pixel_hsv/5, + set_pixel_hsvw/6, refresh/1, set_brightness/2, get_brightness/1 ]). --export([nif_init/3, nif_clear/2, nif_refresh/2, nif_set_pixel_hsv/5, nif_set_pixel_rgb/5, nif_tini/2, - nif_set_brightness/2, nif_get_brightness/1]). %% internal nif APIs +-export([nif_init/4, nif_clear/2, nif_refresh/2, nif_set_pixel_hsv/5, nif_set_pixel_hsvw/6, + nif_set_pixel_rgb/5, nif_set_pixel_rgbw/6, nif_tini/2, nif_set_brightness/2, + nif_get_brightness/1]). %% internal nif APIs -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -behaviour(gen_server). -type neopixel() :: term(). -type pin() :: non_neg_integer(). --type options() :: [option()]. --type option() :: #{timeout => non_neg_integer(), channel => channel()}. +-type options() :: map() | proplists:proplist(). -type channel() :: channel_0 | channel_1 | channel_2 | channel_3. +-type led_type() :: rgb | rgbw. -type color() :: 0..255. -type brightness() :: 0..255. @@ -48,7 +59,7 @@ -type saturation() :: 0..100. -type value() :: 0..100. --define(DEFAULT_OPTIONS, #{timeout => 100, channel => channel_0}). +-define(DEFAULT_OPTIONS, #{timeout => 100, channel => channel_0, led_type => rgb}). -record(state, { pin :: pin(), @@ -79,7 +90,9 @@ start(Pin, NumPixels) -> %%----------------------------------------------------------------------------- -spec start(Pin::pin(), NumPixels::non_neg_integer(), Options::options()) -> {ok, neopixel()} | {error, Reason::term()}. start(Pin, NumPixels, Options) -> - gen_server:start(?MODULE, [Pin, NumPixels, validate_options(maps:merge(Options, ?DEFAULT_OPTIONS))], []). + NormalizedOpts = normalize_options(Options), + MergedOpts = maps:merge(?DEFAULT_OPTIONS, NormalizedOpts), + gen_server:start(?MODULE, [Pin, NumPixels, validate_options(MergedOpts)], []). %%----------------------------------------------------------------------------- %% @returns ok @@ -129,6 +142,25 @@ set_pixel_rgb(Neopixel, I, R, G, B) when is_pid(Neopixel), 0 =< R, R =< 255, 0 = set_pixel_rgb(_Neopixel, _I, _R, _G, _B) -> throw(badarg). +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param I pixel index (`0..NumPixels - 1') +%% @param R Red value (`0..255') +%% @param G Green value (`0..255') +%% @param B Blue value (`0..255') +%% @param W White value (`0..255') +%% @returns ok | {error, Reason} +%% @doc Set a pixel value in the RGBW color space (for SK6812 RGBW strips). +%% +%% Returns `{error, not_supported}' if called on an RGB strip. +%% @end +%%----------------------------------------------------------------------------- +-spec set_pixel_rgbw(Neopixel::neopixel(), I::non_neg_integer(), R::color(), G::color(), B::color(), W::color()) -> ok | {error, Reason::term()}. +set_pixel_rgbw(Neopixel, I, R, G, B, W) when is_pid(Neopixel), 0 =< R, R =< 255, 0 =< G, G =< 255, 0 =< B, B =< 255, 0 =< W, W =< 255 -> + gen_server:call(Neopixel, {set_pixel_rgbw, I, R, G, B, W}); +set_pixel_rgbw(_Neopixel, _I, _R, _G, _B, _W) -> + throw(badarg). + %%----------------------------------------------------------------------------- %% @param Neopixel Neopixel instance %% @param I pixel index (`0..NumPixels - 1') @@ -146,6 +178,26 @@ set_pixel_hsv(Neopixel, I, H, S, V) when is_pid(Neopixel), 0 =< H, H < 360, 0 =< set_pixel_hsv(_Neopixel, _I, _R, _G, _B) -> throw(badarg). +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param I pixel index (`0..NumPixels - 1') +%% @param H Hue value (`0..359') +%% @param S Saturation value (`0..100') +%% @param V Value (`0..100') +%% @param W White value (`0..255') +%% @returns ok | {error, Reason} +%% @doc Set a pixel value in the HSV color space with white channel (for SK6812 RGBW strips). +%% +%% The H, S, V values are converted to RGB, then combined with the white channel. +%% Returns `{error, not_supported}' if called on an RGB strip. +%% @end +%%----------------------------------------------------------------------------- +-spec set_pixel_hsvw(Neopixel::neopixel(), I::non_neg_integer(), H::hue(), S::saturation(), V::value(), W::color()) -> ok | {error, Reason::term()}. +set_pixel_hsvw(Neopixel, I, H, S, V, W) when is_pid(Neopixel), 0 =< H, H < 360, 0 =< S, S =< 100, 0 =< V, V =< 100, 0 =< W, W =< 255 -> + gen_server:call(Neopixel, {set_pixel_hsvw, I, H, S, V, W}); +set_pixel_hsvw(_Neopixel, _I, _H, _S, _V, _W) -> + throw(badarg). + %%----------------------------------------------------------------------------- %% @param Neopixel Neopixel instance %% @param Brightness Brightness value (`0..255') @@ -181,7 +233,7 @@ get_brightness(_Neopixel) -> %% @hidden init([Pin, NumPixels, Options]) -> - Handle = ?MODULE:nif_init(Pin, NumPixels, maps:get(channel, Options)), + Handle = ?MODULE:nif_init(Pin, NumPixels, maps:get(channel, Options), maps:get(led_type, Options)), {ok, #state{ pin=Pin, num_pixels=NumPixels, @@ -198,8 +250,12 @@ handle_call(refresh, _From, State) -> {reply, ?MODULE:nif_refresh(State#state.nif_handle, maps:get(timeout, State#state.options)), State}; handle_call({set_pixel_rgb, I, R, G, B}, _From, State) -> {reply, ?MODULE:nif_set_pixel_rgb(State#state.nif_handle, I, R, G, B), State}; +handle_call({set_pixel_rgbw, I, R, G, B, W}, _From, State) -> + {reply, ?MODULE:nif_set_pixel_rgbw(State#state.nif_handle, I, R, G, B, W), State}; handle_call({set_pixel_hsv, I, H, S, V}, _From, State) -> {reply, ?MODULE:nif_set_pixel_hsv(State#state.nif_handle, I, H, S, V), State}; +handle_call({set_pixel_hsvw, I, H, S, V, W}, _From, State) -> + {reply, ?MODULE:nif_set_pixel_hsvw(State#state.nif_handle, I, H, S, V, W), State}; handle_call({set_brightness, Brightness}, _From, State) -> {reply, ?MODULE:nif_set_brightness(State#state.nif_handle, Brightness), State}; handle_call(get_brightness, _From, State) -> @@ -227,10 +283,20 @@ code_change(_OldVsn, State, _Extra) -> %% internal operations %% +%% @private +%% @doc Convert options to map format, supporting both maps and proplists +normalize_options(Options) when is_map(Options) -> + Options; +normalize_options(Options) when is_list(Options) -> + maps:from_list(Options); +normalize_options(_) -> + throw(badarg). + %% @private validate_options(Options) -> validate_timeout_option(maps:get(timeout, Options, undefined)), validate_channel_option(maps:get(channel, Options, undefined)), + validate_led_type_option(maps:get(led_type, Options, undefined)), Options. %% @private @@ -251,13 +317,21 @@ validate_channel_option(channel_3) -> validate_channel_option(_Timeout) -> throw(badarg). +%% @private +validate_led_type_option(rgb) -> + ok; +validate_led_type_option(rgbw) -> + ok; +validate_led_type_option(_LedType) -> + throw(badarg). + %% %% Nifs %% %% @hidden -nif_init(_Pin, _NumPixels, _Channel) -> +nif_init(_Pin, _NumPixels, _Channel, _LedType) -> throw(nif_error). %% @hidden @@ -272,10 +346,18 @@ nif_refresh(_NifHandle, _Timeout) -> nif_set_pixel_rgb(_NifHandle, _Index, _Red, _Green, _Blue) -> throw(nif_error). +%% @hidden +nif_set_pixel_rgbw(_NifHandle, _Index, _Red, _Green, _Blue, _White) -> + throw(nif_error). + %% @hidden nif_set_pixel_hsv(_NifHandle, _Index, _Hue, _Saturation, _Value) -> throw(nif_error). +%% @hidden +nif_set_pixel_hsvw(_NifHandle, _Index, _Hue, _Saturation, _Value, _White) -> + throw(nif_error). + %% @hidden nif_set_brightness(_NifHandle, _Brightness) -> throw(nif_error). From 5965ab46550d7bc41acbafc8368cfc6c4a1b3e4e Mon Sep 17 00:00:00 2001 From: "Doug W." Date: Sat, 6 Dec 2025 18:53:10 -0500 Subject: [PATCH 4/5] Fill and setpixels (#4) * Add fill and set pixel functions for RGB and RGBW strips - Implemented `fill_rgb/4` and `fill_rgbw/5` functions to fill the entire strip with a single color. - Added `set_pixels_rgb/2`, `set_pixels_rgb/3`, `set_pixels_rgbw/2`, and `set_pixels_rgbw/3` functions to set multiple pixels from a list of colors. - Updated the NIF layer to support new functionalities and ensure proper error handling. - Enhanced documentation for new functions and usage examples in the markdown files. * Add HSV and HSVW fill functions for NeoPixel strips --- README.md | 1 + .../neopixel_example/src/neopixel_example.erl | 37 +- markdown/neopixel.md | 234 +++++++++++ nifs/atomvm_neopixel.c | 363 ++++++++++++++++++ nifs/include/led_strip.h | 28 ++ nifs/led_strip_rmt_ws2812.c | 85 +++- src/neopixel.erl | 187 ++++++++- 7 files changed, 928 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ac1536f6..78f7055f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ This AtomVM Erlang library and Nif can be used to control WS2812 and SK6812 LED - **RGBW LED support** (SK6812) - 32-bit color with dedicated white channel - **Global brightness control** - Hardware-efficient brightness scaling (0-255) - **Multiple color spaces** - RGB, RGBW, HSV, and HSVW +- **Batch operations** - Fill entire strip or set multiple pixels in a single call - **ESP-IDF 5.x compatible** - Uses the new RMT driver API This Nif is included as an add-on to the AtomVM base image. In order to use this Nif in your AtomVM program, you must be able to build the AtomVM virtual machine, which in turn requires installation of the Espressif IDF SDK and tool chain. diff --git a/examples/neopixel_example/src/neopixel_example.erl b/examples/neopixel_example/src/neopixel_example.erl index 10784d2c..6eed188c 100644 --- a/examples/neopixel_example/src/neopixel_example.erl +++ b/examples/neopixel_example/src/neopixel_example.erl @@ -16,7 +16,7 @@ %% -module(neopixel_example). --export([start/0]). +-export([start/0, demo_fill/0, demo_pattern/0]). -define(NEOPIXEL_PIN, 18). -define(NUM_PIXELS, 4). @@ -24,6 +24,7 @@ -define(SATURATION, 100). -define(VALUE, 15). +%% @doc Main example - rainbow cycle on each pixel start() -> {ok, NeoPixel} = neopixel:start(?NEOPIXEL_PIN, ?NUM_PIXELS), ok = neopixel:clear(NeoPixel), @@ -35,6 +36,40 @@ start() -> ), timer:sleep(infinity). +%% @doc Demo: Fill entire strip with solid colors +demo_fill() -> + {ok, NeoPixel} = neopixel:start(?NEOPIXEL_PIN, ?NUM_PIXELS), + ok = neopixel:clear(NeoPixel), + %% Cycle through red, green, blue + fill_loop(NeoPixel, [{255, 0, 0}, {0, 255, 0}, {0, 0, 255}]). + +fill_loop(NeoPixel, []) -> + fill_loop(NeoPixel, [{255, 0, 0}, {0, 255, 0}, {0, 0, 255}]); +fill_loop(NeoPixel, [{R, G, B} | Rest]) -> + ok = neopixel:fill_rgb(NeoPixel, R, G, B), + ok = neopixel:refresh(NeoPixel), + timer:sleep(1000), + fill_loop(NeoPixel, Rest). + +%% @doc Demo: Set multiple pixels with a pattern using set_pixels_rgb +demo_pattern() -> + {ok, NeoPixel} = neopixel:start(?NEOPIXEL_PIN, ?NUM_PIXELS), + ok = neopixel:clear(NeoPixel), + %% Create a rainbow pattern + Pattern = [{255, 0, 0}, {255, 127, 0}, {0, 255, 0}, {0, 0, 255}], + pattern_loop(NeoPixel, Pattern, 0). + +pattern_loop(NeoPixel, Pattern, Offset) -> + %% Rotate the pattern by Offset positions + RotatedPattern = rotate_list(Pattern, Offset), + ok = neopixel:set_pixels_rgb(NeoPixel, RotatedPattern), + ok = neopixel:refresh(NeoPixel), + timer:sleep(200), + pattern_loop(NeoPixel, Pattern, (Offset + 1) rem length(Pattern)). + +rotate_list(List, 0) -> List; +rotate_list([H | T], N) -> rotate_list(T ++ [H], N - 1). + loop(NeoPixel, I, Hue, SleepMs) -> ok = neopixel:set_pixel_hsv(NeoPixel, I, Hue, ?SATURATION, ?VALUE), ok = neopixel:refresh(NeoPixel), diff --git a/markdown/neopixel.md b/markdown/neopixel.md index 66111d9f..5b8dba07 100644 --- a/markdown/neopixel.md +++ b/markdown/neopixel.md @@ -74,6 +74,45 @@ Use the `neopixel:stop/1` function to stop a neopixel instance and free any reso %% erlang neopixel:stop(NeoPixel). +### Multiple Strips + +You can drive multiple LED strips simultaneously by starting multiple neopixel instances on different GPIO pins: + + %% erlang + %% Start three independent strips + {ok, Strip1} = neopixel:start(18, 30), %% 30-pixel RGB strip on pin 18 + {ok, Strip2} = neopixel:start(19, 60), %% 60-pixel RGB strip on pin 19 + {ok, Strip3} = neopixel:start(21, 16, #{led_type => rgbw}), %% 16-pixel RGBW strip on pin 21 + +Or in Elixir: + + # elixir + {:ok, strip1} = :neopixel.start(18, 30) + {:ok, strip2} = :neopixel.start(19, 60) + {:ok, strip3} = :neopixel.start(21, 16, led_type: :rgbw) + +Each strip is completely independent: +- Separate gen_server process +- Separate pixel buffer +- Separate RMT hardware channel +- Can be controlled from different processes concurrently + +**Hardware Limits:** The number of simultaneous strips is limited by available RMT TX channels: + +| ESP32 Variant | Max TX Channels | Max Strips | +|---------------|-----------------|------------| +| ESP32 | 4-8* | 4-8 | +| ESP32-S2 | 4 | 4 | +| ESP32-S3 | 4 | 4 | +| ESP32-C3 | 2 | 2 | +| ESP32-C6 | 2 | 2 | + +*ESP32 has 8 RMT channels that can be configured as TX or RX. Default is 4 TX + 4 RX. + +RMT channels are automatically allocated by ESP-IDF when you call `neopixel:start`. If no channels are available, start will return an error. + +> **Note:** The `channel` option in `neopixel:start/3` is legacy and ignored in ESP-IDF 5.x. Channel allocation is now automatic. + ### Clearing pixels Use the `neopixel:clear/1` function to clear all the pixels in the LED strip to an "off" value. @@ -174,6 +213,90 @@ Or in Elixir: > **Note:** Calling `set_pixel_hsvw/6` on an RGB strip will return `{error, not_supported}`. +### Filling the Strip + +Use the `neopixel:fill_rgb/4` function to set all pixels in the strip to the same color with a single call. + + %% erlang + %% Fill entire strip with red + ok = neopixel:fill_rgb(NeoPixel, 255, 0, 0). + +Or in Elixir: + + # elixir + :ok = :neopixel.fill_rgb(neo_pixel, 255, 0, 0) + +For RGBW strips, use `neopixel:fill_rgbw/5`: + + %% erlang + %% Fill entire strip with pure white (using white LED only) + ok = neopixel:fill_rgbw(NeoPixel, 0, 0, 0, 255). + +> **Note:** Calling `fill_rgbw/5` on an RGB strip will return `{error, not_supported}`. + +#### Filling with HSV Colors + +Use `neopixel:fill_hsv/4` to fill the strip using the HSV color space. This is convenient for color cycling effects like rainbows: + + %% erlang + %% Fill entire strip with red (hue=0) + ok = neopixel:fill_hsv(NeoPixel, 0, 100, 100). + + %% Fill with cyan (hue=180) at 50% brightness + ok = neopixel:fill_hsv(NeoPixel, 180, 100, 50). + +Or in Elixir: + + # elixir + # Rainbow cycle - just increment hue each frame + :ok = :neopixel.fill_hsv(neo_pixel, hue, 100, 50) + +For RGBW strips, use `neopixel:fill_hsvw/5` to combine HSV color with the white channel: + + %% erlang + %% Warm white: orange tint (H=30) plus white LED + ok = neopixel:fill_hsvw(NeoPixel, 30, 50, 50, 200). + +> **Note:** Calling `fill_hsvw/5` on an RGB strip will return `{error, not_supported}`. + +### Setting Multiple Pixels + +Use `neopixel:set_pixels_rgb/2` to set multiple pixels at once from a list of `{R, G, B}` tuples. Pixels are set starting at index 0. + + %% erlang + %% Set first 3 pixels to red, green, blue + Colors = [{255, 0, 0}, {0, 255, 0}, {0, 0, 255}], + ok = neopixel:set_pixels_rgb(NeoPixel, Colors). + +Or in Elixir: + + # elixir + colors = [{255, 0, 0}, {0, 255, 0}, {0, 0, 255}] + :ok = :neopixel.set_pixels_rgb(neo_pixel, colors) + +Use `neopixel:set_pixels_rgb/3` to set pixels starting at a specific offset: + + %% erlang + %% Set pixels 5, 6, 7 to red, green, blue + Colors = [{255, 0, 0}, {0, 255, 0}, {0, 0, 255}], + ok = neopixel:set_pixels_rgb(NeoPixel, 5, Colors). + +Or in Elixir: + + # elixir + :ok = :neopixel.set_pixels_rgb(neo_pixel, 5, colors) + +For RGBW strips, use `neopixel:set_pixels_rgbw/2` or `neopixel:set_pixels_rgbw/3` with `{R, G, B, W}` tuples: + + %% erlang + %% Set pixels 2, 3, 4 with different white levels + Colors = [{255, 0, 0, 0}, {0, 255, 0, 128}, {0, 0, 255, 255}], + ok = neopixel:set_pixels_rgbw(NeoPixel, 2, Colors). + +The list can be shorter than the strip length - only the specified pixels will be updated. + +> **Note:** Calling `set_pixels_rgbw/2` or `set_pixels_rgbw/3` on an RGB strip will return `{error, not_supported}`. + ### Refreshing pixels Use the `neopixel:refresh/1` function to ref refresh all the pixels in the LED strip. @@ -205,6 +328,77 @@ Use the `neopixel:get_brightness/1` function to get the current brightness: %% erlang Brightness = neopixel:get_brightness(NeoPixel). +### Concurrency + +The neopixel driver is implemented as a `gen_server` process, which has important implications for concurrent access. + +#### Safety + +**Yes, you can safely call neopixel functions from multiple processes.** All API functions (`set_pixel_*`, `fill_*`, `set_pixels_*`, `refresh`, etc.) use synchronous `gen_server:call/2`, which means: + +- Requests are serialized through the gen_server's mailbox +- Only one operation executes at a time +- Each call blocks until the operation completes +- No race conditions on the underlying hardware + +Example with multiple processes: + + %% erlang + {ok, NeoPixel} = neopixel:start(18, 30), + + %% Process 1: animate first half of strip + spawn(fun() -> animate_section(NeoPixel, 0, 14) end), + + %% Process 2: animate second half of strip + spawn(fun() -> animate_section(NeoPixel, 15, 29) end), + + %% Process 3: periodically refresh + spawn(fun() -> refresh_loop(NeoPixel, 16) end). %% 60 FPS + +#### Mailbox Considerations + +Since the gen_server processes requests sequentially, a few things to keep in mind: + +1. **Backpressure is automatic**: Because `gen_server:call` is synchronous, a calling process blocks until its request is handled. This naturally prevents any single process from flooding the mailbox. + +2. **Many concurrent callers**: If many processes call simultaneously, requests queue in the mailbox. Each caller blocks until their specific request completes. This is generally fine for typical LED animation patterns. + +3. **Blocking NIFs**: The NIF operations (especially `refresh`) block the gen_server while communicating with hardware. For a 30-LED strip, `refresh` typically takes ~1ms. During this time, other requests wait in the mailbox. + +4. **No mailbox overflow risk**: Under normal usage, the mailbox won't overflow because: + - Synchronous calls provide natural backpressure + - NIF operations are fast (microseconds to low milliseconds) + - Callers block while waiting, limiting request rate + +#### Performance Tips + +For best performance with multiple processes: + +1. **Batch updates**: Use `set_pixels_rgb/2,3` instead of multiple `set_pixel_rgb/5` calls - one NIF call vs N calls: + + %% Slow: 30 gen_server calls + [neopixel:set_pixel_rgb(NP, I, R, G, B) || I <- lists:seq(0, 29)]. + + %% Fast: 1 gen_server call + neopixel:set_pixels_rgb(NP, Colors). + +2. **Coordinate refresh**: If multiple processes set pixels, consider having a single process handle `refresh` at a fixed rate rather than each process refreshing after every update. + +3. **Partition the strip**: Assign different pixel ranges to different processes to avoid visual conflicts, then have a coordinator refresh. + +#### Timeout Handling + +All gen_server calls use the default 5-second timeout. If a call times out (extremely unlikely under normal conditions), the calling process crashes with a timeout error. The gen_server continues running. + +For custom timeout handling: + + %% erlang + try + neopixel:set_pixel_rgb(NeoPixel, 0, 255, 0, 0) + catch + exit:{timeout, _} -> handle_timeout() + end. + ### API Reference To generate Reference API documentation in HTML, issue the rebar3 target @@ -212,3 +406,43 @@ To generate Reference API documentation in HTML, issue the rebar3 target shell$ rebar3 edoc from the top level of the `atomvm_neopixel` source tree. Output is written to the `doc` directory. + +### Future Optimizations + +The following optimizations have been identified but not yet implemented. They could further improve performance on resource-constrained hardware: + +#### Binary Input for `set_pixels` + +Accept a binary `<>` instead of a list of tuples. This would avoid the overhead of unpacking Erlang tuples in the NIF and reduce memory allocations on the Erlang side. + +```erlang +%% Current (tuple list) +neopixel:set_pixels_rgb(Strip, [{255,0,0}, {0,255,0}, {0,0,255}]). + +%% Potential future API (binary) +neopixel:set_pixels_rgb_bin(Strip, <<255,0,0, 0,255,0, 0,0,255>>). +``` + +#### Asynchronous Operations + +Add `gen_server:cast` versions of operations for fire-and-forget scenarios where the caller doesn't need to wait for completion: + +```erlang +%% Current (blocking) +ok = neopixel:refresh(Strip). + +%% Potential future API (non-blocking) +ok = neopixel:refresh_async(Strip). +``` + +#### Dirty NIF for Refresh + +Mark the `refresh` NIF as a dirty NIF so it runs on a separate scheduler and doesn't block the main BEAM scheduler during RMT transmission. Most beneficial for very long strips (100+ LEDs). + +#### DMA Double-Buffering + +Prepare the next frame in a second buffer while the RMT peripheral transmits the current frame. This would allow true zero-copy animation at high frame rates, but adds memory overhead and complexity. + +--- + +Contributions implementing any of these optimizations are welcome! diff --git a/nifs/atomvm_neopixel.c b/nifs/atomvm_neopixel.c index 881f823c..50ad1c6a 100644 --- a/nifs/atomvm_neopixel.c +++ b/nifs/atomvm_neopixel.c @@ -372,6 +372,315 @@ static term nif_get_brightness(Context *ctx, int argc, term argv[]) } +static term nif_fill_rgb(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + term num_pixels = argv[1]; + VALIDATE_VALUE(num_pixels, term_is_integer); + term red = argv[2]; + VALIDATE_VALUE(red, term_is_integer); + term green = argv[3]; + VALIDATE_VALUE(green, term_is_integer); + term blue = argv[4]; + VALIDATE_VALUE(blue, term_is_integer); + + led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + + // Use optimized direct buffer fill + esp_err_t err = strip->fill(strip, term_to_int(red), term_to_int(green), term_to_int(blue)); + if (err != ESP_OK) { + TRACE("Failed to fill pixels. err=%i\n", err); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; + } + TRACE("Filled pixels with r=%i g=%i b=%i\n", term_to_int(red), term_to_int(green), term_to_int(blue)); + return OK_ATOM; +} + + +static term nif_fill_rgbw(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + term num_pixels = argv[1]; + VALIDATE_VALUE(num_pixels, term_is_integer); + term red = argv[2]; + VALIDATE_VALUE(red, term_is_integer); + term green = argv[3]; + VALIDATE_VALUE(green, term_is_integer); + term blue = argv[4]; + VALIDATE_VALUE(blue, term_is_integer); + term white = argv[5]; + VALIDATE_VALUE(white, term_is_integer); + + led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + + // Use optimized direct buffer fill + esp_err_t err = strip->fill_rgbw(strip, term_to_int(red), term_to_int(green), term_to_int(blue), term_to_int(white)); + if (err == ESP_ERR_NOT_SUPPORTED) { + TRACE("fill_rgbw called on non-RGBW strip\n"); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(ctx->global, not_supported_atom)); + return error_tuple; + } + if (err != ESP_OK) { + TRACE("Failed to fill pixels. err=%i\n", err); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; + } + TRACE("Filled pixels with r=%i g=%i b=%i w=%i\n", term_to_int(red), term_to_int(green), term_to_int(blue), term_to_int(white)); + return OK_ATOM; +} + + +static term nif_fill_hsv(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + term num_pixels = argv[1]; + VALIDATE_VALUE(num_pixels, term_is_integer); + term hue = argv[2]; + VALIDATE_VALUE(hue, term_is_integer); + term saturation = argv[3]; + VALIDATE_VALUE(saturation, term_is_integer); + term value = argv[4]; + VALIDATE_VALUE(value, term_is_integer); + + led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + + // Convert HSV to RGB + uint32_t red = 0, green = 0, blue = 0; + led_strip_hsv2rgb(term_to_int(hue), term_to_int(saturation), term_to_int(value), &red, &green, &blue); + + // Use optimized direct buffer fill with converted RGB values + esp_err_t err = strip->fill(strip, red, green, blue); + if (err != ESP_OK) { + TRACE("Failed to fill pixels. err=%i\n", err); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; + } + TRACE("Filled pixels with h=%i s=%i v=%i (r=%i g=%i b=%i)\n", + term_to_int(hue), term_to_int(saturation), term_to_int(value), red, green, blue); + return OK_ATOM; +} + + +static term nif_fill_hsvw(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + term num_pixels = argv[1]; + VALIDATE_VALUE(num_pixels, term_is_integer); + term hue = argv[2]; + VALIDATE_VALUE(hue, term_is_integer); + term saturation = argv[3]; + VALIDATE_VALUE(saturation, term_is_integer); + term value = argv[4]; + VALIDATE_VALUE(value, term_is_integer); + term white = argv[5]; + VALIDATE_VALUE(white, term_is_integer); + + led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + + // Convert HSV to RGB + uint32_t red = 0, green = 0, blue = 0; + led_strip_hsv2rgb(term_to_int(hue), term_to_int(saturation), term_to_int(value), &red, &green, &blue); + + // Use optimized direct buffer fill with converted RGB + white values + esp_err_t err = strip->fill_rgbw(strip, red, green, blue, term_to_int(white)); + if (err == ESP_ERR_NOT_SUPPORTED) { + TRACE("fill_hsvw called on non-RGBW strip\n"); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(ctx->global, not_supported_atom)); + return error_tuple; + } + if (err != ESP_OK) { + TRACE("Failed to fill pixels. err=%i\n", err); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; + } + TRACE("Filled pixels with h=%i s=%i v=%i w=%i\n", + term_to_int(hue), term_to_int(saturation), term_to_int(value), term_to_int(white)); + return OK_ATOM; +} + + +static term nif_set_pixels_rgb(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + term offset_term = argv[1]; + VALIDATE_VALUE(offset_term, term_is_integer); + term pixel_list = argv[2]; + VALIDATE_VALUE(pixel_list, term_is_list); + + led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_int_t offset = term_to_int(offset_term); + + avm_int_t index = 0; + term current = pixel_list; + while (!term_is_nil(current)) { + term color = term_get_list_head(current); + if (!term_is_tuple(color) || term_get_tuple_arity(color) != 3) { + TRACE("Invalid color tuple at index %i\n", index); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, BADARG_ATOM); + return error_tuple; + } + + term r_term = term_get_tuple_element(color, 0); + term g_term = term_get_tuple_element(color, 1); + term b_term = term_get_tuple_element(color, 2); + + if (!term_is_integer(r_term) || !term_is_integer(g_term) || !term_is_integer(b_term)) { + TRACE("Invalid color values at index %i\n", index); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, BADARG_ATOM); + return error_tuple; + } + + esp_err_t err = strip->set_pixel(strip, offset + index, term_to_int(r_term), term_to_int(g_term), term_to_int(b_term)); + if (err != ESP_OK) { + TRACE("Failed to set pixel %i. err=%i\n", offset + index, err); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; + } + + current = term_get_list_tail(current); + index++; + } + TRACE("Set %i pixels from list starting at offset %i\n", index, offset); + return OK_ATOM; +} + + +static term nif_set_pixels_rgbw(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + term offset_term = argv[1]; + VALIDATE_VALUE(offset_term, term_is_integer); + term pixel_list = argv[2]; + VALIDATE_VALUE(pixel_list, term_is_list); + + led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_int_t offset = term_to_int(offset_term); + + avm_int_t index = 0; + term current = pixel_list; + while (!term_is_nil(current)) { + term color = term_get_list_head(current); + if (!term_is_tuple(color) || term_get_tuple_arity(color) != 4) { + TRACE("Invalid color tuple at index %i\n", index); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, BADARG_ATOM); + return error_tuple; + } + + term r_term = term_get_tuple_element(color, 0); + term g_term = term_get_tuple_element(color, 1); + term b_term = term_get_tuple_element(color, 2); + term w_term = term_get_tuple_element(color, 3); + + if (!term_is_integer(r_term) || !term_is_integer(g_term) || !term_is_integer(b_term) || !term_is_integer(w_term)) { + TRACE("Invalid color values at index %i\n", index); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, BADARG_ATOM); + return error_tuple; + } + + esp_err_t err = strip->set_pixel_rgbw(strip, offset + index, term_to_int(r_term), term_to_int(g_term), term_to_int(b_term), term_to_int(w_term)); + if (err == ESP_ERR_NOT_SUPPORTED) { + TRACE("set_pixels_rgbw called on non-RGBW strip\n"); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(ctx->global, not_supported_atom)); + return error_tuple; + } + if (err != ESP_OK) { + TRACE("Failed to set pixel %i. err=%i\n", offset + index, err); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; + } + + current = term_get_list_tail(current); + index++; + } + TRACE("Set %i RGBW pixels from list starting at offset %i\n", index, offset); + return OK_ATOM; +} + + static term nif_tini(Context *ctx, int argc, term argv[]) { UNUSED(argc); @@ -446,6 +755,36 @@ static const struct Nif get_brightness_nif = .base.type = NIFFunctionType, .nif_ptr = nif_get_brightness }; +static const struct Nif fill_rgb_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_fill_rgb +}; +static const struct Nif fill_rgbw_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_fill_rgbw +}; +static const struct Nif fill_hsv_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_fill_hsv +}; +static const struct Nif fill_hsvw_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_fill_hsvw +}; +static const struct Nif set_pixels_rgb_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_set_pixels_rgb +}; +static const struct Nif set_pixels_rgbw_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_set_pixels_rgbw +}; static const struct Nif tini_nif = { .base.type = NIFFunctionType, @@ -501,6 +840,30 @@ const struct Nif *atomvm_neopixel_get_nif(const char *nifname) TRACE("Resolved platform nif %s ...\n", nifname); return &get_brightness_nif; } + if (strcmp("neopixel:nif_fill_rgb/5", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &fill_rgb_nif; + } + if (strcmp("neopixel:nif_fill_rgbw/6", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &fill_rgbw_nif; + } + if (strcmp("neopixel:nif_fill_hsv/5", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &fill_hsv_nif; + } + if (strcmp("neopixel:nif_fill_hsvw/6", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &fill_hsvw_nif; + } + if (strcmp("neopixel:nif_set_pixels_rgb/3", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &set_pixels_rgb_nif; + } + if (strcmp("neopixel:nif_set_pixels_rgbw/3", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &set_pixels_rgbw_nif; + } if (strcmp("neopixel:nif_tini/2", nifname) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &tini_nif; diff --git a/nifs/include/led_strip.h b/nifs/include/led_strip.h index d8cdac60..bb775cea 100644 --- a/nifs/include/led_strip.h +++ b/nifs/include/led_strip.h @@ -123,6 +123,34 @@ struct led_strip_s { * @return current brightness value (0-255) */ uint8_t (*get_brightness)(led_strip_t *strip); + + /** + * @brief Fill entire strip with a single RGB color + * + * @param strip: LED strip + * @param red: red part of color + * @param green: green part of color + * @param blue: blue part of color + * + * @return + * - ESP_OK: Fill successfully + */ + esp_err_t (*fill)(led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue); + + /** + * @brief Fill entire strip with a single RGBW color + * + * @param strip: LED strip + * @param red: red part of color + * @param green: green part of color + * @param blue: blue part of color + * @param white: white part of color + * + * @return + * - ESP_OK: Fill successfully + * - ESP_ERR_NOT_SUPPORTED: Strip is not RGBW type + */ + esp_err_t (*fill_rgbw)(led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue, uint32_t white); }; /** diff --git a/nifs/led_strip_rmt_ws2812.c b/nifs/led_strip_rmt_ws2812.c index a511c107..e8c27cbc 100644 --- a/nifs/led_strip_rmt_ws2812.c +++ b/nifs/led_strip_rmt_ws2812.c @@ -230,9 +230,23 @@ static esp_err_t ws2812_refresh(led_strip_t *strip, uint32_t timeout_ms) // Apply brightness scaling to output buffer uint8_t br = ws2812->brightness; if (br < 255) { + uint8_t *src = ws2812->buffer; + uint8_t *dst = ws2812->out_buf; uint16_t scale = br + 1; - for (uint32_t i = 0; i < buf_size; i++) { - ws2812->out_buf[i] = (ws2812->buffer[i] * scale) >> 8; + + // Process 4 bytes at a time when possible (common case for RGBW, + // and RGB strips with pixel count divisible by 4/3) + uint32_t i = 0; + uint32_t fast_end = buf_size & ~3U; // Round down to multiple of 4 + for (; i < fast_end; i += 4) { + dst[i + 0] = (src[i + 0] * scale) >> 8; + dst[i + 1] = (src[i + 1] * scale) >> 8; + dst[i + 2] = (src[i + 2] * scale) >> 8; + dst[i + 3] = (src[i + 3] * scale) >> 8; + } + // Handle remaining bytes + for (; i < buf_size; i++) { + dst[i] = (src[i] * scale) >> 8; } tx_buf = ws2812->out_buf; } else { @@ -273,6 +287,66 @@ static uint8_t ws2812_get_brightness(led_strip_t *strip) return ws2812->brightness; } +static esp_err_t ws2812_fill(led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue) +{ + ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); + uint8_t bytes_per_pixel = ws2812->bytes_per_pixel; + uint32_t strip_len = ws2812->strip_len; + uint8_t *buf = ws2812->buffer; + + // Pre-compute the GRB(W) values once + uint8_t g = green & 0xFF; + uint8_t r = red & 0xFF; + uint8_t b = blue & 0xFF; + + if (bytes_per_pixel == 3) { + // RGB: write 3 bytes per pixel + for (uint32_t i = 0; i < strip_len; i++) { + uint32_t idx = i * 3; + buf[idx + 0] = g; + buf[idx + 1] = r; + buf[idx + 2] = b; + } + } else { + // RGBW: write 4 bytes per pixel, W=0 + for (uint32_t i = 0; i < strip_len; i++) { + uint32_t idx = i * 4; + buf[idx + 0] = g; + buf[idx + 1] = r; + buf[idx + 2] = b; + buf[idx + 3] = 0; + } + } + return ESP_OK; +} + +static esp_err_t ws2812_fill_rgbw(led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue, uint32_t white) +{ + ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); + + if (ws2812->led_type != LED_STRIP_RGBW) { + return ESP_ERR_NOT_SUPPORTED; + } + + uint32_t strip_len = ws2812->strip_len; + uint8_t *buf = ws2812->buffer; + + // Pre-compute the GRBW values once + uint8_t g = green & 0xFF; + uint8_t r = red & 0xFF; + uint8_t b = blue & 0xFF; + uint8_t w = white & 0xFF; + + for (uint32_t i = 0; i < strip_len; i++) { + uint32_t idx = i * 4; + buf[idx + 0] = g; + buf[idx + 1] = r; + buf[idx + 2] = b; + buf[idx + 3] = w; + } + return ESP_OK; +} + static esp_err_t ws2812_del(led_strip_t *strip) { ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); @@ -348,6 +422,8 @@ led_strip_t *led_strip_new_rmt_ws2812(const led_strip_config_t *config) ws2812->parent.del = ws2812_del; ws2812->parent.set_brightness = ws2812_set_brightness; ws2812->parent.get_brightness = ws2812_get_brightness; + ws2812->parent.fill = ws2812_fill; + ws2812->parent.fill_rgbw = ws2812_fill_rgbw; return &ws2812->parent; err: @@ -363,8 +439,9 @@ led_strip_t *led_strip_new_rmt_ws2812(const led_strip_config_t *config) void led_strip_hsv2rgb(uint32_t h, uint32_t s, uint32_t v, uint32_t *r, uint32_t *g, uint32_t *b) { h %= 360; - uint32_t rgb_max = v * 2.55f; - uint32_t rgb_min = rgb_max * (100 - s) / 100.0f; + // Use integer math: rgb_max = v * 255 / 100 (avoiding float 2.55f) + uint32_t rgb_max = (v * 255 + 50) / 100; // +50 for rounding + uint32_t rgb_min = rgb_max * (100 - s) / 100; uint32_t i = h / 60; uint32_t diff = h % 60; diff --git a/src/neopixel.erl b/src/neopixel.erl index 0354de3f..84c19330 100644 --- a/src/neopixel.erl +++ b/src/neopixel.erl @@ -38,11 +38,14 @@ -export([ start/2, start/3, stop/1, clear/1, set_pixel_rgb/5, set_pixel_rgbw/6, set_pixel_hsv/5, - set_pixel_hsvw/6, refresh/1, set_brightness/2, get_brightness/1 + set_pixel_hsvw/6, refresh/1, set_brightness/2, get_brightness/1, + fill_rgb/4, fill_rgbw/5, fill_hsv/4, fill_hsvw/5, + set_pixels_rgb/2, set_pixels_rgb/3, set_pixels_rgbw/2, set_pixels_rgbw/3 ]). -export([nif_init/4, nif_clear/2, nif_refresh/2, nif_set_pixel_hsv/5, nif_set_pixel_hsvw/6, nif_set_pixel_rgb/5, nif_set_pixel_rgbw/6, nif_tini/2, nif_set_brightness/2, - nif_get_brightness/1]). %% internal nif APIs + nif_get_brightness/1, nif_fill_rgb/5, nif_fill_rgbw/6, nif_fill_hsv/5, nif_fill_hsvw/6, + nif_set_pixels_rgb/3, nif_set_pixels_rgbw/3]). %% internal nif APIs -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -behaviour(gen_server). @@ -58,6 +61,8 @@ -type hue() :: 0..359. -type saturation() :: 0..100. -type value() :: 0..100. +-type rgb_color() :: {color(), color(), color()}. +-type rgbw_color() :: {color(), color(), color(), color()}. -define(DEFAULT_OPTIONS, #{timeout => 100, channel => channel_0, led_type => rgb}). @@ -227,6 +232,148 @@ get_brightness(Neopixel) when is_pid(Neopixel) -> get_brightness(_Neopixel) -> throw(badarg). +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param R Red value (`0..255') +%% @param G Green value (`0..255') +%% @param B Blue value (`0..255') +%% @returns ok | {error, Reason} +%% @doc Fill entire strip with a single RGB color. +%% +%% Sets all pixels to the same color. Call refresh/1 to display. +%% @end +%%----------------------------------------------------------------------------- +-spec fill_rgb(Neopixel::neopixel(), R::color(), G::color(), B::color()) -> ok | {error, Reason::term()}. +fill_rgb(Neopixel, R, G, B) when is_pid(Neopixel), 0 =< R, R =< 255, 0 =< G, G =< 255, 0 =< B, B =< 255 -> + gen_server:call(Neopixel, {fill_rgb, R, G, B}); +fill_rgb(_Neopixel, _R, _G, _B) -> + throw(badarg). + +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param R Red value (`0..255') +%% @param G Green value (`0..255') +%% @param B Blue value (`0..255') +%% @param W White value (`0..255') +%% @returns ok | {error, Reason} +%% @doc Fill entire strip with a single RGBW color (for SK6812 RGBW strips). +%% +%% Sets all pixels to the same color. Call refresh/1 to display. +%% Returns `{error, not_supported}' if called on an RGB strip. +%% @end +%%----------------------------------------------------------------------------- +-spec fill_rgbw(Neopixel::neopixel(), R::color(), G::color(), B::color(), W::color()) -> ok | {error, Reason::term()}. +fill_rgbw(Neopixel, R, G, B, W) when is_pid(Neopixel), 0 =< R, R =< 255, 0 =< G, G =< 255, 0 =< B, B =< 255, 0 =< W, W =< 255 -> + gen_server:call(Neopixel, {fill_rgbw, R, G, B, W}); +fill_rgbw(_Neopixel, _R, _G, _B, _W) -> + throw(badarg). + +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param H Hue value (`0..359') +%% @param S Saturation value (`0..100') +%% @param V Value (`0..100') +%% @returns ok | {error, Reason} +%% @doc Fill entire strip with a single HSV color. +%% +%% HSV is converted to RGB in the NIF. Sets all pixels to the same color. +%% Call refresh/1 to display. +%% @end +%%----------------------------------------------------------------------------- +-spec fill_hsv(Neopixel::neopixel(), H::hue(), S::saturation(), V::value()) -> ok | {error, Reason::term()}. +fill_hsv(Neopixel, H, S, V) when is_pid(Neopixel), 0 =< H, H < 360, 0 =< S, S =< 100, 0 =< V, V =< 100 -> + gen_server:call(Neopixel, {fill_hsv, H, S, V}); +fill_hsv(_Neopixel, _H, _S, _V) -> + throw(badarg). + +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param H Hue value (`0..359') +%% @param S Saturation value (`0..100') +%% @param V Value (`0..100') +%% @param W White value (`0..255') +%% @returns ok | {error, Reason} +%% @doc Fill entire strip with a single HSVW color (for SK6812 RGBW strips). +%% +%% HSV is converted to RGB in the NIF, combined with the white channel. +%% Sets all pixels to the same color. Call refresh/1 to display. +%% Returns `{error, not_supported}' if called on an RGB strip. +%% @end +%%----------------------------------------------------------------------------- +-spec fill_hsvw(Neopixel::neopixel(), H::hue(), S::saturation(), V::value(), W::color()) -> ok | {error, Reason::term()}. +fill_hsvw(Neopixel, H, S, V, W) when is_pid(Neopixel), 0 =< H, H < 360, 0 =< S, S =< 100, 0 =< V, V =< 100, 0 =< W, W =< 255 -> + gen_server:call(Neopixel, {fill_hsvw, H, S, V, W}); +fill_hsvw(_Neopixel, _H, _S, _V, _W) -> + throw(badarg). + +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param Colors List of `{R, G, B}' tuples +%% @returns ok | {error, Reason} +%% @doc Set multiple pixels from a list of RGB colors. +%% +%% Each element in the list sets the corresponding pixel starting at index 0. +%% The list can be shorter than the strip length. +%% Call refresh/1 to display. +%% @end +%%----------------------------------------------------------------------------- +-spec set_pixels_rgb(Neopixel::neopixel(), Colors::[rgb_color()]) -> ok | {error, Reason::term()}. +set_pixels_rgb(Neopixel, Colors) -> + set_pixels_rgb(Neopixel, 0, Colors). + +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param Offset Starting pixel index (`0..NumPixels - 1') +%% @param Colors List of `{R, G, B}' tuples +%% @returns ok | {error, Reason} +%% @doc Set multiple pixels from a list of RGB colors starting at offset. +%% +%% Each element in the list sets the corresponding pixel starting at the given offset. +%% The list can be shorter than the strip length. +%% Call refresh/1 to display. +%% @end +%%----------------------------------------------------------------------------- +-spec set_pixels_rgb(Neopixel::neopixel(), Offset::non_neg_integer(), Colors::[rgb_color()]) -> ok | {error, Reason::term()}. +set_pixels_rgb(Neopixel, Offset, Colors) when is_pid(Neopixel), is_integer(Offset), Offset >= 0, is_list(Colors) -> + gen_server:call(Neopixel, {set_pixels_rgb, Offset, Colors}); +set_pixels_rgb(_Neopixel, _Offset, _Colors) -> + throw(badarg). + +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param Colors List of `{R, G, B, W}' tuples +%% @returns ok | {error, Reason} +%% @doc Set multiple pixels from a list of RGBW colors (for SK6812 RGBW strips). +%% +%% Each element in the list sets the corresponding pixel starting at index 0. +%% The list can be shorter than the strip length. +%% Call refresh/1 to display. +%% Returns `{error, not_supported}' if called on an RGB strip. +%% @end +%%----------------------------------------------------------------------------- +-spec set_pixels_rgbw(Neopixel::neopixel(), Colors::[rgbw_color()]) -> ok | {error, Reason::term()}. +set_pixels_rgbw(Neopixel, Colors) -> + set_pixels_rgbw(Neopixel, 0, Colors). + +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param Offset Starting pixel index (`0..NumPixels - 1') +%% @param Colors List of `{R, G, B, W}' tuples +%% @returns ok | {error, Reason} +%% @doc Set multiple pixels from a list of RGBW colors starting at offset. +%% +%% Each element in the list sets the corresponding pixel starting at the given offset. +%% The list can be shorter than the strip length. +%% Call refresh/1 to display. +%% Returns `{error, not_supported}' if called on an RGB strip. +%% @end +%%----------------------------------------------------------------------------- +-spec set_pixels_rgbw(Neopixel::neopixel(), Offset::non_neg_integer(), Colors::[rgbw_color()]) -> ok | {error, Reason::term()}. +set_pixels_rgbw(Neopixel, Offset, Colors) when is_pid(Neopixel), is_integer(Offset), Offset >= 0, is_list(Colors) -> + gen_server:call(Neopixel, {set_pixels_rgbw, Offset, Colors}); +set_pixels_rgbw(_Neopixel, _Offset, _Colors) -> + throw(badarg). + %% %% gen_server API %% @@ -260,6 +407,18 @@ handle_call({set_brightness, Brightness}, _From, State) -> {reply, ?MODULE:nif_set_brightness(State#state.nif_handle, Brightness), State}; handle_call(get_brightness, _From, State) -> {reply, ?MODULE:nif_get_brightness(State#state.nif_handle), State}; +handle_call({fill_rgb, R, G, B}, _From, State) -> + {reply, ?MODULE:nif_fill_rgb(State#state.nif_handle, State#state.num_pixels, R, G, B), State}; +handle_call({fill_rgbw, R, G, B, W}, _From, State) -> + {reply, ?MODULE:nif_fill_rgbw(State#state.nif_handle, State#state.num_pixels, R, G, B, W), State}; +handle_call({fill_hsv, H, S, V}, _From, State) -> + {reply, ?MODULE:nif_fill_hsv(State#state.nif_handle, State#state.num_pixels, H, S, V), State}; +handle_call({fill_hsvw, H, S, V, W}, _From, State) -> + {reply, ?MODULE:nif_fill_hsvw(State#state.nif_handle, State#state.num_pixels, H, S, V, W), State}; +handle_call({set_pixels_rgb, Offset, Colors}, _From, State) -> + {reply, ?MODULE:nif_set_pixels_rgb(State#state.nif_handle, Offset, Colors), State}; +handle_call({set_pixels_rgbw, Offset, Colors}, _From, State) -> + {reply, ?MODULE:nif_set_pixels_rgbw(State#state.nif_handle, Offset, Colors), State}; handle_call(Request, _From, State) -> {reply, {error, {unknown_request, Request}}, State}. @@ -366,6 +525,30 @@ nif_set_brightness(_NifHandle, _Brightness) -> nif_get_brightness(_NifHandle) -> throw(nif_error). +%% @hidden +nif_fill_rgb(_NifHandle, _NumPixels, _Red, _Green, _Blue) -> + throw(nif_error). + +%% @hidden +nif_fill_rgbw(_NifHandle, _NumPixels, _Red, _Green, _Blue, _White) -> + throw(nif_error). + +%% @hidden +nif_fill_hsv(_NifHandle, _NumPixels, _Hue, _Saturation, _Value) -> + throw(nif_error). + +%% @hidden +nif_fill_hsvw(_NifHandle, _NumPixels, _Hue, _Saturation, _Value, _White) -> + throw(nif_error). + +%% @hidden +nif_set_pixels_rgb(_NifHandle, _Offset, _Colors) -> + throw(nif_error). + +%% @hidden +nif_set_pixels_rgbw(_NifHandle, _Offset, _Colors) -> + throw(nif_error). + %% @hidden nif_tini(_NifHandle, _Channel) -> throw(nif_error). From e3cffe3f2fcc8069beb60528366ad9baf5974940 Mon Sep 17 00:00:00 2001 From: "Doug W." Date: Sun, 7 Dec 2025 13:22:04 -0500 Subject: [PATCH 5/5] Use `led_strip` component for backend (#5) * Refactor to use ESP-IDF led_strip component - Replace custom RMT driver with ESP-IDF led_strip component - Add idf_component.yml with espressif/led_strip dependency - Rename types to avm_led_strip_* to avoid conflicts - New led_strip_driver.c wrapper supports: - RMT with DMA on ESP32-S3/C6 - RMT without DMA on ESP32/C3 - SPI backend fallback for reliable DMA on all chips - Remove old led_strip_rmt_ws2812.c and led_strip.h * Update led_strip dependency to ^3.0.2 * Fix led_strip v3.x API: use color_component_format instead of led_pixel_format * Improve backend selection for better WiFi coexistence - Prefer SPI backend on original ESP32 (best WiFi coexistence) - Add Kconfig options to force backend selection (Auto/SPI/RMT) - Cleaner fallback logic with informative log messages - ESP32-S3/C6/P4 still prefer RMT with DMA --- CMakeLists.txt | 4 +- Kconfig | 36 +++ idf_component.yml | 5 + nifs/atomvm_neopixel.c | 48 ++-- nifs/include/atomvm_led_strip.h | 88 ++++++ nifs/include/led_strip.h | 188 ------------- nifs/led_strip_driver.c | 455 ++++++++++++++++++++++++++++++ nifs/led_strip_rmt_ws2812.c | 482 -------------------------------- 8 files changed, 610 insertions(+), 696 deletions(-) create mode 100644 idf_component.yml create mode 100644 nifs/include/atomvm_led_strip.h delete mode 100644 nifs/include/led_strip.h create mode 100644 nifs/led_strip_driver.c delete mode 100644 nifs/led_strip_rmt_ws2812.c diff --git a/CMakeLists.txt b/CMakeLists.txt index b17005cd..6428423b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ set(ATOMVM_NEOPIXEL_COMPONENT_SRCS "nifs/atomvm_neopixel.c" - "nifs/led_strip_rmt_ws2812.c" + "nifs/led_strip_driver.c" ) # WHOLE_ARCHIVE option is supported only with esp-idf 5.x @@ -34,7 +34,7 @@ endif() idf_component_register( SRCS ${ATOMVM_NEOPIXEL_COMPONENT_SRCS} INCLUDE_DIRS "nifs/include" - PRIV_REQUIRES "libatomvm" "avm_sys" "driver" + PRIV_REQUIRES "libatomvm" "avm_sys" "driver" "led_strip" ${OPTIONAL_WHOLE_ARCHIVE} ) diff --git a/Kconfig b/Kconfig index b99037d7..38105b18 100644 --- a/Kconfig +++ b/Kconfig @@ -5,5 +5,41 @@ config AVM_NEOPIXEL_ENABLE default y help Use this parameter to enable or disable the AtomVM NEOPIXEL driver. + + This driver uses the ESP-IDF led_strip component which provides: + - Automatic DMA support on ESP32-S3/C6 + - SPI backend fallback for better WiFi coexistence + - Maintained by Espressif for optimal performance + +choice AVM_NEOPIXEL_BACKEND + prompt "LED strip backend" + default AVM_NEOPIXEL_BACKEND_AUTO + depends on AVM_NEOPIXEL_ENABLE + help + Select the backend peripheral for driving the LED strip. + +config AVM_NEOPIXEL_BACKEND_AUTO + bool "Auto (recommended)" + help + Automatically select the best backend for your chip: + - ESP32: SPI with DMA (best WiFi coexistence) + - ESP32-S3/C6/P4: RMT with DMA + - Others: RMT, fallback to SPI + +config AVM_NEOPIXEL_BACKEND_SPI + bool "Force SPI" + help + Always use SPI backend with DMA. + Best for WiFi coexistence on all chips. + Note: Uses entire SPI bus (SPI2_HOST). + +config AVM_NEOPIXEL_BACKEND_RMT + bool "Force RMT" + help + Always use RMT backend. + May cause flickering with WiFi on ESP32 (no DMA). + Works well on ESP32-S3/C6/P4 with DMA. + +endchoice endmenu diff --git a/idf_component.yml b/idf_component.yml new file mode 100644 index 00000000..4f144007 --- /dev/null +++ b/idf_component.yml @@ -0,0 +1,5 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/led_strip: "^3.0.2" + idf: + version: ">=5.0" diff --git a/nifs/atomvm_neopixel.c b/nifs/atomvm_neopixel.c index 50ad1c6a..5a369da1 100644 --- a/nifs/atomvm_neopixel.c +++ b/nifs/atomvm_neopixel.c @@ -24,7 +24,7 @@ #include #include #include -#include "led_strip.h" +#include "atomvm_led_strip.h" // #define ENABLE_TRACE #include "trace.h" @@ -71,18 +71,18 @@ static term nif_init(Context *ctx, int argc, term argv[]) } // Determine LED type from atom - led_strip_type_t led_type = LED_STRIP_RGB; + avm_led_strip_type_t led_type = AVM_LED_STRIP_RGB; if (globalcontext_is_term_equal_to_atom_string(ctx->global, led_type_term, rgbw_atom)) { - led_type = LED_STRIP_RGBW; + led_type = AVM_LED_STRIP_RGBW; } - led_strip_config_t strip_config = { + avm_led_strip_config_t strip_config = { .max_leds = term_to_int(num_pixels), .gpio_num = term_to_int(pin), .led_type = led_type }; - led_strip_t *strip = led_strip_new_rmt_ws2812(&strip_config); + avm_led_strip_t *strip = avm_led_strip_new(&strip_config); if (!strip) { TRACE("Failed to install WS2812 driver.\n"); term error_tuple = term_alloc_tuple(2, &ctx->heap); @@ -105,7 +105,7 @@ static term nif_clear(Context *ctx, int argc, term argv[]) term timeout = argv[1]; VALIDATE_VALUE(timeout, term_is_integer); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); esp_err_t err = strip->clear(strip, term_to_int(timeout)); if (err != ESP_OK) { @@ -132,7 +132,7 @@ static term nif_refresh(Context *ctx, int argc, term argv[]) term timeout = argv[1]; VALIDATE_VALUE(timeout, term_is_integer); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); esp_err_t err = strip->refresh(strip, term_to_int(timeout)); if (err != ESP_OK) { @@ -165,7 +165,7 @@ static term nif_set_pixel_rgb(Context *ctx, int argc, term argv[]) term blue = argv[4]; VALIDATE_VALUE(blue, term_is_integer); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); avm_int_t i = term_to_int(index); esp_err_t err = strip->set_pixel(strip, i, term_to_int(red), term_to_int(green), term_to_int(blue)); @@ -199,12 +199,12 @@ static term nif_set_pixel_hsv(Context *ctx, int argc, term argv[]) term value = argv[4]; VALIDATE_VALUE(value, term_is_integer); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); uint32_t red = 0; uint32_t green = 0; uint32_t blue = 0; - led_strip_hsv2rgb(term_to_int(hue), term_to_int(saturation), term_to_int(value), &red, &green, &blue); + avm_led_strip_hsv2rgb(term_to_int(hue), term_to_int(saturation), term_to_int(value), &red, &green, &blue); avm_int_t i = term_to_int(index); esp_err_t err = strip->set_pixel(strip, i, red, green, blue); @@ -240,12 +240,12 @@ static term nif_set_pixel_hsvw(Context *ctx, int argc, term argv[]) term white = argv[5]; VALIDATE_VALUE(white, term_is_integer); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); uint32_t red = 0; uint32_t green = 0; uint32_t blue = 0; - led_strip_hsv2rgb(term_to_int(hue), term_to_int(saturation), term_to_int(value), &red, &green, &blue); + avm_led_strip_hsv2rgb(term_to_int(hue), term_to_int(saturation), term_to_int(value), &red, &green, &blue); avm_int_t i = term_to_int(index); esp_err_t err = strip->set_pixel_rgbw(strip, i, red, green, blue, term_to_int(white)); @@ -291,7 +291,7 @@ static term nif_set_pixel_rgbw(Context *ctx, int argc, term argv[]) term white = argv[5]; VALIDATE_VALUE(white, term_is_integer); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); avm_int_t i = term_to_int(index); esp_err_t err = strip->set_pixel_rgbw(strip, i, term_to_int(red), term_to_int(green), term_to_int(blue), term_to_int(white)); @@ -329,7 +329,7 @@ static term nif_set_brightness(Context *ctx, int argc, term argv[]) term brightness = argv[1]; VALIDATE_VALUE(brightness, term_is_integer); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); avm_int_t br = term_to_int(brightness); if (br < 0 || br > 255) { @@ -365,7 +365,7 @@ static term nif_get_brightness(Context *ctx, int argc, term argv[]) term handle = argv[0]; VALIDATE_VALUE(handle, term_is_binary); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); uint8_t brightness = strip->get_brightness(strip); return term_from_int(brightness); @@ -387,7 +387,7 @@ static term nif_fill_rgb(Context *ctx, int argc, term argv[]) term blue = argv[4]; VALIDATE_VALUE(blue, term_is_integer); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); // Use optimized direct buffer fill esp_err_t err = strip->fill(strip, term_to_int(red), term_to_int(green), term_to_int(blue)); @@ -423,7 +423,7 @@ static term nif_fill_rgbw(Context *ctx, int argc, term argv[]) term white = argv[5]; VALIDATE_VALUE(white, term_is_integer); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); // Use optimized direct buffer fill esp_err_t err = strip->fill_rgbw(strip, term_to_int(red), term_to_int(green), term_to_int(blue), term_to_int(white)); @@ -467,11 +467,11 @@ static term nif_fill_hsv(Context *ctx, int argc, term argv[]) term value = argv[4]; VALIDATE_VALUE(value, term_is_integer); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); // Convert HSV to RGB uint32_t red = 0, green = 0, blue = 0; - led_strip_hsv2rgb(term_to_int(hue), term_to_int(saturation), term_to_int(value), &red, &green, &blue); + avm_led_strip_hsv2rgb(term_to_int(hue), term_to_int(saturation), term_to_int(value), &red, &green, &blue); // Use optimized direct buffer fill with converted RGB values esp_err_t err = strip->fill(strip, red, green, blue); @@ -508,11 +508,11 @@ static term nif_fill_hsvw(Context *ctx, int argc, term argv[]) term white = argv[5]; VALIDATE_VALUE(white, term_is_integer); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); // Convert HSV to RGB uint32_t red = 0, green = 0, blue = 0; - led_strip_hsv2rgb(term_to_int(hue), term_to_int(saturation), term_to_int(value), &red, &green, &blue); + avm_led_strip_hsv2rgb(term_to_int(hue), term_to_int(saturation), term_to_int(value), &red, &green, &blue); // Use optimized direct buffer fill with converted RGB + white values esp_err_t err = strip->fill_rgbw(strip, red, green, blue, term_to_int(white)); @@ -553,7 +553,7 @@ static term nif_set_pixels_rgb(Context *ctx, int argc, term argv[]) term pixel_list = argv[2]; VALIDATE_VALUE(pixel_list, term_is_list); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); avm_int_t offset = term_to_int(offset_term); avm_int_t index = 0; @@ -617,7 +617,7 @@ static term nif_set_pixels_rgbw(Context *ctx, int argc, term argv[]) term pixel_list = argv[2]; VALIDATE_VALUE(pixel_list, term_is_list); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); avm_int_t offset = term_to_int(offset_term); avm_int_t index = 0; @@ -691,7 +691,7 @@ static term nif_tini(Context *ctx, int argc, term argv[]) VALIDATE_VALUE(channel, term_is_atom); // Note: channel argument is kept for API compatibility but ignored in ESP-IDF 5.x - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); esp_err_t err = strip->del(strip); if (err != ESP_OK) { diff --git a/nifs/include/atomvm_led_strip.h b/nifs/include/atomvm_led_strip.h new file mode 100644 index 00000000..ff1005ef --- /dev/null +++ b/nifs/include/atomvm_led_strip.h @@ -0,0 +1,88 @@ +// Copyright 2019 Espressif Systems (Shanghai) PTE LTD +// Copyright 2024 dushin.net (modifications for AtomVM) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Internal AtomVM LED strip interface +// This wraps the ESP-IDF led_strip component with our interface + +#ifndef ATOMVM_LED_STRIP_H +#define ATOMVM_LED_STRIP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "esp_err.h" +#include + +/** + * @brief AtomVM LED Strip Type (forward declaration) + */ +typedef struct avm_led_strip_s avm_led_strip_t; + +/** + * @brief AtomVM LED Strip interface structure + */ +struct avm_led_strip_s { + esp_err_t (*set_pixel)(avm_led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue); + esp_err_t (*set_pixel_rgbw)(avm_led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white); + esp_err_t (*refresh)(avm_led_strip_t *strip, uint32_t timeout_ms); + esp_err_t (*clear)(avm_led_strip_t *strip, uint32_t timeout_ms); + esp_err_t (*del)(avm_led_strip_t *strip); + esp_err_t (*set_brightness)(avm_led_strip_t *strip, uint8_t brightness); + uint8_t (*get_brightness)(avm_led_strip_t *strip); + esp_err_t (*fill)(avm_led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue); + esp_err_t (*fill_rgbw)(avm_led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue, uint32_t white); +}; + +/** + * @brief LED Strip Type (RGB vs RGBW) + */ +typedef enum { + AVM_LED_STRIP_RGB = 0, /*!< RGB LEDs (WS2812, WS2812B) - 3 bytes per pixel */ + AVM_LED_STRIP_RGBW = 1, /*!< RGBW LEDs (SK6812) - 4 bytes per pixel */ +} avm_led_strip_type_t; + +/** + * @brief AtomVM LED Strip Configuration + */ +typedef struct { + uint32_t max_leds; /*!< Maximum LEDs in a single strip */ + int gpio_num; /*!< GPIO number */ + uint8_t brightness; /*!< Global brightness (0-255), default 255 */ + avm_led_strip_type_t led_type; /*!< LED type (RGB or RGBW), default RGB */ +} avm_led_strip_config_t; + +/** + * @brief Create a new LED strip driver + * + * Uses ESP-IDF led_strip component internally with automatic backend selection: + * - ESP32-S3/C6: RMT with DMA (best performance) + * - ESP32/C3: RMT without DMA, falls back to SPI if needed + * + * @param config LED strip configuration + * @return LED strip instance or NULL on failure + */ +avm_led_strip_t *avm_led_strip_new(const avm_led_strip_config_t *config); + +/** + * @brief Convert HSV to RGB color space + */ +void avm_led_strip_hsv2rgb(uint32_t h, uint32_t s, uint32_t v, uint32_t *r, uint32_t *g, uint32_t *b); + +#ifdef __cplusplus +} +#endif + +#endif // ATOMVM_LED_STRIP_H diff --git a/nifs/include/led_strip.h b/nifs/include/led_strip.h deleted file mode 100644 index bb775cea..00000000 --- a/nifs/include/led_strip.h +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2019 Espressif Systems (Shanghai) PTE LTD -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include "esp_err.h" - -/** -* @brief LED Strip Type -* -*/ -typedef struct led_strip_s led_strip_t; - -/** -* @brief Declare of LED Strip Type -* -*/ -struct led_strip_s { - /** - * @brief Set RGB for a specific pixel - * - * @param strip: LED strip - * @param index: index of pixel to set - * @param red: red part of color - * @param green: green part of color - * @param blue: blue part of color - * - * @return - * - ESP_OK: Set RGB for a specific pixel successfully - * - ESP_ERR_INVALID_ARG: Set RGB for a specific pixel failed because of invalid parameters - * - ESP_FAIL: Set RGB for a specific pixel failed because other error occurred - */ - esp_err_t (*set_pixel)(led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue); - - /** - * @brief Set RGBW for a specific pixel (for RGBW strips like SK6812) - * - * @param strip: LED strip - * @param index: index of pixel to set - * @param red: red part of color - * @param green: green part of color - * @param blue: blue part of color - * @param white: white part of color - * - * @return - * - ESP_OK: Set RGBW for a specific pixel successfully - * - ESP_ERR_INVALID_ARG: Set RGBW failed because of invalid parameters - * - ESP_ERR_NOT_SUPPORTED: Strip is not RGBW type - * - ESP_FAIL: Set RGBW for a specific pixel failed because other error occurred - */ - esp_err_t (*set_pixel_rgbw)(led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white); - - /** - * @brief Refresh memory colors to LEDs - * - * @param strip: LED strip - * @param timeout_ms: timeout value for refreshing task - * - * @return - * - ESP_OK: Refresh successfully - * - ESP_ERR_TIMEOUT: Refresh failed because of timeout - * - ESP_FAIL: Refresh failed because some other error occurred - * - * @note: - * After updating the LED colors in the memory, a following invocation of this API is needed to flush colors to strip. - */ - esp_err_t (*refresh)(led_strip_t *strip, uint32_t timeout_ms); - - /** - * @brief Clear LED strip (turn off all LEDs) - * - * @param strip: LED strip - * @param timeout_ms: timeout value for clearing task - * - * @return - * - ESP_OK: Clear LEDs successfully - * - ESP_ERR_TIMEOUT: Clear LEDs failed because of timeout - * - ESP_FAIL: Clear LEDs failed because some other error occurred - */ - esp_err_t (*clear)(led_strip_t *strip, uint32_t timeout_ms); - - /** - * @brief Free LED strip resources - * - * @param strip: LED strip - * - * @return - * - ESP_OK: Free resources successfully - * - ESP_FAIL: Free resources failed because error occurred - */ - esp_err_t (*del)(led_strip_t *strip); - - /** - * @brief Set global brightness for the strip - * - * @param strip: LED strip - * @param brightness: brightness value (0-255) - * - * @return - * - ESP_OK: Set brightness successfully - */ - esp_err_t (*set_brightness)(led_strip_t *strip, uint8_t brightness); - - /** - * @brief Get current global brightness - * - * @param strip: LED strip - * - * @return current brightness value (0-255) - */ - uint8_t (*get_brightness)(led_strip_t *strip); - - /** - * @brief Fill entire strip with a single RGB color - * - * @param strip: LED strip - * @param red: red part of color - * @param green: green part of color - * @param blue: blue part of color - * - * @return - * - ESP_OK: Fill successfully - */ - esp_err_t (*fill)(led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue); - - /** - * @brief Fill entire strip with a single RGBW color - * - * @param strip: LED strip - * @param red: red part of color - * @param green: green part of color - * @param blue: blue part of color - * @param white: white part of color - * - * @return - * - ESP_OK: Fill successfully - * - ESP_ERR_NOT_SUPPORTED: Strip is not RGBW type - */ - esp_err_t (*fill_rgbw)(led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue, uint32_t white); -}; - -/** -* @brief LED Strip Type (RGB vs RGBW) -*/ -typedef enum { - LED_STRIP_RGB = 0, /*!< RGB LEDs (WS2812, WS2812B) - 3 bytes per pixel */ - LED_STRIP_RGBW = 1, /*!< RGBW LEDs (SK6812) - 4 bytes per pixel */ -} led_strip_type_t; - -/** -* @brief LED Strip Configuration Type -* -*/ -typedef struct { - uint32_t max_leds; /*!< Maximum LEDs in a single strip */ - int gpio_num; /*!< GPIO number */ - uint8_t brightness; /*!< Global brightness (0-255), default 255 */ - led_strip_type_t led_type; /*!< LED type (RGB or RGBW), default RGB */ -} led_strip_config_t; - -/** -* @brief Install a new ws2812 driver (based on RMT peripheral) -* -* @param config: LED strip configuration -* @return -* LED strip instance or NULL -*/ -led_strip_t *led_strip_new_rmt_ws2812(const led_strip_config_t *config); - -void led_strip_hsv2rgb(uint32_t h, uint32_t s, uint32_t v, uint32_t *r, uint32_t *g, uint32_t *b); - -#ifdef __cplusplus -} -#endif diff --git a/nifs/led_strip_driver.c b/nifs/led_strip_driver.c new file mode 100644 index 00000000..d7fc358f --- /dev/null +++ b/nifs/led_strip_driver.c @@ -0,0 +1,455 @@ +// +// Copyright (c) 2021-2024 dushin.net +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// This implementation uses the ESP-IDF led_strip component for better +// WiFi coexistence and simpler maintenance. +// + +#include +#include +#include "sdkconfig.h" +#include "esp_log.h" + +// ESP-IDF led_strip component +#include "led_strip.h" + +// Our internal interface +#include "atomvm_led_strip.h" + +static const char *TAG = "avm_led_strip"; + +// Internal wrapper structure +typedef struct { + avm_led_strip_t parent; // Our interface (function pointers) + led_strip_handle_t idf_strip; // ESP-IDF led_strip handle + uint32_t strip_len; + uint8_t brightness; + uint8_t bytes_per_pixel; + avm_led_strip_type_t led_type; + uint8_t *pixel_buf; // Raw pixel values (before brightness) +} strip_wrapper_t; + +// Forward declarations +static esp_err_t wrapper_set_pixel(avm_led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue); +static esp_err_t wrapper_set_pixel_rgbw(avm_led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white); +static esp_err_t wrapper_refresh(avm_led_strip_t *strip, uint32_t timeout_ms); +static esp_err_t wrapper_clear(avm_led_strip_t *strip, uint32_t timeout_ms); +static esp_err_t wrapper_del(avm_led_strip_t *strip); +static esp_err_t wrapper_set_brightness(avm_led_strip_t *strip, uint8_t brightness); +static uint8_t wrapper_get_brightness(avm_led_strip_t *strip); +static esp_err_t wrapper_fill(avm_led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue); +static esp_err_t wrapper_fill_rgbw(avm_led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue, uint32_t white); + +// Apply brightness to a color value +static inline uint8_t apply_brightness(uint8_t value, uint8_t brightness) +{ + if (brightness == 255) return value; + return (uint8_t)(((uint16_t)value * (brightness + 1)) >> 8); +} + +static esp_err_t wrapper_set_pixel(avm_led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue) +{ + strip_wrapper_t *wrapper = __containerof(strip, strip_wrapper_t, parent); + + if (index >= wrapper->strip_len) { + return ESP_ERR_INVALID_ARG; + } + + // Store raw values in our buffer + uint32_t offset = index * wrapper->bytes_per_pixel; + wrapper->pixel_buf[offset + 0] = red & 0xFF; + wrapper->pixel_buf[offset + 1] = green & 0xFF; + wrapper->pixel_buf[offset + 2] = blue & 0xFF; + if (wrapper->bytes_per_pixel == 4) { + wrapper->pixel_buf[offset + 3] = 0; + } + + // Apply brightness and send to ESP-IDF driver + uint8_t br = wrapper->brightness; + return led_strip_set_pixel(wrapper->idf_strip, index, + apply_brightness(red, br), + apply_brightness(green, br), + apply_brightness(blue, br)); +} + +static esp_err_t wrapper_set_pixel_rgbw(avm_led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white) +{ + strip_wrapper_t *wrapper = __containerof(strip, strip_wrapper_t, parent); + + if (wrapper->led_type != AVM_LED_STRIP_RGBW) { + return ESP_ERR_NOT_SUPPORTED; + } + + if (index >= wrapper->strip_len) { + return ESP_ERR_INVALID_ARG; + } + + // Store raw values + uint32_t offset = index * 4; + wrapper->pixel_buf[offset + 0] = red & 0xFF; + wrapper->pixel_buf[offset + 1] = green & 0xFF; + wrapper->pixel_buf[offset + 2] = blue & 0xFF; + wrapper->pixel_buf[offset + 3] = white & 0xFF; + + // Apply brightness and send to ESP-IDF driver + uint8_t br = wrapper->brightness; + return led_strip_set_pixel_rgbw(wrapper->idf_strip, index, + apply_brightness(red, br), + apply_brightness(green, br), + apply_brightness(blue, br), + apply_brightness(white, br)); +} + +static esp_err_t wrapper_refresh(avm_led_strip_t *strip, uint32_t timeout_ms) +{ + strip_wrapper_t *wrapper = __containerof(strip, strip_wrapper_t, parent); + (void)timeout_ms; // ESP-IDF driver doesn't use timeout + + return led_strip_refresh(wrapper->idf_strip); +} + +static esp_err_t wrapper_clear(avm_led_strip_t *strip, uint32_t timeout_ms) +{ + strip_wrapper_t *wrapper = __containerof(strip, strip_wrapper_t, parent); + (void)timeout_ms; + + // Clear our buffer + memset(wrapper->pixel_buf, 0, wrapper->strip_len * wrapper->bytes_per_pixel); + + return led_strip_clear(wrapper->idf_strip); +} + +static esp_err_t wrapper_del(avm_led_strip_t *strip) +{ + strip_wrapper_t *wrapper = __containerof(strip, strip_wrapper_t, parent); + + esp_err_t ret = led_strip_del(wrapper->idf_strip); + + if (wrapper->pixel_buf) { + free(wrapper->pixel_buf); + } + free(wrapper); + + return ret; +} + +static esp_err_t wrapper_set_brightness(avm_led_strip_t *strip, uint8_t brightness) +{ + strip_wrapper_t *wrapper = __containerof(strip, strip_wrapper_t, parent); + wrapper->brightness = brightness; + + // Re-apply brightness to all pixels + uint8_t br = brightness; + for (uint32_t i = 0; i < wrapper->strip_len; i++) { + uint32_t offset = i * wrapper->bytes_per_pixel; + uint8_t r = wrapper->pixel_buf[offset + 0]; + uint8_t g = wrapper->pixel_buf[offset + 1]; + uint8_t b = wrapper->pixel_buf[offset + 2]; + + if (wrapper->bytes_per_pixel == 4) { + uint8_t w = wrapper->pixel_buf[offset + 3]; + led_strip_set_pixel_rgbw(wrapper->idf_strip, i, + apply_brightness(r, br), + apply_brightness(g, br), + apply_brightness(b, br), + apply_brightness(w, br)); + } else { + led_strip_set_pixel(wrapper->idf_strip, i, + apply_brightness(r, br), + apply_brightness(g, br), + apply_brightness(b, br)); + } + } + + return ESP_OK; +} + +static uint8_t wrapper_get_brightness(avm_led_strip_t *strip) +{ + strip_wrapper_t *wrapper = __containerof(strip, strip_wrapper_t, parent); + return wrapper->brightness; +} + +static esp_err_t wrapper_fill(avm_led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue) +{ + strip_wrapper_t *wrapper = __containerof(strip, strip_wrapper_t, parent); + + uint8_t br = wrapper->brightness; + uint8_t r = apply_brightness(red, br); + uint8_t g = apply_brightness(green, br); + uint8_t b = apply_brightness(blue, br); + + // Store in our buffer and set in ESP-IDF driver + for (uint32_t i = 0; i < wrapper->strip_len; i++) { + uint32_t offset = i * wrapper->bytes_per_pixel; + wrapper->pixel_buf[offset + 0] = red & 0xFF; + wrapper->pixel_buf[offset + 1] = green & 0xFF; + wrapper->pixel_buf[offset + 2] = blue & 0xFF; + if (wrapper->bytes_per_pixel == 4) { + wrapper->pixel_buf[offset + 3] = 0; + } + + if (wrapper->bytes_per_pixel == 4) { + led_strip_set_pixel_rgbw(wrapper->idf_strip, i, r, g, b, 0); + } else { + led_strip_set_pixel(wrapper->idf_strip, i, r, g, b); + } + } + + return ESP_OK; +} + +static esp_err_t wrapper_fill_rgbw(avm_led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue, uint32_t white) +{ + strip_wrapper_t *wrapper = __containerof(strip, strip_wrapper_t, parent); + + if (wrapper->led_type != AVM_LED_STRIP_RGBW) { + return ESP_ERR_NOT_SUPPORTED; + } + + uint8_t br = wrapper->brightness; + uint8_t r = apply_brightness(red, br); + uint8_t g = apply_brightness(green, br); + uint8_t b = apply_brightness(blue, br); + uint8_t w = apply_brightness(white, br); + + for (uint32_t i = 0; i < wrapper->strip_len; i++) { + uint32_t offset = i * 4; + wrapper->pixel_buf[offset + 0] = red & 0xFF; + wrapper->pixel_buf[offset + 1] = green & 0xFF; + wrapper->pixel_buf[offset + 2] = blue & 0xFF; + wrapper->pixel_buf[offset + 3] = white & 0xFF; + + led_strip_set_pixel_rgbw(wrapper->idf_strip, i, r, g, b, w); + } + + return ESP_OK; +} + +avm_led_strip_t *avm_led_strip_new(const avm_led_strip_config_t *config) +{ + if (!config) { + ESP_LOGE(TAG, "Configuration cannot be null"); + return NULL; + } + + uint8_t bytes_per_pixel = (config->led_type == AVM_LED_STRIP_RGBW) ? 4 : 3; + + // Allocate wrapper structure + strip_wrapper_t *wrapper = calloc(1, sizeof(strip_wrapper_t)); + if (!wrapper) { + ESP_LOGE(TAG, "Failed to allocate wrapper"); + return NULL; + } + + // Allocate pixel buffer + wrapper->pixel_buf = calloc(config->max_leds, bytes_per_pixel); + if (!wrapper->pixel_buf) { + ESP_LOGE(TAG, "Failed to allocate pixel buffer"); + free(wrapper); + return NULL; + } + + // Configure ESP-IDF led_strip + led_strip_config_t strip_config = { + .strip_gpio_num = config->gpio_num, + .max_leds = config->max_leds, + .led_model = LED_MODEL_WS2812, + .color_component_format = (config->led_type == AVM_LED_STRIP_RGBW) ? LED_STRIP_COLOR_COMPONENT_FMT_GRBW : LED_STRIP_COLOR_COMPONENT_FMT_GRB, + .flags.invert_out = false, + }; + + esp_err_t ret = ESP_FAIL; + +// Helper macros for backend selection +#if defined(CONFIG_AVM_NEOPIXEL_BACKEND_SPI) + #define TRY_SPI_FIRST 1 + #define TRY_RMT_FIRST 0 +#elif defined(CONFIG_AVM_NEOPIXEL_BACKEND_RMT) + #define TRY_SPI_FIRST 0 + #define TRY_RMT_FIRST 1 +#else + // Auto mode: prefer SPI on ESP32 (original), RMT elsewhere + #if CONFIG_IDF_TARGET_ESP32 + #define TRY_SPI_FIRST 1 + #define TRY_RMT_FIRST 0 + #else + #define TRY_SPI_FIRST 0 + #define TRY_RMT_FIRST 1 + #endif +#endif + +#if TRY_SPI_FIRST + // Try SPI backend first + { + led_strip_spi_config_t spi_config = { + .spi_bus = SPI2_HOST, + .flags.with_dma = true, + }; + + ret = led_strip_new_spi_device(&strip_config, &spi_config, &wrapper->idf_strip); + + if (ret == ESP_OK) { + ESP_LOGI(TAG, "Using SPI backend with DMA"); + } else { + ESP_LOGW(TAG, "SPI backend failed (err=%d), trying RMT", ret); + } + } + + // Fallback to RMT + if (ret != ESP_OK) { + led_strip_rmt_config_t rmt_config = { + .clk_src = RMT_CLK_SRC_DEFAULT, + .resolution_hz = 10 * 1000 * 1000, +#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32P4 + .mem_block_symbols = 64, + .flags.with_dma = true, +#elif CONFIG_IDF_TARGET_ESP32 + .mem_block_symbols = 192, + .flags.with_dma = false, +#else + .mem_block_symbols = 64, + .flags.with_dma = false, +#endif + }; + ret = led_strip_new_rmt_device(&strip_config, &rmt_config, &wrapper->idf_strip); + if (ret == ESP_OK) { +#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32P4 + ESP_LOGI(TAG, "Using RMT with DMA"); +#else + ESP_LOGW(TAG, "Using RMT without DMA (may flicker with WiFi)"); +#endif + } + } +#else // TRY_RMT_FIRST + // Try RMT backend first + { + led_strip_rmt_config_t rmt_config = { + .clk_src = RMT_CLK_SRC_DEFAULT, + .resolution_hz = 10 * 1000 * 1000, +#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32P4 + .mem_block_symbols = 64, + .flags.with_dma = true, +#elif CONFIG_IDF_TARGET_ESP32 + .mem_block_symbols = 192, + .flags.with_dma = false, +#else + .mem_block_symbols = 64, + .flags.with_dma = false, +#endif + }; + ret = led_strip_new_rmt_device(&strip_config, &rmt_config, &wrapper->idf_strip); + if (ret == ESP_OK) { +#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32P4 + ESP_LOGI(TAG, "Using RMT with DMA"); +#else + ESP_LOGI(TAG, "Using RMT without DMA"); +#endif + } else { + ESP_LOGW(TAG, "RMT backend failed (err=%d), trying SPI", ret); + } + } + + // Fallback to SPI + if (ret != ESP_OK) { + led_strip_spi_config_t spi_config = { + .spi_bus = SPI2_HOST, + .flags.with_dma = true, + }; + ret = led_strip_new_spi_device(&strip_config, &spi_config, &wrapper->idf_strip); + if (ret == ESP_OK) { + ESP_LOGI(TAG, "Using SPI backend with DMA"); + } + } +#endif + +#undef TRY_SPI_FIRST +#undef TRY_RMT_FIRST + + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to create LED strip: %d", ret); + free(wrapper->pixel_buf); + free(wrapper); + return NULL; + } + + // Initialize wrapper + wrapper->strip_len = config->max_leds; + wrapper->brightness = config->brightness ? config->brightness : 255; + wrapper->bytes_per_pixel = bytes_per_pixel; + wrapper->led_type = config->led_type; + + // Set up function pointers + wrapper->parent.set_pixel = wrapper_set_pixel; + wrapper->parent.set_pixel_rgbw = wrapper_set_pixel_rgbw; + wrapper->parent.refresh = wrapper_refresh; + wrapper->parent.clear = wrapper_clear; + wrapper->parent.del = wrapper_del; + wrapper->parent.set_brightness = wrapper_set_brightness; + wrapper->parent.get_brightness = wrapper_get_brightness; + wrapper->parent.fill = wrapper_fill; + wrapper->parent.fill_rgbw = wrapper_fill_rgbw; + + ESP_LOGI(TAG, "LED strip initialized: %lu LEDs, %s", + (unsigned long)config->max_leds, + config->led_type == AVM_LED_STRIP_RGBW ? "RGBW" : "RGB"); + + return &wrapper->parent; +} + +void avm_led_strip_hsv2rgb(uint32_t h, uint32_t s, uint32_t v, uint32_t *r, uint32_t *g, uint32_t *b) +{ + h %= 360; + uint32_t rgb_max = (v * 255 + 50) / 100; + uint32_t rgb_min = rgb_max * (100 - s) / 100; + + uint32_t i = h / 60; + uint32_t diff = h % 60; + uint32_t rgb_adj = (rgb_max - rgb_min) * diff / 60; + + switch (i) { + case 0: + *r = rgb_max; + *g = rgb_min + rgb_adj; + *b = rgb_min; + break; + case 1: + *r = rgb_max - rgb_adj; + *g = rgb_max; + *b = rgb_min; + break; + case 2: + *r = rgb_min; + *g = rgb_max; + *b = rgb_min + rgb_adj; + break; + case 3: + *r = rgb_min; + *g = rgb_max - rgb_adj; + *b = rgb_max; + break; + case 4: + *r = rgb_min + rgb_adj; + *g = rgb_min; + *b = rgb_max; + break; + default: + *r = rgb_max; + *g = rgb_min; + *b = rgb_max - rgb_adj; + break; + } +} diff --git a/nifs/led_strip_rmt_ws2812.c b/nifs/led_strip_rmt_ws2812.c deleted file mode 100644 index e8c27cbc..00000000 --- a/nifs/led_strip_rmt_ws2812.c +++ /dev/null @@ -1,482 +0,0 @@ -// Copyright 2019 Espressif Systems (Shanghai) PTE LTD -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include -#include -#include -#include "esp_log.h" -#include "esp_attr.h" -#include "driver/rmt_tx.h" -#include "driver/rmt_encoder.h" -#include "led_strip.h" - -static const char *TAG = "ws2812"; - -#define STRIP_CHECK(a, str, goto_tag, ret_value, ...) \ - do \ - { \ - if (!(a)) \ - { \ - ESP_LOGE(TAG, "%s(%d): " str, __FUNCTION__, __LINE__, ##__VA_ARGS__); \ - ret = ret_value; \ - goto goto_tag; \ - } \ - } while (0) - -#define WS2812_T0H_NS (350) -#define WS2812_T0L_NS (1000) -#define WS2812_T1H_NS (1000) -#define WS2812_T1L_NS (350) -#define WS2812_RESET_US (280) - -static uint32_t ws2812_t0h_ticks = 0; -static uint32_t ws2812_t1h_ticks = 0; -static uint32_t ws2812_t0l_ticks = 0; -static uint32_t ws2812_t1l_ticks = 0; - -typedef struct { - led_strip_t parent; - rmt_channel_handle_t rmt_chan; - rmt_encoder_handle_t rmt_encoder; - uint32_t strip_len; - uint8_t brightness; - uint8_t bytes_per_pixel; // 3 for RGB, 4 for RGBW - led_strip_type_t led_type; - uint8_t *out_buf; // Brightness-scaled output buffer - uint8_t buffer[0]; // Raw RGB/RGBW values (flexible array member) -} ws2812_t; - -// LED strip encoder -typedef struct { - rmt_encoder_t base; - rmt_encoder_t *bytes_encoder; - rmt_encoder_t *copy_encoder; - int state; - rmt_symbol_word_t reset_code; -} rmt_led_strip_encoder_t; - -static size_t rmt_encode_led_strip(rmt_encoder_t *encoder, rmt_channel_handle_t channel, - const void *primary_data, size_t data_size, rmt_encode_state_t *ret_state) -{ - rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base); - rmt_encoder_handle_t bytes_encoder = led_encoder->bytes_encoder; - rmt_encoder_handle_t copy_encoder = led_encoder->copy_encoder; - rmt_encode_state_t session_state = RMT_ENCODING_RESET; - rmt_encode_state_t state = RMT_ENCODING_RESET; - size_t encoded_symbols = 0; - - switch (led_encoder->state) { - case 0: // send RGB data - encoded_symbols += bytes_encoder->encode(bytes_encoder, channel, primary_data, data_size, &session_state); - if (session_state & RMT_ENCODING_COMPLETE) { - led_encoder->state = 1; - } - if (session_state & RMT_ENCODING_MEM_FULL) { - state |= RMT_ENCODING_MEM_FULL; - goto out; - } - // fall-through - case 1: // send reset code - encoded_symbols += copy_encoder->encode(copy_encoder, channel, &led_encoder->reset_code, - sizeof(led_encoder->reset_code), &session_state); - if (session_state & RMT_ENCODING_COMPLETE) { - led_encoder->state = RMT_ENCODING_RESET; - state |= RMT_ENCODING_COMPLETE; - } - if (session_state & RMT_ENCODING_MEM_FULL) { - state |= RMT_ENCODING_MEM_FULL; - goto out; - } - } -out: - *ret_state = state; - return encoded_symbols; -} - -static esp_err_t rmt_del_led_strip_encoder(rmt_encoder_t *encoder) -{ - rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base); - rmt_del_encoder(led_encoder->bytes_encoder); - rmt_del_encoder(led_encoder->copy_encoder); - free(led_encoder); - return ESP_OK; -} - -static esp_err_t rmt_led_strip_encoder_reset(rmt_encoder_t *encoder) -{ - rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base); - rmt_encoder_reset(led_encoder->bytes_encoder); - rmt_encoder_reset(led_encoder->copy_encoder); - led_encoder->state = RMT_ENCODING_RESET; - return ESP_OK; -} - -static esp_err_t rmt_new_led_strip_encoder(rmt_encoder_handle_t *ret_encoder) -{ - esp_err_t ret = ESP_OK; - rmt_led_strip_encoder_t *led_encoder = NULL; - - led_encoder = calloc(1, sizeof(rmt_led_strip_encoder_t)); - STRIP_CHECK(led_encoder, "allocate memory for led strip encoder failed", err, ESP_ERR_NO_MEM); - - led_encoder->base.encode = rmt_encode_led_strip; - led_encoder->base.del = rmt_del_led_strip_encoder; - led_encoder->base.reset = rmt_led_strip_encoder_reset; - - rmt_bytes_encoder_config_t bytes_encoder_config = { - .bit0 = { - .level0 = 1, - .duration0 = ws2812_t0h_ticks, - .level1 = 0, - .duration1 = ws2812_t0l_ticks, - }, - .bit1 = { - .level0 = 1, - .duration0 = ws2812_t1h_ticks, - .level1 = 0, - .duration1 = ws2812_t1l_ticks, - }, - .flags.msb_first = 1 - }; - - STRIP_CHECK(rmt_new_bytes_encoder(&bytes_encoder_config, &led_encoder->bytes_encoder) == ESP_OK, - "create bytes encoder failed", err, ESP_FAIL); - - rmt_copy_encoder_config_t copy_encoder_config = {}; - STRIP_CHECK(rmt_new_copy_encoder(©_encoder_config, &led_encoder->copy_encoder) == ESP_OK, - "create copy encoder failed", err, ESP_FAIL); - - uint32_t reset_ticks = WS2812_RESET_US * 40; // 40MHz resolution - led_encoder->reset_code = (rmt_symbol_word_t) { - .level0 = 0, - .duration0 = reset_ticks, - .level1 = 0, - .duration1 = reset_ticks, - }; - - *ret_encoder = &led_encoder->base; - return ESP_OK; - -err: - if (led_encoder) { - if (led_encoder->bytes_encoder) { - rmt_del_encoder(led_encoder->bytes_encoder); - } - if (led_encoder->copy_encoder) { - rmt_del_encoder(led_encoder->copy_encoder); - } - free(led_encoder); - } - return ret; -} - -static esp_err_t ws2812_set_pixel(led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue) -{ - esp_err_t ret = ESP_OK; - ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); - STRIP_CHECK(index < ws2812->strip_len, "index out of the maximum number of leds", err, ESP_ERR_INVALID_ARG); - - // Store raw RGB values - brightness applied at refresh time - uint32_t start = index * ws2812->bytes_per_pixel; - // In the order of GRB(W) - ws2812->buffer[start + 0] = green & 0xFF; - ws2812->buffer[start + 1] = red & 0xFF; - ws2812->buffer[start + 2] = blue & 0xFF; - if (ws2812->bytes_per_pixel == 4) { - ws2812->buffer[start + 3] = 0; // White channel defaults to 0 for RGB calls - } - return ESP_OK; -err: - return ret; -} - -static esp_err_t ws2812_set_pixel_rgbw(led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white) -{ - esp_err_t ret = ESP_OK; - ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); - STRIP_CHECK(ws2812->led_type == LED_STRIP_RGBW, "set_pixel_rgbw called on non-RGBW strip", err, ESP_ERR_NOT_SUPPORTED); - STRIP_CHECK(index < ws2812->strip_len, "index out of the maximum number of leds", err, ESP_ERR_INVALID_ARG); - - // Store raw RGBW values - brightness applied at refresh time - uint32_t start = index * 4; - // In the order of GRBW - ws2812->buffer[start + 0] = green & 0xFF; - ws2812->buffer[start + 1] = red & 0xFF; - ws2812->buffer[start + 2] = blue & 0xFF; - ws2812->buffer[start + 3] = white & 0xFF; - return ESP_OK; -err: - return ret; -} - -static esp_err_t ws2812_refresh(led_strip_t *strip, uint32_t timeout_ms) -{ - esp_err_t ret = ESP_OK; - ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); - uint32_t buf_size = ws2812->strip_len * ws2812->bytes_per_pixel; - uint8_t *tx_buf; - - // Apply brightness scaling to output buffer - uint8_t br = ws2812->brightness; - if (br < 255) { - uint8_t *src = ws2812->buffer; - uint8_t *dst = ws2812->out_buf; - uint16_t scale = br + 1; - - // Process 4 bytes at a time when possible (common case for RGBW, - // and RGB strips with pixel count divisible by 4/3) - uint32_t i = 0; - uint32_t fast_end = buf_size & ~3U; // Round down to multiple of 4 - for (; i < fast_end; i += 4) { - dst[i + 0] = (src[i + 0] * scale) >> 8; - dst[i + 1] = (src[i + 1] * scale) >> 8; - dst[i + 2] = (src[i + 2] * scale) >> 8; - dst[i + 3] = (src[i + 3] * scale) >> 8; - } - // Handle remaining bytes - for (; i < buf_size; i++) { - dst[i] = (src[i] * scale) >> 8; - } - tx_buf = ws2812->out_buf; - } else { - // Full brightness - transmit raw buffer directly (no copy needed) - tx_buf = ws2812->buffer; - } - - rmt_transmit_config_t tx_config = { - .loop_count = 0, - }; - - STRIP_CHECK(rmt_transmit(ws2812->rmt_chan, ws2812->rmt_encoder, tx_buf, buf_size, &tx_config) == ESP_OK, - "transmit RMT samples failed", err, ESP_FAIL); - STRIP_CHECK(rmt_tx_wait_all_done(ws2812->rmt_chan, timeout_ms) == ESP_OK, - "wait tx done failed", err, ESP_FAIL); - return ESP_OK; -err: - return ret; -} - -static esp_err_t ws2812_clear(led_strip_t *strip, uint32_t timeout_ms) -{ - ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); - memset(ws2812->buffer, 0, ws2812->strip_len * ws2812->bytes_per_pixel); - return ws2812_refresh(strip, timeout_ms); -} - -static esp_err_t ws2812_set_brightness(led_strip_t *strip, uint8_t brightness) -{ - ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); - ws2812->brightness = brightness; - return ESP_OK; -} - -static uint8_t ws2812_get_brightness(led_strip_t *strip) -{ - ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); - return ws2812->brightness; -} - -static esp_err_t ws2812_fill(led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue) -{ - ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); - uint8_t bytes_per_pixel = ws2812->bytes_per_pixel; - uint32_t strip_len = ws2812->strip_len; - uint8_t *buf = ws2812->buffer; - - // Pre-compute the GRB(W) values once - uint8_t g = green & 0xFF; - uint8_t r = red & 0xFF; - uint8_t b = blue & 0xFF; - - if (bytes_per_pixel == 3) { - // RGB: write 3 bytes per pixel - for (uint32_t i = 0; i < strip_len; i++) { - uint32_t idx = i * 3; - buf[idx + 0] = g; - buf[idx + 1] = r; - buf[idx + 2] = b; - } - } else { - // RGBW: write 4 bytes per pixel, W=0 - for (uint32_t i = 0; i < strip_len; i++) { - uint32_t idx = i * 4; - buf[idx + 0] = g; - buf[idx + 1] = r; - buf[idx + 2] = b; - buf[idx + 3] = 0; - } - } - return ESP_OK; -} - -static esp_err_t ws2812_fill_rgbw(led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue, uint32_t white) -{ - ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); - - if (ws2812->led_type != LED_STRIP_RGBW) { - return ESP_ERR_NOT_SUPPORTED; - } - - uint32_t strip_len = ws2812->strip_len; - uint8_t *buf = ws2812->buffer; - - // Pre-compute the GRBW values once - uint8_t g = green & 0xFF; - uint8_t r = red & 0xFF; - uint8_t b = blue & 0xFF; - uint8_t w = white & 0xFF; - - for (uint32_t i = 0; i < strip_len; i++) { - uint32_t idx = i * 4; - buf[idx + 0] = g; - buf[idx + 1] = r; - buf[idx + 2] = b; - buf[idx + 3] = w; - } - return ESP_OK; -} - -static esp_err_t ws2812_del(led_strip_t *strip) -{ - ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); - - if (ws2812->rmt_encoder) { - rmt_del_encoder(ws2812->rmt_encoder); - } - if (ws2812->rmt_chan) { - rmt_disable(ws2812->rmt_chan); - rmt_del_channel(ws2812->rmt_chan); - } - if (ws2812->out_buf) { - free(ws2812->out_buf); - } - free(ws2812); - return ESP_OK; -} - -led_strip_t *led_strip_new_rmt_ws2812(const led_strip_config_t *config) -{ - led_strip_t *ret = NULL; - ws2812_t *ws2812 = NULL; - uint8_t *out_buf = NULL; - - STRIP_CHECK(config, "configuration can't be null", err, NULL); - - // Determine bytes per pixel based on LED type - uint8_t bytes_per_pixel = (config->led_type == LED_STRIP_RGBW) ? 4 : 3; - - // Allocate buffer based on LED type (3 bytes for RGB, 4 bytes for RGBW) - uint32_t buf_size = config->max_leds * bytes_per_pixel; - uint32_t ws2812_size = sizeof(ws2812_t) + buf_size; - ws2812 = calloc(1, ws2812_size); - STRIP_CHECK(ws2812, "request memory for ws2812 failed", err, NULL); - - // Allocate output buffer for brightness-scaled data - out_buf = malloc(buf_size); - STRIP_CHECK(out_buf, "request memory for output buffer failed", err, NULL); - ws2812->out_buf = out_buf; - - rmt_tx_channel_config_t tx_chan_config = { - .clk_src = RMT_CLK_SRC_DEFAULT, - .gpio_num = (gpio_num_t)config->gpio_num, - .mem_block_symbols = 64, - .resolution_hz = 40000000, // 40MHz - .trans_queue_depth = 4, - }; - STRIP_CHECK(rmt_new_tx_channel(&tx_chan_config, &ws2812->rmt_chan) == ESP_OK, - "create RMT TX channel failed", err, NULL); - - // Calculate timing ticks (40MHz resolution) - uint32_t counter_clk_hz = 40000000; - float ratio = (float)counter_clk_hz / 1e9; - ws2812_t0h_ticks = (uint32_t)(ratio * WS2812_T0H_NS); - ws2812_t0l_ticks = (uint32_t)(ratio * WS2812_T0L_NS); - ws2812_t1h_ticks = (uint32_t)(ratio * WS2812_T1H_NS); - ws2812_t1l_ticks = (uint32_t)(ratio * WS2812_T1L_NS); - - STRIP_CHECK(rmt_new_led_strip_encoder(&ws2812->rmt_encoder) == ESP_OK, - "create led strip encoder failed", err, NULL); - - STRIP_CHECK(rmt_enable(ws2812->rmt_chan) == ESP_OK, - "enable RMT TX channel failed", err, NULL); - - ws2812->strip_len = config->max_leds; - ws2812->brightness = config->brightness ? config->brightness : 255; - ws2812->bytes_per_pixel = bytes_per_pixel; - ws2812->led_type = config->led_type; - ws2812->parent.set_pixel = ws2812_set_pixel; - ws2812->parent.set_pixel_rgbw = ws2812_set_pixel_rgbw; - ws2812->parent.refresh = ws2812_refresh; - ws2812->parent.clear = ws2812_clear; - ws2812->parent.del = ws2812_del; - ws2812->parent.set_brightness = ws2812_set_brightness; - ws2812->parent.get_brightness = ws2812_get_brightness; - ws2812->parent.fill = ws2812_fill; - ws2812->parent.fill_rgbw = ws2812_fill_rgbw; - - return &ws2812->parent; -err: - if (out_buf) { - free(out_buf); - } - if (ws2812) { - free(ws2812); - } - return ret; -} - -void led_strip_hsv2rgb(uint32_t h, uint32_t s, uint32_t v, uint32_t *r, uint32_t *g, uint32_t *b) -{ - h %= 360; - // Use integer math: rgb_max = v * 255 / 100 (avoiding float 2.55f) - uint32_t rgb_max = (v * 255 + 50) / 100; // +50 for rounding - uint32_t rgb_min = rgb_max * (100 - s) / 100; - - uint32_t i = h / 60; - uint32_t diff = h % 60; - uint32_t rgb_adj = (rgb_max - rgb_min) * diff / 60; - - switch (i) { - case 0: - *r = rgb_max; - *g = rgb_min + rgb_adj; - *b = rgb_min; - break; - case 1: - *r = rgb_max - rgb_adj; - *g = rgb_max; - *b = rgb_min; - break; - case 2: - *r = rgb_min; - *g = rgb_max; - *b = rgb_min + rgb_adj; - break; - case 3: - *r = rgb_min; - *g = rgb_max - rgb_adj; - *b = rgb_max; - break; - case 4: - *r = rgb_min + rgb_adj; - *g = rgb_min; - *b = rgb_max; - break; - default: - *r = rgb_max; - *g = rgb_min; - *b = rgb_max - rgb_adj; - break; - } -}