Move the RX decoder into a host testable header, detach the ISR and resize the buffer on reload, log the active RX decoder

This commit is contained in:
J. Nick Koston
2026-08-19 13:38:39 -05:00
parent 9709e336f6
commit f633fd5566
4 changed files with 449 additions and 121 deletions
@@ -0,0 +1,184 @@
#pragma once
#include <cstddef>
#include <cstdint>
#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
@@ -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
@@ -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_;
};
@@ -0,0 +1,234 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <functional>
#include <random>
#include <vector>
#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<uint8_t> &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<int32_t()> &jitter = [] { return 0; }) {
std::vector<bool> 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<uint32_t>(static_cast<int64_t>(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<uint8_t> buffer_;
std::vector<uint8_t> 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<double> jit(-jitter_bits, jitter_bits);
const uint8_t mask = static_cast<uint8_t>((1U << f.data_bits) - 1);
std::vector<uint8_t> 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<int32_t>(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<uint8_t>(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