I²C on ATTiny45 (bit-banging)

There are a few libraries out there for doing I²C (I2C) using the USI or TWI hardware for AVRs. Unfortunately documentation that explains exactly what is happening is hard to come by. There are a number of well done explanations on what it is, and extensive examples for PICs, though few for AVRs and much less for ATTiny45s.

So I decided to do a simple bit-banged version for my own project.

Here is a link to an excellent explanation of I2C and it’s general protocol (no need to re-explain it). It sounds rather simple to implement, though the method and code to make it work on AVR is a little counter intuitive. The following is done in C using AVR Studio.

As an example, I attempted communication with a digital accelerometer, the Freescale MMA7660FC 3-axis +/-1.5g.

MMA7660FC Breakout Solder Compressed

Meticilous breakout of accelerometer to breadboard

As mentioned, the ATTiny45 uC has USI (Universal Serial Interface) capabilities pre-installed (which use the USCK and SDA pins), but due to my non-knowledge of how to do I2C, this first attempt was gonna use I/O pins.

ATTiny45 Pinout Diagram ©Atmel Corporation

ATTiny45 Pinout Diagram ©Atmel Corporation

So I connected the two via the ATTiny45’s PB3 and PB4 pins, eagle schematic below.

ATTiny45 to the MMA7660FC Accelerometer

ATTiny45 to the MMA7660FC Accelerometer

First tricky thing to get around. You can’t just set the I/O pins as outputs and drive them low or high depending on if you want to send a high or low signal.

Since the Slave device (the accel) needs control of the SDA data line when the AVR isn’t using it, you can’t be driving it with current in the high state. You also need the ability to read the status of the SDA line when the uC is accepting data or checking for a message receipt acknowledgment from the Slave device.

The proper way to do this is to initialize the SDA and SCL pins in the ATTiny45 as Inputs and set them both to low. The syntax may look odd, it’s just setting the bits in the registers without messing with any others.

DDRB &= ~(1<<4); //Set the SDA Line to Input
DDRB &= ~(1<<3); //Set the SCL Line to Input

PORTB &= ~(1<<4); //Set the SDA Line to 0 (do not change)
PORTB &= ~(1<<3); //Set the SCL Line to 0 (do not change)

I then created some simple functions for sending an I2C Start Condition and Stop Condition (which are elaborated on in the linked explanation).

To properly send a high signal using I2C, instead of setting the actual port value to hi (PORTB = …) set the Data Direction Register (DDRB) to Input (which causes the pin to neither sink or source current and allows the pull-up resistors to pull the line high). To send a low, set the DDRB bit to Output so the pin acts as a current sink, pulling the line low.

void TWIStartCondition(void)
//Defined in the datasheet as transitioning the Data line (SDA) from HI to LOW while the SCL Line is HI
DDRB &= 0b11101111; //SDA HI (set to input)
DDRB &= 0b11110111; //SCL HI
DDRB |= 0b00010000; //SDA LOW (set to output)
DDRB |= 0b00001000; //SCL LOW

void TWIStopCondition(void)
//Defined in the datasheet as transitioning the Data line (SDA) from LOW to HI while the SCL Line is HI
DDRB |= 0b00010000; //SDA LOW
DDRB &= 0b11110111; //SCL HI
DDRB &= 0b11101111; //SDA HI
DDRB |= 0b00001000; //SCL LOW

With those functions defined it’s then time to write a simple bit-banging