Topics

► Games

► Sound & Music

► Watches & Clocks

► GPS

► Power Supplies

► Computers

► Graphics

► Thermometers

► Wearables

► Test Equipment

► Tutorials

► Libraries

► PCB-Based Projects

By processor

AVR ATtiny

► ATtiny10

► ATtiny2313

► ATtiny84

► ATtiny841

► ATtiny85

► ATtiny861

► ATtiny88

AVR ATmega

► ATmega328

► ATmega1284

AVR 0-series and 1-series

► ATmega4809

► ATtiny1604

► ATtiny1614

► ATtiny3216

► ATtiny3227

► ATtiny402

► ATtiny404

► ATtiny414

► ATtiny814

AVR DA/DB-series

► AVR128DA28

► AVR128DA32

► AVR128DA48

► AVR128DB28

ARM

► ATSAMD21

► RP2040

► RA4M1

About me

  • About me
  • Twitter
  • Mastodon

Feeds

RSS feed

Simple ATtiny USI UART

26th November 2014

This article describes a simple receive-only serial UART for the ATtiny85, using the USI (Universal Serial Interface), the basic serial communication module provided on most ATtiny chips.

I wrote this to receive a 9600 baud serial signal from a GPS module using an ATtiny85 processor. Although there are some published routines to do this, they were all a bit complicated, so I set out to write a minimal routine that would do just what I wanted.

ATtiny85 USI UART

One way to implement serial communication on the ATtiny85 is using software, but this requires tricky programming to get the timing correct, and ties up the processor making it difficult to synchronise reception of the data with other tasks.

The ATtiny85 does provide a hardware USI, which can be used to implement two-wire I2C or three-wire SPI, and this can be used to do part of the job in implementing a UART; Atmel have written an application note describing how to do this [1]. The application note is based on the ATtiny26 which has a more primitive timer/counter than the ones in the ATtiny85, and so requires you to reprogram the timer for each of the 8 bits in the byte being received. In the ATtiny85 the USI shift register is clocked in on a Timer/Counter0 compare match, so the task is slightly simpler; we can set the appropriate count and leave it to receive all 8 bits automatically.

The circuit uses an 8MHz crystal clock for accurate timing. The internal clock is only guaranteed to be accurate to within 10% without special calibration, and that isn't really accurate enough for a UART; after 5 bits we may be half a bit out. If you're prepared to calibrate the internal clock you could dispense with the crystal.

Operation

The hardware/software UART works as follows:

At 9600 baud, with an 8MHz clock, the duration of one bit is 8000000/9600 or 833.3 clock cycles.

First we disable the USI. The input of the USI shift-register is connected to PB0, so we define that as an input, and set up a pin-change interrupt on it:

void InitialiseUSI (void) {  
  pinMode(DataIn, INPUT);         // Define DI as input
  USICR = 0;                      // Disable USI.
  GIFR = 1<<PCIF;                 // Clear pin change interrupt flag.
  GIMSK |= 1<<PCIE;               // Enable pin change interrupts
  PCMSK |= 1<<PCINT0;             // Enable pin change on pin 0
}

The start of a byte causes a pin-change interrupt. In the pin-change interrupt service routine we check that it's a falling edge, and if so we set up Timer/Counter0 in CTC mode. We want to set up a delay of half a bit, to get into the middle of the start bit, which is 416.7 cycles. The closest we can get to that is a prescaler of 8 and a compare match of 52. Finally we clear and enable the output compare interrupt:

ISR (PCINT0_vect) {
  if (!(PINB & 1<<PINB0)) {       // Ignore if DI is high
    GIMSK &= ~(1<<PCIE);          // Disable pin change interrupts
    TCCR0A = 2<<WGM00;            // CTC mode
    TCCR0B = 0<<WGM02 | 2<<CS00;  // Set prescaler to /8
    TCNT0 = 0;                    // Count up from 0
    OCR0A = 51;                   // Delay (51+1)*8 cycles
    TIFR |= 1<<OCF0A;             // Clear output compare flag
    TIMSK |= 1<<OCIE0A;           // Enable output compare interrupt
  }
}

The compare match interrupt occurs in the middle of the start bit. In the compare match interrupt service routine we reset the compare match to the duration of one bit, 104, enable the USI to start shifting in the data bits on the next compare match, and enable the USI overflow interrupt:

ISR (TIMER0_COMPA_vect) {
  TIMSK &= ~(1<<OCIE0A);          // Disable COMPA interrupt
  TCNT0 = 0;                      // Count up from 0
  OCR0A = 103;                    // Shift every (103+1)*8 cycles
  // Enable USI OVF interrupt, and select Timer0 compare match as USI Clock source:
  USICR = 1<<USIOIE | 0<<USIWM0 | 1<<USICS0;
  USISR = 1<<USIOIF | 8;          // Clear USI OVF flag, and set counter
}

Note that we set the Wire Mode to 0 with 0<<USIWM0. This ensures that the output of the USI shift register won't affect the data output pin, PB1.

When 8 bits have been shifted in the USI overflow interrupt occurs. The interrupt service routine disables the USI, reads the USI shift register, and enables the pin change interrupt ready for the next byte:

ISR (USI_OVF_vect) {
  USICR = 0;                      // Disable USI         
  int temp = USIDR;
  Display(ReverseByte(temp));
  GIFR = 1<<PCIF;                 // Clear pin change interrupt flag.
  GIMSK |= 1<<PCIE;               // Enable pin change interrupts again
}

The only catch is that the UART sends the bits LSB first, whereas the USI assumes that the MSB is first, so we need to reverse the order of the bits after reception. This can be done by a short software routine ReverseByte():

unsigned char ReverseByte (unsigned char x) {
  x = ((x >> 1) & 0x55) | ((x << 1) & 0xaa);
  x = ((x >> 2) & 0x33) | ((x << 2) & 0xcc);
  x = ((x >> 4) & 0x0f) | ((x << 4) & 0xf0);
  return x;    
}

This works efficiently by first interchanging adjacent single bits, then interchanging adjacent 2-bit fields, then exchanging the two 4-bit fields. 

Then I just call Display() to display the byte on a seven-segment display for debugging purposes, but the code to handle the received byte should go here in the final version.

Compiling the program

I compiled the program using the ATtiny core extension to the Arduino IDE [2]. This doesn't include a setting for the ATtiny85 with an 8MHz crystal, so I added the following definition to the boards.txt file:

###########################################################################

attiny85at8x.name=ATtiny85 @ 8 MHz  (external crystal; BOD disabled)

attiny85at8x.upload.using=arduino:arduinoisp
attiny85at8x.upload.maximum_size=8192

# Ext. Crystal Osc. 8.0 MHz; Start-up time: 16K CK/14 CK + 65 ms; [CKSEL=1111 SUT=11]
# Brown-out detection disabled; [BODLEVEL=111]
# Preserve EEPROM memory through the Chip Erase cycle; [EESAVE=0]
# Serial program downloading (SPI) enabled; [SPIEN=0]

attiny85at8x.bootloader.low_fuses=0xFF
attiny85at8x.bootloader.high_fuses=0xD7
attiny85at8x.bootloader.extended_fuses=0xFF
attiny85at8x.bootloader.path=empty
attiny85at8x.bootloader.file=empty85at16.hex

attiny85at8x.build.mcu=attiny85
attiny85at8x.build.f_cpu=8000000L
attiny85at8x.build.core=tiny
 
###########################################################################

This adds an ATtiny85 @ 8MHz (external crystal; BOD disabled) option to the Board submenu. Select this, and choose Burn Bootloader to set the fuses appropriately using the Tiny AVR Programmer Board; see ATtiny-Based Beginner's Kit. Then upload the program to the ATtiny85.

Other options

To use this routine with different crystal frequencies, or for different baud rates, you will need to change the prescaler and compare-match values for Timer/Counter0 in the pin-change interrupt routine and USI overflow interrupt routine. With higher baud rates you may need to take into account the delay of handling the interrupt.

Here's the whole ATtiny85 USI UART program: Simple ATtiny USI UART Program.

Updates

10th December 2014: Changed the output compare interrupt routine, ISR (PCINT0_vect), to avoid affecting the Timer/Counter1 interrupt bits. This leaves you free to use the Arduino timing functions millis() and delay() which use Timer/Counter1.

3rd May 2015: Henry Choi and Edgar Bonet have pointed out that in my original version the COMPA interrupt is called immediately, with the effect that the subsequent data bits are sampled near the beginning of each bit. The solution, incorporated above, is to clear the output compare flag before enabling the output compare interrupt, by adding the line:

TIFR |= 1<<OCF0A;

in the PCINT0_vect subroutine, and then delay half a bit, into the middle of the start bit, by changing the setting of OCR0A to:

OCR0A = 51;                   // Delay (51+1)*8 cycles
This ensures that each sample is read in the middle of each bit.

  1. ^ AVR307: Half Duplex UART Using the USI Module (PDF).
  2. ^ ATtiny core for Arduino: arduino-tiny on Google Code.

blog comments powered by Disqus