├── CMakeLists.txt ├── LICENSE ├── README.md ├── component.mk ├── examples └── loopback │ └── loopback.ino ├── idf_component.yml ├── library.json ├── library.properties ├── platformio.ini └── src ├── USBHostSerial.cpp └── USBHostSerial.h /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(COMPONENT_SRCDIRS 2 | "src" 3 | ) 4 | 5 | set(COMPONENT_ADD_INCLUDEDIRS 6 | "src" 7 | ) 8 | 9 | set(COMPONENT_REQUIRES 10 | "arduino-esp32" 11 | "usb_host_ch34x_vcp" 12 | "usb_host_cp210x_vcp" 13 | "usb_host_ftdi_vcp" 14 | "usb_host_vcp" 15 | ) 16 | 17 | register_component() 18 | 19 | target_compile_options(${COMPONENT_TARGET} PRIVATE -fno-rtti) 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Bert Melis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # USBHostSerial 2 | 3 | :construction: **Work in progress.** :construction: 4 | 5 | The goal is to create an "Arduino Serial"-like interface for USB-to-UART devices, connected to your ESP32-S3 which acts as USB-host. 6 | 7 | Based on the [virtual com port example from Espressif](https://github.com/espressif/esp-idf/tree/master/examples/peripherals/usb/host/cdc/cdc_acm_vcp). 8 | 9 | ## Dependencies 10 | 11 | The following dependencies have to be installed: 12 | 13 | - [Espressif TinyUSB fork](https://components.espressif.com/components/espressif/tinyusb) 14 | - [Espressif's additions to TinyUSB](https://components.espressif.com/components/espressif/esp_tinyusb) 15 | - [USB Host CDC-ACM Class Drive](https://components.espressif.com/components/espressif/usb_host_cdc_acm/versions/2.0.3) 16 | - [Virtual COM Port Service](https://components.espressif.com/components/espressif/usb_host_vcp) 17 | - Device drivers, depending in your application: 18 | - [CH34x USB-UART converter driver](https://components.espressif.com/components/espressif/usb_host_ch34x_vcp/versions/2.0.0) 19 | - [Silicon Labs CP210x USB-UART converter driver](https://components.espressif.com/components/espressif/usb_host_cp210x_vcp/versions/2.0.0) 20 | - [FTDI UART-USB converters driver](https://components.espressif.com/components/espressif/usb_host_ftdi_vcp/versions/2.0.0) 21 | 22 | 23 | 24 | ## License 25 | 26 | The original example code is covered by this copyright notice: 27 | 28 | ``` 29 | /* 30 | * SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD 31 | * 32 | * SPDX-License-Identifier: CC0-1.0 33 | */ 34 | ``` 35 | 36 | Any modifications deemed "substantial" in this repo are covered by the included MIT license. 37 | -------------------------------------------------------------------------------- /component.mk: -------------------------------------------------------------------------------- 1 | COMPONENT_ADD_INCLUDEDIRS := src 2 | COMPONENT_SRCDIRS := src 3 | CXXFLAGS += -fno-rtti -------------------------------------------------------------------------------- /examples/loopback/loopback.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | USBHostSerial usbSerial; 6 | 7 | void setup() { 8 | /* 9 | baudrate 10 | stopbits: 0: 1 stopbit, 1: 1.5 stopbits, 2: 2 stopbits 11 | parity: 0: None, 1: Odd, 2: Even, 3: Mark, 4: Space 12 | databits: 8 13 | */ 14 | usbSerial.begin(9600, 0, 0, 8); 15 | } 16 | 17 | void loop() { 18 | // send message every 10s 19 | static uint32_t lastMessage = 0; 20 | if (millis() - lastMessage > 10000) { 21 | lastMessage = millis(); 22 | const char message[] = "USB says hello\n"; 23 | usbSerial.write((uint8_t*)message, 15); 24 | } 25 | 26 | // echo received data 27 | if (usbSerial.available()) { 28 | uint8_t buff[256]; 29 | std::size_t len = 0; 30 | while (usbSerial.available() && len > 0) { 31 | len += usbSerial.read(&buff[len], 256 - len); 32 | } 33 | usbSerial.write(buff, len); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /idf_component.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | usb_host_ch34x_vcp: "^2" 3 | usb_host_cp210x_vcp: "^2" 4 | usb_host_ftdi_vcp: "^2" 5 | usb_host_vcp: "^1" 6 | idf: ">=5.1.0" 7 | description: Use ESP32-S3 USB host as Serial 8 | url: https://github.com/bertmelis/USBHostSerial 9 | version: 0.0.1 -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "USBHostSerial", 3 | "keywords": "Serial, USB, host, esp32-s3", 4 | "description": "Use ESP32-S3 USB host as Serial", 5 | "authors": 6 | { 7 | "name": "Bert Melis", 8 | "url": "https://github.com/bertmelis" 9 | }, 10 | "license": "MIT", 11 | "homepage": "https://github.com/bertmelis/USBHostSerial", 12 | "repository": 13 | { 14 | "type": "git", 15 | "url": "https://github.com/bertmelis/USBHostSerial.git" 16 | }, 17 | "version": "0.0.1", 18 | "frameworks": "arduino", 19 | "platforms": ["espressif32"], 20 | "headers": ["USBHostSerial.h"], 21 | "build": 22 | { 23 | "libLDFMode": "deep+" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=USBHostSerial 2 | version=0.0.1 3 | author=Bert Melis 4 | maintainer=Bert Melis 5 | sentence=Use ESP32-S3 USB host as Serial 6 | paragraph= 7 | category=Communication 8 | url=https://github.com/bertmelis/USBHostSerial 9 | architectures=esp32 -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | [env:default] 2 | platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.03/platform-espressif32.zip 3 | board = lolin_s3_mini 4 | framework = arduino 5 | build_flags = 6 | -Wall 7 | -Wextra 8 | lib_ldf_mode = deep 9 | -------------------------------------------------------------------------------- /src/USBHostSerial.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2024 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | 8 | Based on example code: 9 | SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD 10 | SPDX-License-Identifier: CC0-1.0 11 | */ 12 | 13 | #include "USBHostSerial.h" 14 | 15 | using namespace esp_usb; 16 | 17 | USBHostSerial::USBHostSerial() 18 | : _host_config{} 19 | , _dev_config{} 20 | , _line_coding{} 21 | , _tx_buf_mem{} 22 | , _rx_buf_mem{} 23 | , _tx_buf_handle(nullptr) 24 | , _rx_buf_handle(nullptr) 25 | , _setupDone(false) 26 | , _connected(false) 27 | , _device_disconnected_sem(nullptr) 28 | , _usb_lib_task_handle(nullptr) { 29 | _dev_config.connection_timeout_ms = 10; 30 | _dev_config.out_buffer_size = USBHOSTSERIAL_BUFFERSIZE; 31 | _dev_config.in_buffer_size = USBHOSTSERIAL_BUFFERSIZE; 32 | _dev_config.event_cb = _handle_event; 33 | _dev_config.data_cb = _handle_rx; 34 | _dev_config.user_arg = this; 35 | 36 | _tx_buf_handle = xRingbufferCreateStatic(USBHOSTSERIAL_BUFFERSIZE, RINGBUF_TYPE_BYTEBUF, _tx_buf_mem, &_tx_buf_data); 37 | _rx_buf_handle = xRingbufferCreateStatic(USBHOSTSERIAL_BUFFERSIZE, RINGBUF_TYPE_BYTEBUF, _rx_buf_mem, &_rx_buf_data); 38 | if (!_tx_buf_handle || !_rx_buf_handle) { 39 | abort(); 40 | } 41 | } 42 | 43 | USBHostSerial::~USBHostSerial() { 44 | // TODO (bertmelis): implement destruction. 45 | vRingbufferDelete(_tx_buf_handle); 46 | vRingbufferDelete(_rx_buf_handle); 47 | abort(); 48 | } 49 | 50 | USBHostSerial::operator bool() const { 51 | if (xSemaphoreTake(_device_disconnected_sem, 0) == pdTRUE) { 52 | xSemaphoreGive(_device_disconnected_sem); 53 | return false; 54 | } 55 | return true; 56 | } 57 | 58 | bool USBHostSerial::begin(int baud, int stopbits, int parity, int databits) { 59 | if (!_setupDone) { 60 | _setupDone = true; 61 | _setup(); 62 | } 63 | 64 | _line_coding.dwDTERate = baud; 65 | _line_coding.bCharFormat = stopbits; 66 | _line_coding.bParityType = parity; 67 | _line_coding.bDataBits = databits; 68 | 69 | if (xTaskCreate(_USBHostSerial_task, "usb_dev_lib", 4096, this, 1, &_USBHostSerial_task_handle) == pdTRUE) { 70 | return true; 71 | } 72 | return false; 73 | } 74 | 75 | void USBHostSerial::end() { 76 | // TODO (bertmelis): implement end, together with destruction. 77 | } 78 | 79 | std::size_t USBHostSerial::write(uint8_t data) { 80 | if (xRingbufferSend(_tx_buf_handle, &data, 1, 10) == pdTRUE) { 81 | return 1; 82 | } 83 | return 0; 84 | } 85 | 86 | std::size_t USBHostSerial::write(const uint8_t *data, std::size_t len) { 87 | std::size_t i = 0; 88 | while (i < len && write(data[i]) == 1) { 89 | ++i; 90 | } 91 | return i; 92 | } 93 | 94 | std::size_t USBHostSerial::available() { 95 | UBaseType_t numItemsWaiting; 96 | vRingbufferGetInfo(_rx_buf_handle, nullptr, nullptr, nullptr, nullptr, &numItemsWaiting); 97 | return numItemsWaiting; 98 | } 99 | 100 | uint8_t USBHostSerial::read() { 101 | std::size_t pxItemSize = 0; 102 | uint8_t retVal = 0; 103 | void* ret = xRingbufferReceiveUpTo(_rx_buf_handle, &pxItemSize, 0, 1); 104 | if (pxItemSize > 0) { 105 | retVal = *reinterpret_cast(ret); 106 | vRingbufferReturnItem(_rx_buf_handle, ret); 107 | } 108 | return retVal; 109 | } 110 | 111 | std::size_t USBHostSerial::read(uint8_t *dest, std::size_t size) { 112 | std::size_t retVal = 0; 113 | std::size_t pxItemSize = 0; 114 | while (size > pxItemSize) { 115 | void *ret = xRingbufferReceiveUpTo(_rx_buf_handle, &pxItemSize, 10, size - pxItemSize); 116 | if (ret) { 117 | std::memcpy(dest, ret, pxItemSize); 118 | retVal += pxItemSize; 119 | vRingbufferReturnItem(_rx_buf_handle, ret); 120 | } else { 121 | break; 122 | } 123 | } 124 | return retVal; 125 | } 126 | 127 | void USBHostSerial::_setup() { 128 | _device_disconnected_sem = xSemaphoreCreateBinary(); 129 | assert(_device_disconnected_sem); 130 | 131 | // Install USB Host driver. Should only be called once in entire application 132 | _host_config.skip_phy_setup = false; 133 | _host_config.intr_flags = ESP_INTR_FLAG_LEVEL1; 134 | ESP_ERROR_CHECK(usb_host_install(&_host_config)); 135 | 136 | // Create a task that will handle USB library events 137 | BaseType_t task_created = xTaskCreate(_usb_lib_task, "usb_lib", 4096, this, 1, &_usb_lib_task_handle); 138 | assert(task_created == pdTRUE); 139 | 140 | ESP_ERROR_CHECK(cdc_acm_host_install(NULL)); 141 | 142 | // Register VCP drivers to VCP service 143 | VCP::register_driver(); 144 | VCP::register_driver(); 145 | VCP::register_driver(); 146 | } 147 | 148 | bool USBHostSerial::_handle_rx(const uint8_t *data, size_t data_len, void *arg) { 149 | std::size_t lenReceived = 0; 150 | while (lenReceived < data_len && xRingbufferSend(static_cast(arg)->_rx_buf_handle, &data[lenReceived], 1, 10) == pdTRUE) { 151 | ++lenReceived; 152 | } 153 | if (lenReceived < data_len) { 154 | // log overflow warning 155 | } 156 | return true; 157 | } 158 | 159 | void USBHostSerial::_handle_event(const cdc_acm_host_dev_event_data_t *event, void *user_ctx) { 160 | if (event->type == CDC_ACM_HOST_DEVICE_DISCONNECTED) { 161 | xSemaphoreGive(static_cast(user_ctx)->_device_disconnected_sem); 162 | } 163 | } 164 | 165 | void USBHostSerial::_usb_lib_task(void *arg) { 166 | while (1) { 167 | uint32_t event_flags; 168 | usb_host_lib_handle_events(portMAX_DELAY, &event_flags); 169 | if (event_flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS) { 170 | ESP_ERROR_CHECK(usb_host_device_free_all()); 171 | } 172 | } 173 | } 174 | 175 | void USBHostSerial::_USBHostSerial_task(void *arg) { 176 | USBHostSerial* thisInstance = static_cast(arg); 177 | while (1) { 178 | // try to open USB VCP device 179 | auto vcp = std::unique_ptr(VCP::open(&(thisInstance->_dev_config))); 180 | vTaskDelay( 10 / portTICK_PERIOD_MS ); 181 | if (vcp == nullptr) continue; 182 | 183 | // Mark connected and configure 184 | xSemaphoreTake(thisInstance->_device_disconnected_sem, portMAX_DELAY); 185 | ESP_ERROR_CHECK(vcp->line_coding_set(&(thisInstance->_line_coding))); 186 | 187 | while (1) { 188 | // check if still connected 189 | if (xSemaphoreTake(thisInstance->_device_disconnected_sem, 0) == pdTRUE) { 190 | break; 191 | } 192 | 193 | // check for data to send 194 | std::size_t pxItemSize = 0; 195 | void *data = xRingbufferReceiveUpTo(thisInstance->_tx_buf_handle, &pxItemSize, 10, USBHOSTSERIAL_BUFFERSIZE); 196 | if (pxItemSize > 0) { 197 | ESP_ERROR_CHECK(vcp->tx_blocking((uint8_t*)data, pxItemSize, 500)); 198 | vRingbufferReturnItem(thisInstance->_tx_buf_handle, data); 199 | } 200 | taskYIELD(); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/USBHostSerial.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2024 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | 8 | Based on example code: 9 | SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD 10 | SPDX-License-Identifier: CC0-1.0 11 | */ 12 | 13 | #pragma once 14 | 15 | #include // std::memcpy 16 | 17 | #include "esp_log.h" 18 | #include "freertos/FreeRTOS.h" 19 | #include "freertos/task.h" 20 | #include "freertos/semphr.h" 21 | #include "freertos/ringbuf.h" 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | #ifndef USBHOSTSERIAL_BUFFERSIZE 31 | #define USBHOSTSERIAL_BUFFERSIZE 256 32 | #endif 33 | 34 | class USBHostSerial { 35 | public: 36 | USBHostSerial(); 37 | ~USBHostSerial(); 38 | 39 | // true if serial-over-usb device is available eg. a device is connected 40 | explicit operator bool() const; 41 | 42 | /* 43 | baudrate 44 | stopbits: 0: 1 stopbit, 1: 1.5 stopbits, 2: 2 stopbits 45 | parity: 0: None, 1: Odd, 2: Even, 3: Mark, 4: Space 46 | databits: 8 47 | */ 48 | bool begin(int baud, int stopbits, int parity, int databits); 49 | 50 | // not yet implemented 51 | void end(); 52 | 53 | // write one byte to serial-over-usb. returns 0 when buffer is full or device is not available 54 | std::size_t write(uint8_t data); 55 | 56 | // write data to serial-over-usb. returns length of data that was actually written: 0 when buffer is full or device is not available 57 | std::size_t write(const uint8_t *data, std::size_t len); 58 | 59 | // get size of available RX data 60 | std::size_t available(); 61 | 62 | // read one byte from available data. If no data is available, 0 is returned (check with `available()`) 63 | uint8_t read(); 64 | 65 | // read available data into `dest`. returns number of bytes written. maximum number of `size` bytes will be written 66 | std::size_t read(uint8_t *dest, std::size_t size); 67 | 68 | protected: 69 | usb_host_config_t _host_config; 70 | cdc_acm_host_device_config_t _dev_config; 71 | cdc_acm_line_coding_t _line_coding; 72 | uint8_t _tx_buf_mem[USBHOSTSERIAL_BUFFERSIZE]; 73 | uint8_t _rx_buf_mem[USBHOSTSERIAL_BUFFERSIZE]; 74 | RingbufHandle_t _tx_buf_handle; 75 | StaticRingbuffer_t _tx_buf_data; 76 | RingbufHandle_t _rx_buf_handle; 77 | StaticRingbuffer_t _rx_buf_data; 78 | bool _setupDone; 79 | bool _connected; 80 | 81 | private: 82 | void _setup(); 83 | static bool _handle_rx(const uint8_t *data, size_t data_len, void *arg); 84 | static void _handle_event(const cdc_acm_host_dev_event_data_t *event, void *user_ctx); 85 | static void _usb_lib_task(void *arg); 86 | static void _USBHostSerial_task(void *arg); 87 | 88 | SemaphoreHandle_t _device_disconnected_sem; 89 | 90 | TaskHandle_t _usb_lib_task_handle; 91 | TaskHandle_t _USBHostSerial_task_handle; 92 | }; 93 | --------------------------------------------------------------------------------