#ifdef USE_ESP8266 #include "uart_component_esp8266.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_UART_WAKE_LOOP_ON_RX #include "esphome/core/wake.h" #endif #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" #endif 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() { uint32_t config = 0; if (this->parity_ == UART_CONFIG_PARITY_NONE) { config |= UART_PARITY_NONE; } else if (this->parity_ == UART_CONFIG_PARITY_EVEN) { config |= UART_PARITY_EVEN; } else if (this->parity_ == UART_CONFIG_PARITY_ODD) { config |= UART_PARITY_ODD; } switch (this->data_bits_) { case 5: config |= UART_NB_BIT_5; break; case 6: config |= UART_NB_BIT_6; break; case 7: config |= UART_NB_BIT_7; break; case 8: config |= UART_NB_BIT_8; break; } if (this->stop_bits_ == 1) { config |= UART_NB_STOP_BIT_1; } else { config |= UART_NB_STOP_BIT_2; } if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) config |= BIT(22); if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) config |= BIT(19); return config; } void ESP8266UartComponent::setup() { auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; } const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { pin->setup(); } }; setup_pin_if_needed(this->rx_pin_); if (this->rx_pin_ != this->tx_pin_) { setup_pin_if_needed(this->tx_pin_); } // Use Arduino HardwareSerial UARTs if all used pins match the ones // preconfigured by the platform. For example if RX disabled but TX pin // is 1 we still want to use Serial. SerialConfig config = static_cast(get_config()); #ifdef USE_ESP8266_UART_SERIAL if (!ESP8266UartComponent::serial0_in_use && (tx_pin_ == nullptr || tx_pin_->get_pin() == 1) && (rx_pin_ == nullptr || rx_pin_->get_pin() == 3) #ifdef USE_LOGGER // we will use UART0 if logger isn't using it in swapped mode && (logger::global_logger->get_hw_serial() == nullptr || logger::global_logger->get_uart() != logger::UART_SELECTION_UART0_SWAP) #endif ) { this->hw_serial_ = &Serial; this->hw_serial_->begin(this->baud_rate_, config); this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); ESP8266UartComponent::serial0_in_use = true; } else if (!ESP8266UartComponent::serial0_in_use && (tx_pin_ == nullptr || tx_pin_->get_pin() == 15) && (rx_pin_ == nullptr || rx_pin_->get_pin() == 13) #ifdef USE_LOGGER // we will use UART0 swapped if logger isn't using it in regular mode && (logger::global_logger->get_hw_serial() == nullptr || logger::global_logger->get_uart() != logger::UART_SELECTION_UART0) #endif ) { this->hw_serial_ = &Serial; this->hw_serial_->begin(this->baud_rate_, config); this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); this->hw_serial_->swap(); ESP8266UartComponent::serial0_in_use = true; } else #endif // USE_ESP8266_UART_SERIAL #ifdef USE_ESP8266_UART_SERIAL1 if ((tx_pin_ == nullptr || tx_pin_->get_pin() == 2) && (rx_pin_ == nullptr || rx_pin_->get_pin() == 8)) { this->hw_serial_ = &Serial1; this->hw_serial_->begin(this->baud_rate_, config); this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); } else #endif // USE_ESP8266_UART_SERIAL1 { this->sw_serial_ = new ESP8266SoftwareSerial(); // NOLINT this->sw_serial_->setup(tx_pin_, rx_pin_, this->baud_rate_, this->stop_bits_, this->data_bits_, this->parity_, this->rx_buffer_size_); } } void ESP8266UartComponent::load_settings(bool dump_config) { ESP_LOGCONFIG(TAG, "Loading UART bus settings"); if (this->hw_serial_ != nullptr) { SerialConfig config = static_cast(get_config()); this->hw_serial_->begin(this->baud_rate_, config); this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); } else { this->sw_serial_->setup(this->tx_pin_, this->rx_pin_, this->baud_rate_, this->stop_bits_, this->data_bits_, this->parity_, this->rx_buffer_size_); } if (dump_config) { ESP_LOGCONFIG(TAG, "UART bus was reloaded."); this->dump_config(); } } void ESP8266UartComponent::dump_config() { ESP_LOGCONFIG(TAG, "UART Bus:"); LOG_PIN(" TX Pin: ", this->tx_pin_); LOG_PIN(" RX Pin: ", this->rx_pin_); if (this->rx_pin_ != nullptr) { ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); // NOLINT } ESP_LOGCONFIG(TAG, " Baud Rate: %u baud\n" " Data Bits: %u\n" " Parity: %s\n" " Stop bits: %u", this->baud_rate_, this->data_bits_, LOG_STR_ARG(parity_to_str(this->parity_)), this->stop_bits_); if (this->hw_serial_ != nullptr) { ESP_LOGCONFIG(TAG, " Using hardware serial interface."); } else { ESP_LOGCONFIG(TAG, " Using software serial" #ifdef USE_UART_WAKE_LOOP_ON_RX "\n Wake on data RX: ENABLED" #endif ); } this->check_logger_conflict(); } void ESP8266UartComponent::check_logger_conflict() { #ifdef USE_LOGGER if (this->hw_serial_ == nullptr || logger::global_logger->get_baud_rate() == 0) { return; } if (this->hw_serial_ == logger::global_logger->get_hw_serial()) { ESP_LOGW(TAG, " You're using the same serial port for logging and the UART component. Please " "disable logging over the serial port by setting logger->baud_rate to 0."); } #endif } void ESP8266UartComponent::write_array(const uint8_t *data, size_t len) { if (this->hw_serial_ != nullptr) { this->hw_serial_->write(data, len); } else { for (size_t i = 0; i < len; i++) this->sw_serial_->write_byte(data[i]); } #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_TX, data[i]); } #endif } bool ESP8266UartComponent::peek_byte(uint8_t *data) { if (!this->check_read_timeout_()) return false; if (this->hw_serial_ != nullptr) { *data = this->hw_serial_->peek(); } else { *data = this->sw_serial_->peek_byte(); } return true; } bool ESP8266UartComponent::read_array(uint8_t *data, size_t len) { if (!this->check_read_timeout_(len)) return false; if (this->hw_serial_ != nullptr) { this->hw_serial_->readBytes(data, len); } else { for (size_t i = 0; i < len; i++) data[i] = this->sw_serial_->read_byte(); } #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_RX, data[i]); } #endif return true; } size_t ESP8266UartComponent::available() { if (this->hw_serial_ != nullptr) { return this->hw_serial_->available(); } else { return this->sw_serial_->available(); } } UARTFlushResult ESP8266UartComponent::flush() { ESP_LOGVV(TAG, " Flushing"); if (this->hw_serial_ != nullptr) { this->hw_serial_->flush(); } else { this->sw_serial_->flush(); } return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } 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) { 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(); tx_pin_ = gpio_tx_pin_->to_isr(); tx_pin_.digital_write(true); } if (rx_pin != nullptr) { 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 } // 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(); uint8_t rec = 0; // Manually unroll the loop for (int i = 0; i < arg->data_bits_; i++) rec |= arg->read_bit_(&wait, start) << i; /* If parity is enabled, just read it and ignore it. */ /* TODO: Should we check parity? Or is it too slow for nothing added..*/ if (arg->parity_ == UART_CONFIG_PARITY_EVEN || arg->parity_ == UART_CONFIG_PARITY_ODD) arg->read_bit_(&wait, start); // Stop bit arg->wait_(&wait, start); if (arg->stop_bits_ == 2) arg->wait_(&wait, start); 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 // Wake the main loop so the consuming component drains the byte promptly // instead of waiting for the next loop_interval_ tick. Important for timing // sensitive setups that poll read() in a tight loop (e.g. fingerprint_grow). 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!"); return; } bool parity_bit = false; bool need_parity_bit = true; if (this->parity_ == UART_CONFIG_PARITY_EVEN) { parity_bit = false; } else if (this->parity_ == UART_CONFIG_PARITY_ODD) { parity_bit = true; } else { need_parity_bit = false; } { InterruptLock lock; uint32_t wait = this->bit_time_; const uint32_t start = arch_get_cpu_cycle_count(); // Start bit this->write_bit_(false, &wait, start); for (int i = 0; i < this->data_bits_; i++) { bool bit = data & (1 << i); this->write_bit_(bit, &wait, start); if (need_parity_bit) parity_bit ^= bit; } if (need_parity_bit) this->write_bit_(parity_bit, &wait, start); // Stop bit this->write_bit_(true, &wait, start); if (this->stop_bits_ == 2) this->wait_(&wait, start); } } void IRAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) { while (arch_get_cpu_cycle_count() - start < *wait) ; *wait += this->bit_time_; } bool IRAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start) { this->wait_(wait, start); return this->rx_pin_.digital_read(); } void IRAM_ATTR ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const uint32_t &start) { this->tx_pin_.digital_write(bit); 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_]; this->rx_out_pos_ = (this->rx_out_pos_ + 1) % this->rx_buffer_size_; 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_]; } 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; } } // namespace esphome::uart #endif // USE_ESP8266