mirror of
https://github.com/esphome/esphome.git
synced 2026-11-03 11:47:38 +00:00
180 lines
5.2 KiB
C++
180 lines
5.2 KiB
C++
#pragma once
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
|
|
#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
|