Implementing UART driver with Interrupts and DMA - UART_RXC ISR is executed only first time a character is received

Go To Last Post
19 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I am using ATxMega 32E5 and my current task is to rewrite existing RS485 driver and use DMA to save in my case 
valuable cycles. The interrupt based code is communicating at a speed of 230.400k bauds, but this new driver will

have to be able to work at 2MHz, so this is why I want to use DMA, since I will be able to do only 160 cycles per

character and the RXC ISR uses at best 110 cycles (110/160 = 70% duty cycle, which is too much).
So naturaly I have been experimenting with single design that I iteratively reduced to 
minimal example shown below. 

 

Let me explain what I am trying to accomplish. We have defined proprietary 
protocol (slighty similar to modbus) and I have already implemented it using interrupts. 
This simplified version however is trying to do only the following:

 

1) Wait for a character in RXC ISR and read it
2) Disable interrupts for RXC by setting the interrupt priority to *0b00*
3) Set-up DMA to receive one character (I want to use DMA because in real life at this stage I will read between 40-120 characters, but for test I read here just one byte).
4) Wait until the DMA finishes
5) Set interrupt priority for RXC to *0b10*, disable and re-enable reception
6) repeat from 1.

 

The code is below (I am testing it with 230.400k baudrate at 32MHz mcu, you can skip the initialization - it should be OK since it worked well with previous interrupt-based design).

 

    #define RS_PRIORITY_RXC USART_RXCINTLVL_HI_gc
    #define RS_PRIORITY_TXC USART_TXCINTLVL_HI_gc

    // baud rate 230400
    #define RS_BAUDCTRL 131
    #define RS_BAUDSCALE -3
    
    void rs_init() {
        // Enable Transmit Complete Interrupt
        USARTD0.CTRLA = RS_PRIORITY_TXC;
    
        PORTD.REMAP = 1<<4; // Remap usart from 2,3 to 6,7
        PORTD.DIRSET = 1<<7;
        PORTD.OUTSET = 1<<7; //set TX high
        PORTD.OUTCLR = 1<<5; // DIR low = input
        USARTD0.CTRLB = 0b11100; //enable TX and RX, 2X speed
        USARTD0.CTRLC = 0b011; //no parity, async, 8bit, 1 stop bit
    
      // Sets-up 230.400k baud rate
        USARTD0.BAUDCTRLA = RS_BAUDCTRL & 0xff; // BSEL lo
        USARTD0.BAUDCTRLB = ((RS_BAUDSCALE & 0xf) << 4) | ((RS_BAUDCTRL >> 8) & 0xf); //BSCALE:7-4, BSEL:3-0
    
        // Enable Receive Complete interrupt
        USARTD0.CTRLA |= RS_PRIORITY_RXC;
    
        // Initialize EDMA - this is needed for SPI and RS485
        EDMA.CTRL = (1<<6); // reset
        EDMA.CTRL = (1<<7) | 0b0000; // enable, no double buffering, four perip chan
    
        // CH0 is for RS485 RX, CH2 for TX
        // CH1 is for SPI TX, CH3 for RX
        EDMA.CH1.CTRLA = EDMA.CH0.CTRLA = 1<<6; // reset
        EDMA.CH3.CTRLA = EDMA.CH2.CTRLA = 1<<6; // reset
    
    }
    
    struct {
      char buffer[128];
    } state;
    
    static void setup_rx_dma(uint8_t len) {
        // USARTD0.CTRLA &= ~RS_PRIORITY_RXC;
        // volatile uint32_t reg __attribute__((__unused__));
        // reg = USARTD0.DATA;
    
        EDMA.CH0.CTRLB = 0b1010; // set medium interrupt levels
        EDMA.CH0.ADDRCTRL = 0b001; // none reload, incremental transfer peripheral to memory
        EDMA.CH0.TRIGSRC = EDMA_CH_TRIGSRC_USARTD0_RXC_gc;
        EDMA.CH0.TRFCNT = len;
        EDMA.CH0.ADDR = (register16_t) state.buffer;
        EDMA.CH0.CTRLA = (1<<7) /* enable */ | (1<<2) /* single shot mode */;
    }
    
    static inline void dontExpect(uint8_t *expr, uint8_t exp_value, uint16_t delay) {
        if(*expr == exp_value) {
            while(1) {
                led_toggle();
                rtc_delay_ms(delay);
            }
        }
        *expr = exp_value;
    }
    
    static uint8_t _state = 0;
    
    ISR(EDMA_CH0_vect) {
        led_disable();
        // Check
        dontExpect(&_state, 0, 100);
    
        // Clear timeout interrupt
        working = 0;
    
        // Disable reception of frames
        USARTD0.CTRLB &= ~USART_RXEN_bm;
        // Set RXC interrupt from 0b00 to 0b10
        USARTD0.CTRLA |= RS_PRIORITY_RXC;
        // Enable reception of frames
        USARTD0.CTRLB |= USART_RXEN_bm;
    
        // Reset DMA
        EDMA.CH0.CTRLA = (1<<6) /* reset */;
    }
    
    
    ISR(USARTD0_RXC_vect) {
        led_enable();
        // Check 
        dontExpect(&_state, 1, 500);
    
        // Read the received character
        volatile uint32_t reg __attribute__((__unused__));
        reg = USARTD0.DATA;
    
        // Disable RXC interrupts by setting interrupt priority from 0b10 to 0b00
        USARTD0.CTRLA &= ~RS_PRIORITY_RXC;
    
        // Start DMA transfer for 1 byte
        // note: in real application there will be more data to be read (40-120 bytes), this is just for a test
        setup_rx_dma(1);
    }

 

What happens however is that it blinks only once (very fast blink) and then it remains 
off and the mcu does nothing - I re-enable the interrupts in DMA_CH0 ISR, but it has no 
effect since the LED remains off which means that RXC is called only once first and 
last time when a byte is received.

I have tested the DMA code on its own and it successfully received however many bytes I wanted.

But together the Interrupts + DMA do not work. Does someone know why is this not working?

 

Basically I enable the interrupts for RXC in DMA_CH0_vect and even restart the transmission, but the USARTD0_RXC_vect is not getting called again.

I am sorry, but I have spent way too much time debugging this driver and 
I can't even write working minimal example code.

This topic has a solution.
Last Edited: Mon. Jul 6, 2020 - 08:19 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Why do you need to use DMA?  It is not clear from your explanation steps 1-6

 

the speed is going to be multiplied by 8x going from 230k to 2M baudrate ...what does this mean?  you want to rcv a char at 230k baud and resend at 2 Mbaud?

When in the dark remember-the future looks brighter than ever.   I look forward to being able to predict the future!

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Current driver works with 230k baudrate, but my goal is to re-implement this driver so that it supports higher data rates - 2 MHz. I have to use DMA since basic interrupt based design requires 110 cycles per character, and since this driver will operate at 2MHZ and mcu has frequency of 32MHz, there is only 160 cycles per character, which means high cpu usage which is not practical. If it were 1MHz, then I would go for interrupt driver design, but here with 2 MHz the duty cycle is around  110/160 = 70 %, which is too high. Also the test code initializes the DMA to receive only one character, but in real application this will be around 40-120 bytes, but here I am just trying to test this short piece of code that does not work for some reason.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I don't read C.

I've not used the DMA module on the Xmega.

 

That said, why do you disable the interrupt?

What does that gain you?

I would think that you want to process the ISR faster than the data are incoming, and you don't want to miss one.

I would think you would leave the USART Interrupts enabled.

 

On the Xmega32E5 you MIGHT need to reset an interrupt flag manually within your code.

I haven't looked at the USART interrupts and the data sheet to see if this is the case, you can do that.

On the Xmega32E5 series at least some of the interrupts need to be manually reset from within the ISR.

 

Have you looked at the EVENT SYSTEM to see if the DMA module can automatically load USART received data without the need for any code on your part to handle the data transfer?

That would seem to be the ideal solution, and perhaps the fastest, (although not tested by myself).

 

JC

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Can you fill in the missing defines for your code in #1, their absence prevents us compiling it.

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Filled in the RS_BAUDCTRL and RS_BAUDSCALE macros.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

@JC I disable the interrupts for RXC because they get fired when DMA is working. I also tried to clear USARTD0.STATUS.RXCIF (receive complete interrupt flag) and USARTD0.STATUS.DRIF (data reception flag) after disabling the interrupt in USARTD0_RXC_vect and even tried to clear these flags after enabling RXC interrupt in DMA_CH0_vect but no difference.

 

Have you looked at the EVENT SYSTEM to see if the DMA module can automatically load USART received data without the need for any code on your part to handle the data transfer?

That would seem to be the ideal solution, and perhaps the fastest, (although not tested by myself).

I don't understand. I am setting up the DMA in setup_rx_dma() to single shot mode, which means it will automatically fire when new character is received. I tested this DMA code on its own together with disabled interrupts for USARTD0_RXC_vect and it worked. Then I tested it with RXC interrupt enabled and it kind of worked, but I also got interrupts for RXC while the DMA was working.

 

The problem is that I even though I restart the receiver in DMA_CH0_vect, the USARTD0_RXC_vect somehow does not get triggered again.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

You still haven't clearly stated what you are trying to do.

 

From your description, sounds like you want to rcv a char at 2 megabaud in RXC (about 200000 chars/sec* @10 bits/char), then store that char using direct memory access (DMA) technique.  Is that a clear suummary?

You should be able to handle 200000 IRQ's/sec if you don't have any overhead...just a few assembly instructions to get there & get back.

 

 

*this also hasn't been stated, in fact you could have 2 Megabaud, but only send 5 chars per second (and then: as a burst of 5 or every 200ms)...they are one-way dependency with baud.

When in the dark remember-the future looks brighter than ever.   I look forward to being able to predict the future!

Last Edited: Sat. Jul 4, 2020 - 06:12 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

As I said the resulting ISR for RXC ends up using 110 cycles out of 160 (32mhz/200khz) because there is simple single state machine check needed. If rewritten in assembly, it could go lower, but still would require lot of cycles neverthless. This is why I want to use DMA, I want to save those cycles because the mcu is doing CPU intenstive calculations.

 

In real application code I want to check for STX (start of transmission) byte and length byte and then set up DMA to receive that many bytes. But this is not needed here - what I want to do is to debug this short code, which just waits for a character and then fires DMA for single character. The problem is that this simple code does not work, and how can I expand it when even this is not working?

This reply has been marked as the solution. 
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 1

I looked at this as a learning exercise and I struggled to get your code working in the Atmel Simulator basically because you didn't provide a complete example. I had to fill in a fair bit of initialisation.

 

That aside - it actually works now. The ISR(USARTD0_RXC_vect) receives the first character of a message frame and sets up DMA to receive the remainder of the Frame. After "TransferComplete" we again wait for the first character of the Frame.

 

Here it all is. It's not too different from your own code and you can run it in the Simuilator using STIM files for two test messages;  "JKL" and "PQRS".

 

/*
* delidado.c
*
* Created: 03/07/2020 21:34:38
* Author : Nigel
* URL    : https://www.avrfreaks.net/forum/implementing-uart-driver-interrupts-and-dma-uartrxc-isr-executed-only-first-time-character
*/

#include <avr/io.h>
#include <avr/interrupt.h>
#include <string.h>

#define RS_PRIORITY_RXC USART_RXCINTLVL_HI_gc
#define RS_PRIORITY_TXC USART_TXCINTLVL_HI_gc

// baud rate 230400
#define RS_BAUDCTRL 131
#define RS_BAUDSCALE -3

void rs_init();

typedef struct s_frame {
    char byte0;
    char buffer[16];
} Frame;

static volatile Frame frame;
static volatile uint8_t _state = 0;
static volatile uint8_t transferIsComplete;

int main (void)
{
    /* Initialize OSC Module */
    /* -- Enable the 32 MHz output of the internal RC oscillator */
    OSC.CTRL |= OSC_RC32MEN_bm;

    /* Initialize CPU & Peripheral Clock Prescalers */
    /* ClkPer4: CLK_sys */
    /* ClkPer2: CLK_sys */
    /* ClkPer:  CLK_sys */
    /* ClkCPU:  CLK_sys */
    CCP = CCP_IOREG_gc;
    CLK.PSCTRL = CLK_PSADIV_1_gc | CLK_PSBCDIV_1_1_gc;

    /* Switchover System clock source to: (32MHz Int. RC OSC) */
    while ((OSC.STATUS & OSC_RC32MRDY_bm)==0); // Wait for the 32 MHz RC oscillator to stabilize
    CCP = CCP_IOREG_gc;
    CLK.CTRL = CLK_SCLKSEL_RC32M_gc;

    /* Disable CLK Output */
    PORTCFG.CLKOUT &= ~(PORTCFG_CLKOUTSEL_gm | PORTCFG_CLKOUT_gm);

    /* Initialize PMIC Module */
    PMIC.INTPRI = 0x00;
    PMIC.CTRL   = 0 << PMIC_RREN_bp
                | 0 << PMIC_IVSEL_bp
                | 1 << PMIC_HILVLEN_bp
                | 1 << PMIC_MEDLVLEN_bp
                | 1 << PMIC_LOLVLEN_bp;

    rs_init();

    sei();
    for ( ;; ) {
        while(!transferIsComplete) {
            __builtin_avr_nop();
        }

        transferIsComplete = 0;
        memset((Frame*)(&frame), 0, 8);

    }

    return 0;
}

void rs_init()
{
    // Enable Transmit Complete Interrupt
    USARTD0.CTRLA = RS_PRIORITY_TXC;

    PORTD.REMAP = 1<<4; // Remap usart from 2,3 to 6,7
    PORTD.DIRSET = 1<<7;
    PORTD.OUTSET = 1<<7; //set TX high
    PORTD.OUTCLR = 1<<5; // DIR low = input
    USARTD0.CTRLB = 0b11100; //enable TX and RX, 2X speed
    USARTD0.CTRLC = 0b011; //no parity, async, 8bit, 1 stop bit

    // Sets-up 230.400k baud rate
    USARTD0.BAUDCTRLA = RS_BAUDCTRL & 0xff; // BSEL lo
    USARTD0.BAUDCTRLB = ((RS_BAUDSCALE & 0xf) << 4) | ((RS_BAUDCTRL >> 8) & 0xf); //BSCALE:7-4, BSEL:3-0

    // Enable Receive Complete interrupt
    USARTD0.CTRLA |= RS_PRIORITY_RXC;

    // Initialize EDMA - this is needed for SPI and RS485
    EDMA.CTRL = (1<<6); // reset
    EDMA.CTRL = (1<<7) | 0b0000; // enable, no double buffering, four perip chan

    // CH0 is for RS485 RX, CH2 for TX
    // CH1 is for SPI TX, CH3 for RX
    EDMA.CH1.CTRLA = EDMA.CH0.CTRLA = 1<<6; // reset
    EDMA.CH3.CTRLA = EDMA.CH2.CTRLA = 1<<6; // reset

}

static void setup_rx_dma(uint8_t len)
{
    EDMA.CH0.CTRLB = 0b1010; // set medium interrupt levels
    EDMA.CH0.ADDRCTRL = 0b001; // none reload, incremental transfer peripheral to memory
    EDMA.CH0.TRIGSRC = EDMA_CH_TRIGSRC_USARTD0_RXC_gc;
    EDMA.CH0.TRFCNT = len;
    EDMA.CH0.ADDR = (register16_t) &frame.buffer[0];
    EDMA.CH0.CTRLA = (1<<7) /* enable */ | (1<<2) /* single shot mode */;
}

ISR(EDMA_CH0_vect)
{
    /* Signal Transfer Complete */
    transferIsComplete = 1;

    // Disable reception of frames
    USARTD0.CTRLB &= ~USART_RXEN_bm;
    // Set RXC interrupt from 0b00 to 0b10
    USARTD0.CTRLA |= RS_PRIORITY_RXC;
    // Enable reception of frames
    USARTD0.CTRLB |= USART_RXEN_bm;

    // Reset DMA
    EDMA.CH0.CTRLA = (1<<6) /* reset */;
}

ISR(USARTD0_RXC_vect)
{
    // Read and store the received character
    frame.byte0 = USARTD0.DATA;

    // Start DMA transfer for some bytes
    uint8_t len;
    switch (frame.byte0) {
        case 'J': len = 2;
        break;
        case 'P': len = 3;
        break;
        default: len = 0;
    }
    if (len > 0) {
        /* Disable RXC interrupts by setting interrupt priority from 0b10 to 0b00 */
        USARTD0.CTRLA &= ~RS_PRIORITY_RXC;
        
        setup_rx_dma(len);
    }
}

Enjoy

 

Attachment(s): 

Last Edited: Sun. Jul 5, 2020 - 02:20 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I doubt if I can explain it well.
I have implemented receive processing with EDMA and it worked fine. My implementation has completely eliminated interrupts.

Prepare a 256-byte receive buffer. (Ring buffer)
Set CH0 and CH1 to trigger on RXC.

Operate CH0 and CH1 in double buffer mode.
Whether or not there is data in the ring buffer can be known by EDMA TRFCNT. This allows the receive routine to correctly retrieve the data from the ring buffer.

I'm going to sleep now, so when I wake up I look for the code. It's many years old.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

N.Winterbottom wrote:
N.Winterbottom
Thanks for the effort, I am going to test it in simulator and then on real hardware.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

kabasan wrote:

I doubt if I can explain it well.
I have implemented receive processing with EDMA and it worked fine. My implementation has completely eliminated interrupts.

Prepare a 256-byte receive buffer. (Ring buffer)
Set CH0 and CH1 to trigger on RXC.

Operate CH0 and CH1 in double buffer mode.
Whether or not there is data in the ring buffer can be known by EDMA TRFCNT. This allows the receive routine to correctly retrieve the data from the ring buffer.

I'm going to sleep now, so when I wake up I look for the code. It's many years old.

That is one of the possible ways, but what I forgot to tell is that this (in the final extended form) is supposed to be driver for half-duplex UART where I want to receive frames from single specific node and ignore all other messages, so this solution is way too simple. I need to use interrupts to be able to distinguish between start-of-the-frame character and any other character so that I don't store frames that I don't need (the node that I want to read data from has custom start-of-frame character, while other 'slaves' that are reporting their data have custom start-of-frame character).

 

In fact the frame of the protocol looks in following way:

 

0xCA (start-of-frame character), LEN (8bit), LEN again(8bit), 0xCA, data... of length LEN, CRC16 (16bit).

 

The master node (computer) that I want to receive data from has start-of-frame character set to be 0xCA, while other slaves (atxmegas) start their transmission with 0x35.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

If so, I'll stop searching.
I'll consider your code if I have time.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Hi @N.Winterbottom, thanks for the effort. It works, so I will try to base my code on this. Not sure why my code didn't work, it's not that different... Maybe I am doing something wrong here... will see and let you guys know. Thank again.

Last Edited: Mon. Jul 6, 2020 - 08:21 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I found the issue!

 

In my test code's main() i was calling rs_popmsg() function. The problem was that this function disabled the RXC interrupt, checked few variables and re-enabled the RXC even when it was previously disabled. This lead to race conditions, because RXC was enabled even when it was supposed to be disabled. Which means that the original code I wrote probably worked as it is. Thanks everyone for help, didn't expect this error to be that stupid simple! It almost feels like deja vu.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 2

I hope all this has illustrated the value in producing a small complete test code that demonstrates the problem.

 

In your case however, that small test code would have worked; which itself would imply the bug lay elsewhere. It can therefore be a useful de-bugging exercise also.

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

N.Winterbottom wrote:
It can therefore be a useful de-bugging exercise also.

Absolutely!

 

A previous example:

 

https://www.avrfreaks.net/commen...

Top Tips:

  1. How to properly post source code - see: https://www.avrfreaks.net/comment... - also how to properly include images/pictures
  2. "Garbage" characters on a serial terminal are (almost?) invariably due to wrong baud rate - see: https://learn.sparkfun.com/tutorials/serial-communication
  3. Wrong baud rate is usually due to not running at the speed you thought; check by blinking a LED to see if you get the speed you expected
  4. Difference between a crystal, and a crystal oscillatorhttps://www.avrfreaks.net/comment...
  5. When your question is resolved, mark the solution: https://www.avrfreaks.net/comment...
  6. Beginner's "Getting Started" tips: https://www.avrfreaks.net/comment...
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

awneil wrote:

N.Winterbottom wrote:
It can therefore be a useful de-bugging exercise also.

Absolutely!

 

A previous example:

 

https://www.avrfreaks.net/commen...

 

Will know what to do next time I get stuck. Thanks!