Original address:
http://hengch.blog.163.com/blog/static/10780067200842311132428
In the previous article, we already gave the link to the USB specification. Starting from this article, we will cover a lot of USB 1.1 content. Our guiding idea is to first become familiar with the USB 1.1 protocol, first use existing HCD and USBD, directly work on client driver programming, see results as soon as possible, build the reader's confidence in USB development, and then go on to study the programming methods of USBD and HCD. Readers should read the specification by themselves. As for the detailed protocol-related matters in the article, since that would involve far too much text, please forgive us for not explaining too much.
1. Software structure of the USB host side
Generally speaking, textbooks or the specification will describe USB host-side software as having three layers. The first layer is called the host controller driver HCD (Host Controller Driver), the second layer is called the USB driver USBD (USB Driver), and the third layer is called the client driver (Client Driver). In practice, what we actually see is often that HCD and USBD are handled by a single program. For example, Windows provides HCD and USBD. If you develop a USB device yourself, you only need to develop a client driver on top of HCD and USBD. Linux is the same: the Linux kernel already provides HCD and USBD. So under Windows and Linux there is basically no need for us to develop HCD and USBD, and Linux even provides the source code. DOS is different, however. DOS itself has no USB support at all, so if you want to really master USB under DOS, you need to study HCD, USBD, and client drivers.
2. Introduction to DOSUSB
Obviously, HCD and USBD are lower-level, and there is more to understand. If we can bypass HCD and USBD and start directly from the client driver, things will be much easier. Fortunately, we can find a free USB driver under DOS called DOSUSB. This driver implements most of the functions of HCD and USBD, making it a great helper for USB programming.
DOSUSB currently has not implemented an EHIC driver, which means it still does not support USB 2.0. This is also one of the reasons we are starting from USB 1.1. On the other hand, since USB 2.0 is compatible with USB 1.1, even on a USB 2.0 device you can still use a USB 1.1 driver, it's just that you cannot achieve a transfer speed of 480MB/sec.
Below we will introduce DOSUSB. The official DOSUSB website is:
http://www.usbdos.net
You can download the latest version of DOGUSB from its official website; the current version is 1.1.1. Or you can download this version of DOSUSB from the site below.
http://blog.hengch.com/software/dosusb/dosusb.zip
DOSUSB can be used free of charge in non-commercial fields. If you are willing to spend the money, you can purchase the source code. From the forum on its official website, it can be seen that in September 2006 the author quoted a price of 1000 euros for the source code.
Installing DOSUSB is very simple. You only need to extract it into some directory, for example under c:\dosusb目录下,请自行阅读DOSUSB自带的文档,使用也非常简单,在DOS提示符下键入dosusb. Please read the documentation that comes with DOSUSB yourself. It is also very simple to use: at the DOS prompt, just type dosusb and run it.
c:\dosusb>dosusb
By default, DOSUSB uses int 65h as the software interrupt for calling its driver. If this conflicts with your system, you can add the /I parameter when running dosusb. Please read the DOSUSB documentation yourself.
DOSUSB communicates with client drivers through a data structure called URB (USB Request Block). This is very similar to Linux, and presumably the author referred to the Linux source code. The DOSUSB documentation gives the definition of this structure as follows:
struct {
BYTE transaction_type; // setup transaction (control transfer) (2Dh), input transaction (69h), output transaction (E1h)
BYTE chain_end_flag; // reserved
BYTE dev_add; // device address
BYTE end_point; // endpoint number
BYTE error_code; // error code
BYTE status; // device status
WORD transaction_flags; // reserved
WORD buffer_off; // receive/send buffer offset address
WORD buffer_seg; // receive/send buffer segment address
WORD buffer_length; // receive/send buffer length
WORD actual_length; // maximum length of each packet when receiving/sending
WORD setup_buffer_off; // offset address of setup_request structure
WORD setup_buffer_seg; // segment address of setup_request structure
WORD start_frame; // reserved
WORD nr_of_packets; // when >0, real-time transfer will be started
WORD int_interval; // reserved
WORD error_count; // number of retries
WORD timeout; // reserved
WORD next_urb_off; // reserved
WORD next_urb_seg; // reserved
} urb // 32 bytes
The reason for listing this structure is that we will use it to interact with USBD. As for the definition of the fields in the structure, there are detailed explanations in the DOSUSB documentation. Except for the reserved fields, which do not need to be filled in, error_code and status are returned by DOSUSB, so just fill in 0. More detailed filling methods will be introduced later.
In the DOSUSB distribution package, there is a sample directory containing many examples. Most of them are written in Power Basic, but they still have good reference value.
3. Some content from the USB 1.1 specification
The USB specification defines a set of descriptors for USB devices to describe inherent structures for device functions and attributes, including standard descriptors (device descriptor, configuration descriptor, interface descriptor, endpoint descriptor, and string descriptor), as well as non-standard descriptors such as class descriptors. According to the specification, under a device descriptor there can be several configuration descriptors; under each configuration descriptor there can be several interface descriptors; under each interface descriptor there can be several endpoint descriptors. String descriptors are mainly used to describe textual information such as manufacturer name, product name, and so on. The purpose of this article is to read out these descriptors.
In fact, a descriptor is just a data structure. No matter what kind of descriptor it is, the meanings of its first two bytes are the same: the first byte is the length of the descriptor, and the second byte is the type of the descriptor.
Device Descriptor:
struct {
BYTE bLength; // length of the descriptor, in bytes
BYTE bDescriptorType; // device descriptor type, 0x01
WORD bcdUSB; // USB protocol version supported by the device, BCD code
BYTE bDeviceClass; // device class code (assigned by USB-IF)
BYTE bDeviceSubClass; // subclass code
BYTE bDeviceProtocol; // protocol code
BYTE bMaxPacketSize0; // maximum packet length of endpoint 0 (only 8,16,32,64)
WORD idVendor; // vendor ID (assigned by USB-IF)
WORD idProduct; // product ID (defined by manufacturer)
WORD bcdDevice; // device release number (BCD code)
BYTE iManufacture; // index value of the string descriptor describing manufacturer information
BYTE iProduct; // index value of the string descriptor describing product information
BYTE iSerialNumber; // index value of the string descriptor describing device serial number information
BYTE bNumConfigurations; // number of possible configuration descriptors
} device_descriptor
Configuration Descriptor
struct {
BYTE bLength; // length of the descriptor, in bytes
BYTE bDescriptorType; // configuration descriptor type, 0x02
WORD wTotalLength; // total length of configuration information
BYTE bNumInterfaces; // number of interfaces supported by this configuration
BYTE bConfigurationValue; // used as a parameter by the SetCongiguration() request to select this configuration
BYTE bConfiguration; // index value of the string descriptor describing this configuration
BYTE bmAttributes; // configuration attributes
BYTE MaxPower; // bus power consumption of this configuration, in units of 2mA
}configuration_descriptor;
bmAttributes: b7: reserved, b6: self-powered, b5: remote wakeup, b4--b0: reserved
In addition, when reading a configuration descriptor, you can read out all the descriptors under that configuration at once. The total length of those descriptors is the value of the wTotalLength field. After reading out all descriptors, split them one by one.
Interface Descriptor:
struct {
BYTE bLength; // length of the descriptor, in bytes
BYTE bDescriptorType; // interface descriptor type, 0x04
BYTE bInterfaceNumber; // interface number, starting from 0
BYTE bAlternateSetting; // index value of the alternate setting.
BYTE bNumEndpoints; // number of endpoints for this interface.
BYTE bInterfaceClass; // interface class code (assigned by USB-IF)
BYTE bInterfaceSubClass; // interface subclass code (assigned by USB-IF)
BYTE bInterfaceProtocol; // protocol code (assigned by USB-IF)
BYTE iInterface; // index value of the string descriptor describing this interface
}interface_descriptor;
bInterfaceClass: the USB specification divides different interfaces into different classes according to function, as follows:
1: audio class, 2: CDC control class, 3: human interface class (HID), 5: physical class, 6: image class, 7: printer class, 8: mass storage class, 9: hub class, 10: CDC data class, 11: smart card class, 13: security class, 220: diagnostic device class, 224: wireless control class, 254: application-specific class, 255: vendor-defined device.
Endpoint Descriptor:
struct {
BYTE bLength; // length of the descriptor, in bytes
BYTE bDescriptorType; // endpoint descriptor type, 0x05
BYTE bEndpointAddress; // endpoint address
BYTE bmAttributes; // endpoint attributes under the configuration pointed to by bconfigurationValue.
WORD wMaxPacketSize; // maximum datagram length for receiving/sending.
BYTE bInterval; // time interval for periodic data transfer endpoints.
}endpoint_descriptor;
bmAttributes: bit 1:0--transfer type, 00=control transfer, 01=real-time transfer, 10=bulk transfer, 11=interrupt transfer; all other bits are reserved.
String Descriptor:
struct {
BYTE bLength; // length of the descriptor, in bytes
BYTE bDescriptorType; // string descriptor type, 0x03
char bString; // UNICODE-encoded string
}string_descriptor;
USB device requests (USB DEVICE REQUEST)
In order to better coordinate data communication between the USB host and device, the USB specification defines a set of command requests for unified control by the host over all USB devices on the bus. USB device requests have a unified format, and their structure is as follows:
struct {
BYTE bmRequestType; // request type
BYTE bRequest; // code of the device request
WORD wValue; // meaning varies with the request
WORD wIndex; // meaning varies with the request
WORD wLength; // if there is a data stage, this field is the number of data bytes
} setup_request;
Later on, all the commands we send to devices will rely on this structure. For the command request used in this article to read USB device descriptors, the fields of this structure are filled in as follows.
bmRequestType: b7--data transfer direction, 0=host to device, 1=device to host; b6:5--request type, 0=standard request, 1=class request, 2=vendor-provided request, 3=reserved; b4:0--recipient, 0=device, 1=interface, 2=endpoint, 3=other. The command we send to get a descriptor has data transfer direction device to host, standard request, recipient device, so this field should be 0x80.
bRequest: the codes for standard requests are as follows: GET_STATUS=0;CLEAR_FEATURE=1;SET_FEATURE=3;SET_ADDRESS=5;GET_DESCRIPTOR=6;SET_DESCRIPTOR=7;GET_CONFIGURATION=8;SET_CONFIGURATION=9;GET_INTERFACE=10;SET_INTERFACE=11;SYNCH_FRAME=12. Our command is GET_DESCRIPTOR, so it should be filled with 6.
wValue: the high byte indicates the descriptor type, 0x01=device descriptor, 0x02=configuration descriptor, 0x03=string descriptor, 0x04=interface descriptor, 0x05=endpoint descriptor, 0x29=hub class descriptor, 0x21=human interface class descriptor, 0xFF=vendor-defined descriptor.
The low byte indicates the descriptor index value. Therefore, when reading a device descriptor, this field value is 0x100. When reading a configuration descriptor, it should be 0x03yy, where yy is the descriptor index value.
wIndex: when reading a string descriptor, fill in 0x0409, meaning an English string is requested; in other cases fill in 0.
wLength: data length, generally this should be filled with the first byte of the descriptor, bLength. Since when reading a descriptor we do not know its actual length, the usual method is to first read 8 bytes, then read the complete descriptor again according to the value of the first byte bLength. Note that after reading the first 8 bytes of a configuration descriptor, you should use the value of the wTotalLength field as the length to read all descriptors related to that configuration.
4. Example program for reading device descriptors
According to the habit of our articles, almost every article has an example program, and this article is no exception. Please download the example program for this article from the address below:
http://blog.hengch.com/source/usbview.zip
The program was written in C++ and compiled successfully under DJGPP, so it is 32-bit protected-mode code. One thing to note is that DOSUSB is a real-mode driver, so when allocating a memory block you must allocate memory below 1M that real mode can read. Otherwise, there will definitely be problems when using int 65h to call DOSUSB.
There are four header files. public.h defines some convenient data types, for example BYTE as char, WORD as short int, and so on, so there is no need to pay too much attention to it. x86int.h defines the functions and data structures needed for calling DOS interrupts; just know how to use them. dosmem.h defines a DOS_MEM class, mainly to make allocating and using DOS memory blocks under protected mode more convenient; again, as long as you know its methods and can understand their meaning in the program, that's enough. usb.h defines all constants related to the USB specification. These constants correspond one by one with the various data structures introduced earlier. Since we are using DOS memory in protected mode, mapping a memory block onto a data structure is a bit troublesome, so reading each field mainly relies on these constants defined in usb.h.
The main program is in usbview.cc, and the main idea is as follows:
USB device addresses range from 1--127, so we loop from 1--127 and read the USB device descriptors one by one, until “illegal address” appears.
Each USB device has only one device descriptor, so we start by reading the device descriptor at a certain address.
At the beginning we do not know the length of the device descriptor. Even though we know its length is 18 bytes, we still do not know the packet length allowed by endpoint 0 (the bMaxPacketSize0 field in the device descriptor). But we know the minimum packet length is 8, so we first read 8 bytes of the device descriptor.
After we obtain the 8-byte device descriptor, we can get the length of the descriptor and the packet length of endpoint 0. In all subsequent commands, this value must always be filled into the actual_length field of the URB structure.
buffer is used to store the descriptor returned by USBD. It is recommended to initialize it before use, setting everything to 0.
To send a request to the device, we must first fill in the setup_request structure. As mentioned earlier, bmRequestType=0x80 and bRequest=6; the way these two fields are filled never changes. Since we are now reading a device descriptor, wValue=0x100, wIndex=0, and wLength is set to 8 in the first call, and to the returned bLength field in the second call (which should be 18).
After preparing buffer and setup_request, we then fill in the URB to interact with DOSUSB. Reading a descriptor is a control transfer, so transaction=0x2D (and from then on it stays 0x2D); dev_add is filled with the loop variable mentioned above; end_point=0, because we always work with endpoint 0 (see the USB specification); buffer_off and buffer_seg are filled with the offset address and segment address of buffer respectively; setup_buffer_off and setup_buffer_seg are filled with the offset address and segment address of the setup_request structure above; buffer_length is the same as wLength in the setup_request structure, or it can be set between wLength and the maximum length of buferr. If the maximum length of buffer is less than wLength, then we can only fill it with the maximum length of buffer, but in that case we will not get the complete descriptor; actual_length is set to 8 in the first call, and afterward is always set to the returned bMaxPacketsize0 field; all other fields are 0.
Make DS:DX point to the URB structure you just filled in, and call software interrupt 65h. DOSUSB will handle the rest for you.
If all is normal, error_code should return 0. If it is not 0, the meanings are as follows:
1--illegal device address; 2--internal error; 3--illegal transation_type field; 4--illegal buffer length.
If all is normal, the status field should be 0. This field is the status byte returned by the USB controller, and different controllers (OHCI or UHCI) will return different values.
After we get the device descriptor, if the iManufacturer field in the device descriptor is not 0, we can get the corresponding string descriptor according to that index value and then display the manufacturer information. Note that the string descriptor is UNICODE encoded. For ASCII, it consists of an ASSCII code followed by an ASCII 0. Likewise, we can obtain the product information and serial number information.
After we get the device descriptor of the device, we can know how many configurations the device has (bNumConfigurations in the device descriptor), and then use a loop to get all the configuration descriptors and all the descriptors under each configuration.
The method of reading a configuration descriptor is much the same as reading a device descriptor. Again, first read 8 bytes, then read all descriptor contents according to the returned content. One thing to note is that in fact we cannot obtain interface descriptors and endpoint descriptors separately. The only way is to read out all the descriptors under one configuration, so the wLength field in the setup_request structure must match the wTotalLength value returned in the configuration descriptor.
What remains is how to display the descriptors we have obtained. I think that is not difficult for any reader.
http://hengch.blog.163.com/blog/static/10780067200842311132428
In the previous article, we already gave the link to the USB specification. Starting from this article, we will cover a lot of USB 1.1 content. Our guiding idea is to first become familiar with the USB 1.1 protocol, first use existing HCD and USBD, directly work on client driver programming, see results as soon as possible, build the reader's confidence in USB development, and then go on to study the programming methods of USBD and HCD. Readers should read the specification by themselves. As for the detailed protocol-related matters in the article, since that would involve far too much text, please forgive us for not explaining too much.
1. Software structure of the USB host side
Generally speaking, textbooks or the specification will describe USB host-side software as having three layers. The first layer is called the host controller driver HCD (Host Controller Driver), the second layer is called the USB driver USBD (USB Driver), and the third layer is called the client driver (Client Driver). In practice, what we actually see is often that HCD and USBD are handled by a single program. For example, Windows provides HCD and USBD. If you develop a USB device yourself, you only need to develop a client driver on top of HCD and USBD. Linux is the same: the Linux kernel already provides HCD and USBD. So under Windows and Linux there is basically no need for us to develop HCD and USBD, and Linux even provides the source code. DOS is different, however. DOS itself has no USB support at all, so if you want to really master USB under DOS, you need to study HCD, USBD, and client drivers.
2. Introduction to DOSUSB
Obviously, HCD and USBD are lower-level, and there is more to understand. If we can bypass HCD and USBD and start directly from the client driver, things will be much easier. Fortunately, we can find a free USB driver under DOS called DOSUSB. This driver implements most of the functions of HCD and USBD, making it a great helper for USB programming.
DOSUSB currently has not implemented an EHIC driver, which means it still does not support USB 2.0. This is also one of the reasons we are starting from USB 1.1. On the other hand, since USB 2.0 is compatible with USB 1.1, even on a USB 2.0 device you can still use a USB 1.1 driver, it's just that you cannot achieve a transfer speed of 480MB/sec.
Below we will introduce DOSUSB. The official DOSUSB website is:
http://www.usbdos.net
You can download the latest version of DOGUSB from its official website; the current version is 1.1.1. Or you can download this version of DOSUSB from the site below.
http://blog.hengch.com/software/dosusb/dosusb.zip
DOSUSB can be used free of charge in non-commercial fields. If you are willing to spend the money, you can purchase the source code. From the forum on its official website, it can be seen that in September 2006 the author quoted a price of 1000 euros for the source code.
Installing DOSUSB is very simple. You only need to extract it into some directory, for example under c:\dosusb目录下,请自行阅读DOSUSB自带的文档,使用也非常简单,在DOS提示符下键入dosusb. Please read the documentation that comes with DOSUSB yourself. It is also very simple to use: at the DOS prompt, just type dosusb and run it.
c:\dosusb>dosusb
By default, DOSUSB uses int 65h as the software interrupt for calling its driver. If this conflicts with your system, you can add the /I parameter when running dosusb. Please read the DOSUSB documentation yourself.
DOSUSB communicates with client drivers through a data structure called URB (USB Request Block). This is very similar to Linux, and presumably the author referred to the Linux source code. The DOSUSB documentation gives the definition of this structure as follows:
struct {
BYTE transaction_type; // setup transaction (control transfer) (2Dh), input transaction (69h), output transaction (E1h)
BYTE chain_end_flag; // reserved
BYTE dev_add; // device address
BYTE end_point; // endpoint number
BYTE error_code; // error code
BYTE status; // device status
WORD transaction_flags; // reserved
WORD buffer_off; // receive/send buffer offset address
WORD buffer_seg; // receive/send buffer segment address
WORD buffer_length; // receive/send buffer length
WORD actual_length; // maximum length of each packet when receiving/sending
WORD setup_buffer_off; // offset address of setup_request structure
WORD setup_buffer_seg; // segment address of setup_request structure
WORD start_frame; // reserved
WORD nr_of_packets; // when >0, real-time transfer will be started
WORD int_interval; // reserved
WORD error_count; // number of retries
WORD timeout; // reserved
WORD next_urb_off; // reserved
WORD next_urb_seg; // reserved
} urb // 32 bytes
The reason for listing this structure is that we will use it to interact with USBD. As for the definition of the fields in the structure, there are detailed explanations in the DOSUSB documentation. Except for the reserved fields, which do not need to be filled in, error_code and status are returned by DOSUSB, so just fill in 0. More detailed filling methods will be introduced later.
In the DOSUSB distribution package, there is a sample directory containing many examples. Most of them are written in Power Basic, but they still have good reference value.
3. Some content from the USB 1.1 specification
The USB specification defines a set of descriptors for USB devices to describe inherent structures for device functions and attributes, including standard descriptors (device descriptor, configuration descriptor, interface descriptor, endpoint descriptor, and string descriptor), as well as non-standard descriptors such as class descriptors. According to the specification, under a device descriptor there can be several configuration descriptors; under each configuration descriptor there can be several interface descriptors; under each interface descriptor there can be several endpoint descriptors. String descriptors are mainly used to describe textual information such as manufacturer name, product name, and so on. The purpose of this article is to read out these descriptors.
In fact, a descriptor is just a data structure. No matter what kind of descriptor it is, the meanings of its first two bytes are the same: the first byte is the length of the descriptor, and the second byte is the type of the descriptor.
Device Descriptor:
struct {
BYTE bLength; // length of the descriptor, in bytes
BYTE bDescriptorType; // device descriptor type, 0x01
WORD bcdUSB; // USB protocol version supported by the device, BCD code
BYTE bDeviceClass; // device class code (assigned by USB-IF)
BYTE bDeviceSubClass; // subclass code
BYTE bDeviceProtocol; // protocol code
BYTE bMaxPacketSize0; // maximum packet length of endpoint 0 (only 8,16,32,64)
WORD idVendor; // vendor ID (assigned by USB-IF)
WORD idProduct; // product ID (defined by manufacturer)
WORD bcdDevice; // device release number (BCD code)
BYTE iManufacture; // index value of the string descriptor describing manufacturer information
BYTE iProduct; // index value of the string descriptor describing product information
BYTE iSerialNumber; // index value of the string descriptor describing device serial number information
BYTE bNumConfigurations; // number of possible configuration descriptors
} device_descriptor
Configuration Descriptor
struct {
BYTE bLength; // length of the descriptor, in bytes
BYTE bDescriptorType; // configuration descriptor type, 0x02
WORD wTotalLength; // total length of configuration information
BYTE bNumInterfaces; // number of interfaces supported by this configuration
BYTE bConfigurationValue; // used as a parameter by the SetCongiguration() request to select this configuration
BYTE bConfiguration; // index value of the string descriptor describing this configuration
BYTE bmAttributes; // configuration attributes
BYTE MaxPower; // bus power consumption of this configuration, in units of 2mA
}configuration_descriptor;
bmAttributes: b7: reserved, b6: self-powered, b5: remote wakeup, b4--b0: reserved
In addition, when reading a configuration descriptor, you can read out all the descriptors under that configuration at once. The total length of those descriptors is the value of the wTotalLength field. After reading out all descriptors, split them one by one.
Interface Descriptor:
struct {
BYTE bLength; // length of the descriptor, in bytes
BYTE bDescriptorType; // interface descriptor type, 0x04
BYTE bInterfaceNumber; // interface number, starting from 0
BYTE bAlternateSetting; // index value of the alternate setting.
BYTE bNumEndpoints; // number of endpoints for this interface.
BYTE bInterfaceClass; // interface class code (assigned by USB-IF)
BYTE bInterfaceSubClass; // interface subclass code (assigned by USB-IF)
BYTE bInterfaceProtocol; // protocol code (assigned by USB-IF)
BYTE iInterface; // index value of the string descriptor describing this interface
}interface_descriptor;
bInterfaceClass: the USB specification divides different interfaces into different classes according to function, as follows:
1: audio class, 2: CDC control class, 3: human interface class (HID), 5: physical class, 6: image class, 7: printer class, 8: mass storage class, 9: hub class, 10: CDC data class, 11: smart card class, 13: security class, 220: diagnostic device class, 224: wireless control class, 254: application-specific class, 255: vendor-defined device.
Endpoint Descriptor:
struct {
BYTE bLength; // length of the descriptor, in bytes
BYTE bDescriptorType; // endpoint descriptor type, 0x05
BYTE bEndpointAddress; // endpoint address
BYTE bmAttributes; // endpoint attributes under the configuration pointed to by bconfigurationValue.
WORD wMaxPacketSize; // maximum datagram length for receiving/sending.
BYTE bInterval; // time interval for periodic data transfer endpoints.
}endpoint_descriptor;
bmAttributes: bit 1:0--transfer type, 00=control transfer, 01=real-time transfer, 10=bulk transfer, 11=interrupt transfer; all other bits are reserved.
String Descriptor:
struct {
BYTE bLength; // length of the descriptor, in bytes
BYTE bDescriptorType; // string descriptor type, 0x03
char bString; // UNICODE-encoded string
}string_descriptor;
USB device requests (USB DEVICE REQUEST)
In order to better coordinate data communication between the USB host and device, the USB specification defines a set of command requests for unified control by the host over all USB devices on the bus. USB device requests have a unified format, and their structure is as follows:
struct {
BYTE bmRequestType; // request type
BYTE bRequest; // code of the device request
WORD wValue; // meaning varies with the request
WORD wIndex; // meaning varies with the request
WORD wLength; // if there is a data stage, this field is the number of data bytes
} setup_request;
Later on, all the commands we send to devices will rely on this structure. For the command request used in this article to read USB device descriptors, the fields of this structure are filled in as follows.
bmRequestType: b7--data transfer direction, 0=host to device, 1=device to host; b6:5--request type, 0=standard request, 1=class request, 2=vendor-provided request, 3=reserved; b4:0--recipient, 0=device, 1=interface, 2=endpoint, 3=other. The command we send to get a descriptor has data transfer direction device to host, standard request, recipient device, so this field should be 0x80.
bRequest: the codes for standard requests are as follows: GET_STATUS=0;CLEAR_FEATURE=1;SET_FEATURE=3;SET_ADDRESS=5;GET_DESCRIPTOR=6;SET_DESCRIPTOR=7;GET_CONFIGURATION=8;SET_CONFIGURATION=9;GET_INTERFACE=10;SET_INTERFACE=11;SYNCH_FRAME=12. Our command is GET_DESCRIPTOR, so it should be filled with 6.
wValue: the high byte indicates the descriptor type, 0x01=device descriptor, 0x02=configuration descriptor, 0x03=string descriptor, 0x04=interface descriptor, 0x05=endpoint descriptor, 0x29=hub class descriptor, 0x21=human interface class descriptor, 0xFF=vendor-defined descriptor.
The low byte indicates the descriptor index value. Therefore, when reading a device descriptor, this field value is 0x100. When reading a configuration descriptor, it should be 0x03yy, where yy is the descriptor index value.
wIndex: when reading a string descriptor, fill in 0x0409, meaning an English string is requested; in other cases fill in 0.
wLength: data length, generally this should be filled with the first byte of the descriptor, bLength. Since when reading a descriptor we do not know its actual length, the usual method is to first read 8 bytes, then read the complete descriptor again according to the value of the first byte bLength. Note that after reading the first 8 bytes of a configuration descriptor, you should use the value of the wTotalLength field as the length to read all descriptors related to that configuration.
4. Example program for reading device descriptors
According to the habit of our articles, almost every article has an example program, and this article is no exception. Please download the example program for this article from the address below:
http://blog.hengch.com/source/usbview.zip
The program was written in C++ and compiled successfully under DJGPP, so it is 32-bit protected-mode code. One thing to note is that DOSUSB is a real-mode driver, so when allocating a memory block you must allocate memory below 1M that real mode can read. Otherwise, there will definitely be problems when using int 65h to call DOSUSB.
There are four header files. public.h defines some convenient data types, for example BYTE as char, WORD as short int, and so on, so there is no need to pay too much attention to it. x86int.h defines the functions and data structures needed for calling DOS interrupts; just know how to use them. dosmem.h defines a DOS_MEM class, mainly to make allocating and using DOS memory blocks under protected mode more convenient; again, as long as you know its methods and can understand their meaning in the program, that's enough. usb.h defines all constants related to the USB specification. These constants correspond one by one with the various data structures introduced earlier. Since we are using DOS memory in protected mode, mapping a memory block onto a data structure is a bit troublesome, so reading each field mainly relies on these constants defined in usb.h.
The main program is in usbview.cc, and the main idea is as follows:
USB device addresses range from 1--127, so we loop from 1--127 and read the USB device descriptors one by one, until “illegal address” appears.
Each USB device has only one device descriptor, so we start by reading the device descriptor at a certain address.
At the beginning we do not know the length of the device descriptor. Even though we know its length is 18 bytes, we still do not know the packet length allowed by endpoint 0 (the bMaxPacketSize0 field in the device descriptor). But we know the minimum packet length is 8, so we first read 8 bytes of the device descriptor.
After we obtain the 8-byte device descriptor, we can get the length of the descriptor and the packet length of endpoint 0. In all subsequent commands, this value must always be filled into the actual_length field of the URB structure.
buffer is used to store the descriptor returned by USBD. It is recommended to initialize it before use, setting everything to 0.
To send a request to the device, we must first fill in the setup_request structure. As mentioned earlier, bmRequestType=0x80 and bRequest=6; the way these two fields are filled never changes. Since we are now reading a device descriptor, wValue=0x100, wIndex=0, and wLength is set to 8 in the first call, and to the returned bLength field in the second call (which should be 18).
After preparing buffer and setup_request, we then fill in the URB to interact with DOSUSB. Reading a descriptor is a control transfer, so transaction=0x2D (and from then on it stays 0x2D); dev_add is filled with the loop variable mentioned above; end_point=0, because we always work with endpoint 0 (see the USB specification); buffer_off and buffer_seg are filled with the offset address and segment address of buffer respectively; setup_buffer_off and setup_buffer_seg are filled with the offset address and segment address of the setup_request structure above; buffer_length is the same as wLength in the setup_request structure, or it can be set between wLength and the maximum length of buferr. If the maximum length of buffer is less than wLength, then we can only fill it with the maximum length of buffer, but in that case we will not get the complete descriptor; actual_length is set to 8 in the first call, and afterward is always set to the returned bMaxPacketsize0 field; all other fields are 0.
Make DS:DX point to the URB structure you just filled in, and call software interrupt 65h. DOSUSB will handle the rest for you.
If all is normal, error_code should return 0. If it is not 0, the meanings are as follows:
1--illegal device address; 2--internal error; 3--illegal transation_type field; 4--illegal buffer length.
If all is normal, the status field should be 0. This field is the status byte returned by the USB controller, and different controllers (OHCI or UHCI) will return different values.
After we get the device descriptor, if the iManufacturer field in the device descriptor is not 0, we can get the corresponding string descriptor according to that index value and then display the manufacturer information. Note that the string descriptor is UNICODE encoded. For ASCII, it consists of an ASSCII code followed by an ASCII 0. Likewise, we can obtain the product information and serial number information.
After we get the device descriptor of the device, we can know how many configurations the device has (bNumConfigurations in the device descriptor), and then use a loop to get all the configuration descriptors and all the descriptors under each configuration.
The method of reading a configuration descriptor is much the same as reading a device descriptor. Again, first read 8 bytes, then read all descriptor contents according to the returned content. One thing to note is that in fact we cannot obtain interface descriptors and endpoint descriptors separately. The only way is to read out all the descriptors under one configuration, so the wLength field in the setup_request structure must match the wTotalLength value returned in the configuration descriptor.
What remains is how to display the descriptors we have obtained. I think that is not difficult for any reader.

DigestI