ESPHome 2026.9.0-dev
Loading...
Searching...
No Matches
remote_transmitter_bk72xx.cpp
Go to the documentation of this file.
3#include "esphome/core/log.h"
4
5// clang-tidy cannot parse the Beken SDK headers pulled in via ArduinoPrivate.h
6#if defined(USE_BK72XX) && !defined(CLANG_TIDY)
7
8// ArduinoPrivate.h = Arduino.h + the BDK SDK headers (pwm_pub.h, bk_timer_pub.h, icu_pub.h)
9// with the core's fixes for type-name collisions between the two
10#include <ArduinoPrivate.h>
11
12// Only the BK7231N-style PWM block (shadow registers with a hardware CFG_UPDATA load bit)
13// supports glitch-free per-edge duty updates; older SoCs compile the generic bit-bang
14// implementation (remote_transmitter.cpp) instead, and this file compiles to nothing.
15// REMOTE_TRANSMITTER_BK_PWM is set per-family in remote_transmitter.h.
16
18
19static const char *const TAG = "remote_transmitter";
20
21#ifdef REMOTE_TRANSMITTER_BK_PWM
22
23// PWM peripheral carrier (26MHz block), envelope paced by a BKTIMER1 interrupt chain: each
24// interrupt writes the next duty through the shadow registers (T1..T4 + CFG_UPDATA hardware
25// load, glitch-free at the next carrier period). Direct register writes beat the driver's
26// pwm_update_param() (~19us vs ~26us edge error) and have no shared state to race against.
27// BKTIMER1 is the only free channel: TIMER0 = FreeRTOS tick, TIMER2 = SDK cal, TIMER4 = wdt.
28
29static constexpr uint32_t REG_PWM_BASE = 0x00802B00UL;
30static constexpr uint32_t REG_PWM_GROUP_STRIDE = 0x40; // one register group per channel pair
31static constexpr uint32_t REG_PWM_T_REGS[2] = {0x04, 0x14}; // T1..T4 offsets within a group
32static constexpr uint32_t PWM_INT_STATUS_MASK = 3UL << 30; // write-1-clear -- always write as zero
33static constexpr uint8_t ENVELOPE_TIMER = BKTIMER1;
34
35// The bk_timer handler receives only the channel number, so the chain resolves the instance
36// that owns the timer. No IRAM_ATTR: hal.h makes it a no-op on BK72xx (the SDK masks IRQs
37// around flash writes).
38static void envelope_timer_isr(UINT8 channel) { RemoteTransmitterComponent::advance_active_isr(); }
39
40// Channel <-> pin comes from the board variant's own PIN_PWMn defines rather than a
41// family-wide assumption, so an unusual pinout maps correctly instead of silently
42// driving another pad
43struct PwmPinChannel {
44 uint8_t pin;
45 int8_t channel;
46};
47static constexpr PwmPinChannel PWM_PIN_CHANNELS[] = {
48#ifdef PIN_PWM0
49 {PIN_PWM0, 0},
50#endif
51#ifdef PIN_PWM1
52 {PIN_PWM1, 1},
53#endif
54#ifdef PIN_PWM2
55 {PIN_PWM2, 2},
56#endif
57#ifdef PIN_PWM3
58 {PIN_PWM3, 3},
59#endif
60#ifdef PIN_PWM4
61 {PIN_PWM4, 4},
62#endif
63#ifdef PIN_PWM5
64 {PIN_PWM5, 5},
65#endif
66};
67
68static int8_t pwm_channel_for_pin(uint8_t pin) {
69 for (const auto &entry : PWM_PIN_CHANNELS) {
70 if (entry.pin == pin)
71 return entry.channel;
72 }
73 return -1;
74}
75
77 // Deliberately no pin_->setup(): the pin must belong to the PWM function, not GPIO
78 const int8_t channel = pwm_channel_for_pin(this->pin_->get_pin());
79 if (channel < 0) {
80 ESP_LOGE(TAG, "Pin %u is not PWM-capable", this->pin_->get_pin());
81 this->mark_failed();
82 return;
83 }
84 this->pwm_channel_ = channel;
85 const uint32_t idle_t1 = this->pin_->is_inverted() ? this->isr_period_t4_ : 0;
86 pwm_param_st param{};
87 param.chan = channel;
88 param.t1 = idle_t1;
89 param.t4 = this->isr_period_t4_;
90 param.init_level = idle_t1 ? 1 : 0;
91 if (pwm_init_param(&param) != 0 || pwm_start(channel) != 0) {
92 ESP_LOGE(TAG, "PWM init failed on pin %u", this->pin_->get_pin());
93 this->pwm_channel_ = -1;
94 this->mark_failed();
95 return;
96 }
97 this->disable_loop(); // loop() is only needed while a non-blocking completion is pending
98}
99
101 ESP_LOGCONFIG(TAG,
102 "Remote Transmitter:\n"
103 " Carrier Duty: %u%%\n"
104 " Non-blocking: %s",
105 this->carrier_duty_percent_, YESNO(this->non_blocking_));
106 LOG_PIN(" Pin: ", this->pin_);
107}
108
109// Writes the duty compare registers and sets the hardware CFG_UPDATA shadow-load bit;
110// the new duty latches glitch-free at the next carrier period. ISR-safe: registers only.
111// The group control word is shared with the paired channel, but every SDK write to it runs
112// under GLOBAL_INT_DISABLE (bk_pwm), so it cannot be torn by this interrupt.
114 const uint32_t group = this->pwm_channel_ / 2;
115 const uint32_t post = this->pwm_channel_ % 2;
116 const uint32_t group_base = REG_PWM_BASE + REG_PWM_GROUP_STRIDE * group;
117 auto *t_regs = (volatile uint32_t *) (group_base + REG_PWM_T_REGS[post]);
118 auto *ctrl = (volatile uint32_t *) group_base;
119 const uint32_t init_level_bit = 1UL << (8 * post + 6); // output level while the counter is stopped
120 const uint32_t cfg_updata_bit = 1UL << (8 * post + 7); // 0->1 latches T1..T4 at the next period
121 t_regs[0] = t1_counts; // T1: high time
122 t_regs[1] = 0; // T2
123 t_regs[2] = 0; // T3
124 t_regs[3] = this->isr_period_t4_; // T4: period
125 uint32_t cfg = *ctrl;
126 cfg &= ~(PWM_INT_STATUS_MASK | init_level_bit | cfg_updata_bit);
127 if (t1_counts != 0)
128 cfg |= init_level_bit;
129 *ctrl = cfg;
130 *ctrl = cfg | cfg_updata_bit;
131}
132
133// --- envelope chain hooks (see remote_transmitter_libretiny_isr.cpp) ---
134
136
137// Recomputes the carrier period in 26MHz counts and stages the per-item duties;
138// unmodulated protocols drive the pin constantly during marks
140 if (carrier_frequency > 0) {
141 this->isr_period_t4_ = std::max(uint32_t(2), (26000000UL + carrier_frequency / 2) / carrier_frequency);
142 }
143 uint32_t mark_t1 = (carrier_frequency > 0 && this->carrier_duty_percent_ < 100)
144 ? std::max(uint32_t(1), this->isr_period_t4_ * this->carrier_duty_percent_ / 100)
145 : this->isr_period_t4_;
146 uint32_t space_t1 = 0;
147 if (this->pin_->is_inverted()) {
148 mark_t1 = this->isr_period_t4_ - mark_t1;
149 space_t1 = this->isr_period_t4_;
150 }
151 this->isr_mark_t1_ = mark_t1;
152 this->isr_space_t1_ = space_t1;
153}
154
158
159// The driver's microsecond init path is register writes under a nested interrupt guard,
160// so it is safe to call from the chain's own interrupt
162 timer_param_t param{};
163 param.channel = ENVELOPE_TIMER;
164 param.div = 1;
165 param.period = duration_us;
166 param.t_Int_Handler = envelope_timer_isr;
167 sddev_control((char *) TIMER_DEV_NAME, CMD_TIMER_INIT_PARAM_US, &param);
168}
169
171 UINT32 channel = ENVELOPE_TIMER;
172 sddev_control((char *) TIMER_DEV_NAME, CMD_TIMER_UNIT_DISABLE, &channel);
173}
174
176 if (this->pwm_channel_ < 0)
177 return;
178 // serialize behind an in-flight chain, matching the ESP32/RMT non-blocking behavior
179 this->wait_until_idle_();
180 this->write_pwm_t1_((value != this->pin_->is_inverted()) ? this->isr_period_t4_ : 0);
181}
182
183#endif // REMOTE_TRANSMITTER_BK_PWM
184
185} // namespace esphome::remote_transmitter
186
187#endif // USE_BK72XX && !CLANG_TIDY
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
static void uint32_t