Skip to content

Refactoring of libusbhost

Ondřej Hlavatý edited this page Jan 17, 2018 · 3 revisions

As a part of out project, we decided to do major refactoring of libusbhost. We introduced several changes, that better suit writing drivers of usb host controllers. It was a lot of unneeded work, but not doing it would result in code duplication.

Refactoring of bus - device - endpoint chain

Previously, the only entity with "virtual" methods was the HC driver, which contained both HC driver methods (start, stop, dev_add, dev_remove), and HC methods (schedule). Also, the bus behavior was pretty much hardcoded to libusbhost, because all previous HC drivers (UHCI, OHCI, EHCI) were written in the USB 2 scheme. XHCI abstracts the bus more, and therefore a lot of code must have been shortcut. To avoid merely wrapping code blocks in conditions, we decided to take a different leap.

Every HC driver now has an instance of bus_t, with operation table bus_ops_t filled. This operation table serves as an interface between host controller and the reset of the system - bus operations include device enumeration and removal, endpoint management and transfer batch management. This adds a great flexibility for the user of libusbhost. To avoid redundant code in older HC drivers, usb2_bus contains the implementation in a USB 2-way, effectively shielding most of the changes from them.

The overall view on the current architecture of the USB stack can be seen on the following diagram:

Also, a usbhost-private structure was previously kept by a function node, which required host controller drivers to do a lookup every time a request for device was made. This was changed to allow drivers to specify size of their own "inherited" device_t structure, which is now passed to every device-related method.

In a similar way, drivers "inherit" endpoint_t with their own endpoints, which can contain HC-specific information.

When we have these structures, we can use them to pass iformation to functions instead of having tons of arguments to describe an entity. Also, passing these structures makes it future-proof.

USB transfer batch refactoring

Previously, usb_transfer_batch was a structure which was created inside libusbhost for every transfer. Every single driver then allocated their own structure for keeping transfer-related information - at least the pointers to DMA buffers. Now, transfer batches can be also inherited and are created by the bus implementation.

Together with the refactored bus, the in-memory structures common to all HC drivers can be seen on following diagram, which is an example of how XHCI uses it. Older HC drivers might not need some features, thus do not extend some structures.

Also, we reduced the confusion around completion codes and callbacks. Now, there is just one callback, which is called after the transfer is completed, and is given the error code and number of transfered bytes.

DDF interface restructuralization

There were originally two interfaces related to USB Host Controllers: usbhc_iface and usb_iface. Sadly, the usbhc iface was unused and the usb iface contained the host controller interface for usb functions. We first wiped out the usbhc_iface to ensure it really wasn't used anywhere, then moved HC functionality to usbhc iface. USB iface now contains functions that are useful for USB device drivers.

Endpoint description

USB 3 introduces new SuperSpeed companion descriptor. We would need to add properties from it to the whole stack, which was already cluttered with loads of argumets. Instead, we modified the interface to send the endpoint descriptor (together with the companion descriptor) directly, and moved the responsibility of parsing it to the HC driver, which is extensible.

DMA buffers

There was a common scheme of allocating a buffer in DMA-accessible memory, so we decided to create a thin abstraction for it. Previously, the host controller drivers were "lazy" and remembered only the virtual address of the buffer, and used a syscall to determine the physical address. This is not necessary, as the physical address is a part of the output from dmamem_map_anonymous. The structure dma_buffer_t contains both addresses, eliminating the need for calling the kernel pretty often.

Also, DMA buffers we written with a particular optimization in mind: memory pools. The implementation can be fairly simply extended to build a userspace allocator over DMA-accessible memory. In previous HCs, there are a lot of situations where significantly less memory than PAGE_SIZE is needed, yet whole page from the precious address space must be allocated.