USB (Universal Serial Bus)

USB is a common bus protocol, which transmits data via a serial connection between a two USB devices. This section will explain the basics of USB and the ways it can be implemented with the XDK.

  1. General Information
    1. The Universal Serial Bus
    2. Host Peripheral Architecture
    3. Descriptors
    4. Device Classes
  2. USB API Overview
  3. Initializing USB
  4. Sending USB data
    1. Sending data via USB API
    2. Sending data via printf
    3. Full Code Example
  5. Receiving USB data
    1. Receiving data via USB API
    2. Full Code Example

General Information

The Universal Serial Bus

USB, short for Universal Serial Bus, is an industrial standard which was developed in the mid-1990s. It is an uniform, user-friendly interface for all peripheral devices which can be connected to a computer for both data transfer and power supply purposes. Additional features are hot swapping (connecting and removing an active device) and plug & play (automatic recognition of peripheral devices).

USB was designed to meet three major requirements:

  • uniform interface for all peripheral devices
  • stable mechanical and easy plugin connection
  • small, space saving plugs

Keyboards, mouses, printer, microphones, cameras, scanners and many other devices can be treated as peripheral USB devices.

Host Peripheral Architecture

The connection between two USB connectable devices is physically realized as a point to point connection via one cable. Furthermore, one device acts as a host and the other device as peripheral. The host device reads the peripheral’s configuration and initializes it with its own specific configuration.

The available configuration states are listed in the following table:

StateDescription
AttachedDevice is connected to the USB interface
PoweredDevice is powered via the USB interface
DefaultDevice is reset and uses the initial address 0
AddressDevice uses an explicit address
ConfiguredDevice is configured and operational
SuspendedDevice conserves energy and is not operational

Please note, every USB device is specified by its own configuration to act as a host, a peripheral, or both. In case of the XDK, it is configured to act as a peripheral device and does not allow the attachment of other peripheral USB devices.

A host can maintain more than one connection to peripherals at a time, for example by using a USB hub. The USB hub constitutes the new host for all other connected peripheral devices. This architecture is shown in the following picture.

Image

Descriptors

USB uses so-called descriptors to exchange interaction data between peripheral devices and host devices. These descriptors store all information and functions of the corresponding USB device. The descriptor storage is split up in multiple sub-descriptors. Each of them has its own defined purpose. The descriptor hierarchy in the following picture shows how the different descriptors refer to each other.

Image

The device descriptor references to other descriptors with stored information. Additionally, the device descriptor consists of information of the implemented USB version, a producer ID, and a product ID. The next descriptor (or descriptors) is the configuration descriptor. This descriptor holds the information about the operating states of the USB device and the amount of endpoints which can be used with this USB device. The interface descriptor is the third stage of the descriptor hierarchy. These descriptors are designed to contain the functionality and usage of the USB device. It includes for example the information about the USB class and their class specific purpose. The endpoint descriptor is the fourth stage in descriptor hierarchy and contains information about the endpoints and the access permission of the USB device. The access permission contains additional information about the addresses of the endpoints and the transfer type of every of endpoint.

Possible transfer types are:

  • control for identification and configuration
  • bulk for printer, scanner or drives
  • interrupts for mouses and keyboards
  • isochron for audio and video streaming

Device Classes

USB devices can be used with different data rates to send stored data to the host and backwards. These data rates differ depending on the USB version in use (e.g. 12 Mbits/s for USB 1.0 or 5 Gbit/s for USB 3.0). Devices used with similar data rates can be combined in defined USB classes. Devices of the same class can use the same driver even if they differ in functionality.

The following six classes are available for USB devices.

  • Audio
  • Communications
  • Human Interface Device (HID)
  • Physical Interface Device (PID)
  • Mass Storage
  • Printer

The XDK is classified as a communication device.

USB API Overview

In general, it is recommended to develop an application based on the highest API level the XDK framework supports. It is possible to access deeper API levels, if an application requires more specific functionalities.

The XDK USB API is a part of the XDK platform section and accesses the EmLib, which abstracts parts of the EFM 32GG micro controller of the XDK and the freeRTOS library, which are both part of the library layer.

Image

Initializing USB

This chapter introduces the configuration and use of the USB interface. An initialization of the USB interface is not required, because it is initialized every time the XDK reboots. Only the USB specific interfaces need to be included to make the functionality work.

One of them is USB_ih.h, which is used for the USB sending functionality. XdkUsbResetUtility.h is used for the USB receiving functionality.

#include "USB_ih.h"

#include "XdkUsbResetUtility.h"

Sending USB data

Sending data via USB API

Sending data over USB can be performed in two different approaches with the USB interface. The XDK Workbench will act as USB host application for the complete USB sending implementation and display all incoming data from the XDK. USB_transmitData() is the first function, which can be used to send arrays of any data type to a desired destination USB device.

Please note, that this may cause misinterpretation in the XDK Workbench console, depending on which data is sent.

The following code snippet shows a simple implementation, which sends the string “Hello XDK Workbench" to the XDK Workbench via the function USB_transmitData(). Additionally a short delay is added to ensure, that the XDK and the XDK Workbench have established a working USB connection and the string can be displayed in the XDK Workbench.

/* Place the following code into appInitSystem() */

/* Delay because USB to workbench connection need to be established */
/* Note: This delay is only for demonstration purposes */
const TickType_t xDelay = 3000 / portTICK_PERIOD_MS;
vTaskDelay( xDelay );

char * content = "Hello XDK Workbench \n\r";
uint32_t contentlength = strlen(content);
USB_transmitData((uint8_t*) content, contentlength);

USB_trasmitByte() is the other function, which can be used to send data via USB. The difference to the function USB_trasmitData() is, that only a bytewise sending of data is possible with USB_trasmitByte().

Both explained functions are implemented to send only data from the datatyp uint8_t at a time. Unfortunately is all data of the different sensors of the XDK always stored as uint32_t. To be able to send them, the uint32_t datatype needs to be converted into 4 pieces of the datatyp uint8_t. The following two helper macros SWAP_32_MACRO() and SWAP_32_MEMCPY_MACRO() are used to implement this functionality.

SWAP_32_MACRO() implement this behavior by transforming the inserted data via bit shifting and ‘or’ operations.

Please note, SWAP_32_MACRO() transforms the inserted data to accomplish sending it via USB. The data needs to be converted back at the endpoint USB application receiving the data.

#define SWAP_32_MACRO(x) \
    (uint32_t)((x & 0xff) << 24 | \
    (x & 0xff00) << 8 | \ (x & 0xff0000) >> 8 | \
    (x & 0xff000000) >> 24);

The SWAP_32_MEMCPY_MACRO() macro takes a buffer address and the uint32_t data. First, the data is converted via SWAP_32_MACRO(), then the converted data is placed at the taken buffer address.

Please note, if you are using an array of the datatyp uint8_t to send the converted data, that the data is not placed at a certain element of the buffer but at a corresponding buffer address which should the given fourth array element. The uint32_t is then placed into the first four array elements.

#define SWAP_32_MEMCPY_MACRO(bufAddress,data) \
    {uint32_t _tmp = SWAP_32_MACRO(data);\
    memcpy(bufAddress, &_tmp, sizeof(_tmp));}

Sending data via printf

XDK applications are also able to send data to the host device via the printf() functionaly. The function call works out of the box like the standard implementation of printf().

The following code snippet shows exemplarly such an implementation. In addition a short delay is added to ensure, that the XDK and the XDK Workbench have established a working USB connection and the string can be displayed in the XDK Workbench.

#include "stdio.h"

/* Place the following code into appInitSystem() */

/* Delay because USB to workbench connection need to be established */
/* Note: This delay is only for demonstration purposes */
const TickType_t xDelay = 3000 / portTICK_PERIOD_MS;
vTaskDelay( xDelay );

printf("Hello XDK Workbench \n\r");

The output in the XDK Workbench is similar to the as the output by using the function USB_transmitData, if strings or characters are send.

Please note that the data send by using printf() is always encoded in ASCII. The XDK Workbench which was used to display the sent data also decodes the received data into ASCII, independent from its data type. All other data types are displayed as in the following picture.

Image

Full Code Example

Note: The full code example is intended for XDK-Workbench versions 3.4.0 and higher.

/*----------------------------------------------------------------------------/

/* --------------------------------------------------------------------------- |
 * INCLUDES & DEFINES ******************************************************** |
 * -------------------------------------------------------------------------- */

/* own header files */
#include "XdkAppInfo.h"
#undef BCDS_MODULE_ID  /* Module ID define before including Basics package*/
#define BCDS_MODULE_ID XDK_APP_MODULE_ID_APP_CONTROLLER

/* system header files */
#include <stdio.h>

/* additional interface header files */
#include "BCDS_CmdProcessor.h"
#include "FreeRTOS.h"
#include "timers.h"
#include "XdkUsbResetUtility.h"
#include "BCDS_USB.h"

/* --------------------------------------------------------------------------- |
 * HANDLES ******************************************************************* |
 * -------------------------------------------------------------------------- */

static CmdProcessor_T * AppCmdProcessor;/**< Handle to store the main Command processor handle to be used by run-time event driven threads */
xTimerHandle sendingUsbDataHandle = NULL;

/* --------------------------------------------------------------------------- |
 * VARIABLES ***************************************************************** |
 * -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------- |
 * EXECUTING FUNCTIONS ******************************************************* |
 * -------------------------------------------------------------------------- */

static void sendingUsbData(xTimerHandle xTimer){
    (void) xTimer;

    /* Delay because USB to workbench connection need to be established */
    /* Note: This delay is only for demonstration purposes */
    const TickType_t xDelay = 3000 / portTICK_PERIOD_MS;
    vTaskDelay( xDelay );

    char * content = "Hello XDK Workbench \n\r";
    uint32_t contentlength = strlen(content);
    USB_transmitData((uint8_t*) content, contentlength);
}

/* --------------------------------------------------------------------------- |
 * BOOTING- AND SETUP FUNCTIONS ********************************************** |
 * -------------------------------------------------------------------------- */

static void AppControllerEnable(void * param1, uint32_t param2)
{
    BCDS_UNUSED(param1);
    BCDS_UNUSED(param2);

    /* Enable necessary modules for the application and check their return values */
    uint32_t timerBlockTime = UINT32_MAX;

    xTimerStart(sendingUsbDataHandle,timerBlockTime);
}

static void AppControllerSetup(void * param1, uint32_t param2)
{
    BCDS_UNUSED(param1);
    BCDS_UNUSED(param2);
    Retcode_T retcode = RETCODE_OK;

    /* Setup the necessary modules required for the application */

    uint32_t oneSecondDelay = UINT32_C(1000);
    uint32_t timerAutoReloadOn = UINT32_C(1);

    sendingUsbDataHandle = xTimerCreate((const char *) "sendingUsbData", oneSecondDelay,
            timerAutoReloadOn, NULL, sendingUsbData);

    retcode = CmdProcessor_Enqueue(AppCmdProcessor, AppControllerEnable, NULL, UINT32_C(0));
    if (RETCODE_OK != retcode)
    {
        printf("AppControllerSetup : Failed \r\n");
        Retcode_RaiseError(retcode);
        assert(0); /* To provide LED indication for the user */
    }
}

void AppController_Init(void * cmdProcessorHandle, uint32_t param2)
{
    BCDS_UNUSED(param2);

    Retcode_T retcode = RETCODE_OK;

    if (cmdProcessorHandle == NULL)
    {
        printf("AppController_Init : Command processor handle is NULL \r\n");
        retcode = RETCODE(RETCODE_SEVERITY_ERROR, RETCODE_NULL_POINTER);
    }
    else
    {
        AppCmdProcessor = (CmdProcessor_T *) cmdProcessorHandle;
        retcode = CmdProcessor_Enqueue(AppCmdProcessor, AppControllerSetup, NULL, UINT32_C(0));
    }

    if (RETCODE_OK != retcode)
    {
        Retcode_RaiseError(retcode);
        assert(0); /* To provide LED indication for the user */
    }
}
/** ************************************************************************* */

Receiving USB data

Receiving data via USB API

In many cases it is needed to receive data on the XDK, for example to change settings. Unlike the sending functionality, receiving data requires the usage of interrupts.Interrupts are a mechanism to execute code when a specific event occurs as for example incoming data. This also includes incoming workbench interrupts such as boot, reboot, reset or flashing a new application onto the device. To receive data via USB, it is necessary to implement a custom interrupt. A manual implementation of the workbench interrupts is not required.

Image

The workbench can send interrupts while the XDK is in bootloader or application mode. As XDK applications contain the interrupt table, a corrupted application also corrupts the USB connection to the workbench, but the device can be reset by manually forcing it into bootloader mode.

To implement a custom interrupt are first some variables defined to handle the incoming data interrupt.

The interruptHandler is used to be set with the previously defined variables USB_ENABLE_FLAG and USB_DISABLE_FLAG. receivedData is used as a buffer to store the incoming data.

Please note, the use of USB_ENABLE_FLAG and USB_DISABLE_FLAG is not mandatory but recommended to avoid callback conflicts.

#define USB_ENABLE_FLAG UINT8_C(1)
#define USB_DISABLE_FLAG UINT8_C(0)
#define USB_RECEIVE_BUFFER_LENGTH UINT8_C(20)
static volatile uint8_t interruptHandler = USB_ENABLE_FLAG;
static char receivedData[USB_RECEIVE_BUFFER_LENGTH];

The function USB_CallbackIsr() will be used to implement the custom interrupt for receiving incoming data. First, xHigherPriorityTaskWoken is declared and initialized and then used to prohibit other tasks from taking the interrupt’s priority. If interruptHandler was enabled, it is being disabled. Then, if the incoming data fits into the buffer, it is copied to the receivedData buffer. Finally, the function usbInterruptHandling() is called by xTimerPendFunctionCallFromISR() to proceed further with the incoming data. This is where custom callback code is executed.

Please note, USB_RECEIVE_BUFFER_LENGTH size has to be adjusted depending on the use case. If the buffer size is to small, the incoming data will not be processed further.

/* Place the following code above initReceiveDataISR() */

extern void USB_CallbackIsr(uint8_t *usbRcvBuffer, uint16_t count) {
    portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
    if (USB_ENABLE_FLAG == interruptHandler) {
        interruptHandler = USB_DISABLE_FLAG;
        if(USB_RECEIVE_BUFFER_LENGTH > count) {
            memcpy(receivedData, usbRcvBuffer, count);
            receivedData[count] = (char) 0;

            /* add to timer queue*/
            if(xTimerPendFunctionCallFromISR(usbInterruptHandling,
                NULL, count, &xHigherPriorityTaskWoken)==pdPASS){
                    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
            }
        }
        else {
           interruptHandler = USB_ENABLE_FLAG;
        }
    }
}

The function usbInterruptHandling is executed every time the application receives data via the USB connection. There is no limitation set on what code is actually executed within the function. It is recommended to use short code without any loops, because interrupts have the highest priority possible and therefore block all other timer or operating tasks for their duration. For this particular example a second function reSendIncomingData() is being called by usbInterruptHandling(), which will be explained later. Afterwards interruptHandler is enabled again.

usbInterruptHandling() can be used to add desired functionality about how to process with the received data.

/* Place the following code above USB_CallbackIsr() */

static void usbInterruptHandling(void *callBackParam1, uint32_t count){
    (void) callback Param1;
    /* do something with receivedData and count */
    reSendIncomingData((uint8_t*)receivedData, count);

    /* re-enable the usb interrupt flag*/
    interruptHandler = USB_ENABLE_FLAG;
}

reSendIncomingData() works as an example function which processes the incoming data. The received data is sent back to the USB application which sent the data in the first place plus two additional text strings.

Please note that this code snippet could also be implemented as mentioned in usbInterruptHandling() to avoid another function call.

/* Place the following code above usbInterruptHandling() */

static void reSendIncomingData(uint8_t *usbRcvBuffer, uint16_t count){
    char * outputText = "XDK received following data: ";
    char * endingTag = "\n\r";
    uint32_t textLength = strlen(outputText);
    uint32_t endingLength = strlen(endingTag);
    USB_transmitData((uint8_t*)outputText, textLength);
    USB_transmitData((uint8_t*)usbRcvBuffer, count);
    USB_transmitData((uint8_t*)endingTag, endingLength);
}

To make the receiving interrupt work, USB_CallbackIsr() needs to be included as custom interrupt in the interrupt table of the XDK by passing it as custom interrupt callback to the function UsbResetUtility_RegAppISR to enable application interrupts. After flashing the application, the XDK is able to receive data via USB.

/* Place the following code in AppControllerEnable() */

UsbResetUtility_RegAppISR((UsbResetUtility_UsbAppCallback_T) USB_CallbackIsr);

Full Code Example

Note: The full code example is intended for XDK-Workbench versions 3.4.0 and higher.

/*----------------------------------------------------------------------------*/

/* --------------------------------------------------------------------------- |
 * INCLUDES & DEFINES ******************************************************** |
 * -------------------------------------------------------------------------- */

/* own header files */
#include "XdkAppInfo.h"
#undef BCDS_MODULE_ID  /* Module ID define before including Basics package*/
#define BCDS_MODULE_ID XDK_APP_MODULE_ID_APP_CONTROLLER

/* system header files */
#include <stdio.h>

/* additional interface header files */
#include "BCDS_CmdProcessor.h"
#include "FreeRTOS.h"
#include "timers.h"
#include "XdkUsbResetUtility.h"
#include "BCDS_USB.h"

/* --------------------------------------------------------------------------- |
 * HANDLES ******************************************************************* |
 * -------------------------------------------------------------------------- */

static CmdProcessor_T * AppCmdProcessor;/**< Handle to store the main Command processor handle to be used by run-time event driven threads */

/* --------------------------------------------------------------------------- |
 * VARIABLES ***************************************************************** |
 * -------------------------------------------------------------------------- */

#define USB_ENABLE_FLAG UINT8_C(1)
#define USB_DISABLE_FLAG UINT8_C(0)
#define USB_RECEIVE_BUFFER_LENGTH UINT8_C(20)
static volatile uint8_t interruptHandler = USB_ENABLE_FLAG;
static char receivedData[USB_RECEIVE_BUFFER_LENGTH];

/* --------------------------------------------------------------------------- |
 * EXECUTING FUNCTIONS ******************************************************* |
 * -------------------------------------------------------------------------- */

/* Place the following code above usbInterruptHandling() */
static void reSendIncomingData(uint8_t *usbRcvBuffer, uint16_t count){
    char * outputText = "XDK received following data: ";
    char * endingTag = "\n\r";
    uint32_t textLength = strlen(outputText);
    uint32_t endingLength = strlen(endingTag);

    USB_transmitData((uint8_t*)outputText, textLength);
    USB_transmitData((uint8_t*)usbRcvBuffer, count);
    USB_transmitData((uint8_t*)endingTag, endingLength);
}

/* Place the following code above USB_CallbackIsr() */
static void usbInterruptHandling(void *callBackParam1, uint32_t count){
    (void) callBackParam1;
    /* do something with receivedData and count */
    reSendIncomingData((uint8_t*)receivedData, count);
    /* re-enable the usb interrupt flag*/
    interruptHandler = USB_ENABLE_FLAG;
}

/* Place the following code above initReceiveDataISR() */
extern void USB_CallbackIsr(uint8_t *usbRcvBuffer, uint16_t count){
    portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
    if (USB_ENABLE_FLAG == interruptHandler){
    interruptHandler = USB_DISABLE_FLAG;
    if(USB_RECEIVE_BUFFER_LENGTH > count){
        memcpy(receivedData, usbRcvBuffer, count);
        receivedData[count] = (char) 0;
        /* add to timer queue*/
        if(xTimerPendFunctionCallFromISR(usbInterruptHandling,
        NULL, count, &xHigherPriorityTaskWoken)==pdPASS){
            portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
        }
        }
        else{
        interruptHandler = USB_ENABLE_FLAG;
    }
    }
}

/* --------------------------------------------------------------------------- |
 * BOOTING- AND SETUP FUNCTIONS ********************************************** |
 * -------------------------------------------------------------------------- */

static void AppControllerEnable(void * param1, uint32_t param2)
{
    BCDS_UNUSED(param1);
    BCDS_UNUSED(param2);

    UsbResetUtility_RegAppISR((UsbResetUtility_UsbAppCallback_T) USB_CallbackIsr);
}

static void AppControllerSetup(void * param1, uint32_t param2)
{
    BCDS_UNUSED(param1);
    BCDS_UNUSED(param2);
    Retcode_T retcode = RETCODE_OK;

    /* Setup the necessary modules required for the application */

    /* No initialization of the USB module necessary, since it is intialized at every boot up of the XDK */

    retcode = CmdProcessor_Enqueue(AppCmdProcessor, AppControllerEnable, NULL, UINT32_C(0));
    if (RETCODE_OK != retcode)
    {
        printf("AppControllerSetup : Failed \r\n");
        Retcode_RaiseError(retcode);
        assert(0); /* To provide LED indication for the user */
    }
}

void AppController_Init(void * cmdProcessorHandle, uint32_t param2)
{
    BCDS_UNUSED(param2);

    Retcode_T retcode = RETCODE_OK;

    if (cmdProcessorHandle == NULL)
    {
        printf("AppController_Init : Command processor handle is NULL \r\n");
        retcode = RETCODE(RETCODE_SEVERITY_ERROR, RETCODE_NULL_POINTER);
    }
    else
    {
        AppCmdProcessor = (CmdProcessor_T *) cmdProcessorHandle;
        retcode = CmdProcessor_Enqueue(AppCmdProcessor, AppControllerSetup, NULL, UINT32_C(0));
    }

    if (RETCODE_OK != retcode)
    {
        Retcode_RaiseError(retcode);
        assert(0); /* To provide LED indication for the user */
    }
}

/** ************************************************************************* */