mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 06:36:23 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c6dd27ad5 | ||
|
|
8e27b2c717 | ||
|
|
1f981b6994 | ||
|
|
cd350825c7 | ||
|
|
f633fd5566 | ||
|
|
9709e336f6 | ||
|
|
4d68f0cb98 |
@@ -0,0 +1,177 @@
|
||||
#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() decodes from the
|
||||
/// time between edges so the ISR never waits for the line; a byte with an idle
|
||||
/// high tail has no closing edge and is completed by finalize() from the loop.
|
||||
/// Platform free for host tests; hot methods force inlined to stay in IRAM.
|
||||
class SoftwareSerialRxDecoder {
|
||||
public:
|
||||
static constexpr uint8_t RX_IDLE = 0xFF;
|
||||
|
||||
/// Configure framing and buffer; drops all state. Follow with reset().
|
||||
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);
|
||||
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;
|
||||
this->reset(0, true);
|
||||
}
|
||||
|
||||
/// Forget any partial frame; `level` is the current line level.
|
||||
void reset(uint32_t now, bool level) {
|
||||
this->bit_ = RX_IDLE;
|
||||
this->cur_byte_ = 0;
|
||||
this->last_level_ = level;
|
||||
this->last_edge_ = now;
|
||||
}
|
||||
|
||||
/// ISR: the line changed to `level` at cycle `now`. Returns true to wake the
|
||||
/// loop: a byte was pushed, or a frame is open with the line high (finalize()
|
||||
/// may be needed; a data 1 and the idle tail look the same at the edge).
|
||||
bool ESPHOME_ALWAYS_INLINE on_edge(uint32_t now, bool level) {
|
||||
const bool last_level = this->last_level_;
|
||||
// Collapsed edges: skip to keep the frame aligned to the last real edge.
|
||||
if (level == last_level)
|
||||
return false;
|
||||
// Bits since the last edge, rounded; LX106 has no divider, 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);
|
||||
}
|
||||
|
||||
/// A frame is open with the line idle high: a byte may be waiting on finalize().
|
||||
bool pending() const { return this->bit_ != RX_IDLE && this->last_level_; }
|
||||
|
||||
/// Unlocked check whether the pending byte's tail has elapsed.
|
||||
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 byte, dropping it when 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 of high line needed to finish the frame through 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` bits at `level` into the frame; 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) {
|
||||
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; low 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};
|
||||
/// Next frame bit (data, 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
|
||||
@@ -15,6 +15,11 @@
|
||||
namespace esphome::uart {
|
||||
|
||||
static const char *const TAG = "uart";
|
||||
// Edge decoder up to this baud rate, start bit sampler above it. The cutoff is
|
||||
// a deliberate tradeoff: the decoder tolerates ~0.25 bit of ISR latency jitter
|
||||
// (~6.5us at 38400), too tight for higher rates where the sampler's whole byte
|
||||
// ISR block is short anyway.
|
||||
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() {
|
||||
@@ -157,6 +162,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();
|
||||
}
|
||||
@@ -232,8 +241,10 @@ 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: detach the ISR before touching its state.
|
||||
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;
|
||||
@@ -247,8 +258,22 @@ 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_size_ != rx_buffer_size) {
|
||||
delete[] this->rx_buffer_; // NOLINT
|
||||
this->rx_buffer_ = nullptr;
|
||||
}
|
||||
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 {
|
||||
gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) {
|
||||
@@ -269,8 +294,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 +304,28 @@ void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) {
|
||||
wake_loop_isrsafe();
|
||||
#endif
|
||||
}
|
||||
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();
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
if (arg->rx_.on_edge(now, level))
|
||||
wake_loop_isrsafe();
|
||||
#else
|
||||
arg->rx_.on_edge(now, level);
|
||||
#endif
|
||||
}
|
||||
void ESP8266SoftwareSerial::rx_finalize_pending_() {
|
||||
if (!this->rx_.finalize_due(arch_get_cpu_cycle_count())) {
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
// Byte not old enough yet: re-run the loop once the buffer is drained.
|
||||
if (this->rx_.available() == 0)
|
||||
wake_loop_threadsafe();
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
InterruptLock lock;
|
||||
this->rx_.finalize(arch_get_cpu_cycle_count());
|
||||
}
|
||||
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,28 +375,19 @@ void IRAM_ATTR ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const
|
||||
this->wait_(wait, start);
|
||||
}
|
||||
uint8_t ESP8266SoftwareSerial::read_byte() {
|
||||
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;
|
||||
this->rx_sync_();
|
||||
return this->rx_.read_byte();
|
||||
}
|
||||
uint8_t ESP8266SoftwareSerial::peek_byte() {
|
||||
if (this->rx_in_pos_ == this->rx_out_pos_)
|
||||
return 0;
|
||||
return this->rx_buffer_[this->rx_out_pos_];
|
||||
this->rx_sync_();
|
||||
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() {
|
||||
// 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;
|
||||
this->rx_sync_();
|
||||
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 {
|
||||
@@ -26,24 +27,34 @@ 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);
|
||||
|
||||
/// 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_.pending())
|
||||
this->rx_finalize_pending_();
|
||||
}
|
||||
|
||||
// Members ordered largest to smallest to minimize padding
|
||||
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};
|
||||
uint8_t stop_bits_;
|
||||
uint8_t data_bits_;
|
||||
UARTParityOptions parity_;
|
||||
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_;
|
||||
uint8_t stop_bits_;
|
||||
uint8_t data_bits_;
|
||||
};
|
||||
|
||||
class ESP8266UartComponent final : public UARTComponent, public Component {
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
#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;
|
||||
|
||||
// Simulated line: frames become edges at ideal cycle counts plus jitter;
|
||||
// poll() emulates the main loop (finalize pending byte, drain 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 at `start` with per edge `jitter` in cycles; returns the end cycle.
|
||||
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) {
|
||||
// A quarter bit per edge keeps each run within the half bit rounding budget.
|
||||
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;
|
||||
// 0xFF: start bit, then the line stays high with no closing edge.
|
||||
sim.edge(t, false);
|
||||
sim.edge(t + bit, true);
|
||||
EXPECT_TRUE(sim.decoder().pending());
|
||||
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: lose the rising edge of bit 4, so the next edge repeats the last level.
|
||||
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, SetupAgainDropsStaleStateAndUsesNewBufferAndFraming) {
|
||||
// Mirrors load_settings(): buffered 8N1 bytes and an open frame, then setup() as 5E2.
|
||||
LineSim sim(9600, 8, false, false, 1);
|
||||
uint32_t t = 5000;
|
||||
for (int n = 0; n < 10; n++)
|
||||
t = sim.send(static_cast<uint8_t>(0x40 + n), t);
|
||||
sim.edge(t + 8 * sim.bit_cycles(), false); // open a frame, never closed
|
||||
SoftwareSerialRxDecoder &dec = sim.decoder();
|
||||
ASSERT_GE(dec.available(), 9u);
|
||||
|
||||
std::vector<uint8_t> small(4, 0xEE);
|
||||
const uint32_t bit = CPU_HZ / 2400;
|
||||
dec.setup(bit, 5, true, 2, small.data(), small.size());
|
||||
EXPECT_EQ(dec.available(), 0u);
|
||||
EXPECT_FALSE(dec.pending());
|
||||
EXPECT_EQ(dec.read_byte(), 0);
|
||||
|
||||
// Capacity of the 4 byte buffer is 3; the spare slot must stay untouched.
|
||||
auto send_5e2 = [&](uint8_t value, uint32_t start) {
|
||||
bool line = true;
|
||||
uint32_t at = start;
|
||||
auto put = [&](bool b) {
|
||||
if (b != line) {
|
||||
dec.on_edge(at, b);
|
||||
line = b;
|
||||
}
|
||||
at += bit;
|
||||
};
|
||||
put(false);
|
||||
int ones = 0;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
bool b = (value >> i) & 1;
|
||||
ones += b;
|
||||
put(b);
|
||||
}
|
||||
put(ones & 1);
|
||||
put(true);
|
||||
put(true);
|
||||
return at;
|
||||
};
|
||||
uint32_t t2 = 5000;
|
||||
for (int n = 1; n <= 6; n++)
|
||||
t2 = send_5e2(static_cast<uint8_t>(n), t2);
|
||||
dec.finalize(t2 + 20 * bit);
|
||||
ASSERT_EQ(dec.available(), 3u);
|
||||
EXPECT_EQ(dec.read_byte(), 1);
|
||||
EXPECT_EQ(dec.read_byte(), 2);
|
||||
EXPECT_EQ(dec.read_byte(), 3);
|
||||
EXPECT_EQ(small[3], 0xEE); // capacity slot is never written
|
||||
}
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user