Skip to content

Commit 8b37aa1

Browse files
committed
nrf5x: Fix DMA access
There were two problems: - dma_running flag could be checked in USB interrupt (not set yet) then higher priority interrupt could start transfer, check dma_running (not set yet) set it to true start DMA; then when USB interrupt continues it starts another DMA that is not allowed - when DMA is started some registers can't be safely accessed, read can yield invalid values (SIZE.EPOUT, SIZE.EPISO) current implementation could start DMA for one OUT endpoint then check that another endpoint also has data and while DMA was not started right away, SIZE.EPOUT was copied already to MAXCNT register. Later on when DMA was started not all data was read from endpoint due to incorrect DMA size previously set. To prevent both cases dma_running is changed in atomic way. Only code that actually set this value to true starts DMA, code that tried and had dma_running flag already set simply defers DMA start to USB task. This eliminates also need to have mutex that was there to limit access to dma_running flag to one task only. transfer also now has started flag that is set only after dcd_edpt_xfer() sets up total_len and actua_len. Previously USB interrupt was disabled when total_len and actual_len were setup to prevent race condition when data arrived to ednpoint before transfer was setup was finished.
1 parent b6a8d0d commit 8b37aa1

File tree

1 file changed

+37
-56
lines changed

1 file changed

+37
-56
lines changed

src/portable/nordic/nrf5x/dcd_nrf5x.c

Lines changed: 37 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
#if CFG_TUD_ENABLED && CFG_TUSB_MCU == OPT_MCU_NRF5X
3030

31+
#include <stdatomic.h>
3132
#include "nrf.h"
3233
#include "nrf_clock.h"
3334
#include "nrf_power.h"
@@ -72,16 +73,14 @@ typedef struct
7273
// nRF will auto accept OUT packet after DMA is done
7374
// indicate packet is already ACK
7475
volatile bool data_received;
76+
volatile bool started;
7577

7678
// Set to true when data was transferred from RAM to ISO IN output buffer.
7779
// New data can be put in ISO IN output buffer after SOF.
7880
bool iso_in_transfer_ready;
7981

8082
} xfer_td_t;
8183

82-
static osal_mutex_def_t dcd_mutex_def;
83-
static osal_mutex_t dcd_mutex;
84-
8584
// Data for managing dcd
8685
static struct
8786
{
@@ -90,7 +89,7 @@ static struct
9089
xfer_td_t xfer[EP_CBI_COUNT + 1][2];
9190

9291
// nRF can only carry one DMA at a time, this is used to guard the access to EasyDMA
93-
volatile bool dma_running;
92+
atomic_bool dma_running;
9493
}_dcd;
9594

9695
/*------------------------------------------------------------------*/
@@ -121,8 +120,6 @@ TU_ATTR_ALWAYS_INLINE static inline bool is_in_isr(void)
121120
// helper to start DMA
122121
static void start_dma(volatile uint32_t* reg_startep)
123122
{
124-
_dcd.dma_running = true;
125-
126123
(*reg_startep) = 1;
127124
__ISB(); __DSB();
128125

@@ -131,58 +128,26 @@ static void start_dma(volatile uint32_t* reg_startep)
131128
// Therefore dma_pending is corrected right away
132129
if ( (reg_startep == &NRF_USBD->TASKS_EP0STATUS) || (reg_startep == &NRF_USBD->TASKS_EP0RCVOUT) )
133130
{
134-
_dcd.dma_running = false;
131+
atomic_flag_clear(&_dcd.dma_running);
135132
}
136133
}
137134

138-
// only 1 EasyDMA can be active at any time
139-
// TODO use Cortex M4 LDREX and STREX command (atomic) to have better mutex access to EasyDMA
140-
// since current implementation does not 100% guarded against race condition
141135
static void edpt_dma_start(volatile uint32_t* reg_startep)
142136
{
143-
// Called in critical section i.e within USB ISR, or USB/Global interrupt disabled
144-
if ( is_in_isr() || __get_PRIMASK() || !NVIC_GetEnableIRQ(USBD_IRQn) )
137+
if ( atomic_flag_test_and_set(&_dcd.dma_running) )
145138
{
146-
if (_dcd.dma_running)
147-
{
148-
//use usbd task to defer later
149-
usbd_defer_func((osal_task_func_t) edpt_dma_start, (void*) (uintptr_t) reg_startep, true);
150-
}else
151-
{
152-
start_dma(reg_startep);
153-
}
139+
usbd_defer_func((osal_task_func_t) edpt_dma_start, (void*) (uintptr_t) reg_startep, true);
154140
}else
155141
{
156-
// Called in non-critical thread-mode, should be 99% of the time.
157-
// Should be safe to blocking wait until previous DMA transfer complete
158-
uint8_t const rhport = 0;
159-
bool started = false;
160-
osal_mutex_lock(dcd_mutex, OSAL_TIMEOUT_WAIT_FOREVER);
161-
while(!started)
162-
{
163-
// LDREX/STREX may be needed in form of std atomic (required C11) or
164-
// use osal mutex to guard against multiple core MCUs such as nRF53
165-
dcd_int_disable(rhport);
166-
167-
if ( !_dcd.dma_running )
168-
{
169-
start_dma(reg_startep);
170-
started = true;
171-
}
172-
173-
dcd_int_enable(rhport);
174-
175-
// osal_yield();
176-
}
177-
osal_mutex_unlock(dcd_mutex);
142+
start_dma(reg_startep);
178143
}
179144
}
180145

181146
// DMA is complete
182147
static void edpt_dma_end(void)
183148
{
184149
TU_ASSERT(_dcd.dma_running, );
185-
_dcd.dma_running = false;
150+
atomic_flag_clear(&_dcd.dma_running);
186151
}
187152

188153
// helper getting td
@@ -191,27 +156,42 @@ static inline xfer_td_t* get_td(uint8_t epnum, uint8_t dir)
191156
return &_dcd.xfer[epnum][dir];
192157
}
193158

159+
static void xact_out_dma(uint8_t epnum);
160+
// Function wraps xact_out_dma which wants uint8_t while usbd_defer_func wants void (*)(void *)
161+
static void xact_out_dma_wrapper(void *epnum)
162+
{
163+
xact_out_dma((uint8_t)((uintptr_t)epnum));
164+
}
165+
194166
// Start DMA to move data from Endpoint -> RAM
195167
static void xact_out_dma(uint8_t epnum)
196168
{
197169
xfer_td_t* xfer = get_td(epnum, TUSB_DIR_OUT);
198170
uint32_t xact_len;
199171

172+
// DMA can't be active during read of SIZE.EPOUT or SIZE.ISOOUT, so try to lock,
173+
// If already running deffer call regardless if it was called from ISR or task,
174+
if ( atomic_flag_test_and_set(&_dcd.dma_running) )
175+
{
176+
usbd_defer_func((osal_task_func_t)xact_out_dma_wrapper, (void *)(uint32_t)epnum, is_in_isr());
177+
return;
178+
}
200179
if (epnum == EP_ISO_NUM)
201180
{
202181
xact_len = NRF_USBD->SIZE.ISOOUT;
203182
// If ZERO bit is set, ignore ISOOUT length
204183
if (xact_len & USBD_SIZE_ISOOUT_ZERO_Msk)
205184
{
206185
xact_len = 0;
186+
atomic_flag_clear(&_dcd.dma_running);
207187
}
208188
else
209189
{
210190
// Trigger DMA move data from Endpoint -> SRAM
211191
NRF_USBD->ISOOUT.PTR = (uint32_t) xfer->buffer;
212192
NRF_USBD->ISOOUT.MAXCNT = xact_len;
213193

214-
edpt_dma_start(&NRF_USBD->TASKS_STARTISOOUT);
194+
start_dma(&NRF_USBD->TASKS_STARTISOOUT);
215195
}
216196
}
217197
else
@@ -223,7 +203,7 @@ static void xact_out_dma(uint8_t epnum)
223203
NRF_USBD->EPOUT[epnum].PTR = (uint32_t) xfer->buffer;
224204
NRF_USBD->EPOUT[epnum].MAXCNT = xact_len;
225205

226-
edpt_dma_start(&NRF_USBD->TASKS_STARTEPOUT[epnum]);
206+
start_dma(&NRF_USBD->TASKS_STARTEPOUT[epnum]);
227207
}
228208
}
229209

@@ -248,7 +228,6 @@ static void xact_in_dma(uint8_t epnum)
248228
void dcd_init (uint8_t rhport)
249229
{
250230
TU_LOG1("dcd init\r\n");
251-
dcd_mutex = osal_mutex_create(&dcd_mutex_def);
252231
(void) rhport;
253232
}
254233

@@ -471,17 +450,10 @@ bool dcd_edpt_xfer (uint8_t rhport, uint8_t ep_addr, uint8_t * buffer, uint16_t
471450

472451
xfer_td_t* xfer = get_td(epnum, dir);
473452

474-
if (!is_in_isr()) {
475-
osal_mutex_lock(dcd_mutex, OSAL_TIMEOUT_WAIT_FOREVER);
476-
dcd_int_disable(rhport);
477-
}
453+
TU_ASSERT(!xfer->started);
478454
xfer->buffer = buffer;
479455
xfer->total_len = total_bytes;
480456
xfer->actual_len = 0;
481-
if (!is_in_isr()) {
482-
dcd_int_enable(rhport);
483-
osal_mutex_unlock(dcd_mutex);
484-
}
485457

486458
// Control endpoint with zero-length packet and opposite direction to 1st request byte --> status stage
487459
bool const control_status = (epnum == 0 && total_bytes == 0 && dir != tu_edpt_dir(NRF_USBD->BMREQUESTTYPE));
@@ -496,13 +468,20 @@ bool dcd_edpt_xfer (uint8_t rhport, uint8_t ep_addr, uint8_t * buffer, uint16_t
496468
}
497469
else if ( dir == TUSB_DIR_OUT )
498470
{
471+
xfer->started = true;
499472
if ( epnum == 0 )
500473
{
501474
// Accept next Control Out packet. TASKS_EP0RCVOUT also require EasyDMA
502475
edpt_dma_start(&NRF_USBD->TASKS_EP0RCVOUT);
503476
}else
504477
{
505-
if ( xfer->data_received && xfer->total_len > xfer->actual_len)
478+
// started just set, it could start DMA transfer if interrupt was trigger after this line
479+
// code only needs to start transfer (from Endpoint to RAM) when data_received was set
480+
// before started was set. If started is NOT set but data_received is, it means that
481+
// current transfer was already finished and next data is already present in endpoint and
482+
// can be consumed by future transfer
483+
__ISB(); __DSB();
484+
if ( xfer->data_received && xfer->started )
506485
{
507486
// Data is already received previously
508487
// start DMA to copy to SRAM
@@ -804,7 +783,9 @@ void dcd_int_handler(uint8_t rhport)
804783
}
805784
}else
806785
{
786+
TU_ASSERT(xfer->started,);
807787
xfer->total_len = xfer->actual_len;
788+
xfer->started = false;
808789

809790
// CBI OUT complete
810791
dcd_event_xfer_complete(0, epnum, xfer->actual_len, XFER_RESULT_SUCCESS, true);
@@ -857,7 +838,7 @@ void dcd_int_handler(uint8_t rhport)
857838
{
858839
xfer_td_t* xfer = get_td(epnum, TUSB_DIR_OUT);
859840

860-
if (xfer->actual_len < xfer->total_len)
841+
if ( xfer->started && xfer->actual_len < xfer->total_len )
861842
{
862843
xact_out_dma(epnum);
863844
}else

0 commit comments

Comments
 (0)