ESPHome 2026.9.0-dev
Loading...
Searching...
No Matches
remote_transmitter_rtl87xx.cpp
Go to the documentation of this file.
3#include "esphome/core/log.h"
4
5// clang-tidy cannot parse the Realtek SDK headers pulled in via ArduinoPrivate.h
6#if defined(USE_RTL87XX) && !defined(CLANG_TIDY)
7
8// ArduinoPrivate.h = Arduino.h + the SDK's mbed HAL (pwmout, gtimer) with the core's fixes for
9// type-name collisions between the two (e.g. PinMode)
10#include <ArduinoPrivate.h>
11#ifndef USE_LIBRETINY_VARIANT_RTL8720C
12#include <FreeRTOS.h>
13#include <task.h>
14#endif
15
17
18static const char *const TAG = "remote_transmitter";
19
20// PWM peripheral carrier, envelope paced by a gtimer interrupt chain. Bit-banging would need
21// interrupts disabled for the whole frame, but this core's micros() derives from the FreeRTOS
22// tick and freezes then. The SDK pwmout HAL is driven directly: the Arduino wiring layer's
23// GPIO/PWM mode round-trip use-after-frees LibreTiny's per-pin state.
24
25#ifdef USE_LIBRETINY_VARIANT_RTL8720C
26static constexpr uint32_t ENVELOPE_TIMER_ID = TIMER6; // GTimer7
27
28// One envelope timer for all instances: a second gtimer_init on the same id fails silently,
29// so the chain serializes them (remote_transmitter_libretiny_isr.cpp)
30// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
31static uint8_t s_pwm_tick_sources[] = {GTimer1, GTimer2, GTimer3, GTimer4, GTimer5, GTimer6, 0xff};
32static gtimer_t s_envelope_timer;
33static bool s_envelope_timer_ready = false;
34// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
35
36static void IRAM_ATTR envelope_timer_isr(uint32_t arg) {
37 reinterpret_cast<RemoteTransmitterComponent *>(arg)->advance_envelope_isr();
38}
39#endif // USE_LIBRETINY_VARIANT_RTL8720C
40
42 // no pin_->setup(): a GPIO claim in the SDK's pin management blocks pwmout_init from
43 // owning the pad
44 PinInfo *info = pinInfo(this->pin_->get_pin());
45 if (info == nullptr || !pinSupported(info, PIN_PWM)) {
46 // checked here because the AmebaZ (RTL8710B) SDK does not report PWM init failure
47 ESP_LOGE(TAG, "Pin %u is not PWM-capable", this->pin_->get_pin());
48 this->mark_failed();
49 return;
50 }
51 auto *pwm = new pwmout_t();
52 this->pwm_ = pwm;
53 pwmout_init(pwm, static_cast<PinName>(info->gpio));
54#ifdef USE_LIBRETINY_VARIANT_RTL8720C
55 // only the AmebaZ2 SDK's pwmout_s reports init success
56 if (!pwm->is_init) {
57 ESP_LOGE(TAG, "PWM init failed on pin %u", this->pin_->get_pin());
58 delete pwm;
59 this->pwm_ = nullptr;
60 this->mark_failed();
61 return;
62 }
63 // Shrink the PWM tick-source pool before the period claim below so GTimer7 stays free
64 // for the envelope; pwmout_init just registered the full pool.
65 hal_pwm_comm_tick_source_list(s_pwm_tick_sources);
66#endif
67 pwmout_period_us(pwm, 26); // placeholder; the real carrier period is set per transmission
68 pwmout_write(pwm, this->pin_->is_inverted() ? 1.0f : 0.0f);
69#ifdef USE_LIBRETINY_VARIANT_RTL8720C
70 if (!s_envelope_timer_ready) {
71 gtimer_init(&s_envelope_timer, ENVELOPE_TIMER_ID);
72 s_envelope_timer_ready = true;
73 }
74 this->disable_loop(); // loop() is only needed while a non-blocking completion is pending
75#endif
76}
77
79 ESP_LOGCONFIG(TAG,
80 "Remote Transmitter:\n"
81 " Carrier Duty: %u%%",
83#ifdef USE_LIBRETINY_VARIANT_RTL8720C
84 ESP_LOGCONFIG(TAG, " Non-blocking: %s", YESNO(this->non_blocking_));
85#endif
86 LOG_PIN(" Pin: ", this->pin_);
87}
88
90 if (this->pwm_ == nullptr)
91 return;
92#ifdef USE_LIBRETINY_VARIANT_RTL8720C
93 // serialize behind an in-flight chain, matching the ESP32/RMT non-blocking behavior
94 this->wait_until_idle_();
95#endif
96 pwmout_write(static_cast<pwmout_t *>(this->pwm_), (value != this->pin_->is_inverted()) ? 1.0f : 0.0f);
97}
98
99#ifdef USE_LIBRETINY_VARIANT_RTL8720C
100// --- envelope chain hooks (see remote_transmitter_libretiny_isr.cpp) ---
101
102bool RemoteTransmitterComponent::envelope_ready_() const { return this->pwm_ != nullptr; }
103
104// Retunes the PWM period when the carrier changes and stages the per-item duties;
105// unmodulated protocols (no carrier or 100% duty) drive the pin constantly during marks
107 float mark_duty =
108 (carrier_frequency > 0 && this->carrier_duty_percent_ < 100) ? this->carrier_duty_percent_ / 100.0f : 1.0f;
109 float space_duty = 0.0f;
110 if (this->pin_->is_inverted()) {
111 mark_duty = 1.0f - mark_duty;
112 space_duty = 1.0f;
113 }
114 this->isr_mark_duty_ = mark_duty;
115 this->isr_space_duty_ = space_duty;
116 if (carrier_frequency == 0 || carrier_frequency == this->current_carrier_frequency_)
117 return;
118 // round(1000000/freq), clamped so a bad lambda can't hand the SDK a zero period
119 const uint32_t period = std::max(uint32_t(1), (1000000UL + carrier_frequency / 2) / carrier_frequency);
120 pwmout_period_us(static_cast<pwmout_t *>(this->pwm_), period);
121 this->current_carrier_frequency_ = carrier_frequency;
122}
123
125 pwmout_write(static_cast<pwmout_t *>(this->pwm_), mark ? this->isr_mark_duty_ : this->isr_space_duty_);
126}
127
128void IRAM_ATTR RemoteTransmitterComponent::arm_one_shot_(uint32_t duration_us) {
129 gtimer_start_one_shout(&s_envelope_timer, duration_us, (void *) envelope_timer_isr, (uint32_t) this);
130}
131
132void IRAM_ATTR RemoteTransmitterComponent::stop_envelope_timer_() { gtimer_stop(&s_envelope_timer); }
133
134#else // !USE_LIBRETINY_VARIANT_RTL8720C -- AmebaZ (RTL8710B): spin-based envelope, per-frame priority boost
135
137 const uint32_t current_time = micros();
138 if (this->target_time_ == 0) {
139 this->target_time_ = current_time;
140 } else {
141 while ((int32_t) (this->target_time_ - micros()) > 0) {
142 }
143 }
144}
145
147 if (this->pwm_ == nullptr) {
148 ESP_LOGW(TAG, "Cannot send: PWM not initialized");
149 return;
150 }
151 ESP_LOGD(TAG, "Sending remote code");
152 const uint32_t carrier_frequency = this->temp_.get_carrier_frequency();
153 // unmodulated protocols (no carrier or 100% duty) drive the pin constantly during marks
154 float mark_duty =
155 (carrier_frequency > 0 && this->carrier_duty_percent_ < 100) ? this->carrier_duty_percent_ / 100.0f : 1.0f;
156 float space_duty = 0.0f;
157 if (this->pin_->is_inverted()) {
158 mark_duty = 1.0f - mark_duty;
159 space_duty = 1.0f;
160 }
161 auto *pwm = static_cast<pwmout_t *>(this->pwm_);
162 if (carrier_frequency > 0 && carrier_frequency != this->current_carrier_frequency_) {
163 // round(1000000/freq), clamped like the bit-bang path so a bad lambda can't hand the SDK a zero period
164 const uint32_t period = std::max(uint32_t(1), (1000000UL + carrier_frequency / 2) / carrier_frequency);
165 pwmout_period_us(pwm, period);
166 this->current_carrier_frequency_ = carrier_frequency;
167 }
169 const UBaseType_t saved_priority = uxTaskPriorityGet(nullptr);
170 for (uint32_t i = 0; i < send_times; i++) {
171 // Boost task priority for the frame only, so WiFi/lwIP tasks can't preempt mid-frame and
172 // merge adjacent marks. Interrupts stay enabled: micros() needs the FreeRTOS tick, and
173 // ISR latency is within receiver tolerance.
174 vTaskPrioritySet(nullptr, configMAX_PRIORITIES - 1);
175 // Re-anchor every iteration: a late exit from the normal-priority gap wait must not
176 // leave the schedule behind micros(), which would compress the next frame's leading items
177 this->target_time_ = 0;
178 for (int32_t item : this->temp_.get_data()) {
179 const bool is_mark = item > 0;
180 this->await_target_time_();
181 pwmout_write(pwm, is_mark ? mark_duty : space_duty);
182 this->target_time_ += is_mark ? uint32_t(item) : uint32_t(-item);
183 App.feed_wdt();
184 }
185 this->await_target_time_(); // wait for duration of last pulse
186 pwmout_write(pwm, space_duty);
187 vTaskPrioritySet(nullptr, saved_priority);
188 if (i + 1 < send_times) {
189 // The repeat gap is user-configurable and unbounded, so wait it out at normal
190 // priority, feeding the watchdog
191 const uint32_t gap_end = micros() + send_wait;
192 while ((int32_t) (gap_end - micros()) > 0) {
193 App.feed_wdt();
194 }
195 }
196 }
198}
199
200#endif // USE_LIBRETINY_VARIANT_RTL8720C
201
202} // namespace esphome::remote_transmitter
203
204#endif // USE_RTL87XX && !CLANG_TIDY
void feed_wdt()
Feed the task watchdog.
void mark_failed()
Mark this component as failed.
void disable_loop()
Disable this component's loop.
virtual uint8_t get_pin() const =0
virtual bool is_inverted() const =0
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
uint32_t IRAM_ATTR HOT micros()
Definition hal.cpp:43
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t