ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
remote_transmitter_libretiny_isr.cpp
Go to the documentation of this file.
3#include "esphome/core/hal.h"
4#include "esphome/core/log.h"
5
6// Envelope chain shared by the LibreTiny families that pace transmission from a hardware timer
7// interrupt: RTL8720C (gtimer) and BK7238 (BKTIMER1). Everything platform-specific sits behind
8// five hooks implemented in the per-family files -- carrier setup, duty writes, one-shot arming
9// and timer stop. Families without a usable timer keep the generic bit-bang implementation and
10// compile none of this.
11#if defined(USE_LIBRETINY_VARIANT_RTL8720C) || defined(REMOTE_TRANSMITTER_BK_PWM)
12
14
15ESPHOME_LOG_TAG(TAG, "remote_transmitter");
16
17// Margin past a transmission's expected duration before the chain is declared stalled
18static constexpr uint32_t STALL_MARGIN_MS = 1000;
19// Longest single one-shot armed; longer durations are chained. Both families need the cap:
20// the Beken driver computes period_us * 26 in 32 bits (overflows past ~165s) and the Realtek
21// us->tick conversion lives in mask ROM with unverified headroom.
22static constexpr uint32_t MAX_ONE_SHOT_US = 50000;
23
24// One hardware timer is shared by all instances (MULTI_CONF), so they serialize on this
25// token; the deadline always describes whichever chain currently owns it.
26// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
27static RemoteTransmitterComponent *volatile s_active_transmitter = nullptr;
28static uint32_t s_expected_end_ms = 0;
29// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
30
31// Entry point for trampolines whose SDK callback carries no user argument
33 auto *transmitter = s_active_transmitter;
34 if (transmitter != nullptr)
35 transmitter->advance_envelope_isr();
36}
37
38// Arms the envelope timer, chaining durations longer than MAX_ONE_SHOT_US. ISR-safe.
40 // clamp to 1us (a zero-length one-shot never fires); the remainder must not underflow
41 const uint32_t chunk = std::max(uint32_t(1), std::min(duration_us, MAX_ONE_SHOT_US));
42 this->isr_wait_remaining_ = duration_us > chunk ? duration_us - chunk : 0;
43 this->arm_one_shot_(chunk);
44}
45
46// Writes the level for one envelope item and arms the timer for its duration.
47// Runs in ISR context (and once from arm_chain_ to kick the chain): no logging, no allocation.
48void IRAM_ATTR RemoteTransmitterComponent::start_isr_item_(size_t index) {
49 const int32_t item = this->isr_data_[index];
50 this->write_envelope_level_(item > 0);
51 this->arm_envelope_timer_(uint32_t(item > 0 ? item : -item));
52}
53
55 if (!this->transmitting_)
56 return; // chain was aborted; this is a stale one-shot that was already latched
57 if (this->isr_wait_remaining_ > 0) {
58 // continue a duration longer than one hardware one-shot
60 return;
61 }
62 if (this->isr_in_gap_) {
63 // inter-repeat gap elapsed; restart the item chain
64 this->isr_in_gap_ = false;
65 this->isr_index_ = 0;
66 this->start_isr_item_(0);
67 return;
68 }
69 this->isr_index_ = this->isr_index_ + 1;
70 if (this->isr_index_ < this->isr_data_.size()) {
71 this->start_isr_item_(this->isr_index_);
72 return;
73 }
74 // end of one repetition
75 this->write_envelope_level_(false);
76 if (this->isr_repeats_left_ > 1) {
77 this->isr_repeats_left_ = this->isr_repeats_left_ - 1;
78 this->isr_index_ = 0;
79 if (this->isr_send_wait_ > 0) {
80 this->isr_in_gap_ = true;
82 } else {
83 this->start_isr_item_(0);
84 }
85 return;
86 }
87 // required on Beken (its timer reloads); on Realtek this only clears the enable bit of a
88 // one-shot that has already fired
90 this->transmitting_ = false;
91 s_active_transmitter = nullptr;
92}
93
94// Aborts a chain that stopped advancing: stop the timer, idle the pin, release the token.
95// Every step is a no-op if the chain completed meanwhile. Task context only.
97 // cleared first so a straggler one-shot bails at the ISR entry check
98 this->transmitting_ = false;
100 this->write_envelope_level_(false);
101 s_active_transmitter = nullptr;
102 this->stall_aborted_ = true;
103 this->status_set_warning("envelope timer stalled");
104 ESP_LOGE(TAG, "Envelope timer stalled; transmission aborted");
105 delay(1); // let any already-latched interrupt land while the chain state is safe
106}
107
108// Delivers one deferred completion with its status bookkeeping
110 if (!this->stall_aborted_)
111 this->status_clear_warning();
112 this->complete_pending_ = false;
113 this->fire_complete_(!this->stall_aborted_);
114}
115
116// Waits until no chain is in flight, delivering any deferred completions; a completion
117// automation may start a new send, so repeat until truly idle. Bounded by the stall deadline.
119 while (true) {
120 while (true) {
121 // snapshot: the final ISR can clear the volatile pointer between a check and a use
122 auto *active = s_active_transmitter;
123 if (active == nullptr)
124 break;
125 if ((int32_t) (millis() - s_expected_end_ms) > 0) {
126 active->abort_stalled_chain_();
127 break;
128 }
129 App.feed_wdt();
130 delay(1);
131 }
132 if (!this->complete_pending_)
133 break;
134 this->deliver_completion_();
135 }
136}
137
138// Stages the repeat schedule and stall deadline, then starts the interrupt chain
140 this->isr_repeats_left_ = send_times;
141 this->isr_send_wait_ = send_wait;
142 this->isr_index_ = 0;
143 this->isr_in_gap_ = false;
144 this->stall_aborted_ = false;
145 uint64_t frame_us = 0;
146 for (int32_t item : this->isr_data_)
147 frame_us += uint32_t(item > 0 ? item : -item);
148 const uint64_t total_us = frame_us * send_times + uint64_t(send_wait) * (send_times - 1);
149 s_expected_end_ms = millis() + uint32_t(total_us / 1000) + STALL_MARGIN_MS;
150 this->transmitting_ = true;
151 s_active_transmitter = this;
152 this->start_isr_item_(0);
153}
154
156
158 if (!this->envelope_ready_()) {
159 // both triggers still fire, so an on_complete-sequenced automation does not stall
160 ESP_LOGW(TAG, "Cannot send: PWM not initialized");
162 this->fire_complete_(false);
163 return;
164 }
165 if (send_times == 0) {
166 // parity with the loop-based implementations: transmit nothing, but both triggers
167 // still fire so an on_complete-sequenced automation does not stall
169 this->fire_complete_(false);
170 return;
171 }
172 ESP_LOGD(TAG, "Sending remote code");
174 // own copy: with non_blocking the caller may re-encode temp_ while this frame is in flight
175 this->isr_data_.assign(this->temp_.get_data().begin(), this->temp_.get_data().end());
176 if (this->isr_data_.empty()) {
177 ESP_LOGW(TAG, "Empty data");
179 this->fire_complete_(false);
180 return;
181 }
182 // trigger first: the deadline computed in arm_chain_ must not be charged for user code
184 // the automation may have started a send on another instance; let it finish before
185 // claiming the shared timer (a same-instance send remains unsupported here)
186 this->wait_until_idle_();
187 this->arm_chain_(send_times, send_wait);
188 if (this->non_blocking_) {
189 this->complete_pending_ = true;
190 this->enable_loop();
191 return;
192 }
193 // blocking mode: wait out the chain, bounded by the stall deadline
194 while (this->transmitting_) {
195 if ((int32_t) (millis() - s_expected_end_ms) > 0) {
196 this->abort_stalled_chain_();
197 break;
198 }
199 App.feed_wdt();
200 delay(1);
201 }
202 this->deliver_completion_();
203}
204
206 if (!this->complete_pending_) {
207 this->disable_loop();
208 return;
209 }
210 if (this->transmitting_) {
211 // non-blocking stall recovery: without this, a dead chain would leave the carrier
212 // driven and on_complete unfired until the next send happened to abort it
213 if ((int32_t) (millis() - s_expected_end_ms) <= 0)
214 return;
215 this->abort_stalled_chain_();
216 }
217 // release the loop before user code runs: the automation may start a new non-blocking
218 // send, and its enable_loop() must be the last writer or its completion would strand
219 this->disable_loop();
220 this->deliver_completion_();
221}
222
223} // namespace esphome::remote_transmitter
224
225#endif // USE_LIBRETINY_VARIANT_RTL8720C || REMOTE_TRANSMITTER_BK_PWM
void feed_wdt()
Feed the task watchdog.
void enable_loop()
Enable this component's loop.
Definition component.h:248
void disable_loop()
Disable this component's loop.
void status_clear_warning()
Definition component.h:291
void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE
Inform the parent automation that the event has triggered.
Definition automation.h:533
const RawTimings & get_data() const
Definition remote_base.h:33
RemoteTransmitData temp_
Use same vector for all transmits, avoids many allocations.
void send_internal(uint32_t send_times, uint32_t send_wait) override
ESPHOME_LOG_TAG(TAG, "remote_transmitter")
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t