nxp-mcuxpresso / mcux-sdk

MCUXpresso SDK
BSD 3-Clause "New" or "Revised" License
301 stars 136 forks source link

[BUG] DMA channel functions are not thread-safe #173

Closed JonathonReinhart closed 4 months ago

JonathonReinhart commented 4 months ago

Background

While access to global DMA controller configuration should of course be single-threaded, it is reasonable to expect that configuration of individual channels can happen safely on different threads. For example, I might have one FreeRTOS thread managing a SPI interface while another thread deals with I2C.

Most of the per-channel configuration happens in per-channel registers (DMA_Type.CHANNEL[i]), so as long as only one thread manages one channel, there is no concern here.

But there are also per-channel bits in a few registers in the common (DMA_Type.COMMON) area, e.g. DMA_Type.COMMON[0].ENABLESET) Accessing these bits with a read-modify-write sequence is racy and could potentially inadvertently re-set bits which were recently cleared. That's why either:

Problem

Some DMA functions use |= in an attempt to only set the desired bit. As described above, this is unnecessary and can actually set bits which were cleared on another thread.

E.g. DMA_EnableChannel():

https://github.com/nxp-mcuxpresso/mcux-sdk/blob/cf224e2df8c2d8ae5c4f27743259803c365a6a60/drivers/lpc_dma/fsl_dma.h#L405-L410

This will compile to a read-modify-write sequence like this:

ldr     r1, DMA0_ENABLESET0
ldr     r2, [r1]
orr     r2, r2, #1
; ----
str     r2, [r1]

If this thread is preempted on the indicated line, and another thread clears bits in the register, they will be re-set when this thread resumes.

To Reproduce I haven't actually observed this yet.

Solution

The code should not care about the current value of the registers. (For write-only registers, it is all zero anyway.)

It should simply write a 1 in the bit to be modified, and let the SET/CLR semantics do the right thing.

zejiang0jason commented 4 months ago

Hi @JonathonReinhart , you are right, the |= shall be =. Thanks for point out this. Will fix.

JonathonReinhart commented 4 months ago

Hi @JonathonReinhart , you are right, the |= shall be =. Thanks for point out this. Will fix.

Or use the DMA_COMMON_REG_SET macro, e.g.:

DMA_COMMON_REG_SET(base, channel, ENABLESET, 1UL << DMA_CHANNEL_INDEX(base, channel));
JonathonReinhart commented 4 months ago

Here's my fix: #174

JonathonReinhart commented 4 months ago

@zejiang0jason I've tested #174 in my target application but haven't evaluated in any of the EVK sample apps. If you could help test, that would be great.