diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 2f8b4dbd11..e6ce91a22e 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -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). diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h index 469885b6b6..2c00ed8658 100644 --- a/esphome/components/uart/uart_component_esp8266.h +++ b/esphome/components/uart/uart_component_esp8266.h @@ -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_;