ESPHome  2024.12.2
led_strip.cpp
Go to the documentation of this file.
1 #include "led_strip.h"
2 
3 #ifdef USE_RP2040
4 
5 #include "esphome/core/helpers.h"
6 #include "esphome/core/log.h"
7 
8 #include <hardware/clocks.h>
9 #include <hardware/dma.h>
10 #include <hardware/irq.h>
11 #include <hardware/pio.h>
12 #include <pico/stdlib.h>
13 #include <pico/sem.h>
14 
15 namespace esphome {
16 namespace rp2040_pio_led_strip {
17 
18 static const char *TAG = "rp2040_pio_led_strip";
19 
20 static uint8_t num_instance_[2] = {0, 0};
21 static std::map<Chipset, uint> chipset_offsets_ = {
23 };
24 static std::map<Chipset, bool> conf_count_ = {
25  {CHIPSET_WS2812, false}, {CHIPSET_WS2812B, false}, {CHIPSET_SK6812, false},
26  {CHIPSET_SM16703, false}, {CHIPSET_CUSTOM, false},
27 };
28 static bool dma_chan_active_[12];
29 static struct semaphore dma_write_complete_sem_[12];
30 
31 // DMA interrupt service routine
33  uint32_t channel = dma_hw->ints0;
34  for (uint dma_chan = 0; dma_chan < 12; ++dma_chan) {
35  if (RP2040PIOLEDStripLightOutput::dma_chan_active_[dma_chan] && (channel & (1u << dma_chan))) {
36  dma_hw->ints0 = (1u << dma_chan); // Clear the interrupt
37  sem_release(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[dma_chan]); // Handle the interrupt
38  }
39  }
40 }
41 
43  ESP_LOGCONFIG(TAG, "Setting up RP2040 LED Strip...");
44 
45  size_t buffer_size = this->get_buffer_size_();
46 
48  this->buf_ = allocator.allocate(buffer_size);
49  if (this->buf_ == nullptr) {
50  ESP_LOGE(TAG, "Failed to allocate buffer of size %u", buffer_size);
51  this->mark_failed();
52  return;
53  }
54 
55  this->effect_data_ = allocator.allocate(this->num_leds_);
56  if (this->effect_data_ == nullptr) {
57  ESP_LOGE(TAG, "Failed to allocate effect data of size %u", this->num_leds_);
58  this->mark_failed();
59  return;
60  }
61 
62  // Initialize the PIO program
63 
64  // Select PIO instance to use (0 or 1)
65  if (this->pio_ == nullptr) {
66  ESP_LOGE(TAG, "Failed to claim PIO instance");
67  this->mark_failed();
68  return;
69  }
70 
71  // if there are multiple strips, we can reuse the same PIO program and save space
72  // but there are only 4 state machines on each PIO so we can only have 4 strips per PIO
73  uint offset = 0;
74 
75  if (RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1] > 4) {
76  ESP_LOGE(TAG, "Too many instances of PIO program");
77  this->mark_failed();
78  return;
79  }
80  // keep track of how many instances of the PIO program are running on each PIO
81  RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1]++;
82 
83  // if there are multiple strips of the same chipset, we can reuse the same PIO program and save space
84  if (this->conf_count_[this->chipset_]) {
85  offset = RP2040PIOLEDStripLightOutput::chipset_offsets_[this->chipset_];
86  } else {
87  // Load the assembled program into the PIO and get its location in the PIO's instruction memory and save it
88  offset = pio_add_program(this->pio_, this->program_);
89  RP2040PIOLEDStripLightOutput::chipset_offsets_[this->chipset_] = offset;
90  RP2040PIOLEDStripLightOutput::conf_count_[this->chipset_] = true;
91  }
92 
93  // Configure the state machine's PIO, and start it
94  this->sm_ = pio_claim_unused_sm(this->pio_, true);
95  if (this->sm_ < 0) {
96  // in theory this code should never be reached
97  ESP_LOGE(TAG, "Failed to claim PIO state machine");
98  this->mark_failed();
99  return;
100  }
101 
102  // Initalize the DMA channel (Note: There are 12 DMA channels and 8 state machines so we won't run out)
103 
104  this->dma_chan_ = dma_claim_unused_channel(true);
105  if (this->dma_chan_ < 0) {
106  ESP_LOGE(TAG, "Failed to claim DMA channel");
107  this->mark_failed();
108  return;
109  }
110 
111  // Mark the DMA channel as active
112  RP2040PIOLEDStripLightOutput::dma_chan_active_[this->dma_chan_] = true;
113 
114  this->dma_config_ = dma_channel_get_default_config(this->dma_chan_);
115  channel_config_set_transfer_data_size(
116  &this->dma_config_,
117  DMA_SIZE_8); // 8 bit transfers (could be 32 but the pio program would need to be changed to handle junk data)
118  channel_config_set_read_increment(&this->dma_config_, true); // increment the read address
119  channel_config_set_write_increment(&this->dma_config_, false); // don't increment the write address
120  channel_config_set_dreq(&this->dma_config_,
121  pio_get_dreq(this->pio_, this->sm_, true)); // set the DREQ to the state machine's TX FIFO
122 
123  dma_channel_configure(this->dma_chan_, &this->dma_config_,
124  &this->pio_->txf[this->sm_], // write to the state machine's TX FIFO
125  this->buf_, // read from memory
126  this->is_rgbw_ ? num_leds_ * 4 : num_leds_ * 3, // number of bytes to transfer
127  false // don't start yet
128  );
129 
130  // Initialize the semaphore for this DMA channel
131  sem_init(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[this->dma_chan_], 1, 1);
132 
133  irq_set_exclusive_handler(DMA_IRQ_0, dma_write_complete_handler_); // after DMA all data, raise an interrupt
134  dma_channel_set_irq0_enabled(this->dma_chan_, true); // map DMA channel to interrupt
135  irq_set_enabled(DMA_IRQ_0, true); // enable interrupt
136 
137  this->init_(this->pio_, this->sm_, offset, this->pin_, this->max_refresh_rate_);
138 }
139 
141  ESP_LOGVV(TAG, "Writing state...");
142 
143  if (this->is_failed()) {
144  ESP_LOGW(TAG, "Light is in failed state, not writing state.");
145  return;
146  }
147 
148  if (this->buf_ == nullptr) {
149  ESP_LOGW(TAG, "Buffer is null, not writing state.");
150  return;
151  }
152 
153  // the bits are already in the correct order for the pio program so we can just copy the buffer using DMA
154  sem_acquire_blocking(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[this->dma_chan_]);
155  dma_channel_transfer_from_buffer_now(this->dma_chan_, this->buf_, this->get_buffer_size_());
156 }
157 
159  int32_t r = 0, g = 0, b = 0, w = 0;
160  switch (this->rgb_order_) {
161  case ORDER_RGB:
162  r = 0;
163  g = 1;
164  b = 2;
165  break;
166  case ORDER_RBG:
167  r = 0;
168  g = 2;
169  b = 1;
170  break;
171  case ORDER_GRB:
172  r = 1;
173  g = 0;
174  b = 2;
175  break;
176  case ORDER_GBR:
177  r = 2;
178  g = 0;
179  b = 1;
180  break;
181  case ORDER_BGR:
182  r = 2;
183  g = 1;
184  b = 0;
185  break;
186  case ORDER_BRG:
187  r = 1;
188  g = 2;
189  b = 0;
190  break;
191  }
192  uint8_t multiplier = this->is_rgbw_ ? 4 : 3;
193  return {this->buf_ + (index * multiplier) + r,
194  this->buf_ + (index * multiplier) + g,
195  this->buf_ + (index * multiplier) + b,
196  this->is_rgbw_ ? this->buf_ + (index * multiplier) + 3 : nullptr,
197  &this->effect_data_[index],
198  &this->correction_};
199 }
200 
202  ESP_LOGCONFIG(TAG, "RP2040 PIO LED Strip Light Output:");
203  ESP_LOGCONFIG(TAG, " Pin: GPIO%d", this->pin_);
204  ESP_LOGCONFIG(TAG, " Number of LEDs: %d", this->num_leds_);
205  ESP_LOGCONFIG(TAG, " RGBW: %s", YESNO(this->is_rgbw_));
206  ESP_LOGCONFIG(TAG, " RGB Order: %s", rgb_order_to_string(this->rgb_order_));
207  ESP_LOGCONFIG(TAG, " Max Refresh Rate: %f Hz", this->max_refresh_rate_);
208 }
209 
211 
212 } // namespace rp2040_pio_led_strip
213 } // namespace esphome
214 
215 #endif
This class represents the communication layer between the front-end MQTT layer and the hardware outpu...
Definition: light_state.h:63
void write_state(light::LightState *state) override
Definition: led_strip.cpp:140
bool is_failed() const
Definition: component.cpp:143
T * allocate(size_t n)
Definition: helpers.h:690
const char * rgb_order_to_string(RGBOrder order)
Definition: led_strip.h:40
light::ESPColorView get_view_internal(int32_t index) const override
Definition: led_strip.cpp:158
const float HARDWARE
For components that deal with hardware and are very important like GPIO switch.
Definition: component.cpp:18
virtual void mark_failed()
Mark this component as failed.
Definition: component.cpp:118
Implementation of SPI Controller mode.
Definition: a01nyub.cpp:7
An STL allocator that uses SPI or internal RAM.
Definition: helpers.h:675
bool state
Definition: fan.h:34