fix nrf easy dma race condition

This commit is contained in:
hathach 2021-11-23 09:36:28 +07:00
parent ae73873b5c
commit a994540860
2 changed files with 71 additions and 67 deletions

View File

@ -80,7 +80,7 @@ typedef struct
//--------------------------------------------------------------------+ //--------------------------------------------------------------------+
CFG_TUSB_MEM_SECTION static cdcd_interface_t _cdcd_itf[CFG_TUD_CDC]; CFG_TUSB_MEM_SECTION static cdcd_interface_t _cdcd_itf[CFG_TUD_CDC];
static void _prep_out_transaction (cdcd_interface_t* p_cdc) static bool _prep_out_transaction (cdcd_interface_t* p_cdc)
{ {
uint8_t const rhport = TUD_OPT_RHPORT; uint8_t const rhport = TUD_OPT_RHPORT;
uint16_t available = tu_fifo_remaining(&p_cdc->rx_ff); uint16_t available = tu_fifo_remaining(&p_cdc->rx_ff);
@ -89,21 +89,23 @@ static void _prep_out_transaction (cdcd_interface_t* p_cdc)
// TODO Actually we can still carry out the transfer, keeping count of received bytes // TODO Actually we can still carry out the transfer, keeping count of received bytes
// and slowly move it to the FIFO when read(). // and slowly move it to the FIFO when read().
// This pre-check reduces endpoint claiming // This pre-check reduces endpoint claiming
TU_VERIFY(available >= sizeof(p_cdc->epout_buf), ); TU_VERIFY(available >= sizeof(p_cdc->epout_buf));
// claim endpoint // claim endpoint
TU_VERIFY(usbd_edpt_claim(rhport, p_cdc->ep_out), ); TU_VERIFY(usbd_edpt_claim(rhport, p_cdc->ep_out));
// fifo can be changed before endpoint is claimed // fifo can be changed before endpoint is claimed
available = tu_fifo_remaining(&p_cdc->rx_ff); available = tu_fifo_remaining(&p_cdc->rx_ff);
if ( available >= sizeof(p_cdc->epout_buf) ) if ( available >= sizeof(p_cdc->epout_buf) )
{ {
usbd_edpt_xfer(rhport, p_cdc->ep_out, p_cdc->epout_buf, sizeof(p_cdc->epout_buf)); return usbd_edpt_xfer(rhport, p_cdc->ep_out, p_cdc->epout_buf, sizeof(p_cdc->epout_buf));
}else }else
{ {
// Release endpoint since we don't make any transfer // Release endpoint since we don't make any transfer
usbd_edpt_release(rhport, p_cdc->ep_out); usbd_edpt_release(rhport, p_cdc->ep_out);
return false;
} }
} }

View File

@ -84,8 +84,6 @@ static struct
// Number of pending DMA that is started but not handled yet by dcd_int_handler(). // Number of pending DMA that is started but not handled yet by dcd_int_handler().
// Since nRF can only carry one DMA can run at a time, this value is normally be either 0 or 1. // Since nRF can only carry one DMA can run at a time, this value is normally be either 0 or 1.
// However, in critical section with interrupt disabled, the DMA can be finished and added up
// until handled by dcd_int_handler() when exiting critical section.
volatile uint8_t dma_pending; volatile uint8_t dma_pending;
}_dcd; }_dcd;
@ -115,67 +113,68 @@ TU_ATTR_ALWAYS_INLINE static inline bool is_in_isr(void)
} }
// helper to start DMA // helper to start DMA
static void start_dma(volatile uint32_t* reg_startep)
{
_dcd.dma_pending = true;
(*reg_startep) = 1;
__ISB(); __DSB();
// TASKS_EP0STATUS, TASKS_EP0RCVOUT seem to need EasyDMA to be available
// However these don't trigger any DMA transfer and got ENDED event subsequently
// Therefore dma_pending is corrected right away
if ( (reg_startep == &NRF_USBD->TASKS_EP0STATUS) || (reg_startep == &NRF_USBD->TASKS_EP0RCVOUT) )
{
_dcd.dma_pending = false;
}
}
// only 1 EasyDMA can be active at any time
// TODO use Cortex M4 LDREX and STREX command (atomic) to have better mutex access to EasyDMA // TODO use Cortex M4 LDREX and STREX command (atomic) to have better mutex access to EasyDMA
// since current implementation does not 100% guarded against race condition // since current implementation does not 100% guarded against race condition
static void edpt_dma_start(volatile uint32_t* reg_startep) static void edpt_dma_start(volatile uint32_t* reg_startep)
{ {
// Only one dma can be active // Called in critical section i.e within USB ISR, or USB/Global interrupt disabled
if ( _dcd.dma_pending ) if ( is_in_isr() || __get_PRIMASK() || !NVIC_GetEnableIRQ(USBD_IRQn) )
{ {
if (is_in_isr()) if (_dcd.dma_pending)
{ {
// Called within ISR, use usbd task to defer later //use usbd task to defer later
usbd_defer_func((osal_task_func_t) edpt_dma_start, (void*) (uintptr_t) reg_startep, true); usbd_defer_func((osal_task_func_t) edpt_dma_start, (void*) (uintptr_t) reg_startep, true);
return; }else
}
else
{ {
if ( __get_PRIMASK() || !NVIC_GetEnableIRQ(USBD_IRQn) ) start_dma(reg_startep);
{
// Called in critical section with interrupt disabled. We have to manually check
// for the DMA complete by comparing current pending DMA with number of ENDED Events
uint32_t ended = 0;
while ( _dcd.dma_pending > ((uint8_t) ended) )
{
ended = NRF_USBD->EVENTS_ENDISOIN + NRF_USBD->EVENTS_ENDISOOUT;
for (uint8_t i=0; i<EP_CBI_COUNT; i++)
{
ended += NRF_USBD->EVENTS_ENDEPIN[i] + NRF_USBD->EVENTS_ENDEPOUT[i];
}
} }
}else }else
{ {
// Called in non-critical thread-mode, should be 99% of the time. // Called in non-critical thread-mode, should be 99% of the time.
// Should be safe to blocking wait until previous DMA transfer complete // Should be safe to blocking wait until previous DMA transfer complete
while ( _dcd.dma_pending ) { } uint8_t const rhport = 0;
} bool started = false;
} while(!started)
{
// LDREX/STREX may be needed in form of std atomic (required C11) or
// use osal mutex to guard against multiple core MCUs such as nRF53
dcd_int_disable(rhport);
if ( !_dcd.dma_pending )
{
start_dma(reg_startep);
started = true;
} }
_dcd.dma_pending++; dcd_int_enable(rhport);
(*reg_startep) = 1; // osal_yield();
__ISB(); __DSB(); }
}
} }
// DMA is complete // DMA is complete
static void edpt_dma_end(void) static void edpt_dma_end(void)
{ {
TU_ASSERT(_dcd.dma_pending, ); TU_ASSERT(_dcd.dma_pending, );
_dcd.dma_pending = 0; _dcd.dma_pending = false;
}
// helper to set TASKS_EP0STATUS / TASKS_EP0RCVOUT since they also need EasyDMA
// However TASKS_EP0STATUS doesn't trigger any DMA transfer and got ENDED event subsequently
// Therefore dma_running state will be corrected right away
void start_ep0_task(volatile uint32_t* reg_task)
{
edpt_dma_start(reg_task);
// correct the dma_running++ in dma start
if (_dcd.dma_pending) _dcd.dma_pending--;
} }
// helper getting td // helper getting td
@ -194,7 +193,10 @@ static void xact_out_dma(uint8_t epnum)
{ {
xact_len = NRF_USBD->SIZE.ISOOUT; xact_len = NRF_USBD->SIZE.ISOOUT;
// If ZERO bit is set, ignore ISOOUT length // If ZERO bit is set, ignore ISOOUT length
if (xact_len & USBD_SIZE_ISOOUT_ZERO_Msk) xact_len = 0; if (xact_len & USBD_SIZE_ISOOUT_ZERO_Msk)
{
xact_len = 0;
}
else else
{ {
// Trigger DMA move data from Endpoint -> SRAM // Trigger DMA move data from Endpoint -> SRAM
@ -216,8 +218,8 @@ static void xact_out_dma(uint8_t epnum)
edpt_dma_start(&NRF_USBD->TASKS_STARTEPOUT[epnum]); edpt_dma_start(&NRF_USBD->TASKS_STARTEPOUT[epnum]);
} }
xfer->buffer += xact_len; // xfer->buffer += xact_len;
xfer->actual_len += xact_len; // xfer->actual_len += xact_len;
} }
// Prepare for a CBI transaction IN, call at the start // Prepare for a CBI transaction IN, call at the start
@ -232,7 +234,7 @@ static void xact_in_dma(uint8_t epnum)
NRF_USBD->EPIN[epnum].PTR = (uint32_t) xfer->buffer; NRF_USBD->EPIN[epnum].PTR = (uint32_t) xfer->buffer;
NRF_USBD->EPIN[epnum].MAXCNT = xact_len; NRF_USBD->EPIN[epnum].MAXCNT = xact_len;
xfer->buffer += xact_len; //xfer->buffer += xact_len;
edpt_dma_start(&NRF_USBD->TASKS_STARTEPIN[epnum]); edpt_dma_start(&NRF_USBD->TASKS_STARTEPIN[epnum]);
} }
@ -242,6 +244,7 @@ static void xact_in_dma(uint8_t epnum)
//--------------------------------------------------------------------+ //--------------------------------------------------------------------+
void dcd_init (uint8_t rhport) void dcd_init (uint8_t rhport)
{ {
TU_LOG1("dcd init\r\n");
(void) rhport; (void) rhport;
} }
@ -466,7 +469,7 @@ bool dcd_edpt_xfer (uint8_t rhport, uint8_t ep_addr, uint8_t * buffer, uint16_t
if ( control_status ) if ( control_status )
{ {
// Status Phase also requires EasyDMA has to be available as well !!!! // Status Phase also requires EasyDMA has to be available as well !!!!
start_ep0_task(&NRF_USBD->TASKS_EP0STATUS); edpt_dma_start(&NRF_USBD->TASKS_EP0STATUS);
// The nRF doesn't interrupt on status transmit so we queue up a success response. // The nRF doesn't interrupt on status transmit so we queue up a success response.
dcd_event_xfer_complete(0, ep_addr, 0, XFER_RESULT_SUCCESS, is_in_isr()); dcd_event_xfer_complete(0, ep_addr, 0, XFER_RESULT_SUCCESS, is_in_isr());
@ -476,7 +479,7 @@ bool dcd_edpt_xfer (uint8_t rhport, uint8_t ep_addr, uint8_t * buffer, uint16_t
if ( epnum == 0 ) if ( epnum == 0 )
{ {
// Accept next Control Out packet. TASKS_EP0RCVOUT also require EasyDMA // Accept next Control Out packet. TASKS_EP0RCVOUT also require EasyDMA
start_ep0_task(&NRF_USBD->TASKS_EP0RCVOUT); edpt_dma_start(&NRF_USBD->TASKS_EP0RCVOUT);
}else }else
{ {
if ( xfer->data_received ) if ( xfer->data_received )
@ -522,7 +525,6 @@ void dcd_edpt_stall (uint8_t rhport, uint8_t ep_addr)
// There maybe data in endpoint fifo already, we need to pull it out // There maybe data in endpoint fifo already, we need to pull it out
if ( (dir == TUSB_DIR_OUT) && xfer->data_received ) if ( (dir == TUSB_DIR_OUT) && xfer->data_received )
{ {
TU_LOG_LOCATION();
xfer->data_received = false; xfer->data_received = false;
xact_out_dma(epnum); xact_out_dma(epnum);
} }
@ -717,7 +719,8 @@ void dcd_int_handler(uint8_t rhport)
if ( int_status & EDPT_END_ALL_MASK ) if ( int_status & EDPT_END_ALL_MASK )
{ {
// DMA complete move data from SRAM -> Endpoint // DMA complete move data from SRAM <-> Endpoint
// Must before endpoint transfer handling
edpt_dma_end(); edpt_dma_end();
} }
@ -732,7 +735,7 @@ void dcd_int_handler(uint8_t rhport)
* - Host -> Endpoint * - Host -> Endpoint
* EPDATA (or EP0DATADONE) interrupted, check EPDATASTATUS.EPOUT[i] * EPDATA (or EP0DATADONE) interrupted, check EPDATASTATUS.EPOUT[i]
* to start DMA. For Bulk/Interrupt, this step can occur automatically (without sw), * to start DMA. For Bulk/Interrupt, this step can occur automatically (without sw),
* which means data may or may not be ready (data_received flag). * which means data may or may not be ready (out_received flag).
* - Endpoint -> RAM * - Endpoint -> RAM
* ENDEPOUT[i] interrupted, transaction complete, sw prepare next transaction * ENDEPOUT[i] interrupted, transaction complete, sw prepare next transaction
* *
@ -764,20 +767,16 @@ void dcd_int_handler(uint8_t rhport)
xfer_td_t* xfer = get_td(epnum, TUSB_DIR_OUT); xfer_td_t* xfer = get_td(epnum, TUSB_DIR_OUT);
uint8_t const xact_len = NRF_USBD->EPOUT[epnum].AMOUNT; uint8_t const xact_len = NRF_USBD->EPOUT[epnum].AMOUNT;
xfer->buffer += xact_len;
xfer->actual_len += xact_len;
// Transfer complete if transaction len < Max Packet Size or total len is transferred // Transfer complete if transaction len < Max Packet Size or total len is transferred
if ( (epnum != EP_ISO_NUM) && (xact_len == xfer->mps) && (xfer->actual_len < xfer->total_len) ) if ( (epnum != EP_ISO_NUM) && (xact_len == xfer->mps) && (xfer->actual_len < xfer->total_len) )
{ {
if ( epnum == 0 ) if ( epnum == 0 )
{ {
// Accept next Control Out packet. TASKS_EP0RCVOUT also require EasyDMA // Accept next Control Out packet. TASKS_EP0RCVOUT also require EasyDMA
if ( _dcd.dma_pending ) edpt_dma_start(&NRF_USBD->TASKS_EP0RCVOUT);
{
// use usbd task to defer later
usbd_defer_func((osal_task_func_t) start_ep0_task, (void*) (uintptr_t) &NRF_USBD->TASKS_EP0RCVOUT, true);
}else
{
start_ep0_task(&NRF_USBD->TASKS_EP0RCVOUT);
}
}else }else
{ {
// nRF auto accept next Bulk/Interrupt OUT packet // nRF auto accept next Bulk/Interrupt OUT packet
@ -814,8 +813,10 @@ void dcd_int_handler(uint8_t rhport)
if ( tu_bit_test(data_status, epnum) || (epnum == 0 && is_control_in) ) if ( tu_bit_test(data_status, epnum) || (epnum == 0 && is_control_in) )
{ {
xfer_td_t* xfer = get_td(epnum, TUSB_DIR_IN); xfer_td_t* xfer = get_td(epnum, TUSB_DIR_IN);
uint8_t const xact_len = NRF_USBD->EPIN[epnum].AMOUNT; // MAXCNT
xfer->actual_len += NRF_USBD->EPIN[epnum].MAXCNT; xfer->buffer += xact_len;
xfer->actual_len += xact_len;
if ( xfer->actual_len < xfer->total_len ) if ( xfer->actual_len < xfer->total_len )
{ {
@ -842,6 +843,7 @@ void dcd_int_handler(uint8_t rhport)
}else }else
{ {
// Data overflow !!! Nah, nRF will auto accept next Bulk/Interrupt OUT packet // Data overflow !!! Nah, nRF will auto accept next Bulk/Interrupt OUT packet
// If USBD is already queued this
// Mark this endpoint with data received // Mark this endpoint with data received
xfer->data_received = true; xfer->data_received = true;
} }