#pragma once #include #include #include "esphome/core/helpers.h" namespace esphome::uart { /// Software serial RX decoder: on_edge() counts bits from the time between edges; /// finalize() completes a byte whose idle high tail has no closing edge. /// Times are in any steady tick unit. 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_ticks, uint8_t data_bits, bool parity, uint8_t stop_bits, uint8_t *buffer, size_t buffer_size) { this->bit_ticks_ = bit_ticks; this->data_bits_ = data_bits; this->stop_bit_ = data_bits + (parity ? 1 : 0); this->max_run_ticks_ = bit_ticks * (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. True to wake the loop: a byte was pushed, or the line went high in an /// open frame (a data 1 and the idle tail look alike) so finalize() may be needed. bool ESPHOME_ALWAYS_INLINE on_edge(uint32_t now, bool level) { const bool last_level = this->last_level_; // Collapsed edge if (level == last_level) return false; // LX106 has no divider, so count uint32_t delta = now - this->last_edge_; if (delta > this->max_run_ticks_) delta = this->max_run_ticks_; delta += this->bit_ticks_ / 2; uint32_t bits = 0; while (delta >= this->bit_ticks_) { delta -= this->bit_ticks_; 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); } bool pending() const { return this->bit_ != RX_IDLE && this->last_level_; } 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_ticks_(bit); } /// Call with the ISR masked. void finalize(uint32_t now) { if (!this->last_level_ || !this->finalize_due(now)) return; // Remaining bits are all ones const uint8_t bit = this->bit_; uint8_t cur = this->cur_byte_; if (bit < this->data_bits_) cur |= ((1U << this->data_bits_) - 1) & ~((1U << bit) - 1); this->push_byte(cur); this->bit_ = RX_IDLE; } /// Drops the byte when full; holds buffer_size - 1 bytes. 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; } uint8_t *buffer() const { return this->buffer_; } size_t buffer_size() const { return this->buffer_size_; } 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: uint32_t tail_ticks_(uint8_t bit) const { return (this->stop_bit_ + 1 - bit) * this->bit_ticks_ + this->bit_ticks_ / 2; } 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, not checked bit++; bits--; } else { // Stop bit; low is a framing error 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_ticks_{0}; uint32_t max_run_ticks_{0}; volatile uint32_t last_edge_{0}; uint8_t *buffer_{nullptr}; size_t buffer_size_{0}; volatile size_t in_pos_{0}; size_t out_pos_{0}; // reader only /// 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