USBD ROM Stack  1.0
ROM based USB device stack
Developing with USB Device ROM Stack

This section introduces you to application development using USB ROM stack. The topics in this section describe high-level USB concepts and provide step-by-step instructions on implementing a USB device using USB ROM stack. For detailed information on USB concepts, see USB specifications at usb.org.

For advanced user who wants to change the default stack behavior by implementing custom overrides see Customizing USBD ROM Core Layer.

Prerequisites

  • A sample project which initializes processor, clocks (oscillator, system PLL & USB PLL), I/O configuration, memory subsystem and C-runtime library (includes initialization of process stack and other resources).
  • Obtain and include all USB ROM stack API header files in the project.
  • Initialize the appropriate USB clocks per chip specific clock controller.
  • Initialize all USB related pins (USB_VBUS, USB_ID, USB_DP, USB_DM) according to chip and board connections. Clock and pin initialization is outside the scope of USBD ROM stack.

Step1: Define USB descriptors

A USB device provides information about itself in data structures called USB descriptors to a host during enumeration process. Hence user application should create these descriptors as per the application and provide it to the ROM stack. So that the stack takes care of responding to the standard USB requests generated by the host. See Defining USB Descriptors for more details on how to define these descriptor arrays accepted by the stack.

Step2: Initializing the USBD ROM Stack

To initialize the USBD ROM stack configure the initialization parameters.

  • Define any handle to access the ROM table as shown below.
    /* A table of pointers to the USBD functions contained in ROM is located at the address contained at this location */
    #define USBD_FUNCTION_TABLE_PTR_ADDR                    (0x1040011CUL)
    
    #define USBD_API (*((USBD_API_T**)USBD_FUNCTION_TABLE_PTR_ADDR))
    
  • Create an instance of USBD_API_INIT_PARAM_T as a local variable. Reset all data member of the instance to 0.
        USBD_API_INIT_PARAM_T usb_param;
        USB_CORE_DESCS_T desc;
    
        /* reset all init params */
        memset((void*)&usb_param, 0, sizeof(USBD_API_INIT_PARAM_T));
    
  • Define the memory area ROM stack can use for its global variables. As described in Memory Model section, the application needs to provide memory area in which the stack can allocate its global variables and also the device controller driver buffers. This memory address should be accessible by USB bus master.
        usb_param.mem_base = 0x20000000;
        usb_param.mem_size = 0x2000;
    
  • If the application is using dynamic memory allocator or wants to know exactly how much memory stack needs. It can call USBD_HW_API_T::GetMemSize routine to know the size of memory buffer stack needs.
    • Set the USB register base address location and number of endpoints used by the application to optimize driver buffer space. Setting max_num_ep to more than the maximum number of endpoints available in hardware will cause program crashes.
          usb_param.usb_reg_base = LPC_USB0_BASE;
          usb_param.max_num_ep = 6;
      
  • Provide event callback routines for event in which application is interested in. See USBD_API_INIT_PARAM_T for list of USB device events to which the application can subscribe.
    usb_param.USB_Configure_Event = App_Configure_Event;
    
  • Define USB descriptors (see Step1: Define USB descriptors ) and initialize the descriptor parameter.
        /* Set the USB descriptors */
        desc.device_desc = USB_DeviceDescriptor;
        desc.string_desc = USB_StringDescriptor;
        desc.full_speed_desc = USB_FsConfigDescriptor;
        desc.high_speed_desc = USB_HsConfigDescriptor;
        desc.device_qualifier = USB_DeviceQualifier; 
    
    Call Init() routine to initialize the stack. If the routine returns anything other than LPC_OK then the parameters are not configured properly. See USBD_HW_API_T::Init for more details. If the initialization call is successful the handle to the instance of USBD ROM stack is return in phUsb parameter. Application should store this handle in a global scope variable to access stack functions from USBD and IRQ contexts.
    /* USB Initialization */
    ret = USBD_API->hw->Init(&g_hUsb, &desc, &usb_param);  
    if (ret == LPC_OK) {
    
        ..... do some USB stuff...
    }

Step3: Connecting USB IRQ handler

USBD ROM stack implements the interrupt handler for USB device controller and invokes the USBD ROM stack routines according to the event type. Hence application should connect this handler to the application vector table. This is done by calling USBD_HW_API_T::ISR from the USB IRQ handler as shown in the code snippet below. The USBD ROM handle passed to the ISR() routine is the one obtained in previous step.

void USB0_IRQHandler(void)
{
    USBD_API->hw->ISR(g_hUsb);
}

Step4: Initialize and attach ROM class drivers

USBD ROM stack implements several function drivers as part of its class layer. These function drivers can be attached to their corresponding interface to handle the class specific request associated with that interface. Most of the class/function drivers implemented by the USBD ROM stack take care of the class requests sent on the default control endpoint and leave most of the data handling done on bulk/isochronous endpoints to the application. Except Mass Storage class driver which implement bulk-only handler to parse SCSI commands. See individual class driver module help pages to learn more about its behavior.

In the following example Mass Storage class driver is attached to the only interface (interface 0, alt 0) present in the device configuration shown in Defining USB Configuration Descriptors Array section.

  • Configure initialization parameters of the MSC class driver
        USBD_MSC_INIT_PARAM_T msc_param;
    
        memset((void*)&msc_param, 0, sizeof(USBD_MSC_INIT_PARAM_T));
    
  • Set the memory location where the MSC class driver can allocate buffers and global variables. All the init() routines of the ROM components are written in such a way that the mem_base and mem_size members of XXX_INIT_PARAM_T are updated with free location and size before returning. So that the next component XXX_INIT_PARAM_T can use the update values for its init() routine. Applications can cascade the component initialization this way without worrying about memory wastage/overlap issues.
        msc_param.mem_base = usb_param.mem_base;
        msc_param.mem_size = usb_param.mem_size;
    
  • Now set the MSC specific parameters.
        /* mass storage params */
        msc_param.InquiryStr = (uint8_t*)InquiryStr; 
        msc_param.BlockCount = MSC_MemorySize / MSC_BlockSize;
        msc_param.BlockSize = MSC_BlockSize;
        msc_param.MemorySize = MSC_MemorySize;
    
  • Obtain the address location of the interface descriptor, within the device configuration descriptor array, to which the class driver has to be attached.
        pIntfDesc =
           (USB_INTERFACE_DESCRIPTOR*)((uint32_t)desc.high_speed_desc + USB_CONFIGUARTION_DESC_SIZE);
        /* check we are referencing to the proper interface descriptor */ 
        if ((pIntfDesc == 0) ||
           (pIntfDesc->bInterfaceClass != USB_DEVICE_CLASS_STORAGE) ||
           (pIntfDesc->bInterfaceSubClass != MSC_SUBCLASS_SCSI) )
        return ERR_FAILED;
        msc_param.intf_desc = (uint8_t*)pIntfDesc;
    
  • Define and set the MSC class callback routines.
        /* user defined functions */
        msc_param.MSC_Write = translate_wr; 
        msc_param.MSC_Read = translate_rd;
        msc_param.MSC_Verify = translate_verify;
        msc_param.MSC_GetWriteBuf = translate_GetWrBuf;
    
  • Now call the USBD_MSC_API_T::init routine of the MSC class.
        ret = USBD_API->msc->init(hUsb, &msc_param);
    
    
    if (ret != LPC_OK) {
      vCatchError(0); //"usb_msc_mem_init error!!!"
    }
    

Step5: Initialize and attach custom class drivers

To implement custom class driver or usb.org defined standard class driver which is not supported by the ROM stack, the following steps need to be followed.

  • Install default endpoint handler to handle class specific requests by call USBD_CORE_API_T::RegisterClassHandler. The stack calls all the registered class handlers on any EP0 event before going through default handling of the event. This gives the class handlers to implement class specific request handlers and also to override the default stack handling for a particular event targeted to the interface. Check USB_EP_HANDLER_T for more details on implementing endpoint handlers.
  • Implement class handler.
  • Install endpoint handlers for other bulk/isochronous/interrupt endpoints present in the interface using USBD_CORE_API_T::RegisterEpHandler routine.
  • Implement endpoint handlers.

Step6: Enable IRQ and connect the device.

Once the core, device controller and class drivers are initialized the stack is ready to receive the packets from host. Even if the device is physically connected to the host using an USB cable the host does not recognize the presence of device until the D+ line is pulled up using 1.5K ohm resistor (LPC device controllers operate in high-speed or full-speed mode only). To enable the connection the application software should call USBD_HW_API_T::Connect with enable set to 1. Before enabling the connection the application should enable the USB interrupt.

NVIC_EnableIRQ(USB0_IRQn); //  enable USB0 interrupts
/* now connect */
USBD_API->hw->Connect(g_hUsb, 1);

Subsections :