Cover image for Linux USB

Linux USB


Timeline

Timeline

2026-02-16

init

This article introduces the basics of Linux USB drivers, thoroughly reviews the technological development history of USB, physical interface classifications and their relationship with transmission protocol versions, the evolution rules of USB version naming, and explains the USB Host and Slave interface configurations in conjunction with the RK3568 processor.

Linux Driver Notes

Table of ContentsLinks
1. Linux Driver Framework
2. Linux Driver Loading Logic
3. Character Device Basics
4. Concurrency and Competition
5. Advanced Character Device Progression
6. Interrupts
7. Platform Bus
8. Device Tree
9. Device Model
10. Hot Plug
11. pinctrl Subsystem
12. gpio Subsystem
13. Input Subsystem
14. Single Bus
15. I2C
16. SPI
17. UART
18. PWM
19. RTC
20. Watchdog
21. CAN
22. Network Device
23. ADC
24. IIO
25. USB
26. LCD

USB Basics

**USB (Universal Serial Bus)**is a widely used universal interface standard between computers and electronic devices, capable of transmitting data and power. USB is a standard.

  1. Concept Proposal and First Generation Standard Development (1994-1996)
    In 1994, companies such as Compaq, IBM, and Intel jointly developed USB technology to simplify the connection between computers and peripherals. In 1996, the USB 1.0 standard was released, supporting 1.5Mbps (Low Speed) and 12Mbps (Full Speed) transfer rates, meeting the needs of low-speed devices like mice and keyboards, and also enabling hot-swapping for user convenience.
  2. Technical Improvement and Popularization (1998-2000)
    In 1998, USB 1.1 optimized electrical characteristics and compatibility, improving reliability and driving device adoption. In 2000, USB 2.0 was a major breakthrough, introducing a 480Mbps High-Speed transfer mode to meet the needs of high-speed devices like digital cameras and external hard drives, becoming the mainstream interface standard.
  3. High-Speed Development and Performance Enhancement (2008-2013)
    In 2008, USB 3.0 (later called USB 3.1 Gen 1) emerged, increasing the speed to 5Gbps, optimizing power management and signal transmission, supporting high-power device charging, and meeting needs such as high-definition video transmission. In 2013, USB 3.1 Gen 2 was released, reaching speeds of 10Gbps, enabling possibilities for high-performance external SSDs and other devices.
  4. Continuous Evolution and Diversification (2014 to Present)
    In 2014, the USB Type-C interface appeared. It is small in size, reversible, supports high-power transmission (up to 100W), and can transfer data at high speeds, gradually becoming mainstream. In 2017, USB 3.2 defined different speed identifiers, enriching speed options. In 2019, USB4, based on Thunderbolt 3 technology, reached speeds of up to 40Gbps, enhancing features like multi-display support and expanding application scenarios.

USB continues to evolve, constantly improving in data transfer, device compatibility, and power delivery, becoming an indispensable connection method for modern electronic devices. There are different types of USB interfaces, and the interface standards can be referenced on the official website:

USB Interface Classification

USB interfaces can be roughly divided into three types: Type-A, Type-B, and Type-C interfaces, which include Type-A, Type-B, Mini-USB, Micro-USB, and Micro-B SuperSpeed interfaces. The following provides a detailed introduction.

  1. The Type-A interface, as shown in the figure below, is a rectangular interface widely used in host devices such as computers and chargers, as well as traditional peripherals like USB drives, mice, and keyboards.

Type-A
Type-A

  1. The Type-B interface, as shown in the figure below, is a square interface commonly used in devices such as printers, microphones, and scanners.

Type-B
Type-B

  1. The Mini-B interface, as shown in the figure below, is often used in dashcam devices.

Mini-B
Mini-B

  1. The Micro-USB interface, as shown in the figure below, is a small interface that was used in older mobile phones, power banks, and other portable devices.

Micro-USB
Micro-USB

  1. The Micro-B SuperSpeed interface, as shown in the figure below, is commonly used in external hard drives.

Micro-B SuperSpeed
Micro-B SuperSpeed

  1. The Type-C interface, as shown in the figure below, is reversible. Currently, the Type-C interface is the mainstream interface for modern devices such as smartphones, laptops, and headphones.

Type-C
Type-C

Summary:

Interface Category
Interface Category

Although the different types of USB interfaces in the figure vary in appearance and usage, they all follow the USB standard protocol, ensuring compatibility and interoperability between devices.

It should be noted here that just because a device uses a Type-C interface, it cannot be assumed to have high-speed data transfer capabilities. Type-C is merely a physical specification; the actual data transfer speed depends on the USB protocol version it supports. For example, if it uses the USB 2.0 protocol, even with a Type-C interface, its speed is relatively limited, while USB 3.0 and higher versions can achieve high-speed transmission.

USB Version

USB Version
USB Version

Initially, USB 3.0 was named SuperSpeed USB, emphasizing its high-speed transmission characteristics. Later, the USB 3.1 version was released, which further improved the transfer rate.

However, to simplify the naming:

  • USB 3.0 was renamed to USB 3.1 Gen 1
  • USB 3.1 was renamed to USB 3.1 Gen 2.

USB-IF introduced the latest USB naming specification:

  • USB 3.1 Gen 1 was renamed to USB 3.2 Gen 1
  • USB 3.1 Gen 2 was renamed to USB 3.2 Gen 2
  • USB 3.2 Gen 2x2 is USB 3.2

After two naming changes:

  • USB 3.0 is named USB 3.2 Gen 1
  • USB 3.1 is named USB 3.2 Gen 2
  • USB 3.2 is named USB 3.2 Gen 2x2

The version of a USB interface can generally be inferred from its color.

  • Black/White: USB 1.0/2.0
  • Dark Blue: USB 3.0
  • Light Blue: USB 2.0
  • Red: USB 3.1

RK3568 USB Interface

topeet RK3568
topeet RK3568

In the diagram above, we can see that the RK3568 processor has USB 2.0 HOSTx2, one USB 3.0 HOST, and one USB 3.0 OTG.

USB devices are divided into Host (master device) and Slave (slave device). Data transmission can only occur when a Host is connected to a Slave.

  • USB Host: USB Host refers to a device with USB host functionality. The USB master device controls and manages the USB bus, and is typically a computer or other host device, such as a tablet, laptop, or desktop.

  • USB Slave: USB Slave refers to a device with USB slave functionality. The USB slave device is controlled by the USB host and relies on the USB host device for data transmission and communication. USB slave devices can be various peripherals, such as keyboards, mice, USB flash drives, etc.

USB OTG(USB On-The-Go)

Refers to devices that support USB OTG functionality (plug-and-play). USB OTG allows a device to switch between master and slave roles, enabling direct communication with other USB devices without the need for a traditional USB host. For example, OTG can be used to directly connect a camera and a printer.

The RK3568 chip has two USB 2.0 host controllers. Each USB 2.0 host controller fully supports USB 2.0 functionality through an Enhanced Host Controller Interface (EHCI) host controller and an Open Host Controller Interface (OHCI) host controller, and each host controller has one USB port.

  • The OHCI host controller only supports full-speed and low-speed modes, used for connecting full-speed and low-speed devices.

  • The EHCI host controller only supports high-speed mode, used for connecting high-speed devices.

The OHCI host controller and the EHCI host controller share the same USB port. The EHCI host controller automatically selects the control ownership of the USB port (OHCI or EHCI) based on the speed mode of the connected device.

  • When OHCI is selected as the host controller, the OHCI host controller will provide services to the connected devices;
  • When EHCI is selected as the host controller, the EHCI host controller will provide services to the connected devices.

USB 2.0 Host Controller Block Diagram
USB 2.0 Host Controller Block Diagram

The RK3568 chip has two USB3.0 controllers, one of which can be used as a USB3.0 OTG (On-The-Go) controller, while the other can only be used as a USB3.0 host controller. The USB3.0 OTG controller can act as a static host, static device, USB2.0/3.0 OTG A device, or B device based on the input ID state of the USB2.0 physical layer (PHY).
It can perform data transmission between the host and device as either a host or device at SuperSpeed, High-Speed, Full-Speed, or Low-Speed.

USB 3.0 OTG Block Diagram
USB 3.0 OTG Block Diagram

As can be seen from the figure above, the USB3 OTG module connects to the USB2.0 PHY module below via the HS/FS/LS MAC (High-Speed/Full-Speed/Low-Speed MAC) interface. The USB2.0 PHY module is responsible for the physical layer functions of USB2.0. The USB3 OTG module connects to the SS PHY module on the right via the SS MAC (SuperSpeed MAC) interface. The SS PHY module is responsible for the physical layer functions of SuperSpeed.

RK3568 USB Multiplexing Relationship
RK3568 USB Multiplexing Relationship

From the above diagram, it can be seen that the RK3568 chip is equipped with two USB2.0 HOSTs, and the USB2.0 HOST pins do not have multiplexing. USB 2.0 Host_2 controller and USB 2.0 Host_3 controller use port0 and port1 of USB 2.0 Comb PHY_1 respectively.

At the same time, the chip has a USB3.0 OTG, which is backward compatible with USB2.0 OTG, and this USB 3.0 OTG controller shares the USB3/SATA Combo PHY_0 controller with the USB3/SATA Combo PHY_0。

Additionally, the RK3568 chip also has a USB3.0 HOST, which is backward compatible with USB2.0 HOST. USB 3.0 Host_1 controller shares the USB3/SATA/QSGMII Combo PHY_1 with SATA_1/QSGMII controller.

USB 3.0 OTG controller and USB 3.0 Host_Controller 1 uses USB 2.0 Comb PHY_for port0 and port1 of Controller 0.

topeet RK3568 USB

topeet RK3568 USB
topeet RK3568 USB

Two USB 2.0 interfaces, where the lower one is shared with the RTL8723DU WiFi module.

The development board also has a USB 3.0 OTG interface, as shown in the schematic diagram below:

USB3.0 OTG
USB3.0 OTG

USB 3.0 OTG
USB 3.0 OTG

In the above schematic diagram,USB3_OTG0_VBUSDETis a high-level active detection signal used for OTG and Device mode identification. USB 3.0 OTG supports three modes: OTG mode, Device mode, and Host mode.

  1. In OTG mode: The working mode is automatically switched by detecting the ID pin level (high level for Device mode, low level for Host mode). Additionally, VBUSDET must be high to pull up USB3_OTG0DP and start enumeration.
  2. In Device mode: Only VBUSDET needs to be high to trigger enumeration, without detecting the ID pin.
  3. In Host mode: The ID and VBUSDET states are completely ignored. Note that although some products may only require Host mode, since the USB 3.0_OTG0_DP/DM interface also handles system firmware flashing and ADB debugging, it must be switched to Device mode during debugging and production, so the VBUSDET signal connection must be retained.

The system defaults to Device mode before U-Boot starts. After entering U-Boot, these three working modes can be configured according to actual needs.

The RK3568 chip is also equipped with a USB 3.0 HOST interface. By checking the baseboard schematic, as shown below, this USB 3.0 HOST interface is specifically reserved for the 5G module (RM500U-CN module). On the iTOP-RK3568 development board, the 4G and 5G modules share the same U58 socket, achieving compatibility through hardware design.

MiniPCIe2.0 Slot_Support 4G module
MiniPCIe2.0 Slot_Support 4G module

USB Bus Architecture and Device Interaction Mechanism

USB Topology

USB adopts a tree topology, which is a master-slave structure, i.e.,All devices are connected to the host via hubs, forming a tree-like structure, as shown in the figure below. USB only allows data communication between the host and devices; communication between devices is not permitted

USB Topology
USB Topology

Although USB hubs can expand the number of interfaces, their expansion capability is strictly limited. According to the USB 2.0 protocol specification, the entire system supports a maximum of 7 levels of cascading expansion (counting from the root hub), and the total number of connected devices must not exceed 127 (including the hub itself, with address 0 having a special role). This pyramid-shaped topology design (as shown in the figure below) ensures flexibility in interface expansion while avoiding signal attenuation and system overload through level restrictions.

USB Hub Levels
USB Hub Levels

USB Device State Transition

USB Device States
USB Device States

From the USB device not yet being connected, to being fully recognized by the USB Host and ensuring its normal functionality, the USB device will go through the following stages in sequence.

  1. Attached State: This is the stage where the device is physically connected to the USB host but not yet powered. This stage is primarily ensured by hardware.
  2. Powered State: This is the second stage, corresponding to a device connected to the USB host and just powered on. This stage is primarily ensured by hardware.
  3. Default State: When a USB device first connects to the host, it enters the default state. In the default state, the device waits for the host to send a reset signal.
  4. Address State: After receiving a reset signal, the device enters the address state. In this state, the host assigns a unique address to the device.
  5. Configured State: Once the device receives its address, it enters the configured state. In this state, the device can select its configuration descriptor, which determines how it communicates with the host.
  6. Suspend State: When the USB device is inactive or the host enters a low-power mode, the device can enter the suspend state. In the suspend state, the device consumes no or very little power.

USB Device Hardware Identification

How does the USB HOST detect the insertion of a USB device?

USB 2.0 is backward compatible with USB 1.0 and USB 1.1, and is divided into three modes: Low-speed, Full-speed, and High-speed.

  • Full-speed Device

A full-speed device is connected in the following manner, as shown in the figure below, with the USB host side on the left and the USB device side on the right.

Full-speed Device Cable and Resistor Connections
Full-speed Device Cable and Resistor Connections

As shown in the figure above, on the left hub side, both data lines D+ and D- have 15kΩ pull-down resistors (Rpd), while on the right device side, the D+ data line has a 1.5kΩ pull-up resistor (Rpu).

When the D+ signal line transitions from low to high, the USB host can determine that a full-speed device has been inserted.

  • Low-speed Device

A low-speed device is connected in the following manner, as shown in the figure below, with the USB host side on the left and the USB device side on the right.

Low-speed Device Cable and Resistor Connections
Low-speed Device Cable and Resistor Connections

As shown in the figure above, on the left hub side, the data lines D+ and D- both have 15kΩ pull-down resistors Rpd, while on the right device side, the data line D- has a 1.5kΩ pull-up resistor Rpu.

When the D- signal line transitions from low to high level, the USB host can determine that a low-speed device has been inserted.

  • High-speed device

When a high-speed device is connected to the system, it is initially recognized as a full-speed device. Subsequently, the HOST detects the DEVICE to determine if it is a high-speed device, during which the HOST and DEVICE must mutually confirm. After confirmation, the system switches to high-speed mode. In high-speed mode, current transmission mode is used, and the pull-up resistor on D+ needs to be disconnected.

When a device is disconnected, its differential termination resistors disappear immediately, but high-speed data packets continue to be transmitted from the port where the device was connected. When these packets reach the unloaded path endpoint, strong reflections occur, and the reflected signals return to the hub interface, causing the differential voltage at the hub’s connected port to rise. When the differential signal level on D+ and D- of a high-speed device exceeds 625mV, it can be determined that the USB device has been disconnected.

USB 3.0 principle architecture diagram
USB 3.0 principle architecture diagram

When a USB host and USB device are connected, an equivalent pull-down resistor is generated on theSSRX+/-USB host and deviceR_Term, with a range of 18 to 30 ohms, formed bySSRX+/-the parallel combination of two 50-ohm equivalent pull-down resistors on each side.

In the figure below, the left circuit is the equivalent circuit when no device is connected, and the right circuit is the equivalent circuit when a USB device is connected.

Equivalent circuit
Equivalent circuit

From the figure above, when no device is connected on the left, the circuit model is actually an RC series circuit, with a charge/discharge time constantT = R_Detect * C_Parasitic. When a device is connected on the right, at this timeR_termexists, and the charge/discharge time constantT=(R_Detect+R_Term)(C_AC+C_Parasitic)Obviously, the latter is much larger than the former, so whether a device is connected can be
judged based on the time constant.

OTG Dual Role Switching

USB OTG can act as both HOST and Device.

The schematic diagram of the topeet RK3568 OTG interface circuit is shown below:

topeet RK3568 OTG Interface Circuit Schematic
topeet RK3568 OTG Interface Circuit Schematic

From the above figure, it can be seen that J48 is a USB socket. The USB OTG interface has 5 lines: 2 for data transmission (D+, D-), 1 power line (VBUS), 1 ground line (GND), and 1 ID line. The ID line is used to implement the OTG function, determining whether the currently connected device is a host (HOST) or a slave (SLAVE).

  • If a slave device (such as a USB flash drive) is connected, the development board’s USB device acts as the host, and the ID pin is pulled low.

    • WhenUSB_OTG1_IDthe pin is at low level (i.e., the development board acts as the host), Q7 is cut off, and the EN pin of U14 is at high level,USB_OTG1_VBUSoutputting 5V voltage to power the slave device.
  • If a host device (such as a computer) is connected, the development board’s USB device acts as the slave, and the ID pin remains at high level.

    • WhenUSB_OTG1_IDthe pin is at high level (i.e., the development board acts as the slave), Q7 conducts, the EN pin of U14 is at low level, U14 stops working,USB_OTG1_VBUSand no 5V voltage is output.

The Type-C interface on the iTOP-RK3399 baseboard is as follows

topeet RK3399 Type-C
topeet RK3399 Type-C

topeet RK3399 Type-C
topeet RK3399 Type-C

In the figure above,Typec_CC1andTypec_CC2The pins are used as ID pins, and their working principle is similar to that described earlier. After detecting the states of CC1 and CC2, the U24 chip sends an interrupt signal to the main controller and reads the chip register values via the I2C pins. Subsequently, the main controller controls the power supply pins based on this information.

USB Protocol

USB Descriptors

USB descriptors are structures that describe USB device information. The host reads these descriptors to identify the device type, functions, configuration, and other information, thereby correctly loading drivers and communicating. The main USB descriptors are:

  • Device Descriptor
  • Configuration Descriptor
  • Interface Descriptor
  • Endpoint Descriptor

Device Descriptor

The device descriptor contains basic information about the device, such as the vendor ID and product ID. It is the first information requested and returned when the device connects to the host, providing the basic characteristics of the device. In the Linux kernel, USB devices are described using theusb_devicestructure, and the USB device descriptor is defined as theusb_device_descriptorstructure. The device descriptor structure is as follows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// include/uapi/linux/usb/ch9.h
/* USB_DT_DEVICE: Device descriptor */
struct usb_device_descriptor {
__u8 bLength; // Descriptor Length
__u8 bDescriptorType; // Descriptor Type

__le16 bcdUSB; // USB Version Number
__u8 bDeviceClass; // Device Class
__u8 bDeviceSubClass; // Device Subclass
__u8 bDeviceProtocol; // Device Protocol
__u8 bMaxPacketSize0; // Max Packet Size for Endpoint 0
__le16 idVendor; // Vendor ID
__le16 idProduct; // Product ID
__le16 bcdDevice; // Device Version Number
__u8 iManufacturer; // Index of Manufacturer String Descriptor
__u8 iProduct; // Index of Product String Descriptor
__u8 iSerialNumber; // Index of Serial Number String Descriptor
__u8 bNumConfigurations; // Number of Possible Configuration Descriptors
} __attribute__ ((packed));

Configuration Descriptor

The configuration descriptor describes the different configurations supported by the device, including the number of interfaces, configuration number, power supply information, etc. Every USB device must have one configuration descriptor. Additionally, a USB device can have multiple configurations, but only one configuration is used per transfer process. USB configurations are described in the kernel using theusb_host_configstructure, while the USB configuration descriptor is defined as theusb_config_descriptorstructure, as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// include/uapi/linux/usb/ch9.h
/* USB_DT_CONFIG: Configuration descriptor information.
*
* USB_DT_OTHER_SPEED_CONFIG is the same descriptor, except that the
* descriptor type is different. Highspeed-capable devices can look
* different depending on what speed they're currently running. Only
* devices with a USB_DT_DEVICE_QUALIFIER have any OTHER_SPEED_CONFIG
* descriptors.
*/
struct usb_config_descriptor {
__u8 bLength; /* Descriptor length */
__u8 bDescriptorType; /* Descriptor type number */

__le16 wTotalLength; /* Total size of data returned for this configuration */
__u8 bNumInterfaces; /* Number of interfaces supported by this configuration */
__u8 bConfigurationValue; /* Parameter value required for the Set_Configuration command */
__u8 iConfiguration; /* Index of string descriptor describing this configuration */
__u8 bmAttributes; /* Power supply mode selection */
__u8 bMaxPower; /* Maximum current drawn by the device from the bus */
} __attribute__ ((packed));

Interface descriptor

The interface descriptor describes the characteristics of an interface within a configuration, including the number of endpoints, device class and subclass, etc. A USB configuration has one or more interface descriptors. USB interfaces are described in the kernel using theusb_interfacestructure, while the USB interface descriptor is defined as theusb_interface_descriptorstructure, as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// include/uapi/linux/usb/ch9.h
/* USB_DT_INTERFACE: Interface descriptor */
struct usb_interface_descriptor {
__u8 bLength; /* Descriptor Length */
__u8 bDescriptorType; /* Descriptor Type */

__u8 bInterfaceNumber; /* Interface Number */
__u8 bAlternateSetting; /* Alternate Interface Descriptor Number */
__u8 bNumEndpoints; /* Number of Endpoints Used by This Interface, Excluding Endpoint 0 */
__u8 bInterfaceClass; /* Interface Class */
__u8 bInterfaceSubClass; /* Interface Subclass */
__u8 bInterfaceProtocol; /* Interface Protocol */
__u8 iInterface; /* String Index Describing This Interface */
} __attribute__ ((packed));

Endpoint Descriptor

An endpoint descriptor describes an endpoint in an interface. An endpoint is the termination point for data transfer between a device and a host. A specific endpoint can only belong to one of four transfer modes. A USB interface has zero or more endpoint descriptors (excluding endpoint 0). In the Linux kernel, USB endpoints are described using theusb_host_endpointstructure, while the USB endpoint descriptor is defined as theusb_endpoint_descriptorstructure. The endpoint descriptor structure is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// include/uapi/linux/usb/ch9.h
/* USB_DT_ENDPOINT: Endpoint descriptor */
struct usb_endpoint_descriptor {
__u8 bLength; // Descriptor Length
__u8 bDescriptorType; // Descriptor Type

__u8 bEndpointAddress; // Endpoint address: bits[3:0] are the endpoint number, bit 7 is the direction bit (1 for IN, 0 for OUT)
__u8 bmAttributes; // Endpoint attributes: bits[1:0] determine the transfer type (00=Control, 01=Isochronous, 10=Bulk, 11=Interrupt)
__le16 wMaxPacketSize; // Maximum packet size (in bytes), used to limit the amount of data this endpoint can transfer at one time
__u8 bInterval; // Polling interval
// - For bulk/control transfer endpoints, this field is ignored
// - For isochronous transfer endpoints, this field must be 1
// - For interrupt transfer endpoints, this field value ranges from 1 to 255

/* NOTE: these two are _only_ in audio endpoints. */
/* use USB_DT_ENDPOINT*_SIZE in bLength, not sizeof. */
__u8 bRefresh;
__u8 bSynchAddress;
} __attribute__ ((packed));

A USB device has a device descriptor, which determines how many configurations the device has, each configuration corresponding to a configuration descriptor; the configuration descriptor defines how many interfaces are in that configuration, each interface having a corresponding interface descriptor; the interface descriptor defines how many endpoints the interface has, each endpoint corresponding to an endpoint descriptor; the endpoint descriptor defines the endpoint’s size, type, etc. From this, we can see that the relationship between USB descriptors is hierarchical, with the device descriptor at the top, followed by the configuration descriptor, then the interface descriptor, and then the endpoint descriptor, as shown in the following figure:

USB descriptor structure relationship
USB descriptor structure relationship

For example:

lsusb -v
lsusb -v

It can be seen that the root hub contains one device descriptor, one configuration descriptor, one interface descriptor, and one endpoint descriptor. The information content in the figure directly corresponds to theusb_device_descriptorusb_config_descriptorusb_interface_descriptorusb_endpoint_descriptorstructure.

USB communication data format

The USB communication data format uses packets as the basic unit, achieving reliable communication between the device and the host through a hierarchical structure (field -> packet -> transaction -> transfer).

Field

A field is the smallest unit of USB data, composed of binary bits with different functions. There are 7 types in total, as shown in the table below.

Field TypeFunctionLengthExample
Sync Field (SYNC)Synchronizes the clock to ensure alignment between transmitter and receiver.8 bits (full/low speed) or 32 bits (high speed)0000 0001 (full speed)
Packet Identifier Field (PID)Identifies the packet type (e.g., token, data, handshake), consisting of 4 valid bits + 4 complement check bits.8 bits0001 1000 (OUT packet)
Address Field (ADDR)Device address (7 bits), supporting up to 127 devices.7 bits0101 010 (address 5)
Endpoint field (ENDP)Endpoint number (4 bits), up to 16 endpoints per device4 bits0001 (endpoint 1)
Frame number field (FRAM)Frame number (11 bits), increments every 1ms (full speed) or 125μs (high speed), used for synchronization11 bits0x7FF (maximum frame number)
Data field (DATA)Transmitted data (0-1024 bytes), length determined by transfer type0-1024 bytes0x01 0x02 0x03
CRC field (CRC)Error checking, token packets and data packets use different algorithms5 bits (token packet) or 16 bits (data packet)CRC-5 or CRC-16

Packet

USB is like a conveyor belt that can only queue (serial communication), where data (fields) must be sent one bit at a time in sequence.

To prevent data from getting lost, USB first packages data into small parcels (data packets), and then uses four different types of “delivery boxes” (packet structures) to distinguish purposes—such as those for files, commands, data transmission, etc.—ensuring each parcel is accurately delivered to its destination.

These four different types of “delivery boxes” correspond to four different packet structures, which are:Token packetData packetHandshake packetandSpecial packet

These four packet structures are distinguished by the identifier PID. In a USB packet, the PID field uses 8 bits, with the format as follows:

PID Format
PID Format

The first 4 bits represent the PID, and the last 4 bits are the complement of the first 4 bits. In the first 4 bits of the PID, bits 1 and 0 determine the category, while bits 3 and 2 further subdivide, as shown in the table below:

PID TypePID NamePID[3:0]Description
Token TypeOUT0001BNotify device that data will be output
IN1001BNotify device that data will be input
SOF0101BNotify device that this is a start-of-frame packet
SETUP1101BNotify device that a control transfer will begin
Data TypeDATA00011BData packets of different types
DATA11011B
DATA20111B
MDATA1111B
Handshake TypeACK0010BAcknowledge
NACK1010BNot Acknowledge
STALL1110BSuspend
NYET0110BNot Ready
Special TypePRE1100BPreamble, this is a token packet
ERR1100BError, this is a handshake packet.
SPLIT1000BSplit transaction (this is a token packet).
PING0100BPING test (this is a token packet).
Reserved0000BReserved, not used.

A complete data packet consists of multiple different fields.

All data packets start with a SYNC field, followed by a Packet Identifier (PID), and end with an End of Packet (EOP) signal. The bit fields in the middle vary for different types of packets, commonly including the target address (ADDR), target endpoint (ENDP), data, frame index, and CRC. The specific structure of each data packet needs to be analyzed based on the actual situation.

Transaction

The basic unit of USB transmission is the packet, and the type of packet is indicated by the PID. A single packet alone cannot transmit complete data. For example, to output data, an OUT token packet can be sent because it can specify the destination. However, an OUT token packet alone is not enough; data transmission also requires sending a DATA0 or DATA1 data packet.

When the device receives data, it also needs to reply with an ACK handshake packet. Therefore, a complete data transmission involves multiple packets, including token packets, data packets, and handshake packets. This complete data transmission process is called a transaction. There are different types of transactions; some require handshake packets, while others do not; some can transmit large amounts of data, while others can only transmit small amounts of data.

There are four types of transactions, which are:

  • Bulk transaction

Bulk transactions are used to transmit large amounts of data, with guaranteed correctness but no guaranteed timeliness.

  • Interrupt transaction

Interrupt transactions are used to transmit periodic, small amounts of data, with both correctness and timeliness guaranteed.

  • Isochronous transaction

Real-time transactions are used to transmit real-time data, where data correctness is not guaranteed, but timeliness is guaranteed.

  • Setup Transaction

A setup transaction is similar to a bulk transaction, except that the token packet is a SETUP token packet.

USB Transfer Types

  1. Control TransferIt is the most basic data transfer method in the USB protocol, mainly used for querying, configuring, and sending general commands to USB devices. Control transfer is bidirectional, and the data volume is usually small. Control transfer consists of setup transactions and bulk transactions, and all USB devices must support control transfer.
  2. Isochronous TransferUsed to transmit large amounts of data at a constant rate with requirements on service period. Generally, isochronous transfer is commonly used for audio and video devices, as these devices require high real-time performance. Isochronous transfer uses real-time transactions for data transmission.
  3. Interrupt TransferSuitable for transmitting small or medium amounts of data with a fixed transaction processing period. Generally, USB interrupt transfer is often used in HID human interface devices such as USB mice and USB keyboards, because these devices require fast response and a fixed transaction processing period, but have low data demand, which is the advantage of USB interrupt transfer. Interrupt transfer uses interrupt transactions for data transmission.
  4. Bulk TransferAlso called USB block transfer. Bulk transfer is suitable for transmitting large amounts of data, requiring correct transmission, but with no requirements on transmission time, rate, or real-time performance. Generally, bulk transfer is used for storage devices such as USB flash drives. Bulk transfer uses bulk transactions for data transmission.

Bits form fields, fields form packets, packets form transactions, and transactions form transfers.

USB Enumeration

After a USB device is connected to a USB host, the host uses the bus enumeration process to identify and manage the connected device.**USB enumeration is essentially the process of information exchange between the USB host and device.**During this process, the device reports its own parameters to the host, and the host obtains key information based on these parameters, thereby determining the specific type of device and the method of communication with it.

Based on the obtained information, the host can load the appropriate driver for the device. The USB device enumeration process is described as follows:

  1. After a USB device is inserted into a USB port, the host detectsD+/D-the voltage on the line, confirms that a device is connected, and the USB hub reports the connection of a USB device to the host via the interrupt IN channel.
  2. After detecting the insertion of a USB device, the host resets the USB device. After reset, the USB device’s address is 0, allowing the host to communicate with the USB device using address 0.
  3. The host sends a standard request to get the device descriptor to endpoint 0 (the default endpoint) of the device at address 0 (i.e., the newly inserted USB device). After receiving the request, the USB device sends the device descriptor to the host. Upon receiving the device descriptor, the host returns a zero-length data acknowledgment packet (ACK).
  4. The host resets the device again. After reset, the host sends a set address request packet to endpoint 0 of the device at address 0, with the new device address contained in this packet. The host sends a request for status return, and the device returns a zero-length status packet.
  5. After receiving the status packet, the host sends an ACK packet to the device. Upon receiving the ACK, the device activates the new device address, and thereafter the host accesses the device using the new address.
  6. The host retrieves the device descriptor again. This time is slightly different from the first; it needs to obtain the full 18-byte device descriptor.
  7. Next, the host retrieves the configuration descriptor. After obtaining the configuration descriptor, the host retrieves the configuration set based on the total length of the configuration set within it. The configuration set includes the configuration descriptor, interface descriptor, endpoint descriptor, and so on. If there are also string descriptors, the system will retrieve them as well.

USB Waveform

Start-of-Frame Packet
Start-of-Frame Packet

In the figure above, it is a start-of-frame packet. The SYNC field starts with 0x80 (10000000) and is used for clock synchronization. The PID field value is 0xA5, marked as SOF in the figure, indicating that this is a start-of-frame packet.

Token Packet
Token Packet

On the left is a token packet. The SYNC field starts with 0x80 (10000000) to indicate clock synchronization. The PID field value is 0x69, marked as IN in the diagram, indicating that the device will be notified to input data. Next, the host requests data from endpoint 2.

In the diagram above, on the right is a handshake packet. The SYNC field starts with 0x80 (10000000) to indicate clock synchronization. The PID field value is 0x5A, marked as NAK in the diagram, indicating that the device is temporarily unable to respond.

Control transfer request during the enumeration phase
Control transfer request during the enumeration phase

This waveform is a control transfer request during the enumeration phase. The SYNC field starts with 0x80 (10000000) to indicate clock synchronization. The PID field is SETUP, indicating that the device will be notified to start a control transfer. Next are the address field Address=0x11 and the endpoint field Endpoint=0x00, indicating that the host sends a control request to endpoint 0. Finally, there is the CRC field, and the packet ends with EOP.

Data stage of control transfer
Data stage of control transfer

The SYNC field starts with 0x80 (10000000) to indicate clock synchronization. The PID field is DATA0, indicating the data stage used for control transfer.

Application Programming Basics

Introduction to libusb Library

libusb is a library written in C that provides a generic way to access USB devices. Through it, applications can conveniently access USB devices without needing to write USB device drivers. The libusb library has three features:

  1. Good portability, supporting Linux, macOS, and Windows systems.
  2. Simple and easy to use; applications do not require privileged mode or elevated permissions to access USB devices.
  3. Supports all USB protocols from USB 1.0 to USB 3.1, with the API interface remaining unchanged for ease of development.

Compiling libusb

Source code:

Local compilation

1
2
3
4
5
6
sudo apt install autoconf automake libtool libudev-dev m4

./bootstrap.sh
./autogen.sh
make -j$(nproc)
make install

In the examples directory of the libusb source code, libusb provides some official examples as follows:

1
2
3
4
5
6
$ ls examples
dpfp ezusb.c fxload.o listdevs.c sam3u_benchmark testlibusb.o
dpfp.c ezusb.h hotplugtest listdevs.o sam3u_benchmark.c xusb
dpfp.o ezusb.o hotplugtest.c Makefile sam3u_benchmark.o xusb.c
dpfp_threaded fxload hotplugtest.o Makefile.am testlibusb xusb.o
dpfp_threaded-dpfp.o fxload.c listdevs Makefile.in testlibusb.c
  • hotplugtest.cUsed to monitor the insertion and removal of USB devices in the system.

  • listdevs.cRetrieves and displays the current USB device information of the system, including: VID, PID, bus number, device address, and port number.

  • testlibusb.cUsed to print detailed information of the USB device list: including device descriptors, configurations, interfaces, and endpoint descriptors.

Cross Compilation

1
2
3
4
5
6
7
8
9
10
11
12
make distclean
./autogen.sh
mkdir _install # Create Installation Directory

# Add $HOME/tools/gcc-arm-11.2-2022.02-x86_64-aarch64-none-linux-gnu/bin to the environment variable
./configure --host=aarch64-none-linux-gnu --disable-udev --prefix=$PWD/_install CC=aarch64-none-linux-gnu-gcc CXX=aarch64-none-linux-gnu-g++

make -j$(nproc)

make install

ls _install

Compile Examples

1
2
3
4
5
6
make -C examples \
CC=aarch64-none-linux-gnu-gcc \
CXX=aarch64-none-linux-gnu-g++ \
LDFLAGS="-L$(pwd)/_install/lib -pthread -static" \
CFLAGS="-I$(pwd)/_install/include/libusb-1.0" \
CPPFLAGS="-I$(pwd)/_install/include/libusb-1.0"

libusb API

libusb_init()

ProjectDescription
Function Prototypeint libusb_init(libusb_context **ctx);
Parameterslibusb_context **ctx: Context pointer, can be NULL to use the default context
Return ValueReturns 0 on success; returns a negative error code on failure
Function DescriptionInitialize the libusb library, must be called before any other libusb operations

libusb_get_device_list()

ItemDescription
Function Prototypessize_t libusb_get_device_list(libusb_context *ctx, libusb_device ***list);
Parametersctx: Context
list: Output device array
Return ValueReturns the number of devices on success; returns a negative number on failure
Function DescriptionGet the list of currently connected USB devices

libusb_free_device_list()

ItemDescription
Function Prototypevoid libusb_free_device_list(libusb_device **list, int unref_devices);
Parameterslist: Device list
unref_devices: Whether to decrease the reference count
Return valueNone
Function descriptionRelease device list

libusb_open_device_with_vid_pid()

ItemDescription
Function prototypelibusb_device_handle *libusb_open_device_with_vid_pid(libusb_context *ctx, uint16_t vid, uint16_t pid);
Parametersctx: Context
vid: Vendor ID
pid: Product ID
Return valueReturns device handle on success; returns NULL on failure.
Function DescriptionFind and open device by VID/PID.

libusb_open()

ItemDescription
Function Prototypeint libusb_open(libusb_device *dev, libusb_device_handle **dev_handle);
Parametersdev: Device pointer
dev_handle: Output device handle
Return ValueReturns 0 on success; returns a negative number on failure.
Function DescriptionOpen the specified USB device.

libusb_get_device_descriptor()

ItemDescription
Function Prototypeint libusb_get_device_descriptor(libusb_device *dev, struct libusb_device_descriptor *desc);
Parametersdev: Device
desc: Output Device Descriptor
Return ValueReturns 0 on success;
Returns a negative number on failure
Function DescriptionGet Device Descriptor

libusb_get_config_descriptor()

ItemDescription
Function Prototypeint libusb_get_config_descriptor(libusb_device *dev, uint8_t config_index, struct libusb_config_descriptor **config);
Parametersdev: Device
config_index: Configuration Index
config: Output configuration descriptor
Return valueReturns 0 on success;
Returns a negative number on failure
Function descriptionGet device configuration descriptor

libusb_free_config_descriptor()

ItemDescription
Function prototypevoid libusb_free_config_descriptor(struct libusb_config_descriptor *config);
Parametersconfig: Configuration descriptor
Return valueNone
Function descriptionRelease configuration descriptor

libusb_bulk_transfer()

ItemDescription
Function Prototypeint libusb_bulk_transfer(libusb_device_handle *dev_handle, unsigned char endpoint, unsigned char *data, int length, int *actual_length, unsigned int timeout);
Parametersdev_handle: Device Handle
endpoint: Endpoint Address
data: Buffer
length: Length
actual_length: Actual Length
timeout: Timeout (ms)
Return ValueReturns 0 on success;
Returns a negative number on failure
Function DescriptionPerforms USB bulk transfer

libusb_interrupt_transfer()

ItemDescription
Function Prototypeint libusb_interrupt_transfer(libusb_device_handle *dev_handle, unsigned char endpoint, unsigned char *data, int length, int *actual_length, unsigned int timeout);
Parametersdev_handle: Device Handle
endpoint: Endpoint Address
data: Buffer
length: Length
actual_length: Actual Length
timeout: Timeout (ms)
Return ValueReturns 0 on success;
Returns a negative number on failure
Function DescriptionPerforms USB interrupt transfer

libusb_set_auto_detach_kernel_driver()

ItemDescription
Function Prototypeint libusb_set_auto_detach_kernel_driver(libusb_device_handle *dev_handle, int enable);
Parametersdev_handle: Device Handle
enable: 1 Enable / 0 Disable
Return ValueReturns 0 on success;
Returns a negative number on failure
Function DescriptionAutomatically detach kernel driver

libusb_claim_interface()

ItemDescription
Function Prototypeint libusb_claim_interface(libusb_device_handle *dev_handle, int interface_number);
Parametersdev_handle: Device handle
interface_number: Interface number
Return valueReturns 0 on success;
Returns a negative number on failure
Function descriptionDeclares a device interface

libusb_alloc_transfer()

ItemDescription
Function prototypestruct libusb_transfer *libusb_alloc_transfer(int iso_packets);
Parametersiso_packets: Number of isochronous transfer packets
Return valueReturns a transfer pointer on success;
Returns NULL on failure
Function DescriptionAllocate Asynchronous Transfer Structure

libusb_fill_interrupt_transfer()

ItemDescription
Function Prototypevoid libusb_fill_interrupt_transfer(struct libusb_transfer *transfer, libusb_device_handle *dev_handle,unsigned char endpoint, unsigned char *buffer, int length,libusb_transfer_cb_fn callback, void *user_data, unsigned int timeout)
Parameterstransfer: Transfer Object
dev_handle: Device Handle
endpoint: Endpoint
buffer: Buffer
length: Length
callback: Callback Function
user_data: User Data
timeout: Timeout
Return ValueNone
FunctionConfigure asynchronous interrupt transfer object

libusb_submit_transfer()

ItemDescription
Function prototypeint libusb_submit_transfer(struct libusb_transfer *transfer);
Parameterstransfer: Configured transfer object
Return valueReturns 0 on success;
Returns a negative number on failure
FunctionSubmit asynchronous transfer

libusb_cancel_transfer()

ItemDescription
Function Prototypeint libusb_cancel_transfer(struct libusb_transfer *transfer);
Parameterstransfer: Transfer Object
Return ValueReturns 0 on success;
Returns a negative number on failure
Function DescriptionCancel asynchronous transfer

libusb_free_transfer()

ItemDescription
Function Prototypevoid libusb_free_transfer(struct libusb_transfer *transfer);
Parameterstransfer: Transfer Object
Return ValueNone
Function DescriptionRelease the transmission structure

libusb_handle_events()

ItemDescription
Function Prototypeint libusb_handle_events(libusb_context *ctx);
Parametersctx: Context
Return ValueReturns 0 on success;
Returns a negative number on failure
Function DescriptionProcess pending events (blocking mode)

Example

Example 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>
#include "libusb.h"

int main(void)
{
int ret; // Function return value storage
int cnt; // Device count
libusb_device **device = NULL; // Device list pointer
libusb_device *dev; // Single device pointer
int i = 0; // Loop counter

// Initialize libusb library
ret = libusb_init(NULL);
if (ret < 0) {
printf("libusb_init error \n");
return ret;
}

// Get USB device list
cnt = libusb_get_device_list(NULL, &device);
if (cnt < 0) {
printf("libusb_get_device_list error \n");
return cnt;
}

// Traverse device list
while (dev = device[i++]) {
struct libusb_device_descriptor desc; // Device descriptor

// Get device descriptor
ret = libusb_get_device_descriptor(dev, &desc);
if (ret < 0) {
printf("libusb_get_device_descriptor error \n");
return ret;
}

// Print vendor ID and product ID
printf("%04x : %04x\n", desc.idVendor, desc.idProduct);
}

// Free device list (unref=1 means decrement reference count)
libusb_free_device_list(device, 1);

// Free all resources occupied by libusb library
libusb_exit(NULL);

return 0;
}

Example 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#include <stdio.h>
#include "libusb.h"

int main(void)
{
// Variable declaration area
int ret; // Store function return value
int cnt; // Device Counter
libusb_device **device = NULL; // USB Device List Pointer
libusb_device *dev; // Single USB Device Pointer
int i = 0, k = 0, j = 0; // Loop Counter
int endpoint; // Endpoint Address
int flag = 0; // Flag Bit

// Descriptor Structure
struct libusb_config_descriptor *config_desc = NULL; // USB Configuration Descriptor
const struct libusb_interface_descriptor *interface_descriptor = NULL; // Interface Descriptor
libusb_device_handle *dev_handle = NULL; // USB Device Handle

// Data Buffer
unsigned char buffer[16]; // Data Receive Buffer
int transferred; // Actual Transferred Bytes

// Initialize libusb Library
ret = libusb_init(NULL);
if (ret < 0) {
printf("libusb初始化失败\n");
return ret;
}

// Get USB Device List
cnt = libusb_get_device_list(NULL, &device);
if (cnt < 0) {
printf("获取设备列表失败\n");
return cnt;
}

// Traverse the device list to find HID devices
while (dev = device[i++]) {
// Get the device configuration descriptor
ret = libusb_get_config_descriptor(dev, 0, &config_desc);
if (ret < 0) {
printf("获取配置描述符失败\n");
return ret;
}

// Traverse all interfaces
for (k = 0; k < config_desc->bNumInterfaces; k++) {
interface_descriptor = &config_desc->interface[k].altsetting[0];

// Find HID class devices (Class=3) with keyboard protocol (Protocol=1)
if (interface_descriptor->bInterfaceClass == 3 &&
interface_descriptor->bInterfaceProtocol == 1) {

// Traverse endpoints to find the interrupt input endpoint
for (j = 0; j < interface_descriptor->bNumEndpoints; j++) {
if ((interface_descriptor->endpoint[j].bmAttributes & 3) == LIBUSB_TRANSFER_TYPE_INTERRUPT ||
(interface_descriptor->endpoint[j].bmAttributes & 0x80) == LIBUSB_ENDPOINT_IN) {
endpoint = interface_descriptor->endpoint[j].bEndpointAddress;
flag = 1; // Target device found
break;
}
}
}
if (flag) break;
}
if (flag) break;
}

// Open the target device
ret = libusb_open(dev, &dev_handle);
if (ret < 0) {
printf("打开设备失败\n");
return ret;
}

// Set auto-detach kernel driver
libusb_set_auto_detach_kernel_driver(dev_handle, 1);

// Claim the interface
libusb_claim_interface(dev_handle, interface_descriptor->bInterfaceNumber);

// Main loop: continuously read keyboard data
while (1) {
// Read keyboard data via interrupt transfer
ret = libusb_interrupt_transfer(dev_handle, endpoint, buffer, 16, &transferred, 5000);
if (ret >= 0) { // Read successful
// Print the received keyboard data
for (i = 0; i < transferred; i++) {
printf("%02x ", buffer[i]);
}
printf("\n");
}
}

// Resource release
libusb_free_config_descriptor(config_desc); // Release the configuration descriptor
libusb_free_device_list(device, 1); // Release device list
libusb_exit(NULL); // Release libusb resources

return 0;
}

Example 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#include <stdio.h>
#include "libusb.h"

// Global variable declaration
struct libusb_transfer *keyboard_transfer = NULL; // Asynchronous transfer control block
unsigned char buffer[16]; // Keyboard data receive buffer
int ret; // Function return value storage

// Asynchronous transfer callback function
void callback_recv(struct libusb_transfer *transfer) {
int k;
if (keyboard_transfer->status == LIBUSB_TRANSFER_COMPLETED) {
if (transfer->actual_length > 0) {
// Print received keyboard data (hexadecimal)
for (k = 0; k < transfer->actual_length; k++) {
printf("%02x ", buffer[k]);
}
printf("\n");
}
}
// Resubmit transfer request (achieve continuous monitoring)
ret = libusb_submit_transfer(keyboard_transfer);
if (ret < 0) {
libusb_cancel_transfer(keyboard_transfer);
libusb_free_transfer(keyboard_transfer);

}
}

int main(void) {
// Device enumeration related variables
int cnt; // Device count counter
libusb_device **device = NULL; // USB device list
libusb_device *dev = NULL; // Current device pointer
int i = 0, k = 0, j = 0; // Loop counter
int endpoint; // Endpoint Address
int flag = 0; // Device Lookup Flag

// USB Descriptor Pointer
struct libusb_config_descriptor *config_desc = NULL; // Configuration Descriptor
const struct libusb_interface_descriptor *interface_descriptor = NULL; // Interface Descriptor
libusb_device_handle *dev_handle = NULL; // Device Handle

// Initialize libusb Library
ret = libusb_init(NULL);
if (ret < 0) {
printf("libusb初始化失败\n");
return ret;
}

// Get USB Device List
cnt = libusb_get_device_list(NULL, &device);
if (cnt < 0) {
printf("获取设备列表失败\n");
return cnt;
}

// Enumerate Devices to Find HID Keyboard
while ((dev = device[i++])) {
// Get Default Configuration Descriptor
ret = libusb_get_config_descriptor(dev, 0, &config_desc);
if (ret < 0) {
printf("获取配置描述符失败\n");
return ret;
}

// Enumerate All Interfaces
for (k = 0; k < config_desc->bNumInterfaces; k++) {
interface_descriptor = &config_desc->interface[k].altsetting[0];

// Check if HID Keyboard Device (Class=3, Protocol=1)
if (interface_descriptor->bInterfaceClass == 3 &&
interface_descriptor->bInterfaceProtocol == 1) {

// Find Interrupt Input Endpoint
for (j = 0; j < interface_descriptor->bNumEndpoints; j++) {
if ((interface_descriptor->endpoint[j].bmAttributes & 3) == LIBUSB_TRANSFER_TYPE_INTERRUPT ||
(interface_descriptor->endpoint[j].bmAttributes & 0x80) == LIBUSB_ENDPOINT_IN) {
endpoint = interface_descriptor->endpoint[j].bEndpointAddress;
flag = 1; // Mark Target Device Found
break;
}
}
}
if (flag) break;
}
if (flag) break;
}

// Open Found Keyboard Device
ret = libusb_open(dev, &dev_handle);
if (ret < 0) {
printf("打开设备失败\n");
return ret;
}

// Set automatic unloading of kernel driver (to avoid conflicts)
libusb_set_auto_detach_kernel_driver(dev_handle, 1);

// Declare the use of this interface
libusb_claim_interface(dev_handle, interface_descriptor->bInterfaceNumber);

// Configure asynchronous transfer
keyboard_transfer = libusb_alloc_transfer(0); // Allocate transfer control block
libusb_fill_interrupt_transfer( // Fill interrupt transfer parameters
keyboard_transfer, dev_handle, endpoint,
buffer, 16, callback_recv, NULL, 5000);

// Submit asynchronous transfer request
ret = libusb_submit_transfer(keyboard_transfer);
if (ret < 0) {
libusb_cancel_transfer(keyboard_transfer);
libusb_free_transfer(keyboard_transfer);
return ret;
}

// Main event loop (handle USB events)
while (1) {
libusb_handle_events(NULL);
}

// Resource cleanup (will not actually be executed here)
libusb_free_config_descriptor(config_desc);
libusb_free_device_list(device, 1);
libusb_exit(NULL);

return 0;
}

USB driver development framework

In the Linux kernel, USB driver development is mainly divided into two categories:

  • Host-side driver
  • and Device-side driver.

The host-side driver is responsible for managing USB devices plugged into the host; the device-side driver is responsible for how the device communicates with the host as a USB device.

Since the term “USB devices drivers” can be confusing, people usually use “usb gadget driver” to describe USB device drivers.

At the hardware level, the USB controllers on the host side and device side are called the Host Controller and USB Device Controller (UDC), respectively, as shown in the figure below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────────────┐      ┌─────────────────────┐
│ USB设备驱动 │ │ Gadget Function驱动 │
└───────────┬─────────┘ └───────────┬─────────┘
│ │
┌───────────▼─────────┐ ┌───────────▼─────────┐
│ USB核心层 │ │ Gadget Function API │
└───────────┬─────────┘ └───────────┬─────────┘
│ │
┌───────────▼─────────┐ ┌───────────▼─────────┐
│ USB主机控制器驱动 │ │ UDC驱动 │
└───────────┬─────────┘ └───────────┬─────────┘
└──────────────┬──────────────┘

┌────────▼────────┐
│ USB总线 │
└─────────────────┘
  • In Linux systems, USB drivers can be observed from both the host side and the device side. From the host side, the USB host controller driver is at the bottom layer of USB drivers, responsible for controlling the hardware. Above it runs the USB core layer, and above that is the USB device driver.

  • From the device side, USB device-side drivers have three layers: the UDC driver, the Gadget Function API, and the Gadget Function driver.

The USB driver architecture is shown in the following figure.

Rockchip_Developer_Guide_USB_Section 5.1 of CN.pdf
Rockchip_Developer_Guide_USB_Section 5.1 of CN.pdf

The Linux USB protocol stack is a layered architecture, with USB Device drivers on the left, USB Host drivers on the right, the application layer at the top, the drivers for different USB controllers and PHYs of Rockchip series chips at the bottom, and the USB core layer in the middle.

USB Keyboard Driver Example

Let the iTOP-RK3568 development board act as a USB host, connecting external USB devices such as a USB keyboard.

In Rockchip’s official kernel source code, USB keyboard and mouse drivers are enabled by default. To conduct subsequent USB keyboard driver development experiments, this default driver setting must be disabled. Since USB mice and keyboards are HID devices, we can open the kernel configuration interface by entering the following command to enable or disable the HID driver.

1
2
3
4
5
6
---> Device Drivers
---> HID support
---> USB HID support
<*> USB HID transport layer
[ ] PID device support
[*] /dev/hiddev raw HID device support

Disable the USB HID transport layer.

Its help information is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CONFIG_USB_HID:

Say Y here if you want to connect USB keyboards, mice, joysticks, graphic tablets, or any other HID based devices to your computer via USB, as well as Uninterruptible Power Supply (UPS) and monitor control devices.

You can't use this driver and the HIDBP (Boot Protocol) keyboard and mouse drivers at the same time. More information is available: file:Documentation/input/input.rst.

If unsure, say Y.

To compile this driver as a module, choose M here: the module will be called usbhid.

Symbol: USB_HID [=y]
Type : tristate
Defined at drivers/hid/usbhid/Kconfig:5
Prompt: USB HID transport layer
Depends on: USB [=y] && INPUT [=y]
Location:
-> Device Drivers
-> HID support
-> USB HID support
Selects: HID [=y]

struct usb_driver *driver

is the driver’s description structure, with its core members defined as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**
* struct usb_driver - identifies USB interface driver to usbcore
* @name: The driver name should be unique among USB drivers,
* and should normally be the same as the module name.
* @probe: Called to see if the driver is willing to manage a particular
* interface on a device. If it is, probe returns zero and uses
* usb_set_intfdata() to associate driver-specific data with the
* interface. It may also use usb_set_interface() to specify the
* appropriate altsetting. If unwilling to manage the interface,
* return -ENODEV, if genuine IO errors occurred, an appropriate
* negative errno value.
* @disconnect: Called when the interface is no longer accessible, usually
* because its device has been (or is being) disconnected or the
* driver module is being unloaded.
* @unlocked_ioctl: Used for drivers that want to talk to userspace through
* the "usbfs" filesystem. This lets devices provide ways to
* expose information to user space regardless of where they
* do (or don't) show up otherwise in the filesystem.
* @suspend: Called when the device is going to be suspended by the
* system either from system sleep or runtime suspend context. The
* return value will be ignored in system sleep context, so do NOT
* try to continue using the device if suspend fails in this case.
* Instead, let the resume or reset-resume routine recover from
* the failure.
* @resume: Called when the device is being resumed by the system.
* @reset_resume: Called when the suspended device has been reset instead
* of being resumed.
* @pre_reset: Called by usb_reset_device() when the device is about to be
* reset. This routine must not return until the driver has no active
* URBs for the device, and no more URBs may be submitted until the
* post_reset method is called.
* @post_reset: Called by usb_reset_device() after the device
* has been reset
* @id_table: USB drivers use ID table to support hotplugging.
* Export this with MODULE_DEVICE_TABLE(usb,...). This must be set
* or your driver's probe function will never get called.
* @dev_groups: Attributes attached to the device that will be created once it
* is bound to the driver.
* @dynids: used internally to hold the list of dynamically added device
* ids for this driver.
* @drvwrap: Driver-model core structure wrapper.
* @no_dynamic_id: if set to 1, the USB core will not allow dynamic ids to be
* added to this driver by preventing the sysfs file from being created.
* @supports_autosuspend: if set to 0, the USB core will not allow autosuspend
* for interfaces bound to this driver.
* @soft_unbind: if set to 1, the USB core will not kill URBs and disable
* endpoints before calling the driver's disconnect method.
* @disable_hub_initiated_lpm: if set to 1, the USB core will not allow hubs
* to initiate lower power link state transitions when an idle timeout
* occurs. Device-initiated USB 3.0 link PM will still be allowed.
*
* USB interface drivers must provide a name, probe() and disconnect()
* methods, and an id_table. Other driver fields are optional.
*
* The id_table is used in hotplugging. It holds a set of descriptors,
* and specialized data may be associated with each entry. That table
* is used by both user and kernel mode hotplugging support.
*
* The probe() and disconnect() methods are called in a context where
* they can sleep, but they should avoid abusing the privilege. Most
* work to connect to a device should be done when the device is opened,
* and undone at the last close. The disconnect code needs to address
* concurrency issues with respect to open() and close() methods, as
* well as forcing all pending I/O requests to complete (by unlinking
* them as necessary, and blocking until the unlinks complete).
*/
struct usb_driver {
const char *name;// Driver name

int (*probe) (struct usb_interface *intf,
const struct usb_device_id *id); // When a matching USB device is detected, the kernel calls this function.

void (*disconnect) (struct usb_interface *intf); // Called when the device is disconnected or the driver is unloaded.

int (*unlocked_ioctl) (struct usb_interface *intf, unsigned int code,
void *buf);

int (*suspend) (struct usb_interface *intf, pm_message_t message);
int (*resume) (struct usb_interface *intf);
int (*reset_resume)(struct usb_interface *intf);

int (*pre_reset)(struct usb_interface *intf);
int (*post_reset)(struct usb_interface *intf);

const struct usb_device_id *id_table; // Pointer to usb_device_id structure array, defines the list of devices supported by the driver
const struct attribute_group **dev_groups;

struct usb_dynids dynids;
struct usbdrv_wrap drvwrap;
unsigned int no_dynamic_id:1;
unsigned int supports_autosuspend:1;
unsigned int disable_hub_initiated_lpm:1;
unsigned int soft_unbind:1;
};

struct usb_device_id

In the main structure of the driverusb_driver, through.id_tablethe field binds the device table to the driver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
* struct usb_device_id - identifies USB devices for probing and hotplugging
* @match_flags: Bit mask controlling which of the other fields are used to
* match against new devices. Any field except for driver_info may be
* used, although some only make sense in conjunction with other fields.
* This is usually set by a USB_DEVICE_*() macro, which sets all
* other fields in this structure except for driver_info.
* @idVendor: USB vendor ID for a device; numbers are assigned
* by the USB forum to its members.
* @idProduct: Vendor-assigned product ID.
* @bcdDevice_lo: Low end of range of vendor-assigned product version numbers.
* This is also used to identify individual product versions, for
* a range consisting of a single device.
* @bcdDevice_hi: High end of version number range. The range of product
* versions is inclusive.
* @bDeviceClass: Class of device; numbers are assigned
* by the USB forum. Products may choose to implement classes,
* or be vendor-specific. Device classes specify behavior of all
* the interfaces on a device.
* @bDeviceSubClass: Subclass of device; associated with bDeviceClass.
* @bDeviceProtocol: Protocol of device; associated with bDeviceClass.
* @bInterfaceClass: Class of interface; numbers are assigned
* by the USB forum. Products may choose to implement classes,
* or be vendor-specific. Interface classes specify behavior only
* of a given interface; other interfaces may support other classes.
* @bInterfaceSubClass: Subclass of interface; associated with bInterfaceClass.
* @bInterfaceProtocol: Protocol of interface; associated with bInterfaceClass.
* @bInterfaceNumber: Number of interface; composite devices may use
* fixed interface numbers to differentiate between vendor-specific
* interfaces.
* @driver_info: Holds information used by the driver. Usually it holds
* a pointer to a descriptor understood by the driver, or perhaps
* device flags.
*
* In most cases, drivers will create a table of device IDs by using
* USB_DEVICE(), or similar macros designed for that purpose.
* They will then export it to userspace using MODULE_DEVICE_TABLE(),
* and provide it to the USB core through their usb_driver structure.
*
* See the usb_match_id() function for information about how matches are
* performed. Briefly, you will normally use one of several macros to help
* construct these entries. Each entry you provide will either identify
* one or more specific products, or will identify a class of products
* which have agreed to behave the same. You should put the more specific
* matches towards the beginning of your table, so that driver_info can
* record quirks of specific products.
*/
struct usb_device_id {
/* which fields to match against? */
__u16 match_flags; // Match flags (specify which fields participate in matching)

/* Used for product specific matches; range is inclusive */
__u16 idVendor; // Vendor ID (VID)
__u16 idProduct; // Product ID (PID)
__u16 bcdDevice_lo; // Device version lower limit
__u16 bcdDevice_hi; // Device version upper limit

/* Used for device class matches */
__u8 bDeviceClass; // Device class
__u8 bDeviceSubClass; // Device subclass
__u8 bDeviceProtocol; // Device protocol

/* Used for interface class matches */
__u8 bInterfaceClass; // Interface class
__u8 bInterfaceSubClass; // Interface subclass
__u8 bInterfaceProtocol; // Interface protocol

/* Used for vendor-specific interface matches */
__u8 bInterfaceNumber;

/* not matched against */
kernel_ulong_t driver_info
__attribute__((aligned(sizeof(kernel_ulong_t)))); // Driver private data (optional)
};

usb_register()

ItemDescription
Function prototypeint usb_register(struct usb_driver *driver);
Header file#include <linux/usb.h>
Parametersstruct usb_driver *driver: Pointer to USB driver description structure
Return valueReturns 0 on success;
Returns negative error code on failure
Function descriptionRegisters the USB driver with the Linux USB subsystem, enabling the driver to respond to plug/unplug events of matching devices

In theusb_registerfunction, it internally callsusb_register_driver, passing the driver structureusb_driverRegister to the kernel USB subsystem. Callusb_register_driverAfter, theusb_driverstructure is mounted to the USB bus (usb_bus_type) driver list, as shown below:

1
2
3
/* use a define to avoid include chaining to get THIS_MODULE & friends */
#define usb_register(driver) \
usb_register_driver(driver, THIS_MODULE, KBUILD_MODNAME)

usb_register_driver()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* usb_register_driver - register a USB interface driver
* @new_driver: USB operations for the interface driver
* @owner: module owner of this driver.
* @mod_name: module name string
*
* Registers a USB interface driver with the USB core. The list of
* unattached interfaces will be rescanned whenever a new driver is
* added, allowing the new driver to attach to any recognized interfaces.
*
* Return: A negative error code on failure and 0 on success.
*
* NOTE: if you want your driver to use the USB major number, you must call
* usb_register_dev() to enable that functionality. This function no longer
* takes care of that.
*/
int usb_register_driver(struct usb_driver *new_driver, struct module *owner,
const char *mod_name)
{
int retval = 0;

if (usb_disabled())
return -ENODEV;

new_driver->drvwrap.for_devices = 0;
new_driver->drvwrap.driver.name = new_driver->name;
new_driver->drvwrap.driver.bus = &usb_bus_type;
new_driver->drvwrap.driver.probe = usb_probe_interface;
new_driver->drvwrap.driver.remove = usb_unbind_interface;
new_driver->drvwrap.driver.owner = owner;
new_driver->drvwrap.driver.mod_name = mod_name;
new_driver->drvwrap.driver.dev_groups = new_driver->dev_groups;
spin_lock_init(&new_driver->dynids.lock);
INIT_LIST_HEAD(&new_driver->dynids.list);

retval = driver_register(&new_driver->drvwrap.driver);
if (retval)
goto out;

retval = usb_create_newid_files(new_driver);
if (retval)
goto out_newid;

pr_info("%s: registered new interface driver %s\n",
usbcore_name, new_driver->name);

out:
return retval;

out_newid:
driver_unregister(&new_driver->drvwrap.driver);

pr_err("%s: error %d registering interface driver %s\n",
usbcore_name, retval, new_driver->name);
goto out;
}
EXPORT_SYMBOL_GPL(usb_register_driver);

usb_bus_typeThe structure has a match member,usb_device_matchthe function is responsible for the matching logic.

1
2
3
4
5
6
struct bus_type usb_bus_type = {
.name = "usb",
.match = usb_device_match,
.uevent = usb_uevent,
.need_parent_lock = true,
};

usb_device_match()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
static int usb_device_match(struct device *dev, struct device_driver *drv)
{
/* devices and interfaces are handled separately */
if (is_usb_device(dev)) { // Determine if it is a USB device object
struct usb_device *udev;
struct usb_device_driver *udrv;

/* interface drivers never match devices */
if (!is_usb_device_driver(drv)) // Check if the driver is a device-level driver
return 0;

udev = to_usb_device(dev);
udrv = to_usb_device_driver(drv);

/* If the device driver under consideration does not have a
* id_table or a match function, then let the driver's probe
* function decide.
*/
if (!udrv->id_table && !udrv->match)
return 1;

return usb_driver_applicable(udev, udrv);

} else if (is_usb_interface(dev)) { // Determine if it is a USB interface object
struct usb_interface *intf;
struct usb_driver *usb_drv;
const struct usb_device_id *id;

/* device drivers never match interfaces */
/* Device driver does not match interface: prevent device driver from incorrectly binding to interface */
if (is_usb_device_driver(drv))
return 0;

intf = to_usb_interface(dev); // Get interface instance
usb_drv = to_usb_driver(drv); // Get USB driver instance

/* Match static device ID table (id_table defined during driver registration) */
id = usb_match_id(intf, usb_drv->id_table);
if (id)
return 1;

/* Match dynamic IDs (such as temporary device IDs added via sysfs) */
id = usb_match_dynamic_id(intf, usb_drv);
if (id)
return 1;
}

return 0;
}

The above function usesusb_match_idThe function matches the static device ID table,usb_match_idThe function implementation is as follows:

usb_match_id()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* usb_match_id - find first usb_device_id matching device or interface
* @interface: the interface of interest
* @id: array of usb_device_id structures, terminated by zero entry
*
* usb_match_id searches an array of usb_device_id's and returns
* the first one matching the device or interface, or null.
* This is used when binding (or rebinding) a driver to an interface.
* Most USB device drivers will use this indirectly, through the usb core,
* but some layered driver frameworks use it directly.
* These device tables are exported with MODULE_DEVICE_TABLE, through
* modutils, to support the driver loading functionality of USB hotplugging.
*
* Return: The first matching usb_device_id, or %NULL.
*
* What Matches:
*
* The "match_flags" element in a usb_device_id controls which
* members are used. If the corresponding bit is set, the
* value in the device_id must match its corresponding member
* in the device or interface descriptor, or else the device_id
* does not match.
*
* "driver_info" is normally used only by device drivers,
* but you can create a wildcard "matches anything" usb_device_id
* as a driver's "modules.usbmap" entry if you provide an id with
* only a nonzero "driver_info" field. If you do this, the USB device
* driver's probe() routine should use additional intelligence to
* decide whether to bind to the specified interface.
*
* What Makes Good usb_device_id Tables:
*
* The match algorithm is very simple, so that intelligence in
* driver selection must come from smart driver id records.
* Unless you have good reasons to use another selection policy,
* provide match elements only in related groups, and order match
* specifiers from specific to general. Use the macros provided
* for that purpose if you can.
*
* The most specific match specifiers use device descriptor
* data. These are commonly used with product-specific matches;
* the USB_DEVICE macro lets you provide vendor and product IDs,
* and you can also match against ranges of product revisions.
* These are widely used for devices with application or vendor
* specific bDeviceClass values.
*
* Matches based on device class/subclass/protocol specifications
* are slightly more general; use the USB_DEVICE_INFO macro, or
* its siblings. These are used with single-function devices
* where bDeviceClass doesn't specify that each interface has
* its own class.
*
* Matches based on interface class/subclass/protocol are the
* most general; they let drivers bind to any interface on a
* multiple-function device. Use the USB_INTERFACE_INFO
* macro, or its siblings, to match class-per-interface style
* devices (as recorded in bInterfaceClass).
*
* Note that an entry created by USB_INTERFACE_INFO won't match
* any interface if the device class is set to Vendor-Specific.
* This is deliberate; according to the USB spec the meanings of
* the interface class/subclass/protocol for these devices are also
* vendor-specific, and hence matching against a standard product
* class wouldn't work anyway. If you really want to use an
* interface-based match for such a device, create a match record
* that also specifies the vendor ID. (Unforunately there isn't a
* standard macro for creating records like this.)
*
* Within those groups, remember that not all combinations are
* meaningful. For example, don't give a product version range
* without vendor and product IDs; or specify a protocol without
* its associated class and subclass.
*/
const struct usb_device_id *usb_match_id(struct usb_interface *interface,
const struct usb_device_id *id)
{
/* proc_connectinfo in devio.c may call us with id == NULL. */
/* Handle special case: return directly when the ID table is empty */
if (id == NULL)
return NULL;

/* It is important to check that id->driver_info is nonzero,
since an entry that is all zeroes except for a nonzero
id->driver_info is the way to create an entry that
indicates that the driver want to examine every
device and interface. */
/*
* Traverse the device ID table,until encountering an all-zero termination entry:
* Loop condition check id field is not all zeros
*/
for (; id->idVendor || id->idProduct || id->bDeviceClass ||
id->bInterfaceClass || id->driver_info; id++) {
/* Call the core matching function to check if the current ID matches the interface */
if (usb_match_one_id(interface, id))
return id; // Return the entry if matching succeeds
}

return NULL; // No matching entry found after traversal
}
EXPORT_SYMBOL_GPL(usb_match_id);

In the above code,usb_match_one_idthe function is used to determine whether the USB interface matchesusb_device_idthe implementation of the function is as follows:

usb_match_one_id()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* returns 0 if no match, 1 if match */
int usb_match_one_id(struct usb_interface *interface,
const struct usb_device_id *id)
{
struct usb_host_interface *intf;
struct usb_device *dev;

/* proc_connectinfo in devio.c may call us with id == NULL. */
// 1. Null pointer protection: if id is null, directly return no match
if (id == NULL)
return 0;
// 2. Get the current interface configuration (cur_altsetting) and the associated USB device
intf = interface->cur_altsetting;
dev = interface_to_usbdev(interface);

// 3. Device-level matching: Check the device descriptor (VID/PID/device class, etc.)
if (!usb_match_device(dev, id))
return 0;

// 4. Interface-level matching: Check the interface descriptor (interface class/subclass/protocol, etc.)
return usb_match_one_id_intf(dev, intf, id);
}
EXPORT_SYMBOL_GPL(usb_match_one_id);

According to theusb_device_idfield in thematch_flagsfield, determine the fields to match (such as VID, PID, interface class, etc.). An example matching table is shown below

1
2
3
4
5
// Define the device matching table
static const struct usb_device_id my_kbd_id_table[] = {
{USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT, USB_INTERFACE_PROTOCOL_KEYBOARD)},
{}, // Terminator (must keep empty entry)
};

usb_deregister()

ItemDescription
Function prototypevoid usb_deregister(struct usb_driver *driver);
Header file#include <linux/usb.h>
Parametersstruct usb_driver *driver: Registered USB driver
Return valueNone
Function DescriptionUnregister the driver from the USB subsystem and trigger the disconnect() callback for all matching devices

USB Request Block

URB, which stands for USB Request Block, is the basic carrier used to describe communication with USB devices in USB device drivers, and is also a core data structure, similar to thesk_buffstructure in network device drivers. The URB structure is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
/**
* struct urb - USB Request Block
* @urb_list: For use by current owner of the URB.
* @anchor_list: membership in the list of an anchor
* @anchor: to anchor URBs to a common mooring
* @ep: Points to the endpoint's data structure. Will eventually
* replace @pipe.
* @pipe: Holds endpoint number, direction, type, and more.
* Create these values with the eight macros available;
* usb_{snd,rcv}TYPEpipe(dev,endpoint), where the TYPE is "ctrl"
* (control), "bulk", "int" (interrupt), or "iso" (isochronous).
* For example usb_sndbulkpipe() or usb_rcvintpipe(). Endpoint
* numbers range from zero to fifteen. Note that "in" endpoint two
* is a different endpoint (and pipe) from "out" endpoint two.
* The current configuration controls the existence, type, and
* maximum packet size of any given endpoint.
* @stream_id: the endpoint's stream ID for bulk streams
* @dev: Identifies the USB device to perform the request.
* @status: This is read in non-iso completion functions to get the
* status of the particular request. ISO requests only use it
* to tell whether the URB was unlinked; detailed status for
* each frame is in the fields of the iso_frame-desc.
* @transfer_flags: A variety of flags may be used to affect how URB
* submission, unlinking, or operation are handled. Different
* kinds of URB can use different flags.
* @transfer_buffer: This identifies the buffer to (or from) which the I/O
* request will be performed unless URB_NO_TRANSFER_DMA_MAP is set
* (however, do not leave garbage in transfer_buffer even then).
* This buffer must be suitable for DMA; allocate it with
* kmalloc() or equivalent. For transfers to "in" endpoints, contents
* of this buffer will be modified. This buffer is used for the data
* stage of control transfers.
* @transfer_dma: When transfer_flags includes URB_NO_TRANSFER_DMA_MAP,
* the device driver is saying that it provided this DMA address,
* which the host controller driver should use in preference to the
* transfer_buffer.
* @sg: scatter gather buffer list, the buffer size of each element in
* the list (except the last) must be divisible by the endpoint's
* max packet size if no_sg_constraint isn't set in 'struct usb_bus'
* @num_mapped_sgs: (internal) number of mapped sg entries
* @num_sgs: number of entries in the sg list
* @transfer_buffer_length: How big is transfer_buffer. The transfer may
* be broken up into chunks according to the current maximum packet
* size for the endpoint, which is a function of the configuration
* and is encoded in the pipe. When the length is zero, neither
* transfer_buffer nor transfer_dma is used.
* @actual_length: This is read in non-iso completion functions, and
* it tells how many bytes (out of transfer_buffer_length) were
* transferred. It will normally be the same as requested, unless
* either an error was reported or a short read was performed.
* The URB_SHORT_NOT_OK transfer flag may be used to make such
* short reads be reported as errors.
* @setup_packet: Only used for control transfers, this points to eight bytes
* of setup data. Control transfers always start by sending this data
* to the device. Then transfer_buffer is read or written, if needed.
* @setup_dma: DMA pointer for the setup packet. The caller must not use
* this field; setup_packet must point to a valid buffer.
* @start_frame: Returns the initial frame for isochronous transfers.
* @number_of_packets: Lists the number of ISO transfer buffers.
* @interval: Specifies the polling interval for interrupt or isochronous
* transfers. The units are frames (milliseconds) for full and low
* speed devices, and microframes (1/8 millisecond) for highspeed
* and SuperSpeed devices.
* @error_count: Returns the number of ISO transfers that reported errors.
* @context: For use in completion functions. This normally points to
* request-specific driver context.
* @complete: Completion handler. This URB is passed as the parameter to the
* completion function. The completion function may then do what
* it likes with the URB, including resubmitting or freeing it.
* @iso_frame_desc: Used to provide arrays of ISO transfer buffers and to
* collect the transfer status for each buffer.
*
* This structure identifies USB transfer requests. URBs must be allocated by
* calling usb_alloc_urb() and freed with a call to usb_free_urb().
* Initialization may be done using various usb_fill_*_urb() functions. URBs
* are submitted using usb_submit_urb(), and pending requests may be canceled
* using usb_unlink_urb() or usb_kill_urb().
*
* Data Transfer Buffers:
*
* Normally drivers provide I/O buffers allocated with kmalloc() or otherwise
* taken from the general page pool. That is provided by transfer_buffer
* (control requests also use setup_packet), and host controller drivers
* perform a dma mapping (and unmapping) for each buffer transferred. Those
* mapping operations can be expensive on some platforms (perhaps using a dma
* bounce buffer or talking to an IOMMU),
* although they're cheap on commodity x86 and ppc hardware.
*
* Alternatively, drivers may pass the URB_NO_TRANSFER_DMA_MAP transfer flag,
* which tells the host controller driver that no such mapping is needed for
* the transfer_buffer since
* the device driver is DMA-aware. For example, a device driver might
* allocate a DMA buffer with usb_alloc_coherent() or call usb_buffer_map().
* When this transfer flag is provided, host controller drivers will
* attempt to use the dma address found in the transfer_dma
* field rather than determining a dma address themselves.
*
* Note that transfer_buffer must still be set if the controller
* does not support DMA (as indicated by hcd_uses_dma()) and when talking
* to root hub. If you have to trasfer between highmem zone and the device
* on such controller, create a bounce buffer or bail out with an error.
* If transfer_buffer cannot be set (is in highmem) and the controller is DMA
* capable, assign NULL to it, so that usbmon knows not to use the value.
* The setup_packet must always be set, so it cannot be located in highmem.
*
* Initialization:
*
* All URBs submitted must initialize the dev, pipe, transfer_flags (may be
* zero), and complete fields. All URBs must also initialize
* transfer_buffer and transfer_buffer_length. They may provide the
* URB_SHORT_NOT_OK transfer flag, indicating that short reads are
* to be treated as errors; that flag is invalid for write requests.
*
* Bulk URBs may
* use the URB_ZERO_PACKET transfer flag, indicating that bulk OUT transfers
* should always terminate with a short packet, even if it means adding an
* extra zero length packet.
*
* Control URBs must provide a valid pointer in the setup_packet field.
* Unlike the transfer_buffer, the setup_packet may not be mapped for DMA
* beforehand.
*
* Interrupt URBs must provide an interval, saying how often (in milliseconds
* or, for highspeed devices, 125 microsecond units)
* to poll for transfers. After the URB has been submitted, the interval
* field reflects how the transfer was actually scheduled.
* The polling interval may be more frequent than requested.
* For example, some controllers have a maximum interval of 32 milliseconds,
* while others support intervals of up to 1024 milliseconds.
* Isochronous URBs also have transfer intervals. (Note that for isochronous
* endpoints, as well as high speed interrupt endpoints, the encoding of
* the transfer interval in the endpoint descriptor is logarithmic.
* Device drivers must convert that value to linear units themselves.)
*
* If an isochronous endpoint queue isn't already running, the host
* controller will schedule a new URB to start as soon as bandwidth
* utilization allows. If the queue is running then a new URB will be
* scheduled to start in the first transfer slot following the end of the
* preceding URB, if that slot has not already expired. If the slot has
* expired (which can happen when IRQ delivery is delayed for a long time),
* the scheduling behavior depends on the URB_ISO_ASAP flag. If the flag
* is clear then the URB will be scheduled to start in the expired slot,
* implying that some of its packets will not be transferred; if the flag
* is set then the URB will be scheduled in the first unexpired slot,
* breaking the queue's synchronization. Upon URB completion, the
* start_frame field will be set to the (micro)frame number in which the
* transfer was scheduled. Ranges for frame counter values are HC-specific
* and can go from as low as 256 to as high as 65536 frames.
*
* Isochronous URBs have a different data transfer model, in part because
* the quality of service is only "best effort". Callers provide specially
* allocated URBs, with number_of_packets worth of iso_frame_desc structures
* at the end. Each such packet is an individual ISO transfer. Isochronous
* URBs are normally queued, submitted by drivers to arrange that
* transfers are at least double buffered, and then explicitly resubmitted
* in completion handlers, so
* that data (such as audio or video) streams at as constant a rate as the
* host controller scheduler can support.
*
* Completion Callbacks:
*
* The completion callback is made in_interrupt(), and one of the first
* things that a completion handler should do is check the status field.
* The status field is provided for all URBs. It is used to report
* unlinked URBs, and status for all non-ISO transfers. It should not
* be examined before the URB is returned to the completion handler.
*
* The context field is normally used to link URBs back to the relevant
* driver or request state.
*
* When the completion callback is invoked for non-isochronous URBs, the
* actual_length field tells how many bytes were transferred. This field
* is updated even when the URB terminated with an error or was unlinked.
*
* ISO transfer status is reported in the status and actual_length fields
* of the iso_frame_desc array, and the number of errors is reported in
* error_count. Completion callbacks for ISO transfers will normally
* (re)submit URBs to ensure a constant transfer rate.
*
* Note that even fields marked "public" should not be touched by the driver
* when the urb is owned by the hcd, that is, since the call to
* usb_submit_urb() till the entry into the completion routine.
*/
struct urb {
/* private: usb core and host controller only fields in the urb */
struct kref kref; /* reference count of the URB */
int unlinked; /* unlink error code */
void *hcpriv; /* private data for host controller */
atomic_t use_count; /* concurrent submissions counter */
atomic_t reject; /* submissions will fail */

/* public: documented fields in the urb that can be used by drivers */
struct list_head urb_list; /* list head for use by the urb's
* current owner */
struct list_head anchor_list; /* the URB may be anchored */
struct usb_anchor *anchor;
struct usb_device *dev; /* (in) pointer to associated device */
struct usb_host_endpoint *ep; /* (internal) pointer to endpoint */
unsigned int pipe; /* (in) pipe information */
unsigned int stream_id; /* (in) stream ID */
int status; /* (return) non-ISO status */
unsigned int transfer_flags; /* (in) URB_SHORT_NOT_OK | ...*/
void *transfer_buffer; /* (in) associated data buffer */
dma_addr_t transfer_dma; /* (in) dma addr for transfer_buffer */
struct scatterlist *sg; /* (in) scatter gather buffer list */
int num_mapped_sgs; /* (internal) mapped sg entries */
int num_sgs; /* (in) number of entries in the sg list */
u32 transfer_buffer_length; /* (in) data buffer length */
u32 actual_length; /* (return) actual transfer length */
unsigned char *setup_packet; /* (in) setup packet (control only) */
dma_addr_t setup_dma; /* (in) dma addr for setup_packet */
int start_frame; /* (modify) start frame (ISO) */
int number_of_packets; /* (in) number of ISO packets */
int interval; /* (modify) transfer interval
* (INT/ISO) */
int error_count; /* (return) number of ISO errors */
void *context; /* (in) context for completion */
usb_complete_t complete; /* (in) completion routine */

struct usb_iso_packet_descriptor iso_frame_desc[];
/* (in) ISO ONLY */
};

Each endpoint in a USB device processes a URB queue. The USB communication steps are as follows:

USB Communication Steps

The first step uses theusb_alloc_urbfunction to create a URB structure,usb_alloc_urbThe function description is as follows:

1
struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags);

usb_alloc_urbThe function is used to create a USB Request Block (URB) for managing data transfer requests of USB devices. URB is the core structure for data transfer in USB driver development, supporting the following transfer types:

  • Control Transfer: Device initialization and configuration.
  • Interrupt Transfer: Devices with high real-time requirements (e.g., keyboard, mouse).
  • Bulk Transfer: Large-volume data transfer (e.g., USB flash drives).
  • Isochronous Transfer: Real-time streaming data (e.g., cameras, audio devices).

Function parameters:

  • int iso_packets: Number of packets for isochronous transfer (only for isochronous transfer; set to 0 for other transfer types)
  • gfp_t mem_flags: Memory allocation flags, specifying the memory allocation method (e.g.,GFP_KERNELGFP_ATOMIC

Returns the allocated URB pointer on success, or NULL on failure.

Step 2: URB initialization, specified for a particular endpoint of a specific USB device. For interrupt URBs, use theusb_fill_int_urbfunction to initialize the URB,usb_fill_int_urbThe function is described as follows:

1
2
3
4
5
6
7
8
static inline void usb_fill_int_urb(struct urb *urb, // Target URB pointer
struct usb_device *dev, // USB device instance
unsigned int pipe, // Endpoint pipe (direction + endpoint number)
void *transfer_buffer, // Data buffer
int buffer_length, // Buffer length
usb_complete_t complete_fn, // Transfer completion callback function
void *context, // Callback function context data
int interval) // Polling interval (unit: ms)

usb_fill_int_urbFunction used to initialize an interrupt transfer URB.

Function parameters:

  • struct urb *urb: Pointer to the URB to be initialized
  • struct usb_device *dev: Pointer to the USB device to which this URB will be sent
  • unsigned int pipe: The specific endpoint of the USB device to which this URB will be sent

The pipe parameter is created using the following function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Create various pipes... */
#define usb_sndctrlpipe(dev, endpoint) \
((PIPE_CONTROL << 30) | __create_pipe(dev, endpoint))
#define usb_rcvctrlpipe(dev, endpoint) \
((PIPE_CONTROL << 30) | __create_pipe(dev, endpoint) | USB_DIR_IN)
#define usb_sndisocpipe(dev, endpoint) \
((PIPE_ISOCHRONOUS << 30) | __create_pipe(dev, endpoint))
#define usb_rcvisocpipe(dev, endpoint) \
((PIPE_ISOCHRONOUS << 30) | __create_pipe(dev, endpoint) | USB_DIR_IN)
#define usb_sndbulkpipe(dev, endpoint) \
((PIPE_BULK << 30) | __create_pipe(dev, endpoint))
#define usb_rcvbulkpipe(dev, endpoint) \
((PIPE_BULK << 30) | __create_pipe(dev, endpoint) | USB_DIR_IN)
#define usb_sndintpipe(dev, endpoint) \
((PIPE_INTERRUPT << 30) | __create_pipe(dev, endpoint))
#define usb_rcvintpipe(dev, endpoint) \
((PIPE_INTERRUPT << 30) | __create_pipe(dev, endpoint) | USB_DIR_IN)
  • void *transfer_buffer: Pointer to the buffer for sending or receiving data
  • int buffer_length: Istransfer_bufferThe size of the buffer pointed to by the pointer
  • usb_complete_t complete_fn: Transfer completion callback function
  • void *context: The “context” of the completion handler
  • int interval: Context data passed to the callback function (usually a driver private structure pointer)

For bulk URBs, use usb_fill_bulk_urb() function to initialize. For control URBs, use usb_fill_control_urb() function to initialize.

After completing the first and second steps of creating and initializing the URB, the URB can be submitted to the USB core usingusb_submit_urbfunction.usb_submit_urbThe function is described as follows

1
int usb_submit_urb(struct urb *urb, gfp_t mem_flags);

Function purpose:

Submit the initialized USB Request Block (URB) to the kernel’s USB subsystem to start data transfer. Upon completion of the transfer, the callback function specified in the URB is triggered.

Function parameters:

  • struct urb *urb: Pointer to the URB
  • gfp_t mem_flags: Memory allocation flags, specifying the memory allocation method (e.g.,GFP_KERNELGFP_ATOMIC
    • GFP_ATOMIC: In interrupt handlers, bottom halves,tasklet, timer handlers, and URB completion functions, when the caller holds a spinlock or read-write lock, and when the driver setscurrent->stateto non-TASK_RUNNINGthis flag should be used.
    • GFP_NOIO: This flag should be used in the block I/O and error handling paths of storage devices.
    • GFP_KERNEL: If there is no reason to useGFP_ATOMICandGFP_NOIO, then useGFP_KERNEL

Returns 0 on success, and a negative error code on submission failure.

Step 4: Submit the USB host controller driver specified by the USB core.

Step 5: Processed by the USB host controller, performing a transfer to the USB device. Steps 4 and 5 are completed by the USB core and host controller and are not controlled by the USB device driver.

Step 6: When the URB processing is complete, the USB host controller driver notifies the USB device driver.

The data reported by a USB keyboard is 8 bytes, which represents the data of a USB key being pressed and then released.

USB Keyboard Data Report Format
USB Keyboard Data Report Format

The format is as follows:

OffsetSizeDescription
0ByteSpecific function keys
1ByteReserved bits
2ByteKey 1
3ByteButton 2
4ByteButton 3
5ByteButton 4
6ByteButton 5
7ByteButton 6

Here, buff[0] is a bit field, where each bit corresponds to the function of a special button. When a bit is set to 1, it indicates that the button is pressed. The bit field structure is shown in the following table:

BitBit LengthDescription
01Left Ctrl
11Left Shift
21Left Alt
31Left GUI(Windows/Super key)
41Right Ctrl
51Right Shift
61Right Alt
71Right GUI(Windows/Super key)

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#include <linux/init.h>
#include <linux/module.h>
#include <linux/usb.h>
#include <linux/usb/input.h>
#include <linux/hid.h>
#include <linux/slab.h>

struct input_dev *mykbd_inputdev = NULL;
struct urb *mykbd_urb;
unsigned char *mykbd_buf;
unsigned char mykbd_buf_old[8];
int mykbd_size;
dma_addr_t mykbd_dma;

// Keyboard key code table, containing the key mapping of a standard keyboard
static const unsigned char usb_kbd_keycode[256] = {
0, 0, 0, 0, 30, 48, 46, 32, 18, 33, 34, 35, 23, 36, 37, 38,
50, 49, 24, 25, 16, 19, 31, 20, 22, 47, 17, 45, 21, 44, 2, 3,
4, 5, 6, 7, 8, 9, 10, 11, 28, 1, 14, 15, 57, 12, 13, 26,
27, 43, 43, 39, 40, 41, 51, 52, 53, 58, 59, 60, 61, 62, 63, 64,
65, 66, 67, 68, 87, 88, 99, 70, 119, 110, 102, 104, 111, 107, 109, 106,
105, 108, 103, 69, 98, 55, 74, 78, 96, 79, 80, 81, 75, 76, 77, 71,
72, 73, 82, 83, 86, 127, 116, 117, 183, 184, 185, 186, 187, 188, 189, 190,
191, 192, 193, 194, 134, 138, 130, 132, 128, 129, 131, 137, 133, 135, 136, 113,
115, 114, 0, 0, 0, 121, 0, 89, 93, 124, 92, 94, 95, 0, 0, 0,
122, 123, 90, 91, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
29, 42, 56, 125, 97, 54, 100, 126, 164, 166, 165, 163, 161, 115, 114, 113,
150, 158, 159, 128, 136, 177, 178, 176, 142, 152, 173, 140
};

// USB device ID matching table (currently empty, needs to be filled with supported devices)
const struct usb_device_id my_kbd_id_table[] = {
{ USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT,
USB_INTERFACE_PROTOCOL_KEYBOARD) },
{},

};

// Callback function for processing USB keyboard data
void mykbd_func(struct urb *urb)
{
int i;
// Process key data
for (i = 0; i < 8; i++) {
input_report_key(mykbd_inputdev, usb_kbd_keycode[i + 224], (mykbd_buf[0] >> i) & 1);
}

// Process key state changes
for (i = 2; i < 8; i++) {
// Check old key state
if (mykbd_buf_old[i] > 3 && memscan(mykbd_buf + 2, mykbd_buf_old[i], 6) == mykbd_buf + 8) {
if (usb_kbd_keycode[mykbd_buf_old[i]]) {
input_report_key(mykbd_inputdev, usb_kbd_keycode[mykbd_buf_old[i]], 0);
}
}
// Check new key state
if (mykbd_buf[i] > 3 && memscan(mykbd_buf_old + 2, mykbd_buf[i], 6) == mykbd_buf_old + 8) {
if (usb_kbd_keycode[mykbd_buf[i]]) {
input_report_key(mykbd_inputdev, usb_kbd_keycode[mykbd_buf[i]], 1);
}
}
}

// Synchronize input device state
input_sync(mykbd_inputdev);
// Save current key state
memcpy(mykbd_buf_old, mykbd_buf, 8);
// Resubmit URB to continue receiving data
usb_submit_urb(mykbd_urb, GFP_ATOMIC);
}

// Probe function: called when a device is matched
int my_kbd_probe(struct usb_interface *intf, const struct usb_device_id *id)
{
int i;
int ret;
int pipe;
struct usb_host_interface *interface;
struct usb_endpoint_descriptor *endpoint;
struct usb_device *mykbd_dev = interface_to_usbdev(intf);

// Initialize interface
interface = intf->cur_altsetting;
// Get endpoint descriptor
endpoint = &interface->endpoint[0].desc;

// Allocate memory for input device
mykbd_inputdev = input_allocate_device();

mykbd_inputdev->name = "mykbd_inputdev";
__set_bit(EV_KEY, mykbd_inputdev->evbit);
__set_bit(EV_REP, mykbd_inputdev->evbit);
for (i = 0; i < 255; i++) {
__set_bit(usb_kbd_keycode[i], mykbd_inputdev->keybit);
}
clear_bit(0, mykbd_inputdev->keybit);

// Register input device
ret = input_register_device(mykbd_inputdev);

//Allocate URB
mykbd_urb = usb_alloc_urb(0,GFP_KERNEL);

//Get endpoint maximum packet size
mykbd_size = endpoint->wMaxPacketSize;

mykbd_buf = usb_alloc_coherent(mykbd_dev, mykbd_size, GFP_ATOMIC, &mykbd_dma);

// Get receive interrupt pipe
pipe = usb_rcvintpipe(mykbd_dev, endpoint->bEndpointAddress);

// Fill URB
usb_fill_int_urb(mykbd_urb, mykbd_dev, pipe, mykbd_buf, mykbd_size, mykbd_func, 0, endpoint->bInterval);

mykbd_urb->transfer_dma = mykbd_dma;
mykbd_urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
// Submit URB for data transfer
usb_submit_urb(mykbd_urb, GFP_KERNEL);

return 0;
}

// Disconnect function: called when device is removed or driver is unloaded
void my_kbd_disconnect(struct usb_interface *intf)
{
struct usb_device *mykbd_dev = interface_to_usbdev(intf);

// Cancel URB transfer
usb_kill_urb(mykbd_urb);
// Release URB
usb_free_urb(mykbd_urb);
// Release consistent memory
usb_free_coherent(mykbd_dev, mykbd_size, mykbd_buf, mykbd_dma);
// Unregister input device
input_unregister_device(mykbd_inputdev);
// Release input device
input_free_device(mykbd_inputdev);
}

// USB driver structure definition
struct usb_driver my_kbd_driver = {
.name = "my_kbd", // Driver name (visible under /sys/bus/usb/drivers/)
.probe = my_kbd_probe, // Callback when device matches
.disconnect = my_kbd_disconnect, // Callback when device disconnects
.id_table = my_kbd_id_table, // Device ID table (to be filled)
};

// Module initialization function
static int my_kdb_init(void)
{
int ret;
// Register USB driver to kernel subsystem
ret = usb_register(&my_kbd_driver);
if (ret < 0) {
printk(KERN_ERR "USB驱动注册失败: %d\n", ret);
return ret;
}
printk(KERN_INFO "USB键盘驱动已加载\n");
return 0;
}

// Module exit function
static void my_kdb_exit(void)
{
// Unregister driver (triggers disconnect callback for all devices)
usb_deregister(&my_kbd_driver);
printk(KERN_INFO "USB键盘驱动已卸载\n");
}

// Module declaration
module_init(my_kdb_init); // Specify initialization function
module_exit(my_kdb_exit); // Specify exit function
MODULE_LICENSE("GPL"); // Must declare the GPL license

UDC and Gadget Driver

USB gadget refers to the driver framework on the USB device side, primarily used to configure a USB-capable device as a slave device to communicate with the host.

Taking a smartphone as an example, when the phone is connected to a computer via a USB cable, the phone acts as a USB Gadget. In the following experiments, we will replace the phone with a development board to act as a USB Gadget.

According to the USB driver development framework diagram below, the Gadget framework provides a set of standardized API interfaces (Gadget Function API), which are implemented at the bottom layer by the USB Device Controller (UDC) driver.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────────────┐      ┌─────────────────────┐
│ USB设备驱动 │ │ Gadget Function驱动 │
└───────────┬─────────┘ └───────────┬─────────┘
│ │
┌───────────▼─────────┐ ┌───────────▼─────────┐
│ USB核心层 │ │ Gadget Function API │
└───────────┬─────────┘ └───────────┬─────────┘
│ │
┌───────────▼─────────┐ ┌───────────▼─────────┐
│ USB主机控制器驱动 │ │ UDC驱动 │
└───────────┬─────────┘ └───────────┬─────────┘
└──────────────┬──────────────┘

┌────────▼────────┐
│ USB总线 │
└─────────────────┘

It is worth noting that different UDCs (USB Device Controllers) may require different drivers to be adapted, and even different hardware boards based on the same UDC may require modifications to the driver. The USB Gadget module in the kernel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
DeviceDrivers--->
[*]USB support--->
[*] USB Gadget Support--->
...
USBGadget Drivers (USB functions configurable through configfs)
[ ] Generic serial bulk in/out
[*] Abstract Control Model (CDC ACM)
[ ] Object Exchange Model (CDC OBEX)
[ ] Network Control Model (CDC NCM)
[ ] Ethernet Control Model (CDC ECM)
[ ] Ethernet Control Model (CDC ECM) subset
[*] RNDIS
[ ] Ethernet Emulation Model (EEM)
[*] Mass storage
[ ] Loopback and sourcesink function (for testing)
[*] Function filesystem (FunctionFS)
[*] MTP gadget
[*]
PTP gadget
[*] Accessory gadget
[*]
Audio Source gadget
[*] Uevent notification of Gadget state
[ ] Audio Class 1.0
[ ] Audio Class 2.0
[*] MIDI function
[ ] HID function
[ ] USB Webcam function
[ ] Printer function

Configure the iTOP-RK3568 development board as a USB network card device

  1. Enable the RNDIS driver (if compiled as a module, compiling the kernel alone will not generate .ko files; to avoid cumbersome mounting steps)
  2. Next, select some network protocol configurations

As shown in the figure:

USB Gadget Support
USB Gadget Support

Next, find USB Gadget precomposed configurations and select ‘with CDC Ethernet support’, as shown in the figure below

USB Gadget precomposed configurations
USB Gadget precomposed configurations

After configuration is complete, recompile the kernel source code, then reflash the kernel image. After the development board completes the reboot process, executeifconfig -athe command to list network configuration interfaces, and you will be able to observe the usb0 network interface.

Host and PHY driver

Host and PHY driver
Host and PHY driver

In the Linux kernel source code, USB host and PHY drivers are loaded by default. The USB PHY module is located at the following path in the kernel.

1
2
3
4
5
Device Drivers--->
PHY Subsystem--->
<*> Rockchip INNO USB2PHY Driver
<*> Rockchip TYPEC PHY Driver
<*> Rockchip INNO USB 3.0 PHY Driver
  • The USB 2.0 PHY uses Innosilicon IP, so you should select “Rockchip INNO USB2PHY Driver”.
  • The USB 3.0 PHY uses Type-C, so you should select “Rockchip TYPEC PHY Driver”.
  • The USB 3.0 PHY uses Innosilicon USB 3.0 PHY, so you should select “Rockchip INNO USB 3.0 PHY Driver”.

The configuration of the USB Host module is located at the following path in the kernel.

1
2
3
4
5
6
7
8
9
10
11
12
Device Drivers--->
-*- Support for Host-side USB
[*] USB support--->
<*> xHCI HCD (USB 3.0) support
-*- Generic xHCI driver for a platform device
<*> EHCI HCD (USB 2.0) support
[ ] Root Hub Transaction Translators
[*] Improved Transaction Translator scheduling
<*> Generic EHCI driver for a platform device
<*> OHCI HCD (USB 1.1) support
< > OHCI support for PCI-bus USB controllers
<*> Generic OHCI driver for a platform device

You must select the USB Support option to enable the USB module and perform further configuration. To support USB Host, first select<*>Supportfor Host-side USBoption, and then the following Host-related configurations will appear, where:

  • For USB Host 1.1, select the OHCI Driver configuration.
  • For USB Host 2.0, select the EHCI Driver configuration.
  • For USB Host 3.0, select the xHCI Driver configuration.

After confirming that the above modules are selected, proceed to configure the device tree.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// rk3568.dtsi

/* USB 3.0 OTG/SATA Combo PHY_0 */
combphy0_us: phy@fe820000 {
compatible = "rockchip,rk3568-naneng-combphy";
reg = <0x0 0xfe820000 0x0 0x100>;
#phy-cells = <1>;
clocks = <&pmucru CLK_PCIEPHY0_REF>, <&cru PCLK_PIPEPHY0>,
<&cru PCLK_PIPE>;
clock-names = "refclk", "apbclk", "pipe_clk";
assigned-clocks = <&pmucru CLK_PCIEPHY0_REF>;
assigned-clock-rates = <100000000>;
resets = <&cru SRST_P_PIPEPHY0>, <&cru SRST_PIPEPHY0>;
reset-names = "combphy-apb", "combphy";
rockchip,pipe-grf = <&pipegrf>;
rockchip,pipe-phy-grf = <&pipe_phy_grf0>;
status = "disabled";
};
/* USB 3.0 Host/SATA/QSGMII Combo PHY_1 */
combphy1_usq: phy@fe830000 {
compatible = "rockchip,rk3568-naneng-combphy";
reg = <0x0 0xfe830000 0x0 0x100>;
#phy-cells = <1>;
clocks = <&pmucru CLK_PCIEPHY1_REF>, <&cru PCLK_PIPEPHY1>,
<&cru PCLK_PIPE>;
clock-names = "refclk", "apbclk", "pipe_clk";
assigned-clocks = <&pmucru CLK_PCIEPHY1_REF>;
assigned-clock-rates = <100000000>;
resets = <&cru SRST_P_PIPEPHY1>, <&cru SRST_PIPEPHY1>;
reset-names = "combphy-apb", "combphy";
rockchip,pipe-grf = <&pipegrf>;
rockchip,pipe-phy-grf = <&pipe_phy_grf1>;
status = "disabled";
};

combphy2_psq: phy@fe840000 {
compatible = "rockchip,rk3568-naneng-combphy";
reg = <0x0 0xfe840000 0x0 0x100>;
#phy-cells = <1>;
clocks = <&pmucru CLK_PCIEPHY2_REF>, <&cru PCLK_PIPEPHY2>,
<&cru PCLK_PIPE>;
clock-names = "refclk", "apbclk", "pipe_clk";
assigned-clocks = <&pmucru CLK_PCIEPHY2_REF>;
assigned-clock-rates = <100000000>;
resets = <&cru SRST_P_PIPEPHY2>, <&cru SRST_PIPEPHY2>;
reset-names = "combphy-apb", "combphy";
rockchip,pipe-grf = <&pipegrf>;
rockchip,pipe-phy-grf = <&pipe_phy_grf2>;
status = "disabled";
};

/* USB 3.0 OTG controller */
usbdrd30: usbdrd {
compatible = "rockchip,rk3568-dwc3", "rockchip,rk3399-dwc3";
clocks = <&cru CLK_USB3OTG0_REF>, <&cru CLK_USB3OTG0_SUSPEND>,
<&cru ACLK_USB3OTG0>, <&cru PCLK_PIPE>;
clock-names = "ref_clk", "suspend_clk",
"bus_clk", "pipe_clk";
#address-cells = <2>;
#size-cells = <2>;
ranges;
status = "disabled";

usbdrd_dwc3: dwc3@fcc00000 {
compatible = "snps,dwc3";
reg = <0x0 0xfcc00000 0x0 0x400000>;
interrupts = <GIC_SPI 169 IRQ_TYPE_LEVEL_HIGH>;
dr_mode = "otg";
phys = <&u2phy0_otg>, <&combphy0_us PHY_TYPE_USB3>;
phy-names = "usb2-phy", "usb3-phy";
phy_type = "utmi_wide";
power-domains = <&power RK3568_PD_PIPE>;
resets = <&cru SRST_USB3OTG0>;
reset-names = "usb3-otg";
snps,dis_enblslpm_quirk;
snps,dis-u1-entry-quirk;
snps,dis-u2-entry-quirk;
snps,dis-u2-freeclk-exists-quirk;
snps,dis-del-phy-power-chg-quirk;
snps,dis-tx-ipgap-linecheck-quirk;
snps,dis_rxdet_inp3_quirk;
snps,parkmode-disable-hs-quirk;
snps,parkmode-disable-ss-quirk;
quirk-skip-phy-init;
status = "disabled";
};
};

/* USB 3.0 Host_1 controller */
usbhost30: usbhost {
compatible = "rockchip,rk3568-dwc3", "rockchip,rk3399-dwc3";
clocks = <&cru CLK_USB3OTG1_REF>, <&cru CLK_USB3OTG1_SUSPEND>,
<&cru ACLK_USB3OTG1>, <&cru PCLK_PIPE>;
clock-names = "ref_clk", "suspend_clk",
"bus_clk", "pipe_clk";
#address-cells = <2>;
#size-cells = <2>;
ranges;
status = "disabled";

usbhost_dwc3: dwc3@fd000000 {
compatible = "snps,dwc3";
reg = <0x0 0xfd000000 0x0 0x400000>;
interrupts = <GIC_SPI 170 IRQ_TYPE_LEVEL_HIGH>;
dr_mode = "host";
phys = <&u2phy0_host>, <&combphy1_usq PHY_TYPE_USB3>;
phy-names = "usb2-phy", "usb3-phy";
phy_type = "utmi_wide";
power-domains = <&power RK3568_PD_PIPE>;
resets = <&cru SRST_USB3OTG1>;
reset-names = "usb3-host";
snps,dis_enblslpm_quirk;
snps,dis-u2-freeclk-exists-quirk;
snps,dis-del-phy-power-chg-quirk;
snps,dis-tx-ipgap-linecheck-quirk;
snps,dis_rxdet_inp3_quirk;
snps,parkmode-disable-hs-quirk;
snps,parkmode-disable-ss-quirk;
status = "disabled";
};
};

/* USB 2.0 Host_2 EHCI controller for high speed */
usb_host0_ehci: usb@fd800000 {
compatible = "generic-ehci";
reg = <0x0 0xfd800000 0x0 0x40000>;
interrupts = <GIC_SPI 130 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru HCLK_USB2HOST0>, <&cru HCLK_USB2HOST0_ARB>,
<&cru PCLK_USB>, <&usb2phy1>;
clock-names = "usbhost", "arbiter", "pclk", "utmi";
phys = <&u2phy1_otg>;
phy-names = "usb2-phy";
status = "disabled";
};
/* USB 2.0 Host_2 OHCI controller for full/low speed */
usb_host0_ohci: usb@fd840000 {
compatible = "generic-ohci";
reg = <0x0 0xfd840000 0x0 0x40000>;
interrupts = <GIC_SPI 131 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru HCLK_USB2HOST0>, <&cru HCLK_USB2HOST0_ARB>,
<&cru PCLK_USB>, <&usb2phy1>;
clock-names = "usbhost", "arbiter", "pclk", "utmi";
phys = <&u2phy1_otg>;
phy-names = "usb2-phy";
status = "disabled";
};

usb_host1_ehci: usb@fd880000 {
compatible = "generic-ehci";
reg = <0x0 0xfd880000 0x0 0x40000>;
interrupts = <GIC_SPI 133 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru HCLK_USB2HOST1>, <&cru HCLK_USB2HOST1_ARB>,
<&cru PCLK_USB>, <&usb2phy1>;
clock-names = "usbhost", "arbiter", "pclk", "utmi";
phys = <&u2phy1_host>;
phy-names = "usb2-phy";
status = "disabled";
};

usb_host1_ohci: usb@fd8c0000 {
compatible = "generic-ohci";
reg = <0x0 0xfd8c0000 0x0 0x40000>;
interrupts = <GIC_SPI 134 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru HCLK_USB2HOST1>, <&cru HCLK_USB2HOST1_ARB>,
<&cru PCLK_USB>, <&usb2phy1>;
clock-names = "usbhost", "arbiter", "pclk", "utmi";
phys = <&u2phy1_host>;
phy-names = "usb2-phy";
status = "disabled";
};

To configure the USB 3.0 OTG function, the device tree configuration is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// topeet-rk3568-linux.dtsi
&usbdrd_dwc3 {
dr_mode = "otg";
extcon = <&usb2phy0>;
status = "okay";
};

&usbdrd30 {
status = "okay";
};

// topeet-rk3568-linux.dts
//USB and sata0 share the combphy0_us PHY node.
&combphy0_us {
status = "okay";
};

HOST1 is configured as a USB 3.0 Host function, the device tree configuration is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
// topeet-rk3568-linux.dtsi
&usbhost_dwc3 {
status = "okay";
};

&usbhost30 {
status = "okay";
};

// topeet-rk3568-linux.dts
&combphy1_usq {
status = "okay";
};

HOST1 is configured as a USB 2.0 Host, the device tree configuration is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
&usbhost30 {
status = "okay";
};

&usbhost_dwc3 {
phys = <&u2phy0_host>;
phy-names = "usb2-phy";
maximum-speed = "high-speed";
status = "okay";
};

&combphy1_usq {
rockchip,dis-u3otg1-port;
/*If HOST1, SATA1, and QSGMII do not use combphy1_usq, then this is disabled. */
status = "okay";
};

&usb2phy0 {
status = "okay";
};

&u2phy0_host {
phy-supply = <&vcc5v0_host>;
status = "okay";
}

For more detailed information, please refer to Rockchip’s official documentation.rk356x_linux/doc/Common/USB/Rockchip_Developer_Guide_USB_CN.pdf

4G Module Porting

The schematic diagram of the iTOP-RK3568 baseboard, 4G module interface section.
The schematic diagram of the iTOP-RK3568 baseboard, 4G module interface section.

The U58 interface in the above figure is compatible with both 4G and 5G modules. Here, it connects to the CPU via a USB interface, with the purpose ofThe CPU mounts the EM05-CE (4G) module to the system via the USB interface, emulating the EM05-CE as a network device and 4 USB serial port devices.

  • The serial port devices are used for performing initialization operations, error detection, reading positioning information, etc., on the module.
  • The network device serves to use the EM05-CE as a general network card device for upper-layer socket calls.

For porting 4G or 5G modules, please refer to Quectel’s technical documentation.

Kernel Driver Porting

First, modify the driver to support the PID and VID of the EM05-CE module. Opendrivers/usb/serial/option.cfile,struct usb_device_id option_ids[]add the following code to the array:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static const struct usb_device_id option_ids[] = {
{ USB_DEVICE(0x2C7C, 0x0125) }, // add
{ USB_DEVICE(0x1286, 0x4e3c) },
{ USB_DEVICE_AND_INTERFACE_INFO(0x2c7c, 0x0900, 0xff, 0x00, 0x00) },
{ USB_DEVICE(OPTION_VENDOR_ID, OPTION_PRODUCT_COLT) },
{ USB_DEVICE(OPTION_VENDOR_ID, OPTION_PRODUCT_RICOLA) },
{ USB_DEVICE(OPTION_VENDOR_ID, OPTION_PRODUCT_RICOLA_LIGHT) },
{ USB_DEVICE(OPTION_VENDOR_ID, OPTION_PRODUCT_RICOLA_QUAD) },
...
{ USB_DEVICE_AND_INTERFACE_INFO(OPPO_VENDOR_ID, OPPO_PRODUCT_R11, 0xff, 0xff, 0x30) },
{ USB_DEVICE_AND_INTERFACE_INFO(SIERRA_VENDOR_ID, SIERRA_PRODUCT_EM9191, 0xff, 0xff, 0x30) },
{ USB_DEVICE_AND_INTERFACE_INFO(SIERRA_VENDOR_ID, SIERRA_PRODUCT_EM9191, 0xff, 0, 0) },
{ } /* Terminating entry */
};
MODULE_DEVICE_TABLE(usb, option_ids);

Modifydrivers/usb/serial/usb_wwan.cfile, add the zero-packet mechanism.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static struct urb *usb_wwan_setup_urb(struct usb_serial_port *port,
int endpoint,
int dir, void *ctx, char *buf, int len,
void (*callback) (struct urb *))
{
struct usb_serial *serial = port->serial;
struct usb_wwan_intf_private *intfdata = usb_get_serial_data(serial);
struct urb *urb;

urb = usb_alloc_urb(0, GFP_KERNEL); /* No ISO */
if (!urb)
return NULL;

usb_fill_bulk_urb(urb, serial->dev,
usb_sndbulkpipe(serial->dev, endpoint) | dir,
buf, len, callback, ctx);

#if 1 //Added by Quectel for zero packet
if (dir == USB_DIR_OUT) {
struct usb_device_descriptor *desc = &serial->dev->descriptor;
if (desc->idVendor == cpu_to_le16(0x2C7C))
urb->transfer_flags |= URB_ZERO_PACKET;
}
#endif

if (intfdata->use_zlp && dir == USB_DIR_OUT)
urb->transfer_flags |= URB_ZERO_PACKET;

return urb;
}

Then add the restart sleep mechanism, opendrivers/usb/serial/option.cfile, and add the following statement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static struct usb_serial_driver option_1port_device = {
.driver = {
.owner = THIS_MODULE,
.name = "option1",
},
.description = "GSM modem (1-port)",
.id_table = option_ids,
.num_ports = 1,
.probe = option_probe,
.open = usb_wwan_open,
.close = usb_wwan_close,
.dtr_rts = usb_wwan_dtr_rts,
.write = usb_wwan_write,
.write_room = usb_wwan_write_room,
.chars_in_buffer = usb_wwan_chars_in_buffer,
.tiocmget = usb_wwan_tiocmget,
.tiocmset = usb_wwan_tiocmset,
.get_serial = usb_wwan_get_serial_info,
.set_serial = usb_wwan_set_serial_info,
.attach = option_attach,
.release = option_release,
.port_probe = usb_wwan_port_probe,
.port_remove = usb_wwan_port_remove,
.read_int_callback = option_instat_callback,
#ifdef CONFIG_PM
.suspend = usb_wwan_suspend,
.resume = usb_wwan_resume,
#if 1 //Added by Quectel
.reset_resume = usb_wwan_resume,
#endif

#endif
};

For modules accessed via USB interface, integrate the USB driver into the Linux kernel. We need to configure the kernel to select the USB-to-serial driver supporting GSM and CDMA modules. Select the driver at the following path. This driver virtualizes in the kernel/dev/ttyUSB0/dev/ttyUSB1/dev/ttyUSB2/dev/ttyUSB3these serial ports, which serve as error diagnosis, GPS information interface, and module communication interface respectively, as shown below.

1
2
3
4
--> Device Drivers
--> USB support (USB_SUPPORT [=y])
--> USB Serial Converter support (USB_SERIAL [=y])
--> USB driver for GSM and CDMA modems

Dial-up Script Porting

First, configure the kernel to support PPP dial-up. In menuconfig, select the following options as shown below.

1
2
3
4
5
6
7
8
9
10
11
12
13
--> Device Drivers
--> Network device support
<*>PPP (point-to-point protocol) support
<*>PPP BSD-Compress compression
<*>PPP Deflate compression
<*>PPP filtering
<*>PPP MPPE compression (encryption)
<*>PPP multilink support
<*>PPP over Ethernet
<*>PPP on L2TP Access Concentrator
<*>PPP on PPTP Network Server
<*>PPP support for async serial ports
<*>PPP support for sync tty ports

After connecting the hardware, power on the development board. Switch to ECM dial-up and enter the commandcat /dev/ttyUSB2 & echo -e “AT+QCFG=\”usbnet\”,1\r\n” > /dev/ttyUSB2, then restart the development board.

After the development board restarts, it will automatically dial up. Then enter the “ifconfig” command in the debug serial terminal to see the usb0 node, and then enterping www.baidu.comcommand to check if the network is reachable via ping.

In addition to the ECM dial-up mentioned above, we can also use the quectel-CM tool for dial-up

After compilation, the quectel-CM executable is generated. We copy this program to the development board for the following tests. Since ECM dial-up was used in the previous operation, before using quectel-CM dial-up, we need to entercat /dev/ttyUSB2 & echo -e “AT+QCFG=\”usbnet\”,0\r\n” > /dev/ttyUSB2command to switch to RmNET dial-up. Then run the quectel-CM executable, as shown below:

After the dial-up script is executed, enter in the serial terminalifconfigcommand to see an additional wwan0 node.