mirror of
https://github.com/esphome/esphome.git
synced 2026-08-30 17:46:01 +00:00
[uart] Stop ESP8266 software serial RX ISR from blocking for a whole byte
This commit is contained in:
@@ -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_;
|
||||
|
||||
Reference in New Issue
Block a user