From f633fd556699a6108714b7fb6e52b8d95b37a403 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Aug 2026 13:38:39 -0500 Subject: [PATCH] Move the RX decoder into a host testable header, detach the ISR and resize the buffer on reload, log the active RX decoder --- .../uart/software_serial_rx_decoder.h | 184 ++++++++++++++ .../uart/uart_component_esp8266.cpp | 129 ++-------- .../components/uart/uart_component_esp8266.h | 23 +- .../uart/test_software_serial_rx_decoder.cpp | 234 ++++++++++++++++++ 4 files changed, 449 insertions(+), 121 deletions(-) create mode 100644 esphome/components/uart/software_serial_rx_decoder.h create mode 100644 tests/components/uart/test_software_serial_rx_decoder.cpp diff --git a/esphome/components/uart/software_serial_rx_decoder.h b/esphome/components/uart/software_serial_rx_decoder.h new file mode 100644 index 0000000000..29c31b3c36 --- /dev/null +++ b/esphome/components/uart/software_serial_rx_decoder.h @@ -0,0 +1,184 @@ +#pragma once + +#include +#include + +#include "esphome/core/helpers.h" + +namespace esphome::uart { + +/// Bit timing decoder for a software serial RX pin. +/// +/// on_edge() is fed from the pin change interrupt with the cycle count of the edge +/// and the level the line changed to; it counts how many bit times the previous +/// level lasted and runs them through a start/data/parity/stop state machine, +/// so the interrupt never waits for the line. Decoded bytes land in a ring buffer. +/// A byte whose trailing bits are idle high has no closing edge; the main loop +/// completes it with finalize() once enough time has passed. +/// +/// Kept free of platform headers so it can be unit tested on the host. The hot +/// methods are force inlined so the platform ISR that calls them stays in IRAM. +class SoftwareSerialRxDecoder { + public: + static constexpr uint8_t RX_IDLE = 0xFF; + + void setup(uint32_t bit_cycles, uint8_t data_bits, bool parity, uint8_t stop_bits, uint8_t *buffer, + size_t buffer_size) { + this->bit_cycles_ = bit_cycles; + this->data_bits_ = data_bits; + this->stop_bit_ = data_bits + (parity ? 1 : 0); + // Runs longer than a whole frame plus one bit are idle; cap them there. + this->max_run_cycles_ = bit_cycles * (this->stop_bit_ + stop_bits + 2); + this->buffer_ = buffer; + this->buffer_size_ = buffer_size; + this->in_pos_ = 0; + this->out_pos_ = 0; + } + + /// Forget any partial frame; `level` is the current line level. + void reset(uint32_t now, bool level) { + this->bit_ = RX_IDLE; + this->last_level_ = level; + this->last_edge_ = now; + } + + /// ISR: the line changed to `level` at cycle `now`. + /// Returns true when the main loop should be woken: a byte was pushed, or the + /// line went idle high mid frame and finalize() is needed to complete the byte. + bool ESPHOME_ALWAYS_INLINE on_edge(uint32_t now, bool level) { + const bool last_level = this->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 false; + // Bits since the last edge, rounded to nearest; no hardware divider on the LX106, so count. + uint32_t delta = now - this->last_edge_; + if (delta > this->max_run_cycles_) + delta = this->max_run_cycles_; + delta += this->bit_cycles_ / 2; + uint32_t bits = 0; + while (delta >= this->bit_cycles_) { + delta -= this->bit_cycles_; + bits++; + } + const bool pushed = this->consume_run_(bits, last_level); + this->last_edge_ = now; + this->last_level_ = level; + return pushed || (level && this->bit_ != RX_IDLE); + } + + /// True while a frame is open and the line sits idle high, so a byte may be waiting on finalize(). + bool pending() const { return this->bit_ != RX_IDLE && this->last_level_; } + + /// Cheap unlocked check whether the pending byte's tail has elapsed by `now`. + bool finalize_due(uint32_t now) const { + const uint8_t bit = this->bit_; + if (bit == RX_IDLE) + return false; + return now - this->last_edge_ >= this->tail_cycles_(bit); + } + + /// Complete the pending byte if its tail has elapsed. Call with the ISR masked. + void finalize(uint32_t now) { + const uint8_t bit = this->bit_; + if (bit == RX_IDLE || !this->last_level_ || now - this->last_edge_ < this->tail_cycles_(bit)) + return; + this->consume_run_(this->stop_bit_ + 1 - bit, true); + } + + /// Store a decoded byte, dropping it when the buffer is full. Also used by the start bit sampler. + bool ESPHOME_ALWAYS_INLINE push_byte(uint8_t data) { + size_t in = this->in_pos_; + size_t next = in + 1; + if (next == this->buffer_size_) + next = 0; + if (next == this->out_pos_) + return false; + this->buffer_[in] = data; + this->in_pos_ = next; + return true; + } + + size_t available() const { + // Read volatile in_pos_ once to avoid TOCTOU race with ISR. + size_t in = this->in_pos_; + if (in >= this->out_pos_) + return in - this->out_pos_; + return this->buffer_size_ - this->out_pos_ + in; + } + uint8_t peek_byte() const { + if (this->in_pos_ == this->out_pos_) + return 0; + return this->buffer_[this->out_pos_]; + } + uint8_t read_byte() { + if (this->in_pos_ == this->out_pos_) + return 0; + uint8_t data = this->buffer_[this->out_pos_]; + size_t next = this->out_pos_ + 1; + this->out_pos_ = next == this->buffer_size_ ? 0 : next; + return data; + } + + protected: + /// Cycles the line must stay high after the last edge to hold every bit up to the first stop bit. + uint32_t tail_cycles_(uint8_t bit) const { + return (this->stop_bit_ + 1 - bit) * this->bit_cycles_ + this->bit_cycles_ / 2; + } + + /// Feed `bits` consecutive bits at `level` into the frame. Returns true when a byte was pushed. + bool ESPHOME_ALWAYS_INLINE consume_run_(uint32_t bits, bool level) { + uint8_t bit = this->bit_; + uint8_t cur = this->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->stop_bit_) { + // Parity bit: consumed but not checked. + bit++; + bits--; + } else { + // Stop bit; a low level here is a framing error, drop the byte. + if (level) + pushed = this->push_byte(cur); + bit = RX_IDLE; + break; + } + } + this->bit_ = bit; + this->cur_byte_ = cur; + return pushed; + } + + // Members ordered largest to smallest to minimize padding + uint32_t bit_cycles_{0}; + uint32_t max_run_cycles_{0}; + volatile uint32_t last_edge_{0}; + uint8_t *buffer_{nullptr}; + size_t buffer_size_{0}; + volatile size_t in_pos_{0}; + volatile size_t out_pos_{0}; + /// Index of the next frame bit after the start bit (data, then parity, then stop at stop_bit_) or RX_IDLE. + volatile uint8_t bit_{RX_IDLE}; + volatile uint8_t cur_byte_{0}; + volatile bool last_level_{true}; + uint8_t data_bits_{8}; + uint8_t stop_bit_{8}; +}; + +} // namespace esphome::uart diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index e6ce91a22e..0eab780114 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -160,6 +160,10 @@ void ESP8266UartComponent::dump_config() { "\n Wake on data RX: ENABLED" #endif ); + if (this->rx_pin_ != nullptr) { + ESP_LOGCONFIG(TAG, " RX decoder: %s", + this->baud_rate_ <= SW_SERIAL_EDGE_MODE_MAX_BAUD ? "edge" : "start bit sampler"); + } } this->check_logger_conflict(); } @@ -235,14 +239,13 @@ UARTFlushResult ESP8266UartComponent::flush() { void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, uint32_t baud_rate, uint8_t stop_bits, uint32_t data_bits, UARTParityOptions parity, size_t rx_buffer_size) { + // load_settings() re-enters here: stop the RX interrupt before touching anything it reads. + if (this->gpio_rx_pin_ != nullptr) + this->gpio_rx_pin_->detach_interrupt(); this->bit_time_ = F_CPU / baud_rate; - this->rx_buffer_size_ = rx_buffer_size; 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(); @@ -253,13 +256,17 @@ 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(); - if (this->rx_buffer_ == nullptr) { - this->rx_buffer_ = new uint8_t[this->rx_buffer_size_]; // NOLINT + if (this->rx_buffer_ != nullptr && this->rx_buffer_size_ != rx_buffer_size) { + delete[] this->rx_buffer_; // NOLINT + this->rx_buffer_ = nullptr; } - // 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(); + this->rx_buffer_size_ = rx_buffer_size; + if (this->rx_buffer_ == nullptr) { + this->rx_buffer_ = new uint8_t[rx_buffer_size]; // NOLINT + } + this->rx_.setup(this->bit_time_, data_bits, parity != UART_CONFIG_PARITY_NONE, stop_bits, this->rx_buffer_, + rx_buffer_size); + this->rx_.reset(arch_get_cpu_cycle_count(), this->rx_pin_.digital_read()); if (baud_rate <= SW_SERIAL_EDGE_MODE_MAX_BAUD) { gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr_edge, this, gpio::INTERRUPT_ANY_EDGE); } else { @@ -267,17 +274,6 @@ void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_p } } } -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(); @@ -296,7 +292,7 @@ void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) { if (arg->stop_bits_ == 2) arg->wait_(&wait, start); - arg->rx_push_byte_(rec); + 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 @@ -306,77 +302,18 @@ 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)) + if (arg->rx_.on_edge(now, level)) wake_loop_isrsafe(); +#else + arg->rx_.on_edge(now, level); #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) { + if (!this->rx_.finalize_due(arch_get_cpu_cycle_count())) { #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(); @@ -384,9 +321,7 @@ void ESP8266SoftwareSerial::rx_finalize_pending_() { 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); + this->rx_.finalize(arch_get_cpu_cycle_count()); } void IRAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) { if (this->gpio_tx_pin_ == nullptr) { @@ -438,30 +373,18 @@ void IRAM_ATTR ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const } 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_]; - this->rx_out_pos_ = (this->rx_out_pos_ + 1) % this->rx_buffer_size_; - return data; + return this->rx_.read_byte(); } 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_]; + return this->rx_.peek_byte(); } 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). - size_t in = this->rx_in_pos_; - if (in >= this->rx_out_pos_) - return in - this->rx_out_pos_; - return this->rx_buffer_size_ - this->rx_out_pos_ + in; + return this->rx_.available(); } } // namespace esphome::uart diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h index 10ae253350..7bc7ec96fa 100644 --- a/esphome/components/uart/uart_component_esp8266.h +++ b/esphome/components/uart/uart_component_esp8266.h @@ -7,6 +7,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "software_serial_rx_decoder.h" #include "uart_component.h" namespace esphome::uart { @@ -35,37 +36,23 @@ class ESP8266SoftwareSerial { 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. + /// 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_) + if (this->rx_.pending()) this->rx_finalize_pending_(); } - // 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; - // Members ordered largest to smallest to minimize padding uint32_t bit_time_{0}; - uint32_t rx_max_run_cycles_{0}; - volatile uint32_t rx_last_edge_{0}; uint8_t *rx_buffer_{nullptr}; - size_t rx_buffer_size_; - volatile size_t rx_in_pos_{0}; - size_t rx_out_pos_{0}; + size_t rx_buffer_size_{0}; InternalGPIOPin *gpio_tx_pin_{nullptr}; ISRInternalGPIOPin tx_pin_; InternalGPIOPin *gpio_rx_pin_{nullptr}; ISRInternalGPIOPin rx_pin_; + SoftwareSerialRxDecoder rx_; UARTParityOptions parity_; - 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_; }; diff --git a/tests/components/uart/test_software_serial_rx_decoder.cpp b/tests/components/uart/test_software_serial_rx_decoder.cpp new file mode 100644 index 0000000000..c8961fc3ad --- /dev/null +++ b/tests/components/uart/test_software_serial_rx_decoder.cpp @@ -0,0 +1,234 @@ +#include + +#include +#include +#include +#include + +#include "esphome/components/uart/software_serial_rx_decoder.h" + +namespace esphome::uart::testing { + +namespace { + +// 80 MHz ESP8266 clock +constexpr uint32_t CPU_HZ = 80000000; + +// Drives a SoftwareSerialRxDecoder with a simulated line: frames are turned into +// edges at their ideal cycle counts (plus optional jitter), and the main loop is +// emulated by poll(), which finalizes a pending byte and drains the buffer. +class LineSim { + public: + LineSim(uint32_t baud, uint8_t data_bits, bool parity, bool odd, uint8_t stop_bits, size_t buffer_size = 64) + : data_bits_(data_bits), parity_(parity), odd_(odd), stop_bits_(stop_bits), buffer_(buffer_size) { + this->bit_ = CPU_HZ / baud; + this->dec_.setup(this->bit_, data_bits, parity, stop_bits, this->buffer_.data(), buffer_size); + this->dec_.reset(this->now_, true); + } + + uint32_t bit_cycles() const { return this->bit_; } + SoftwareSerialRxDecoder &decoder() { return this->dec_; } + const std::vector &received() const { return this->received_; } + + void edge(uint32_t at, bool level) { + this->now_ = at; + this->line_ = level; + this->dec_.on_edge(at, level); + } + + // Main loop pass at cycle `at`. + void poll(uint32_t at) { + this->now_ = at; + if (this->dec_.pending() && this->dec_.finalize_due(at)) + this->dec_.finalize(at); + while (this->dec_.available() > 0) + this->received_.push_back(this->dec_.read_byte()); + } + + // Emit one frame starting at cycle `start`; `jitter` is the per edge offset in cycles + // (positive or negative) supplied by the caller. Returns the cycle at which the frame ends. + uint32_t send( + uint8_t value, uint32_t start, const std::function &jitter = [] { return 0; }) { + std::vector bits; + bits.push_back(false); + int ones = 0; + for (int i = 0; i < this->data_bits_; i++) { + bool b = (value >> i) & 1; + bits.push_back(b); + ones += b; + } + if (this->parity_) + bits.push_back(this->odd_ ? !(ones & 1) : (ones & 1)); + for (int i = 0; i < this->stop_bits_; i++) + bits.push_back(true); + uint32_t t = start; + for (bool b : bits) { + if (b != this->line_) + this->edge(static_cast(static_cast(t) + jitter()), b); + t += this->bit_; + } + return t; + } + + protected: + uint8_t data_bits_; + bool parity_; + bool odd_; + uint8_t stop_bits_; + uint32_t bit_{0}; + uint32_t now_{1000}; + bool line_{true}; + std::vector buffer_; + std::vector received_; + SoftwareSerialRxDecoder dec_; +}; + +struct FrameFormat { + uint32_t baud; + uint8_t data_bits; + bool parity; + bool odd; + uint8_t stop_bits; +}; + +// Random bytes, back to back or with idle gaps, with bounded edge jitter. +void run_stream(const FrameFormat &f, double jitter_bits, uint32_t gap_bits, int count, unsigned seed) { + LineSim sim(f.baud, f.data_bits, f.parity, f.odd, f.stop_bits); + std::mt19937 rng(seed); + std::uniform_real_distribution jit(-jitter_bits, jitter_bits); + const uint8_t mask = static_cast((1U << f.data_bits) - 1); + std::vector sent; + uint32_t t = 5000; + for (int n = 0; n < count; n++) { + uint8_t b = rng() & mask; + sent.push_back(b); + t = sim.send(b, t, [&] { return static_cast(jit(rng) * sim.bit_cycles()); }); + if (gap_bits != 0 && n % 3 == 2) { + t += gap_bits * sim.bit_cycles(); + sim.poll(t); + } else if (rng() % 4 == 0) { + sim.poll(t); + } + } + sim.poll(t + 20 * sim.bit_cycles()); + EXPECT_EQ(sim.received(), sent) << "baud " << f.baud << " jitter " << jitter_bits; +} + +} // namespace + +TEST(SoftwareSerialRxDecoder, DecodesBackToBackFramesAtCommonBaudRates) { + for (uint32_t baud : {2400U, 4800U, 9600U, 19200U, 38400U}) { + run_stream({baud, 8, false, false, 1}, 0.0, 0, 300, baud); + } +} + +TEST(SoftwareSerialRxDecoder, ToleratesEdgeJitterUpToAQuarterBit) { + // Consecutive edges can each be off by up to a quarter bit in either direction, + // so the measured run length is within half a bit of the true one. + run_stream({9600, 8, false, false, 1}, 0.24, 0, 2000, 1); + run_stream({9600, 8, false, false, 1}, 0.24, 7, 2000, 2); + run_stream({38400, 8, false, false, 1}, 0.24, 4, 2000, 3); +} + +TEST(SoftwareSerialRxDecoder, HandlesParityDataBitsAndStopBits) { + run_stream({9600, 8, true, false, 1}, 0.2, 3, 1000, 4); // 8E1 + run_stream({4800, 8, true, true, 2}, 0.2, 2, 1000, 5); // 8O2 + run_stream({19200, 7, false, false, 2}, 0.2, 5, 1000, 6); // 7N2 + run_stream({2400, 5, false, false, 1}, 0.2, 1, 1000, 7); // 5N1 +} + +TEST(SoftwareSerialRxDecoder, AllOnesByteCompletesOnlyByFinalize) { + LineSim sim(9600, 8, false, false, 1); + const uint32_t bit = sim.bit_cycles(); + uint32_t t = 5000; + // Start bit, then the line goes high and stays there: 0xFF has no closing edge. + sim.edge(t, false); + sim.edge(t + bit, true); + EXPECT_TRUE(sim.decoder().pending()); + // Eight data bits plus the stop bit must elapse after the rising edge. + sim.poll(t + bit + 8 * bit); + EXPECT_TRUE(sim.received().empty()); + EXPECT_FALSE(sim.decoder().finalize_due(t + bit + 8 * bit)); + sim.poll(t + bit + 10 * bit); + ASSERT_EQ(sim.received().size(), 1u); + EXPECT_EQ(sim.received()[0], 0xFF); + EXPECT_FALSE(sim.decoder().pending()); +} + +TEST(SoftwareSerialRxDecoder, LastByteOfBurstIsFinalizedThenNextFrameDecodes) { + LineSim sim(9600, 8, false, false, 1); + const uint32_t bit = sim.bit_cycles(); + uint32_t t = sim.send(0xA5, 5000); + t = sim.send(0xF0, t); // ends high, needs finalize + EXPECT_TRUE(sim.received().empty()); + sim.poll(t + 2 * bit); + ASSERT_EQ(sim.received().size(), 2u); + EXPECT_EQ(sim.received()[0], 0xA5); + EXPECT_EQ(sim.received()[1], 0xF0); + // The stale last edge must not confuse the next start bit. + t = sim.send(0x3C, t + 50 * bit); + sim.poll(t + 2 * bit); + ASSERT_EQ(sim.received().size(), 3u); + EXPECT_EQ(sim.received()[2], 0x3C); +} + +TEST(SoftwareSerialRxDecoder, BreakConditionIsDroppedAndResyncs) { + LineSim sim(9600, 8, false, false, 1); + const uint32_t bit = sim.bit_cycles(); + uint32_t t = 5000; + sim.edge(t, false); + t += 25 * bit; // line held low for far longer than a frame + sim.edge(t, true); + t += 3 * bit; + sim.poll(t); + EXPECT_TRUE(sim.received().empty()); + t = sim.send(0xA5, t); + sim.poll(t + 2 * bit); + ASSERT_EQ(sim.received().size(), 1u); + EXPECT_EQ(sim.received()[0], 0xA5); +} + +TEST(SoftwareSerialRxDecoder, CollapsedEdgeIsIgnoredAndStreamRealignsAtIdle) { + LineSim sim(9600, 8, false, false, 1); + const uint32_t bit = sim.bit_cycles(); + // 0x31 = 0b00110001: start, 1, 0, 0, 0, 1, 1, 0, 0, stop. Lose the rising edge + // of data bit 4 (ISR delayed past two edges), so the next edge arrives at the + // same level as the last one the decoder saw. + uint32_t t = 5000; + sim.edge(t, false); // start + sim.edge(t + 1 * bit, true); // bit0 = 1 + sim.edge(t + 2 * bit, false); // bits1..3 = 0 + sim.decoder().on_edge(t + 7 * bit, false); // should have been bit6 falling edge; level still low + sim.edge(t + 9 * bit, true); // stop + t += 10 * bit; + // Next frame decodes correctly once the line has idled. + t = sim.send(0x5A, t + 12 * bit); + sim.poll(t + 2 * bit); + ASSERT_FALSE(sim.received().empty()); + EXPECT_EQ(sim.received().back(), 0x5A); +} + +TEST(SoftwareSerialRxDecoder, DropsBytesWhenBufferIsFullAndKeepsOldest) { + LineSim sim(9600, 8, false, false, 1, 8); + uint32_t t = 5000; + for (int n = 0; n < 20; n++) + t = sim.send(static_cast(n), t); + sim.poll(t + 20 * sim.bit_cycles()); + ASSERT_EQ(sim.received().size(), 7u); // capacity is size - 1 + for (int n = 0; n < 7; n++) + EXPECT_EQ(sim.received()[n], n); +} + +TEST(SoftwareSerialRxDecoder, ResetDiscardsPartialFrame) { + LineSim sim(9600, 8, false, false, 1); + const uint32_t bit = sim.bit_cycles(); + sim.edge(5000, false); + sim.edge(5000 + bit, true); + EXPECT_TRUE(sim.decoder().pending()); + sim.decoder().reset(5000 + 2 * bit, true); + EXPECT_FALSE(sim.decoder().pending()); + sim.poll(5000 + 30 * bit); + EXPECT_TRUE(sim.received().empty()); +} + +} // namespace esphome::uart::testing