Read/Write I2C with ATtiny85

Hi!

I’ve been working on a project for the last few days and spent a LOT of time googling, reading and testing. I’m kinda running out of ideas now and instead of bumping my head to the wall I thought posting my issue here in the hopes someone might be able to point me towards the right direction

I’ve got an Arduino UNO programmed as I2C Master and an ATtiny85 as an I2C slave. An single Adafruit Neopixel LED is attached to the slave.

When running the two sketches below everything actually works perfect…for about a minute or so.
While during correct operations the Neopixel LED is actually changing color correctly and the serial monitor on the Uno shows the correctly updated R-G-B values, things change after about 1 minute of running.
The LED keeps blinking, which tells me the ATtiny is not frozen.
The UNO keeps sending I2C and is receiving I2C data…but the data is constant. The updated RGB that the Uno (master) has been sending to the ATtiny slave are somehow not handled (correctly) by the slave.

Any thoughts are more then appreciated. I’m totally lost and do not know which directions to go from here so any pointers would be more then welcome.

The Master - Arduino Uno -

#include <Wire.h>

#define I2C_MASTER_ADDR 0x04
#define I2C_SLAVE_ADDR 0x05

/*
* Software config
*/
int pollInterval = 700;//Milliseconds

//Demo's
#define demoLedInterval 150//In milliseconds
boolean demoRequest = false;

/*
* Internal variables
*/
unsigned long lastPoll = 0;

int demoLedState = 0;
unsigned long lastDemoLedUpdate = 0;

/*
* Setup function
*/
void setup() 
{
 Wire.begin(I2C_MASTER_ADDR);  // join i2c bus (address optional for master)
 Serial.begin(115200);         // start serial for output
 Serial.println("Setup complete");
}

/*
* The main loop
*/
int i =0; 
void loop() 
{
 if( (millis()-lastPoll) > pollInterval)
 {
   int red=0; int green=0; int blue=0;

     if(i==0)
     {
       Serial.println("Write data to slave - set led red");
       Wire.beginTransmission(I2C_SLAVE_ADDR);
       Wire.write(0xC2);//Command
       Wire.write(0x00);
       Wire.write(255);//Data
       Wire.endTransmission();
       Wire.beginTransmission(I2C_SLAVE_ADDR);
       Wire.write(0xC3);//Command
       Wire.write(0x00);
       Wire.write(0);//Data
       Wire.endTransmission();
       Wire.beginTransmission(I2C_SLAVE_ADDR);
       Wire.write(0xC4);//Command
       Wire.write(0x00);
       Wire.write(0);//Data
       Wire.endTransmission();
     }
     if(i==1)
     {
       Serial.println("Write data to slave - set led green");
       Wire.beginTransmission(I2C_SLAVE_ADDR);
       Wire.write(0xC2);//Command
       Wire.write(0x00);
       Wire.write(0);//Data
       Wire.endTransmission();
       Wire.beginTransmission(I2C_SLAVE_ADDR);
       Wire.write(0xC3);//Command
       Wire.write(0x00);
       Wire.write(255);//Data
       Wire.endTransmission();
       Wire.beginTransmission(I2C_SLAVE_ADDR);
       Wire.write(0xC4);//Command
       Wire.write(0x00);
       Wire.write(0);//Data
       Wire.endTransmission();
     }
     
   i++; if(i>1) i =0;

   /*
    * General request (without data). Should return something though
    */
   Wire.requestFrom(I2C_SLAVE_ADDR, 7);//Request N bytes, N=byte_len
   delay(10);
   while (Wire.available())
   {
     uint8_t next_byte = Wire.read();
     Serial.print(next_byte);Serial.print(" ");    
   }
   Serial.println("\n");
   
   lastPoll = millis();
 }
 
}

The Slave code - ATtiny 85 - Main file

#include <EEPROM.h>
#include <OneWire.h>
#include <TinyWireS.h>
#include <Bounce2.h>
#include <Adafruit_NeoPixel.h>
#ifdef __AVR__ //Which will be true for ATtiny85
  #include <avr/power.h>
#endif

#define I2C_SLAVE_DEFAULT_ADDR 0x05
#include "namespaces.h"//Include at last to make sure all libs are present
#define BUTTON_DEBOUNCE 5//Debounce milliseconds
#define NEOPIXEL_PIN  1
#define BUTTON_PIN    3
#define DS2401_PIN    4
#define NUMPIXELS     1

Bounce button = Bounce();
Adafruit_NeoPixel neopixel = Adafruit_NeoPixel(NUMPIXELS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
OneWire ds(DS2401_PIN);

void setup() 
{
  uint32_t _now = millis();
  
  TinyWireS.begin(I2C_SLAVE_DEFAULT_ADDR);
  TinyWireS.onReceive(I2C::receiveEvent);
  TinyWireS.onRequest(I2C::requestEvent);

  neopixel.begin();
  neopixel.setPixelColor(NUMPIXELS-1, neopixel.Color(255, 255, 255) );
  neopixel.show();

  pinMode(BUTTON_PIN, INPUT_PULLUP);
  button.attach(BUTTON_PIN);
  button.interval(BUTTON_DEBOUNCE);
}

unsigned long lastBlink = 0;
boolean lastBlinkState;

void loop() 
{

  button.update();
  if(VARS::led_needs_update)
  {
    led_update();
    VARS::led_needs_update = false;
  }
 
  if(button.fell())
  {
    VARS::button_state = HIGH;
  }
  if(button.rose())
  {
    VARS::button_state = LOW;
  }

  //testblink
  if ( (millis()-lastBlink) > 600)
  {
    if(lastBlinkState)
    {
      neopixel.setPixelColor(NUMPIXELS-1, 0,0,0 );
      neopixel.show();
      lastBlinkState = false;
    }
    else
    {
      led_update();
      lastBlinkState = true;
    }
    lastBlink = millis();
  }
  
  // This needs to be here for the TinyWireS lib
  TinyWireS_stop_check();
}

/*
 * Helper functions
 */
void led_update()
{
  neopixel.setPixelColor(NUMPIXELS-1, 
    VARS::led_red,
    VARS::led_green,
    VARS::led_blue
  );
  neopixel.show();
}

The Slave code - ATtiny 85 - namespaces.h

namespace VARS
{
  volatile boolean button_state = 0;
  volatile int led_red = 0;
  volatile int led_green = 0;
  volatile int led_blue = 0;
  uint8_t sn_family;
  uint8_t sn_bit1;

  volatile boolean led_needs_update = false;
  int update_interval     = 300;
  unsigned long last_update = 0;
}

namespace EEPROM_DATA 
{
  uint8_t default_device_addr   =     I2C_SLAVE_DEFAULT_ADDR;
  uint8_t this_device_address   =     0x05;
  
  bool device_addr_valid(uint8_t addr) 
  { 
    return (addr > 0x04 && addr < 0x7E); 
  }
  bool store_device_addr(uint8_t new_device_addr) 
  {
    if (!device_addr_valid(new_device_addr)) return false;//cancel if out of bounds. Assume corrupt data.
    EEPROM.write(EEPROM_DATA::this_device_address, new_device_addr);//Set new i2c address into EEPROM
    return true;
  }
  uint8_t get_device_addr() 
  {
    uint8_t _addr = EEPROM.read(EEPROM_DATA::this_device_address);
    if (!device_addr_valid(_addr)) 
    {
      _addr = EEPROM_DATA::default_device_addr;
      store_device_addr(_addr); //update EEPROM with a valid/default device id.
    }
    return _addr;
  }
}

namespace COMMANDS 
{
  enum {
    assign_new_device_addr  =     0xC1,
    set_led_red             =     0xC2,
    set_led_green           =     0xC3,
    set_led_blue            =     0xC4
  };
  bool do_command(uint8_t cmd, uint16_t cmd_data) 
  {
    switch (cmd) 
    {
      case COMMANDS::assign_new_device_addr:
        uint8_t new_device_addr;
        new_device_addr = uint8_t(cmd_data & 0x007F);//ensure the address is 7-bit.
        return EEPROM_DATA::store_device_addr(new_device_addr);//return true if saved, false if invalid address.
        break;
  
      case COMMANDS::set_led_red:
        VARS::led_red = int(cmd_data);
        VARS::led_needs_update = true;
        break;
  
      case COMMANDS::set_led_green:
        VARS::led_green = int(cmd_data);
        VARS::led_needs_update = true;
        break;
  
      case COMMANDS::set_led_blue:
        VARS::led_blue = int(cmd_data);
        VARS::led_needs_update = true;
        break;
    }
  }
}

//===========================================================================
int pointer = 0;
namespace I2C{
  void requestEvent()
  {
    TinyWireS.send( VARS::button_state );
    TinyWireS.send( VARS::led_red );
    TinyWireS.send( VARS::led_green );
    TinyWireS.send( VARS::led_blue );
    
  }

  void receiveEvent(uint8_t byte_count) 
  {
    //first byte is command, next 2 bytes is data for command. Total must equal 3 bytes.
    if (!TinyWireS.available() || byte_count != 3 ) return;
  
    uint8_t cmd = TinyWireS.receive();
    uint16_t cmd_data = (TinyWireS.receive() << 8);
    cmd_data += TinyWireS.receive();
  
    COMMANDS::do_command(cmd, cmd_data);
  }
  
}

What is the lenght of the wires your are using?

I think it's clear that the problem is with the tiny slave code - it seems unlikely that it's a hardware issue since you're still able to talk to it.

Maybe someone who is better at processing code stored across two files will be able to spot the bug; I can't.

Does it always last the same length of time before failing? Time it.
Does changing how often the master talks to it change how long it takes to fail? This will tell you whether it depends on time (hmm, look at anywhere you're using millis()) or the total number of communication transactions (be suspicious of the I2C stuff itself).

DrAzzy:
Maybe someone who is better at processing code stored across two files will be able to spot the bug; I can't.

There is a bug in usiTwiSlave.c. Technically two (serious) bugs.

rxCount and txCount are shared with an interrupt service routine. Both sides read and write which requires the variables to be protected by a critical section. (Serial in the Arduino API avoids the problem by always calculating the counts.)

Can you provide a bit more detail on that? I have a similar (I think derived from the same codebase) USI TWI library in my Unified Wire library (part of ATTinyCore) and I'd like to make sure the same bug isn't present there.

Not in the context of an interrupt...

Interrupt service routine...

The first one has to be performed with interrupts disabled (critical section).

Thanks for all the suggestions and thoughts.
They made me think a big deal. I ended up fixing the ‘freeze’ issue:

For anyone interested, the updated code is below.

There is however an annoying issue with the code below: although request multiple bytes from the slave, I have not been able to receive more then a single byte. The wire.available() loop will only print the first byte. The other requested bytes are always being returned as 255.
Anyone got any thoughts on that?

The Master - Arduino Uno

/*
 * 
 */

#include <Wire.h>

#define I2C_MASTER_ADDR 0x04
#define I2C_SLAVE_ADDR 0x05

/*
 * Software config
 */
int pollInterval = 700;//Milliseconds

/*
 * Internal variables
 */
unsigned long lastPoll = 0;

/*
 * Setup function
 */
void setup() 
{
  Wire.begin(I2C_MASTER_ADDR);  // join i2c bus (address optional for master)
  Serial.begin(115200);         // start serial for output
  Serial.println("Setup complete");
}

/*
 * The main loop
 */
int i = 0;
void loop() 
{
  
  /*
   * 
   */
  if( (millis()-lastPoll) > pollInterval)
  {
    Serial.println("Write data to slave");
    Wire.beginTransmission(I2C_SLAVE_ADDR);
    Wire.write(0x01);//Register to start at
    switch(i)
    {
      case 0:
        Wire.write(255);
        Wire.write(0);
        Wire.write(0);
        i++;
        break;
      case 1:
        Wire.write(0);
        Wire.write(255);
        Wire.write(0);
        i++;
        break;
      case 2:
        Wire.write(0);
        Wire.write(0);
        Wire.write(255);
        i = 0;
        break;
    }
    Wire.endTransmission();
    delay(1);//Dont let the slave panic
    
    //Set the register pointer back to 0x01
    Wire.beginTransmission(I2C_SLAVE_ADDR);
    Wire.write(0x01);//Register to start at
    Wire.endTransmission();
    delay(1);//Dont let the slave panic

    //Get values from the three registers up form 0x01
    Wire.requestFrom(I2C_SLAVE_ADDR, 4);//Request N bytes
    while (Wire.available())
    {
      uint8_t next_byte = Wire.read();
      Serial.print(next_byte);Serial.print(" ");    
    }
    Serial.println("\n");
    
    lastPoll = millis();
  }//End if time to poll again
  
}//End loop

The Slave code - ATtiny 85 - Main file

#include <EEPROM.h>
#include <OneWire.h>
#include <TinyWireS.h>
#include <Bounce2.h>
#include <WS2812.h>
#ifdef __AVR__ //Which will be true for ATtiny85
  #include <avr/power.h>
#endif

#define I2C_SLAVE_DEFAULT_ADDR 0x05

#include "namespaces.h"//Include at last to make sure all libs are present

//Software config
#define BUTTON_DEBOUNCE 5//Debounce milliseconds

//Hardware config
#define NEOPIXEL_PIN  1
#define BUTTON_PIN    3
#define DS2401_PIN    4

#define NUMPIXELS     1

/*
 * Initialize instances/classes
 */
Bounce button = Bounce();
WS2812 led(1);//1 led


void setup() 
{
  uint32_t _now = millis();
  
  //Start I2C
  uint8_t _device_addr = EEPROM_DATA::get_device_addr();
  TinyWireS.begin(I2C_SLAVE_DEFAULT_ADDR);
  TinyWireS.onReceive(I2C::receiveEvent);
  TinyWireS.onRequest(I2C::requestEvent);

  //Start Led
  led.setOutput(NEOPIXEL_PIN);
  cRGB value;
  value.b = 255; value.g = 0; value.r = 0;
  led.set_crgb_at(0, value); //Set value at LED found at index 0
  led.sync(); // Sends the value to the LED

  //Start Button
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  button.attach(BUTTON_PIN);
  button.interval(BUTTON_DEBOUNCE);

}

long blinkInterval = 600;
unsigned long lastBlink = 0;
boolean lastBlinkState;

void loop() 
{
  button.update();

  if(VARS::led_needs_update)
  {
    led_update();
    VARS::led_needs_update = false;
  }
 
  if(button.fell())
  {
    VARS::i2c_regs[0x00] = 0xFF;
  }
  if(button.rose())
  {
    VARS::i2c_regs[0x00] = 0x00;
  }
  
  // This needs to be here for the TinyWireS lib
  TinyWireS_stop_check();
}

/*
 * Helper functions
 */
void led_update()
{
  cRGB val;
  val.r = VARS::i2c_regs[0x01];
  val.g = VARS::i2c_regs[0x02];
  val.b = VARS::i2c_regs[0x03];
  led.set_crgb_at(0, val);
  led.sync(); // Sends the value to the LED
}

The Slave code - ATtiny 85 - namespaces.h (going to merge both slave files tough)

namespace VARS
{
  //These are available in a I2C response
  volatile boolean button_state = 0;
 
  cRGB value;

  //These are internal
  volatile boolean led_needs_update = false;
  long update_interval     = 300;
  unsigned long last_update = 0;

  /*
   * Register map:
   * 0x00 - Button state
   * 0x01 - led value red
   * 0x02 - led value green
   * 0x03 - led value blue
   * 
   * Total size: 4
   */
  const byte reg_size = 4;
  volatile uint8_t i2c_regs[reg_size];
  
  volatile byte reg_position;
}


//===========================================================================

namespace EEPROM_DATA 
{
  uint8_t default_device_addr   =     I2C_SLAVE_DEFAULT_ADDR;
  uint8_t this_device_address   =     0x05;
  
  bool device_addr_valid(uint8_t addr) 
  { 
    return (addr > 0x04 && addr < 0x7E); 
  }
  bool store_device_addr(uint8_t new_device_addr) 
  {
    if (!device_addr_valid(new_device_addr)) return false;//cancel if out of bounds. Assume corrupt data.
    EEPROM.write(EEPROM_DATA::this_device_address, new_device_addr);//Set new i2c address into EEPROM
    return true;
  }
  uint8_t get_device_addr() 
  {
    uint8_t _addr = EEPROM.read(EEPROM_DATA::this_device_address);
    if (!device_addr_valid(_addr)) 
    {
      _addr = EEPROM_DATA::default_device_addr;
      store_device_addr(_addr); //update EEPROM with a valid/default device id.
    }
    return _addr;
  }
}

//===========================================================================
namespace I2C{
  
  void requestEvent()
  {
    //Send the value on the current register position
    TinyWireS.send(VARS::i2c_regs[VARS::reg_position]);

    // Increment the reg position on each read, and loop back to zero
    VARS::reg_position++;
    if (VARS::reg_position >= VARS::reg_size)
    {
        VARS::reg_position = 0;
    }
    
  }

  void receiveEvent(uint8_t howMany)
  {
    if (howMany < 1)
    {
        return;// Sanity-check
    }

    VARS::reg_position = TinyWireS.receive();
    howMany--;
    if (!howMany)
    {
        return;// This write was only to set the buffer for next read
    }
    
    while(howMany--)
    {
        //Store the recieved data in the currently selected register
        VARS::i2c_regs[VARS::reg_position] = TinyWireS.receive();
        
        //Proceed to the next register
        VARS::reg_position++;
        if (VARS::reg_position >= VARS::reg_size)
        {
            VARS::reg_position = 0;
        }
    }
    VARS::led_needs_update = true;
    
  }
}