Lifecycle of a USB request
Glossary
- HCI — Host Controller Interface: A host controller interface driver is responsible for queueing USB requests to hardware, and managing the state of connected devices while operating as a USB host.
- DCI — Device controller interface: A device controller interface is responsible for queueing USB requests to a USB host that the device is connected to.
Allocation
The first step in a USB request’s lifecycle is allocation. USB requests contain data from all of the drivers in the request stack in a single allocation. Each driver that is upstream of a USB device driver should provide a GetRequestSize method — which returns the size it needs to contain its local request context. When a USB device driver allocates a request, it should invoke this method to determine the size of the parent’s request context.
Note: It is important to ensure that the lifetime of a request does not exceed the lifetime of your driver. If your driver is released with outstanding requests, you will encounter a use-after-free scenario when the parent tries to send the request back to you by invoking your callback. Drivers should not reply to unbind until all outstanding requests have been returned.
C example {#c-example}
size_t parent_req_size = usb_get_request_size(&usb);
usb_request_t* request;
usb_request_alloc(&request, transfer_length, endpoint_addr,
parent_req_size+sizeof(your_context_struct_t));
usb_request_complete_callback_t complete = {
.callback = usb_request_complete,
.ctx = your_context_pointer,
};
your_context_pointer.completion = complete;
usb_request_queue(&usb, request, &complete);
...
void usb_request_complete(void* cookie, usb_request_t* request) {
your_context_struct_t data;
// memcpy is needed to ensure alignment
memcpy(&data, cookie, sizeof(data));
// Do something here to process the response
// ...
// Requeue the request
usb_request_queue(&data.usb, request, &data.completion);
}
C++ example
parent_req_size = usb.GetRequestSize();
std::optional<usb::Request<void>> req;
status = usb::Request<void>::Alloc(&req, transfer_length,
endpoint_addr, parent_req_size);
usb_request_complete_callback_t complete = {
.callback =
[](void* ctx, usb_request_t* request) {
static_cast<YourDeviceClass*>(ctx)->YourHandlerFunction(request);
},
.ctx = this,
};
usb.RequestQueue(req->take(), &complete);
C++ example (with lambdas)
size_t parent_size = usb_.GetRequestSize();
using Request = usb::CallbackRequest<sizeof(std::max_align_t) * 4>;
std::optional<Request> request;
Request::Alloc(&request, max_packet_size, endpoint_address,
parent_size, [=](Request request) {
// Do some processing here.
// ...
// Re-queue the request
Request::Queue(std::move(request), usb_client_);
});
Submission
You can submit requests using the
RequestQueue method,
or — in the case of CallbackRequests (as seen here), using
Request::Queue
or simply request.Queue(client)
. In all cases, ownership of
the USB request is transferred to the parent driver (usually usb-device
).
The typical lifecycle of a USB request (from a device driver to either a host controller or device controller is as follows):
- The USB device driver queues the request
- The
usb-device
core driver receives the request, and now owns the request object. - The
usb-device
core driver injects its own callback (if the direct flag is not set), or passes through the request (if the direct flag is set) to the HCI or DCI driver. - The HCI or DCI driver now owns the request. The HCI or DCI driver submits this request to hardware.
- The request completes. When this happens, if the direct flag was set, the callback in the device driver is invoked, and the device driver now owns the request. If the direct flag is not set, the usb-device (core) driver now owns the request.
- If the core driver owns the request; it is added to a queue for dispatch by another thread.
- The core driver eventually invokes the callback, and the request is now owned by the device driver. The device driver can now re-submit the request.
Cancellation
Requests may be cancelled by invoking
CancelAll. When
CancelAll completes, all
requests are owned by the caller. Drivers implementing a
CancelAll function (such
as the usb-device core driver and any HCI/DCI drivers) are responsible for
transferring ownership to their children with a ZX_ERR_CANCELLED
status code.
Implementation notes for writers of HCI, DCI, or filter drivers
Implementing GetRequestSize
The value returned by
GetRequestSize should
equal the value of your parent’s
GetRequestSize + the size
of your request context, including any padding that would be necessary to
ensure proper alignment of your data structures (if applicable). If you are
implementing an HCI or DCI driver, you must include sizeof(usb_request_t)
in
your size calculation in addition to any other data structures that you are
storing. usb_request_t
has no special alignment requirements, so it is not
necessary to add padding for that structure.
Implementing RequestQueue
Implementors of
RequestQueue temporarily
assumes ownership of the USB request from its client driver. As an implementor
of RequestQueue, you are
allowed to access all fields of the usb_request_t
, as well as any private
data that you have appended to the usb_request_t
structure (by requesting
additional space through
GetRequestSize), but you
are not allowed to modify any data outside of your private area, which starts
at parent_req_size
bytes (past the end of usb_request_t
).
Example USB request stack (HCI)
xHCI (host controller) -> usb-bus
-> usb-device
(core USB device driver) ->
usb-mass-storage
Example USB request stack (DCI)
dwc2
(device-side controller) -> usb-peripheral
(peripheral core driver) ->
usb-function
(core function driver) -> cdc-eth-function
(ethernet
peripheral mode driver)