ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
ft23xx.cpp
Go to the documentation of this file.
1#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) || \
2 defined(USE_ESP32_VARIANT_ESP32S31) || defined(USE_ESP32_VARIANT_ESP32H4)
3#include "usb_uart.h"
4#include "usb/usb_host.h"
5#include "esphome/core/log.h"
8
10#include <cinttypes>
11
12namespace esphome::usb_uart {
13
14using namespace bytebuffer;
15
16static constexpr uint8_t SIO_SET_MODEM_CTRL = 0x01;
17static constexpr uint8_t SIO_SET_FLOW_CTRL = 0x02;
18// The high byte of wIndex selects the flow control mode, the low byte the channel
19static constexpr uint16_t SIO_DISABLE_FLOW_CTRL = 0x0000;
20static constexpr uint16_t SIO_RTS_CTS_HS = 0x0100;
21static constexpr uint16_t SIO_XON_XOFF_HS = 0x0400;
22// wValue carries the XOFF and XON characters, which the termios defaults fix as DC3 and DC1
23static constexpr uint16_t SIO_XON_XOFF_CHARS = 0x1311;
24// The high byte selects which lines the low byte changes
25static constexpr uint16_t SIO_SET_DTR_MASK = 0x0100;
26static constexpr uint16_t SIO_SET_RTS_MASK = 0x0200;
27static constexpr uint16_t SIO_DTR_HIGH = 0x0001;
28static constexpr uint16_t SIO_RTS_HIGH = 0x0002;
29
30// FTDI chip family identifiers. These map to USB device bcdDevice values
31// and determine how baudrate divisors and clock sources are calculated.
42
43static int ftdi_to_clkbits_am(int baudrate, uint32_t *encoded_divisor) {
44 static const char FRAC_CODE[8] = {0, 3, 2, 4, 1, 5, 6, 7};
45 static const char AM_ADJUST_UP[8] = {0, 0, 0, 1, 0, 3, 2, 1};
46 static const char AM_ADJUST_DN[8] = {0, 0, 0, 1, 0, 1, 2, 3};
47 int divisor, best_divisor, best_baud, best_baud_diff;
48 int i;
49 divisor = 24000000 / baudrate;
50
51 divisor -= AM_ADJUST_DN[divisor & 7];
52
53 best_divisor = 0;
54 best_baud = 0;
55 best_baud_diff = 0;
56 for (i = 0; i < 2; i++) {
57 int try_divisor = divisor + i;
58 int baud_estimate;
59 int baud_diff;
60
61 if (try_divisor <= 8) {
62 try_divisor = 8;
63 } else if (divisor < 16) {
64 try_divisor = 16;
65 } else {
66 try_divisor += AM_ADJUST_UP[try_divisor & 7];
67 if (try_divisor > 0x1FFF8) {
68 // Round down to maximum supported divisor value (for AM)
69 try_divisor = 0x1FFF8;
70 }
71 }
72 baud_estimate = (24000000 + (try_divisor / 2)) / try_divisor;
73 if (baud_estimate < baudrate) {
74 baud_diff = baudrate - baud_estimate;
75 } else {
76 baud_diff = baud_estimate - baudrate;
77 }
78 if (i == 0 || baud_diff < best_baud_diff) {
79 best_divisor = try_divisor;
80 best_baud = baud_estimate;
81 best_baud_diff = baud_diff;
82 if (baud_diff == 0) {
83 break;
84 }
85 }
86 }
87 *encoded_divisor = (best_divisor >> 3) | (FRAC_CODE[best_divisor & 7] << 14);
88 if (*encoded_divisor == 1) {
89 *encoded_divisor = 0; // 3000000 baud
90 } else if (*encoded_divisor == 0x4001) {
91 *encoded_divisor = 1; // 2000000 baud (BM only)
92 }
93 return best_baud;
94}
95
96static int ftdi_to_clkbits(int baudrate, unsigned int clk, int clk_div, uint32_t *encoded_divisor) {
97 static const char FRAC_CODE[8] = {0, 3, 2, 4, 1, 5, 6, 7};
98 int best_baud = 0;
99 int divisor, best_divisor;
100 if (baudrate >= clk / clk_div) {
101 *encoded_divisor = 0;
102 best_baud = clk / clk_div;
103 } else if (baudrate >= clk / (clk_div + clk_div / 2)) {
104 *encoded_divisor = 1;
105 best_baud = clk / (clk_div + clk_div / 2);
106 } else if (baudrate >= clk / (2 * clk_div)) {
107 *encoded_divisor = 2;
108 best_baud = clk / (2 * clk_div);
109 } else {
110 divisor = clk * 16 / clk_div / baudrate;
111 if (divisor & 1) {
112 best_divisor = divisor / 2 + 1;
113 } else {
114 best_divisor = divisor / 2;
115 }
116 if (best_divisor > 0x20000)
117 best_divisor = 0x1ffff;
118 best_baud = clk * 16 / clk_div / best_divisor;
119 if (best_baud & 1) {
120 best_baud = best_baud / 2 + 1;
121 } else {
122 best_baud = best_baud / 2;
123 }
124 *encoded_divisor = (best_divisor >> 3) | (FRAC_CODE[best_divisor & 0x7] << 14);
125 }
126 return best_baud;
127}
128
129struct FtdiConfig {
130 uint16_t value;
131 uint16_t ftdi_index;
132 int best_baud;
133};
134
135static FtdiConfig ftdi_convert_baudrate(int baudrate, uint8_t chip_type, uint8_t channel_index) {
136 uint32_t encoded_divisor;
137
138 FtdiConfig config{};
139
140 if (baudrate <= 0) {
141 return config;
142 }
143
144 static constexpr uint32_t H_CLK = 120000000;
145 static constexpr uint32_t C_CLK = 48000000;
146 if ((chip_type == TYPE_2232H) || (chip_type == TYPE_4232H) || (chip_type == TYPE_232H)) {
147 if (baudrate * 10 > H_CLK / 0x3fff) {
148 config.best_baud = ftdi_to_clkbits(baudrate, H_CLK, 10, &encoded_divisor);
149 encoded_divisor |= 0x20000; /* switch on CLK/10*/
150 } else {
151 config.best_baud = ftdi_to_clkbits(baudrate, C_CLK, 16, &encoded_divisor);
152 }
153 } else if ((chip_type == TYPE_BM) || (chip_type == TYPE_2232C) || (chip_type == TYPE_R) || (chip_type == TYPE_230X)) {
154 config.best_baud = ftdi_to_clkbits(baudrate, C_CLK, 16, &encoded_divisor);
155 } else {
156 config.best_baud = ftdi_to_clkbits_am(baudrate, &encoded_divisor);
157 }
158
159 config.value = (uint16_t) (encoded_divisor & 0xFFFF);
160 if (chip_type == TYPE_2232H || chip_type == TYPE_4232H || chip_type == TYPE_232H) {
161 config.ftdi_index = (uint16_t) (encoded_divisor >> 8);
162 config.ftdi_index &= 0xFF00;
163 config.ftdi_index |= (channel_index + 1);
164 } else {
165 config.ftdi_index = (uint16_t) (encoded_divisor >> 16);
166 }
167
168 return config;
169}
170
171static optional<CdcEps> get_uart(const usb_config_desc_t *config_desc, uint8_t intf_idx) {
172 int conf_offset, ep_offset;
173 CdcEps eps{};
174
175 const auto *intf_desc = usb_parse_interface_descriptor(config_desc, intf_idx, 0, &conf_offset);
176 if (!intf_desc) {
177 ESP_LOGD(TAG, "usb_parse_interface_descriptor failed for intf_idx=%d (end of interfaces)", intf_idx);
178 return nullopt;
179 }
180 ESP_LOGD(TAG,
181 "intf_desc [idx=%d]: bInterfaceClass=%02X, bInterfaceSubClass=%02X, bInterfaceProtocol=%02X, "
182 "bNumEndpoints=%d, bInterfaceNumber=%d",
183 intf_idx, intf_desc->bInterfaceClass, intf_desc->bInterfaceSubClass, intf_desc->bInterfaceProtocol,
184 intf_desc->bNumEndpoints, intf_desc->bInterfaceNumber);
185
186 std::vector<const usb_ep_desc_t *> endpoints;
187 for (uint8_t i = 0; i != intf_desc->bNumEndpoints; i++) {
188 ep_offset = conf_offset;
189 const auto *ep = usb_parse_endpoint_descriptor_by_index(intf_desc, i, config_desc->wTotalLength, &ep_offset);
190 if (!ep) {
191 ESP_LOGE(TAG, "Ran out of endpoints at %d before finding all %d endpoints", i, intf_desc->bNumEndpoints);
192 return nullopt;
193 }
194 ESP_LOGD(TAG, "ep: bEndpointAddress=%02X, bmAttributes=%02X", ep->bEndpointAddress, ep->bmAttributes);
195
196 if (ep->bmAttributes != 0x2) {
197 ESP_LOGD(TAG, "Skipping non-bulk endpoint: %02X", ep->bEndpointAddress);
198 continue;
199 }
200 endpoints.push_back(ep);
201 }
202
203 const usb_ep_desc_t *ep1 = nullptr;
204 const usb_ep_desc_t *ep2 = nullptr;
205 for (const auto *ep : endpoints) {
206 if (ep1 == nullptr) {
207 ep1 = ep;
208 } else if (ep2 == nullptr) {
209 ep2 = ep;
210 break;
211 }
212 }
213
214 if (ep1 == nullptr || ep2 == nullptr) {
215 ESP_LOGD(TAG, "Interface %d has %zu endpoints (need 2 bulk endpoints)", intf_idx, endpoints.size());
216 return nullopt;
217 }
218
219 ESP_LOGD(TAG, "Interface %d: ep1=0x%02X, ep2=0x%02X", intf_idx, ep1->bEndpointAddress, ep2->bEndpointAddress);
220
221 if (ep1->bEndpointAddress & usb_host::USB_DIR_IN) {
222 eps.in_ep = ep1;
223 eps.out_ep = ep2;
224 ESP_LOGD(TAG, "ep1 is IN (RX): ep1=0x%02X (in_ep), ep2=0x%02X (out_ep)", ep1->bEndpointAddress,
225 ep2->bEndpointAddress);
226 } else {
227 eps.out_ep = ep1;
228 eps.in_ep = ep2;
229 ESP_LOGD(TAG, "ep1 is OUT (TX): ep1=0x%02X (out_ep), ep2=0x%02X (in_ep)", ep1->bEndpointAddress,
230 ep2->bEndpointAddress);
231 }
232
233 eps.bulk_interface_number = intf_desc->bInterfaceNumber;
234 eps.bulk_interface_class = intf_desc->bInterfaceClass;
235 return eps;
236}
237
238std::vector<CdcEps> USBUartTypeFT23XX::parse_descriptors(usb_device_handle_t dev_hdl) {
239 const usb_config_desc_t *config_desc;
240 const usb_device_desc_t *device_desc;
241 std::vector<CdcEps> cdc_devs{};
242 std::string type_string;
243
244 if (usb_host_get_device_descriptor(dev_hdl, &device_desc) != ESP_OK) {
245 ESP_LOGE(TAG, "get_device_descriptor failed");
246 return {};
247 }
248 if (usb_host_get_active_config_descriptor(dev_hdl, &config_desc) != ESP_OK) {
249 ESP_LOGE(TAG, "get_active_config_descriptor failed");
250 return {};
251 }
252 if (device_desc->bcdDevice == 0x400 || (device_desc->bcdDevice == 0x200 && device_desc->iSerialNumber == 0)) {
253 this->chip_type_ = TYPE_BM;
254 type_string = "BM type chip";
255 } else if (device_desc->bcdDevice == 0x200) {
256 this->chip_type_ = TYPE_AM;
257 type_string = "AM type chip";
258 } else if (device_desc->bcdDevice == 0x500) {
259 this->chip_type_ = TYPE_2232C;
260 type_string = "2232C chip";
261 } else if (device_desc->bcdDevice == 0x600) {
262 this->chip_type_ = TYPE_R;
263 type_string = "type R chip";
264 } else if (device_desc->bcdDevice == 0x700) {
265 this->chip_type_ = TYPE_2232H;
266 type_string = "2232H chip";
267 } else if (device_desc->bcdDevice == 0x800) {
268 this->chip_type_ = TYPE_4232H;
269 type_string = "4232H chip";
270 } else if (device_desc->bcdDevice == 0x900) {
271 this->chip_type_ = TYPE_232H;
272 type_string = "232H type chip";
273 } else if (device_desc->bcdDevice == 0x1000) {
274 this->chip_type_ = TYPE_230X;
275 type_string = "230x chip";
276 }
277
278 ESP_LOGD(TAG, "Found FTDI %s based device", type_string.c_str());
279 for (size_t intf_idx = 0; intf_idx < this->channels_.size(); intf_idx++) {
280 if (auto eps = get_uart(config_desc, static_cast<uint8_t>(intf_idx))) {
281 cdc_devs.push_back(*eps);
282 ESP_LOGD(TAG, "Found CDC interface at USB interface index %zu", intf_idx);
283 }
284 }
285 return cdc_devs;
286}
287
289 if (!channel->initialised_.load())
290 return;
291
292 // Use compare_exchange_strong to avoid a check-then-act race: start_input() is called
293 // from both the USB task (self-restart on success) and the main loop (backpressure
294 // restart), so a plain load()/store() pair can let both threads submit a transfer.
295 auto started = false;
296 if (!channel->input_started_.compare_exchange_strong(started, true))
297 return;
298
299 const auto *ep = channel->cdc_dev_.in_ep;
300
301 auto callback = [this, channel](const usb_host::TransferStatus &status) {
302 if (!status.success) {
303 ESP_LOGE(TAG, "RX Transfer failed, status=%s", esp_err_to_name(status.error_code));
304 channel->input_started_.store(false);
305 return;
306 }
307
308 // FTDI prepends a 2-byte modem/line status header to every bulk IN packet.
309 size_t uart_data_len = (status.data_len > 2) ? (status.data_len - 2) : 0;
310
311 if (uart_data_len > 0) {
312 ESP_LOGV(TAG, "RX callback: Received %zu bytes, channel=%d", uart_data_len, channel->index_);
313 if (!channel->dummy_receiver_) {
314 UsbDataChunk *chunk = this->chunk_pool_.allocate();
315 if (chunk == nullptr) {
316 this->usb_data_queue_.increment_dropped_count();
317 channel->input_started_.store(false);
318 // Queue is full — wake the main loop to drain it, then let read_array()
319 // retrigger start_input() rather than spinning here in the USB task.
322 return;
323 }
324 // Strip the 2-byte FTDI header before queuing.
325 memcpy(chunk->data, status.data + 2, uart_data_len);
326 chunk->length = static_cast<uint16_t>(uart_data_len);
327 chunk->channel = channel;
328 this->usb_data_queue_.push(chunk);
329#ifdef USE_UART_DEBUGGER
330 if (channel->debug_) {
332 std::vector<uint8_t>(status.data + 2, status.data + 2 + uart_data_len), ',',
333 channel->debug_prefix_);
334 }
335#endif
338 }
339 } else if (status.data_len >= 2) {
340 ESP_LOGVV(TAG, "RX: Status packet, modem=0x%02X line=0x%02X, ch=%d", status.data[0], status.data[1],
341 channel->index_);
342 }
343
344 channel->input_started_.store(false);
345 this->start_input(channel);
346 };
347
348 if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) {
349 ESP_LOGE(TAG, "RX transfer submission failed for ep=0x%02X", ep->bEndpointAddress);
350 channel->input_started_.store(false);
351 }
352}
353
355 ESP_LOGW(TAG, "RX buffer overflow on channel %d, clearing to resync", channel->index_);
356 channel->input_buffer_.clear();
357}
358
359bool USBUartTypeFT23XX::config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok,
360 const uint8_t *response) {
361 // On reload (settings change on an open channel) skip the SIO reset; the FTDI set_termios
362 // path only re-applies baud + line properties and does not re-assert DTR/RTS.
363 if (reload)
364 step++;
365 switch (step) {
366 case 0: // SIO reset (init only)
367 this->config_transfer_(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, 0x00, 0x00,
368 channel->cdc_dev_.bulk_interface_number + 1);
369 return true;
370 case 1: { // set baudrate
371 auto config = ftdi_convert_baudrate(channel->baud_rate_, this->chip_type_, channel->index_);
372 uint16_t usb_index = (config.ftdi_index & 0xFF00) | (channel->cdc_dev_.bulk_interface_number + 1);
373 ESP_LOGD(TAG, "Baudrate: %u, value=0x%04X, ftdi_index=0x%04X", (unsigned) channel->baud_rate_, config.value,
374 config.ftdi_index);
375 this->config_transfer_(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, 0x03, config.value, usb_index);
376 return true;
377 }
378 case 2: { // set line properties (data bits / parity / stop bits)
379 uint16_t value = channel->data_bits_;
380 value |= static_cast<uint8_t>(channel->line_parity_()) << 8;
381 value |= static_cast<uint8_t>(channel->line_stop_bits_()) << 11;
382 value |= (0x00 << 14);
383 this->config_transfer_(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, 0x04, value,
384 channel->cdc_dev_.bulk_interface_number + 1);
385 return true;
386 }
387 case 3: { // set flow control, re-applied on every settings change
388 uint16_t mode = SIO_DISABLE_FLOW_CTRL;
389 uint16_t value = 0;
391 mode = SIO_RTS_CTS_HS;
392 } else if (channel->flow_control_ == uart::UART_FLOW_CONTROL_XON_XOFF) {
393 mode = SIO_XON_XOFF_HS;
394 value = SIO_XON_XOFF_CHARS;
395 }
396 ESP_LOGD(TAG, "SIO_SET_FLOW_CTRL: mode=0x%04X", mode);
397 this->config_transfer_(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, SIO_SET_FLOW_CTRL, value,
398 mode | (channel->cdc_dev_.bulk_interface_number + 1));
399 return true;
400 }
401 case 4: // set modem control DTR+RTS (init only)
402 if (reload)
403 return false;
404 this->modem_control_transfer(channel);
405 return true;
406 default:
407 return false;
408 }
409}
410
412 const uint16_t value =
413 SIO_SET_DTR_MASK | SIO_SET_RTS_MASK | (channel->dtr_ ? SIO_DTR_HIGH : 0) | (channel->rts_ ? SIO_RTS_HIGH : 0);
414 ESP_LOGD(TAG, "SIO_SET_MODEM_CTRL: DTR=%s RTS=%s", ONOFF(channel->dtr_), ONOFF(channel->rts_));
415 this->config_transfer_(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, SIO_SET_MODEM_CTRL, value,
416 channel->cdc_dev_.bulk_interface_number + 1);
417}
418
419} // namespace esphome::usb_uart
420#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32P4 ||
421 // USE_ESP32_VARIANT_ESP32S31 || USE_ESP32_VARIANT_ESP32H4
BedjetMode mode
BedJet operating mode.
uint8_t status
Definition bl0942.h:8
void wake_loop_threadsafe()
Wake the main event loop from another thread or callback.
void enable_loop_soon_any_context()
Thread and ISR-safe version of enable_loop() that can be called from any context.
static void log_hex(UARTDirection direction, std::vector< uint8_t > bytes, uint8_t separator, StringRef prefix=StringRef())
Log the bytes as hex values, separated by the provided separator character.
bool transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length)
Performs a transfer input operation.
std::atomic< bool > input_started_
Definition usb_uart.h:235
LineStopBits line_stop_bits_() const
Definition usb_uart.cpp:744
std::vector< USBUartChannelBase * > channels_
Definition usb_uart.h:320
LockFreeQueue< UsbDataChunk, USB_DATA_QUEUE_SIZE > usb_data_queue_
Definition usb_uart.h:284
void config_transfer_(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const std::vector< uint8_t > &data={}, uint8_t *dest=nullptr)
Definition usb_uart.cpp:604
EventPool< UsbDataChunk, USB_DATA_QUEUE_SIZE - 1 > chunk_pool_
Definition usb_uart.h:286
void modem_control_transfer(USBUartChannelBase *channel) override
Definition ft23xx.cpp:411
bool config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override
Definition ft23xx.cpp:359
void on_rx_overflow(USBUartChannelBase *channel) override
Definition ft23xx.cpp:354
std::vector< CdcEps > parse_descriptors(usb_device_handle_t dev_hdl) override
Definition ft23xx.cpp:238
void start_input(USBUartChannelBase *channel) override
Definition ft23xx.cpp:288
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
const usb_ep_desc_t * in_ep
Definition usb_uart.h:36
uint8_t data[usb_host::USB_MAX_PACKET_SIZE]
Definition usb_uart.h:139
USBUartChannelBase * channel
Definition usb_uart.h:141