[uart] Stop ESP8266 software serial RX ISR from blocking for a whole byte

This commit is contained in:
J. Nick Koston
2026-08-19 13:16:37 -05:00
parent 470226ca03
commit 4d68f0cb98
2 changed files with 139 additions and 4 deletions
@@ -15,6 +15,9 @@
namespace esphome::uart {
static const char *const TAG = "uart";
// Edge decoder up to this baud rate; above it the start bit sampler, whose
// whole-byte block in the ISR is short there and whose timing still holds up.
static constexpr uint32_t SW_SERIAL_EDGE_MODE_MAX_BAUD = 38400;
bool ESP8266UartComponent::serial0_in_use = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
uint32_t ESP8266UartComponent::get_config() {
@@ -237,6 +240,9 @@ void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_p
this->stop_bits_ = stop_bits;
this->data_bits_ = data_bits;
this->parity_ = parity;
this->rx_stop_bit_ = data_bits + (parity != UART_CONFIG_PARITY_NONE ? 1 : 0);
// Runs longer than a whole frame plus one bit are idle; cap them there.
this->rx_max_run_cycles_ = this->bit_time_ * (this->rx_stop_bit_ + stop_bits + 2);
if (tx_pin != nullptr) {
gpio_tx_pin_ = tx_pin;
gpio_tx_pin_->setup();
@@ -247,10 +253,31 @@ void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_p
gpio_rx_pin_ = rx_pin;
gpio_rx_pin_->setup();
rx_pin_ = gpio_rx_pin_->to_isr();
rx_buffer_ = new uint8_t[this->rx_buffer_size_]; // NOLINT
gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
if (this->rx_buffer_ == nullptr) {
this->rx_buffer_ = new uint8_t[this->rx_buffer_size_]; // NOLINT
}
// load_settings() re-enters here, so reset the decoder before re-attaching.
this->rx_bit_ = RX_IDLE;
this->rx_last_level_ = this->rx_pin_.digital_read();
this->rx_last_edge_ = arch_get_cpu_cycle_count();
if (baud_rate <= SW_SERIAL_EDGE_MODE_MAX_BAUD) {
gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr_edge, this, gpio::INTERRUPT_ANY_EDGE);
} else {
gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
}
}
}
inline bool ESPHOME_ALWAYS_INLINE ESP8266SoftwareSerial::rx_push_byte_(uint8_t data) {
size_t in = this->rx_in_pos_;
size_t next = in + 1;
if (next == this->rx_buffer_size_)
next = 0;
if (next == this->rx_out_pos_)
return false; // full, drop the byte
this->rx_buffer_[in] = data;
this->rx_in_pos_ = next;
return true;
}
void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) {
uint32_t wait = arg->bit_time_ + arg->bit_time_ / 3 - 500;
const uint32_t start = arch_get_cpu_cycle_count();
@@ -269,8 +296,7 @@ void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) {
if (arg->stop_bits_ == 2)
arg->wait_(&wait, start);
arg->rx_buffer_[arg->rx_in_pos_] = rec;
arg->rx_in_pos_ = (arg->rx_in_pos_ + 1) % arg->rx_buffer_size_;
arg->rx_push_byte_(rec);
// Clear RX pin so that the interrupt doesn't re-trigger right away again.
arg->rx_pin_.clear_interrupt();
#ifdef USE_UART_WAKE_LOOP_ON_RX
@@ -280,6 +306,88 @@ void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) {
wake_loop_isrsafe();
#endif
}
inline bool ESPHOME_ALWAYS_INLINE ESP8266SoftwareSerial::rx_consume_run_(uint32_t bits, bool level) {
uint8_t bit = this->rx_bit_;
uint8_t cur = this->rx_cur_byte_;
bool pushed = false;
while (bits > 0) {
if (bit == RX_IDLE) {
// Idle line, or what is left of a run after a framing error.
if (level)
break;
// Start bit
bit = 0;
cur = 0;
bits--;
} else if (bit < this->data_bits_) {
uint8_t n = this->data_bits_ - bit;
if (n > bits)
n = bits;
if (level)
cur |= ((1U << n) - 1) << bit;
bit += n;
bits -= n;
} else if (bit < this->rx_stop_bit_) {
// Parity bit: consumed but not checked, same as gpio_intr.
bit++;
bits--;
} else {
// Stop bit; a low level here is a framing error, drop the byte.
if (level)
pushed = this->rx_push_byte_(cur);
bit = RX_IDLE;
break;
}
}
this->rx_bit_ = bit;
this->rx_cur_byte_ = cur;
return pushed;
}
void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr_edge(ESP8266SoftwareSerial *arg) {
const uint32_t now = arch_get_cpu_cycle_count();
const bool level = arg->rx_pin_.digital_read();
const bool last_level = arg->rx_last_level_;
// Two edges collapsed into one interrupt: skip it so the run is still
// measured from the last real edge and the frame stays aligned.
if (level == last_level)
return;
// Bits since the last edge, rounded to nearest; no hardware divider on the LX106, so count.
uint32_t delta = now - arg->rx_last_edge_;
if (delta > arg->rx_max_run_cycles_)
delta = arg->rx_max_run_cycles_;
delta += arg->bit_time_ / 2;
uint32_t bits = 0;
while (delta >= arg->bit_time_) {
delta -= arg->bit_time_;
bits++;
}
const bool pushed = arg->rx_consume_run_(bits, last_level);
arg->rx_last_edge_ = now;
arg->rx_last_level_ = level;
#ifdef USE_UART_WAKE_LOOP_ON_RX
// A frame's last edge is always rising and its tail is completed by
// rx_finalize_pending_() on the main loop, so wake on those as well.
if (pushed || (level && arg->rx_bit_ != RX_IDLE))
wake_loop_isrsafe();
#endif
}
void ESP8266SoftwareSerial::rx_finalize_pending_() {
const uint8_t bit = this->rx_bit_;
const uint32_t edge = this->rx_last_edge_;
// Bits still needed up to and including the first stop bit.
const uint32_t remaining = this->rx_stop_bit_ + 1 - bit;
if (arch_get_cpu_cycle_count() - edge < remaining * this->bit_time_ + this->bit_time_ / 2) {
#ifdef USE_UART_WAKE_LOOP_ON_RX
// Not old enough yet: run the loop again right away instead of after a full loop_interval_.
wake_loop_threadsafe();
#endif
return;
}
InterruptLock lock;
// If the ISR moved on in the meantime the next call picks it up.
if (this->rx_bit_ == bit && this->rx_last_edge_ == edge && this->rx_last_level_)
this->rx_consume_run_(remaining, true);
}
void IRAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) {
if (this->gpio_tx_pin_ == nullptr) {
ESP_LOGE(TAG, "UART doesn't have TX pins set!");
@@ -329,6 +437,7 @@ void IRAM_ATTR ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const
this->wait_(wait, start);
}
uint8_t ESP8266SoftwareSerial::read_byte() {
this->rx_sync_();
if (this->rx_in_pos_ == this->rx_out_pos_)
return 0;
uint8_t data = this->rx_buffer_[this->rx_out_pos_];
@@ -336,6 +445,7 @@ uint8_t ESP8266SoftwareSerial::read_byte() {
return data;
}
uint8_t ESP8266SoftwareSerial::peek_byte() {
this->rx_sync_();
if (this->rx_in_pos_ == this->rx_out_pos_)
return 0;
return this->rx_buffer_[this->rx_out_pos_];
@@ -344,6 +454,7 @@ void ESP8266SoftwareSerial::flush() {
// Flush is a NO-OP with software serial, all bytes are written immediately.
}
size_t ESP8266SoftwareSerial::available() {
this->rx_sync_();
// Read volatile rx_in_pos_ once to avoid TOCTOU race with ISR.
// When in >= out, data is contiguous: [out..in).
// When in < out, data wraps: [out..buf_size) + [0..in).
@@ -26,17 +26,41 @@ class ESP8266SoftwareSerial {
size_t available();
protected:
/// Start bit sampler for high baud rates: reads the whole byte inside the ISR.
static void gpio_intr(ESP8266SoftwareSerial *arg);
/// Edge decoder for low baud rates: counts bits from the time between edges, returns at once.
static void gpio_intr_edge(ESP8266SoftwareSerial *arg);
void wait_(uint32_t *wait, const uint32_t &start);
bool read_bit_(uint32_t *wait, const uint32_t &start);
void write_bit_(bool bit, uint32_t *wait, const uint32_t &start);
bool rx_push_byte_(uint8_t data);
/// Feed `bits` consecutive bits at `level` into the edge decoder. Returns true when a byte was pushed.
bool rx_consume_run_(uint32_t bits, bool level);
/// Complete a byte whose trailing bits are idle-high and so never produce a closing edge.
void rx_finalize_pending_();
void ESPHOME_ALWAYS_INLINE rx_sync_() {
if (this->rx_bit_ != RX_IDLE && this->rx_last_level_)
this->rx_finalize_pending_();
}
uint32_t bit_time_{0};
uint8_t *rx_buffer_{nullptr};
size_t rx_buffer_size_;
volatile size_t rx_in_pos_{0};
size_t rx_out_pos_{0};
// Edge decoder state. rx_bit_ is the index of the next frame bit after the
// start bit (data, then parity, then stop at rx_stop_bit_) or RX_IDLE.
static constexpr uint8_t RX_IDLE = 0xFF;
uint32_t rx_max_run_cycles_{0};
volatile uint32_t rx_last_edge_{0};
volatile uint8_t rx_bit_{RX_IDLE};
volatile uint8_t rx_cur_byte_{0};
volatile bool rx_last_level_{true};
uint8_t rx_stop_bit_{0};
uint8_t stop_bits_;
uint8_t data_bits_;
UARTParityOptions parity_;