USI Serial UART Send on ATtiny

 

In my previous post, USI Serial UART Receive on ATtiny, we discussed how to implement a Serial UART receiver on an Atmel ATtiny using the USI module. This post describes how to implement a simple UART transmitter using the USI module.

UART Signal

The transmitter will be simpler than the receiver as we just need to generate the signal with the correct timing.

serialtiming

Our transmitter will send 8 data bits with one or two stop bits.

#define STOPBITS           1

Calculating the width of a bit

We define the clock speed, F_CPU, and the baud rate. Note F_CPU is already defined in Arduino and may already be defined in your development environment, it’s normally defined as a build symbol.

#define F_CPU              8000000
#define BAUDRATE           9600

We can use these to calculate the number of CPU clock cycles per bit width.

#define CYCLES_PER_BIT     ( F_CPU / BAUDRATE )

If this number is 255 or less then we set the clock source to be the CPU clock, otherwise we will use the prescaler to divide the clock input to Timer/Counter0 by 8.

We use the bottom three Clock Select bits of Timer/Counter0 Control Register B, TCCR0B, to configure the prescaler. A Clock Select value of 1 is for the CPU clock and a value of 2 is for CPU clock divided by 8.

#if (CYCLES_PER_BIT > 255)
#define DIVISOR             8
#define PRESCALE            2
#else
#define DIVISOR             1
#define PRESCALE            1
#endif
#define FULL_BIT_TICKS      ( CYCLES_PER_BIT / DIVISOR )

Choosing the Tx pin

We can configure the USI module to output data on PB0 (SDA), used in Two Wire I2C mode or PB1 (DO), used in Three Wire SPI mode. But it can only read data from PB0 (DI/SDA).

A serial UART has separate transmit, Tx, and receive, Rx, lines. So if we want to enable both transmit and receive on the same device then we will need to use DO/PB1 for output.

ATtiny85 pinout

Sending the first buffer

Finally we need to fill the USI Data Register with some data and start the clock countdown. Our packet is going to be longer than the 8 bit USI data register once we have added start and stop bits. So will need to split our packet across two buffer sends. The first buffer from our send function and will contain the start bit and 7 bits of data. The second buffer will be sent from the USI overflow interrupt and contain 1 bit of data and one or two stop bits.

usi_send_buffers

Because the USI overflow interrupt will be called twice, once after sending the first buffer and again after sending the second buffer, we need a way to tell it which buffer it has just sent, so it can decide what to do next. We will store this state in a volatile variable as it will be accessed from an interrupt.

enum USISERIAL_SEND_STATE { AVAILABLE, FIRST, SECOND };
static volatile enum USISERIAL_SEND_STATE usiserial_send_state = AVAILABLE;

The USI sends the most significant bit (bit 7) and shifts left on each clock cycle, but the UART standard requires that the least significant bit (bit 0) be sent first. So the data needs to be loaded into the USI register in reverse bit order.

We can use the following function to efficiently reverse the bits in a byte:

static uint8_t reverse_byte (uint8_t x) {
   x = ((x >> 1) & 0x55) | ((x << 1) & 0xaa);
   x = ((x >> 2) & 0x33) | ((x << 2) & 0xcc);
   x = ((x >> 4) & 0x0f) | ((x << 4) & 0xf0);
   return x;
}

We will use a volatile variable to store the reversed data as it will be accessed from an interrupt.

static volatile uint8_t usiserial_tx_data;

The send routine

The first thing our serial send routine does is set the usiserial_send_state variable to indicate that we are sending the FIRST buffer.

void usiserial_send_byte(uint8_t data)
{
  usiserial_send_state = FIRST;

Then we save the reversed data in our volatile usiserial_tx_data variable.

  usiserial_tx_data = reverse_byte(data);

Configuring the timer

We want to configure Timer/Counter0 to Clear Timer on Compare Match (CTC) mode. We do this by setting the three bits of the Waveform Generation Mode, WGM0, flag to 2 (binary 010). Just for fun bit 0 and bit 1 are in the TCCR0A register and bit 2 is in the TCCR0B register. The other bits of TCCR0A are set to zero to indicate normal port operation. The least significant three bits of TCCR0B are used for the Clock Select mode and the rest of the bits (including WGM0 bit 2) are set to zero.

  TCCR0A = 2<<WGM00;           // CTC mode
  TCCR0B = CLOCKSELECT;        // Set prescaler to clk or clk /8

Then we reset the prescaler to indicate that we changed its configuration and start Timer/Counter0 at zero.

  GTCCR |= 1 << PSR0;           // Reset prescaler
  TCNT0 = 0;                    // Count up from 0

We store the number of bits to count in Output Compare Register A, OCR0A.

  OCR0A = FULL_BIT_TICKS;       // Trigger every full bit width

 Configuring the USI module

To use PB1 (DO) for USI output we select Three Wire SPI mode by setting Wire Mode, USIWM[1:0], to 01 in the USI Control Register USICR and setting the PB1 (DO) data direction to output. We also use the USI Control Register to enable the USI Counter overflow interrupt, USIOIE=1 and select Timer0 Compare Match as the input source, USICS[1:0]=01 and USICLK=0.

  USICR = (0<<USIWM1)|(1<<USIWM0)|       // Select three wire mode so USI output on PB1
    (1<<USIOIE)|                         // Enable USI Counter OVF interrupt.
    (0<<USICS1)|(1<<USICS0)|(0<<USICLK); // Timer0 Compare Match as USI clock source
  DDRB |= (1<<PB1);                      // Configure USI DO, PB1 as output

Now we can load up the USI data register with the start bit (low) and the first 7 bits of reversed data.

  USIDR = 0x00 |                     // Start bit (low)
          usiserial_tx_data >> 1;    // followed by first 7 bits of serial data

Finally we ensure that the USI Overflow Interrupt Flag is cleared and set the counter to increment eight times before overflowing. The 4 bit USI counter can hold a value from 0 to 15 and overflows as it attempts to increment to 16, so we set the starting value of the counter to 16 – 8 = 8.

  USISR = 1<<USIOIF |               // Clear USI overflow interrupt flag
          (16 - 8);                 // and set USI counter to count 8 bits
}

The USI interrupt vector

After the first buffer has been sent by the USI module, the USI overflow interrupt vector will be called with our state variable, usiserial_send_state, set to FIRST. Now we set usiserial_send_state to SECOND to indicate we are sending the second buffer.

ISR (USI_OVF_vect)
{
  if (usiserial_send_state == FIRST)
  {
    usiserial_send_state = SECOND;

We load up the USI Data Register with our last bit of data and the stop bits (high). There should only be one or two stop bits, but we need to set the rest of the bits to something, so we set them high. Interestingly, while one and two are the UART supported number of stop bits, values higher than two would work as the serial UART standard supports periods of line idle (high) between packets.

    USIDR = usiserial_tx_data << 7         // Send last 1 bit of data
            | 0x7F;                        // and stop bits (high)

The number of bits to send is the number of stop bits + 1, so we set the counter to 16 minus this value. The USI Overflow Interrupt Flag is cleared to indicate the interrupt has been processed. If we did not do this the microcontroller would immediately re-invoke the interrupt vector rather than when the counter overflowed.

    USISR = 1<<USIOIF |                 // Clear USI overflow interrupt flag
            (16 - (1 + (STOPBITS)));    // Set USI counter for 1 data bit and stop bits
  }

After the second buffer

After the second buffer has been sent by the USI module, the USI overflow interrupt vector will be called with our state variable, usiserial_send_state, set to SECOND.

Our packet has been sent, so we just need to turn off the USI module returning the Tx pin to an idle high state. Just to be safe we make sure PB1 is still configured as a high output.

  else // usiserial_send_state == SECOND
  {
    PORTB |= 1 << PB1;                  // Ensure output is high
    DDRB |= (1<<PB1);                   // Configure USI_DO as output.
    USICR = 0;                          // Disable USI.

Finally we clear the USI Overflow Interrupt Flag and set our state variable to AVAILABLE, to indicate that our routine is available to send another packet.

    USISR |= 1<<USIOIF;                  // clear interrupt flag
    usiserial_send_state = AVAILABLE;
  }
}

Sending multiple bytes

To keep this tutorial simple our routine only sends one byte at a time. To send multiple bytes we need to wait for our state variable to become AVAILABLE before calling our send routine with the next byte.

   char message[] = "USI Serial\r\n";
   uint8_t len = sizeof(message)-1;
   for (uint8_t i = 0; i<len; i++)
   {
       while (!(usiserial_send_state==AVAILABLE))
       {
           // Wait for last send to complete
       }
       usiserial_send_byte(message[i]);
   }

For safety and convenience we could also wait for usiserial_send_state to become AVAILABLE at the very beginning of our send routine, usiserial_send_byte.

Enhancing for multiple bytes

When sending multiple bytes we stop the USI module after sending the second buffer and reinitialize the timer when we start sending the next packet. This introduces a small gap of idle time between packets, this isn’t a problem as the UART standard supports idle (line high) time between packets.

However if we knew that we were going to send another packet we could just load the USI Data Register with the first buffer of the next packet and set the state variable back to FIRST.

Arduino Timer

When running this code on Arduino we need to borrow the timer used for millis. To avoid disrupting the Arduino timer we should save the state of the timer registers in volatile variables.

#ifdef ARDUINO
volatile static uint8_t oldTCCR0A;
volatile static uint8_t oldTCCR0B;
volatile static uint8_t oldTCNT0;
#endif

We can save these registers in our send routine after setting the usiserial_send_state to FIRST.

#ifdef ARDUINO  
   oldTCCR0B = TCCR0B;
   oldTCCR0A = TCCR0A;
   oldTCNT0 = TCNT0;
#endif

Then we can restore the registers it in the USI Overflow Interrupt vector before setting usiserial_send_state to AVAILABLE.

#ifdef ARDUINO
       TCCR0A = oldTCCR0A;
       TCCR0B = oldTCCR0B;
       TCNT0 = oldTCNT0;
#endif

Arduino will lose the time it takes us to send each byte, approximately 1ms at 9600 baud, which will not be an issue for most programs. However, if you need to maintain Arduino time with more accuracy, see my post on Borrowing an Arduino Timer.

Get the example code

I have uploaded an Arduino sketch to GitHub at:

https://github.com/MarkOsborne/becomingmaker/tree/master/USISerialSend

This example sends the message “USI Serial” every second. Connect your favorite serial device’s Rx pin to PB1/DO.

4 thoughts on “USI Serial UART Send on ATtiny”

  1. Thanks for posting these tutorials – great way to learn. I’m playing with your code in Atmel Studio 7 attempting to run directly on the Attiny85. I have a data collector EEPROM I built with a RTC on an I2C bus. My hope is to also have the UART from your code to extract the collected data to a PC. I have already written and tested the data logging code. To bridge between the two protocols I hope to use the 512 Byte Attiny85 EEPROM as a buffer. So in summary: two communication protocols out of one 8-pin Attiny85. Your code is over my head, but I’m reaching hard as I can. A few questions:
    1. Do you see any problem with this idea?
    2. The in the variable assignment “static volatile enum USISERIAL_SEND_STATE usiserial_send_state = x;” does this simply assign x to both the small and large case usiserial_send_state’s. Why?
    3. When debugging in AS7, I’m getting an error on USISERIAL_SEND_STATE of “unexpected type def in expression”. Google tells me that this can be from defining a function within a function. Is this because I’m not using Arduino? The code gets hung on the state of FIRST and I think the type def issue is the reason.
    Thanks again for the very helpful tutorial!

    1. Hey Craig, I am glad you found this post useful.
      EEPROM is limited to about 100,000 write/erase cycles and is slow to write (3.4ms), so you’ll need to think carefully about how you use it. It sounds like it would be fine as a non-volatile buffer for a data logger and adding UART to extract the data seems like reasonable plan.
      The following lines of code:
      enum USISERIAL_SEND_STATE { AVAILABLE, FIRST, SECOND };
      static volatile enum USISERIAL_SEND_STATE usiserial_send_state = AVAILABLE;

      Define an enum type called USISERIAL_SEND_STATE and a variable called usiserial_send_state. Then set usiserial_send_state to one of the valid values for the type. The C syntax for enumerated types can be a little confusing. I could have just used a uint8_t and values 0, 1 and 2 with #defines for AVAILABLE, FIRST and SECOND. Or because enums are really just integers in C I could use unit_8 as the variable type:
      enum USISERIAL_SEND_STATE { AVAILABLE, FIRST, SECOND };
      static volatile uint8_t usiserial_send_state = AVAILABLE;

      The code is written to compile in AtmelStudio7 and Arduino, but maybe there is an issue with project/toolchain settings. To make your life easier I have uploaded an AtmelStudio7 project to GitHub at: https://github.com/MarkOsborne/becomingmaker/tree/master/USISerialSendAtmelStudio7.

  2. Hello, very interesting for your tutorial.
    I actually looking to send Serial Text over IR,
    I have success with arduino, but with the attiny84, big trouble..

    I’m using PA7 to send over Timer0 channel B

    but I have many problems doing this fonctionnal…

    I’m looking to with Softserial with no more success..

    The carriage wave is a 38khz , and good controle with the timer 1….
    After 2 days looking for the timer register I’m very in pain….

    1. I’m glad you enjoyed the post Thierry. I hope you get your serial IR working. You will want to make sure your ATtiny is running at a reasonable speed. 8Mhz internal clock at a minimum and you may need to tune the internal clock – see Tuning ATtiny internal oscillator post. Or you can run at 16Mhz like the Arduino with an external oscillator.
      Softserial introduces more timing issues as you are bit-banging in software, the USI can help improve the timing accuracy.
      I’ve written IR receive code from scratch and IR can been difficult to work with as the signal is noisy and timings can drift. A good IR receiver and an RC filter on the voltage input to the IR receiver can help. You may need to reject and handle timing drift on the receive side to be reliable.

Comments are closed.