560 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			560 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /**************************************************************************/
 | |
| /*!
 | |
|     @file     dcd_nrf5x.c
 | |
|     @author   hathach
 | |
| 
 | |
|     @section LICENSE
 | |
| 
 | |
|     Software License Agreement (BSD License)
 | |
| 
 | |
|     Copyright (c) 2018, hathach (tinyusb.org)
 | |
|     All rights reserved.
 | |
| 
 | |
|     Redistribution and use in source and binary forms, with or without
 | |
|     modification, are permitted provided that the following conditions are met:
 | |
|     1. Redistributions of source code must retain the above copyright
 | |
|     notice, this list of conditions and the following disclaimer.
 | |
|     2. Redistributions in binary form must reproduce the above copyright
 | |
|     notice, this list of conditions and the following disclaimer in the
 | |
|     documentation and/or other materials provided with the distribution.
 | |
|     3. Neither the name of the copyright holders nor the
 | |
|     names of its contributors may be used to endorse or promote products
 | |
|     derived from this software without specific prior written permission.
 | |
| 
 | |
|     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY
 | |
|     EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 | |
|     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 | |
|     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
 | |
|     DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 | |
|     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 | |
|     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 | |
|     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 | |
|     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 | |
|     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 | |
| */
 | |
| /**************************************************************************/
 | |
| 
 | |
| #include "tusb_option.h"
 | |
| 
 | |
| #if MODE_DEVICE_SUPPORTED && CFG_TUSB_MCU == OPT_MCU_NRF5X
 | |
| 
 | |
| // TODO remove
 | |
| #include "nrf.h"
 | |
| #include "nrf_power.h"
 | |
| #include "nrf_usbd.h"
 | |
| #include "nrf_clock.h"
 | |
| 
 | |
| #include "nrf_drv_usbd_errata.h"
 | |
| 
 | |
| #include "device/dcd.h"
 | |
| 
 | |
| /*------------------------------------------------------------------*/
 | |
| /* MACRO TYPEDEF CONSTANT ENUM
 | |
|  *------------------------------------------------------------------*/
 | |
| enum
 | |
| {
 | |
|   // Max allowed by USB specs
 | |
|   MAX_PACKET_SIZE   = 64,
 | |
| 
 | |
|   // Mask of all END event (IN & OUT) for all endpoints. ENDEPIN0-7, ENDEPOUT0-7, ENDISOIN, ENDISOOUT
 | |
|   EDPT_END_ALL_MASK = 0x1FFBFCUL
 | |
| };
 | |
| 
 | |
| /*------------------------------------------------------------------*/
 | |
| /* VARIABLE DECLARATION
 | |
|  *------------------------------------------------------------------*/
 | |
| typedef struct
 | |
| {
 | |
|   uint8_t* buffer;
 | |
| 
 | |
|   uint16_t total_len;
 | |
|   uint16_t actual_len;
 | |
| 
 | |
|   uint8_t  mps; // max packet size
 | |
| 
 | |
|   // FIXME Errata 104 walkaround
 | |
|   uint16_t frame_num;
 | |
| } nom_xfer_t;
 | |
| 
 | |
| /*static*/ struct
 | |
| {
 | |
|   struct
 | |
|   {
 | |
|     uint8_t* buffer;
 | |
|     uint16_t len;
 | |
|     uint8_t  dir;
 | |
|   }control;
 | |
| 
 | |
|   // Non control: 7 endpoints IN & OUT (offset 1)
 | |
|   nom_xfer_t xfer[7][2];
 | |
| 
 | |
|   volatile bool dma_running;
 | |
| }_dcd;
 | |
| 
 | |
| void bus_reset(void)
 | |
| {
 | |
|   for(int i=0; i<8; i++)
 | |
|   {
 | |
|     NRF_USBD->TASKS_STARTEPIN[i] = 0;
 | |
|     NRF_USBD->TASKS_STARTEPOUT[i] = 0;
 | |
|   }
 | |
| 
 | |
|   NRF_USBD->TASKS_STARTISOIN  = 0;
 | |
|   NRF_USBD->TASKS_STARTISOOUT = 0;
 | |
| 
 | |
|   varclr(&_dcd);
 | |
| }
 | |
| 
 | |
| /*------------------------------------------------------------------*/
 | |
| /* Controller API
 | |
|  *------------------------------------------------------------------*/
 | |
| bool dcd_init (uint8_t rhport)
 | |
| {
 | |
|   (void) rhport;
 | |
|   return true;
 | |
| }
 | |
| 
 | |
| void dcd_connect (uint8_t rhport)
 | |
| {
 | |
| 
 | |
| }
 | |
| void dcd_disconnect (uint8_t rhport)
 | |
| {
 | |
| 
 | |
| }
 | |
| 
 | |
| void dcd_set_address (uint8_t rhport, uint8_t dev_addr)
 | |
| {
 | |
|   (void) rhport;
 | |
|   // Set Address is automatically update by hw controller
 | |
| }
 | |
| 
 | |
| void dcd_set_config (uint8_t rhport, uint8_t config_num)
 | |
| {
 | |
|   (void) rhport;
 | |
|   (void) config_num;
 | |
|   // Nothing to do
 | |
| }
 | |
| 
 | |
| /*------------------------------------------------------------------*/
 | |
| /* Control
 | |
|  *------------------------------------------------------------------*/
 | |
| static void edpt_dma_start(uint8_t epnum, uint8_t dir)
 | |
| {
 | |
|   // Only one dma could be active, TODO resolve when this is called in ISR and dma is running
 | |
|   while ( _dcd.dma_running ) { }
 | |
| 
 | |
|   _dcd.dma_running = true;
 | |
| 
 | |
|   if ( dir == TUSB_DIR_OUT )
 | |
|   {
 | |
|     NRF_USBD->TASKS_STARTEPOUT[epnum] = 1;
 | |
|   } else
 | |
|   {
 | |
|     NRF_USBD->TASKS_STARTEPIN[epnum] = 1;
 | |
|   }
 | |
| 
 | |
|   __ISB(); __DSB();
 | |
| }
 | |
| 
 | |
| static void edpt_dma_end(void)
 | |
| {
 | |
|   _dcd.dma_running = false;
 | |
| }
 | |
| 
 | |
| static void control_xact_start(void)
 | |
| {
 | |
|   // Each transaction is up to 64 bytes
 | |
|   uint8_t const xact_len = min16_of(_dcd.control.len, MAX_PACKET_SIZE);
 | |
| 
 | |
|   if ( _dcd.control.dir == TUSB_DIR_OUT )
 | |
|   {
 | |
|     // TODO control out
 | |
|     NRF_USBD->EPOUT[0].PTR    = (uint32_t) _dcd.control.buffer;
 | |
|     NRF_USBD->EPOUT[0].MAXCNT = xact_len;
 | |
| 
 | |
|     NRF_USBD->TASKS_EP0RCVOUT = 1;
 | |
|     __ISB(); __DSB();
 | |
|   }else
 | |
|   {
 | |
|     NRF_USBD->EPIN[0].PTR        = (uint32_t) _dcd.control.buffer;
 | |
|     NRF_USBD->EPIN[0].MAXCNT     = xact_len;
 | |
| 
 | |
|     edpt_dma_start(0, TUSB_DIR_IN);
 | |
|   }
 | |
| 
 | |
|   _dcd.control.buffer += xact_len;
 | |
|   _dcd.control.len    -= xact_len;
 | |
| }
 | |
| 
 | |
| //static void control_xact_done(void)
 | |
| //{
 | |
| //  if ( _dcd_data.control.xfer_len > 0 )
 | |
| //  {
 | |
| //    if ( _dcd_data.control.dir == TUSB_DIR_OUT )
 | |
| //    {
 | |
| //      // out control need to wait for END EPOUT event before updating Pointer
 | |
| //      edpt_dma_start(0, TUSB_DIR_OUT);
 | |
| //    }else
 | |
| //    {
 | |
| //      control_xact_start();
 | |
| //    }
 | |
| //  }else
 | |
| //  {
 | |
| //    dcd_xfer_complete(0, 0, 0, true);
 | |
| //  }
 | |
| //}
 | |
| 
 | |
| 
 | |
| bool dcd_control_xfer (uint8_t rhport, tusb_dir_t dir, uint8_t * buffer, uint16_t length)
 | |
| {
 | |
|   (void) rhport;
 | |
| 
 | |
|   if ( length )
 | |
|   {
 | |
|     // Data Phase
 | |
|     _dcd.control.len    = length;
 | |
|     _dcd.control.buffer = buffer;
 | |
|     _dcd.control.dir    = (uint8_t) dir;
 | |
| 
 | |
|     control_xact_start();
 | |
|   }else
 | |
|   {
 | |
|     // Status Phase
 | |
|     NRF_USBD->TASKS_EP0STATUS = 1;
 | |
|     __ISB(); __DSB();
 | |
|   }
 | |
| 
 | |
|   return true;
 | |
| }
 | |
| 
 | |
| /*------------------------------------------------------------------*/
 | |
| /*
 | |
|  *------------------------------------------------------------------*/
 | |
| 
 | |
| static inline nom_xfer_t* get_td(uint8_t epnum, uint8_t dir)
 | |
| {
 | |
|   return &_dcd.xfer[epnum-1][dir];
 | |
| }
 | |
| 
 | |
| static void normal_xact_start(uint8_t epnum, uint8_t dir)
 | |
| {
 | |
|   if ( dir == TUSB_DIR_OUT )
 | |
|   {
 | |
|     // Errata : HW issue on nrf5284 sample, SIZE.EPOUT won't trigger ACK as spec
 | |
|     // use the back door interface as sdk for walk around
 | |
|     if ( nrf_drv_usbd_errata_sizeepout_rw() )
 | |
|     {
 | |
|       *((volatile uint32_t *)(NRF_USBD_BASE + 0x800)) = 0x7C5 + 2*epnum;
 | |
|       *((volatile uint32_t *)(NRF_USBD_BASE + 0x804)) = 0;
 | |
|       (void) (((volatile uint32_t *)(NRF_USBD_BASE + 0x804)));
 | |
|     }
 | |
|     else
 | |
|     {
 | |
|       // Overwrite size will allow hw to accept data
 | |
|       NRF_USBD->SIZE.EPOUT[epnum] = 0;
 | |
|       __ISB(); __DSB();
 | |
|     }
 | |
|   }else
 | |
|   {
 | |
|     nom_xfer_t* xfer = get_td(epnum, dir);
 | |
| 
 | |
|     // Each transaction is up to Max Packet Size
 | |
|     uint8_t const xact_len = min16_of(xfer->total_len - xfer->actual_len, xfer->mps);
 | |
| 
 | |
|     NRF_USBD->EPIN[epnum].PTR    = (uint32_t) xfer->buffer;
 | |
|     NRF_USBD->EPIN[epnum].MAXCNT = xact_len;
 | |
| 
 | |
|     xfer->buffer += xact_len;
 | |
| 
 | |
|     edpt_dma_start(epnum, TUSB_DIR_IN);
 | |
|   }
 | |
| }
 | |
| 
 | |
| bool dcd_edpt_open (uint8_t rhport, tusb_desc_endpoint_t const * desc_edpt)
 | |
| {
 | |
|   (void) rhport;
 | |
| 
 | |
|   uint8_t const epnum = edpt_number(desc_edpt->bEndpointAddress);
 | |
|   uint8_t const dir   = edpt_dir(desc_edpt->bEndpointAddress);
 | |
| 
 | |
|   _dcd.xfer[epnum-1][dir].mps = desc_edpt->wMaxPacketSize.size;
 | |
| 
 | |
|   if ( dir == TUSB_DIR_OUT )
 | |
|   {
 | |
|     NRF_USBD->INTENSET = BIT_(USBD_INTEN_ENDEPOUT0_Pos + epnum);
 | |
|     NRF_USBD->EPOUTEN |= BIT_(epnum);
 | |
|   }else
 | |
|   {
 | |
|     NRF_USBD->INTENSET = BIT_(USBD_INTEN_ENDEPIN0_Pos + epnum);
 | |
|     NRF_USBD->EPINEN  |= BIT_(epnum);
 | |
|   }
 | |
|   __ISB(); __DSB();
 | |
| 
 | |
|   return true;
 | |
| }
 | |
| 
 | |
| bool dcd_edpt_xfer (uint8_t rhport, uint8_t ep_addr, uint8_t * buffer, uint16_t total_bytes)
 | |
| {
 | |
|   (void) rhport;
 | |
| 
 | |
|   uint8_t const epnum = edpt_number(ep_addr);
 | |
|   uint8_t const dir   = edpt_dir(ep_addr);
 | |
| 
 | |
|   nom_xfer_t* xfer = get_td(epnum, dir);
 | |
| 
 | |
|   xfer->buffer     = buffer;
 | |
|   xfer->total_len  = total_bytes;
 | |
|   xfer->actual_len = 0;
 | |
| 
 | |
|   // FIXME Errata 104 walkaround
 | |
|   if ( nrf_drv_usbd_errata_104() )
 | |
|   {
 | |
|     xfer->frame_num  = (uint16_t) NRF_USBD->FRAMECNTR;
 | |
|   }
 | |
| 
 | |
|   normal_xact_start(epnum, dir);
 | |
| 
 | |
|   return true;
 | |
| }
 | |
| 
 | |
| void dcd_edpt_stall (uint8_t rhport, uint8_t ep_addr)
 | |
| {
 | |
|   (void) rhport;
 | |
| 
 | |
|   if ( ep_addr == 0)
 | |
|   {
 | |
|     NRF_USBD->TASKS_EP0STALL = 1;
 | |
|   }else
 | |
|   {
 | |
|     NRF_USBD->EPSTALL = (USBD_EPSTALL_STALL_Stall << USBD_EPSTALL_STALL_Pos) | ep_addr;
 | |
|   }
 | |
| 
 | |
|   __ISB(); __DSB();
 | |
| }
 | |
| 
 | |
| void dcd_edpt_clear_stall (uint8_t rhport, uint8_t ep_addr)
 | |
| {
 | |
|   (void) rhport;
 | |
|   if ( ep_addr )
 | |
|   {
 | |
|     NRF_USBD->EPSTALL = (USBD_EPSTALL_STALL_UnStall << USBD_EPSTALL_STALL_Pos) | ep_addr;
 | |
|   }
 | |
| }
 | |
| 
 | |
| bool dcd_edpt_busy (uint8_t rhport, uint8_t ep_addr)
 | |
| {
 | |
|   (void) rhport;
 | |
| 
 | |
|   // USBD shouldn't check control endpoint state
 | |
|   if ( 0 == ep_addr ) return false;
 | |
| 
 | |
|   uint8_t const epnum = edpt_number(ep_addr);
 | |
|   uint8_t const dir   = edpt_dir(ep_addr);
 | |
| 
 | |
|   nom_xfer_t* xfer = get_td(epnum, dir);
 | |
| 
 | |
|   return xfer->actual_len < xfer->total_len;
 | |
| }
 | |
| 
 | |
| /*------------------------------------------------------------------*/
 | |
| /*
 | |
|  *------------------------------------------------------------------*/
 | |
| void USBD_IRQHandler(void)
 | |
| {
 | |
|   uint32_t const inten  = NRF_USBD->INTEN;
 | |
|   uint32_t int_status = 0;
 | |
| 
 | |
|   volatile uint32_t* regclr = &NRF_USBD->EVENTS_USBRESET;
 | |
| 
 | |
|   for(int i=0; i<32; i++)
 | |
|   {
 | |
|     if ( BIT_TEST_(inten, i) && regclr[i]  )
 | |
|     {
 | |
|       int_status |= BIT_(i);
 | |
| 
 | |
|       // nrf_usbd_event_clear()
 | |
|       regclr[i] = 0;
 | |
| 
 | |
|       __ISB(); __DSB();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /*------------- Interrupt Processing -------------*/
 | |
|   if ( int_status & USBD_INTEN_USBRESET_Msk )
 | |
|   {
 | |
|     bus_reset();
 | |
| 
 | |
|     dcd_bus_event(0, USBD_BUS_EVENT_RESET);
 | |
|   }
 | |
| 
 | |
|   if ( int_status & EDPT_END_ALL_MASK )
 | |
|   {
 | |
|     // DMA complete move data from SRAM -> Endpoint
 | |
|     edpt_dma_end();
 | |
|   }
 | |
| 
 | |
|   /*------------- Control Transfer -------------*/
 | |
|   if ( int_status & USBD_INTEN_EP0SETUP_Msk )
 | |
|   {
 | |
|     uint8_t setup[8] = {
 | |
|         NRF_USBD->BMREQUESTTYPE, NRF_USBD->BREQUEST, NRF_USBD->WVALUEL, NRF_USBD->WVALUEH,
 | |
|         NRF_USBD->WINDEXL, NRF_USBD->WINDEXH, NRF_USBD->WLENGTHL, NRF_USBD->WLENGTHH
 | |
|     };
 | |
| 
 | |
|     dcd_setup_received(0, setup);
 | |
|   }
 | |
| 
 | |
|   if ( int_status & USBD_INTEN_EP0DATADONE_Msk )
 | |
|   {
 | |
|     if ( _dcd.control.dir == TUSB_DIR_OUT )
 | |
|     {
 | |
|       // OUT data from Host -> Endpoint
 | |
|       // Trigger DMA to move Endpoint -> SRAM
 | |
|       edpt_dma_start(0, TUSB_DIR_OUT);
 | |
|     }else
 | |
|     {
 | |
|       // IN: data transferred from Endpoint -> Host
 | |
|       if ( _dcd.control.len > 0 )
 | |
|       {
 | |
|         control_xact_start();
 | |
|       }else
 | |
|       {
 | |
|         // Control IN complete
 | |
|         dcd_xfer_complete(0, 0, 0, true);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if ( int_status & USBD_INTEN_ENDEPOUT0_Msk)
 | |
|   {
 | |
|     // OUT data moved from Endpoint -> SRAM
 | |
|     if ( _dcd.control.len > 0 )
 | |
|     {
 | |
|       control_xact_start();
 | |
|     }else
 | |
|     {
 | |
|       // Control OUT complete
 | |
|       dcd_xfer_complete(0, 0, 0, true);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /*------------- Bulk/Interrupt Transfer -------------*/
 | |
|   if ( int_status & USBD_INTEN_EPDATA_Msk)
 | |
|   {
 | |
|     uint32_t data_status = NRF_USBD->EPDATASTATUS;
 | |
| 
 | |
|     nrf_usbd_epdatastatus_clear(data_status);
 | |
| 
 | |
|     // In: data from Endpoint -> Host
 | |
|     for(uint8_t epnum=1; epnum<8; epnum++)
 | |
|     {
 | |
|       if ( BIT_TEST_(data_status, epnum ) )
 | |
|       {
 | |
|         nom_xfer_t* xfer = get_td(epnum, TUSB_DIR_IN);
 | |
| 
 | |
|         xfer->actual_len += NRF_USBD->EPIN[epnum].MAXCNT;
 | |
| 
 | |
|         if ( xfer->actual_len < xfer->total_len )
 | |
|         {
 | |
|           // more to xfer
 | |
|           normal_xact_start(epnum, TUSB_DIR_IN);
 | |
|         } else
 | |
|         {
 | |
|           // BULK/INT IN complete
 | |
|           dcd_xfer_complete(0, epnum | TUSB_DIR_IN_MASK, xfer->actual_len, true);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // OUT: data from Host -> Endpoint
 | |
|     for(uint8_t epnum=1; epnum<8; epnum++)
 | |
|     {
 | |
|       if ( BIT_TEST_(data_status, 16+epnum ) )
 | |
|       {
 | |
|         nom_xfer_t* xfer = get_td(epnum, TUSB_DIR_OUT);
 | |
| 
 | |
|         uint8_t const xact_len = NRF_USBD->SIZE.EPOUT[epnum];
 | |
| 
 | |
|         // Trigger DMA move data from Endpoint -> SRAM
 | |
|         NRF_USBD->EPOUT[epnum].PTR    = (uint32_t) xfer->buffer;
 | |
|         NRF_USBD->EPOUT[epnum].MAXCNT = xact_len;
 | |
| 
 | |
|         edpt_dma_start(epnum, TUSB_DIR_OUT);
 | |
| 
 | |
|         xfer->buffer     += xact_len;
 | |
|         xfer->actual_len += xact_len;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // OUT: data from DMA -> SRAM
 | |
|   for(uint8_t epnum=1; epnum<8; epnum++)
 | |
|   {
 | |
|     if ( BIT_TEST_(int_status, USBD_INTEN_ENDEPOUT0_Pos+epnum) )
 | |
|     {
 | |
|       nom_xfer_t* xfer = get_td(epnum, TUSB_DIR_OUT);
 | |
| 
 | |
|       uint8_t const xact_len = NRF_USBD->EPOUT[epnum].AMOUNT;
 | |
| 
 | |
|       // Transfer complete if transaction len < Max Packet Size or total len is transferred
 | |
|       if ( (xact_len == xfer->mps) && (xfer->actual_len < xfer->total_len) )
 | |
|       {
 | |
|         // Prepare for more data from Host -> Endpoint
 | |
|         normal_xact_start(epnum, TUSB_DIR_OUT);
 | |
|       }else
 | |
|       {
 | |
|         xfer->total_len = xfer->actual_len;
 | |
| 
 | |
|         // BULK/INT OUT complete
 | |
|         dcd_xfer_complete(0, epnum, xfer->actual_len, true);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
| 
 | |
|   // SOF interrupt
 | |
|   if ( int_status & USBD_INTEN_SOF_Msk )
 | |
|   {
 | |
| #if 0
 | |
|     // FIXME Errata 104 The EPDATA event might not be generated, and the related update of EPDATASTATUS does not occur.
 | |
|     // There is no way for software to tell if a xfer is complete or not.
 | |
|     // Walkaround: we will asssume an non-control IN transfer is always complete after 10 frames
 | |
|     if ( nrf_drv_usbd_errata_104() )
 | |
|     {
 | |
|       // Check all the queued IN transfer, retire all transfer if 10 frames has passed
 | |
|       for (int ep=1; ep<= 8; ep++)
 | |
|       {
 | |
|         nom_xfer_t* xfer = get_td(ep, TUSB_DIR_IN);
 | |
| 
 | |
|         if (xfer->actual_len < xfer->total_len)
 | |
|         {
 | |
|           uint16_t diff = (uint16_t) NRF_USBD->FRAMECNTR;
 | |
| 
 | |
|           if ( diff > xfer->frame_num )
 | |
|           {
 | |
|             diff -= xfer->frame_num;
 | |
|           }else
 | |
|           {
 | |
|             diff = (diff + 1024) - xfer->frame_num; // Frame counter cap at 1024
 | |
|           }
 | |
| 
 | |
|           // Walkaround, mark this transfer as complete
 | |
|           if (diff > 10)
 | |
|           {
 | |
|             xfer->actual_len = xfer->total_len;
 | |
|             dcd_xfer_complete(0, ep | TUSB_DIR_IN_MASK, xfer->actual_len, true);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| #endif
 | |
| 
 | |
|     dcd_bus_event(0, USBD_BUS_EVENT_SOF);
 | |
|   }
 | |
| 
 | |
| 
 | |
| }
 | |
| 
 | |
| #endif
 | 
