Skip to main content
Sean Kelly
Associate
August 1, 2017
Solved

USART Rx with DMA: IDLE interrupt vs timer?

  • August 1, 2017
  • 15 replies
  • 4761 views
Posted on August 01, 2017 at 16:03

Hi Forum

I'm trying to implement USART Rx of variable, unknown length messages at a 1Mbps baud rate through DMA. I would like to minimize the latency from receiving to consuming the data, so I'm trying to do this with some sort of idle detection as opposed to having my consumer code periodically drain a buffer.

I can attach my code if necessary, but for right now I'm mostly looking for high-level answers on how this is best accomplished to make sure I'm on the right track.

My first attempt was to configure a buffer in DMA and enable the USART IDLE interrupt. In the USART interrupt handler, I first disable DMA (and wait for the enable bit to clear on the DMA stream) and then clear the IDLE flag (by reading the bit and then reading the data register). I copy the data out of the DMA buffer and then re-enable DMA in the interrupt to be ready for the next message.

Doing this gets mixed results -- I do get valid packets occasionally, but most often my received data falls into one or more of these failure categories:

  1. The valid data starts at byte 1 instead of byte 0 of the DMA buffer. The extra byte is very often a duplicate start byte. Where's the extra byte coming from? Should I be disabling the UART peripheral as well?
  2. Truncated message - A valid message is in the buffer, but it abruptly ends. It appears the IDLE interrupt might have come early, or the sender stalled (thus triggering IDLE)?
  3. Data underrun - NDTR says N bytes were received but memory inspection shows fewer than N bytes were actually touched. Do I need to wait a short while after the IDLE flag for DMA to complete before disabling the stream?

I've spent a lot of time searching for this online, and it seems the consensus is that using the IDLE interrupt is not the way to go, and I should be using a timer on the Rx pin (as described in AN3109). The reasons I have found are unclear, though. I'd like to understand more about why IDLE doesn't work for this, and what's the point of the IDLE interrupt if not for this sort of application?

Thanks

Sean

#uart-dma #usart #usart-idle #dma
This topic has been closed for replies.
Best answer by waclawek.jan
Posted on August 02, 2017 at 16:39

Oh, so the code (in the flat view I am using) appeared *above* rather than *below*... with a timestamp breaking the timeline... jive's code behind the moderation feature is ehm well how to say that politely... :|

In

void __attribute__((used)) USART6_IRQHandler(void)

Can we be sure that

Only have USART_IT_IDLE enabled

?

Also, does your scenario account for more than

RX_BUFFER_SIZE

bytes between IDLEs? Is your testing transmitter well-behaved, i.e. transmits only up to

RX_BUFFER_SIZE 

bytes and then long enough IDLEs?

What are the interrupt priorities? May these interrupt nest and if yes, how?

Other than that I see nothing suspicious. I don't use 'libraries' so can't say if there's nothing related to that.

JW

15 replies

waclawek.jan
Super User
August 1, 2017
Posted on August 01, 2017 at 17:07

Which STM32? How do you set the DMA?

The valid data starts at byte 1 instead of byte 0 of the DMA buffer.

RXNE set before DMA enabled, maybe from a previous communication? Read UART_DR before enabling DMA.

JW

Sean Kelly
Associate
August 2, 2017
Posted on August 02, 2017 at 15:14

I read UART_DR in the IDLE interrupt (it's required to clear the flag) and then re-enable DMA in the DMA TC interrupt that results. I'd hope that's not enough time for a new transaction to come in, but I guess I ought to be safe. 

Is it actually better to just disable the UART peripheral in the IDLE interrupt and only re-enable the peripheral when I'm ready for the DMA to get re-enabled too?

Code posted below.

waclawek.jan
Super User
August 2, 2017
Posted on August 02, 2017 at 15:26

Code posted below.

How far below? ;)

JW

Tesla DeLorean
Guru
August 1, 2017
Posted on August 01, 2017 at 17:43

I use DMA for a USART RX FIFO, I use 16-bit mode for the read/write, and tag the unused words as 0xFFFF, and periodically sweep/advance the the flushing from a singular thread (ie not creating race conditions between multiple interrupts to contend over the resource). The buffer is circular and of sufficient depth to guarantee service in the worker task/thread consuming the data. The data words are truncated to 8-bit at this point, and consumed words written as 0xFFFF in the buffer.

The 16-bit DMA from USART->RDR or DR has the top order 7-bits as zero, as the USART supports up to 9-bit data words.

Tips, Buy me a coffee, or three.. PayPal Venmo (See Profile) Up vote any posts that you find helpful, it shows what's working..
Sean Kelly
Associate
August 2, 2017
Posted on August 02, 2017 at 14:43

Thanks for the suggestion; this is a clever scheme that avoids needing to manage several head/tail pointers! I may end up doing something like this.

However, for the purpose of this post I'm more interested in understanding what I'm doing wrong than trying to find workarounds or alternate solutions.

Sean Kelly
Associate
August 2, 2017
Posted on August 02, 2017 at 15:12

Here's some code. Please let me know if more details are needed, but I think the below should be sufficient. DMA setup code followed by my interrupt handlers.

Sean

DMA setup:

#define UART6_RX_DMA_ALL_FLAGS (DMA_FLAG_FEIF1 | DMA_FLAG_DMEIF1 | \
 DMA_FLAG_TEIF1 | DMA_FLAG_HTIF1 | DMA_FLAG_TCIF1 )
// USART RX DMA Channel 
ConfigDMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART6->DR; 
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; 
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; 
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; 
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; 
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; 
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; 
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; 
DMA_InitStructure.DMA_Priority = DMA_Priority_High; 
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; 
DMA_InitStructure.DMA_Channel = DMA_Channel_5; 
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; 
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)RxBuffer;
DMA_InitStructure.DMA_BufferSize = RX_BUFFER_SIZE; 
DMA_Cmd(DMA2_Stream1, DISABLE); 
DMA_Init(DMA2_Stream1, &DMA_InitStructure);�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?
// Enable Rx DMA 
DMA_ITConfig(DMA2_Stream1, DMA_IT_TC, ENABLE); 
DMA_ClearFlag(DMA2_Stream1, UART6_RX_DMA_ALL_FLAGS); 
USART_DMACmd(USART6, USART_DMAReq_Rx, ENABLE); 
DMA_Cmd(DMA2_Stream1, ENABLE);�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?

Interrupt processing:

// Global to store the count of the data in the buffer
static volatile uint32_t rxBufferContentsSize = 0;
// RxBuffer for DMA
#define RX_BUFFER_SIZE 64
static uint8_t RxBuffer[RX_BUFFER_SIZE];
void __attribute__((used)) USART6_IRQHandler(void)
{
 // Only have USART_IT_IDLE enabled
 if (USART_GetITStatus(UARTSLK_TYPE, USART_IT_IDLE) == SET)
 {
 // Disable the DMA - this ought to trigger the TC interrupt
 DMA_Cmd(UARTSLK_RX_DMA_STREAM, DISABLE);
 
 // Block on the EN bit to wait for the DMA to be disabled
 // (Yes, I plan to do a timeout here here eventually)
 while((UARTSLK_RX_DMA_STREAM->CR & CCR_ENABLE_SET) != 0);
 
 // Record the number of bytes the DMA has read out at the time of IDLE
 // Is this the appropriate way to determine bytes read? Could be related
 // to my underrun errors if the DMA isn't actually finished reading data
 // to memory before I disable it, right?
 rxBufferContentsSize = RX_BUFFER_SIZE - UARTSLK_RX_DMA_STREAM->NDTR;
 
 // This step is necessary to clear the IDLE interrupt
 USART_ReceiveData(UARTSLK_TYPE);
 // Would it be a good practice to disable the UART peripheral here as well,
 // until the DMA is re-enabled??? Perhaps this is related to the extra byte
 // of received data I often see, described by problem number 1 in my post?
 }
}
// DMA Rx handler
void __attribute__((used)) DMA2_Stream1_IRQHandler(void)
{ 
 // DMA Rx handler has been entered. This should only happen when the IDLE
 // interrupt has been processed, so the data from 0 through rxBufferContentSize
 // is supposed to contain one complete, valid packet.
 // This is my test function that quickly runs through the buffer starting
 // from index 0 through rxBufferContentsSize (see the UART ISR for where that's set)
 // In my actual application, this is where the upstream code would be signaled that
 // data is ready. This would not be a long, blocking call - we need to get the DMA
 // enabled again ASAP to be ready for the next message.
 if(!isValidPacket())
 {
 DEBUG_PRINT('RECEIVED INVALID PACKET');
 }
 // Clear the Rx Buffer on each iteration so it's easier to inspect for errors
 // Won't need to do this in product code.
 memset(RxBuffer, 0, RX_BUFFER_SIZE);
 // Restart the DMA
 // I suppose this is where JW has suggested I do an additional read of the UART first
 // Would it be better to have disabled the UART perihperal and only re-enable it when
 // I am ready to enable DMA again?
 DMA_ClearFlag(DMA2_Stream1, UART6_RX_DMA_ALL_FLAGS);
 DMA2_Stream1->NDTR = RX_BUFFER_SIZE;
 DMA2_Stream1->M0AR = (uint32_t)RxBuffer;
 /* Enable DMA USART TX Stream */
 DMA_Cmd(DMA2_Stream1, ENABLE);
}�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?

Sean Kelly
Associate
August 16, 2017
Posted on August 16, 2017 at 04:20

I have a follow up on this issue.

Regarding the discussion above about whether my transmitting device was well behaved -- I had found under a debugger that I was quite often filling up the entire DMA buffer before the IDLE interrupt fired, which shouldn't be possible given that my transmitting device is supposed to be sending bursts of data every 1ms. Fortunately, I also have source and JTAG access to the transmitting device and did some digging. Turns out it's behaving just fine, and it takes about 240us to send a 15byte message. This is a bit more than the theoretical 150us, but the implementation is blocking through register access, so I'm not surprised there's more overhead. Either way, there should be plenty of idle time between messages for my receive side's IDLE interrupt to fire and be processed.

I went back to debugging on the STM receive side and I think this is another case of user error: I was using JTAG and breakpoints to try to detect this condition. I'm now finding that the act of setting or enabling a breakpoint is enough to overflow my DMA buffers. If I resume execution, I do not hit the condition again. If I enable or set another breakpoint (anywhere in the code, basically) my overflow breakpoint hits immediately. 

I guess the act of setting breakpoints is enough to stall the CPU? Is that in the realm of possibility? I've never had to think about the overhead for setting breakpoints before..

Thanks

Sean

waclawek.jan
Super User
August 16, 2017
Posted on August 16, 2017 at 09:06

This has nothing to do with *breakpoints* as such, but with you stopping the processor in the debugger.

Note that 32-bitters are *not* microcontrollers, they are SoC - think of a board with a microprocessor and a bunch of peripheral chips, as we built them in the past. The debugger stops the processor, but not the surrounding circuitry, which continues to operate - including the USART and the DMA.

Some peripherals (timers, I2C, CAN, watchdogs) can be *optionally* 'frozen' during debugging, see DBGMCU_xxx registers.

JW

TLin.5
Associate III
October 1, 2018

I'm currently using a multibuffering approach to mix DMA RX and IDLE for UART reception, using a modified HAL in order to achieve all that I need.

On IDLE, I pause the DMA reception to handle the receive event keeping track of where in the buffer I finished reading last.

I used to have problems with IDLE and DMA receive complete happening "at the same time", but I found that the interrupt priorities had been incorrectly set which caused IDLE to preempt while handling receive complete.