The example device implements only a simple function to continously send the message "hello, USB!" to the USB host (master), using a bulk transfer endpoint.
Overview
The BeRTOS USB API makes easy for peripherals and other devices embedding BeRTOS to act in the USB device role (slave).
The API standardizes an architecture-agnostic layer on top of the standard USB software stack.
_________________________________
| High-level USB layer |
| (usb-serial, mass-storage, ...) |
+---------------------------------+
| Middle-level USB layer |
| (hardware-agnostic interface) |
+---------------------------------+
| Low-level USB layer |
| (hardware-specific driver) |
+---------------------------------+
From the bottom those layers are:
- Low-level USB layer: the layer that talks directly to the hardware. Different hardware controllers need different drivers which may also need board-specific customization. These drivers provide the common glue to which the higher level drivers are written.
- Middle-level USB layer: implement the standard USB2.0 specification primitives. Usually, this affects the configuration and management parts of the USB endpoint logic.
- High-level USB layer: implement a single USB function (i.e., serial, mass-storage, HID, ...) that works virtually with any USB hardware controller and exports its functionality via public kernel interfaces, such as the KFile, KBlock or other high-level kernel subsystems.
In this guide you will learn how to create a high-level USB device driver. If you need to help on porting the code to a different low-level controller, ask help on the developer's website.
USB device drivers
USB device drivers are implemented into the high-level USB layer.
Examples:
- HID (keyboard, mouse, storage)
- serial (serial connection between the host and the device)
- mass-storage (SCSI over USB)
- audio/video class driver
USB endpoints
Communication between devices (slaves) and host (master) is conceptualised as using pipes. Each pipe is a communication channel between the host and an endpoint on the device. Each endpoint represents a part of a device that fulfils one specific purpose for that device, such as to receive commands or transmit data.
Each endpoint can be configured exclusively to receive (OUT) or transmit (IN) data with the host. Communication in both directions at the same time is not allowed for a single endpoint. For this purpose an additional endpoint must be allocated.
The endpoint's direction is always relative to the USB host (master):
- IN: from device to host,
- OUT: from host to device.
The USB specification defines four endpoint transfer types:
- Control: typically used for small-transfer operations, like command/status communications.
- Interrupt: typically used for devices that provide small amounts of data at sporadic and unpredictable times; the host periodically polls the device triggering an interrupt on the device, the device's endpoint determines the rate of polling, which can range from 1 to 255 milliseconds (i.e., keyboard, mouse, joystick, ...).
- Bulk: used for devices that have large amounts of data to transmit or receive and that require guaranteed delivery, but do not have any specific bandwidth or latency requirements (i.e., mass-storage, printers, scanners, ...).
- Isochronous: provide guaranteed amounts of bandwidth and latency. They are used for streaming data that is time-critical and error-tolerant or for real-time applications that require a constant data transfer rate (i.e., audio/video stream).
USB device driver example in BeRTOS
This howto reports all the required steps to implement a complete USB device driver from scratch using the BeRTOS API.
You can find example code for keyboard, mouse and usb-serial converter devices inside BeRTOS source code.
Choose valid vendor and product ID of your device
#define USB_DEV_VENDOR_ID 0xffff /* custom (change this in your project!) */ #define USB_DEV_PRODUCT_ID 0x0000 /* custom (change this in your project!) */
The USB IDs are commonly used by the USB host (master) to identify the class of your device and "plug" the approriate driver to manage it.
NOTE: public USB IDs are maintained by USB Implementers Forum, Inc., a non-profit corporation founded by the group of companies that developed the Universal Serial Bus specification.
See the list of allocated USB IDs for details.
Define a device descriptor
The device descriptor of a USB device represents the entire device. As a result a USB device can only have one device descriptor.
static UsbDeviceDesc usb_dev_device_descriptor =
{
.bLength = sizeof(usb_dev_device_descriptor),
.bDescriptorType = USB_DT_DEVICE,
.bcdUSB = 0x100, /* this usually represents the device's firmware version */
.idVendor = USB_DEV_VENDOR_ID,
.idProduct = USB_DEV_PRODUCT_ID,
.bNumConfigurations = 1, /* number of config descriptors (see below) */
};
Define a configuration descriptor
A USB device can have several different configurations although the majority of devices are simple and only have one. The configuration descriptor specifies how the device is powered, what the maximum power consumption is, the number of interfaces it has.
static const UsbConfigDesc usb_dev_config_descriptor =
{
.bLength = sizeof(usb_dev_config_descriptor),
.bDescriptorType = USB_DT_CONFIG,
.bNumInterfaces = 1, /* number of interface descriptors (see below) */
.bConfigurationValue = 1,
.iConfiguration = 0,
.bmAttributes = USB_CONFIG_ATT_ONE,
.bMaxPower = 50, /* 100 mA */
};
Define an interface descriptor
The interface descriptor could be seen as a header or grouping of the endpoints into a functional group performing a single feature of the device.
static const UsbInterfaceDesc usb_dev_interface_descriptor =
{
.bLength = sizeof(usb_dev_interface_descriptor),
.bDescriptorType = USB_DT_INTERFACE,
.bInterfaceNumber = 0,
.bNumEndpoints = 1, /* number of endpoints (see below) */
.bInterfaceClass = USB_CLASS_VENDOR_SPEC, /* vendor specific class */
.iInterface = 0,
};
Define an endpoint descriptors
The first thing to do for this step is to allocate an endpoint for our device. The allocation can be done declaring the name of the endpoint in the file bertos/drv/usb_endpoint.h:
/* Enpoint allocation (according to the compile-time options) */
enum {
USB_CTRL_ENDPOINT = 0, /* This must be always allocated */
#if (defined(CONFIG_USBSER) && CONFIG_USBSER)
USB_SERIAL_EP_REPORT,
USB_SERIAL_EP_OUT,
USB_SERIAL_EP_IN,
#endif
#if (defined(CONFIG_USBKBD) && CONFIG_USBKBD)
USB_KBD_EP_REPORT,
#endif
#if (defined(CONFIG_USBMOUSE) && CONFIG_USBMOUSE)
USB_MOUSE_EP_REPORT,
#endif
USB_DEV_ENDPOINT, // <--- new endpoint allocated for the generic "usbdev"
USB_EP_MAX, /* Number of allocated endpoints */
};
Define the endpoint descriptor in the project, using the chosen endpoint's name in the field .bEndpointAddress:
static const UsbEndpointDesc usb_dev_ep_descriptor =
{
.bLength = sizeof(usb_dev_ep_descriptor),
.bDescriptorType = USB_DT_ENDPOINT,
.bEndpointAddress = USB_DIR_IN | USB_DEV_ENDPOINT,
.bmAttributes = USB_ENDPOINT_XFER_BULK, /* Bulk endpoint type */
.wMaxPacketSize = usb_cpu_to_le16((uint16_t)64),
};
For this example we will use only a single bulk transfer type IN endpoint.
Group all the descriptors together and define the USB device as a whole
/* Define the hierarchy of the previous descriptors */
static const UsbDescHeader *usb_dev_config[] =
{
(const UsbDescHeader *)&usb_dev_config_descriptor,
(const UsbDescHeader *)&usb_dev_interface_descriptor,
(const UsbDescHeader *)&usb_dev_ep_descriptor,
NULL,
};
/* USB device descriptor, as recognized by BeRTOS */
static UsbDevice usb_dev = {
.device = &usb_dev_device_descriptor,
.config = usb_dev_config,
};
Register the driver into the USB subsystem
static int usb_dev_init(void)
{
if (usb_deviceRegister(&usb_dev) < 0)
return -1;
kprintf("usb-dev: registered new USB interface driver\n");
return 0;
}
Declare the project's main loop
The device driver is almost ready, we only need to define the function to send data to the host using the IN endpoint. For example, the main loop of our project could look like the following:
int main(void)
{
static const char msg[] = "hello, USB!";
/* Hardware initialization */
init();
while (1)
{
kprintf("send message to host: %s\n", msg);
/* Send the message to the host and wait 1s */
usb_endpointWrite(usb_dev_ep_descriptor.bEndpointAddress,
msg, sizeof(msg));
timer_delay(1000);
}
}
The function usb_endpointWrite() is provided by the BeRTOS USB subsystem and
performs the actual communication to the underlying USB controller.
The USB host counterpart
To check if the driver is working properly a software counterpart is needed on the host, since the custom device does not implement a specific USB class and the host has not the driver to manage it.
We can implement this software in Linux host using the libusb framework.
Following is reported an example C code using libusb-1.0:
/*
* test.c
*
* Compile:
* gcc -I/usr/include/libusb-1.0 -lusb-1.0 -o test test.c
* Run:
* ./test
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <libusb.h>
#include <sys/types.h>
#include <sys/stat.h>
#define USB_VENDOR_ID 0xffff /* USB vendor ID used by the BeRTOS device */
#define USB_PRODUCT_ID 0x0000 /* USB product ID used by the BeRTOS device */
#define USB_ENDPOINT (LIBUSB_ENDPOINT_IN | 1) /* endpoint address */
#define USB_TIMEOUT 3000 /* Connection timeout (in ms) */
static libusb_context *ctx = NULL;
static libusb_device_handle *handle;
static unsigned char buf[64];
static void usb_read(void)
{
int nread, ret;
ret = libusb_bulk_transfer(handle, USB_ENDPOINT, buf, sizeof(buf),
&nread, 0);
if (ret)
printf("ERROR in bulk read: %d\n", ret);
else
printf("receive message from device = %s\n", buf);
}
static void sighandler(int signum)
{
if (handle)
libusb_close(handle);
libusb_exit(NULL);
exit(0);
}
int main(int argc, char **argv)
{
signal(SIGINT, sighandler);
libusb_init(&ctx);
libusb_set_debug(ctx, 3);
handle = libusb_open_device_with_vid_pid(ctx,
USB_VENDOR_ID, USB_PRODUCT_ID);
if (!handle) {
perror("BeRTOS device not found");
return 1;
}
while (1)
usb_read();
libusb_close(handle);
libusb_exit(NULL);
return 0;
}
Compile and run the above testcase, then connect the BeRTOS USB device to the Linux host. The output on the PC should look as following:
root@linux # ./test receive message from device = hello, USB! receive message from device = hello, USB! receive message from device = hello, USB! receive message from device = hello, USB! ...
