summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHa Thach <[email protected]>2026-03-06 19:59:34 +0700
committerGitHub <[email protected]>2026-03-06 19:59:34 +0700
commit6c2c309316794ee580603db1b48422f9c648b1ad (patch)
tree2a86d1abba22be6b7c68683940b0388429f8519c
parent06a4c6d7190ef83f76b2a65496f2a48a54a8c134 (diff)
parent8878e02c3040ee0b30f9b26c20f45a36260d854a (diff)
Merge pull request #3444 from remiberthoz/device-class-printer
Implement Printer Device Driver
-rw-r--r--AGENTS.md3
-rw-r--r--README.rst1
-rw-r--r--examples/device/CMakeLists.txt1
-rw-r--r--examples/device/printer_to_cdc/CMakeLists.txt29
-rw-r--r--examples/device/printer_to_cdc/Makefile11
-rw-r--r--examples/device/printer_to_cdc/README.md80
-rw-r--r--examples/device/printer_to_cdc/src/main.c117
-rw-r--r--examples/device/printer_to_cdc/src/tusb_config.h117
-rw-r--r--examples/device/printer_to_cdc/src/usb_descriptors.c208
-rw-r--r--examples/device/printer_to_cdc/src/usb_descriptors.h37
-rw-r--r--hw/bsp/rp2040/family.cmake1
-rw-r--r--src/CMakeLists.txt1
-rw-r--r--src/class/printer/printer.h62
-rw-r--r--src/class/printer/printer_device.c329
-rw-r--r--src/class/printer/printer_device.h140
-rw-r--r--src/device/usbd.c37
-rw-r--r--src/device/usbd.h13
-rw-r--r--src/tinyusb.mk1
-rw-r--r--src/tusb.h4
-rw-r--r--src/tusb_option.h4
-rw-r--r--test/fuzz/rules.mk1
-rwxr-xr-xtest/hil/hil_test.py130
-rw-r--r--tools/iar_template.ipcf5
23 files changed, 1330 insertions, 2 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 34fc57cb8..bbbd7c36d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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>