ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
i2s_clockless_led_strip.cpp
Go to the documentation of this file.
2
3#if defined(USE_ESP32) && SOC_I2S_SUPPORTS_TDM
4
5#include <algorithm>
6#include <cinttypes>
7
9
10#include <driver/i2s_tdm.h>
11#include <esp_attr.h>
12
14
15ESPHOME_LOG_TAG(TAG, "i2s_clockless_led_strip");
16
17constexpr const char *const ERROR_ALLOCATION = "Allocation error";
18constexpr const char *const ERROR_I2S = "I2S error";
19
20// These values are hardcoded for an LED strip at 800 kbps.
21//
22// Each bit of LED strip data gets expanded into 3 bits of I2S data, where a 0-bit expands into 100
23// (1/3 duty cycle) and a 1-bit expands into 110 (2/3 duty cycle. Each byte of LED strip data becomes
24// one 24-bit I2S sample to be transmitted at a rate of 100000 samples per second in I2S TDM mode.
25constexpr uint32_t I2S_SAMPLE_RATE_HZ = 100000;
26constexpr size_t I2S_BYTES_PER_SAMPLE = 3;
27
28// Determine how many I2S samples are needed to feed the output for the specified duration and vice-versa.
29constexpr size_t num_i2s_samples_from_duration_us(uint32_t duration_us) {
30 return (duration_us * I2S_SAMPLE_RATE_HZ + 999999) / 1000000;
31}
32constexpr uint32_t num_i2s_samples_to_duration_us(size_t samples) { return samples * 1000000ull / I2S_SAMPLE_RATE_HZ; }
33
34// Determine the number of I2S samples to pad with zeros after the LED data to encode the LED strip reset signal.
35// Minimum 50 us for WS2811, 80 us for SK6812, 280 us for WS2812B-V5.
39
40// Determine the number of I2S samples per DMA buffer to ensure that the interrupt-driven I2S on_sent callback
41// doesn't run too frequently. For example, if the buffers were sized to hold just 1 RGBW pixel then they would be
42// recycled approximately every 40 us (25000 Hz) which is way too fast. The buffer duration also imposes an upper
43// bound on the maximum LED strip refresh rate. Round up to a multiple of 3 for `dma_frame_num` as required by
44// the ESP-IDF programming guide when using 24-bit samples.
48
49I2SClocklessLedStrip::I2SClocklessLedStrip(uint8_t pin, uint16_t num_leds, light::ChannelColors channel_colors)
50 : color_data_bytes_(num_leds * channel_colors.bytes_per_led()),
51 channel_colors_(channel_colors),
52 num_leds_(num_leds),
53 pin_(pin) {}
54
56 ESP_LOGCONFIG(TAG,
57 "I2S Clockless LED Strip:\n"
58 " Pin: %u",
59 this->pin_);
60 char channel_colors[5];
61 ESP_LOGCONFIG(TAG,
62 " Channel colors: %s\n"
63 " Number of LEDs: %" PRIu16 "\n"
64 " DMA buffer: %" PRIuPTR " samples (%" PRIu32 " us)",
65 this->channel_colors_.to_string(channel_colors), this->num_leds_, I2S_BUFFER_SAMPLES,
67}
68
70
72 if (!this->allocate_buffers_()) {
73 this->mark_failed(LOG_STR(ERROR_ALLOCATION));
74 return;
75 }
76
77 i2s_chan_config_t chan_config = {
78 .id = I2S_NUM_AUTO,
79 .role = I2S_ROLE_MASTER,
80 .dma_desc_num = 2,
81 .dma_frame_num = I2S_BUFFER_SAMPLES,
82 .auto_clear_after_cb = false,
83 .auto_clear_before_cb = false,
84 .allow_pd = false,
85 .intr_priority = 0,
86 };
87 esp_err_t err = i2s_new_channel(&chan_config, &this->tx_handle_, NULL);
88 if (err != ESP_OK) {
89 ESP_LOGE(TAG, "Error in i2s_new_channel: %s", esp_err_to_name(err));
90 } else {
91 i2s_tdm_config_t tdm_config = {
92 .clk_cfg =
93 {
94 .sample_rate_hz = I2S_SAMPLE_RATE_HZ,
95 .clk_src = I2S_CLK_SRC_DEFAULT,
96 .mclk_multiple = I2S_MCLK_MULTIPLE_384,
97 },
98 .slot_cfg =
99 {
100 .data_bit_width = I2S_DATA_BIT_WIDTH_24BIT,
101 .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
102 .slot_mode = I2S_SLOT_MODE_MONO,
103 .slot_mask = I2S_TDM_SLOT0,
104 .ws_width = 1,
105 .big_endian = true,
106 .total_slot = I2S_TDM_AUTO_SLOT_NUM,
107 },
108 .gpio_cfg =
109 {
110 .mclk = I2S_GPIO_UNUSED,
111 .bclk = I2S_GPIO_UNUSED,
112 .ws = I2S_GPIO_UNUSED,
113 .dout = gpio_num_t(this->pin_),
114 .din = I2S_GPIO_UNUSED,
115 .invert_flags =
116 {
117 .mclk_inv = false,
118 .bclk_inv = false,
119 .ws_inv = false,
120 },
121 },
122 };
123 err = i2s_channel_init_tdm_mode(this->tx_handle_, &tdm_config);
124 if (err != ESP_OK) {
125 ESP_LOGE(TAG, "Error in i2s_channel_init_tdm_mode: %s", esp_err_to_name(err));
126 } else {
127 i2s_event_callbacks_t event_callbacks = {
128 .on_sent = i2s_on_sent_callback,
129 };
130 err = i2s_channel_register_event_callback(this->tx_handle_, &event_callbacks, this);
131 if (err != ESP_OK) {
132 ESP_LOGE(TAG, "Error in i2s_channel_register_event_callback: %s", esp_err_to_name(err));
133 } else {
134 err = i2s_channel_enable(this->tx_handle_);
135 if (err == ESP_OK)
136 return; // Success!
137 ESP_LOGE(TAG, "Error in i2s_channel_enable: %s", esp_err_to_name(err));
138 }
139 }
140 i2s_del_channel(this->tx_handle_);
141 this->tx_handle_ = {};
142 }
143 this->mark_failed(LOG_STR(ERROR_I2S));
144}
145
147 const size_t i2s_data_bytes = this->color_data_bytes_ * I2S_BYTES_PER_SAMPLE;
148
150 this->i2s_data_ = allocator.allocate(i2s_data_bytes);
151 if (this->i2s_data_ != nullptr) {
152 this->color_data_ = allocator.allocate(this->color_data_bytes_);
153 if (this->color_data_ != nullptr) {
154 this->effect_data_ = allocator.allocate(this->num_leds_);
155 if (this->effect_data_ != nullptr) {
156 memset(this->i2s_data_, 0, i2s_data_bytes);
157 memset(this->color_data_, 0, this->color_data_bytes_);
158 memset(this->effect_data_, 0, this->num_leds_);
159 return true;
160 }
161 allocator.deallocate(this->color_data_, this->color_data_bytes_);
162 this->color_data_ = nullptr;
163 }
164 allocator.deallocate(this->i2s_data_, i2s_data_bytes);
165 this->i2s_data_ = nullptr;
166 }
167 return false;
168}
169
171 auto traits = light::LightTraits();
172 if (this->channel_colors_.has_white()) {
173 traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE});
174 } else {
175 traits.set_supported_color_modes({light::ColorMode::RGB});
176 }
177 return traits;
178}
179
181 if (this->is_failed())
182 return;
183
184 if (this->i2s_data_ready_.load(std::memory_order_acquire)) {
185 // Busy sending the last frame, try again later.
186 this->schedule_show();
187 return;
188 }
189
190 const size_t color_data_bytes = this->color_data_bytes_;
191 uint8_t *color_data = this->color_data_;
192 uint8_t *i2s_data = this->i2s_data_;
193 for (size_t i = 0; i < color_data_bytes; i++) {
194 const uint8_t color_byte = *(color_data++);
195 *(i2s_data++) = 0b10010010 | ((color_byte & 0x80) >> 1) | ((color_byte & 0x40) >> 3) | ((color_byte & 0x20) >> 5);
196 *(i2s_data++) = 0b01001001 | ((color_byte & 0x10) << 1) | ((color_byte & 0x08) >> 1);
197 *(i2s_data++) = 0b00100100 | ((color_byte & 0x04) << 5) | ((color_byte & 0x02) << 3) | ((color_byte & 0x01) << 1);
198 }
199
200 this->i2s_data_sent_ = 0;
201 this->i2s_data_ready_.store(true, std::memory_order_release);
202
203 this->mark_shown_();
204}
205
206bool IRAM_ATTR HOT I2SClocklessLedStrip::i2s_on_sent_callback(i2s_chan_handle_t handle, i2s_event_data_t *event,
207 void *user_ctx) {
208 auto *const self = static_cast<I2SClocklessLedStrip *>(user_ctx);
209 auto *const dma_buf = static_cast<uint8_t *>(event->dma_buf);
210 const size_t dma_buf_size = event->size;
211
212 bool dma_buf_filled = false;
213 if (self->i2s_data_ready_.load(std::memory_order_acquire)) {
214 const size_t i2s_data_bytes = self->color_data_bytes_ * I2S_BYTES_PER_SAMPLE;
215 if (self->i2s_data_sent_ < i2s_data_bytes) {
216 const size_t i2s_data_remaining = i2s_data_bytes - self->i2s_data_sent_;
217 if (i2s_data_remaining >= dma_buf_size) {
218 memcpy(dma_buf, self->i2s_data_ + self->i2s_data_sent_, dma_buf_size);
219 } else {
220 memcpy(dma_buf, self->i2s_data_ + self->i2s_data_sent_, i2s_data_remaining);
221 memset(dma_buf + i2s_data_remaining, 0, dma_buf_size - i2s_data_remaining);
222 }
223 dma_buf_filled = true;
224 }
225 self->i2s_data_sent_ += dma_buf_size;
226 if (self->i2s_data_sent_ >= i2s_data_bytes + I2S_RESET_BYTES) {
227 self->i2s_data_ready_.store(false, std::memory_order_release);
228 }
229 }
230 // We only need to clear the buffer when the first byte is non-zero. Zero is not a value
231 // that can appear within the I2S data that gets copied into the front of the buffer so
232 // if the first byte is zero then the buffer must have already been cleared.
233 if (!dma_buf_filled && dma_buf[0] != 0) {
234 memset(dma_buf, 0, dma_buf_size);
235 }
236 return false;
237}
238
240
242 const light::ChannelColors &colors = this->channel_colors_;
243 uint8_t *led = this->color_data_ + (index * colors.bytes_per_led());
244 return {led + colors.r,
245 led + colors.g,
246 led + colors.b,
247 colors.has_white() ? led + colors.w : nullptr,
248 &this->effect_data_[index],
249 &this->correction_};
250}
251
252} // namespace esphome::i2s_clockless_led_strip
253
254#endif
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:274
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2222
void deallocate(T *p, size_t n)
Definition helpers.h:2279
T * allocate(size_t n)
Definition helpers.h:2249
light::ESPColorView get_view_internal(int32_t index) const override
I2SClocklessLedStrip(uint8_t pin, uint16_t num_leds, light::ChannelColors channel_colors)
static bool i2s_on_sent_callback(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx)
This class represents the communication layer between the front-end MQTT layer and the hardware outpu...
Definition light_state.h:68
This class is used to represent the capabilities of a light.
Definition light_traits.h:9
bool state
Definition fan.h:2
ESPHOME_LOG_TAG(TAG, "i2s_clockless_led_strip")
constexpr uint32_t num_i2s_samples_to_duration_us(size_t samples)
constexpr size_t num_i2s_samples_from_duration_us(uint32_t duration_us)
constexpr const char *const ERROR_ALLOCATION
@ RGB_WHITE
RGB color output and a separate white output.
@ RGB
RGB color output.
@ WHITE
White output only (use only if the light also has another color mode such as RGB).
constexpr float IO
For components that represent GPIO pins like PCF8573.
Definition component.h:41
static void uint32_t
Which byte of an addressable LED's data carries each colour.
const char * to_string(char *buf) const
Write the order back out as text, e.g.
spi_device_handle_t handle