ESPHome 2026.9.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
7// timer interrupt: RTL8720C (gtimer) and the BK7231N-style PWM block (BKTIMER1). Everything
8// platform-specific sits behind five hooks implemented in the per-family files -- carrier
9// setup, duty writes, one-shot arming and timer stop. Families without a usable timer keep
10// the generic bit-bang implementation and compile none of this.
11#if defined(USE_LIBRETINY_VARIANT_RTL8720C) || defined(REMOTE_TRANSMITTER_BK_PWM)
12
14
15static const char *const 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
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 if (!this->envelope_ready_()) {
157 // both triggers still fire, so an on_complete-sequenced automation does not stall
158 ESP_LOGW(TAG, "Cannot send: PWM not initialized");
160 this->deliver_completion_();
161 return;
162 }
163 this->wait_until_idle_();
164 if (send_times == 0) {
165 // parity with the loop-based implementations: transmit nothing, but both triggers
166 // still fire so an on_complete-sequenced automation does not stall
168 this->deliver_completion_();
169 return;
170 }
171 ESP_LOGD(TAG, "Sending remote code");
173 // own copy: with non_blocking the caller may re-encode temp_ while this frame is in flight
174 this->isr_data_.assign(this->temp_.get_data().begin(), this->temp_.get_data().end());
175 if (this->isr_data_.empty()) {
176 ESP_LOGW(TAG, "Empty data");
178 this->deliver_completion_();
179 return;
180 }
181 // trigger first: the deadline computed in arm_chain_ must not be charged for user code
183 // the automation may have started a send on another instance; let it finish before
184 // claiming the shared timer (a same-instance send remains unsupported here)
185 this->wait_until_idle_();
186 this->arm_chain_(send_times, send_wait);
187 if (this->non_blocking_) {
188 this->complete_pending_ = true;
189 this->enable_loop();
190 return;
191 }
192 // blocking mode: wait out the chain, bounded by the stall deadline
193 while (this->transmitting_) {
194 if ((int32_t) (millis() - s_expected_end_ms) > 0) {
195 this->abort_stalled_chain_();
196 break;
197 }
198 App.feed_wdt();
199 delay(1);
200 }
201 this->deliver_completion_();
202}
203
205 if (!this->complete_pending_) {
206 this->disable_loop();
207 return;
208 }
209 if (this->transmitting_) {
210 // non-blocking stall recovery: without this, a dead chain would leave the carrier
211 // driven and on_complete unfired until the next send happened to abort it
212 if ((int32_t) (millis() - s_expected_end_ms) <= 0)
213 return;
214 this->abort_stalled_chain_();
215 }
216 // release the loop before user code runs: the automation may start a new non-blocking
217 // send, and its enable_loop() must be the last writer or its completion would strand
218 this->disable_loop();
219 this->deliver_completion_();
220}
221
222} // namespace esphome::remote_transmitter
223
224#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:246
void disable_loop()
Disable this component's loop.
void status_clear_warning()
Definition component.h:289
void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE
Inform the parent automation that the event has triggered.
Definition automation.h:461
const RawTimings & get_data() const
Definition remote_base.h:31
RemoteTransmitData temp_
Use same vector for all transmits, avoids many allocations.
void send_internal(uint32_t send_times, uint32_t send_wait) override
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t