[Arduino] Software I2C master and slave

Hello everybody,

I have seen a few software I2C libraries for Arduino, but all of them (at least the ones that I have looked at) were master-only.

Is there any library which can be used in either master or slave modes?

Thank you,
Patryk

Implementing the I2C slave protocol in software is quite difficult. You have to do a lot of the work in ISRs and the correct recognition of the start and stop conditions are also quite heavy to implement. Getting all that stuff done with 100kHz interrupt rate and still leaving some time for the Arduino to do other calculations is probably a waste of time.

Each Arduino has a built in I2C hardware interface. Why do you need a software emulation? If you need more than one I2C, use the hardware interface for the slave and emulate the master or use a microcontroller that has two hardware interfaces (Arduino Due).

The default library that's used by the IDE supports both a master, and a slave Arduino. It's just a matter of using the right functions/commands at each end of the link. However, peripheral devices usually have supporting libraries that access it's features. The I2C interface is built into the peripheral's chips.

pylon:
Why do you need a software emulation?

I am just considering a different communication protocol. I have like 10 slave Arduinos with an MPU6050 connected to each of them, and I want a master to get readings from each Arduino and send it to the PC. I'm not sure if using I2C between the master and slave devices is a viable option, though.

That will work nicely. You can set up each slave with it's own I2C address and query each from the master. A command out will tell the slave to expect a request for some particular data and the slave will send it back on the next communication. You can send up to 32 bytes back in a packet.

Here's a link to the master/slave scheme. I've used it successfully sending multiple packets from each of four Nano slaves to one Mega master.

Arctic_Eddie, thanks for your input. Just to verify that my assumptions are right, let me describe the circuit in more detail.

Each slave has an MPU6050 attached to it. The gyro has a fixed I2C address, so I'm using an I2C multiplexer that connects the master to slaves to avoid conflicts. But do you think that it's necessary at all?

Thanks,
Patryk

If the master is connected directly to the MPU6050 devices then you will need some kind of multiplexing or use a separate CS pin for each, easier. A Mega2560 would be a good choice for the master. If each MPU6050 has it's own slave Arduino, Nano or Pro Mini, then multiplexing is not needed. Each slave Arduino would be programmed to assume a different I2C address. The link shows you how to do that.

How much distance do you have between the master and the furthest slave? I2C is intended for short runs, usually less than one meter. If the distance is much longer then RS485 adapters and software serial on the slaves will work but the baud value should be 38400 or less.

Arctic_Eddie:
If each MPU6050 has it's own slave Arduino, Nano or Pro Mini, then multiplexing is not needed. Each slave Arduino would be programmed to assume a different I2C address.

Like I said, my MPU6050 cannot have its address changed, so I think I still need a multiplexer, because both the slaves and MPUs are connected to the same I2C bus. I can knock up a Fritzing sketch if you'd like me to illustrate how the wiring is done.

Arctic_Eddie:
How much distance do you have between the master and the furthest slave? I2C is intended for short runs, usually less than one meter. If the distance is much longer then RS485 adapters and software serial on the slaves will work but the baud value should be 38400 or less.

I think that the distance might be longer than one meter. I will have a peek at RS485, never actually heard of that before.

If you have one master connected directly to multiple sensors then a MUX will be needed or separate CS pins. For longer runs then there are buss extenders for I2C.

When you say 'slave', are you referring to the MPU6050 sensor itself or another Arduino dedicated to each sensor. If the later then an RS485 adapter would be a solution with software serial on the slave side.

A standard schematic is preferred as it's more readable in understanding circuit design.

Arctic_Eddie:
If you have one master connected directly to multiple sensors then a MUX will be needed or separate CS pins.

CS pins? As far as I know, there's no CS pin in I2C. Do you mean SPI?

Arctic_Eddie:
When you say 'slave', are you referring to the MPU6050 sensor itself or another Arduino dedicated to each sensor. If the later then an RS485 adapter would be a solution with software serial on the slave side.

By 'slave' I mean another Arduino, yes.

OK, so your suggestion is to use I2C for communication between a slave Arduino and its associated gyro (there's no other way, actually) and connecting the master Arduino with slave Arduinos using RS485 adapters, is that right?

Arctic_Eddie:
A standard schematic is preferred as it's more readable in understanding circuit design.

Sure, I'll upload it tomorrow for your reference, but I feel like you've already got an idea of my setup :slight_smile:

Regards,
Patryk.

Some breakout boards have a CS or EN pin that can be used to make it active or dormant.

OK, so your suggestion is to use I2C for communication between a slave Arduino and its associated gyro (there's no other way, actually) and connecting the master Arduino with slave Arduinos using RS485 adapters, is that right?

Yes, that will give you 'addressable party line' communication with all slaves and allow for longer cable runs. You can also use a minimal Arduino with the MPU6050 such as a Nano. The master can be whatever you need to do the necessary tasks.

See response #1 at this post. It has links to the adapters and example code.

Arctic_Eddie:
See response #1 at this post. It has links to the adapters and example code.

I ordered the same adapters as you suggested this morning, but do you think they'll work for 10 slaves? I mean they have terminator resistors at the end and AFAIK they should be used at the end of the bus.

Thanks,
Patryk

See my last post in this thread in this forum for comments on the terminating resistors. You can remove all and add one externally to the last adapter via the A/B terminal block. Their location on the adapter board is located at R7/121.

Thank you so much for bearing with me! Cheers, and kudos.

Glad to help. Good luck with your project.

E

A long time ago (2004) i was very in to coding for the Atmel AVR. I made a whole collection of highly optimized "libraries" (back then code-size and execution-speed were even more important because of less available memory and lower clockspeeds).
Only since a short time i started coding for Arduino, and to my happy surprise, i noticed they also use the Atmel AVR MCUs. Only most inside AVR stuff is now "hidden" in a lower level and coding is done in C.

My old routines should work just fine on todays AVR, except they are all written in assembler. I commented my codes extensively, but most comments are in Dutch. That combination, asm+Dutch, will not be easy to read for most people.

Anyway... I myself want to make an I2C-slave of one of my Arduinos. I searched the web, but did not really find a ready-to-use I2C library. So i think i will adapt my old code, and make a easy to use Arduino software I2C library. But it may take a while, because i'm not in a hurry. busy busy already.

I wanted to upload my complete collection on my ISP-webspace, but i have problems connecting via FTP. Later i'll try again... For now, i just attach some zipped code to this post. It is the code of an I2C-slave that uses no interrupts at all. It's a software I2C implementation, so any pins can be used.

It may be of some use to somebody now. In any case it's interesting to check out.

One of these days the Arduino library will follow.

I2C_slave__no_irq.zip (27.6 KB)

I have rewritten my own old AVR assembler code.
Now it is running perfectly on an(y) Arduino.

A Software I2C Slave that uses no interrupts.
The codesize is very small (708 bytes) so it even fits into an ATTiny.

I did not calculate, nor test the maximum speed this implementation can handle.
(There is not much left to optimize in the code..)

#define I2C_SLAVE_ADDRESS 73

#define SCL_BIT PB3
#define SDA_BIT PB4
#define I2C_DDR DDRB
#define I2C_OUT PORTB
#define I2C_IN  PINB


/* These 2 functions retrieve & provide data. */
extern "C" {
  volatile uint8_t I2C_data;
  void InComingData(void) {
        #ifdef DEBUG_PRINT
         Serial.println(I2C_data,HEX);
       #endif
  }
  void OutGoingData(void) {
       I2C_data = 0xAA;
       #ifdef DEBUG_PRINT
         Serial.print("    ");    Serial.println(I2C_data,HEX);
       #endif
  }
}




void setup() {
  #ifdef DEBUG_PRINT
    Serial.begin(115200);
    while(!Serial);
  #endif

   TIMSK0 = 0;
   __asm__ __volatile__ (
           "cli" "\n"
           "cbi %[I2CDDR], %[SCLBIT]" "\n"
           "cbi %[I2COUT], %[SCLBIT]" "\n"
           "cbi %[I2CDDR], %[SDABIT]" "\n"
           "cbi %[I2COUT], %[SDABIT]" "\n"
    :: [I2CDDR] "I" (_SFR_IO_ADDR(I2C_DDR)),
       [I2COUT] "I" (_SFR_IO_ADDR(I2C_OUT)),
       [SCLBIT] "I" (SCL_BIT),
       [SDABIT] "I" (SDA_BIT)
   );
}


void loop() {
  __asm__ __volatile__ (
    "HandleTransaction:" "\n"
           "rcall I2C_activity" "\n"
           "brts StartCondition" "\n"
           "brhs StopCondition" "\n"
           "rjmp HandleTransaction" "\n"
    "StartCondition:" "\n"
           "sbic %[I2CIN], %[SCLBIT]" "\n"
           "rjmp StartCondition" "\n"
           "rcall slave_readByte" "\n"
           "brts StartCondition" "\n"
           "brhs StopCondition" "\n"
           "mov R19, R18" "\n"
           "lsr R19" "\n"
           "cpi R19, %[SLAVE_ADDR]" "\n"
           "brne StopCondition" "\n"
           "rcall slave_writeACK" "\n"
           "andi R18, 0b00000001" "\n"
           "brne MasterRead" "\n"
    "MasterWrite:" "\n"
           "rcall slave_readByte" "\n"
           "brts StartCondition" "\n"
           "brhs StopCondition" "\n"
           "rcall slave_writeACK" "\n"
           "sbi %[I2CDDR], %[SCLBIT]" "\n"
           "sts I2C_data, R18" "\n"
           "call InComingData" "\n"
           "cbi %[I2CDDR], %[SCLBIT]" "\n"
           "rjmp MasterWrite" "\n"
    "MasterRead:" "\n"
           "sbi %[I2CDDR], %[SCLBIT]" "\n"
           "call OutGoingData" "\n"
           "lds R18, I2C_data" "\n"
           "cbi %[I2CDDR], %[SCLBIT]" "\n"
           "rcall slave_writeByte" "\n"
           "brts StartCondition" "\n"
           "brhs StopCondition" "\n"
           "rcall slave_readACK" "\n"
           "breq MasterRead" "\n"
    "StopCondition:" "\n"
           "cbi %[I2CDDR], %[SCLBIT]" "\n"
           "cbi %[I2COUT], %[SCLBIT]" "\n"
           "cbi %[I2CDDR], %[SDABIT]" "\n"
           "cbi %[I2COUT], %[SDABIT]" "\n"
           "rjmp DoneTransaction" "\n"

    "I2C_activity:" "\n"
           "in R16, %[I2CIN]" "\n"
           "andi R16, (1<<%[SCLBIT] | 1<<%[SDABIT])" "\n"
    "ac1:" "in R17, %[I2CIN]" "\n"
           "andi R17, (1<<%[SCLBIT] | 1<<%[SDABIT])" "\n"
           "cp R16, R17" "\n"
           "breq ac1" "\n"
           "clh" "\n"
           "clt" "\n"
           "sbrs R16, %[SCLBIT]" "\n"
           "rjmp ac2" "\n"
           "sbrs R17, %[SCLBIT]" "\n"
           "rjmp ac2" "\n"
           "sbrs R17, %[SDABIT]" "\n"
           "set" "\n"
           "sbrc R17, %[SDABIT]" "\n"
           "seh" "\n"
    "ac2:" "ret" "\n"

    "slave_readByte:" "\n"
           "ldi R18, 0b00000001" "\n"
    "rb1:" "sbis %[I2CIN], %[SCLBIT]" "\n"
           "rjmp rb1" "\n"
           "in R19, %[I2CIN]" "\n"
           "rcall I2C_activity" "\n"
           "brts rb2" "\n"
           "brhs rb2" "\n"
           "sec" "\n"
           "sbrs R19, %[SDABIT]" "\n"
           "clc" "\n"
           "rol R18" "\n"
           "brcc rb1" "\n"
           "clz" "\n"
           "clh" "\n"
    "rb2:" "ret" "\n"

    "slave_writeByte:" "\n"
           "ldi R19, 8" "\n"
    "wb1:" "lsl R18" "\n"
           "brcs wb2" "\n"
           "sbi %[I2CDDR], %[SDABIT]" "\n"
    "wb2:" "brcc wb3" "\n"
           "cbi %[I2CDDR], %[SDABIT]" "\n"
    "wb3:" "sbis %[I2CIN], %[SCLBIT]" "\n"
           "rjmp wb3" "\n"
           "rcall I2C_activity" "\n"
           "brts wb4" "\n"
           "brhs wb4" "\n"
           "dec R19" "\n"
           "brne wb1" "\n"
    "wb4:" "cbi %[I2CDDR], %[SDABIT]" "\n"
           "ret" "\n"

    "slave_writeACK:" "\n"
           "sbi %[I2CDDR], %[SDABIT]" "\n"
    "wa1:" "sbis %[I2CIN], %[SCLBIT]" "\n"
           "rjmp wa1" "\n"
    "wa2:" "sbic %[I2CIN], %[SCLBIT]" "\n"
           "rjmp wa2" "\n"
           "cbi %[I2CDDR], %[SDABIT]" "\n"
           "ret" "\n"

    "slave_writeNACK:" "\n"
           "cbi %[I2CDDR], %[SDABIT]" "\n"
    "wn1:" "sbis %[I2CIN], %[SCLBIT]" "\n"
           "rjmp wn1" "\n"
    "wn2:" "sbic %[I2CIN], %[SCLBIT]" "\n"
           "rjmp wn2" "\n"
           "ret" "\n"

    "slave_readACK:" "\n"
    "ra1:" "sbis %[I2CIN], %[SCLBIT]" "\n"
           "rjmp ra1" "\n"
           "sez" "\n"
           "sbic %[I2CIN], %[SDABIT]" "\n"
           "clz" "\n"
    "ra2:" "sbic %[I2CIN], %[SCLBIT]" "\n"
           "rjmp ra2" "\n"
           "ret" "\n"

    "DoneTransaction:" "\n"
  :: [I2CIN] "I" (_SFR_IO_ADDR(I2C_IN)),
     [I2COUT] "I" (_SFR_IO_ADDR(I2C_OUT)),
     [I2CDDR] "I" (_SFR_IO_ADDR(I2C_DDR)),
     [SCLBIT] "I" (SCL_BIT),
     [SDABIT] "I" (SDA_BIT),
     [SLAVE_ADDR] "M" (I2C_SLAVE_ADDRESS),
     "e" (InComingData),
     "e" (OutGoingData)
  );
}
#include <Wire.h>

//#define DEBUG_PRINT

#define SLAVE_ADDRESS 73

uint8_t valueTX = 0x41;

void bleepLED(int ms_on, int count=1, int ms_off=0) {
  for (int i=count; i>0; i--) {
    digitalWrite(LED_BUILTIN, HIGH);
    delay(ms_on);
    digitalWrite(LED_BUILTIN, LOW);
    delay(ms_off);
  }
}

void bleepHit() {
  for (int i=0; i<15; i++) bleepLED(150-i*10,1,i*10);
}


void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  
  Wire.begin();        // join i2c bus (address optional for master)

  #ifdef DEBUG_PRINT
    Serial.begin(9600);  // start serial for output
    while (!Serial);
  #endif

  bleepLED(101,19,66);
}


void loop() {
  delay(4000);
  bleepLED(200);
  #ifdef DEBUG_PRINT
    Serial.print("writing 1 byte of data to the slave: ");  Serial.println(valueTX++);
  #endif
  Wire.beginTransmission(SLAVE_ADDRESS);
  Wire.write(valueTX);
  Wire.endTransmission();

  delay(4000);
  bleepLED(200);
  #ifdef DEBUG_PRINT
    Serial.print("writing 5 bytes of data to the slave");  Serial.println("/"multi/"");
  #endif
  Wire.beginTransmission(SLAVE_ADDRESS);
  Wire.write("multi");
  Wire.endTransmission();

  delay(4000);
  bleepLED(200);
  #ifdef DEBUG_PRINT
    Serial.print("request 1 byte from slave");
  #endif
  Wire.requestFrom(SLAVE_ADDRESS, 1);    // request 1 byte from slave device #73
  while (Wire.available()) {  // slave may send less than requested
    uint8_t c = Wire.read();     // receive a byte as character
    #ifdef DEBUG_PRINT
      Serial.println(c, HEX);     // print the character value
    #endif
    if (c == 0xAA) bleepHit();
  }

}

i2c_softslave_demo.zip (3.4 KB)

1 Like