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 2

6th May 2015

This is an update to my earlier article Simple ATtiny USI UART, incorporating improvements suggested by Edgar Bonet, who has been developing a version for the ATtiny84 inspired by my original code. I'm also grateful to Edgar for suggesting several improvements to an earlier draft of this article.

Edgar's improvement is to eliminate the TIM0_COMPA_vect interrupt service routine by setting the compare interval to one bit's length when the start of the data is detected, but making the first delay be one and a half bits by setting the counter to 256 minus half a bit period. This way the first COMPA event happens 1.5 bit periods after PCINT0_vect.

This article describes a new version of the ATtiny85 code, incorporating this improvement, and also shows how to modify the code for the ATtiny84.

ATtiny85 version

The new version of 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) {  
  DDRB &= ~(1<<DataIn);           // 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 return if it isn't a falling edge. Then we set up Timer/Counter0 in CTC mode. We set up a delay of one bit, which is 833.3 cycles. The closest we can get to that is a prescaler of 8 and a compare match of 104.

We start the counter TCNT0 at 256-52+2 (the extra 2 allows for the overhead of calling the interrupt routine) or 206, so it runs for an extra half a bit's duration before the first compare match, to get to the middle of the first data bit.

Finally we enable the USI to start shifting in the data bits, and enable the USI overflow interrupt:

// Pin change interrupt detects start of UART reception.
ISR (PCINT0_vect) {
  if (PINB & 1<<DataIn) return;   // Ignore if DI is high
  GIMSK &= ~(1<<PCIE);            // Disable pin change interrupts
  TCCR0A = 2<<WGM00;              // Timer in CTC mode
  TCCR0B = 2<<CS00;               // Set prescaler to /8
  OCR0A = 103;                    // Shift every (103+1)*8 cycles
  TCNT0 = 206;                    // Start counting from (256-52+2)
  // 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 regiater, 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 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.

Here's the new version of ATtiny85 USI UART program: ATtiny85 USI UART Program.

ATtiny84 version

The ATtiny84 version is functionally identical, but a few changes need to be made to reflect the following differences between the two chips:

  • On the ATtiny85 the USI input DI = PB0 = PCINT0, whereas on the ATtiny84 DI = PA6 = PCINT6.
  • The ATtiny85 has a single pin change interrupt vector, whereas the ATtiny84 has two of them and some flags and registers need the suffix '0' to specify which of them it applies to.

Here's a summary of the changes:

ATtiny85 ATtiny84
PINB PINA
PINB0 PINA6
PCINT0 PCINT6
PCIE PCIE0
PCIF PCIF0
PCMSK PCMSK0

Here's the full version of ATtiny84 USI UART program: ATtiny84 USI UART Program.

Extensions

Edgar Bonet has suggested using ISR(TIM0_COMPA_vect) to test the stop bit, and signal an error if it's zero. This adds a bit to the complexity of the program, but would be useful to help you to resynchronize when you get an out-of-sync data stream.


blog comments powered by Disqus