I2C Bus Implementation using USI Hardware Module of ATtiny85

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

1 Like

.... Reserved

...Reserved

Is there an advantage to your technique over using this core?

1. Once, many years ago, I attempted to study the USI interface of the ATtiny85 in order to implement a UART port, but I soon realized my inability to implement the RX section. Later, I found that STX works well using the SoftwareSerial.h library, which I have been using for debugging purposes.

2. This time, I have become interested in implementing the I2C interface using the USI module, being inspired by this thread @i90s.

3. While implementing the I²C interface, I have seen and understood, at least to some extent, the role of the USI hardware in reducing the amount of the control software. The purpose of this thread is to share that experience and understanding with the readers.

4. The core you referred to, in my opinion, does not include library functions that directly convert the USI module into an I2C interface. Libraries such as Wire.h and TinyWireM.h perform this conversion and one of these two libraries must be included in the sketch for I2C communication. The core has mainly mapped/defined the register level names into high level names lke PB0 is named as DPin-0 (0) and many other things.

5. I use ATTinyCore for ATtiny85 programming in the Arduino IDE and include TinyWireM.h/Wire.h Library in my sketch for I2C communication.

Did you check the documentation on the link I shared? I have, and in my opinion, it does.

Do you mean that the inclusion of the ATTinyCore in the IDE is enough to run the I2C communication and there is no need to include TinyWire.h/Wire.h Librray in the sketch?

I have the hardware setup ready and going to check your proposition.

@PaulRB

I have ATTinyCore included in my IDE for ATTiny85. The following sketch even does not get compiled without WIre.h Library.

//#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() //
{
  digitalWrite(ledD2, HIGH);
  delay(5000);
  digitalWrite(ledD2, LOW);
  delay(5000);
}

I think you still need to include wire.h in your sketch. But the core I recommended has it's own implementation of wire.h and hopefully your sketch will pick up and use that version. But if you have other wire libraries installed for ATTINY, maybe things can get confused!

1 Like

@GolamMostafa Have you made any conclusions yet? Have you got your sketch to use ATTinyCore's own wire.h implementation, and if so, do you think it is using USI on ATTINY85? If it is, are there any advantages that it or your own implementation have?

1. ATtiny85 has no dedicated hardware like ATmega328P to handle I2C communication. The ATTiny85 contains a generalized hardware module called USI Module which can be configured to work as (with minimal software).

SPI Port
I2C Bus
Simple Shift Register.

2. If Library like Wire.h or WireTinyM.h is included in the sketch, then the library converts the USI module into I2C logic and directs the SDA/SCL signals over PB0/PB2 pins of the ATtiny85. The library also supports high level constructs for data communication.

3. I have ATTinyCore included in my Arduino IDE, and I still need to include Wire.h Library in my sketch to communicate with other devices using I2C bus. This indicates tat ATTinyCore does contain any functions.

4. I am working on the USI module as an exercise to understand the functionalitiies of varius sub units like: creation of SDA/SCL port pins, bringing START/STOP conditions on the bus, generation of exactly 8 clock pulses to shift-out 8-bit data, and etc.

If you were using i2c bus on, for example, an Uno, you would need to include wire.h in your sketch. Yet you don't have to install any i2c library for the Uno. That's because it's included in the Uno's core.

Did you mean to say "does not contain any functions"?

ATTinyCore has several libraries included, which are special implementations for ATTINY chips. Take a look at this link. There is a wire library there.

If there is a Wire.h Library in the ATTiyCore, then why is the following sketch not workng in my ATtiny85 to detect the presence of UNOR3? The sketch works when I do include the Wire.h Library in the sketch.

//#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() //
{
  digitalWrite(ledD2, HIGH);
  delay(5000);
  digitalWrite(ledD2, LOW);
  delay(5000);
}

I have downloaded and extracted the zip file. Now, where (in which folder) should I look for the Wire.h Library? I have explored relevent folders/subfolders but have not found even the name of the Wire.h Library.

I have already answered these questions. Please read my previous posts again and let me know if you think that's not true.

From what I have understood so far, even though ATTinyCore is included in the IDE, I still need to include the Wire.h / TinyWireM.h library in my sketch for the ATtiny85 to perform I2C communication.

Well what you did there is very in depth. And there are huge advantages to having your own personal way of making things work EXACTLY as you want them to.
One thing I've learned about programming on this journey is it's all about accessing and using the resources in the microcontroller to fit our needs.
So I say : job well done :smiley:

Correct.

But when you are using ATTinyCore, you are using the wire.h that is part of that package (unless maybe you also have some other wire library installed). And that ATTinyCore wire library uses, I believe, USI, when compiling a sketch for upload to ATTINY85.

I am not criticising the efforts of @GolamMostafa. I also think it was a job well done, and I apologise for not saying that until now. I'm not sure I could have done it myself.

But perhaps I am criticising the motivation. I do think that he may have "re-invented the wheel" because there already exists an Arduino core package which, I believe, uses the USI to implement i2c on ATTINY85. But I am struggling to communicate my reasons for believing that to @GolamMostafa.

I thought that after post #9 that "the penny had dropped" (or "the lightbulb went on"). But from the replies since then, perhaps not.

I understand, completely :smiling_face_with_sunglasses:

1 Like

I am not re-inventing the wheel; instead, I am practicing to see if I can configure the USI registers to implement of I2C Interface which are effectively done in Wire.h/WireinyM.h Library.

I cannot yet prove to myself that the ATTinyCore can play te role of Wire.h/WireTinyM.h library to handle I2C communcation for ATtiny85.

I have been motivated by your various posts and have explored the contents of ATTinyCore to see whether it includes code that configures the USI module to implement the I2C interface for the ATtiny85.

I have been struggling to get the code from post #1 working. It is a very tedious process, especially when the Digispark ATtiny85 Dev board does not cooperate properly. Most of the time it goes out of sync with the PC, and I have to reinstall the USB driver repeatedly to bring the device back in the Device Manager. I will report the progress result.

1 Like