[remote_transmitter] Use hardware PWM on rtl87xx to fix watchdog crash (#18579)

This commit is contained in:
Keith Burzinski
2026-08-21 23:53:16 -05:00
committed by GitHub
parent 65704e881f
commit dccf55eadc
5 changed files with 159 additions and 5 deletions
@@ -185,12 +185,14 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"remote_transmitter_rtl87xx.cpp": {
PlatformFramework.RTL87XX_ARDUINO,
},
"remote_transmitter.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
PlatformFramework.ESP8266_ARDUINO,
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
PlatformFramework.RP2_ARDUINO,
},
@@ -2,7 +2,8 @@
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
#if (defined(USE_LIBRETINY) && !defined(USE_RTL87XX)) || defined(USE_ESP8266) || defined(USE_RP2) || \
(defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
namespace esphome::remote_transmitter {
@@ -65,14 +65,21 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa
protected:
void send_internal(uint32_t send_times, uint32_t send_wait) override;
#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
void await_target_time_();
uint32_t target_time_{0};
#endif
#if defined(USE_ESP8266) || (defined(USE_LIBRETINY) && !defined(USE_RTL87XX)) || defined(USE_RP2) || \
(defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
void calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, uint32_t *off_time_period);
void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec);
void space_(uint32_t usec);
void await_target_time_();
uint32_t target_time_{0};
#endif
#ifdef USE_RTL87XX
// Carrier frequency the PWM is currently configured for; 0 = not yet configured
uint32_t current_carrier_frequency_{0};
void *pwm_{nullptr}; // pwmout_t*, opaque here to keep the SDK header out of this shared header
#endif
#if defined(USE_ESP32) && SOC_RMT_SUPPORTED
@@ -0,0 +1,137 @@
#include "remote_transmitter.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
// clang-tidy cannot parse the Realtek SDK headers pulled in via ArduinoPrivate.h
#if defined(USE_RTL87XX) && !defined(CLANG_TIDY)
// ArduinoPrivate.h = Arduino.h + the SDK's mbed HAL (pwmout etc.) with the core's fixes for
// type-name collisions between the two (e.g. PinMode)
#include <ArduinoPrivate.h>
#include <FreeRTOS.h>
#include <task.h>
namespace esphome::remote_transmitter {
static const char *const TAG = "remote_transmitter";
// The carrier is generated by the PWM peripheral instead of bit-banging the pin: software carrier
// generation requires disabling interrupts for the whole frame, but this core's micros() is derived
// from the FreeRTOS tick and freezes while interrupts are off, so the timing loop never advances and
// the watchdog resets the chip. With hardware PWM, software only times the mark/space envelope and
// interrupts can stay enabled.
//
// The PWM is driven through the SDK's pwmout HAL directly rather than the Arduino wiring layer:
// changing the carrier frequency via the wiring requires a GPIO/PWM pin mode round-trip, which
// use-after-frees the core's per-pin state (pinRemoveMode() frees without nulling) and corrupts the
// heap. pwmout_period_us() changes the frequency with no mode transitions.
void RemoteTransmitterComponent::setup() {
// Deliberately no pin_->setup(): registering the pin as GPIO claims it in the SDK's pin
// management, and the pad is then never handed over to the PWM peripheral -- pwmout_init()
// must own the pin from the start.
PinInfo *info = pinInfo(this->pin_->get_pin());
if (info == nullptr || !pinSupported(info, PIN_PWM)) {
// checked here because the AmebaZ (RTL8710B) SDK does not report PWM init failure
ESP_LOGE(TAG, "Pin %u is not PWM-capable", this->pin_->get_pin());
this->mark_failed();
return;
}
auto *pwm = new pwmout_t();
this->pwm_ = pwm;
pwmout_init(pwm, static_cast<PinName>(info->gpio));
#if LT_RTL8720C
// only the AmebaZ2 SDK's pwmout_s reports init success
if (!pwm->is_init) {
ESP_LOGE(TAG, "PWM init failed on pin %u", this->pin_->get_pin());
delete pwm;
this->pwm_ = nullptr;
this->mark_failed();
return;
}
#endif
pwmout_period_us(pwm, 26); // placeholder; the real carrier period is set per transmission
pwmout_write(pwm, this->pin_->is_inverted() ? 1.0f : 0.0f);
}
void RemoteTransmitterComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"Remote Transmitter:\n"
" Carrier Duty: %u%%",
this->carrier_duty_percent_);
LOG_PIN(" Pin: ", this->pin_);
}
void RemoteTransmitterComponent::await_target_time_() {
const uint32_t current_time = micros();
if (this->target_time_ == 0) {
this->target_time_ = current_time;
} else {
while ((int32_t) (this->target_time_ - micros()) > 0) {
}
}
}
void RemoteTransmitterComponent::digital_write(bool value) {
if (this->pwm_ == nullptr)
return;
pwmout_write(static_cast<pwmout_t *>(this->pwm_), (value != this->pin_->is_inverted()) ? 1.0f : 0.0f);
}
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
auto *pwm = static_cast<pwmout_t *>(this->pwm_);
if (pwm == nullptr) {
ESP_LOGW(TAG, "Cannot send: PWM not initialized");
return;
}
ESP_LOGD(TAG, "Sending remote code");
const uint32_t carrier_frequency = this->temp_.get_carrier_frequency();
// unmodulated protocols (no carrier or 100% duty) drive the pin constantly during marks
float mark_duty =
(carrier_frequency > 0 && this->carrier_duty_percent_ < 100) ? this->carrier_duty_percent_ / 100.0f : 1.0f;
float space_duty = 0.0f;
if (this->pin_->is_inverted()) {
mark_duty = 1.0f - mark_duty;
space_duty = 1.0f;
}
if (carrier_frequency > 0 && carrier_frequency != this->current_carrier_frequency_) {
// round(1000000/freq), clamped like the bit-bang path so a bad lambda can't hand the SDK a zero period
const uint32_t period = std::max(uint32_t(1), (1000000UL + carrier_frequency / 2) / carrier_frequency);
pwmout_period_us(pwm, period);
this->current_carrier_frequency_ = carrier_frequency;
}
this->transmit_trigger_.trigger();
const UBaseType_t saved_priority = uxTaskPriorityGet(nullptr);
for (uint32_t i = 0; i < send_times; i++) {
// Boost task priority for the frame only, so WiFi/lwIP tasks can't preempt mid-frame and
// merge adjacent marks. Interrupts stay enabled: micros() needs the FreeRTOS tick, and
// ISR latency is within receiver tolerance.
vTaskPrioritySet(nullptr, configMAX_PRIORITIES - 1);
// Re-anchor every iteration: a late exit from the normal-priority gap wait must not
// leave the schedule behind micros(), which would compress the next frame's leading items
this->target_time_ = 0;
for (int32_t item : this->temp_.get_data()) {
const bool is_mark = item > 0;
this->await_target_time_();
pwmout_write(pwm, is_mark ? mark_duty : space_duty);
this->target_time_ += is_mark ? uint32_t(item) : uint32_t(-item);
App.feed_wdt();
}
this->await_target_time_(); // wait for duration of last pulse
pwmout_write(pwm, space_duty);
vTaskPrioritySet(nullptr, saved_priority);
if (i + 1 < send_times) {
// The repeat gap is user-configurable and unbounded, so wait it out at normal
// priority, feeding the watchdog
const uint32_t gap_end = micros() + send_wait;
while ((int32_t) (gap_end - micros()) > 0) {
App.feed_wdt();
}
}
}
this->complete_trigger_.trigger();
}
} // namespace esphome::remote_transmitter
#endif // USE_RTL87XX && !CLANG_TIDY
@@ -0,0 +1,7 @@
remote_transmitter:
id: xmitr
pin: GPIO12
carrier_duty_percent: 50%
packages:
buttons: !include common-buttons.yaml