diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index 1cf78fdbd5..7b9c320879 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS +from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant from esphome.components.uart import CONF_DEBUG_PREFIX, CONF_FLUSH_TIMEOUT, UARTComponent from esphome.components.usb_host import ( get_max_packet_size, @@ -15,6 +16,7 @@ from esphome.const import ( CONF_DUMMY_RECEIVER, CONF_ID, ) +from esphome.core import CORE from esphome.cpp_types import Component AUTO_LOAD = ["uart", "usb_host", "bytebuffer"] @@ -55,16 +57,24 @@ class Type: uart_types = ( - Type("CH34X", 0x1A86, 0x55D5, "CH34X", 3), - Type("CH340", 0x1A86, 0x7523, "CH34X", 1), - Type("ESP_JTAG", 0x303A, 0x1001, "CdcAcm", 1, baud_rate_required=False), - Type("STM32_VCP", 0x0483, 0x5740, "CdcAcm", 1, baud_rate_required=False), Type("CDC_ACM", 0, 0, "CdcAcm", 1, baud_rate_required=False), Type("CP210X", 0x10C4, 0xEA60, "CP210X", 3), + Type("CH34X", 0x1A86, 0x55D5, "CH34X", 4), + Type("CH340", 0x1A86, 0x7523, "CH34X", 1), + Type("ESP_JTAG", 0x303A, 0x1001, "CdcAcm", 1, baud_rate_required=False), + Type("FT232", 0x0403, 0x6001, "FT23XX", 1), + Type("FT2232", 0x0403, 0x6010, "FT23XX", 2), + Type("FT4232", 0x0403, 0x6011, "FT23XX", 4), + Type("STM32_VCP", 0x0483, 0x5740, "CdcAcm", 1, baud_rate_required=False), ) def channel_schema(channels, baud_rate_required): + # For now S3 is restricted to 3 channels since each needs 2 endpoints, plus the control endpoint, and + # there are only a total of 8 endpoints available. + # This will need updating when the 8 channel devices that multiplex over an endpoint are added. + if CORE.is_esp32 and get_esp32_variant() != VARIANT_ESP32P4 and channels > 3: + channels = 3 return cv.Schema( { cv.Required(CONF_CHANNELS): cv.All( diff --git a/esphome/components/usb_uart/ft23xx.cpp b/esphome/components/usb_uart/ft23xx.cpp new file mode 100644 index 0000000000..d57d7fe3bd --- /dev/null +++ b/esphome/components/usb_uart/ft23xx.cpp @@ -0,0 +1,454 @@ +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) +#include "usb_uart.h" +#include "usb/usb_host.h" +#include "esphome/core/log.h" +#include "esphome/components/uart/uart_debugger.h" + +#include "esphome/components/bytebuffer/bytebuffer.h" + +namespace esphome::usb_uart { + +using namespace bytebuffer; + +// FTDI chip family identifiers. These map to USB device bcdDevice values +// and determine how baudrate divisors and clock sources are calculated. +enum ftdi_chip_type { + TYPE_AM = 0, + TYPE_BM = 1, + TYPE_2232C = 2, + TYPE_R = 3, + TYPE_2232H = 4, + TYPE_4232H = 5, + TYPE_232H = 6, + TYPE_230X = 7, +}; + +static int ftdi_to_clkbits_AM(int baudrate, unsigned long *encoded_divisor) { + static const char frac_code[8] = {0, 3, 2, 4, 1, 5, 6, 7}; + static const char am_adjust_up[8] = {0, 0, 0, 1, 0, 3, 2, 1}; + static const char am_adjust_dn[8] = {0, 0, 0, 1, 0, 1, 2, 3}; + int divisor, best_divisor, best_baud, best_baud_diff; + int i; + divisor = 24000000 / baudrate; + + divisor -= am_adjust_dn[divisor & 7]; + + best_divisor = 0; + best_baud = 0; + best_baud_diff = 0; + for (i = 0; i < 2; i++) { + int try_divisor = divisor + i; + int baud_estimate; + int baud_diff; + + if (try_divisor <= 8) { + try_divisor = 8; + } else if (divisor < 16) { + try_divisor = 16; + } else { + try_divisor += am_adjust_up[try_divisor & 7]; + if (try_divisor > 0x1FFF8) { + // Round down to maximum supported divisor value (for AM) + try_divisor = 0x1FFF8; + } + } + baud_estimate = (24000000 + (try_divisor / 2)) / try_divisor; + if (baud_estimate < baudrate) { + baud_diff = baudrate - baud_estimate; + } else { + baud_diff = baud_estimate - baudrate; + } + if (i == 0 || baud_diff < best_baud_diff) { + best_divisor = try_divisor; + best_baud = baud_estimate; + best_baud_diff = baud_diff; + if (baud_diff == 0) { + break; + } + } + } + *encoded_divisor = (best_divisor >> 3) | (frac_code[best_divisor & 7] << 14); + if (*encoded_divisor == 1) { + *encoded_divisor = 0; // 3000000 baud + } else if (*encoded_divisor == 0x4001) { + *encoded_divisor = 1; // 2000000 baud (BM only) + } + return best_baud; +} + +static int ftdi_to_clkbits(int baudrate, unsigned int clk, int clk_div, unsigned long *encoded_divisor) { + static const char frac_code[8] = {0, 3, 2, 4, 1, 5, 6, 7}; + int best_baud = 0; + int divisor, best_divisor; + if (baudrate >= clk / clk_div) { + *encoded_divisor = 0; + best_baud = clk / clk_div; + } else if (baudrate >= clk / (clk_div + clk_div / 2)) { + *encoded_divisor = 1; + best_baud = clk / (clk_div + clk_div / 2); + } else if (baudrate >= clk / (2 * clk_div)) { + *encoded_divisor = 2; + best_baud = clk / (2 * clk_div); + } else { + divisor = clk * 16 / clk_div / baudrate; + if (divisor & 1) + best_divisor = divisor / 2 + 1; + else + best_divisor = divisor / 2; + if (best_divisor > 0x20000) + best_divisor = 0x1ffff; + best_baud = clk * 16 / clk_div / best_divisor; + if (best_baud & 1) + best_baud = best_baud / 2 + 1; + else + best_baud = best_baud / 2; + *encoded_divisor = (best_divisor >> 3) | (frac_code[best_divisor & 0x7] << 14); + } + return best_baud; +} + +static int ftdi_convert_baudrate(int baudrate, uint8_t chip_type, uint8_t channel_index, unsigned short *value, + unsigned short *index) { + int best_baud; + unsigned long encoded_divisor; + + if (baudrate <= 0) { + return -1; + } + + static constexpr uint32_t H_CLK = 120000000; + static constexpr uint32_t C_CLK = 48000000; + if ((chip_type == TYPE_2232H) || (chip_type == TYPE_4232H) || (chip_type == TYPE_232H)) { + if (baudrate * 10 > H_CLK / 0x3fff) { + best_baud = ftdi_to_clkbits(baudrate, H_CLK, 10, &encoded_divisor); + encoded_divisor |= 0x20000; /* switch on CLK/10*/ + } else + best_baud = ftdi_to_clkbits(baudrate, C_CLK, 16, &encoded_divisor); + } else if ((chip_type == TYPE_BM) || (chip_type == TYPE_2232C) || (chip_type == TYPE_R) || (chip_type == TYPE_230X)) { + best_baud = ftdi_to_clkbits(baudrate, C_CLK, 16, &encoded_divisor); + } else { + best_baud = ftdi_to_clkbits_AM(baudrate, &encoded_divisor); + } + + *value = (unsigned short) (encoded_divisor & 0xFFFF); + if (chip_type == TYPE_2232H || chip_type == TYPE_4232H || chip_type == TYPE_232H) { + *index = (unsigned short) (encoded_divisor >> 8); + *index &= 0xFF00; + *index |= (channel_index + 1); + } else + *index = (unsigned short) (encoded_divisor >> 16); + + return best_baud; +} + +static optional get_uart(const usb_config_desc_t *config_desc, uint8_t intf_idx) { + int conf_offset, ep_offset; + CdcEps eps{}; + + const auto *intf_desc = usb_parse_interface_descriptor(config_desc, intf_idx, 0, &conf_offset); + if (!intf_desc) { + ESP_LOGD(TAG, "usb_parse_interface_descriptor failed for intf_idx=%d (end of interfaces)", intf_idx); + return nullopt; + } + ESP_LOGD(TAG, + "intf_desc [idx=%d]: bInterfaceClass=%02X, bInterfaceSubClass=%02X, bInterfaceProtocol=%02X, " + "bNumEndpoints=%d, bInterfaceNumber=%d", + intf_idx, intf_desc->bInterfaceClass, intf_desc->bInterfaceSubClass, intf_desc->bInterfaceProtocol, + intf_desc->bNumEndpoints, intf_desc->bInterfaceNumber); + + std::vector endpoints; + for (uint8_t i = 0; i != intf_desc->bNumEndpoints; i++) { + ep_offset = conf_offset; + const auto *ep = usb_parse_endpoint_descriptor_by_index(intf_desc, i, config_desc->wTotalLength, &ep_offset); + if (!ep) { + ESP_LOGE(TAG, "Ran out of endpoints at %d before finding all %d endpoints", i, intf_desc->bNumEndpoints); + return nullopt; + } + ESP_LOGD(TAG, "ep: bEndpointAddress=%02X, bmAttributes=%02X", ep->bEndpointAddress, ep->bmAttributes); + + if (ep->bmAttributes != 0x2) { + ESP_LOGD(TAG, "Skipping non-bulk endpoint: %02X", ep->bEndpointAddress); + continue; + } + endpoints.push_back(ep); + } + + const usb_ep_desc_t *ep1 = nullptr; + const usb_ep_desc_t *ep2 = nullptr; + for (const auto *ep : endpoints) { + if (ep1 == nullptr) { + ep1 = ep; + } else if (ep2 == nullptr) { + ep2 = ep; + break; + } + } + + if (ep1 == nullptr || ep2 == nullptr) { + ESP_LOGD(TAG, "Interface %d has %zu endpoints (need 2 bulk endpoints)", intf_idx, endpoints.size()); + return nullopt; + } + + ESP_LOGD(TAG, "Interface %d: ep1=0x%02X, ep2=0x%02X", intf_idx, ep1->bEndpointAddress, ep2->bEndpointAddress); + + if (ep1->bEndpointAddress & usb_host::USB_DIR_IN) { + eps.in_ep = ep1; + eps.out_ep = ep2; + ESP_LOGD(TAG, "ep1 is IN (RX): ep1=0x%02X (in_ep), ep2=0x%02X (out_ep)", ep1->bEndpointAddress, + ep2->bEndpointAddress); + } else { + eps.out_ep = ep1; + eps.in_ep = ep2; + ESP_LOGD(TAG, "ep1 is OUT (TX): ep1=0x%02X (out_ep), ep2=0x%02X (in_ep)", ep1->bEndpointAddress, + ep2->bEndpointAddress); + } + + eps.bulk_interface_number = intf_desc->bInterfaceNumber; + return eps; +} + +std::vector USBUartTypeFT23XX::parse_descriptors(usb_device_handle_t dev_hdl) { + const usb_config_desc_t *config_desc; + const usb_device_desc_t *device_desc; + std::vector cdc_devs{}; + std::string type_string; + + if (usb_host_get_device_descriptor(dev_hdl, &device_desc) != ESP_OK) { + ESP_LOGE(TAG, "get_device_descriptor failed"); + return {}; + } + if (usb_host_get_active_config_descriptor(dev_hdl, &config_desc) != ESP_OK) { + ESP_LOGE(TAG, "get_active_config_descriptor failed"); + return {}; + } + if (device_desc->bcdDevice == 0x400 || (device_desc->bcdDevice == 0x200 && device_desc->iSerialNumber == 0)) { + this->chip_type_ = TYPE_BM; + type_string = "BM type chip"; + } else if (device_desc->bcdDevice == 0x200) { + this->chip_type_ = TYPE_AM; + type_string = "AM type chip"; + } else if (device_desc->bcdDevice == 0x500) { + this->chip_type_ = TYPE_2232C; + type_string = "2232C chip"; + } else if (device_desc->bcdDevice == 0x600) { + this->chip_type_ = TYPE_R; + type_string = "type R chip"; + } else if (device_desc->bcdDevice == 0x700) { + this->chip_type_ = TYPE_2232H; + type_string = "2232H chip"; + } else if (device_desc->bcdDevice == 0x800) { + this->chip_type_ = TYPE_4232H; + type_string = "4232H chip"; + } else if (device_desc->bcdDevice == 0x900) { + this->chip_type_ = TYPE_232H; + type_string = "232H type chip"; + } else if (device_desc->bcdDevice == 0x1000) { + this->chip_type_ = TYPE_230X; + type_string = "230x chip"; + } + + ESP_LOGD(TAG, "Found FTDI %s based device", type_string.c_str()); + for (uint8_t intf_idx = 0; intf_idx < this->channels_.size(); intf_idx++) { + if (auto eps = get_uart(config_desc, intf_idx)) { + cdc_devs.push_back(*eps); + ESP_LOGD(TAG, "Found CDC interface at USB interface index %d", intf_idx); + } + } + return cdc_devs; +} + +int USBUartTypeFT23XX::reset(USBUartChannel *channel) { + usb_host::transfer_cb_t callback = [=, this](const usb_host::TransferStatus &status) { + if (!status.success) { + ESP_LOGE(TAG, "Reset failed, status=%s", esp_err_to_name(status.error_code)); + channel->initialised_.store(false); + } else { + ESP_LOGD(TAG, "Reset successful, setting baudrate..."); + this->set_baudrate(channel); + } + }; + bool ok = this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, 0x00, 0x00, + channel->cdc_dev_.bulk_interface_number + 1, callback); + if (!ok) { + ESP_LOGE(TAG, "Reset control_transfer submit failed"); + channel->initialised_.store(false); + return -1; + } + return 0; +} + +int USBUartTypeFT23XX::set_baudrate(USBUartChannel *channel, uint32_t baudrate) { + usb_host::transfer_cb_t callback = [=, this](const usb_host::TransferStatus &status) { + if (!status.success) { + ESP_LOGE(TAG, "Set baudrate failed, status=%s", esp_err_to_name(status.error_code)); + channel->initialised_.store(false); + } else { + ESP_LOGD(TAG, "Baudrate %d set, setting line properties...", channel->baud_rate_); + this->set_line_properties(channel); + } + }; + if (baudrate == 0) { + baudrate = channel->baud_rate_; + } + unsigned short value, ftdi_index; + ftdi_convert_baudrate(baudrate, this->chip_type_, channel->index_, &value, &ftdi_index); + ESP_LOGD(TAG, "Baudrate: %d, value=0x%04X, ftdi_index=0x%04X", baudrate, value, ftdi_index); + uint16_t usb_index = (ftdi_index & 0xFF00) | (channel->cdc_dev_.bulk_interface_number + 1); + bool ok = this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, 0x03, value, usb_index, callback); + if (!ok) { + ESP_LOGE(TAG, "Set baudrate control_transfer submit failed"); + channel->initialised_.store(false); + return -1; + } + return 0; +} + +int USBUartTypeFT23XX::set_line_properties(USBUartChannel *channel) { + usb_host::transfer_cb_t callback = [=, this](const usb_host::TransferStatus &status) { + if (!status.success) { + ESP_LOGE(TAG, "Set line properties failed, status=%s", esp_err_to_name(status.error_code)); + channel->initialised_.store(false); + return; + } + ESP_LOGD(TAG, "Line properties set, setting modem control..."); + this->set_dtr_rts(channel); + }; + + unsigned short value = channel->data_bits_; + + switch (channel->parity_) { + case UART_CONFIG_PARITY_NONE: + value |= (0x00 << 8); + break; + case UART_CONFIG_PARITY_ODD: + value |= (0x01 << 8); + break; + case UART_CONFIG_PARITY_EVEN: + value |= (0x02 << 8); + break; + case UART_CONFIG_PARITY_MARK: + value |= (0x03 << 8); + break; + case UART_CONFIG_PARITY_SPACE: + value |= (0x04 << 8); + break; + } + + switch (channel->stop_bits_) { + case UART_CONFIG_STOP_BITS_1: + value |= (0x00 << 11); + break; + case UART_CONFIG_STOP_BITS_1_5: + value |= (0x01 << 11); + break; + case UART_CONFIG_STOP_BITS_2: + value |= (0x02 << 11); + break; + } + + value |= (0x00 << 14); + + bool ok = this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, 0x04, value, + channel->cdc_dev_.bulk_interface_number + 1, callback); + if (!ok) { + ESP_LOGE(TAG, "Set line properties control_transfer submit failed"); + channel->initialised_.store(false); + return -1; + } + return 0; +} + +int USBUartTypeFT23XX::set_dtr_rts(USBUartChannel *channel) { + usb_host::transfer_cb_t callback = [=, this](const usb_host::TransferStatus &status) { + if (!status.success) { + ESP_LOGE(TAG, "Set modem control failed, status=%s", esp_err_to_name(status.error_code)); + channel->initialised_.store(false); + return; + } + ESP_LOGD(TAG, "Modem control set for channel %d, starting input...", channel->index_); + channel->initialised_.store(true); + this->start_input(channel); + uint8_t next_index = channel->index_ + 1; + if (next_index < this->channels_.size()) { + USBUartChannel *next_channel = this->channels_[next_index]; + ESP_LOGD(TAG, "Configuring next channel %d", next_channel->index_); + this->reset(next_channel); + return; + } else { + ESP_LOGI(TAG, "All channels configured"); + } + }; + + bool ok = this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, 0x01, 0x0000, + channel->cdc_dev_.bulk_interface_number + 1, callback); + if (!ok) { + ESP_LOGE(TAG, "Set modem control control_transfer submit failed"); + channel->initialised_.store(false); + return -1; + } + return 0; +} + +void USBUartTypeFT23XX::start_input(USBUartChannel *channel) { + if (!channel->initialised_.load() || channel->input_started_.load()) + return; + + const auto *ep = channel->cdc_dev_.in_ep; + + auto callback = [this, channel](const usb_host::TransferStatus &status) { + if (!status.success) { + ESP_LOGE(TAG, "RX Transfer failed, status=%s", esp_err_to_name(status.error_code)); + channel->input_started_.store(false); + return; + } + + size_t uart_data_len = (status.data_len > 2) ? (status.data_len - 2) : 0; + + if (uart_data_len > 0) { + ESP_LOGV(TAG, "RX callback: Received %zu bytes, channel=%d", uart_data_len, channel->index_); + if (!channel->dummy_receiver_) { + // Copy the entire received UART payload into the ring buffer in one + // operation to avoid per-byte overhead and reduce the chance of + // heap activity in hot paths. + channel->input_buffer_.push(status.data + 2, uart_data_len); +#ifdef USE_UART_DEBUGGER + if (channel->debug_) { + // Debug path creates a temporary vector for logging only; this is + // acceptable because debug mode is opt-in and not used in release. + uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX, + std::vector(status.data + 2, status.data + 2 + uart_data_len), ',', + channel->debug_prefix_); + } +#endif + } + } else { + ESP_LOGVV(TAG, "RX: Status packet, modem=0x%02X line=0x%02X, ch=%d", status.data[0], status.data[1], + channel->index_); + } + + channel->input_started_.store(false); + if (channel->dummy_receiver_ || + channel->input_buffer_.get_free_space() >= channel->cdc_dev_.in_ep->wMaxPacketSize) { + this->start_input(channel); + } + }; + + channel->input_started_.store(true); + this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize); +} + +void USBUartTypeFT23XX::enable_channels() { + if (!this->channels_.empty() && this->channels_[0]->initialised_.load()) { + this->reset(this->channels_[0]); + } + + for (auto *channel : this->channels_) { + if (!channel->initialised_.load()) + continue; + channel->input_started_.store(false); + channel->output_started_.store(false); + } +} + +} // namespace esphome::usb_uart +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32P4 diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index fb8425f6cd..7a19aa8e4b 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -129,6 +129,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented parse_descriptors(usb_device_handle_t dev_hdl) override; + void enable_channels() override; + + int reset(USBUartChannel *channel); + int set_baudrate(USBUartChannel *channel, uint32_t baudrate = 0); + int set_line_properties(USBUartChannel *channel); + int set_dtr_rts(USBUartChannel *channel); + + uint8_t chip_type_{255}; +}; + } // namespace esphome::usb_uart #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/tests/components/usb_uart/common.yaml b/tests/components/usb_uart/common.yaml index 5869b9468b..c8c1ee7df2 100644 --- a/tests/components/usb_uart/common.yaml +++ b/tests/components/usb_uart/common.yaml @@ -42,3 +42,13 @@ usb_uart: baud_rate: 9600 debug: true debug_prefix: "[CP210X] " + - id: uart_6 + type: ft2232 + channels: + - id: channel_6_1 + baud_rate: 115200 + - id: channel_6_2 + baud_rate: 9600 + stop_bits: 2 + data_bits: 7 + parity: odd