I remember my first try to understand the basics of USB bus, tons of ambiguous terms here and there: descriptors, pipes, interface, classes, …etc. Even explanations and definitions were there; it was very hard to connect these terms all together with a practical understanding. This article is a try to untangle a tangled ball of yarns of terms of USB. It is not a complete tutorial or guide to USB; only a try that can hopefully give you the basic understanding that allows you to read other available advanced articles or books about USB.
Instead of describing all terms and concepts in detail one by one, this article used a proactive way to merge a good part of terms and concepts together with some practical approach.
A Warm up Introduction
The Universal Serial Bus Specification Revision 2.0 defines USB bus as a “ cable bus that supports data exchange between a host computer and a wide range of simultaneously accessible peripherals”, and this provides one type of connector with different types of devices with different functionality. The folks behind this protocol are USB-IF (USB Implementers Forum) that standardized USB bus.
USB protocol uses star topology starting from a single node called ‘host’ (root hub) and branches to other nodes. If the child node branches again then it is a hub node, otherwise it will be a functional peripheral called ‘Func’ in other words the USB device to be used by a user.
By definition Hubs provide additional attachment points to the USB, while Functions provide capabilities to the system, such as a printer or mouse.
Each USB device needs 2 unique identifiers: the first one is called VID or Vendor ID and it is assigned by the USB-IF, so chips with USB core from Microchip have a VID different from NXP one. The other is called PID or Product ID assigned by the manufacturer not the Implementers Forum USB-IF.
So by knowing the VID and PID you can know what the device and manufacturer name. For example, I’m using a wireless mouse from Logitech, and by getting the VID/PID of USB wireless USB dongle from device manager in Windows or by running lsusb command for Linux, I get this pair 0x046d/0xc534. Now, searching with USB-IF database showing the device I use:
As USB is a cable bus, then you should know why there are different shapes of USB connectors. Actually, all contain basically the same signals +5V, GND, D+, and D-. Basically, you can use any connector type you like in your design even it is not the standard, for example, type ‘A’ in the both ends, type ‘A’ in host side and ‘B’ in the device side, or ‘B’ in host side and ‘A’ in device side or even type ‘B’ in both sides, but you should know that connector type/shape is symbolic to device data stream direction, downstream or upstream. That’s why we used to see printers with a type ‘B’ connector as the printer is a device not a host and ‘B’ type is used as an indication to downstream from host to device.
There is no reason to stop you to use any type in any application, but it was important to mention why these 2 different physical shapes are introduced to indicate device type from the connector shape.
USB is a known used bus for storage devices, because it supports high-speed datarate. USB2.0 has 3 different data rates:
- The USB high-speed signaling bit rate is 480 Mb/s.
- The USB full-speed signaling bit rate is 12 Mb/s.
- The USB low-speed signaling bit rate is 1.5 Mb/s.
A pullup resistor on either D- or D+ will select the USB speed the host should use to communicate with the device. If the pullup resistor is connected to D+ then the speed will be Full-speed, and if connected to D- the speed will be Low-speed. The host senses the voltage levels once the device is connected and knows the device class of speed according to resistor configuration.
USB has four basic types of data transfers to choose based on your application:
- Control Transfers: To configure a device at attach time.
- Bulk Data Transfers: To transmit relatively large and bursty quantities of data.
- Interrupt Data Transfers: Used for timely but reliable delivery of data, for example, mouse or keyboard devices.
- Isochronous Data Transfers: No error correction. Used for streaming real-time transfers.
Based on data transfer type used in the device, USB-IF defines such classes:
- HID (Human Interface Device) / Interrupt.
- MSD (Mass storage Device) / bulk.
- CDC (Communication Device Class) / interrupt + bulk.
More classes are mentioned in an updated Defined Class Codes table at the USB-IF official website.
A Step toward USB in Practice
Once the USB device is connected to a Host, one of the first things to do is to describe itself to the host. This is done through what is called Descriptors. Such Describes are transferred through an Endpoint called Endpoint zero. An Endpoint by definition is “A uniquely addressable portion of a USB device that is the source or sink of information in a communication flow between the host and device”. Each Endpoint has address and direction (IN or OUT).
- IN: From device to host.
- OUT: From host to device.
The Default Control Pipe to Endpoint Zero provides access to the device’s configuration information and allows generic USB status and control access.
One of the key terms you will find in any practical USB device firmware is the descriptor. As the name may imply and as the USB Specification defines it, “USB devices report their attributes using descriptors. A descriptor is a data structure with a defined format. Each descriptor begins with a byte-wide field that contains the total number of bytes in the descriptor followed by a byte-wide field that identifies the descriptor type”.
The last term mentioned in this introduction paragraph is Pipe and it is by definition “an association between an endpoint on a device and software on the host. Pipes represent the ability to move data between the software on the host via a memory buffer and an endpoint on a device.”
However, USB is not a one layer protocol, like many other protocols. An overview of the involved layers are shown in the following diagram:
The USB-IF defined set of standard descriptors which are:
- Device.
- Device_Qualifier.
- Configuration.
- Other_Speed_Qualifier.
- Interface.
- Endpoint.
- String.
This is an example of Interface descriptor:
bDescriptorType value for Interface descriptor is ‘4’ according to the bDescriptorType values table:
To show you a real descriptor, I used Wireshark with USBPcap (a USB sniffer) to capture USB traffic. I have a USB transceiver device from Logitech and here is the Device Descriptor:
Check idVendor and idProduct fields numbers and you can see that they are matching the numbers we saw in the before in the beginning. Moreover, The descriptor format is matching the format by definition stated in USB Specification:
The host may request additional descriptors to describe the device using a string and that is why it is called string descriptors. For instance, Device Descriptor refers to the iProduct field to string descriptor with index 2, so the Host will request it with that index.
However, String descriptors are optional. If a device does not support string descriptors, all references to string descriptors within device, configuration, and interface descriptors must be reset to zero.
A “Hello World” PC Program to Interact with a USB Device
To make things more practical, we will write a small PC program in C++ to scan USB devices, print the VID:PID and show a string descriptor for a connected mouse. The index of the string descriptor will be retrieved from the device descriptor. I will use the well-known library Libusb which is a C library that provides generic access to USB devices and it is cross-platform between Linux, macOS and Windows.
To use Libusb you need to download the source code and build it to generate .lib or .dll files. However, there are ready binaries on the official website for windows.
There are 2 builds inside the ready binaries version, one using MinGW and the other one using the Microsoft Compiler.
To use Libusb you need to tell the linker where the library files (.lib) is or to load .dll file in run time, and to include the libusb.h header file in the code.
During my trials using compiled LIB of Libusb with Microsoft Visual Studio Community 2019 I encountered errors from the library. Later, I found the reason behind these errors (.i.e: unresolved external symbol __imp__iob and Unresolved external symbol _sprintf) which were from the incompatibility between the compiler version used to build the official Libusb and my compiler version. Explanations like “Microsoft sometimes makes changes to their C runtime, creating incompatibilities between libraries compiled with different versions” seems reasonable and I had no way other than compiling the Libusb source code using VS again.
The good thing is that Libusb source code has a VS project (found here libusb-1.0.23\msvc\libusb_static_2017.vcxproj) that can be imported then compiled easily.
Now, The code to get string descriptor of a USB Mouse (as an example) is straightforward:
First we initialize the library using libusb_init, then scan and get a list of connected USB devices to the Host using libusb_get_device_list function. Finally, we request a device descriptor using libusb_get_device_descriptor.
To demonstrate one way of how a host can interact with a device descriptor, we will request to get a string descriptor pointed by the iProduct field in the device descriptor which is a string describing the product. But we need to call libusb_open with that device and get a handle (reference number) to be used later with libusb_get_string_descriptor_ascii function.
#include <libusb.h> #include <stdio.h> libusb_context* context = NULL; static void find_dev(libusb_device **devs) { libusb_device *dev; int i = 0, j = 0; while ((dev = devs[i++]) != NULL) { struct libusb_device_descriptor desc; libusb_device_handle* handle = NULL; int ret; char string[256]; int r = libusb_get_device_descriptor(dev, &desc); if (r < 0) { fprintf(stderr, "failed to get device descriptor"); return; } ret = libusb_open(dev, &handle); if (LIBUSB_SUCCESS == ret) { printf("get %04x:%04x device string descriptor \n", desc.idVendor, desc.idProduct); printf("iProduct[%d]:\n", desc.iProduct); ret = libusb_get_string_descriptor_ascii(handle, desc.iProduct, string, sizeof(string)); if (ret > 0) { printf(string); } } } } int main(void) { libusb_device **devs; int r; ssize_t cnt; r = libusb_init(&context); if (r < 0) return r; cnt = libusb_get_device_list(NULL, &devs); if (cnt < 0){ libusb_exit(NULL); return (int) cnt; } find_dev(devs); libusb_free_device_list(devs, 1); libusb_exit(NULL); return 0; }
You can read more about Libusb do Device handling and enumeration and read more about available APIs from the official documentation.
Till here, it is enough content and information to present for this part of “Gentle Practical Introduction to USB” and we will continue in a second part by presenting a real example of embedded code for a USB device. Till that time I advise you to watch this “USB 2.0 Embedded Host and Device Concepts, Solutions and Traffic Capture” lecture by Microchip and to review The Universal Serial Bus Specification Revision 2.0.
OMG! Thank you thank you thank you. Such a timely article. Have 2 days of chasing the same process, same list of errors from vs 2019. Did not know that the code should be recompiled using the 2017 project files. Will attempt again today. Thanks again.
I’m glad to know that it helped you!
Ok. Just got the code to compile – thanks to you!! For future readers,
a) installed a fresh copy of the libusb 1.0.24 download
b) downloaded again the latest SDK from MS website (took a while)
c) already had VS 2019 installed
d) double-click the libusb_2019 project file to force the compiling of the entire project files. This will freshen up / rebuild the downloaded source files. Now all of my local files are time-stamped for this morning date / time. All code is running correctly using the command line mode.
Very pleased. Now we can continue to expand on this code to make use of lib-usb.
These are great tools but often find that the issues are tool related. They consume so much time.
Sorry, after my review – I think that take away here is that the libusb_2019 SOLUTION file should have been double-clicked all along. That is the trick. This SOLUTION file will then properly rebuild all of the source code files. I was selecting the VC++ project launcher and chasing countless error code messages that could not be resolved.
So for VS 2019 tool chain -> double click the libusb_2019 VISUAL STUDIO SOLUTION file and all will compile properly 🙂
Thanks again!