6

I would like to use an Arduino as an i2c slave. But I require that the Arduino acts as multiple devices by registering itself with multiple i2c addresses.

This is probably not something one would normally do, but here is my reason for doing it:

I want to use an Arduino to act as Telemetry sensors for Spektrum Telemetry. The Telemetry receiver has a few i2c plugs which connects to multiple sensors (current 0x02, voltage 0x03, airspeed 0x11, etc) each that have a fixed i2c address which the Telemetry receiver expects.

I would like to use one Arduino to act as all these devices by registering itself with all of the above addresses, and responding appropriately with the readings.

I could use one Arduino per sensor, which seems silly as I can perform all these readings with one Arduino pro-mini.

I know you can register the Arduino using

Wire.begin(0x02); 

But I require something similar to this (pseudo code)

Wire.begin(0x02, 0x03, 0x11);

And when a request is received, I need to know with what address the Arduino was queried.

For example (pseudo code)

void receiveEvent(byte address, int bytesReceived){
  if(address == 0x02){
    // Current reading
  }
  else if(address == 0x03){
    // Voltage reading
  }
  else if(address == 0x11){
    // Airspeed reading
  }
}

Any advice would be appreciated.

Mykola
  • 3,343
  • 6
  • 23
  • 39
trojanc
  • 171
  • 1
  • 7
  • 2
    If using platforms other than the original Arduino is an option, ATxmegaA1 devices come with 4 I2C interfaces and there's an [ATxmega fork of Arduino](https://github.com/Xmegaduino) called Xmegaduino on github. – vega8 Jan 09 '16 at 15:32

5 Answers5

8

It is not possible to make the Arduino listen to to multiple slave addresses by using the Wire library since Wire.begin() only allows to pass a single slave address.


Even the Atmel ATmega microcontroller on which most Arduinos are based only allows its hardware 2-wire serial interface (TWI) to be set to a single 7-bit address via its 2-wire address register TWAR. However, it is possible to work around this limitation by masking one or more address bits using the TWI address mask register TWAMR as documented (somewhat briefly) in e.g. this ATmega datasheet section 22.9.6:

The TWAMR can be loaded with a 7-bit Salve (sic!) Address mask. Each of the bits in TWAMR can mask (disable) the corresponding address bits in the TWI address Register (TWAR). If the mask bit is set to one then the address match logic ignores the compare between the incoming address bit and the corresponding bit in TWAR.

So we would first have to set up the mask bits based on all I2C addresses we want to respond to by OR'ing them and shifting right to match the TWAMR register layout (TWAMR holds mask in bit7:1, bit0 is unused):

TWAMR = (sensor1_addr | sensor2_addr | sensor3_addr) << 1;

The main problem from here on will be to find out which particular I2C address was queried (we only know it was one that matches the address mask). If I interpret section 22.5.3 correctly, stating

The TWDR contains the address or data bytes to be transmitted, or the address or data bytes received.

we should be able to retrieve the unmasked I2C address from the TWDR register.

ATmega TWI operation is interrupt-based, more specifically, it utilizes a single interrupt vector for a plethora of different TWI events indicated by status codes in the TWSR status register. In the TWI interrupt service routine, we'll have to

  1. make sure the reason why we've entered the ISR is because we've been queried. This can be done by checking TWSR for status code 0xA8 (own SLA+R has been received)
  2. decide which sensor data to send back to the master based on what I2C address was actually queried by checking the last byte on the bus in TWDR.

This part of the ISR could look something like this (untested):

if (TWSR == 0xA8) { // read request has been received
  byte i2c_addr = TWDR >> 1; // retrieve address from last byte on the bus
  switch (i2c_addr) {
    case sensor1_addr:
      // send sensor 1 reading
      break;
    case sensor2_addr:
      // send sensor 2 reading
      break;
    case sensor3_addr:
      // send sensor 3 reading
      break;
    default:
      // I2C address does not match any of our sensors', ignore.
      break;
  }
}

Thanks for asking this interesting question!

vega8
  • 534
  • 1
  • 5
  • 25
  • Thankyou @vega8, your answer is very descriptive! I will give this a try and update if it works. – trojanc Jan 09 '16 at 17:17
  • Great answer. One question; Is there something limiting you in the amount of addresses (aside from the 7 bit address)? Is it possible to do 6 addresses specifically? Like: `(0x28 | 0x29 | 0x2A | 0x2B | 0x2C | 0x2D) << 1`. I'd like to do this to be able to get data from specific sensors on the board, or is it better to just write an address to the board to specify which sensor I'd like to query, before actually requesting it? – Nathan Prins Nov 14 '16 at 23:41
  • 1
    This idea actually works!! Here is a repository with the code: https://github.com/alexisgaziello/TwoWireSimulator The only thing I want to add is that the correct way of calculating the mask is not with and AND operator. – Alexis Dec 04 '18 at 11:03
4

I really do like vega8's answer, but I'd also like to mention that if your I2C master isn't going to clock things incredibly fast, using a software-based implementation of I2C would also be feasible and give you the freedom you want.

You might want to consider that approach if rough calculation shows that the time spent in the TWI ISR is too high and interrupts might start to overlap.

Marcus Müller
  • 34,677
  • 4
  • 53
  • 94
  • 1
    Good comment, thx! This would mean implementing much of what's already there in hardware, though (including increased program size + additional testing). Certainly more flexibility, typical trade-off situation. – vega8 Jan 09 '16 at 14:39
1
void setup() 
{
    Wire.begin(0x11 | 0x12);       // Adr 11 and 12 are used for Alt and Speed by Spectrum DX
    Wire.onRequest(requestEvent);  // register callback function
    TWAMR = (0x11 | 0x12) << 1;    // set filter for given adr
}
  • 1
    Welcome to SO. Please remember to add 4-space indentation to your code so that it is displayed correctly. Also, I'd recommend to add some annotation to it. – YakovL Oct 16 '16 at 21:37
1
void requestEvent() {
    int adr = TWDR >> 1;  // move 1 bit to align I2C adr
    if (adr == 0x12)      // check for altitude request at adr 12
        Wire.write(tmpSpektrumDataAlt, 16); // send buffer
    if (adr == 0x11)      // check for speed request at adr 11
        Wire.write(tmpSpektrumDataSpd, 16); // send buffer
}

This works with a Spectrum DX8 with telemetry module. The Spectrum interface was made public on Sectrums home page. Technical documents.

  • Welcome to SO. Please remember to add 4-space indentation to your code so that it is displayed correctly. – YakovL Oct 16 '16 at 21:42
0

There could be other devices on the I2C bus, the TWAMR should be set with as less bits as possible. So I think the better way to calculate the mask is:

AddrOr =  Addr1 | Addr2 | Addr3 | Addr4 ... 
AddrAnd = Addr1 & Addr2 & Addr3 & Addr4 ...
TWAMR = (AddrOr ^ AddrAnd) << 1

while TWAR can be set as either AddrOr or AddrAnd In this way we can limit the possibility of address conflict to the minimum

John Xu
  • 1
  • 2