mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[i2s_clockless_led_strip] Drives LED strips using I2S and DMA (#20255)
Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
J. Nick Koston
J. Nick Koston
parent
584d4878eb
commit
83e5e32e2b
@@ -276,6 +276,7 @@ esphome/components/i2c_device/* @gabest11
|
||||
esphome/components/i2s_audio/* @jesserockz
|
||||
esphome/components/i2s_audio/microphone/* @jesserockz
|
||||
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
|
||||
esphome/components/i2s_clockless_led_strip/* @j9brown
|
||||
esphome/components/iaqcore/* @yozik04
|
||||
esphome/components/icnt86/* @danepowell
|
||||
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "i2s_clockless_led_strip"
|
||||
@@ -0,0 +1,254 @@
|
||||
#include "i2s_clockless_led_strip.h"
|
||||
|
||||
#if defined(USE_ESP32) && SOC_I2S_SUPPORTS_TDM
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <driver/i2s_tdm.h>
|
||||
#include <esp_attr.h>
|
||||
|
||||
namespace esphome::i2s_clockless_led_strip {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "i2s_clockless_led_strip");
|
||||
|
||||
constexpr const char *const ERROR_ALLOCATION = "Allocation error";
|
||||
constexpr const char *const ERROR_I2S = "I2S error";
|
||||
|
||||
// These values are hardcoded for an LED strip at 800 kbps.
|
||||
//
|
||||
// Each bit of LED strip data gets expanded into 3 bits of I2S data, where a 0-bit expands into 100
|
||||
// (1/3 duty cycle) and a 1-bit expands into 110 (2/3 duty cycle. Each byte of LED strip data becomes
|
||||
// one 24-bit I2S sample to be transmitted at a rate of 100000 samples per second in I2S TDM mode.
|
||||
constexpr uint32_t I2S_SAMPLE_RATE_HZ = 100000;
|
||||
constexpr size_t I2S_BYTES_PER_SAMPLE = 3;
|
||||
|
||||
// Determine how many I2S samples are needed to feed the output for the specified duration and vice-versa.
|
||||
constexpr size_t num_i2s_samples_from_duration_us(uint32_t duration_us) {
|
||||
return (duration_us * I2S_SAMPLE_RATE_HZ + 999999) / 1000000;
|
||||
}
|
||||
constexpr uint32_t num_i2s_samples_to_duration_us(size_t samples) { return samples * 1000000ull / I2S_SAMPLE_RATE_HZ; }
|
||||
|
||||
// Determine the number of I2S samples to pad with zeros after the LED data to encode the LED strip reset signal.
|
||||
// Minimum 50 us for WS2811, 80 us for SK6812, 280 us for WS2812B-V5.
|
||||
constexpr uint32_t I2S_RESET_DURATION_US = 300;
|
||||
constexpr size_t I2S_RESET_SAMPLES = num_i2s_samples_from_duration_us(I2S_RESET_DURATION_US);
|
||||
constexpr size_t I2S_RESET_BYTES = I2S_RESET_SAMPLES * I2S_BYTES_PER_SAMPLE;
|
||||
|
||||
// Determine the number of I2S samples per DMA buffer to ensure that the interrupt-driven I2S on_sent callback
|
||||
// doesn't run too frequently. For example, if the buffers were sized to hold just 1 RGBW pixel then they would be
|
||||
// recycled approximately every 40 us (25000 Hz) which is way too fast. The buffer duration also imposes an upper
|
||||
// bound on the maximum LED strip refresh rate. Round up to a multiple of 3 for `dma_frame_num` as required by
|
||||
// the ESP-IDF programming guide when using 24-bit samples.
|
||||
constexpr uint32_t I2S_BUFFER_DURATION_US_IDEAL = 2500;
|
||||
constexpr size_t I2S_BUFFER_SAMPLES = (num_i2s_samples_from_duration_us(I2S_BUFFER_DURATION_US_IDEAL) + 2) / 3 * 3;
|
||||
constexpr uint32_t I2S_BUFFER_DURATION_US_ACTUAL = num_i2s_samples_to_duration_us(I2S_BUFFER_SAMPLES);
|
||||
|
||||
I2SClocklessLedStrip::I2SClocklessLedStrip(uint8_t pin, uint16_t num_leds, light::ChannelColors channel_colors)
|
||||
: color_data_bytes_(num_leds * channel_colors.bytes_per_led()),
|
||||
channel_colors_(channel_colors),
|
||||
num_leds_(num_leds),
|
||||
pin_(pin) {}
|
||||
|
||||
void I2SClocklessLedStrip::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"I2S Clockless LED Strip:\n"
|
||||
" Pin: %u",
|
||||
this->pin_);
|
||||
char channel_colors[5];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Channel colors: %s\n"
|
||||
" Number of LEDs: %" PRIu16 "\n"
|
||||
" DMA buffer: %" PRIuPTR " samples (%" PRIu32 " us)",
|
||||
this->channel_colors_.to_string(channel_colors), this->num_leds_, I2S_BUFFER_SAMPLES,
|
||||
I2S_BUFFER_DURATION_US_ACTUAL);
|
||||
}
|
||||
|
||||
float I2SClocklessLedStrip::get_setup_priority() const { return setup_priority::IO; }
|
||||
|
||||
void I2SClocklessLedStrip::setup() {
|
||||
if (!this->allocate_buffers_()) {
|
||||
this->mark_failed(LOG_STR(ERROR_ALLOCATION));
|
||||
return;
|
||||
}
|
||||
|
||||
i2s_chan_config_t chan_config = {
|
||||
.id = I2S_NUM_AUTO,
|
||||
.role = I2S_ROLE_MASTER,
|
||||
.dma_desc_num = 2,
|
||||
.dma_frame_num = I2S_BUFFER_SAMPLES,
|
||||
.auto_clear_after_cb = false,
|
||||
.auto_clear_before_cb = false,
|
||||
.allow_pd = false,
|
||||
.intr_priority = 0,
|
||||
};
|
||||
esp_err_t err = i2s_new_channel(&chan_config, &this->tx_handle_, NULL);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Error in i2s_new_channel: %s", esp_err_to_name(err));
|
||||
} else {
|
||||
i2s_tdm_config_t tdm_config = {
|
||||
.clk_cfg =
|
||||
{
|
||||
.sample_rate_hz = I2S_SAMPLE_RATE_HZ,
|
||||
.clk_src = I2S_CLK_SRC_DEFAULT,
|
||||
.mclk_multiple = I2S_MCLK_MULTIPLE_384,
|
||||
},
|
||||
.slot_cfg =
|
||||
{
|
||||
.data_bit_width = I2S_DATA_BIT_WIDTH_24BIT,
|
||||
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
|
||||
.slot_mode = I2S_SLOT_MODE_MONO,
|
||||
.slot_mask = I2S_TDM_SLOT0,
|
||||
.ws_width = 1,
|
||||
.big_endian = true,
|
||||
.total_slot = I2S_TDM_AUTO_SLOT_NUM,
|
||||
},
|
||||
.gpio_cfg =
|
||||
{
|
||||
.mclk = I2S_GPIO_UNUSED,
|
||||
.bclk = I2S_GPIO_UNUSED,
|
||||
.ws = I2S_GPIO_UNUSED,
|
||||
.dout = gpio_num_t(this->pin_),
|
||||
.din = I2S_GPIO_UNUSED,
|
||||
.invert_flags =
|
||||
{
|
||||
.mclk_inv = false,
|
||||
.bclk_inv = false,
|
||||
.ws_inv = false,
|
||||
},
|
||||
},
|
||||
};
|
||||
err = i2s_channel_init_tdm_mode(this->tx_handle_, &tdm_config);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Error in i2s_channel_init_tdm_mode: %s", esp_err_to_name(err));
|
||||
} else {
|
||||
i2s_event_callbacks_t event_callbacks = {
|
||||
.on_sent = i2s_on_sent_callback,
|
||||
};
|
||||
err = i2s_channel_register_event_callback(this->tx_handle_, &event_callbacks, this);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Error in i2s_channel_register_event_callback: %s", esp_err_to_name(err));
|
||||
} else {
|
||||
err = i2s_channel_enable(this->tx_handle_);
|
||||
if (err == ESP_OK)
|
||||
return; // Success!
|
||||
ESP_LOGE(TAG, "Error in i2s_channel_enable: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
i2s_del_channel(this->tx_handle_);
|
||||
this->tx_handle_ = {};
|
||||
}
|
||||
this->mark_failed(LOG_STR(ERROR_I2S));
|
||||
}
|
||||
|
||||
bool I2SClocklessLedStrip::allocate_buffers_() {
|
||||
const size_t i2s_data_bytes = this->color_data_bytes_ * I2S_BYTES_PER_SAMPLE;
|
||||
|
||||
RAMAllocator<uint8_t> allocator(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
|
||||
this->i2s_data_ = allocator.allocate(i2s_data_bytes);
|
||||
if (this->i2s_data_ != nullptr) {
|
||||
this->color_data_ = allocator.allocate(this->color_data_bytes_);
|
||||
if (this->color_data_ != nullptr) {
|
||||
this->effect_data_ = allocator.allocate(this->num_leds_);
|
||||
if (this->effect_data_ != nullptr) {
|
||||
memset(this->i2s_data_, 0, i2s_data_bytes);
|
||||
memset(this->color_data_, 0, this->color_data_bytes_);
|
||||
memset(this->effect_data_, 0, this->num_leds_);
|
||||
return true;
|
||||
}
|
||||
allocator.deallocate(this->color_data_, this->color_data_bytes_);
|
||||
this->color_data_ = nullptr;
|
||||
}
|
||||
allocator.deallocate(this->i2s_data_, i2s_data_bytes);
|
||||
this->i2s_data_ = nullptr;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
light::LightTraits I2SClocklessLedStrip::get_traits() {
|
||||
auto traits = light::LightTraits();
|
||||
if (this->channel_colors_.has_white()) {
|
||||
traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE});
|
||||
} else {
|
||||
traits.set_supported_color_modes({light::ColorMode::RGB});
|
||||
}
|
||||
return traits;
|
||||
}
|
||||
|
||||
void I2SClocklessLedStrip::write_state(light::LightState *state) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
|
||||
if (this->i2s_data_ready_.load(std::memory_order_acquire)) {
|
||||
// Busy sending the last frame, try again later.
|
||||
this->schedule_show();
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t color_data_bytes = this->color_data_bytes_;
|
||||
uint8_t *color_data = this->color_data_;
|
||||
uint8_t *i2s_data = this->i2s_data_;
|
||||
for (size_t i = 0; i < color_data_bytes; i++) {
|
||||
const uint8_t color_byte = *(color_data++);
|
||||
*(i2s_data++) = 0b10010010 | ((color_byte & 0x80) >> 1) | ((color_byte & 0x40) >> 3) | ((color_byte & 0x20) >> 5);
|
||||
*(i2s_data++) = 0b01001001 | ((color_byte & 0x10) << 1) | ((color_byte & 0x08) >> 1);
|
||||
*(i2s_data++) = 0b00100100 | ((color_byte & 0x04) << 5) | ((color_byte & 0x02) << 3) | ((color_byte & 0x01) << 1);
|
||||
}
|
||||
|
||||
this->i2s_data_sent_ = 0;
|
||||
this->i2s_data_ready_.store(true, std::memory_order_release);
|
||||
|
||||
this->mark_shown_();
|
||||
}
|
||||
|
||||
bool IRAM_ATTR HOT I2SClocklessLedStrip::i2s_on_sent_callback(i2s_chan_handle_t handle, i2s_event_data_t *event,
|
||||
void *user_ctx) {
|
||||
auto *const self = static_cast<I2SClocklessLedStrip *>(user_ctx);
|
||||
auto *const dma_buf = static_cast<uint8_t *>(event->dma_buf);
|
||||
const size_t dma_buf_size = event->size;
|
||||
|
||||
bool dma_buf_filled = false;
|
||||
if (self->i2s_data_ready_.load(std::memory_order_acquire)) {
|
||||
const size_t i2s_data_bytes = self->color_data_bytes_ * I2S_BYTES_PER_SAMPLE;
|
||||
if (self->i2s_data_sent_ < i2s_data_bytes) {
|
||||
const size_t i2s_data_remaining = i2s_data_bytes - self->i2s_data_sent_;
|
||||
if (i2s_data_remaining >= dma_buf_size) {
|
||||
memcpy(dma_buf, self->i2s_data_ + self->i2s_data_sent_, dma_buf_size);
|
||||
} else {
|
||||
memcpy(dma_buf, self->i2s_data_ + self->i2s_data_sent_, i2s_data_remaining);
|
||||
memset(dma_buf + i2s_data_remaining, 0, dma_buf_size - i2s_data_remaining);
|
||||
}
|
||||
dma_buf_filled = true;
|
||||
}
|
||||
self->i2s_data_sent_ += dma_buf_size;
|
||||
if (self->i2s_data_sent_ >= i2s_data_bytes + I2S_RESET_BYTES) {
|
||||
self->i2s_data_ready_.store(false, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
// We only need to clear the buffer when the first byte is non-zero. Zero is not a value
|
||||
// that can appear within the I2S data that gets copied into the front of the buffer so
|
||||
// if the first byte is zero then the buffer must have already been cleared.
|
||||
if (!dma_buf_filled && dma_buf[0] != 0) {
|
||||
memset(dma_buf, 0, dma_buf_size);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void I2SClocklessLedStrip::clear_effect_data() { memset(this->effect_data_, 0, this->num_leds_); }
|
||||
|
||||
light::ESPColorView I2SClocklessLedStrip::get_view_internal(int32_t index) const {
|
||||
const light::ChannelColors &colors = this->channel_colors_;
|
||||
uint8_t *led = this->color_data_ + (index * colors.bytes_per_led());
|
||||
return {led + colors.r,
|
||||
led + colors.g,
|
||||
led + colors.b,
|
||||
colors.has_white() ? led + colors.w : nullptr,
|
||||
&this->effect_data_[index],
|
||||
&this->correction_};
|
||||
}
|
||||
|
||||
} // namespace esphome::i2s_clockless_led_strip
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include <soc/soc_caps.h>
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) && SOC_I2S_SUPPORTS_TDM
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/light/addressable_light.h"
|
||||
#include "esphome/components/light/channel_colors.h"
|
||||
|
||||
#include <driver/i2s_types.h>
|
||||
|
||||
namespace esphome::i2s_clockless_led_strip {
|
||||
|
||||
class I2SClocklessLedStrip final : public light::AddressableLight {
|
||||
public:
|
||||
I2SClocklessLedStrip(uint8_t pin, uint16_t num_leds, light::ChannelColors channel_colors);
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
void setup() override;
|
||||
|
||||
light::LightTraits get_traits() override;
|
||||
|
||||
int32_t size() const override { return this->num_leds_; }
|
||||
void clear_effect_data() override;
|
||||
void write_state(light::LightState *state) override;
|
||||
|
||||
protected:
|
||||
static bool i2s_on_sent_callback(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx);
|
||||
|
||||
bool allocate_buffers_();
|
||||
light::ESPColorView get_view_internal(int32_t index) const override;
|
||||
|
||||
const size_t color_data_bytes_;
|
||||
uint8_t *effect_data_{nullptr};
|
||||
uint8_t *color_data_{nullptr};
|
||||
uint8_t *i2s_data_{nullptr};
|
||||
size_t i2s_data_sent_{0};
|
||||
i2s_chan_handle_t tx_handle_{};
|
||||
const light::ChannelColors channel_colors_;
|
||||
const uint16_t num_leds_;
|
||||
const uint8_t pin_;
|
||||
std::atomic<bool> i2s_data_ready_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::i2s_clockless_led_strip
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,86 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, light
|
||||
from esphome.components.const import CONF_CHANNEL_COLORS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_NUM_LEDS, CONF_OUTPUT_ID, CONF_PIN
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
CODEOWNERS = ["@j9brown"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
|
||||
i2s_clockless_led_strip_ns = cg.esphome_ns.namespace("i2s_clockless_led_strip")
|
||||
I2SClocklessLedStrip = i2s_clockless_led_strip_ns.class_(
|
||||
"I2SClocklessLedStrip", light.AddressableLight
|
||||
)
|
||||
|
||||
# These variants define SOC_I2S_SUPPORTS_TDM
|
||||
I2S_TDM_PORTS = {
|
||||
esp32.VARIANT_ESP32C3: 1,
|
||||
esp32.VARIANT_ESP32C5: 1,
|
||||
esp32.VARIANT_ESP32C6: 1,
|
||||
esp32.VARIANT_ESP32C61: 1,
|
||||
esp32.VARIANT_ESP32H2: 1,
|
||||
esp32.VARIANT_ESP32H4: 1,
|
||||
esp32.VARIANT_ESP32P4: 3,
|
||||
esp32.VARIANT_ESP32S3: 2,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
esp32.only_on_variant(
|
||||
supported=list(I2S_TDM_PORTS.keys()),
|
||||
msg_prefix="I2S Clockless LED Strip",
|
||||
),
|
||||
light.ADDRESSABLE_LIGHT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(I2SClocklessLedStrip),
|
||||
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
|
||||
cv.Required(CONF_CHANNEL_COLORS): light.validate_channel_colors,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class I2SClocklessLedStripData:
|
||||
num_ports: int = 0
|
||||
|
||||
|
||||
def _get_data() -> I2SClocklessLedStripData:
|
||||
if DOMAIN not in CORE.data:
|
||||
CORE.data[DOMAIN] = I2SClocklessLedStripData()
|
||||
return CORE.data[DOMAIN]
|
||||
|
||||
|
||||
def _final_validate(_: ConfigType) -> None:
|
||||
_get_data().num_ports += 1
|
||||
variant = esp32.get_esp32_variant()
|
||||
num_ports_available = I2S_TDM_PORTS.get(variant, 0)
|
||||
if _get_data().num_ports > num_ports_available:
|
||||
raise cv.Invalid(
|
||||
f"Only {num_ports_available} I2S TDM ports are supported on {variant}"
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
esp32.include_builtin_idf_component("esp_driver_i2s")
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True)
|
||||
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_OUTPUT_ID],
|
||||
config[CONF_PIN],
|
||||
config[CONF_NUM_LEDS],
|
||||
light.channel_colors_struct(config[CONF_CHANNEL_COLORS]),
|
||||
)
|
||||
|
||||
await light.register_light(var, config)
|
||||
await cg.register_component(var, config)
|
||||
@@ -0,0 +1,11 @@
|
||||
light:
|
||||
- platform: i2s_clockless_led_strip
|
||||
id: i2s_clockless_led_strip1
|
||||
pin: GPIO1
|
||||
num_leds: 100
|
||||
channel_colors: GRB
|
||||
- platform: i2s_clockless_led_strip
|
||||
id: i2s_clockless_led_strip2
|
||||
pin: GPIO2
|
||||
num_leds: 5
|
||||
channel_colors: RWGB
|
||||
Reference in New Issue
Block a user