Serial Communications with the ATtiny84


In a previous post, I talked about serial communications with an ATmega168. But that chip has USART – hardware support for serial communications. But what about the tinyAVRs? As continuation of my last post on setting up the ATtiny84 for programming, this time, I will talk about sending data from an ATtiny84 to a computer using serial communications.


First, I recommend that you watch this fun and informative video on serial communications by Pete from Sparkfun, USA:


So, now that we have some idea about serial communications, let’s talk about implementing a transmit only (TX) for the ATtiny84. Here is our scheme:


 


The above image shows the transmission of a byte (with value 0x95). The value is sent one bit at a time, starting with the least significant bit (LSB), every 1/9600 seconds, thus giving us a baud rate of 9600. The data format for a packet for the 9600 baud 8-N-1 serial connection is as follows:


start(low)-[0]-[1]-[2]-[3]-[4]-[5]-[6]-[7]-stop(high)-idle(high)-idle(high)


(In the above, the 2 idle bits at the end are not strictly necessary.)


So how do we implement sending a bit every 1/9600 seconds? For this, we use the 16-bit timer (Timer1) of the Attiny84 in CTC mode. The value for the top of the counter is calculated as shown in the image above.


In the ISR for the timer, we keep track of the current data byte and the current bit being sent, and keep setting the output pin high/low as appropriate.


Here is the code that implements the above ideas.


 


The Makefile is the same as we used in the ATtiny84 introduction post:


https://gist.github.com/electronut/5689137


This is the schematic for wiring it up:


 

//
// Serials communications (TX only) with ATtiny84
//
// electronut.in
//

#include <avr/io.h>
#include <string.h>
#include <util/delay.h>
#include <avr/interrupt.h>
 
#define F_CPU 8000000

char pinTX = PB1;
volatile unsigned char currByte = 0;
volatile int sendingData = 0;

// initialize 
void init_serial()
{
  // set TX pin as output
  DDRB = (1 << pinTX);

  // set up timer1:
  cli();
  // 16 bit timer 
  // Divide by 1
  TCCR1B |= (1<<CS10);
  // Count cycles - (1/9600)*8000000 
  OCR1A = 833;   
  // Put Timer/Counter1 in CTC mode
  TCCR1B |= 1<<WGM12;
  // set interrupt flag
  TIMSK1 |= 1<<OCIE1A;
  sei();
}

// send a byte
void send_byte(char data)
{
  // set flag
  sendingData = 1;
  // set current data byte
  currByte = data;
  // wait till done
  while(sendingData);
}

// which bit was sent last? (0-10) 
// -1 implies none sent
volatile char bitNum = -1;

// 16-bit timer CTC handler - Interrupt Service Routine
ISR(TIM1_COMPA_vect)      
{
  // serial data sent as:
  //
  // 9600 8 N 1 
  //
  // start(low)-0-1-2-3-4-5-6-7-stop(high)-idle(high)-idle(high)
  // 12 bits sent per packet

  // idle => high
  if(!sendingData) {
    PORTB |= (1 << pinTX);
  }
  else {

    if(bitNum < 0) {
      // start bit - low
      PORTB &= ~(1 << pinTX);
      // set bit number
      bitNum = 0;
    }
    else if(bitNum >= 8) {      
      // stop bit - high
      PORTB |= (1 << pinTX);
      // increment
      bitNum++;

      // send 2 idle - high - bits
      // not necessary strictly speaking
      if(bitNum == 10) {
        // done 
        sendingData = 0;
        // unset bit number
        bitNum = -1;
      }
    }
    else { 
      // data bits:

      // bit num is in [0, 7]

      // extract relevant bit from data
      char dataBit = currByte & (1 << bitNum);
      if(dataBit) {
        PORTB |= (1 << pinTX);
      }
      else { 
        PORTB &= ~(1 << pinTX);
      }

      // update bit number
      bitNum++;
    }
  }
}


// write null terminated string
void send_str(const char* str)
{
  int len = strlen(str);
  int i;
  for (i = 0; i < len; i++) {
    send_byte(str[i]);
  }   
}


int main (void)
{
  init_serial();

  int count = 10;
  // loop
  while (1) {

    // print count 
    char str[16];
    sprintf(str, "%d\n", count);
    if(count) {
      send_str(str);
      count--;
    }
    else {
      send_str("BOOM\n");
      count = 10;
    }

    _delay_ms(200);
  }
 
  return 1;
}

The Makefile is the same as we used in the ATtiny84 introduction post:


https://gist.github.com/electronut/5689137


This is the schematic for wiring it up:


 


Here is what the breadboard setup looks like:


 


And here is the output from CoolTerm. It was surprisingly easy to get this working. 😉


 


Note:


On OS X, you can use the screen command to see the serial output. For example:


$ screen /dev/tty.usbserial-A7006Yqh 9600