From 2cb23b70d3c8151a40ff61b95e2ab6aea4948d9a Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Sat, 29 Nov 2025 22:55:07 -0500 Subject: [PATCH] Add USB Keyboard support --- CMakeLists.txt | 9 +- hello_usb.c | 135 --------------------- hello_usb.cpp | 323 +++++++++++++++++++++++++++++++++++-------------- tusb_config.h | 46 +++++++ 4 files changed, 284 insertions(+), 229 deletions(-) delete mode 100644 hello_usb.c create mode 100644 tusb_config.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 21b19eb..31344a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,7 +35,7 @@ pico_sdk_init() # Add executable. Default name is the project name, version 0.1 -if (TARGET tinyusb_device) +if (TARGET tinyusb_host) add_executable(hello_usb hello_usb.cpp display.cpp @@ -50,6 +50,7 @@ if (TARGET tinyusb_device) # Add include directories for e-Paper target_include_directories(hello_usb PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/Pico_ePaper_Code/c/lib/Config ${CMAKE_CURRENT_SOURCE_DIR}/Pico_ePaper_Code/c/lib/e-Paper ${CMAKE_CURRENT_SOURCE_DIR}/Pico_ePaper_Code/c/lib/Fonts @@ -57,11 +58,11 @@ if (TARGET tinyusb_device) ) # pull in common dependencies - target_link_libraries(hello_usb pico_stdlib pico_ssd1306 hardware_i2c Config ePaper GUI Fonts hardware_spi pico_multicore) + target_link_libraries(hello_usb pico_stdlib pico_ssd1306 hardware_i2c Config ePaper GUI Fonts hardware_spi pico_multicore tinyusb_host tinyusb_board) # enable usb output, disable uart output - pico_enable_stdio_usb(hello_usb 1) - pico_enable_stdio_uart(hello_usb 0) + pico_enable_stdio_usb(hello_usb 0) + pico_enable_stdio_uart(hello_usb 1) # create map/bin/hex/uf2 file etc. pico_add_extra_outputs(hello_usb) diff --git a/hello_usb.c b/hello_usb.c deleted file mode 100644 index f5c56f6..0000000 --- a/hello_usb.c +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Simple USB Serial Echo Program with Timeout, Reconnect, and Backspace Logic. - * This program now correctly handles the backspace character by deleting it - * from the buffer and sending the necessary control codes to the terminal - * for visual deletion. - */ - -#include "pico/stdlib.h" -#include "pico/time.h" // Needed for time_us_64() -#include -#include - -// Define the maximum size for the input string buffer -#define MAX_INPUT_LEN 64 -// Define the timeout period: 1 second in microseconds -#define TIMEOUT_US 5000000 -// ASCII code for Backspace (often sent as 0x08) -#define ASCII_BACKSPACE 8 - -// Helper function to handle the echoing and resetting of the buffer -void echo_and_reset(char* buffer, int* index_ptr) { - if (*index_ptr > 0) { - // Null-terminate the string - buffer[*index_ptr] = '\0'; - - // Echo the string back, converting to uppercase - printf("\nEchoed (ALL CAPS): "); - for (int i = 0; buffer[i] != '\0'; i++) { - printf("%c", toupper(buffer[i])); - } - printf("\n"); - } - - // Reset buffer for next line and print a new prompt - *index_ptr = 0; - printf("> "); -} - -// Function to handle the initial waiting and reconnect waiting -void wait_for_usb_connection() { - printf("Waiting for USB host to connect...\n"); - // Wait until the USB host (e.g., terminal program) opens the serial port - while (!stdio_usb_connected()) { - sleep_ms(100); - } - // Connection established! - printf("\nConnection Established! Starting Echo Session...\n"); -} - -void run_echo_session() { - char input_buffer[MAX_INPUT_LEN]; - int buffer_index = 0; - // Track the time (in microseconds) when the last character was received - uint64_t last_char_time = time_us_64(); - - printf("--- Pico USB String Echo Program Started ---\n"); - printf("Type a sentence (up to %d chars). It will echo on Enter/Timeout.\n", MAX_INPUT_LEN - 2); - printf("--------------------------------------------\n"); - printf("> "); // Prompt for input - - // Inner loop runs ONLY while the USB connection is active - while (stdio_usb_connected()) { - - // Check for incoming character without blocking - int c = getchar_timeout_us(0); - - if (c != PICO_ERROR_TIMEOUT) { - - // Character received, update the timer - last_char_time = time_us_64(); - char input_char = (char)c; - - // 1. --- BACKSPACE HANDLING --- - if (input_char == ASCII_BACKSPACE || input_char == 127) { // 127 is sometimes sent by terminals (DEL key) - if (buffer_index > 0) { - // Decrease buffer index (remove the character) - buffer_index--; - // Send backspace, space, and backspace to erase the character on the terminal: - // \b: move cursor back - // ' ': overwrite with a space - // \b: move cursor back again, ready for the next character - printf("\b \b"); - } - // 2. --- End of Input (Newline/CR) --- - } else if (input_char == '\r' || input_char == '\n') { - echo_and_reset(input_buffer, &buffer_index); - - // 3. --- Collect regular characters --- - } else if (buffer_index < (MAX_INPUT_LEN - 1) && isprint(input_char)) { - - // Echo character locally so the user sees what they type - printf("%c", input_char); - - // Store character in the buffer - input_buffer[buffer_index] = input_char; - buffer_index++; - } - // Ignore non-printable characters and overflowed buffer input - } - - // 4. Timeout Check - if (buffer_index > 0 && (time_us_64() - last_char_time) > TIMEOUT_US) { - printf("\n--- Timeout Reached (5.0s silence) ---\n"); - echo_and_reset(input_buffer, &buffer_index); - last_char_time = time_us_64(); // Reset time to prevent immediate re-trigger - } - - // Add a small delay to prevent the loop from consuming too much CPU time - sleep_us(100); - } - - // Connection lost or closed by host - printf("\nHost disconnected. Ending Echo Session.\n"); -} - - -int main() { - // Initialize Standard I/O (stdio) to use the USB backend - stdio_init_all(); - - // Outer loop: Allows the program to run continuously and restart the - // session after a disconnection. - while (true) { - - // 1. Wait for a connection - wait_for_usb_connection(); - - // 2. Run the program logic while connected - run_echo_session(); - - // Loop repeats, going back to wait_for_usb_connection() - } - - // Note: main should not return -} \ No newline at end of file diff --git a/hello_usb.cpp b/hello_usb.cpp index e76d77c..d45c152 100644 --- a/hello_usb.cpp +++ b/hello_usb.cpp @@ -1,12 +1,11 @@ /* - * Simple USB Serial Echo Program with Timeout, Reconnect, and Backspace Logic. - * Converted to C++ so the project can use the `pico-ssd1306` C++ library. - * Integrated with e-ink display support via Pico_ePaper library. + * USB Host Keyboard Example with Display Support */ #include "pico/stdlib.h" -#include "pico/time.h" // Needed for time_us_64() +#include "pico/time.h" #include "pico/multicore.h" +#include "tusb.h" #include #include #include @@ -65,8 +64,8 @@ void init_epaper_display() { printf("EPD_7IN5B_V2_Init()\r\n"); EPD_7IN5B_V2_Init_Fast(); printf("EPD_7IN5B_V2_Clear()\r\n"); - EPD_7IN5B_V2_Clear(); - DEV_Delay_ms(500); + // EPD_7IN5B_V2_Clear(); + // DEV_Delay_ms(500); // Create image buffers for black and red content // 7.5" display: 800x480, 8 pixels per byte, so (800/8) * 480 = 48000 bytes each @@ -93,27 +92,20 @@ void init_epaper_display() { // Draw header EPD_7IN5B_V2_Init_Part(); + EPD_7IN5B_V2_Display_Partial(g_epd_image, 0, 0, EPD_7IN5B_V2_WIDTH, EPD_7IN5B_V2_HEIGHT); + EPD_7IN5B_V2_Display_Partial(g_epd_image, 0, 0, EPD_7IN5B_V2_WIDTH, EPD_7IN5B_V2_HEIGHT); + EPD_7IN5B_V2_Display_Partial(g_epd_image, 0, 0, EPD_7IN5B_V2_WIDTH, EPD_7IN5B_V2_HEIGHT); + Paint_SelectImage(g_epd_red); printf("Drawing header\r\n"); Paint_DrawString_EN(10, 10, "What's new today:", &Font24, WHITE, RED); // Display the image with both black and red buffers - // printf("display text\r\n"); EPD_7IN5B_V2_Display_Partial(g_epd_red, 0, 0, EPD_7IN5B_V2_WIDTH, EPD_7IN5B_V2_HEIGHT); - // printf("delay\r\n"); - // DEV_Delay_ms(1000); printf("e-Paper display ready!\r\n"); - // EPD_7IN5B_V2_Sleep(); } -// Define the maximum size for the input string buffer -#define MAX_INPUT_LEN 64 -// Define the timeout period: 5 seconds in microseconds -#define TIMEOUT_US 5000000 -// ASCII code for Backspace (often sent as 0x08) -#define ASCII_BACKSPACE 8 - /** * Add an entry to the display list and send update to core 1 */ @@ -207,12 +199,6 @@ void core1_display_init() { // Draw all entries starting below header UWORD y_pos = 0; - /*for (int i = 0; i < msg->entries.count; i++) { - if (y_pos + 20 < 480) { // Don't draw beyond screen - Paint_DrawString_EN(20, y_pos, msg->entries.entries[i], &Font16, WHITE, BLACK); - y_pos += 25; // Space between entries - } - }*/ // paint only the last 3 entries int start_index = msg->entries.count > 2 ? msg->entries.count - 2 : 0; for (int i = start_index; i < msg->entries.count; i++) { @@ -224,21 +210,8 @@ void core1_display_init() { } } // Use partial or full refresh - //if (msg->use_partial) { - printf("[Core 1] Using partial refresh\n"); - EPD_7IN5B_V2_Display_Partial(g_epd_image, msg->xstart, msg->ystart, msg->xend, msg->yend); - /*} else { - printf("[Core 1] Using full refresh\n"); - // EPD_7IN5B_V2_Init(); - // EPD_7IN5B_V2_Clear(); - // DEV_Delay_ms(500); - - // Re-initialize partial refresh mode after full refresh - printf("[Core 1] Reinitializing partial refresh mode\n"); - EPD_7IN5B_V2_Init_Part(); - Paint_SelectImage(g_epd_image); - EPD_7IN5B_V2_Display_Partial(g_epd_image, msg->xstart, msg->ystart, msg->xend, msg->yend); - }*/ + printf("[Core 1] Using partial refresh\n"); + EPD_7IN5B_V2_Display_Partial(g_epd_image, msg->xstart, msg->ystart, msg->xend, msg->yend); } // Free the message (it was allocated by core 0) @@ -252,78 +225,248 @@ void core1_display_init() { } } -void wait_for_usb_connection(DisplayManager &display) { - printf("Waiting for USB host to connect...\n"); - while (!stdio_usb_connected()) { - sleep_ms(100); +// Global DisplayManager pointer +static DisplayManager* g_display_manager = nullptr; + +// Keyboard buffer +#define MAX_INPUT_LEN 64 +static char g_input_buffer[MAX_INPUT_LEN]; +static int g_buffer_index = 0; + +// ASCII code for Backspace (often sent as 0x08) +#define ASCII_BACKSPACE 8 + +static const uint8_t keycode2ascii[128][2] = { + {0, 0}, /* 0x00 */ + {0, 0}, /* 0x01 */ + {0, 0}, /* 0x02 */ + {0, 0}, /* 0x03 */ + {'a', 'A'}, /* 0x04 */ + {'b', 'B'}, /* 0x05 */ + {'c', 'C'}, /* 0x06 */ + {'d', 'D'}, /* 0x07 */ + {'e', 'E'}, /* 0x08 */ + {'f', 'F'}, /* 0x09 */ + {'g', 'G'}, /* 0x0a */ + {'h', 'H'}, /* 0x0b */ + {'i', 'I'}, /* 0x0c */ + {'j', 'J'}, /* 0x0d */ + {'k', 'K'}, /* 0x0e */ + {'l', 'L'}, /* 0x0f */ + {'m', 'M'}, /* 0x10 */ + {'n', 'N'}, /* 0x11 */ + {'o', 'O'}, /* 0x12 */ + {'p', 'P'}, /* 0x13 */ + {'q', 'Q'}, /* 0x14 */ + {'r', 'R'}, /* 0x15 */ + {'s', 'S'}, /* 0x16 */ + {'t', 'T'}, /* 0x17 */ + {'u', 'U'}, /* 0x18 */ + {'v', 'V'}, /* 0x19 */ + {'w', 'W'}, /* 0x1a */ + {'x', 'X'}, /* 0x1b */ + {'y', 'Y'}, /* 0x1c */ + {'z', 'Z'}, /* 0x1d */ + {'1', '!'}, /* 0x1e */ + {'2', '@'}, /* 0x1f */ + {'3', '#'}, /* 0x20 */ + {'4', '$'}, /* 0x21 */ + {'5', '%'}, /* 0x22 */ + {'6', '^'}, /* 0x23 */ + {'7', '&'}, /* 0x24 */ + {'8', '*'}, /* 0x25 */ + {'9', '('}, /* 0x26 */ + {'0', ')'}, /* 0x27 */ + {'\r', '\r'}, /* 0x28 ENTER */ + {'\x1b', '\x1b'}, /* 0x29 ESCAPE */ + {'\b', '\b'}, /* 0x2a BACKSPACE */ + {'\t', '\t'}, /* 0x2b TAB */ + {' ', ' '}, /* 0x2c SPACE */ + {'-', '_'}, /* 0x2d MINUS */ + {'=', '+'}, /* 0x2e EQUAL */ + {'[', '{'}, /* 0x2f BRACKET_LEFT */ + {']', '}'}, /* 0x30 BRACKET_RIGHT */ + {'\\', '|'}, /* 0x31 BACKSLASH */ + {'#', '~'}, /* 0x32 EUROPE_1 */ + {';', ':'}, /* 0x33 SEMICOLON */ + {'\'', '\"'}, /* 0x34 APOSTROPHE */ + {'`', '~'}, /* 0x35 GRAVE */ + {',', '<'}, /* 0x36 COMMA */ + {'.', '>'}, /* 0x37 PERIOD */ + {'/', '?'}, /* 0x38 SLASH */ + {0, 0}, /* 0x39 CAPS_LOCK */ + {0, 0}, /* 0x3a F1 */ + {0, 0}, /* 0x3b F2 */ + {0, 0}, /* 0x3c F3 */ + {0, 0}, /* 0x3d F4 */ + {0, 0}, /* 0x3e F5 */ + {0, 0}, /* 0x3f F6 */ + {0, 0}, /* 0x40 F7 */ + {0, 0}, /* 0x41 F8 */ + {0, 0}, /* 0x42 F9 */ + {0, 0}, /* 0x43 F10 */ + {0, 0}, /* 0x44 F11 */ + {0, 0}, /* 0x45 F12 */ + {0, 0}, /* 0x46 PRINT_SCREEN */ + {0, 0}, /* 0x47 SCROLL_LOCK */ + {0, 0}, /* 0x48 PAUSE */ + {0, 0}, /* 0x49 INSERT */ + {0, 0}, /* 0x4a HOME */ + {0, 0}, /* 0x4b PAGE_UP */ + {0, 0}, /* 0x4c DELETE */ + {0, 0}, /* 0x4d END */ + {0, 0}, /* 0x4e PAGE_DOWN */ + {0, 0}, /* 0x4f RIGHT_ARROW */ + {0, 0}, /* 0x50 LEFT_ARROW */ + {0, 0}, /* 0x51 DOWN_ARROW */ + {0, 0}, /* 0x52 UP_ARROW */ + {0, 0}, /* 0x53 NUM_LOCK */ + {'/', '/'}, /* 0x54 KP_DIVIDE */ + {'*', '*'}, /* 0x55 KP_MULTIPLY */ + {'-', '-'}, /* 0x56 KP_MINUS */ + {'+', '+'}, /* 0x57 KP_PLUS */ + {'\r', '\r'}, /* 0x58 KP_ENTER */ + {'1', '1'}, /* 0x59 KP_1 */ + {'2', '2'}, /* 0x5a KP_2 */ + {'3', '3'}, /* 0x5b KP_3 */ + {'4', '4'}, /* 0x5c KP_4 */ + {'5', '5'}, /* 0x5d KP_5 */ + {'6', '6'}, /* 0x5e KP_6 */ + {'7', '7'}, /* 0x5f KP_7 */ + {'8', '8'}, /* 0x60 KP_8 */ + {'9', '9'}, /* 0x61 KP_9 */ + {'0', '0'}, /* 0x62 KP_0 */ + {'.', '.'}, /* 0x63 KP_DECIMAL */ +}; + +static inline bool find_key_in_report(hid_keyboard_report_t const *report, uint8_t keycode) { + for(uint8_t i=0; i<6; i++) { + if (report->keycode[i] == keycode) return true; } - printf("\nConnection Established! Starting Echo Session...\n"); - display.refresh(">", nullptr); + return false; } -void run_echo_session(DisplayManager &display) { - char input_buffer[MAX_INPUT_LEN]; - int buffer_index = 0; +static void process_kbd_report(hid_keyboard_report_t const *report) { + static hid_keyboard_report_t prev_report = { 0, 0, {0} }; - printf("--- Welcome User! ---\n"); - printf("Type a command (or write help):\n"); - printf("--------------------------------------------\n"); - printf("> "); - - while (stdio_usb_connected()) { - int c = getchar_timeout_us(0); - if (c != PICO_ERROR_TIMEOUT) { - char input_char = (char)c; - if (input_char == ASCII_BACKSPACE || input_char == 127) { - if (buffer_index > 0) { - buffer_index--; - printf("\b \b"); - // update displays to reflect removed char - input_buffer[buffer_index] = '\0'; - display.refresh(input_buffer, g_last_echo); - // send_display_update(input_buffer, g_last_echo); + for(uint8_t i=0; i<6; i++) { + if (report->keycode[i]) { + if (find_key_in_report(&prev_report, report->keycode[i])) { + // exist in previous report means the current key is holding + } else { + // not existed in previous report means the current key is pressed + bool const is_shift = report->modifier & (KEYBOARD_MODIFIER_LEFTSHIFT | KEYBOARD_MODIFIER_RIGHTSHIFT); + uint8_t ch = keycode2ascii[report->keycode[i]][is_shift ? 1 : 0]; + + if (ch) { + printf("%c", ch); + + if (ch == '\r' || ch == '\n') { + printf("\n"); + // Process command + g_input_buffer[g_buffer_index] = '\0'; + + // Echo logic + echo_and_reset(g_input_buffer, &g_buffer_index); + + // Save last echoed (all caps) for display + size_t k = 0; + for (; g_input_buffer[k] != '\0' && k < sizeof(g_last_echo) - 1; ++k) { + g_last_echo[k] = g_input_buffer[k]; + } + g_last_echo[k] = '\0'; + + // Update displays + if (g_display_manager) { + g_display_manager->set_last_echo(g_last_echo); + g_display_manager->refresh("", g_last_echo); + } + send_display_update(g_last_echo); + + // Reset buffer + g_buffer_index = 0; + g_input_buffer[0] = '\0'; + + } else if (ch == '\b' || ch == 127) { + if (g_buffer_index > 0) { + g_buffer_index--; + g_input_buffer[g_buffer_index] = '\0'; + if (g_display_manager) { + g_display_manager->refresh(g_input_buffer, g_last_echo); + } + } + } else if (g_buffer_index < MAX_INPUT_LEN - 1) { + g_input_buffer[g_buffer_index] = ch; + g_buffer_index++; + g_input_buffer[g_buffer_index] = '\0'; + if (g_display_manager) { + g_display_manager->refresh(g_input_buffer, g_last_echo); + } + } } - } else if (input_char == '\r' || input_char == '\n') { - echo_and_reset(input_buffer, &buffer_index); - // Save last echoed (all caps) for display - size_t i = 0; - for (; input_buffer[i] != '\0' && i < sizeof(g_last_echo) - 1; ++i) { - g_last_echo[i] = input_buffer[i]; - } - g_last_echo[i] = '\0'; - // Update both displays to show cleared input and last echo - display.set_last_echo(g_last_echo); - display.refresh("", g_last_echo); - send_display_update(g_last_echo); - } else if (buffer_index < (MAX_INPUT_LEN - 1) && isprint(input_char)) { - printf("%c", input_char); - input_buffer[buffer_index] = input_char; - buffer_index++; - // update both displays with current input - input_buffer[buffer_index] = '\0'; - display.refresh(input_buffer, g_last_echo); - // send_display_update(input_buffer, g_last_echo); } } - sleep_us(100); } - - printf("\nHost disconnected. Ending Echo Session.\n"); + prev_report = *report; +} + +//--------------------------------------------------------------------+ +// TinyUSB Callbacks +//--------------------------------------------------------------------+ + +void tuh_hid_mount_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len) { + printf("HID device address = %d, instance = %d is mounted\r\n", dev_addr, instance); + + uint8_t const itf_protocol = tuh_hid_interface_protocol(dev_addr, instance); + if (itf_protocol == HID_ITF_PROTOCOL_KEYBOARD) { + printf("Keyboard mounted\r\n"); + if (g_display_manager) { + g_display_manager->refresh("Keyboard Connected", nullptr); + } + if (!tuh_hid_receive_report(dev_addr, instance)) { + printf("Error: cannot request to receive report\r\n"); + } + } +} + +void tuh_hid_umount_cb(uint8_t dev_addr, uint8_t instance) { + printf("HID device address = %d, instance = %d is unmounted\r\n", dev_addr, instance); + if (g_display_manager) { + g_display_manager->refresh("Keyboard Disconnected", nullptr); + } +} + +void tuh_hid_report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) { + uint8_t const itf_protocol = tuh_hid_interface_protocol(dev_addr, instance); + + if (itf_protocol == HID_ITF_PROTOCOL_KEYBOARD) { + process_kbd_report((hid_keyboard_report_t const*) report); + } + + // continue to request to receive report + if (!tuh_hid_receive_report(dev_addr, instance)) { + printf("Error: cannot request to receive report\r\n"); + } } int main() { stdio_init_all(); + // Launch display initialization on core 1 printf("Launching e-Paper display init on core 1...\n"); multicore_launch_core1(core1_display_init); DisplayManager display; display.init(); - display.refresh("Waiting for USB/Serial", nullptr); + g_display_manager = &display; + + display.refresh("Waiting for Keyboard", nullptr); + + printf("Initializing TinyUSB Host...\n"); + tuh_init(BOARD_TUH_RHPORT); while (true) { - wait_for_usb_connection(display); - run_echo_session(display); + tuh_task(); } return 0; diff --git a/tusb_config.h b/tusb_config.h new file mode 100644 index 0000000..24f9fb5 --- /dev/null +++ b/tusb_config.h @@ -0,0 +1,46 @@ +#ifndef _TUSB_CONFIG_H_ +#define _TUSB_CONFIG_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +//--------------------------------------------------------------------+ +// COMMON CONFIGURATION +//--------------------------------------------------------------------+ +#define CFG_TUSB_MCU OPT_MCU_RP2040 + +// Host Mode +#define CFG_TUH_ENABLED 1 + +// RHPort number used for Host can be defined by board.mk, default to port 0 +#ifndef BOARD_TUH_RHPORT +#define BOARD_TUH_RHPORT 0 +#endif + +// RHPort max operational speed can defined by board.mk +#ifndef BOARD_TUH_MAX_SPEED +#define BOARD_TUH_MAX_SPEED OPT_MODE_DEFAULT_SPEED +#endif + +//--------------------------------------------------------------------+ +// HOST CONFIGURATION +//--------------------------------------------------------------------+ + +// Size of buffer to hold descriptors and other data used for enumeration +#define CFG_TUH_ENUMERATION_BUFSIZE 256 + +#define CFG_TUH_HUB 1 +#define CFG_TUH_CDC 0 +#define CFG_TUH_HID 4 // typical number of HID devices +#define CFG_TUH_MSC 0 +#define CFG_TUH_VENDOR 0 + +// max device support (excluding hub device) +#define CFG_TUH_DEVICE_MAX (CFG_TUH_HUB ? 4 : 1) // hub typically has 4 ports + +#ifdef __cplusplus +} +#endif + +#endif /* _TUSB_CONFIG_H_ */