Plug in a new USB gadget and sometimes it politely announces, “I’m a lab instrument, not a thumb drive.” That’s USBTMC—the USB Test and Measurement Class—quietly enabling oscilloscopes, power supplies, and homebrew boards to speak SCPI without drama.


Anatomy of a USBTMC Device

USBTMC Class Codes: Class Codes

A proper USBTMC device struts onto the bus with a few essentials:

  • Control Endpoint (EP0): Mandatory. Think of it as the bouncer at the club—everybody has to check in here.
  • Bulk-IN (EP1): The data firehose coming back from the device.
  • Bulk-OUT (EP2): Where the host pours SCPI commands like *IDN? into your gadget.
  • Interrupt-IN (EP3): Optional, but handy for poking the host when something interesting happens.

The Communication Model

The conversation goes something like this:

  1. Host says: “Identify yourself!” → sends *IDN? over Bulk-OUT.
  2. Device answers via Bulk-IN: “I am a respectable instrument, thank you very much.”
  3. Rinse and repeat with more SCPI commands until someone pulls the cable.

Under the hood the usual descriptor crew shows up: device, configuration, interface, endpoints. They aren’t glamorous, but without them nothing enumerates.


Device Descriptor

/* USB descriptor definition */
typedef struct _usb_desc_header {
    uint8_t bLength;                              /*!< size of the descriptor */
    uint8_t bDescriptorType;                      /*!< type of the descriptor */
} usb_desc_header;

typedef struct _usb_desc_dev {
    usb_desc_header header;                       /*!< descriptor header, including type and size */
    uint16_t bcdUSB;                              /*!< BCD of the supported USB specification */
    uint8_t  bDeviceClass;                        /*!< USB device class */
    uint8_t  bDeviceSubClass;                     /*!< USB device subclass */
    uint8_t  bDeviceProtocol;                     /*!< USB device protocol */
    uint8_t  bMaxPacketSize0;                     /*!< size of the control (address 0) endpoint's bank in bytes */
    uint16_t idVendor;                            /*!< vendor ID for the USB product */
    uint16_t idProduct;                           /*!< unique product ID for the USB product */
    uint16_t bcdDevice;                           /*!< product release (version) number */
    uint8_t  iManufacturer;                       /*!< string index for the manufacturer's name */
    uint8_t  iProduct;                            /*!< string index for the product name/details */
    uint8_t  iSerialNumber;                       /*!< string index for the product's globally unique hexadecimal serial number */
    uint8_t  bNumberConfigurations;               /*!< total number of configurations supported by the device */
} usb_desc_dev;
/// USB standard device descriptor
usb_desc_dev tmc_dev_desc = {
    /* Table 40 -- Device Descriptor */
    .header = {
        .bLength          = USB_DEV_DESC_LEN,
        .bDescriptorType  = USB_DESCTYPE_DEV,
    },
    .bcdUSB                = 0x0200U,
    .bDeviceClass          = USB_CLASS_DEVICE,
    .bDeviceSubClass       = 0x00U,
    .bDeviceProtocol       = 0x00U,
    .bMaxPacketSize0       = USBD_EP0_MAX_SIZE,
    .idVendor              = USBD_VID,
    .idProduct             = USBD_PID,
    .bcdDevice             = 0x0001U,
    .iManufacturer         = STR_IDX_MFC,
    .iProduct              = STR_IDX_PRODUCT,
    .iSerialNumber         = STR_IDX_SERIAL,
    .bNumberConfigurations = USBD_CFG_MAX_NUM,
};


Configuration Descriptor

typedef struct _usb_desc_config {
    usb_desc_header header;                       /*!< descriptor header, including type and size */
    uint16_t wTotalLength;                        /*!< size of the configuration descriptor header, and all sub descriptors inside the configuration */
    uint8_t  bNumInterfaces;                      /*!< total number of interfaces in the configuration */
    uint8_t  bConfigurationValue;                 /*!< configuration index of the current configuration */
    uint8_t  iConfiguration;                      /*!< index of a string descriptor describing the configuration */
    uint8_t  bmAttributes;                        /*!< configuration attributes */
    uint8_t  bMaxPower;                           /*!< maximum power consumption of the device while in the current configuration */
} usb_desc_config;
/// USB device configuration descriptor
usb_tmc_desc_config_set tmc_config_desc = {
    .config = {
        .header = {
            .bLength         = sizeof(usb_desc_config),
            .bDescriptorType = USB_DESCTYPE_CONFIG,
        },
        .wTotalLength         = sizeof(usb_tmc_desc_config_set),
        .bNumInterfaces       = 0x01U,
        .bConfigurationValue  = 0x01U,
        .iConfiguration       = 0x00U,
        .bmAttributes         = 0xe0U,
        .bMaxPower            = 0x32U
    },


Interface Descriptor

typedef struct _usb_desc_itf {
    usb_desc_header header;                       /*!< descriptor header, including type and size */
    uint8_t bInterfaceNumber;                     /*!< index of the interface in the current configuration */
    uint8_t bAlternateSetting;                    /*!< alternate setting for the interface number */
    uint8_t bNumEndpoints;                        /*!< total number of endpoints in the interface */
    uint8_t bInterfaceClass;                      /*!< interface class ID */
    uint8_t bInterfaceSubClass;                   /*!< interface subclass ID */
    uint8_t bInterfaceProtocol;                   /*!< interface protocol ID */
    uint8_t iInterface;                           /*!< index of the string descriptor describing the interface */
} usb_desc_itf;
/* Table 21 -- USB488 interface descriptor */
.interface = {
	.header = {
		.bLength         = sizeof(usb_desc_itf),
		.bDescriptorType = USB_DESCTYPE_ITF,
	},
	.bInterfaceNumber     = 0x00U,
	.bAlternateSetting    = 0x00U,
	.bNumEndpoints        = 0x02U,
	.bInterfaceClass      = USB_CLASS_APPLICATION,
	.bInterfaceSubClass   = USB_APPLICATION_SUBCLASS_TMC,
	.bInterfaceProtocol   = USBTMC_PROTOCOL_NONE,
	.iInterface           = 0x00U
},


Endpoint Descriptors

typedef struct _usb_desc_ep {
    usb_desc_header header;                       /*!< descriptor header, including type and size */
    uint8_t  bEndpointAddress;                    /*!< logical address of the endpoint */
    uint8_t  bmAttributes;                        /*!< endpoint attribute */
    uint16_t wMaxPacketSize;                      /*!< size of the endpoint bank, in bytes */
    uint8_t  bInterval;                           /*!< polling interval in milliseconds for the endpoint if it is an INTERRUPT or ISOCHRONOUS type */
} usb_desc_ep;
/* 5.6.1 Bulk-IN Endpoint Descriptor */
.in_endpoint = {
	.header = {
		.bLength         = sizeof(usb_desc_ep),
		.bDescriptorType = USB_DESCTYPE_EP
	},
	.bEndpointAddress     = TMC_IN_EP,
	.bmAttributes         = USB_EP_ATTR_BULK,
	.wMaxPacketSize       = TMC_EP_SIZE,
	// .bInterval            = 0x00U
	.bInterval            = 0x01U
},


USBTMC Get Capabilities

static const struct usb_tmc_get_capabilities_response capabilities = {
    .status                 = USBTMC_STATUS_SUCCESS,
    .reserved0              = 0x00,
    .bcdUSBTMC              = 0x0100,
    .interface_capabilities = 0x00,
    .device_capabilities    = 0x00,
    .reserved1              = {0x00},
    .reserved2              = {0x00},
};
/// Table 37 -- GET_CAPABILITIES response format
struct usb_tmc_get_capabilities_response {
   /* Status indication for this request. See Table 16. */
   uint8_t status;
   /* Reserved. Must be 0x00. */
   uint8_t reserved0;
   /* BCD version number of the relevant USBTMC specification for
      this USBTMC interface. Format is as specified for bcdUSB in the
      USB 2.0 specification, section 9.6.1. */
   uint16_t bcdUSBTMC;
   /* bit2:
        1 – The USBTMC interface accepts the
            INDICATOR_PULSE request.
        0 – The USBTMC interface does not accept the
            INDICATOR_PULSE request. The device, when
            an INDICATOR_PULSE request is received,
            must treat this command as a non-defined
            command and return a STALL handshake
            packet.
      bit1:
        1 – The USBTMC interface is talk-only.
        0 – The USBTMC interface is not talk-only.
      bit0:
        1 – The USBTMC interface is listen-only.
        0 – The USBTMC interface is not listen-only.*/
   uint8_t interface_capabilities;
   /* bit0:
        1 – The device supports ending a Bulk-IN transfer
            from this USBTMC interface when a byte
            matches a specified TermChar.
        0 – The device does not support ending a Bulk-IN
            transfer from this USBTMC interface when a
            byte matches a specified TermChar. */
   uint8_t device_capabilities;
   /* Reserved for USBTMC use. All bytes must be 0x00. */
   uint8_t reserved1[6];
   /* Reserved for USBTMC subclass use. If no subclass specification
      applies, all bytes must be 0x00. */
   uint8_t reserved2[12];
} __attribute__((packed));

USBTMC Bulk-IN Header

/// Table 1 -- USBTMC message Bulk-OUT Header
struct usb_tmc_bulk_header {
   /* Specifies the USBTMC message and the type of the
      USBTMC message. See Table 2. */
   uint8_t MsgID;
   /* A transfer identifier. The Host must set bTag
      different than the bTag used in the previous Bulk-
      OUT Header. The Host should increment the bTag
      by 1 each time it sends a new Bulk-OUT Header.
      The Host must set bTag such that 1<=bTag<=255. */
   uint8_t bTag;
   /* The inverse (one’s complement) of the bTag. For
      example, the bTagInverse of 0x5B is 0xA4. */
   uint8_t bTagInverse;
   /* Reserved. Must be 0x00. */
   uint8_t reserved;

   union {
      /* USBTMC command message specific. See section 3.2.1. */
      uint8_t message[8];
      /// Table 3 -- DEV_DEP_MSG_OUT Bulk-OUT Header with command specific content
      struct _dev_dep_msg_out {
         /* Total number of USBTMC message data bytes to be
          sent in this USB transfer. This does not include the
          number of bytes in this Bulk-OUT Header or
          alignment bytes. Sent least significant byte first,
          most significant byte last. TransferSize must be >
          0x00000000. */
         uint32_t transferSize;
         /* bit0 EOM.
            1 - The last USBTMC message data byte
                in the transfer is the last byte of
                the USBTMC message.
            0 – The last USBTMC message data byte
                in the transfer is not the last byte of
                the USBTMC message. */
         uint8_t bmTransferAttributes;
         /* Reserved. Must be 0x000000. */
         uint8_t reserved[3];
      } dev_dep_msg_out;

      /// Table 4 -- REQUEST_DEV_DEP_MSG_IN Bulk-OUT Header with command specific content
      struct _dev_dep_msg_in {
         /* Maximum number of USBTMC message data bytes to
            be sent in response to the command. This does not
            include the number of bytes in this Bulk-IN Header or
            alignment bytes. Sent least significant byte first, most
            significant byte last. TransferSize must be >
            0x00000000. */
         uint32_t transferSize;
         /* bit1 TermCharEnabled.
            1 – The Bulk-IN transfer must terminate
                on the specified TermChar. The Host
                may only set this bit if the USBTMC
                interface indicates it supports
                TermChar in the GET_CAPABILITIES
                response packet.
            0 – The device must ignore TermChar. */
         uint8_t bmTransferAttributes;
         /* If bmTransferAttributes.D1 = 1, TermChar is an 8-bit
            value representing a termination character. If
            supported, the device must terminate the Bulk-IN
            transfer after this character is sent.
            If bmTransferAttributes.D1 = 0, the device must ignore
            this field. */
         uint8_t TermChar;
         /* Reserved. Must be 0x0000. */
         uint8_t reserved[2];
      } dev_dep_msg_in;

      /// Table 5 -- VENDOR_SPECIFIC_OUT Bulk-OUT Header with command specific content
      struct _vendor_specific_out {
         /* Total number of USBTMC message data bytes to be
            sent in this USB transfer. This does not include the
            number of bytes in this Bulk-OUT Header or
            alignment bytes. Sent least significant byte first,
            most significant byte last. TransferSize must be >
            0x00000000. */
         uint32_t transferSize;
         /* Reserved. Must be 0x0000000. */
         uint8_t reserved[4];
      } vendor_specific_out;

      /// Table 6 -- REQUEST_VENDOR_SPECIFIC_IN Bulk-OUT Header with command specific content
      struct _vendor_specific_in {
         /* Maximum number of USBTMC message data bytes to
            be sent in response to the command. This does not
            include the number of bytes in this Bulk-IN Header or
            alignment bytes. Sent least significant byte first, most
            significant byte last. TransferSize must be >
            0x00000000. */
         uint32_t transferSize;
         /* Reserved. Must be 0x00000000. */
         uint8_t reserved[4];
      } vendor_specific_in;
   } command_specific;
} __attribute__((packed));


Example: A 488.2 USBTMC Message

  • Send: *IDN?

  • Response:


Enumeration Process


Demo Time

When implemented correctly, sending SCPI commands through USB feels like magic:


Useful Resources


Closing Thoughts

USBTMC turns your microcontroller into a lab-friendly chatterbox. With just a few endpoints and some SCPI sprinkled in, you can make your DIY gizmo play nice with professional tools—or at least pretend to.

Descriptors might feel dull, yet the conversations they unlock—automated measurements, scripted calibrations, blinking front-panel LEDs—are worth the paperwork.