Search code examples
cinterruptavrusart

Using Interrupt to Transmit via USART on AVR MCU


I believe I understand how to use interrupts to receive serial data on UART of an ATmega328p, but I don't understand the mechanics of how to transmit data.

Here is a basic program that I want to use to transmit the character string "hello" using interrupts to drive transmission. I understand that the character 'o' will likely be transmitted twice, and I am ok with that.

#include <avr/io.h>
#include <avr/interrupt.h>
#define F_CPU 16000000UL
#define BAUD 19200
#define DOUBLE_SPEED 1

void initUART(unsigned int baud, unsigned int speed);

volatile uint8_t charIndex = 0;
volatile unsigned char command[5] = "hello";

int main(void)
{
    //initialize UART
    initUART(BAUD, DOUBLE_SPEED);

    sei();

    //What do I put here to initiate transmission of character string command?
    //Is this even correct?
    UDR0 = command[0];

    while(1)
    { 

    }   
}

ISR(USART_TX_vect) 
{
   // Transmit complete interrupt triggered
    if (charIndex >= 4) 
    {
        //Reach the end of command, end transmission
        return; 
    }
    //transmit the first char or byte
    UDR0 = command[charIndex];
    //Step to the next place of the command
    charIndex++; 
}

void initUART(unsigned int baud, unsigned int speed)
{
    unsigned int ubrr;

    if(speed)
    {
        //double rate mode
        ubrr = F_CPU/8/baud-1;
        //set double speed mode
        UCSR0A = (speed << U2X0);
    }
    else
    {
        //normal rate mode
        ubrr = F_CPU/16/baud-1;
    }

    //set the baud rate
    UBRR0H = (unsigned char)(ubrr >> 8);
    UBRR0L = (unsigned char)(ubrr);

    //enable Tx and Rx pins on MCU
    UCSR0B = (1 << RXEN0) | (1 << TXEN0);

    //enable transmit interrupt
    UCSR0B = (1 << TXCIE0);

    //set control bits, 8 bit char, 0 stop, no parity
    UCSR0C = (1 <<UCSZ00) | (1 <<UCSZ01);
}

My understanding is that if I wrote the first character to UDR0 (as I did in main()), this would then trigger a Transmit Complete Interrupt, and then the next byte would be transmitted via the ISR. This does not seem to work.

The code shown here compiles using gcc. Can someone offer an explanation?


Solution

  • The key thing to understand is that the USART has 2 separate hardware registers that are used in the data transmission: UDRn and the Transmit Shift Register, which I'll just call TSR from now on.

    When you write data to UDRn, assuming no tx is in progress, it'll get moved to the TSR immediately and the UDRE irq fires to tell you that the UDRn register is "empty". Note that at this point the transmission has just started, but the point is that you can already write the next byte to UDRn.

    When the byte has been fully transmitted, the next byte is moved from UDRn to TSR and UDRE fires again. So, you can write the next byte to UDRn and so on.

    You must only write data to the UDRn when it is "empty", otherwise you'll overwrite the byte it's currently storing and pending transmission.

    In practice, you don't usually mind about the TXC irq, you want to work with the UDRE to feed more data to the USART module.

    The TXC irq, however, is useful if you need to perform some operation when the transmission has actually completed. A common example when dealing with RS485 is to disable the transmitter once you're done sending data and possibly re-enable the receiver that you could have disabled to avoid echo.

    Regarding your code

    Your main issue is that you're setting UCSR0B 2 times in initUART() and the second write clears the bits you just set, so it's disabling the transmitter. You want to set all bits in one go, or use a |= on the second statement.