diff --git a/CODEOWNERS b/CODEOWNERS index 70f8d6864a..46a9ca90fd 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -84,6 +84,7 @@ esphome/components/esp32_ble_server/* @jesserockz esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_can/* @Sympatron esphome/components/esp32_improv/* @jesserockz +esphome/components/esp32_rmt_led_strip/* @jesserockz esphome/components/esp8266/* @esphome/core esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/exposure_notifications/* @OttoWinter diff --git a/esphome/components/esp32_rmt_led_strip/__init__.py b/esphome/components/esp32_rmt_led_strip/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp new file mode 100644 index 0000000000..eec1bdc992 --- /dev/null +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -0,0 +1,207 @@ +#include "led_strip.h" + +#ifdef USE_ESP32 + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace esp32_rmt_led_strip { + +static const char *const TAG = "esp32_rmt_led_strip"; + +static const uint8_t RMT_CLK_DIV = 2; + +void ESP32RMTLEDStripLightOutput::setup() { + ESP_LOGCONFIG(TAG, "Setting up ESP32 LED Strip..."); + + size_t buffer_size = this->get_buffer_size_(); + + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->buf_ = allocator.allocate(buffer_size); + if (this->buf_ == nullptr) { + ESP_LOGE(TAG, "Cannot allocate LED buffer!"); + this->mark_failed(); + return; + } + + this->effect_data_ = allocator.allocate(this->num_leds_); + if (this->effect_data_ == nullptr) { + ESP_LOGE(TAG, "Cannot allocate effect data!"); + this->mark_failed(); + return; + } + + ExternalRAMAllocator rmt_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8); // 8 bits per byte, 1 rmt_item32_t per bit + + rmt_config_t config; + memset(&config, 0, sizeof(config)); + config.channel = this->channel_; + config.rmt_mode = RMT_MODE_TX; + config.gpio_num = gpio_num_t(this->pin_); + config.mem_block_num = 1; + config.clk_div = RMT_CLK_DIV; + config.tx_config.loop_en = false; + config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; + config.tx_config.carrier_en = false; + config.tx_config.idle_level = RMT_IDLE_LEVEL_LOW; + config.tx_config.idle_output_en = true; + + if (rmt_config(&config) != ESP_OK) { + ESP_LOGE(TAG, "Cannot initialize RMT!"); + this->mark_failed(); + return; + } + if (rmt_driver_install(config.channel, 0, 0) != ESP_OK) { + ESP_LOGE(TAG, "Cannot install RMT driver!"); + this->mark_failed(); + return; + } +} + +void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, + uint32_t bit1_low) { + float ratio = (float) APB_CLK_FREQ / RMT_CLK_DIV / 1e09f; + + // 0-bit + this->bit0_.duration0 = (uint32_t) (ratio * bit0_high); + this->bit0_.level0 = 1; + this->bit0_.duration1 = (uint32_t) (ratio * bit0_low); + this->bit0_.level1 = 0; + // 1-bit + this->bit1_.duration0 = (uint32_t) (ratio * bit1_high); + this->bit1_.level0 = 1; + this->bit1_.duration1 = (uint32_t) (ratio * bit1_low); + this->bit1_.level1 = 0; +} + +void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { + // protect from refreshing too often + uint32_t now = micros(); + if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) { + // try again next loop iteration, so that this change won't get lost + this->schedule_show(); + return; + } + this->last_refresh_ = now; + this->mark_shown_(); + + ESP_LOGVV(TAG, "Writing RGB values to bus..."); + + if (rmt_wait_tx_done(this->channel_, pdMS_TO_TICKS(1000)) != ESP_OK) { + ESP_LOGE(TAG, "RMT TX timeout"); + this->status_set_warning(); + return; + } + delayMicroseconds(50); + + size_t buffer_size = this->get_buffer_size_(); + + size_t size = 0; + size_t len = 0; + uint8_t *psrc = this->buf_; + rmt_item32_t *pdest = this->rmt_buf_; + while (size < buffer_size) { + uint8_t b = *psrc; + for (int i = 0; i < 8; i++) { + pdest->val = b & (1 << (7 - i)) ? this->bit1_.val : this->bit0_.val; + pdest++; + len++; + } + size++; + psrc++; + } + + if (rmt_write_items(this->channel_, this->rmt_buf_, len, false) != ESP_OK) { + ESP_LOGE(TAG, "RMT TX error"); + this->status_set_warning(); + return; + } + this->status_clear_warning(); +} + +light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index) const { + int32_t r = 0, g = 0, b = 0; + switch (this->rgb_order_) { + case ORDER_RGB: + r = 0; + g = 1; + b = 2; + break; + case ORDER_RBG: + r = 0; + g = 2; + b = 1; + break; + case ORDER_GRB: + r = 1; + g = 0; + b = 2; + break; + case ORDER_GBR: + r = 2; + g = 0; + b = 1; + break; + case ORDER_BGR: + r = 2; + g = 1; + b = 0; + break; + case ORDER_BRG: + r = 1; + g = 2; + b = 0; + break; + } + uint8_t multiplier = this->is_rgbw_ ? 4 : 3; + return {this->buf_ + (index * multiplier) + r, + this->buf_ + (index * multiplier) + g, + this->buf_ + (index * multiplier) + b, + this->is_rgbw_ ? this->buf_ + (index * multiplier) + 3 : nullptr, + &this->effect_data_[index], + &this->correction_}; +} + +void ESP32RMTLEDStripLightOutput::dump_config() { + ESP_LOGCONFIG(TAG, "ESP32 RMT LED Strip:"); + ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); + ESP_LOGCONFIG(TAG, " Channel: %u", this->channel_); + const char *rgb_order; + switch (this->rgb_order_) { + case ORDER_RGB: + rgb_order = "RGB"; + break; + case ORDER_RBG: + rgb_order = "RBG"; + break; + case ORDER_GRB: + rgb_order = "GRB"; + break; + case ORDER_GBR: + rgb_order = "GBR"; + break; + case ORDER_BGR: + rgb_order = "BGR"; + break; + case ORDER_BRG: + rgb_order = "BRG"; + break; + default: + rgb_order = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " RGB Order: %s", rgb_order); + ESP_LOGCONFIG(TAG, " Max refresh rate: %u", *this->max_refresh_rate_); + ESP_LOGCONFIG(TAG, " Number of LEDs: %u", this->num_leds_); +} + +float ESP32RMTLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; } + +} // namespace esp32_rmt_led_strip +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.h b/esphome/components/esp32_rmt_led_strip/led_strip.h new file mode 100644 index 0000000000..508f784ec8 --- /dev/null +++ b/esphome/components/esp32_rmt_led_strip/led_strip.h @@ -0,0 +1,87 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/components/light/addressable_light.h" +#include "esphome/components/light/light_output.h" +#include "esphome/core/color.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include +#include +#include + +namespace esphome { +namespace esp32_rmt_led_strip { + +enum RGBOrder : uint8_t { + ORDER_RGB, + ORDER_RBG, + ORDER_GRB, + ORDER_GBR, + ORDER_BGR, + ORDER_BRG, +}; + +class ESP32RMTLEDStripLightOutput : public light::AddressableLight { + public: + void setup() override; + void write_state(light::LightState *state) override; + float get_setup_priority() const override; + + int32_t size() const override { return this->num_leds_; } + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + if (this->is_rgbw_) { + traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::RGB_WHITE}); + } else { + traits.set_supported_color_modes({light::ColorMode::RGB}); + } + return traits; + } + + void set_pin(uint8_t pin) { this->pin_ = pin; } + void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; } + void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; } + + /// Set a maximum refresh rate in µs as some lights do not like being updated too often. + void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; } + + void set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low); + + void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; } + void set_rmt_channel(rmt_channel_t channel) { this->channel_ = channel; } + + void clear_effect_data() override { + for (int i = 0; i < this->size(); i++) + this->effect_data_[i] = 0; + } + + void dump_config() override; + + protected: + light::ESPColorView get_view_internal(int32_t index) const override; + + size_t get_buffer_size_() const { return this->num_leds_ * (3 + this->is_rgbw_); } + + uint8_t *buf_{nullptr}; + uint8_t *effect_data_{nullptr}; + rmt_item32_t *rmt_buf_{nullptr}; + + uint8_t pin_; + uint16_t num_leds_; + bool is_rgbw_; + + rmt_item32_t bit0_, bit1_; + RGBOrder rgb_order_; + rmt_channel_t channel_; + + uint32_t last_refresh_{0}; + optional max_refresh_rate_{}; +}; + +} // namespace esp32_rmt_led_strip +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py new file mode 100644 index 0000000000..3ca758c1e1 --- /dev/null +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -0,0 +1,151 @@ +from dataclasses import dataclass + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import esp32, light +from esphome.const import ( + CONF_CHIPSET, + CONF_MAX_REFRESH_RATE, + CONF_NUM_LEDS, + CONF_OUTPUT_ID, + CONF_PIN, + CONF_RGB_ORDER, +) + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["esp32"] + +esp32_rmt_led_strip_ns = cg.esphome_ns.namespace("esp32_rmt_led_strip") +ESP32RMTLEDStripLightOutput = esp32_rmt_led_strip_ns.class_( + "ESP32RMTLEDStripLightOutput", light.AddressableLight +) + +rmt_channel_t = cg.global_ns.enum("rmt_channel_t") + +RGBOrder = esp32_rmt_led_strip_ns.enum("RGBOrder") + +RGB_ORDERS = { + "RGB": RGBOrder.ORDER_RGB, + "RBG": RGBOrder.ORDER_RBG, + "GRB": RGBOrder.ORDER_GRB, + "GBR": RGBOrder.ORDER_GBR, + "BGR": RGBOrder.ORDER_BGR, + "BRG": RGBOrder.ORDER_BRG, +} + + +@dataclass +class LEDStripTimings: + bit0_high: int + bit0_low: int + bit1_high: int + bit1_low: int + + +CHIPSETS = { + "WS2812": LEDStripTimings(400, 1000, 1000, 400), + "SK6812": LEDStripTimings(300, 900, 600, 600), + "APA106": LEDStripTimings(350, 1360, 1360, 350), + "SM16703": LEDStripTimings(300, 900, 1360, 350), +} + + +CONF_IS_RGBW = "is_rgbw" +CONF_BIT0_HIGH = "bit0_high" +CONF_BIT0_LOW = "bit0_low" +CONF_BIT1_HIGH = "bit1_high" +CONF_BIT1_LOW = "bit1_low" +CONF_RMT_CHANNEL = "rmt_channel" + +RMT_CHANNELS = { + esp32.const.VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7], + esp32.const.VARIANT_ESP32S2: [0, 1, 2, 3], + esp32.const.VARIANT_ESP32S3: [0, 1, 2, 3], + esp32.const.VARIANT_ESP32C3: [0, 1], +} + + +def _validate_rmt_channel(value): + variant = esp32.get_esp32_variant() + if variant not in RMT_CHANNELS: + raise cv.Invalid(f"ESP32 variant {variant} does not support RMT.") + if value not in RMT_CHANNELS[variant]: + raise cv.Invalid( + f"RMT channel {value} is not supported for ESP32 variant {variant}." + ) + return value + + +CONFIG_SCHEMA = cv.All( + light.ADDRESSABLE_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput), + cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, + cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), + cv.Required(CONF_RMT_CHANNEL): _validate_rmt_channel, + cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, + cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), + cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, + cv.Inclusive( + CONF_BIT0_HIGH, + "custom", + ): cv.positive_time_period_microseconds, + cv.Inclusive( + CONF_BIT0_LOW, + "custom", + ): cv.positive_time_period_microseconds, + cv.Inclusive( + CONF_BIT1_HIGH, + "custom", + ): cv.positive_time_period_microseconds, + cv.Inclusive( + CONF_BIT1_LOW, + "custom", + ): cv.positive_time_period_microseconds, + } + ), + cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await light.register_light(var, config) + await cg.register_component(var, config) + + cg.add(var.set_num_leds(config[CONF_NUM_LEDS])) + cg.add(var.set_pin(config[CONF_PIN])) + + if CONF_MAX_REFRESH_RATE in config: + cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE])) + + if CONF_CHIPSET in config: + chipset = CHIPSETS[config[CONF_CHIPSET]] + cg.add( + var.set_led_params( + chipset.bit0_high, + chipset.bit0_low, + chipset.bit1_high, + chipset.bit1_low, + ) + ) + else: + cg.add( + var.set_led_params( + config[CONF_BIT0_HIGH], + config[CONF_BIT0_LOW], + config[CONF_BIT1_HIGH], + config[CONF_BIT1_LOW], + ) + ) + + cg.add(var.set_rgb_order(config[CONF_RGB_ORDER])) + cg.add(var.set_is_rgbw(config[CONF_IS_RGBW])) + + cg.add( + var.set_rmt_channel( + getattr(rmt_channel_t, f"RMT_CHANNEL_{config[CONF_RMT_CHANNEL]}") + ) + ) diff --git a/tests/test5.yaml b/tests/test5.yaml index 856889c1f3..6b64ef2d15 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -644,3 +644,22 @@ key_collector: source_id: keypad min_length: 4 max_length: 4 + +light: + - platform: esp32_rmt_led_strip + id: led_strip + pin: 13 + num_leds: 60 + rmt_channel: 6 + rgb_order: GRB + chipset: ws2812 + - platform: esp32_rmt_led_strip + id: led_strip2 + pin: 15 + num_leds: 60 + rmt_channel: 2 + rgb_order: RGB + bit0_high: 100us + bit0_low: 100us + bit1_high: 100us + bit1_low: 100us