diff options
| author | Ha Thach <[email protected]> | 2026-03-06 19:59:34 +0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-06 19:59:34 +0700 |
| commit | 6c2c309316794ee580603db1b48422f9c648b1ad (patch) | |
| tree | 2a86d1abba22be6b7c68683940b0388429f8519c | |
| parent | 06a4c6d7190ef83f76b2a65496f2a48a54a8c134 (diff) | |
| parent | 8878e02c3040ee0b30f9b26c20f45a36260d854a (diff) | |
Merge pull request #3444 from remiberthoz/device-class-printer
Implement Printer Device Driver
| -rw-r--r-- | AGENTS.md | 3 | ||||
| -rw-r--r-- | README.rst | 1 | ||||
| -rw-r--r-- | examples/device/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | examples/device/printer_to_cdc/CMakeLists.txt | 29 | ||||
| -rw-r--r-- | examples/device/printer_to_cdc/Makefile | 11 | ||||
| -rw-r--r-- | examples/device/printer_to_cdc/README.md | 80 | ||||
| -rw-r--r-- | examples/device/printer_to_cdc/src/main.c | 117 | ||||
| -rw-r--r-- | examples/device/printer_to_cdc/src/tusb_config.h | 117 | ||||
| -rw-r--r-- | examples/device/printer_to_cdc/src/usb_descriptors.c | 208 | ||||
| -rw-r--r-- | examples/device/printer_to_cdc/src/usb_descriptors.h | 37 | ||||
| -rw-r--r-- | hw/bsp/rp2040/family.cmake | 1 | ||||
| -rw-r--r-- | src/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | src/class/printer/printer.h | 62 | ||||
| -rw-r--r-- | src/class/printer/printer_device.c | 329 | ||||
| -rw-r--r-- | src/class/printer/printer_device.h | 140 | ||||
| -rw-r--r-- | src/device/usbd.c | 37 | ||||
| -rw-r--r-- | src/device/usbd.h | 13 | ||||
| -rw-r--r-- | src/tinyusb.mk | 1 | ||||
| -rw-r--r-- | src/tusb.h | 4 | ||||
| -rw-r--r-- | src/tusb_option.h | 4 | ||||
| -rw-r--r-- | test/fuzz/rules.mk | 1 | ||||
| -rwxr-xr-x | test/hil/hil_test.py | 130 | ||||
| -rw-r--r-- | tools/iar_template.ipcf | 5 |
23 files changed, 1330 insertions, 2 deletions
@@ -332,6 +332,9 @@ take 2-5 minutes. NEVER CANCEL. Set timeout to 20+ minutes. - `examples/device/cdc_msc/`: Most commonly used example for testing - `test/unit-test/project.yml`: Ceedling test configuration +#### MCU Reference Manuals and Datasheets +- Look in `$HOME/Documents/Calibre Library` for all MCU reference manuals, datasheets and board schematics. + #### Debugging Build Issues - **Missing compiler**: Install `gcc-arm-none-eabi` package - **Missing dependencies**: Run `python3 tools/get_deps.py FAMILY` diff --git a/README.rst b/README.rst index a3863c375..b24396b5a 100644 --- a/README.rst +++ b/README.rst @@ -87,6 +87,7 @@ Supports multiple device configurations by dynamically changing USB descriptors, - Communication Device Class (CDC) - Device Firmware Update (DFU): DFU mode (WIP) and Runtime - Human Interface Device (HID): Generic (In & Out), Keyboard, Mouse, Gamepad etc ... +- Printer class - Mass Storage Class (MSC): with multiple LUNs - Musical Instrument Digital Interface (MIDI) - Media Transfer Protocol (MTP/PTP) diff --git a/examples/device/CMakeLists.txt b/examples/device/CMakeLists.txt index 660df67cb..7173f455e 100644 --- a/examples/device/CMakeLists.txt +++ b/examples/device/CMakeLists.txt @@ -30,6 +30,7 @@ set(EXAMPLE_LIST msc_dual_lun mtp net_lwip_webserver + printer_to_cdc uac2_headset uac2_speaker_fb usbtmc diff --git a/examples/device/printer_to_cdc/CMakeLists.txt b/examples/device/printer_to_cdc/CMakeLists.txt new file mode 100644 index 000000000..3c8ab3653 --- /dev/null +++ b/examples/device/printer_to_cdc/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.20) + +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) + +project(printer_to_cdc C CXX ASM) + +# Checks this example is valid for the family and initializes the project +family_initialize_project(${PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR}) +# Espressif has its own cmake build system +if(FAMILY STREQUAL "espressif") + return() +endif() + +add_executable(${PROJECT_NAME}) + +# Example source +target_sources(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/usb_descriptors.c + ) + +# Example include +target_include_directories(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + ) + +# Configure compilation flags and libraries for the example without RTOS. +# See the corresponding function in hw/bsp/FAMILY/family.cmake for details. +family_configure_device_example(${PROJECT_NAME} noos) diff --git a/examples/device/printer_to_cdc/Makefile b/examples/device/printer_to_cdc/Makefile new file mode 100644 index 000000000..1a4b428dc --- /dev/null +++ b/examples/device/printer_to_cdc/Makefile @@ -0,0 +1,11 @@ +include ../../../hw/bsp/family_support.mk + +INC += \ + src \ + + +# Example source +EXAMPLE_SOURCE += $(wildcard src/*.c) +SRC_C += $(addprefix $(EXAMPLE_PATH)/, $(EXAMPLE_SOURCE)) + +include ../../../hw/bsp/family_rules.mk diff --git a/examples/device/printer_to_cdc/README.md b/examples/device/printer_to_cdc/README.md new file mode 100644 index 000000000..ecb9a678f --- /dev/null +++ b/examples/device/printer_to_cdc/README.md @@ -0,0 +1,80 @@ +#### Printer to CDC + +This example demonstrates a USB composite device with a Printer class interface and a CDC serial interface. Data flows bidirectionally between the two: + +- Data sent to the Printer (from host) is forwarded to the CDC serial port +- Data sent to the CDC serial port (from host) is forwarded to the Printer IN endpoint + +This is useful for debugging printer class communication or as a reference for implementing printer class devices. + +#### USB Interfaces + +| Interface | Class | Description | +|-----------|-------|-------------| +| 0 | CDC ACM | Virtual serial port | +| 2 | Printer | USB Printer (bidirectional, protocol 2) | + +#### How to Test + +The device exposes two endpoints on the host: +- `/dev/ttyACM0` (CDC serial port) +- `/dev/usb/lp0` (USB printer) + +Note: the actual device numbers may vary depending on your system. + +**Prerequisites (Linux):** + +```bash +# Load the USB printer kernel module if not already loaded +sudo modprobe usblp + +# Check devices exist +ls /dev/ttyACM* /dev/usb/lp* +``` + +**Test Printer to CDC (host writes to printer, reads from CDC):** + +```bash +# Terminal 1: read from CDC +cat /dev/ttyACM0 + +# Terminal 2: write to printer +echo "hello from printer" > /dev/usb/lp0 +# "hello from printer" appears in Terminal 1 +``` + +**Test CDC to Printer (host writes to CDC, reads from printer):** + +```bash +# Terminal 1: read from printer IN endpoint +cat /dev/usb/lp0 + +# Terminal 2: write to CDC +echo "hello from cdc" > /dev/ttyACM0 +# "hello from cdc" appears in Terminal 1 +``` + +**Interactive bidirectional test:** + +```bash +# Terminal 1: open CDC serial port +minicom -D /dev/ttyACM0 + +# Terminal 2: send to printer +echo "tinyusb print example" > /dev/usb/lp0 +# Text appears in minicom. Type in minicom to send data back through printer TX. +``` + +#### IEEE 1284 Device ID + +The device responds to GET_DEVICE_ID requests with: + +``` +MFG:TinyUSB;MDL:Printer to CDC;CMD:PS;CLS:PRINTER; +``` + +Verify with: + +```bash +cat /sys/class/usbmisc/lp0/device/ieee1284_id +``` diff --git a/examples/device/printer_to_cdc/src/main.c b/examples/device/printer_to_cdc/src/main.c new file mode 100644 index 000000000..ffaf709b4 --- /dev/null +++ b/examples/device/printer_to_cdc/src/main.c @@ -0,0 +1,117 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +/* This example demonstrates a USB Printer + CDC composite device. + * Data received on the Printer interface is forwarded to the CDC serial port, + * and data received on the CDC serial port is forwarded back to the Printer interface. + * + * To test: + * 1. Flash the device + * 2. Open a serial terminal on the CDC port (e.g. /dev/ttyACM0) + * 3. Send data to the printer: echo "hello" > /dev/usb/lp0 + * 4. The data appears on the CDC serial terminal + * 5. Type in the serial terminal to send data back through the printer TX + */ + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> + +#include "bsp/board_api.h" +#include "tusb.h" + +#include "usb_descriptors.h" + +// -------------------------------------------------------------------+ +// Tasks +// -------------------------------------------------------------------+ + +// Forward data from Printer RX to CDC TX +static void printer_to_cdc_task(void) { + uint32_t avail = tud_printer_read_available(); + if (avail == 0 || !tud_cdc_write_available()) { + return; + } + + uint8_t buf[64]; + uint32_t count = tud_printer_read(buf, TU_MIN(sizeof(buf), tud_cdc_write_available())); + if (count > 0) { + tud_cdc_write(buf, count); + tud_cdc_write_flush(); + } +} + +// Forward data from CDC RX to Printer TX +static void cdc_to_printer_task(void) { + uint32_t avail = tud_printer_write_available(); + if (tud_cdc_available() == 0 || avail == 0) { + return; + } + + uint8_t buf[64]; + uint32_t count = tud_cdc_read(buf, TU_MIN(sizeof(buf), avail)); + if (count > 0) { + tud_printer_write(buf, count); + tud_printer_write_flush(); + } +} + +int main(void) { + board_init(); + // init device stack on configured roothub port + tusb_rhport_init_t dev_init = {.role = TUSB_ROLE_DEVICE, .speed = TUSB_SPEED_AUTO}; + tusb_init(BOARD_TUD_RHPORT, &dev_init); + board_init_after_tusb(); + + while (1) { + tud_task(); // tinyusb device task + printer_to_cdc_task(); // forward printer data to CDC + cdc_to_printer_task(); // forward CDC data to printer + } +} + +//--------------------------------------------------------------------+ +// Printer callbacks +//--------------------------------------------------------------------+ + +void tud_printer_rx_cb(uint8_t itf) { + (void)itf; +} + +// IEEE 1284 Device ID: first 2 bytes are big-endian total length (including the 2 length bytes). +// The rest is the Device ID string using standard abbreviated keys. +static const char printer_device_id[] = + "\x00\x34" // total length = 52 = 0x0034 (big-endian) + "MFG:TinyUSB;" + "MDL:Printer to CDC;" + "CMD:PS;" + "CLS:PRINTER;"; + +TU_VERIFY_STATIC(sizeof(printer_device_id) - 1 == 52, "device ID length mismatch"); + +uint8_t const *tud_printer_get_device_id_cb(uint8_t itf) { + (void)itf; + return (uint8_t const *)printer_device_id; +} diff --git a/examples/device/printer_to_cdc/src/tusb_config.h b/examples/device/printer_to_cdc/src/tusb_config.h new file mode 100644 index 000000000..c38e8e1ee --- /dev/null +++ b/examples/device/printer_to_cdc/src/tusb_config.h @@ -0,0 +1,117 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef _TUSB_CONFIG_H_ +#define _TUSB_CONFIG_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +//--------------------------------------------------------------------+ +// Board Specific Configuration +//--------------------------------------------------------------------+ + +// RHPort number used for device can be defined by board.mk, default to port 0 +#ifndef BOARD_TUD_RHPORT + #define BOARD_TUD_RHPORT 0 +#endif + +// RHPort max operational speed can defined by board.mk +#ifndef BOARD_TUD_MAX_SPEED + #define BOARD_TUD_MAX_SPEED OPT_MODE_DEFAULT_SPEED +#endif + +//-------------------------------------------------------------------- +// COMMON CONFIGURATION +//-------------------------------------------------------------------- + +// defined by compiler flags for flexibility +#ifndef CFG_TUSB_MCU + #error CFG_TUSB_MCU must be defined +#endif + +#ifndef CFG_TUSB_OS + #define CFG_TUSB_OS OPT_OS_NONE +#endif + +#ifndef CFG_TUSB_DEBUG + #define CFG_TUSB_DEBUG 0 +#endif + +// Enable Device stack +#define CFG_TUD_ENABLED 1 + +// Default is max speed that hardware controller could support with on-chip PHY +#define CFG_TUD_MAX_SPEED BOARD_TUD_MAX_SPEED + +/* USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment. + * Tinyusb use follows macros to declare transferring memory so that they can be put + * into those specific section. + * e.g + * - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") )) + * - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4))) + */ +#ifndef CFG_TUSB_MEM_SECTION + #define CFG_TUSB_MEM_SECTION +#endif + +#ifndef CFG_TUSB_MEM_ALIGN + #define CFG_TUSB_MEM_ALIGN __attribute__((aligned(4))) +#endif + +//-------------------------------------------------------------------- +// DEVICE CONFIGURATION +//-------------------------------------------------------------------- + +#ifndef CFG_TUD_ENDPOINT0_SIZE + #define CFG_TUD_ENDPOINT0_SIZE 64 +#endif + +//------------- CLASS -------------// +#define CFG_TUD_HID 0 +#define CFG_TUD_CDC 1 +#define CFG_TUD_MSC 0 +#define CFG_TUD_MIDI 0 +#define CFG_TUD_VENDOR 0 +#define CFG_TUD_PRINTER 1 + +// CDC FIFO size of TX and RX +#define CFG_TUD_CDC_RX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) +#define CFG_TUD_CDC_TX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) + +// CDC Endpoint transfer buffer size +#define CFG_TUD_CDC_EP_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) + +// Printer buffer sizes +#define CFG_TUD_PRINTER_RX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) +#define CFG_TUD_PRINTER_TX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) +#define CFG_TUD_PRINTER_EP_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) + +#ifdef __cplusplus +} +#endif + +#endif /* _TUSB_CONFIG_H_ */ diff --git a/examples/device/printer_to_cdc/src/usb_descriptors.c b/examples/device/printer_to_cdc/src/usb_descriptors.c new file mode 100644 index 000000000..30d309ed4 --- /dev/null +++ b/examples/device/printer_to_cdc/src/usb_descriptors.c @@ -0,0 +1,208 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "bsp/board_api.h" +#include "tusb.h" + +#include "usb_descriptors.h" + +#define USB_VID 0xCafe +#define USB_PID 0x4005 +#define USB_BCD 0x0200 + +//--------------------------------------------------------------------+ +// Device Descriptors +//--------------------------------------------------------------------+ +static tusb_desc_device_t const desc_device = { + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = USB_BCD, + + // Use Interface Association Descriptor (IAD) for CDC + // As required by USB Specs IAD's subclass must be common class (2) and protocol must be IAD (1) + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + + .idVendor = USB_VID, + .idProduct = USB_PID, + .bcdDevice = 0x0100, + + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + + .bNumConfigurations = 0x01 +}; + +uint8_t const *tud_descriptor_device_cb(void) { + return (uint8_t const *) &desc_device; +} + +//--------------------------------------------------------------------+ +// Configuration Descriptor +//--------------------------------------------------------------------+ + +// Endpoint numbers +#if defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x02 + #define EPNUM_CDC_IN 0x83 + #define EPNUM_PRINTER_OUT 0x04 + #define EPNUM_PRINTER_IN 0x85 +#else + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x02 + #define EPNUM_CDC_IN 0x82 + #define EPNUM_PRINTER_OUT 0x03 + #define EPNUM_PRINTER_IN 0x83 +#endif + +// full speed configuration +static uint8_t const desc_fs_configuration[] = { + // Config number, interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), + + // Interface number, string index, EP notification address and size, EP data address (out, in) and size. + TUD_CDC_DESCRIPTOR(ITF_NUM_CDC, 4, EPNUM_CDC_NOTIF, 16, EPNUM_CDC_OUT, EPNUM_CDC_IN, 64), + + // Interface number, string index, EP Bulk Out address, EP Bulk In address, EP size + TUD_PRINTER_DESCRIPTOR(ITF_NUM_PRINTER, 5, EPNUM_PRINTER_OUT, EPNUM_PRINTER_IN, 64), +}; + +#if TUD_OPT_HIGH_SPEED +// high speed configuration +static uint8_t const desc_hs_configuration[] = { + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), + + TUD_CDC_DESCRIPTOR(ITF_NUM_CDC, 4, EPNUM_CDC_NOTIF, 16, EPNUM_CDC_OUT, EPNUM_CDC_IN, 512), + + TUD_PRINTER_DESCRIPTOR(ITF_NUM_PRINTER, 5, EPNUM_PRINTER_OUT, EPNUM_PRINTER_IN, 512), +}; + +// other speed configuration +static uint8_t desc_other_speed_config[CONFIG_TOTAL_LEN]; + +// device qualifier is mostly similar to device descriptor since we don't change configuration based on speed +static tusb_desc_device_qualifier_t const desc_device_qualifier = { + .bLength = sizeof(tusb_desc_device_qualifier_t), + .bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER, + .bcdUSB = USB_BCD, + + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .bNumConfigurations = 0x01, + .bReserved = 0x00 +}; + +uint8_t const *tud_descriptor_device_qualifier_cb(void) { + return (uint8_t const *) &desc_device_qualifier; +} + +uint8_t const *tud_descriptor_other_speed_configuration_cb(uint8_t index) { + (void) index; + + // if link speed is high return fullspeed config, and vice versa + memcpy(desc_other_speed_config, + (tud_speed_get() == TUSB_SPEED_HIGH) ? desc_fs_configuration : desc_hs_configuration, + CONFIG_TOTAL_LEN); + + desc_other_speed_config[1] = TUSB_DESC_OTHER_SPEED_CONFIG; + + return desc_other_speed_config; +} + +#endif // TUD_OPT_HIGH_SPEED + +uint8_t const *tud_descriptor_configuration_cb(uint8_t index) { + (void) index; + +#if TUD_OPT_HIGH_SPEED + return (tud_speed_get() == TUSB_SPEED_HIGH) ? desc_hs_configuration : desc_fs_configuration; +#else + return desc_fs_configuration; +#endif +} + +//--------------------------------------------------------------------+ +// String Descriptors +//--------------------------------------------------------------------+ + +enum { + STRID_LANGID = 0, + STRID_MANUFACTURER, + STRID_PRODUCT, + STRID_SERIAL, + STRID_CDC, + STRID_PRINTER, +}; + +static char const *string_desc_arr[] = { + (const char[]) { 0x09, 0x04 }, // 0: supported language is English (0x0409) + "TinyUSB", // 1: Manufacturer + "TinyUSB Device", // 2: Product + NULL, // 3: Serial, use unique ID if possible + "TinyUSB CDC", // 4: CDC Interface + "TinyUSB Printer", // 5: Printer Interface +}; + +static uint16_t _desc_str[32 + 1]; + +uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) { + (void) langid; + size_t chr_count; + + switch (index) { + case STRID_LANGID: + memcpy(&_desc_str[1], string_desc_arr[0], 2); + chr_count = 1; + break; + + case STRID_SERIAL: + chr_count = board_usb_get_serial(_desc_str + 1, 32); + break; + + default: + if (!(index < sizeof(string_desc_arr) / sizeof(string_desc_arr[0]))) { return NULL; } + + const char *str = string_desc_arr[index]; + + chr_count = strlen(str); + size_t const max_count = sizeof(_desc_str) / sizeof(_desc_str[0]) - 1; + if (chr_count > max_count) { chr_count = max_count; } + + for (size_t i = 0; i < chr_count; i++) { + _desc_str[1 + i] = str[i]; + } + break; + } + + _desc_str[0] = (uint16_t) ((TUSB_DESC_STRING << 8) | (2 * chr_count + 2)); + return _desc_str; +} diff --git a/examples/device/printer_to_cdc/src/usb_descriptors.h b/examples/device/printer_to_cdc/src/usb_descriptors.h new file mode 100644 index 000000000..830593b4d --- /dev/null +++ b/examples/device/printer_to_cdc/src/usb_descriptors.h @@ -0,0 +1,37 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef USB_DESCRIPTORS_H_ +#define USB_DESCRIPTORS_H_ + +enum { + ITF_NUM_CDC, + ITF_NUM_CDC_DATA, + ITF_NUM_PRINTER, + ITF_NUM_TOTAL, +}; + +#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN + TUD_PRINTER_DESC_LEN) + +#endif /* USB_DESCRIPTORS_H_ */ diff --git a/hw/bsp/rp2040/family.cmake b/hw/bsp/rp2040/family.cmake index 82d0034a1..e31abe50b 100644 --- a/hw/bsp/rp2040/family.cmake +++ b/hw/bsp/rp2040/family.cmake @@ -98,6 +98,7 @@ target_sources(tinyusb_device_base INTERFACE ${TOP}/src/class/mtp/mtp_device.c ${TOP}/src/class/net/ecm_rndis_device.c ${TOP}/src/class/net/ncm_device.c + ${TOP}/src/class/printer/printer_device.c ${TOP}/src/class/usbtmc/usbtmc_device.c ${TOP}/src/class/vendor/vendor_device.c ${TOP}/src/class/video/video_device.c diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 48dc75e50..00f466007 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -19,6 +19,7 @@ function(tinyusb_sources_get OUTPUT_VAR) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/mtp/mtp_device.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/net/ecm_rndis_device.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/net/ncm_device.c + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/printer/printer_device.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/usbtmc/usbtmc_device.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/vendor/vendor_device.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/video/video_device.c diff --git a/src/class/printer/printer.h b/src/class/printer/printer.h new file mode 100644 index 000000000..09d1a8956 --- /dev/null +++ b/src/class/printer/printer.h @@ -0,0 +1,62 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * This file is part of the TinyUSB stack. + */ + +#ifndef TUSB_PRINTER_H_ +#define TUSB_PRINTER_H_ + +#include "common/tusb_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/// Printer Class Specific Control Request +typedef enum { + TUSB_PRINTER_REQUEST_GET_DEVICE_ID = 0x00, ///< Get device ID + TUSB_PRINTER_REQUEST_GET_PORT_STATUS = 0x01, ///< Get port status + TUSB_PRINTER_REQUEST_SOFT_RESET = 0x02, ///< Soft reset +} tusb_printer_request_type_t; + +/// Printer Port Status (returned by GET_PORT_STATUS request) +/// USB Printer Class spec 1.1, Section 4.2 +typedef union TU_ATTR_PACKED { + uint8_t status; + struct TU_ATTR_PACKED { + uint8_t reserved0 : 3; ///< Reserved (bits 0-2) + uint8_t not_error : 1; ///< 1 = no error, 0 = error + uint8_t selected : 1; ///< 1 = selected (online), 0 = not selected + uint8_t paper_empty : 1; ///< 1 = paper empty, 0 = paper not empty + uint8_t reserved6 : 2; ///< Reserved (bits 6-7) + } status_bm; +} tusb_printer_port_status_t; + +TU_VERIFY_STATIC(sizeof(tusb_printer_port_status_t) == 1, "size is not correct"); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/class/printer/printer_device.c b/src/class/printer/printer_device.c new file mode 100644 index 000000000..f5bb33795 --- /dev/null +++ b/src/class/printer/printer_device.c @@ -0,0 +1,329 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * This file is part of the TinyUSB stack. + */ + +#include "tusb_option.h" + +#if (CFG_TUD_ENABLED && CFG_TUD_PRINTER) + +#include "device/usbd.h" +#include "device/usbd_pvt.h" + +#include "printer_device.h" + +//--------------------------------------------------------------------+ +// MACRO CONSTANT TYPEDEF +//--------------------------------------------------------------------+ + +typedef struct { + uint8_t itf_num; + + /*------------- From this point, data is not cleared by bus reset -------------*/ + + tu_edpt_stream_t rx_stream; + tu_edpt_stream_t tx_stream; + + uint8_t rx_ff_buf[CFG_TUD_PRINTER_RX_BUFSIZE]; + uint8_t tx_ff_buf[CFG_TUD_PRINTER_TX_BUFSIZE]; +} printer_interface_t; + +#define ITF_MEM_RESET_SIZE offsetof(printer_interface_t, rx_stream) + +#if CFG_TUD_EDPT_DEDICATED_HWFIFO == 0 +typedef struct { + TUD_EPBUF_DEF(epout, CFG_TUD_PRINTER_EP_BUFSIZE); + TUD_EPBUF_DEF(epin, CFG_TUD_PRINTER_EP_BUFSIZE); +} printer_epbuf_t; + +CFG_TUD_MEM_SECTION static printer_epbuf_t _printer_epbuf[CFG_TUD_PRINTER]; +#endif + +static printer_interface_t _printer_itf[CFG_TUD_PRINTER]; + +//--------------------------------------------------------------------+ +// INTERNAL HELPERS +//--------------------------------------------------------------------+ + +TU_ATTR_ALWAYS_INLINE static inline uint8_t _find_itf(uint8_t ep_addr) { + for (uint8_t i = 0; i < CFG_TUD_PRINTER; i++) { + const printer_interface_t *p = &_printer_itf[i]; + if (ep_addr == p->rx_stream.ep_addr || ep_addr == p->tx_stream.ep_addr) { + return i; + } + } + return TUSB_INDEX_INVALID_8; +} + +//--------------------------------------------------------------------+ +// Weak stubs: invoked if no strong implementation is available +//--------------------------------------------------------------------+ +TU_ATTR_WEAK void tud_printer_rx_cb(uint8_t itf) { + (void)itf; +} + +TU_ATTR_WEAK void tud_printer_tx_complete_cb(uint8_t itf) { + (void)itf; +} + +TU_ATTR_WEAK void tud_printer_request_complete_cb(uint8_t itf, tusb_control_request_t const *request) { + (void)itf; + (void)request; +} + +TU_ATTR_WEAK uint8_t const *tud_printer_get_device_id_cb(uint8_t itf) { + (void)itf; + return NULL; +} + +TU_ATTR_WEAK uint8_t tud_printer_get_port_status_cb(uint8_t itf) { + (void)itf; + return 0x18; // not error, selected, paper not empty +} + +TU_ATTR_WEAK void tud_printer_soft_reset_cb(uint8_t itf) { + (void)itf; +} + +//--------------------------------------------------------------------+ +// READ API +//--------------------------------------------------------------------+ +uint32_t tud_printer_n_read_available(uint8_t itf) { + TU_VERIFY(itf < CFG_TUD_PRINTER, 0); + return tu_edpt_stream_read_available(&_printer_itf[itf].rx_stream); +} + +uint32_t tud_printer_n_read(uint8_t itf, void *buffer, uint32_t bufsize) { + TU_VERIFY(itf < CFG_TUD_PRINTER, 0); + return tu_edpt_stream_read(&_printer_itf[itf].rx_stream, buffer, bufsize); +} + +bool tud_printer_n_peek(uint8_t itf, uint8_t *chr) { + TU_VERIFY(itf < CFG_TUD_PRINTER); + return tu_edpt_stream_peek(&_printer_itf[itf].rx_stream, chr); +} + +void tud_printer_n_read_flush(uint8_t itf) { + TU_VERIFY(itf < CFG_TUD_PRINTER, ); + printer_interface_t *p = &_printer_itf[itf]; + tu_edpt_stream_clear(&p->rx_stream); + tu_edpt_stream_read_xfer(&p->rx_stream); +} + +//--------------------------------------------------------------------+ +// WRITE API +//--------------------------------------------------------------------+ +uint32_t tud_printer_n_write(uint8_t itf, const void *buffer, uint32_t bufsize) { + TU_VERIFY(itf < CFG_TUD_PRINTER, 0); + return tu_edpt_stream_write(&_printer_itf[itf].tx_stream, buffer, bufsize); +} + +uint32_t tud_printer_n_write_flush(uint8_t itf) { + TU_VERIFY(itf < CFG_TUD_PRINTER, 0); + return tu_edpt_stream_write_xfer(&_printer_itf[itf].tx_stream); +} + +uint32_t tud_printer_n_write_available(uint8_t itf) { + TU_VERIFY(itf < CFG_TUD_PRINTER, 0); + return tu_edpt_stream_write_available(&_printer_itf[itf].tx_stream); +} + +bool tud_printer_n_write_clear(uint8_t itf) { + TU_VERIFY(itf < CFG_TUD_PRINTER); + tu_edpt_stream_clear(&_printer_itf[itf].tx_stream); + return true; +} + +//--------------------------------------------------------------------+ +// USBD-CLASS API +//--------------------------------------------------------------------+ +void printerd_init(void) { + tu_memclr(_printer_itf, sizeof(_printer_itf)); + + for (uint8_t i = 0; i < CFG_TUD_PRINTER; i++) { + printer_interface_t *p = &_printer_itf[i]; + + #if CFG_TUD_EDPT_DEDICATED_HWFIFO + uint8_t *epout_buf = NULL; + uint8_t *epin_buf = NULL; + #else + uint8_t *epout_buf = _printer_epbuf[i].epout; + uint8_t *epin_buf = _printer_epbuf[i].epin; + #endif + + tu_edpt_stream_init(&p->rx_stream, false, false, false, + p->rx_ff_buf, CFG_TUD_PRINTER_RX_BUFSIZE, + epout_buf, CFG_TUD_PRINTER_EP_BUFSIZE); + + tu_edpt_stream_init(&p->tx_stream, false, true, true, + p->tx_ff_buf, CFG_TUD_PRINTER_TX_BUFSIZE, + epin_buf, CFG_TUD_PRINTER_EP_BUFSIZE); + } +} + +bool printerd_deinit(void) { + for (uint8_t i = 0; i < CFG_TUD_PRINTER; i++) { + printer_interface_t *p = &_printer_itf[i]; + tu_edpt_stream_deinit(&p->rx_stream); + tu_edpt_stream_deinit(&p->tx_stream); + } + return true; +} + +void printerd_reset(uint8_t rhport) { + (void)rhport; + + for (uint8_t i = 0; i < CFG_TUD_PRINTER; i++) { + printer_interface_t *p = &_printer_itf[i]; + tu_memclr(p, ITF_MEM_RESET_SIZE); + tu_edpt_stream_close(&p->rx_stream); + tu_edpt_stream_close(&p->tx_stream); + } +} + +uint16_t printerd_open(uint8_t rhport, const tusb_desc_interface_t *itf_desc, uint16_t max_len) { + TU_VERIFY(TUSB_CLASS_PRINTER == itf_desc->bInterfaceClass, 0); + + // Find available interface slot + uint8_t const printer_id = _find_itf(0); + TU_ASSERT(printer_id < CFG_TUD_PRINTER, 0); + printer_interface_t *p = &_printer_itf[printer_id]; + + p->itf_num = itf_desc->bInterfaceNumber; + + //------------- Endpoints -------------// + const uint8_t *p_desc = (const uint8_t *)itf_desc; + const uint8_t *desc_end = p_desc + max_len; + uint16_t drv_len = sizeof(tusb_desc_interface_t); + + p_desc = tu_desc_next(itf_desc); + for (uint8_t e = 0; e < itf_desc->bNumEndpoints; e++) { + TU_VERIFY(tu_desc_in_bounds(p_desc, desc_end), 0); + const tusb_desc_endpoint_t *desc_ep = (const tusb_desc_endpoint_t *)p_desc; + TU_ASSERT(TUSB_DESC_ENDPOINT == desc_ep->bDescriptorType && TUSB_XFER_BULK == desc_ep->bmAttributes.xfer, 0); + + TU_ASSERT(usbd_edpt_open(rhport, desc_ep), 0); + + if (tu_edpt_dir(desc_ep->bEndpointAddress) == TUSB_DIR_IN) { + tu_edpt_stream_open(&p->tx_stream, rhport, desc_ep); + tu_edpt_stream_clear(&p->tx_stream); + } else { + tu_edpt_stream_open(&p->rx_stream, rhport, desc_ep); + tu_edpt_stream_clear(&p->rx_stream); + TU_ASSERT(tu_edpt_stream_read_xfer(&p->rx_stream) > 0, 0); + } + + drv_len += sizeof(tusb_desc_endpoint_t); + p_desc = tu_desc_next(p_desc); + } + + return drv_len; +} + +bool printerd_control_xfer_cb(uint8_t rhport, uint8_t stage, const tusb_control_request_t *request) { + TU_VERIFY(request->bmRequestType_bit.recipient == TUSB_REQ_RCPT_INTERFACE && + request->bmRequestType_bit.type == TUSB_REQ_TYPE_CLASS); + + // GET_DEVICE_ID: wIndex = (interface_number << 8) | alt_setting + // GET_PORT_STATUS / SOFT_RESET: wIndex = interface_number + uint8_t itf_num; + if (TUSB_PRINTER_REQUEST_GET_DEVICE_ID == request->bRequest) { + itf_num = tu_u16_high(request->wIndex); + } else { + itf_num = tu_u16_low(request->wIndex); + } + + // Find the printer instance index from the USB interface number + uint8_t itf = TUSB_INDEX_INVALID_8; + for (uint8_t i = 0; i < CFG_TUD_PRINTER; i++) { + if (_printer_itf[i].itf_num == itf_num) { + itf = i; + break; + } + } + TU_VERIFY(itf < CFG_TUD_PRINTER); + + // https://www.usb.org/sites/default/files/usbprint11a021811.pdf + if (stage == CONTROL_STAGE_SETUP) { + switch (request->bRequest) { + case TUSB_PRINTER_REQUEST_GET_DEVICE_ID: { + const uint8_t *device_id = tud_printer_get_device_id_cb(itf); + TU_VERIFY(device_id); + const uint16_t total_len = (uint16_t)((device_id[0] << 8) | device_id[1]); + return tud_control_xfer(rhport, request, (void *)(uintptr_t)device_id, total_len); + } + + case TUSB_PRINTER_REQUEST_GET_PORT_STATUS: { + static uint8_t port_status; + port_status = tud_printer_get_port_status_cb(itf); + return tud_control_xfer(rhport, request, &port_status, sizeof(port_status)); + } + + case TUSB_PRINTER_REQUEST_SOFT_RESET: + tud_printer_soft_reset_cb(itf); + tud_control_status(rhport, request); + return true; + + default: + return false; + } + } else if (stage == CONTROL_STAGE_ACK) { + tud_printer_request_complete_cb(itf, request); + } + + return true; +} + +bool printerd_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { + (void)rhport; + (void)result; + + uint8_t const itf = _find_itf(ep_addr); + TU_ASSERT(itf < CFG_TUD_PRINTER); + printer_interface_t *p = &_printer_itf[itf]; + + // Received new data + if (ep_addr == p->rx_stream.ep_addr) { + tu_edpt_stream_read_xfer_complete(&p->rx_stream, xferred_bytes); + + if (!tu_edpt_stream_empty(&p->rx_stream)) { + tud_printer_rx_cb(itf); + } + + tu_edpt_stream_read_xfer(&p->rx_stream); + } + + // Data sent to host + if (ep_addr == p->tx_stream.ep_addr) { + tud_printer_tx_complete_cb(itf); + + if (0 == tu_edpt_stream_write_xfer(&p->tx_stream)) { + tu_edpt_stream_write_zlp_if_needed(&p->tx_stream, xferred_bytes); + } + } + + return true; +} + +#endif diff --git a/src/class/printer/printer_device.h b/src/class/printer/printer_device.h new file mode 100644 index 000000000..a6b7052cb --- /dev/null +++ b/src/class/printer/printer_device.h @@ -0,0 +1,140 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * This file is part of the TinyUSB stack. + */ + +#ifndef TUSB_PRINTER_DEVICE_H_ +#define TUSB_PRINTER_DEVICE_H_ + +#include "printer.h" + +#ifdef __cplusplus +extern "C" { +#endif + +//--------------------------------------------------------------------+ +// Application API (Multiple Ports) i.e. CFG_TUD_PRINTER > 1 +//--------------------------------------------------------------------+ + +// Get the number of bytes available for reading +uint32_t tud_printer_n_read_available(uint8_t itf); + +// Read received bytes +uint32_t tud_printer_n_read(uint8_t itf, void *buffer, uint32_t bufsize); + +// Get the number of bytes available for writing +uint32_t tud_printer_n_write_available(uint8_t itf); + +// Clear the received FIFO +void tud_printer_n_read_flush(uint8_t itf); + +// Get a byte from FIFO without removing it +bool tud_printer_n_peek(uint8_t itf, uint8_t *ui8); + +// Write data to host +uint32_t tud_printer_n_write(uint8_t itf, const void *buffer, uint32_t bufsize); + +// Force sending data in the TX FIFO +uint32_t tud_printer_n_write_flush(uint8_t itf); + +// Clear the transmit FIFO +bool tud_printer_n_write_clear(uint8_t itf); + +//--------------------------------------------------------------------+ +// Application API (Single Port) +//--------------------------------------------------------------------+ + +TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_printer_read_available(void) { + return tud_printer_n_read_available(0); +} + +TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_printer_write_available(void) { + return tud_printer_n_write_available(0); +} + +TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_printer_read(void *buffer, uint32_t bufsize) { + return tud_printer_n_read(0, buffer, bufsize); +} + +TU_ATTR_ALWAYS_INLINE static inline void tud_printer_read_flush(void) { + tud_printer_n_read_flush(0); +} + +TU_ATTR_ALWAYS_INLINE static inline bool tud_printer_peek(uint8_t *ui8) { + return tud_printer_n_peek(0, ui8); +} + +TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_printer_write(const void *buffer, uint32_t bufsize) { + return tud_printer_n_write(0, buffer, bufsize); +} + +TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_printer_write_flush(void) { + return tud_printer_n_write_flush(0); +} + +TU_ATTR_ALWAYS_INLINE static inline bool tud_printer_write_clear(void) { + return tud_printer_n_write_clear(0); +} + +//--------------------------------------------------------------------+ +// Application Callback API (weak is optional) +//--------------------------------------------------------------------+ + +// Invoked when received new data +void tud_printer_rx_cb(uint8_t itf); + +// Invoked when last write transfer is completed +void tud_printer_tx_complete_cb(uint8_t itf); + +// Invoked when host requests device ID string (IEEE 1284). +// Application returns pointer to device ID buffer (must remain valid until transfer completes). +// First 2 bytes of returned buffer must contain big-endian length (including the 2 length bytes). +const uint8_t *tud_printer_get_device_id_cb(uint8_t itf); + +// Invoked when host requests port status. +uint8_t tud_printer_get_port_status_cb(uint8_t itf); + +// Invoked when host requests soft reset. +void tud_printer_soft_reset_cb(uint8_t itf); + +// Invoked when a control request is completed (GET_DEVICE_ID, GET_PORT_STATUS, etc.) +void tud_printer_request_complete_cb(uint8_t itf, tusb_control_request_t const *request); + + +//--------------------------------------------------------------------+ +// Internal Class Driver API +//--------------------------------------------------------------------+ +void printerd_init(void); +bool printerd_deinit(void); +void printerd_reset(uint8_t rhport); +uint16_t printerd_open(uint8_t rhport, const tusb_desc_interface_t *itf_desc, uint16_t max_len); +bool printerd_control_xfer_cb(uint8_t rhport, uint8_t stage, const tusb_control_request_t *request); +bool printerd_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t event, uint32_t xferred_bytes); + + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/device/usbd.c b/src/device/usbd.c index 127a7bd62..42903576c 100644 --- a/src/device/usbd.c +++ b/src/device/usbd.c @@ -334,6 +334,19 @@ static const usbd_class_driver_t _usbd_driver[] = { .sof = NULL }, #endif + + #if CFG_TUD_PRINTER + { + .name = DRIVER_NAME("PRINTER"), + .init = printerd_init, + .deinit = printerd_deinit, + .reset = printerd_reset, + .open = printerd_open, + .control_xfer_cb = printerd_control_xfer_cb, + .xfer_cb = printerd_xfer_cb, + .sof = NULL + }, + #endif }; enum { BUILTIN_DRIVER_COUNT = TU_ARRAY_SIZE(_usbd_driver) }; @@ -823,7 +836,9 @@ static bool process_control_request(uint8_t rhport, tusb_control_request_t const #if CFG_TUSB_DEBUG >= CFG_TUD_LOG_LEVEL if (TUSB_REQ_TYPE_STANDARD == p_request->bmRequestType_bit.type && p_request->bRequest <= TUSB_REQ_SYNCH_FRAME) { TU_LOG_USBD(" %s", tu_str_std_request[p_request->bRequest]); - if (TUSB_REQ_GET_DESCRIPTOR != p_request->bRequest) TU_LOG_USBD("\r\n"); + if (TUSB_REQ_GET_DESCRIPTOR != p_request->bRequest) { + TU_LOG_USBD("\r\n"); + } } #endif @@ -959,7 +974,25 @@ static bool process_control_request(uint8_t rhport, tusb_control_request_t const //------------- Class/Interface Specific Request -------------// case TUSB_REQ_RCPT_INTERFACE: { - uint8_t const itf = tu_u16_low(p_request->wIndex); + uint8_t itf; + #if CFG_TUD_PRINTER + // Printer GET_DEVICE_ID has a weird wIndex = interface (high) | alt (low) + // attempt to interpret this as a printer request if matched + if (TUSB_REQ_TYPE_CLASS == p_request->bmRequestType_bit.type && + TUSB_DIR_IN == p_request->bmRequestType_bit.direction && + TUSB_PRINTER_REQUEST_GET_DEVICE_ID == p_request->bRequest) { + itf = tu_u16_high(p_request->wIndex); + if (itf < TU_ARRAY_SIZE(_usbd_dev.itf2drv)) { + const usbd_class_driver_t * driver = get_driver(_usbd_dev.itf2drv[itf]); + if (driver != NULL && driver->control_xfer_cb == printerd_control_xfer_cb) { + if (invoke_class_control(rhport, driver, p_request)) { + return true; + } + } + } + } + #endif + itf = tu_u16_low(p_request->wIndex); TU_VERIFY(itf < TU_ARRAY_SIZE(_usbd_dev.itf2drv)); usbd_class_driver_t const * driver = get_driver(_usbd_dev.itf2drv[itf]); diff --git a/src/device/usbd.h b/src/device/usbd.h index 825fdba90..d3a6dccbb 100644 --- a/src/device/usbd.h +++ b/src/device/usbd.h @@ -297,6 +297,19 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_requ /* Endpoint In */\ 7, TUSB_DESC_ENDPOINT, _epin, TUSB_XFER_BULK, U16_TO_U8S_LE(_epsize), 0 +//--------------------------------------------------------------------+ +// Printer Descriptor Templates +//--------------------------------------------------------------------+ + +#define TUD_PRINTER_DESC_LEN (9 + 7 + 7) // one interface, two endpoints + +#define TUD_PRINTER_DESCRIPTOR(_itfnum, _stridx, _epout, _epin, _epsize) \ + /* Interface */\ + 9, TUSB_DESC_INTERFACE, _itfnum, 0, 2, TUSB_CLASS_PRINTER, 1, 2, _stridx,\ + /* Endpoint Out */\ + 7, TUSB_DESC_ENDPOINT, _epout, TUSB_XFER_BULK, U16_TO_U8S_LE(_epsize), 0,\ + /* Endpoint In */\ + 7, TUSB_DESC_ENDPOINT, _epin, TUSB_XFER_BULK, U16_TO_U8S_LE(_epsize), 0 //--------------------------------------------------------------------+ // MTP Descriptor Templates diff --git a/src/tinyusb.mk b/src/tinyusb.mk index 8f9d52de9..e7d1c0b5b 100644 --- a/src/tinyusb.mk +++ b/src/tinyusb.mk @@ -15,6 +15,7 @@ TINYUSB_SRC_C += \ src/class/mtp/mtp_device.c \ src/class/net/ecm_rndis_device.c \ src/class/net/ncm_device.c \ + src/class/printer/printer_device.c \ src/class/usbtmc/usbtmc_device.c \ src/class/video/video_device.c \ src/class/vendor/vendor_device.c \ diff --git a/src/tusb.h b/src/tusb.h index 3876bf863..c80c8433c 100644 --- a/src/tusb.h +++ b/src/tusb.h @@ -88,6 +88,10 @@ #include "class/msc/msc_device.h" #endif + #if CFG_TUD_PRINTER + #include "class/printer/printer_device.h" + #endif + #if CFG_TUD_MTP #include "class/mtp/mtp_device.h" #endif diff --git a/src/tusb_option.h b/src/tusb_option.h index 5b8f597b6..4450c7c45 100644 --- a/src/tusb_option.h +++ b/src/tusb_option.h @@ -658,6 +658,10 @@ #define CFG_TUD_NCM 0 #endif +#ifndef CFG_TUD_PRINTER + #define CFG_TUD_PRINTER 0 +#endif + #ifndef CFG_TUD_EDPT_DEDICATED_HWFIFO #define CFG_TUD_EDPT_DEDICATED_HWFIFO 0 #endif diff --git a/test/fuzz/rules.mk b/test/fuzz/rules.mk index b32f8d695..329dcce11 100644 --- a/test/fuzz/rules.mk +++ b/test/fuzz/rules.mk @@ -32,6 +32,7 @@ SRC_C += \ src/class/midi/midi_device.c \ src/class/msc/msc_device.c \ src/class/mtp/mtp_device.c \ + src/class/printer/printer_device.c \ src/class/net/ecm_rndis_device.c \ src/class/net/ncm_device.c \ src/class/usbtmc/usbtmc_device.c \ diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 757456806..f3cead7a3 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -166,6 +166,32 @@ def open_mtp_dev(uid): return None +def get_printer_dev(id, vendor_str, product_str, ifnum): + """Find /dev/usb/lpX by matching USB serial, vendor, product, and interface number via sysfs""" + vendor_str = vendor_str.replace(' ', '_') if vendor_str else '' + product_str = product_str.replace(' ', '_') if product_str else '' + for lp in glob.glob('/sys/class/usbmisc/lp*'): + try: + sn = open(f'{lp}/device/../serial').read().strip() + if sn == id: + return f'/dev/usb/{os.path.basename(lp)}' + except (FileNotFoundError, PermissionError, ValueError): + pass + return None + + +def open_printer_dev(id, vendor_str, product_str, ifnum): + """Wait for printer device to enumerate and return its path""" + timeout = ENUM_TIMEOUT + while timeout > 0: + lp_dev = get_printer_dev(id, vendor_str, product_str, ifnum) + if lp_dev and os.path.exists(lp_dev): + return lp_dev + time.sleep(1) + timeout -= 1 + assert False, f'Printer device not found for {id} if{ifnum:02d}' + + # ------------------------------------------------------------- # Flashing firmware # ------------------------------------------------------------- @@ -552,6 +578,109 @@ def test_device_hid_composite_freertos(id): pass +def test_device_printer_to_cdc(board): + import threading + + uid = board['uid'] + + # Wait for CDC port and printer device + cdc_port = get_serial_dev(uid, 'TinyUSB', "TinyUSB_Device", 0) + ser = open_serial_dev(cdc_port) + lp_dev = open_printer_dev(uid, 'TinyUSB', 'TinyUSB_Device', 2) + + # Test 0: Verify IEEE 1284 Device ID from sysfs + expected_id = 'MFG:TinyUSB;MDL:Printer to CDC;CMD:PS;CLS:PRINTER;' + lp_name = os.path.basename(lp_dev) + sysfs_id_path = f'/sys/class/usbmisc/{lp_name}/device/ieee1284_id' + if os.path.exists(sysfs_id_path): + with open(sysfs_id_path) as f: + ieee1284_id = f.read().strip() + if ieee1284_id: + assert ieee1284_id == expected_id, (f'IEEE 1284 ID mismatch:\n' + f' expected: {expected_id}\n got: {ieee1284_id}') + + def rand_ascii(length): + return "".join(random.choices(string.ascii_letters + string.digits, k=length)).encode("ascii") + + sizes = [32, 64, 128, 256, 512, random.randint(2000, 5000)] + + # flush any stale data + ser.reset_input_buffer() + + # Test 1: Printer -> CDC with multiple sizes, write in random 1-64 byte chunks + for size in sizes: + test_data = rand_ascii(size) + ser.reset_input_buffer() + rd = b'' + offset = 0 + with open(lp_dev, 'wb') as lp: + while offset < size: + chunk_size = min(random.randint(1, 64), size - offset) + lp.write(test_data[offset:offset + chunk_size]) + lp.flush() + rd += ser.read(chunk_size) + offset += chunk_size + # read any remaining bytes (fullspeed devices may need extra time) + while len(rd) < size: + remaining = ser.read(size - len(rd)) + if not remaining: + break + rd += remaining + assert rd == test_data, (f'Printer->CDC wrong data ({size} bytes):\n' + f' expected: {test_data[:64]}\n received: {rd[:64]}') + + # Test 2: CDC -> Printer with multiple sizes, write in random 1-64 byte chunks + # Use a thread to read from printer since /dev/usb/lp read blocks + ser.reset_input_buffer() + time.sleep(0.5) + for size in sizes: + test_data = rand_ascii(size) + rd_result = [b'', None] # [data, error] + reader_ready = threading.Event() + + def lp_reader(): + try: + rd = b'' + fd = os.open(lp_dev, os.O_RDONLY) + reader_ready.set() + try: + while len(rd) < size: + chunk = os.read(fd, min(64, size - len(rd))) + if not chunk: + break + rd += chunk + finally: + os.close(fd) + rd_result[0] = rd + except Exception as e: + rd_result[1] = e + reader_ready.set() + + reader = threading.Thread(target=lp_reader, daemon=True) + reader.start() + # wait for reader to open lp device before writing + reader_ready.wait(timeout=5) + time.sleep(0.1) + + # Write to CDC in small chunks with flush to avoid overflowing device FIFO + offset = 0 + while offset < size: + chunk_size = min(random.randint(1, 64), size - offset) + ser.write(test_data[offset:offset + chunk_size]) + ser.flush() + time.sleep(0.01) + offset += chunk_size + + reader.join(timeout=10) + assert not reader.is_alive(), f'CDC->Printer timeout ({size} bytes)' + assert rd_result[1] is None, f'CDC->Printer read error: {rd_result[1]}' + assert rd_result[0] == test_data, (f'CDC->Printer wrong data ({size} bytes):\n' + f' expected: {test_data[:64]}\n received: {rd_result[0][:64]}') + time.sleep(0.2) + + ser.close() + + def test_device_mtp(board): uid = board['uid'] @@ -623,6 +752,7 @@ device_tests = [ 'device/dfu_runtime', 'device/cdc_msc_freertos', 'device/hid_boot_interface', + 'device/printer_to_cdc', # 'device/mtp' ] diff --git a/tools/iar_template.ipcf b/tools/iar_template.ipcf index caddda826..035e40b94 100644 --- a/tools/iar_template.ipcf +++ b/tools/iar_template.ipcf @@ -69,6 +69,11 @@ <path>$TUSB_DIR$/src/class/net/ncm.h</path> <path>$TUSB_DIR$/src/class/net/net_device.h</path> </group> + <group name="src/class/printer"> + <path>$TUSB_DIR$/src/class/printer/printer_device.c</path> + <path>$TUSB_DIR$/src/class/printer/printer.h</path> + <path>$TUSB_DIR$/src/class/printer/printer_device.h</path> + </group> <group name="src/class/usbtmc"> <path>$TUSB_DIR$/src/class/usbtmc/usbtmc_device.c</path> <path>$TUSB_DIR$/src/class/usbtmc/usbtmc.h</path> |
