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