Performing I/O at endpoints
To communicate with a USB device, you first need to locate the appropriate endpoint for your usecase by parsing the device. Once you locate your desired endpoint, you need to execute the following steps to send or retrieve data:
Allocate a URB (USB Request Block) to store information on your request.
Allocate enough shared memory to store any sent or received data.
Open a pipe to or from the endpoint.
Set up your URB with the pipe in one of the I/O modes.
Submit the URB, pipe, shared memory, and a callback function to
io-usb
for processing.
This section compares these steps between Linux and QNX.
Allocating and preparing URB and buffer memory blocks.
Allocating buffer memory in Linux
In Linux, requests can either be performed synchronously via a macro (typically used for reading), or asynchronously using a URB (typically write) in the kernel. We only need to allocate a URB if we intend to use the asynchronous method.
URBs use their own methods for allocation:
struct urb *usb_alloc_urb(int isoframes, int mem_flags)
void usb_free_urb(struct urb *urb) //URBs are automatically deallocated upon completion
Buffers can be allocated as normal - either dynamically or statically.
#define MAX_PKT_SIZE 40 //Example Packet Size
static unsigned char buffer_style_a[MAX_PKT_SIZE];
static ssize_t example_read_func(...){
int packet_size;
unsigned char * buffer_style_b;
// determine packet size dynamically here
buffer_style_b = (unsigned char *)calloc(sizeof(1, packet_size));
}
Allocating buffer memory and URBs in QNX
In QNX, I/O is only performed asynchronously via messaging. Additionally, there's only one function for I/O. Information is passed to the USB stack via a URB data structure. Because there are data structures shared between the stack and your driver, they both need to be declared in shared memory. The libusbdi library provides functions for doing so; the usbd_alloc*() and usbd_free*() functions. There are two sets available, where usbd_alloc_urb() and usbd_free_urb() are specifically for allocating USB request blocks to be set for I/O later. For the actual buffer data, usbd_alloc() and usbd_free() can allocate and free variable length shared memory blocks. These are shown below:
/* URB */
struct usbd_urb *usbd_alloc_urb( struct usbd_urb *link );
struct usbd_urb *usbd_free_urb( struct usbd_urb *urb );
/* Generic Memory */
void *usbd_alloc( size_t size );
void usbd_free( void* ptr );
While usbd_alloc() and usbd_free() work like malloc() and free(), the URB-specific functions have reserved return types and parameters for chained requests in the future. These aren't currently implemented.
When freeing shared memory via usbd_free() on older QNX versions, you need to provide the physical address of the memory (not the pointer returned above). You can get this using usbd_mphys()
/* myphys function */
paddr_t usbd_mphys( const void *ptr );
/* Example of use with alloc and free */
void* buffer = usbd_alloc(64);
// QNX 6.5
usbd_free(usbd_mphys(buffer)); //safe to call on NULL
// QNX 8.0
usbd_free(buffer);
An example of allocating memory for a given endpoint is shown below:
/* Assuming we have a pointer to an endpoint descriptor as follows */
usbd_endpoint_desriptor_t* endp_desc;
// Get endpoint descriptor (see prior section)
//Allocate a URB
struct usbd_urb* urb_ptr = usbd_alloc_urb(NULL);
//Allocate a shared buffer to store the size of the endpoint's data
void* data_ptr = usbd_alloc(endp_desc->wMaxPacketSize);
Opening a pipe at an endpoint
Opening a pipe in Linux
Pipes can be opened in Linux using their corresponding methods. Typically this is nested in the I/O or URB creation call. Pipes are also set to the data transfer type.
//Examples of creating a pipe
//A device reference is needed (usb_device*)
//Format is usb_ABpipe(), where A is the direction (snd[send]/rcv[receive])
//and B is the type (ctrl[control], bulk[bulk], int[interrupt], iso[isochronous])
//ex.
usb_sndbulkpipe(device, endpoint);
usb_rcvintpipe(device, endpoint);
Opening a pipe in QNX
The libusbdi library provides functions for opening pipes at endpoints. To open a pipe, you need to provide a device handle, a device or endpoint descriptor, and a handle in which to store the pipe's address.
int usbd_open_pipe( struct usbd_device *device,
usbd_descriptors_t *desc,
struct usbd_pipe **pipe );
Closing a pipe is easier, as it only requires the pipe handle:
int usbd_close_pipe( struct usbd_pipe *pipe );
Outside of open and close, there are a few convenience functions for use with a pipe. One returns the associated endpoint, and another for the associated device (whichever is applicable).
/* Returns the endpoint number */
uint32_t usbd_pipe_endpoint( struct usbd_pipe *pipe );
/* Returns the associated device handle */
struct usbd_device*
usbd_pipe_device( struct usbd_pipe *pipe );
Finally, there are some pipe operations used for cancelling URBs or aborting in-progress ones:
/* Clears any stalled URBs and resets them */
int usbd_reset_pipe( struct usbd_pipe *pipe );
/* Aborts all pending operations (e.g. used to handle errors) */
int usbd_abort_pipe( struct usbd_pipe *pipe );
Unlike Linux, these pipes are not typed.
Preparing and submitting a URB for I/O
Setting up a URB or receiving a message in Linux
On Linux, you only need to populate a URB when not using a submission macro, that is, when performing single time data transfers (typically done when reading).
Creating a URB and submitting for I/O in Linux
This process follows the same 2 steps as on QNX:
Set up a URB via a URB setup function.
Submit the URB via a submission function.
The detailed steps are as follows:
URB setup functions take the form usb_fill_TYPE_urb(...)
, where TYPE
is the type of transfer (e.g., control
, bulk
, int
).
These typically take the device pointer, the pipe (returned from
usb_sndTYPEpipe
or usb_rcvTYPEpipe
), a buffer for transfer and
the desired transfer length, and a completion callback.
Options such as direction and type of transfer are implicitly defined by the types of functions you call. On QNX, these are defined as variables when creating a URB. This means that porting a Linux URB setup to QNX is much easier than the inverse, as in QNX you don't need to worry about calling the correct version of a function.
This also means code can be simplified on QNX to a changing of variables instead of changing which function to call.
Here's a bulk example in Linux:
//Assuming we have a struct usb_device*
struct usb_device* device;
//Allocate a URB
struct urb* bulk_urb_ptr = usb_alloc_urb(0,0);
//Allocate a buffer
#define BUF_LEN 64;
unsigned char * buf = calloc(BUF_LEN, sizeof(unsigned char));
// We can fill a bulk URB using the corresponding function
usb_fill_bulk_urb(
bulk_urb_ptr,
device->udev,
usb_sndbulkpipe(device->udev, device->bulk_out_endpointAddr),
buffer, //Buffer
BUF_LEN, //Buffer length
callback, //Assuming this is declared elsewhere
device //Context, set as the device
);
//We now submit our URB to IO. This is analagous to usbd_io on QNX
usb_submit_urb(bulk_urb_ptr); //asynchronous, callback will be called on completion
Using a single time transfer macro for I/O in Linux
Linux's host side API also provides some macros that simplify one-time transfers. These return synchronously from this program's point of view, but it undergoes the above process 'under the hood'.
//Synchronous version of the above fill_bulk and submit_urb
//No QNX Equivilent.
int length_written;
usb_bulk_msg(
device,
usb_sndbulkpipe(device->udev, device->bulk_out_endpointAddr),
buffer,
BUF_LEN,
&length_written,
0 //Timeout, 0 for forever
);
Setting up and Submitting a URB for read or write in QNX
On QNX, both read and write use the same function, which requires a
populated URB. To populate the URB you've set up above, you'll need to
know the endpoint
type, which can be
queried from the bmAttributes
bitmap in an endpoint
descriptor. Then, use
the appropriate setup function (one for each type of transfer):
usbd_setup_bulk() and usbd_setup_interrupt()
int usbd_setup_bulk( struct usbd_urb *urb,
uint32_t flags,
void *addr,
uint32_t len );
int usbd_setup_interrupt( struct usbd_urb *urb,
uint32_t flags,
void *addr,
uint32_t len );
The usbd_setup_bulk() and usbd_setup_interrupt() functions follow the same parameter layout, where:
- urb
The handle of the allocated URB to be set up.
- flags
A bitmap of flags:
A direction flag must be set, either
USB_DIR_IN
,USB_DIR_OUT
, orUSB_DIR_NONE
.Optionally, you may add the
URB_SHORT_XFER_OK
for short transfers (merge flags via bitwise or).
- addr
The handle of a buffer, which must be allocated with usbd_alloc().
- len
The length of that buffer.
usbd_setup_isochronous()
int usbd_setup_isochronous( struct usbd_urb *urb,
uint32_t flags,
int32_t frame,
void *addr,
uint32_t len );
Thr usbd_setup_isochronous() function has the same parameters as usbd_setup_bulk() and usbd_setup_interrupt(), with the following modifications:
- flags
Has one additional valid flag:
Optional
URB_ISOCH_ASAP
, which overrides the frame parameter and sends data as soon as possible.Note that you still must set a direction flag.
- frame
The device frame number.
usbd_setup_vendor()
int usbd_setup_vendor( struct usbd_urb *urb,
uint32_t flags,
uint16_t request,
uint16_t rtype,
uint16_t value,
uint16_t index,
void *addr,
uint32_t len );
The usbd_setup_vendor() function has the same parameters as usbd_setup_bulk() and usbd_setup_interrupt() above, but has the following additions to support vendor-specifics:
- rtype
An additional bitmap which has two parameters that must be set:
A recipient type:
USB_RECIPIENT_DEVICE
,USB_RECIPIENT_INTERFACE
,USB_RECIPIENT_ENDPOINT
,USB_RECIPIENT_OTHER
A USB type:
USB_TYPE_STANDARD
,USB_TYPE_CLASS
,USB_TYPE_VENDOR
Use bitwise OR to combine these.
- value
Vendor-specific. Used for passing parameters.
- index
Vendor-specific. Used for passing parameters.
usbd_setup_control()
usbd_setup_control( struct usbd_urb *urb,
uint32_t flags,
uint16_t request,
uint16_t rtype,
uint16_t value,
uint16_t index,
void *addr,
uint32_t len );
The usbd_setup_control() function has the same parameters as usbd_setup_vendor() but is specifically and exclusively used at usbd_setup_control() endpoints.
The difference between these functions in QNX vs Linux are:
Direction is determined by the flag on the URB, not the pipe.
Callbacks are called asynchronously. In Linux, completion callbacks are called as interrupts.
Once a pipe, buffer, and URB are prepared, the request can be submitted
to io-usb
for processing via the usbd_io() function.
Note that unlike Linux, this process is asynchronous, and as such requires a callback function. Resources shared between the callback and main threads need to be protected in some way (e.g., mutex, semaphores).
usbd_io()
int usbd_io( struct usbd_urb *urb,
struct usbd_pipe *pipe,
void (*func)(struct usbd_urb *, struct usbd_pipe *, void *),
void *handle,
uint32_t timeout );
Where:
- urb
The handle of the URB to be processed.
- pipe
The handle of the pipe it will be sent over.
- func
The handle of the callback, which will be passed:
the urb handle for reference
the pipe handle for reference
a
void *
, which is the provided handle to the usbd_io() function.
- handle
A handle of user data to be passed to the callback. You can set this as needed to any pointer.
- timeout
A time in milliseconds for the request to expire.
USBD_TIME_DEFAULT
andUSBD_TIME_INFINITY
are pre-defined options that are also available.