Multi-master I2C using Teensy3.2 + ATtiny85

Hey there!

I asked the same question in the PJRC forums and was advised to ask here, because I use TinyWire library.

I'm currently working on the following project:
Teensy 3.2 and ATtiny85 (programmed via Arduino ISP) are connected to an FRAM chip (http://www.fujitsu.com/us/Images/MB8...0017-3v0-E.pdf) via I2C.
ATtiny is reading an analog sensor and writes the value to the FRAM. Then the Teensy (which is supposed to work as the "main microcontroller" when everything works) is told that it's his turn and reads out the just written sensor value from the FRAM. The ATtiny always saves the current FRAM memory address in the first memory address. So when Teensy is told to read, it reads out the first address to know where the newest sensor value is saved in the FRAM and then reads out that value at the current address.
For testing, I use the Serial Monitor with the Teensy to print the values (sampled with with 1 Hz) it read from the FRAM.

I began with 2 Teensies which worked perfectly. When I switched to the ATtiny (8 MHz) using the TinyWireM library it became buggy:
The Teensy is still reading a value every second, but the it's always the same value. Even when I connect the analog input of the ATtiny to GND or any other potential the Teensy is always reading the same value.

I noticed that when I switch off the ATtiny just for a second, the correct value is printed on the monitor. But again, when I change the analog input the read-out value won't change!
That's why I think the ATtiny is causing the trouble. It either has trouble with:
-analog reading of the value
-saving the current address at the first address of the FRAM
-saving the current analog reading

Here's the code of ATtiny:

#include <TinyWireM.h>


#define EEPROM_ADR 0x50
#define EEPROM_SIZE 32000
#define a_count 1                   //Number of sensors, maximum is 3

#if (a_count == 1)
#define MAX_ADDR 32000
#elif (a_count == 2)
#define MAX_ADDR 32000
#elif (a_count == 3)
#define MAX_ADDR 31998
#else
#error "a_count maximum is 3"
#endif

//////////////////////////////  Constants & Variables  //////////////////////////////

uint32_t current_addr = 1;
uint8_t counter = 0;
const uint32_t sample_int = 1000000;             //microseconds
uint32_t previous_us = 0;
uint16_t a_data[a_count];                     //Buffer to save analog read
uint8_t WakePin = 1;
uint8_t sample_pin = A3;
/*
  struct analog_data {
  uint16_t data[a_count];
  //  byte high_b[a_count];
  //  byte low_b[a_count];
  } a_data;
*/
const uint8_t addr_step = sizeof(a_data);


/////////////////////////////////////    Setup   ////////////////////////////////////

void setup() {
  delay(100);
  TinyWireM.begin();
  pinMode(WakePin, OUTPUT);
  digitalWrite(WakePin, LOW);
  delay(100);                             // to make sure that T3.2 is in loop() and waiting
}

/////////////////////////////////////    Loop    ////////////////////////////////////

void loop() {
  if (digitalRead(WakePin) == HIGH) {                    //WakePin set LOW, just if it is HIGH
    digitalWrite(WakePin, LOW);
  }
  uint32_t current_us = micros();
  if (current_us - previous_us >= sample_int ) {    //sample-routine
    previous_us = current_us;
    sample();

    writeEEPROM_ADDR(current_addr);                //write the current address to the first memory address
    writeEEPROMPage(current_addr, a_data);        //write the data at the current address

    current_addr += addr_step;
    if (current_addr >= MAX_ADDR) {                //prevent overflow of address
      current_addr = 1;
    }
    digitalWrite(WakePin, HIGH);                  // activate Teensy
    delay(1);
  }

}

////////////////////////////////      Functions      ////////////////////////////////

void writeEEPROMPage(uint32_t eeAddress, uint16_t a_data[])
{

  TinyWireM.beginTransmission(EEPROM_ADR);

  TinyWireM.write((int)(eeAddress >> 8)); // MSB
  TinyWireM.write((int)(eeAddress & 0xFF)); // LSB
  for (uint8_t i = 0; i < sizeof(a_data); i++) {
    byte high = highByte(a_data[i]);
    byte low = lowByte(a_data[i]);
    TinyWireM.write(high); //Write the data
    TinyWireM.write(low);
  }

  TinyWireM.endTransmission(); //Send stop condition
}



void writeEEPROM_ADDR(uint32_t eeAddress)
{

  TinyWireM.beginTransmission(EEPROM_ADR);

  TinyWireM.write((int)(0x00)); // MSB
  TinyWireM.write((int)(0x00)); // LSB

  TinyWireM.write((int)(eeAddress >> 8)); // MSB
  TinyWireM.write((int)(eeAddress & 0xFF)); // LSB
  TinyWireM.endTransmission(); //Send stop condition
}



void sample() {                                   //analogRead, loop just runs once for testing
  for (uint8_t i = 0; i < a_count; i++) {
    a_data[i] = analogRead(sample_pin);
  }
}



void readEEPROM(uint32_t eeaddress, uint16_t fram_array[])
{
  TinyWireM.beginTransmission(EEPROM_ADR);

  TinyWireM.write((int)(eeaddress >> 8)); // MSB
  TinyWireM.write((int)(eeaddress & 0xFF)); // LSB
  TinyWireM.endTransmission();

  TinyWireM.requestFrom(EEPROM_ADR, (sizeof(a_data)));
  byte high = 0xFF;
  byte low = 0xFF;
  uint8_t i = 0;
  while (TinyWireM.available()) {
    high = TinyWireM.read();
    low = TinyWireM.read();
    fram_array[i] = ((unsigned int)high << 8 ) + low;
    i++;
  }

}

And here for the Teensy:

#include <Wire.h>

#define EEPROM_ADR 0x50
#define EEPROM_SIZE 32000
#define a_count 1                   //must be the same on ATtiny

#if (a_count == 1)
#define MAX_ADDR 32000
#elif (a_count == 2)
#define MAX_ADDR 32000
#elif (a_count == 3)
#define MAX_ADDR 31998
#else
#error "a_count maximum is 3"
#endif


//////////////////////////////  Constants & Variables  //////////////////////////////

uint32_t current_addr = 0;
uint8_t counter = 0;
const uint32_t sample_int = 1000000;             //microseconds
uint32_t previous_us = 0;
uint16_t a_data[a_count];                     //dummy for buffer size, could use fram_read[] too...
uint16_t fram_read[a_count];                  //buffer for the values out of the FRAM
uint8_t WakePin = 10;
volatile bool read_allowed = false;
/*
  struct analog_data {
  uint16_t data[a_count];
  //  byte high_b[a_count];
  //  byte low_b[a_count];
  } a_data;
*/
const uint8_t addr_step = sizeof(a_data);

/////////////////////////////////////    Setup   ////////////////////////////////////

void setup() {
  Wire.begin();
//  Wire.setClock(100000);
  attachInterrupt(digitalPinToInterrupt(WakePin), wake_up, RISING);
  read_allowed = false;
}

/////////////////////////////////////    Loop    ////////////////////////////////////


void loop() {
  while (!read_allowed) {
  }                                            // wait for Attiny

  current_addr = readEEPROM_ADDR();      //reads the current address at the first memory address of the FRAM
  readEEPROM(current_addr, fram_read);    //reads the values at the current address

  for (uint8_t i = 0; i < a_count; i++) {
    Serial.print(fram_read[i]);
    Serial.print(" ");
  }
  Serial.println();

  read_allowed = false;
}


////////////////////////////////      Functions      ////////////////////////////////


void writeEEPROMPage(uint32_t eeAddress, uint16_t a_data[])
{

  Wire.beginTransmission(EEPROM_ADR);

  Wire.write((int)(eeAddress >> 8)); // MSB
  Wire.write((int)(eeAddress & 0xFF)); // LSB
  for (uint8_t i = 0; i < sizeof(a_data); i++) {
    byte high = highByte(a_data[i]);
    byte low = lowByte(a_data[i]);
    Wire.write(high); //Write the data
    Wire.write(low);
  }

  Wire.endTransmission(); //Send stop condition
}



void sample() {                                   //analogRead
  for (uint8_t i = 0; i < a_count; i++) {
    a_data[i] = analogRead(A0);
  }
}



void readEEPROM(uint32_t eeaddress, uint16_t fram_array[])
{
  Wire.beginTransmission(EEPROM_ADR);

  Wire.write((int)(eeaddress >> 8)); // MSB
  Wire.write((int)(eeaddress & 0xFF)); // LSB
  Wire.endTransmission();

  Wire.requestFrom(EEPROM_ADR, (sizeof(a_data)));
  byte high = 0xFF;
  byte low = 0xFF;
  uint8_t i = 0;
  while (Wire.available()) {
    high = Wire.read();
    low = Wire.read();
    fram_array[i] = ((unsigned int)high << 8 ) + low;
    i++;
  }

}



uint32_t readEEPROM_ADDR()
{
  Wire.beginTransmission(EEPROM_ADR);

  Wire.write((int)(0x00)); // MSB
  Wire.write((int)(0x00)); // LSB
  Wire.endTransmission();

  Wire.requestFrom(EEPROM_ADR, (sizeof(a_data))); 
  byte high = 0xFF;
  byte low = 0xFF;
  uint32_t addr = 0;
  while (Wire.available()) {
    high = Wire.read();
    low = Wire.read();
    addr = ((unsigned int)high << 8 ) + low;
  }
  return addr;
}



void wake_up() {               //ISR
  read_allowed = true;
}

Any ideas?

Teensy 3.2 and ATtiny85 (programmed via Arduino ISP) are connected to an FRAM chip (http://www.fujitsu.com/us/Images/MB8...0017-3v0-E.pdf) via I2C.

How do you prevent collisions on the I2C bus? Neither TinyWireM nor the Wire library support I2C multimaster. If you ever want to get that system reliable don't use I2C multimaster at all. I never saw nor heard of any multimaster setup working reliable in the long term.

What is the FRAM necessary for? Why don't you simply make the Tiny an I2C slave and the Teensy asks for the value when it needs it?

At the moment I don't even try to prevent collisions. But have a look at the code, the bus is idly nearly all the time:
Attiny reads one analog value -> saves it to FRAM -> "wakes" up the Teensy -> Teensy reads value -> waits to be triggered again
As I sample with 1 Hz at the moment, there should not be problem.. I know... should
But it's working like this when I use two Teensies, so I guess there is a problem with the TinyWire library.

What is the FRAM necessary for?

It's supposed to save the last few seconds of the analog sensor. If a certain event is detected by the ATtiny, it will wake up the main microcontroller which reads in the last few seconds saved in the FRAM.

I will try something like this soon. But still I feel like it's a problem of the TinyWire library...

In writeEEPROMPage(), what's the sizeof(a_data)? I'd think that it is the size of a pointer, so that only the first 2 or 4 entries of a_data[] are written, depending on the controller pointer size.

It's actually the number of bytes inside the data buffer. It's not needed at the moment, as I just read one analog value at a time.. later there will be three values at once, so I prepared the code to be easily edited via the variable "a_count", which is 1 right now for testing.

The compiler and you may have different opinions about your code. Do you realize that a_data in writeEEPROMPage() is different from a_data at global scope?

True, I got confused with the names there, but it shouldn't make a difference there, does it?
Even though I rename it in writeEEPROMPage() and pass the "real" a_data, sizeof(a_data) will return the same number of bytes...

If you rename it in writeEEPROMPage(), sizeof(a_data) returns the known size of the global array. But then it's easier to drop that parameter at all.

I will try something like this soon. But still I feel like it's a problem of the TinyWire library...

That implements an I2C reservation system. In my opinion a bad solution for broken architecture. If you get into the need to have multiple masters on an I2C bus you have done the planning of your project wrong.

Why don't you just make the Tiny an I2C slave and let the Teensy ask for the current value of the sensor at the time it needs the value? It can still save the value to the RAM afterwards if this is necessary.

Yeah it looks kind of improvised..
The thing is: ATtiny is supposed to sample and save to the FRAM all the time. When ATtiny detects a certain event in the sampled data, it wakes up the Teensy which is then supposed to read in the last few seconds of the analog readings from the FRAM. So ATtiny has to be a master as it is always running...

I now implemented the ATtiny wo be a single-master operating as follows:
Sample analog data -> store on FRAM -> when event happens: read back from FRAM -> send to Teensy

This is working fine but: I later want to sample with probably 1kHz, so I need to speed up the I2C-SCL. In the library there a definitions of:

#define T2_TWI    5 		// >4,7us
#define T4_TWI    4 		// >4,0us

This seems to define the clock speed of about 100kHz, when I suppose that T2 is the time to be HIGH and T4 for LOW, is that correct?
When I change these values to 1,3 and 0.6 us (which are values I found in the original AVR library) this speeds up the clock, but not always! :-\

In the picture it looks like every time the device address is transferred, the SCL (bottom line) becomes slower. When the data is transferred (next two bytes) it's faster again. Any explanation why this happens?

How many devices do you have on the bus?

During transmission of the address the bus collision and further checks are performed, what may slow down the transmission.

There is just the Teensy and the FRAM on the bus.

So there is nothing you can do about it?

I found out, that it's just slower during the transmission of the device address to the FRAM but during the whole transmission to the Teensy.

I also just had another idea for the multi-master approach:
Couldn't I use some kind of multiplexing to only pass the "desired" SDA/SCL to the FRAM? I mean, is it technically possible?

Every I2C device can slow down a transmission, if required.

I2C multiplexers and level shifters exist.

Every I2C device can slow down a transmission, if required.

How would that happen if the master dictating the clock signal?

pylon:
How do you prevent collisions on the I2C bus? Neither TinyWireM nor the Wire library support I2C multimaster. If you ever want to get that system reliable don't use I2C multimaster at all. I never saw nor heard of any multimaster setup working reliable in the long term.

What is the FRAM necessary for? Why don't you simply make the Tiny an I2C slave and the Teensy asks for the value when it needs it?

That is not required, hardware should take care of collisions. See https://www.nxp.com/docs/en/user-guide/UM10204.pdf

man clock stretching

So there's another wire between the two masters that allows the Teensy to assert that it's the 'real' master? Maybe two wires: one to request mastery and another to acknowledge "I just finished the last transmission that I was in the middle of when you asked." Otherwise the Teensy would have to make its request and then always wait at least one transaction time before starting to control the bus.

Then it's not really an I2C multi-master. It's just two masters that take turns based on external information outside the bus.

Have you checked you're not going to wear out the first address in the FRAM by writing to it at 1KHz?

looks like Teensy library does not support multi-master mode