Skip to content

Commit 737aa7d

Browse files
mjchen0fabiobaltieri
authored andcommitted
drivers: dma: dma_mcux_lpc: fix multiple bugs with continuous mode
There are multiple bugs related to continuous/circular mode. Continuous/circular mode is where the DMA runs continuously until the stop API is called, instead of auto-stopping on completion on a single transfer. After a stop, the DMA can then be reconfigured/restarted. 1. Fix bug where stop didn't actually stop. This can cause memory corruption if the user thought the DMA stopped and repurposed the dest memory, but in fact the DMA is still writing to it. The bug was due the incorrect usage of the DMA controller busy state. The DMA controller is busy only when a transfer is actively in progress, but the driver needed to stop even if the transfer is not active but is only enabled (and may become active on a subsequent trigger event). Change so that data->busy doesn't use the DMA controller busy state but tracks the enable state. Also, to make it doubly safe, make stop function always stop regardless of data->busy state because it is alway safe/correct to do so. 2. Fix race condition where a stop request from another ISR might race with a DMA completion interrupt, and the DMA completion callback gets invoked after the DMA has already been stopped. The fix is to unregister the callback with the sdk DMA driver, so the ISR still runs and clear the interrupt without invoking the callback. There is potentially still a race if the interrupt is restarted before the ISR fires, so the callback might be called too early. However, the Zephyr DMA driver doesn't have the channel level details that the SDK driver does and it cannot clear just the channel interrupt. Also a couple of general fixes/improvements: a. Use interrupt B for end of transfer (single transfer or end of block list). Use interrupt A for interrupts of a block in the middle of a transfer or for continuous/circular transfers. This fixes the dma callback so it can properly report DMA_STATUS_BLOCK vs DMA_STATUS_COMPLETE. b. Reorder some fields in struct channel_data to pack a little better in memory Signed-off-by: Mike J. Chen <[email protected]>
1 parent 4e0e3c9 commit 737aa7d

File tree

1 file changed

+40
-15
lines changed

1 file changed

+40
-15
lines changed

drivers/dma/dma_mcux_lpc.c

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ struct channel_data {
4444
const struct device *dev;
4545
void *user_data;
4646
dma_callback_t dma_callback;
47+
dma_descriptor_t *curr_descriptor;
48+
uint32_t width;
4749
enum dma_channel_direction dir;
4850
uint8_t src_inc;
4951
uint8_t dst_inc;
50-
dma_descriptor_t *curr_descriptor;
5152
uint8_t num_of_descriptors;
5253
bool descriptors_queued;
53-
uint32_t width;
5454
bool busy;
5555
};
5656

@@ -93,14 +93,20 @@ static void nxp_lpc_dma_callback(dma_handle_t *handle, void *param,
9393

9494
if (intmode == kDMA_IntError) {
9595
DMA_AbortTransfer(handle);
96+
data->busy = false;
9697
} else if (intmode == kDMA_IntA) {
98+
/* this is interrupt for a block that is not the
99+
* last so leave busy flag set.
100+
*/
97101
ret = DMA_STATUS_BLOCK;
98102
} else {
103+
/* this is interrupt for end of a block list
104+
* or a single transfer.
105+
*/
106+
data->busy = false;
99107
ret = DMA_STATUS_COMPLETE;
100108
}
101109

102-
data->busy = DMA_ChannelIsBusy(data->dma_handle.base, channel);
103-
104110
if (data->dma_callback) {
105111
data->dma_callback(data->dev, data->user_data, channel, ret);
106112
}
@@ -238,7 +244,7 @@ static int dma_mcux_lpc_queue_descriptors(struct channel_data *data,
238244
enable_b_interrupt = 0;
239245
} else {
240246
/* Use intB when this is the end of the block list and transfer */
241-
if (last_block) {
247+
if (last_block && !setup_extra_descriptor) {
242248
enable_a_interrupt = 0;
243249
enable_b_interrupt = 1;
244250
} else {
@@ -661,8 +667,8 @@ static int dma_mcux_lpc_configure(const struct device *dev, uint32_t channel,
661667
block_config->block_size);
662668
}
663669
} else {
664-
/* Enable interrupt for the descriptor */
665-
xfer_config = DMA_CHANNEL_XFER(0UL, 0UL, 1UL, 0UL,
670+
/* Enable interrupt for the descriptor. Use int_b */
671+
xfer_config = DMA_CHANNEL_XFER(0UL, 0UL, 0UL, 1UL,
666672
width,
667673
src_inc,
668674
dst_inc,
@@ -796,11 +802,16 @@ static int dma_mcux_lpc_start(const struct device *dev, uint32_t channel)
796802
struct dma_mcux_lpc_dma_data *dev_data = dev->data;
797803
int8_t virtual_channel = dev_data->channel_index[channel];
798804
struct channel_data *data = DEV_CHANNEL_DATA(dev, virtual_channel);
805+
dma_handle_t *p_handle = DEV_DMA_HANDLE(dev, virtual_channel);
799806

800807
LOG_DBG("START TRANSFER");
801808
LOG_DBG("DMA CTRL 0x%x", DEV_BASE(dev)->CTRL);
802809
data->busy = true;
803-
DMA_StartTransfer(DEV_DMA_HANDLE(dev, virtual_channel));
810+
/* In case of a restart after a stop, reinstall the DMA callback
811+
* that was removed by the stop.
812+
*/
813+
DMA_SetCallback(p_handle, nxp_lpc_dma_callback, (void *)data);
814+
DMA_StartTransfer(p_handle);
804815
return 0;
805816
}
806817

@@ -809,14 +820,28 @@ static int dma_mcux_lpc_stop(const struct device *dev, uint32_t channel)
809820
struct dma_mcux_lpc_dma_data *dev_data = dev->data;
810821
int8_t virtual_channel = dev_data->channel_index[channel];
811822
struct channel_data *data = DEV_CHANNEL_DATA(dev, virtual_channel);
823+
dma_handle_t *p_handle = DEV_DMA_HANDLE(dev, virtual_channel);
812824

813-
if (!data->busy) {
814-
return 0;
815-
}
816-
DMA_AbortTransfer(DEV_DMA_HANDLE(dev, virtual_channel));
817-
DMA_DisableChannel(DEV_BASE(dev), channel);
825+
/* Abort/disable even if not busy. it's safe and avoids
826+
* any risk of data->busy not being accurate.
827+
*/
828+
DMA_AbortTransfer(p_handle);
829+
DMA_DisableChannel(DEV_BASE(dev), p_handle->channel);
818830

819831
data->busy = false;
832+
833+
/* Handle race condition where if this is called from an ISR
834+
* and the DMA channel completion interrupt becomes pending
835+
* before we complete the abort and disable, then the DMA ISR
836+
* may run and invoke the callback may run after we return,
837+
* which is unexpected by user since from their perspective
838+
* the stop was already completed. We cannot just checking
839+
* and clear the pending IRQ since it is shared by other
840+
* channels. This way, the pending DMA ISR will still run,
841+
* but should just clear the interrupt and not invoke
842+
* the callback.
843+
*/
844+
DMA_SetCallback(p_handle, NULL, NULL);
820845
return 0;
821846
}
822847

@@ -837,8 +862,8 @@ static int dma_mcux_lpc_reload(const struct device *dev, uint32_t channel,
837862

838863
p_handle = DEV_DMA_HANDLE(dev, virtual_channel);
839864

840-
/* Only one buffer, enable interrupt */
841-
xfer_config = DMA_CHANNEL_XFER(0UL, 0UL, 1UL, 0UL,
865+
/* Only one buffer, enable interrupt b */
866+
xfer_config = DMA_CHANNEL_XFER(0UL, 0UL, 0UL, 1UL,
842867
data->width,
843868
data->src_inc,
844869
data->dst_inc,

0 commit comments

Comments
 (0)