A: Introduction
The USI (Universal Serial Interface) is a hardware module inside the ATtiny85 MCU. It is possible to configure the USI for the implemetation of the following data transfer communication protocols:
1. SPI Port,
2. I2C Bus, and
3. Shift Register.
There is no dedicated TWI/I2C hardware interface/logic in the ATtiny85 MCU like the ATmega328P MCU. This thread presents the procedure for implementing an I2C interface using the USI hardware module of the ATtiny85 (Fig-2) uder minimal software control codes. The timing of the I2C bus (Fig-1) can be generated easily by bit-banging without using the USI module but at the cost of many lines of code. The use of the USI hardware module, as we will see, drastically reduces the amount of program code required.
B: Timing Diagram of I2C Bus showing START state, Slave Address assertion, Write mode bit, receiving ACKnowledgement, and STOP state.
Figure-1a: (compiled)
Figure-1b: (from dataseet)
C: Hardware Setup using Digispark/Colone ATtiny85 Dev Board (Master) and UNO R3 (Slave)
Fig-2a:
There are four registers in the USI Module and these are:
USIDR : USI Data Register,
USIBR: USI Buffer Register,
USISR: USI Satus Register, and
USICR: USI Control Register.
Figure-2b:
Test Sketch for setup of Fig-2a/2b (Ok! ATtiny has found Slave at 0x6A)
Let us verify that the ATtiny85 of Fig-2a does recognize the I2C Slave (UNOR3) at address 0x6A (01101010). If the Slave is found, the onboard LED-D2 would be continuously blinkinh at 400 ms interval; else, LED-D2 would be continuously blinking at 10-sec interval. The following sketches worked well and the ATtiny85 Master has recognized the Slave correctly at address 0x6A.
Master Sketch for ATtiny85:
#include<Wire.h>
#define slaveAddr 0x6A //110 1010
#define ledD2 1
void setup()
{
Serial.begin(9600);
pinMode(ledD2, OUTPUT);
Wire.begin();
//---------------
Wire.beginTransmission(slaveAddr);
byte busStatus = Wire.endTransmission();
if (busStatus == 0)
{
while (true) //Slave is found
{
digitalWrite(ledD2, HIGH);
delay(200);
digitalWrite(ledD2, LOW);
delay(200);
}
}
}
void loop() //Slave is not found
{
digitalWrite(ledD2, HIGH);
delay(5000);
digitalWrite(ledD2, LOW);
delay(5000);
}
Slave Sketch for UNOR3:
#include<Wire.h>
#define slaveAddr 0x6A
void setup()
{
Serial.begin(9600);
Wire.begin(slaveAddr);
pinMode(13, OUTPUT);
}
void loop()
{
digitalWrite(13, HIGH);
delay(200);
digitalWrite(13, LOW);
delay(200);
}
D: Codes Development for the timing diagram of Fig-1. Codes are tentative and they might undergo major changes after testing.
1. Initialize I2C Pins
The PB0 (DPin-0) and PB2 (DPin-2) are fixed pins that the USI module uses for SDA and SCL lines of the I2C interface.
#define SDA 0 //defined/mapped in ATTinyCore
#define SCL 2
pinMode(SDA, OUTPUT);
pinMode(SCL, OUTPUT);
2. Configue USI for I2C Mode
(1) Select two-wire I2C mode, Tmer-0 CTC-2 Mode as clock source to shift-out/shift-in data from/to USIDR register at 100 kHz on 1-MHz ATtiny85.
Figure-3:
bitSet(USICR, USIWM1); //USIWM1:USIWM0 = 1:0 for 2-wire I2C Mode
bitSet(USICR, USICS); //USICS1:USICS0 = 0:1 for Timer-0 Compare Mode
(2) 100 kHz clock generation for data transfer speed of 100 kbits/sec.
Figure-4:
(a) To shift-out 8-bit data from USIDR register, 8 clock pulses are required. In the case of USI, the clock pulses are not automatically generaed when 8-bit data is stored ito USIDR register (say: USIDR = 0x45); instead, they are generated using Timer-0 in CTC-2 mode (Fig-4).
According to I2C rule, data change on SDA line will take place when the clock low (around falling edge) and the data will be stable when the clock is high (around rising edge). To complete 8-bit data transfer, 8 clock pulses would be required, which means 16 (8 + 8) edges.
After loading the USIDR register with 8-bit data, Timer0 is started, and at the same time the dedicated 4-bit counter (USICNT[3:0] in the USISR register) begins counting clock edges. When 16 clock edges have been counted, the counter signals Timer0 to stop marking the end of 8-bit data transfer. This approach is significantly faster than bit-banging, which requires many lines of code and consumes more CPU cycles.
TCCR0A = 0; //reset
TCCR0B = 0;
//------------------
TCCR0A |= bit(COM0A0); //toggle on compare match
//------------------
TCCR0A |= bit(WGM00) | bit(WGM01);
TCCR0B |= bit(WGM02); //CTC-2 Mode for square wave
//---------- f (100 kHz) = 1 MHz/(2N(OCR0A+1))-------
OCR0A = 4; // N (pescaler) = 1
//----------
bitSet(TIMSK, OCIE0A); //oCompare Match A otput interrupt enable
interrupts();
//------------------------
TCCR0B |= bit(CS00); //start TC0 with prescaler = 1
//--------------------------
ISR(TIMER0_COMPA_vect)
{
OCR0A = 4; //reload timing parameter
}
(b) Counting 16 edges and then signal TC0 to stop
Figure-5:
After countig 16 edges, the USIOIF (USI Counter Overflow Flag) assumes HIGH. It is an informaton that can be utilized to signal the TCO to stop.
bitSet(USISR, USOIF); //clear the overflow flag
USICNT[3:0] = 0000; //4-bit counter is initialized to 0s
do
{
; //do nothing
}
while(bitRead(USISR, USIOIF) != HIGH); //until overflow is HIGH
TCCR0B &= ~bit(CS00); //TC0 is stop
3. Generation of START and STOP States/Conditions (Fig-1)
None of the USI registers has the capability to generate START/STOP conditions of the I2C bus. These conditions are to be created manually by executing program instructions.
START (S): SDA goes LOW while SCL is HIGH
#include <avr/io.h>
#include <util/delay.h>
digitalWrite(SDA, HIGH);
digitalWrite(SCL, HIGH);
_delay_us(5);
digitalWrite(SDA, LOW);
_delay_us(5);
digitalWrite(SCL, LOW);
4. Assert 8-bit I2C bus address (7-bit Slave address + 1-bit (0) write/-bit)
USICNT[3:0] = 0000; ////4-bit counter is initialized to 0s
USIDR = 0b11010100; //7-bit Slave addres (1101010 + 0 =11010100 = 0xD4)
//------------------
TCCR0B != bit(CS00); //start TC0 to generate 8 clock pulses
//-----------------
do
{
; //do nothing
}
while(bitRead(USISR, USIOIF) != HIGH); //until overflow is HIGH
TCCR0B &= ~bit(CS00); //TC0 is stop
5. Receiving ACK bit from Slave
Pseudo Codes:
Clear USIOIF flag
Load 14 into USICNT[3:0]
Start TC0
Wait until USIOIF becomes HIGH
Stop TC0
Read USIDR
if(USIDR == 0x00)
{
mySerial.println("Slave is present);
while(true); //wait for ever
}
mySerial.println("Slave is not present.");
while(true); //wait for ever
6. Test Result
... pending






