There are lots of examples on the internet on how to program an Arduino as an I2C master to communicate with I2C slave devices.  There are, however, very few examples out there on how to program your Arduino as a slave device.  One of the best sites I’ve seen for documentation on I2C slave programming is over at Nick Gammon’s blog

Well I thought I would take it a step further and put together a step by step guide showing you a real life example of how to make an I2C slave device using an Arduino. I did a lot of research while making my I2C GPS Shield and thought I’d pass along some of what I learned. 

So what are we going to make?

Well the first thing we need to do is pick some kind of non I2C “module” that we want to get data from and control using I2C.  We don’t want to pick something easy like an A2D converter or photosensor, we want something more challenging so we can cover almost every aspect of a real life I2C device. 

Let’s use a GPS as an example.  Why you ask? Well it has everything we’re looking for (configuration options and a steady stream of slow incoming data) and I just happen to have some experience with this so I should be able to write this guide a little faster!

What do we want our device to do?  Well what do all the other I2C devices do?  They give the user the ability to communicate with a module and download/upload data to/from a defined set of registers. 

So what are these registers and how do we set them up?  There are no hard and fast rules on what registers you need but for the sake of this guide let’s break our register structure down as follows:

  • Data register(s) – this one is pretty self-explanatory, it’s the data that you are looking to collect from your device.  In our case it could be things like latitude, longitude, time, etc… Other devices it could be an A2D reading, temperature, etc…you get the idea.  These registers are usually read-only.
  • Configuration register(s) – configuration registers are typically used when a device can be, well, configured to do different things.  Take for instance the GPS, it can be configured to a baud rate between 4800 and 115200bps.  An A2D converter can be configured to sample a voltage at say 100SPS or 3000SPS.  These registers are typically read/write registers. 
  • Mode register(s) – mode registers are where you might store information related to different modes of a device.  Unfortunately with a GPS there really are no different modes so we normally wouldn't use these registers but for the purpose of this guide we'll include one.  Other devices might have a continuous mode setting, single mode setting and possibly several levels of sleep mode settings.  These registers are typically read/write registers.
  • Status register(s) – status registers are useful when you want to implement interrupts or tell the user that the device is “busy” or simply no new data is available.  These registers are typically read only.
  • Identification register(s) – identification registers are used to store some type of ID for your device.  They could also be used to store such things as firmware revisions so the user knows what rev of the device they are working with.  In my opinion identification registers are one of the most important registers to have on a device especially when troubleshooting.  It’s probably one of the only registers that contain a known value that never changes.  Why is that important, well if you’ve ever had to troubleshoot communications problems with an I2C device you always go straight to reading the data from the ID register and compare it to the data sheet.  Since you know what the number is you can test the communication to that register.  If the number you get back is the same as the datasheet then you know the communication “should” be OK.

So now that we have some kind of naming conventions for the registers, let’s start thinking about what type of functionality we want our device to have.  We want the device to handle both multibyte read and multibyte write.  What that means is that we can send and/or receive multiple bytes at once between the start and stop bits.  Some devices only support single byte read and write which can significantly affect your throughput.  Finally, we want our device to have an external interrupt so the user doesn’t have to poll the slave device to find out when new data is available. 

Now that we know what type of functionality we’re going to have let’s list out a simple register map.  The register map will show you which data is in what register and the actual address locations.  We’re going to need to know how many registers we have when we start coding.  Let’s start by saying we are going to have one configuration register, one mode register, one identification register, one status register and three data registers.

Now that we’ve identified the registers we want to use we need to choose the order that we want to list them in the map.  It’s a good idea to put some thought into the order to make the communication as fluid as possible.  Again there are no rules but my own personal preference is to have the status register(s) listed first, followed by data registers, then the mode and configuration registers (we always want to try and group the writable registers together) and finally the identification register at the end.  Why do I prefer this order?  Well let’s take a step back and look at the device from the user’s point of view because ultimately we’re trying to design our device to meet their needs.  Basically, the user wants to configure the device to meet his specifications and then collect data from the device in the most efficient way possible.  With that in mind, here’s why I’ve chosen the particular register map layout that I did. 

When the user wants to collect the data, the first thing he’s going to want to know is if the data he’s receiving is valid or not.  Valid can mean several different things such as new data is available as compared to previously read data or data that passed a checksum test, etc… This is why we want the status register listed first followed by the data registers.  It would be inefficient and time consuming to address the slave device, download the status register, filter the register, then readdress the slave device and finally download the data registers.  By using this register map order the user can look at the first byte, decide whether the data is valid and then either stop communication and continue on with the rest of their program or continue downloading the data.

Following the data registers we’ll list the configuration and/or mode registers.  The important thing here is that we try and group all the writable registers together.  Why is this important?  Again let’s look at it from the user perspective.  The user will typically make a majority of the changes to the configuration and mode registers just once and that will usually take place in their Setup() function.  Again it is more efficient to address the slave device and just send all the configuration data at once as compared to addressing the slave device, sending one or two bytes to one address, readdressing the slave device and send a few more bytes to different addresses.

Finally at the end of the register map we can put the identification register(s).  Another benefit to putting identification and other registers that are constants at the end is it reduces the overhead of our code as we’re copying data from our temporary array to the data array.  Since the data never changes there's no need to update the registers.  Let's take a look at our map below:

 

Register Map
Address Register Description
0x00 Status Register
0x01 Latitude - MSB
0x02 Latitude
0x03 Latitude
0x04 Latitude - LSB
0x05 Longitude - MSB
0x06 Longitude
0x07 Longitude
0x08 Longitude - LSB
0x09 Speed - MSB
0x0A Speed - LSB
0x0B Mode Register
0x0C Configuration Register
0x0D Identification Register

 

Now before we begin I just want to make it clear that I’m not a very experienced programmer so I’m sure there are better more efficient ways to write the code using structures and unions and pointers, etc… I’m trying to keep this as simple as possible so most people can follow along.

 

Let’s start with the basic template and fill things in as we go:

 

#include <Wire.h>

 

#define  SLAVE_ADDRESS           0x29  //slave address,any number from 0x01 to 0x7F

 

void setup()

{

   Wire.begin(SLAVE_ADDRESS); 

   Wire.onRequest(requestEvent);

   Wire.onReceive(receiveEvent);

}

 

void loop(){

}

void requestEvent(){

}

void receiveEvent(int bytesReceived){

}              

 

As you can see there are a couple of interrupt service routines that are used on the slave side.  The receiveEvent() function is called when the master sends data to the slave (master sender mode i.e. Wire.beginTransmission()  ).  The number of bytes that are received is stored in the variable bytesReceived.  The actual data is stored in an internal buffer in the Wire library that can be accessed using the Wire.receive() command.

The requestEvent() function is called when the master requests data from the slave device (master receiver mode i.e. Wire.requestFrom()  ).  This is where we place the code, Wire.send(), to send the data to the master device.  We’ll talk more about this later.

We defined a few constants such as the slave address (which can be any number from 0x01 to 0x7F), the size of our registry map and finally the maximum amount of bytes we can receive from the master.  The next thing we need to do is start declaring some global variables.  The first variable we need is an array to hold all of the register data (byte registerMap[14]) .  The next variable we need is also going to be an array to temporarily hold the same register data (byte registerMapTemp[13]).  I’ll explain a little later on why it’s a good idea to use two arrays to hold the data. We’re going to need to declare a third array to store all the commands sent from the master (byte receivedCommands[3] ).  Why did we declare the size to three bytes?  We did this because, for our slave device, three bytes is the most amount of data that would ever need to be sent from the master.  The first byte sent from the master is always the register address.  For our device, we only have two writable registers in our map and they are the configuration register and mode register.  The rest of the registers are read only so they can’t be written too.

In our requestEvent() ISR we placed the Wire.send() command to send the entire register map out to the master.  Even though the code says it will send all fourteen bytes, we'll see later how we can control what data is actually sent out.

In our receiveEvent() ISR we set up a basic loop to collect all the commands sent from the master and store them in our array for future use.  As you can see from the code if the master tries to send more bytes than allowed we just simply throw the extra bytes away.

 

#include <Wire.h>

 

#define  SLAVE_ADDRESS         0x29  //slave address, any number from 0x01 to 0x7F

#define  REG_MAP_SIZE             14

#define  MAX_SENT_BYTES       3

 

/********* Global  Variables  ***********/

byte registerMap[REG_MAP_SIZE];

byte registerMapTemp[REG_MAP_SIZE - 1];

byte receivedCommands[MAX_SENT_BYTES];

 

void setup()

{

     Wire.begin(SLAVE_ADDRESS); 

     Wire.onRequest(requestEvent);

     Wire.onReceive(receiveEvent);

}

 

void loop()

{

}

 

void requestEvent()

{

     Wire.send(registerMap, REG_MAP_SIZE);  //Set the buffer up to send all 14 bytes of data

}

 

void receiveEvent(int bytesReceived)

{

     for (int a = 0; a < bytesReceived; a++)

     {

          if ( a < MAX_SENT_BYTES)

          {

               receivedCommands[a] = Wire.receive();

          }

          else

          {

               Wire.receive();  // if we receive more data then allowed just throw it away

          }

     }

}

 

 

Now we have a basic template setup that we can use for any slave device. 

 

I’m not going to show the code that would be used to collect and parse the GPS data because the point of this guide is to give a general overview on how to make a generic I2C slave device.  Instead I’ll show some simple functions that could be called to collect the data.  We’ll call these functions getLatitude(), getLongitude(), getSpeed() and getSignalStatus().

It’s usually a good idea to keep the bulk of the code in the loop section and not in either of the ISR’s (receive and request) which I’ll show you why later.  So let’s go ahead and put some of the function calls into our main loop.

 

 

void loop()

{

     latitude = getLatitude();

     longitude = getLongitude();

     speed = getSpeed();

     gpsStatus = getSignalStatus();

}

 

 

Now that we have our basic functions in place, the next question we need to address is how do we let the master know when we have new data available?  We can simply put a variable in the main loop, after the function calls, that sets a status flag to let us know there is new data available. We’ll call this variable newDataAvailable and it will simply be set to a logic one when new data is available.  Once we send any of the information in the register map to the master we can reset this variable to zero.  We do this in the requestEvent() ISR.  Let’s take a look at our code and see what we have so far.

 

 

void loop()

{

     latitude = getLatitude();

     longitude = getLongitude();

     speed = getSpeed();

     gpsStatus = getSignalStatus();

     newDataAvailable = 1;

}

 

void requestEvent()

{

     //Set the buffer up to send all 14 bytes of data

     Wire.send(registerMap, REG_MAP_SIZE);

     newDataAvailable = 0;

}

 

 

 

 

 

 

 

 

 

 

 

 

 

The next major obstacle we need to tackle is storing the data in the temporary array.  Why is this considered a major obstacle?  Aren’t we just copying some variables into an array?  Yes but here’s the problem, when we use the Wire library in slave mode we can only issue one Wire.send command inside the ISR.  We’ll use this one Wire.send to send our array of bytes.  The trick is storing our data, which is comprised of different data types, into an array of bytes.  In our example, we have fourteen bytes of data comprised of four different data types…unsigned long, long, unsigned int and bytes.  Since three out of the four of our data types are more than one byte in size we can’t simply copy the data into our array.  This is where the use of pointers comes in handy.  We are going to declare a pointer of type byte and transfer one byte at a time into our array.  Here is the code that we’re going to use:

 

 

void loop()

{

     latitude = getLatitude();                 //returns latitude as an unsigned long

     longitude = getLongitude();          //returns longitude a long

     speedKPH = getSpeed();               //returns speed as an unsigned int

     gpsStatus = getSignalStatus();     //returns status as a byte

     storeData();

     newDataAvailable = 1;

}

 

void storeData()

{

     byte * bytePointer;  //we declare a pointer as type byte

     byte arrayIndex = 1; //we need to keep track of where we are storing data in the array

     registerMapTemp[0] = gpsStatus;  //no need to use a pointer for gpsStatus

     bytePointer = (byte*)&latitude; //latitude is 4 bytes

     for (int i = 3; I > -1; i--)

     {

          registerMapTemp[arrayIndex] = bytePointer[i];  //increment pointer to store each byte

          arrayIndex++;

     }

     bytePointer = (byte*)&longitude; //longitude is 4 bytes

     for (int i = 3; I > -1; i--)

     {

          registerMapTemp[arrayIndex] = bytePointer[i];  //increment pointer to store each byte

          arrayIndex++;

     }

     bytePointer = (byte*)&speedKPH; //speedKPH is 2 bytes

     for (int i = 1; I > -1; i--)

     {

          registerMapTemp[arrayIndex] = bytePointer[i];  //increment pointer to store each byte

          arrayIndex++;

     }

     registerMapTemp[arrayIndex] = modeRegister;

     arrayIndex++;

     registerMapTemp[arrayIndex] = configRegister;

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

The first thing you’ll notice is that, because our status register is only one byte, we were able to copy it directly into our temporary array at index zero.  The same is true with our configuration and mode registers at the end of the function. 

We should take a closer look at the code so you understand how we’re using pointers and loops to store the data.  The first thing we do, after defining our bytePointer, is set the pointer to point to the LSB of our variable latitude( bytePointer = (byte*)&latitude ).  Since latitude is four bytes long we need to realize that the pointer is pointing to the first byte address location of the long which is the LSB (Arduino uses little endian).  This is important because when we store our data into our array we are actually going to reverse the order of the bytes. In other words the MSB will be stored at the lower array index and the LSB will be stored at the higher array index.  Why do we do it this way? I’m sure there’s a very good reason most devices out there do it this way so we’ll just do what everyone else is doing, in this case it’s better to stick with what others are doing.  Since we’re storing the MSB in the lower index of the array we need to set our pointer to the last address and decrement from there, hence the reason for the loop counting down.   We do the same for longitude since it too is a long and four bytes in size.  speedKPH, however, is an unsigned integer so it only occupies two bytes so we need to modify the loop to only copy two bytes.  Now some of you have probably noticed that we didn’t include the identification register in our storeCopy() function, the reason being is we’re going to copy that register into the array in the Setup() function because it will never change so it’s only necessary to copy it once.

OK, so what do we have so far: we have our GPS data, we’re able to store it into a temporary array, we have a way of knowing when the data is “new”, we have some basic code to send the data from the slave to the master and we have some basic code to receive all the bytes from the master.  Next we’re going to revisit our receiveEvent() function because we need to add some more code to handle the incoming data from the master.

In our basic template, we just saved all the bytes transmitted from the master into an array.  What we need to do now is expand our code to cover the three possible scenarios when data is received.  The three scenarios are:

1.       The master is setting the address in preparation to read back data from this address

2.       The master is setting the address to write a single byte of data to this address

3.       The master is setting the address to write more than one byte of data to consecutive addresses

Let’s start with scenario number one because it’s the easiest.  In this situation, if the slave receives only one byte (bytesReceived = 1) then we can simply return from the ISR.  The reason being is that we know the byte stored in receivedCommands[0] is an address starting location for sending data later so we have nothing more to do.  One thing we should probably add to the code is some basic error checking.  For example, what if the master tries to set the address to 0x0F when 0x0D is the last address in the register map?  We don’t want to send back random data so in this case we’ll ignore the command and set the address to some default value, in our case we’ll use 0x00. 

Scenario number two, on the other hand, is where it starts to get a bit more complicated.  We’ll need to start adding some more error checking because we only want to write data to registers that are writable.  We need to make sure that if the Master tries to write to a read only register that we ignore the command.  Since scenario two and scenario three are similar, we’ll combine the two as we’re writing the code.  So here’s what we've come up with:

 

void receiveEvent(int bytesReceived)

{

     for (int a = 0; a < bytesReceived; a++)

     {

          if ( a < MAX_SENT_BYTES)

          {

               receivedCommands[a] = Wire.receive();

          }

          else

          {

               Wire.receive();  // if we receive more data then allowed just throw it away

          }

     }

     if(bytesReceived == 1 && (receivedCommands[0] < REG_MAP_SIZE))

     {

          return; 

     }

     if(bytesReceived == 1 && (receivedCommands[0] >= REG_MAP_SIZE))

     {

          receivedCommands[0] = 0x00;

          return;

     }

    switch(receivedCommands[0]){

          case 0x0B:

               zeroB = 1; //status flag to let us know we have new data in register 0x0B

               zeroBData = receivedCommands[1]; //save the data to a separate variable

               bytesReceived--;

               if(bytesReceived == 1)  //only two bytes total were sent so we’re done

               {

                    return;

               }

               zeroC = 1;

               zeroCData = receivedCommands[2];

               return; //we simply return here because the most bytes we can receive is three anyway

               break;

          case 0x0C:

               zeroC = 1;

               zeroCData = receivedCommands[1];

               return; //we simply return here because 0x0C is the last writable register

               break;

          default:

               return; // ignore the commands and return

     }

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

You’ll notice we now have several new variables in our code to store the received bytes and set some flags to let us know when we have new data in our mode and configuration registers.  We could have just copied the receivedCommands data straight to the mode and config registers but what would happen if they sent invalid data or we wanted to check the data first before making changes?  This way we can evaluate the received data later and make possible changes.  You’ll also notice the naming convention I’m using for the variables and flags, it’s the names of the addresses in the register map.  I do this because if you need to add or subtract registers to your register map later on it’s easier to follow the code.  We’re also using the switch command to evaluate the address where the data will be written to.  If the address is 0x0B then we’ll need to be able to write one or two bytes.  If the address is 0x0C then we know we only have one byte that can be written.  If the address is anything other than 0x0B and 0x0C then we know the master is trying to write data to either a read only register or a non-existent one so we just ignore the commands completely. 

Now that we’ve received some data from the master, to change our mode and/or configuration registers, how do we go about doing it?  Well it’s best that we don’t do it inside the receiveEvent ISR, which is why we set flags for the mode and configuration registers.  This is where you need to step back and look at your application and put some thought into what is best for the user.  There’s basically two ways to implement this.  First you could simply check the flags at the end of your Loop() function and make the necessary changes to the device.  This would work for some devices but not others.  Let’s say your “device” is just a single high speed ADC that has a very short main Loop section, you could probably check the flags at the end of the loop and not have any problems whatsoever in your application.  Unfortunately the device we’re using is a GPS which means it’s extremely slow, so waiting for the main loop function to reach the end could take one second or more.  So in this particular case it’s probably worthwhile to test the flags at multiple locations throughout the main loop section.  So for our example we’ll want to do something like what you see below.  You’ll also notice that before branching off to the function to make the changes to the device, we set the newDataAvailable register back to zero.  Why? What would happen if the master requested new data after making the changes to the config/mode registers and the changes hadn’t taken effect yet?  The resulting data the master received would be linked to the old configuration.  This way the status signal doubles as a busy signal when making configuration changes. 

 

 

void loop()

{

     if(zeroB || zeroC)

     {

          newDataAvailable = 0;  //we don’t have any post correction data available

          changeModeConfig();  //call the function to make your changes

          return; //go back to the beginning and start collecting data from scratch

     }

     latitude = getLatitude();                 //returns latitude as an unsigned long

     if(zeroB || zeroC)

     {

          newDataAvailable = 0;  //we don’t have any post correction data available

          changeModeConfig();  //call the function to make your changes

          return; //go back to the beginning and start collecting data from scratch

     }

 

     longitude = getLongitude();          //returns longitude a long

     if(zeroB || zeroC)

     {

          newDataAvailable = 0; //we don’t have any post correction data available

          changeModeConfig();  //call the function to make your changes

          return; //go back to the beginning and start collecting data from scratch

     }

 

     speedKPH = getSpeed();               //returns speed as an unsigned int

     if(zeroB || zeroC)

     {

          newDataAvailable = 0;  //we don’t have any post correction data available

          changeModeConfig();  //call the function to make your changes

          return; //go back to the beginning and start collecting data from scratch

     }

 

     gpsStatus = getSignalStatus();     //returns status as a byte

     if(zeroB || zeroC)

     {

          newDataAvailable = 0;  //we don’t have any post correction data available

          changeModeConfig();  //call the function to make your changes

          return; //go back to the beginning and start collecting data from scratch

     }

     storeData();

     newDataAvailable = 1;

}

 

void changeModeConfig()

{

     /*Put your code here to evaluate which of the registers need changing

          And how to make the changes to the given device.  For our GPS example

          It could be issuing the commands to change the baud rate, update rate,

          Datum, etc… */

     zeroB = 0;

     zeroC = 0;   //always make sure to reset the flags before returning

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

We’ve covered almost every aspect of making an I2C slave device using our Arduino with only a couple of things remaining, first of which is sending the requested data back to the master.  So now we’ll need to look at the code in the requestEvent() ISR.  When designing our slave device we have to be a little careful with the amount of code we use in the requestEvent() function, because the more code in this function the more we “stretch the clock”.  Which raises the question "what exactly is clock stretching?".  Clock stretching is a way for a slave device to slow the communication process down by “stretching” out the clock signal.  Since the master sets the speed of the bus by originating the clock signal on the SCL line, it expects the slave device to send out the corresponding data on the correct clock edges.  Well sometimes the slave device has “other things going on” and isn’t able to keep up with the master.  In these instances the slave is able to hold the clock line (SCL) low which, in essence, suspends communication between the master and slave.  The Master is able to detect this because once it releases the clock line between pulses it’s supposed to check to make sure the line went back to a high.  If it doesn’t see a high it assumes the slave device is busy and waits. In a nutshell, anytime the Arduino enters the requestEvent() ISR it holds the clock line low until it exits.  This is why it’s good practice to minimize the amount of code in requestEvent().  While clock stretching is perfectly acceptable according to the standard, it’s best to minimize it as much as possible.  For our GPS example, the bulk of the code in the ISR will be copying the temporary array to the register map array as follows.

 

 

void requestEvent()

{

     if(newDataAvailable)

     {

          for (int c = 0; c < (REG_MAP_SIZE – 1); c++)

          {

                registerMap[c] = registerMapTemp[c];

           }

     }

     newDataAvailable = 0;

     //Set the buffer up to send all 14 bytes of data

     Wire.send(registerMap+receivedCommands[0], REG_MAP_SIZE); 

     }

}

 

 

 

 

 

 

 

 

 

 

 

 

 

As you can see, if there is new data available, we copy the data from the temporary array into the register map array. We don’t bother copying the last byte of the register because it’s the identification register which never changes.  At the end of the function we reset the newDataAvailable flag back to zero.  We then use Wire’s send command to send the data stored in the register map array.  Since the byte stored at receivedCommands[0] is the requested starting address, we use that number as an offset to the registerMap array index.  Even though the master may not be requesting all fourteen bytes of data we still send all fourteen bytes.  The reason being is it’s the master’s responsibility to stop the transfer of data upon receipt of the last requested byte of data.  For each byte the slave sends to the master, the master in turn generates an acknowledge (ACK) signal.  When the master receives the last byte that it has requested it, instead, replies with a no-acknowledge (NACK) signal which lets the slave device know it is the final byte.

 

There is one other area that we need to discuss and that is the use of external interrupts.  An external interrupt is a way for an I2C device to notify the master when a certain condition has been met.  In our case a good example of an external interrupt would simply be to have an interrupt generated when new data is available.  When choosing an interrupt you’ll need to decide whether you want to use an active low signal (signal goes low when an interrupt takes place) or an active high signal (signal goes high when an interrupt takes place). I’m sure there are advantages and disadvantages to both types but for the purpose of our guide let's use an active low interrupt and set it to Digital pin 2.  We would declare the pinMode setting in our Setup() function and set the initial state to a high to indicate no new data available.  We’ll also use a separate variable so we can set a flag to decide whether or not we want to use an interrupt at all for our application.  This variable would also be placed in the Setup() function.  It’s a good idea to use a variable instead of defining a constant because it gives you the option of tying the variable to a config or mode register to give the user the option of enabling or disabling the use of the external interrupt.  Another option you’ll need to consider is whether or not you want the interrupt to stay low until the master reads the new data or make the interrupt just a single pulse (i.e. low for 10useconds then goes high again).  For the purpose of our guide let’s keep it simple and just keep the interrupt low until the master reads the data.  As a final note on the interrupt level, we're going to have the interrupt reset after the master reads ANY data from the slave.  In other words if the interrupt goes low signaling new data available and the master addresses the slave device and only reads the identification register and nothing else we are still going to reset the interrupt signal.  We’re doing it this way because, once again, we’re trying to keep things simple. You can add your own code to monitor the array index offset to see if the master is addressing the data register or whatever you want but for our example we’re going to do it the easy way.  As a side note, we are going to toggle the interrupt pin using digitalWrite, while this is the easiest way of doing it it’s definitely not the most efficient way.  A more efficient approach would be to use direct port manipulation but we’re not going to use that for our example.  The reason I recommend direct port manipulation is because it executes faster so you have less time spent in the ISR. 

We’re going to write a separate function called toggleInterrupt() and we’ll place the code there.  So when do we actually call this function?  Well that’s the easy part; we place a call to this function every time we change the newDataAvailable variable.  Just make sure to place the function after the variable change because the interrupt pin's state will be tied to the variable state.  Here is an example of the function.

 

 

void toggleInterrupt()

{

     if(!useInterrupt)

          {return;} //first let’s make sure we’re using interrupts, if not just return from the function

     if(newDataAvailable)  // if new data is available set the interrupt low

     {

          digitalWrite(INTERRUPT_PIN,LOW);  //set pin low and return

          return;

     }

     //no new data available or data was just read so set interrupt pin high

     digitalWrite(INTERRUPT_PIN,HIGH); 

     }

}

  

As you can see we’ve tried to implement most of the common features of I2C devices that you see on the market place into our Arduino slave device.  Again this guide is designed to give you a real world example of using Arduino’s Wire library in slave mode.  Hopefully it will come in handy in someone’s next project or help someone in their design of a new I2C device.  As I said before, I’m sure there are more efficient and possibly easier ways to accomplish what we did here but this guide is geared more towards the beginner that has no practical experience using Arduino’s Wire library in slave mode. 

 

Below you’ll find the full sketch and it’s also available for download here…happy coding!  (a corresponding master sketch can be found here)

 

/****************************************************************************************
 * Slave_Example.pde ----- Sample sketch for making an I2C Slave Device
 *
 * A sample sketch that shows the basic steps necessary to making
 * an I2C slave device using Arduino's Wire library.
 *
 *
 * Copyright (c) 2011, DSS Circuits, Inc.  http://www.dsscircuits.com
 *
 ***************************************************************************************/

 

#include <Wire.h>

 

/* These getX DEFINES are only used so the code will compile.  They

** simply return numbers for the function calls.  Instead you would

** use regular functions and place your GPS parsing code inside them */

#define  getLatitude()           ((unsigned long)16909060)

#define  getLongitude()          ((long)84281096)

#define  getSpeed()              ((unsigned int)2314)

#define  getSignalStatus()       ((byte)0x00)

/********************************************************************/

 

#define  SLAVE_ADDRESS           0x29  //slave address,any number from 0x01 to 0x7F

#define  REG_MAP_SIZE            14

#define  MAX_SENT_BYTES          3

#define  INTERRUPT_PIN           2

#define  IDENTIFICATION           0x0D

 

/********* Global  Variables  ***********/

byte registerMap[REG_MAP_SIZE];

byte registerMapTemp[REG_MAP_SIZE];

byte receivedCommands[MAX_SENT_BYTES];

byte newDataAvailable = 0;

byte useInterrupt = 1;

byte modeRegister = 0;

byte configRegister = 0;

byte zeroB = 0;

byte zeroC = 0;

byte zeroBData = 0;

byte zeroCData = 0;

 

long longitude = 0;

unsigned long latitude = 0;

unsigned int speedKPH = 0;

byte gpsStatus = 0;

 

void setup()

{

  if(useInterrupt)

  {

    pinMode(INTERRUPT_PIN,OUTPUT);

    digitalWrite(INTERRUPT_PIN,HIGH);

  }

  Wire.begin(SLAVE_ADDRESS); 

  Wire.onRequest(requestEvent);

  Wire.onReceive(receiveEvent);

  registerMap[13] = IDENTIFICATION; // ID register

}

 

void loop()

{

  if(zeroB || zeroC)

  {

    newDataAvailable = 0;  //let the master know we don’t have any post correction data yet

    toggleInterrupt();

    changeModeConfig();  //call the function to make your changes

    return; //go back to the beginning and start collecting data from scratch

  }

  latitude = getLatitude();                 //returns latitude as an unsigned long

  if(zeroB || zeroC)

  {

    newDataAvailable = 0;  //let the master know we don’t have any post correction data yet

    toggleInterrupt();

    changeModeConfig();  //call the function to make your changes

    return; //go back to the beginning and start collecting data from scratch

  }

 

  longitude = getLongitude();          //returns longitude a long

  if(zeroB || zeroC)

  {

    newDataAvailable = 0;  //let the master know we don’t have any post correction data yet

    toggleInterrupt();

    changeModeConfig();  //call the function to make your changes

    return; //go back to the beginning and start collecting data from scratch

  }

 

  speedKPH = getSpeed();               //returns speed as an unsigned int

  if(zeroB || zeroC)

  {

    newDataAvailable = 0;  //let the master know we don’t have any post correction data yet

    toggleInterrupt();

    changeModeConfig();  //call the function to make your changes

    return; //go back to the beginning and start collecting data from scratch

  }

 

  gpsStatus = getSignalStatus();     //returns status as a byte

  if(zeroB || zeroC)

  {

    newDataAvailable = 0;  //let the master know we don’t have any post correction data yet

    toggleInterrupt();

    changeModeConfig();  //call the function to make your changes

    return; //go back to the beginning and start collecting data from scratch

  }

  storeData();

  newDataAvailable = 1;

  toggleInterrupt();

}

void storeData()

{

  byte * bytePointer;  //we declare a pointer as type byte

  byte arrayIndex = 1; //we need to keep track of where we are storing data in the array

  registerMapTemp[0] = gpsStatus;  //no need to use a pointer for gpsStatus

  bytePointer = (byte*)&latitude; //latitude is 4 bytes

  for (int i = 3; i > -1; i--)

  {

    registerMapTemp[arrayIndex] = bytePointer[i];  //increment pointer to store each byte

    arrayIndex++;

  }

  bytePointer = (byte*)&longitude; //longitude is 4 bytes

  for (int i = 3; i > -1; i--)

  {

    registerMapTemp[arrayIndex] = bytePointer[i];  //increment pointer to store each byte

    arrayIndex++;

  }

  bytePointer = (byte*)&speedKPH; //speedKPH is 2 bytes

  for (int i = 1; i > -1; i--)

  {

    registerMapTemp[arrayIndex] = bytePointer[i];  //increment pointer to store each byte

    arrayIndex++;

  }

  registerMapTemp[arrayIndex] = modeRegister;

  arrayIndex++;

  registerMapTemp[arrayIndex] = configRegister;

}

 

void changeModeConfig()

{

  /*Put your code here to evaluate which of the registers need changing

   And how to make the changes to the given device.  For our GPS example

   It could be issuing the commands to change the baud rate, update rate,

   Datum, etc…

   We just put some basic code below to copy the data straight to the registers*/

   if(zeroB)
      {
        modeRegister = zeroBData;
        zeroB = 0;  //always make sure to reset the flags before returning from the function
        zeroBData = 0;
      }
      if(zeroC)
      {
        configRegister = zeroCData;
        zeroC = 0;  //always make sure to reset the flags before returning from the function
        zeroCData = 0;
      }

}

 

void toggleInterrupt()

{

  if(!useInterrupt)

  {return;} //first let’s make sure we’re using interrupts, if not just return from the function

  if(newDataAvailable)  // if new data is available set the interrupt low

  {

    digitalWrite(INTERRUPT_PIN,LOW);  //set pin low and return

    return;

  }

  //no new data available or data was just read so set interrupt pin high

  digitalWrite(INTERRUPT_PIN,HIGH); 

}

 

void requestEvent()

{

  if(newDataAvailable)

  {

    for (int c = 0; c < (REG_MAP_SIZE-1); c++)

    {

      registerMap[c] = registerMapTemp[c];

    }

  }

  newDataAvailable = 0;

  toggleInterrupt();

  //Set the buffer to send all 14 bytes

  Wire.send(registerMap + receivedCommands[0], REG_MAP_SIZE); 

}

 

void receiveEvent(int bytesReceived)

{

  for (int a = 0; a < bytesReceived; a++)

  {

    if ( a < MAX_SENT_BYTES)

    {

      receivedCommands[a] = Wire.receive();

    }

    else

    {

      Wire.receive();  // if we receive more data then allowed just throw it away

    }

  }

  if(bytesReceived == 1 && (receivedCommands[0] < REG_MAP_SIZE))

  {

    return; 

  }

  if(bytesReceived == 1 && (receivedCommands[0] >= REG_MAP_SIZE))

  {

    receivedCommands[0] = 0x00;

    return;

  }

  switch(receivedCommands[0]){

  case 0x0B:

    zeroB = 1; //this variable is a status flag to let us know we have new data in register 0x0B

    zeroBData = receivedCommands[1]; //save the data to a separate variable

    bytesReceived--;

    if(bytesReceived == 1)  //only two bytes total were sent so we’re done

    {

      return;

    }

    zeroC = 1;

    zeroCData = receivedCommands[2];

    return; //we can return here because the most bytes we can receive is three anyway

    break;

  case 0x0C:

    zeroC = 1;

    zeroCData = receivedCommands[1];

    return; //we can return here because 0x0C is the last writable register

    break;

  default:

    return; // ignore the commands and return

  }

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Comments   

# Peter Wonder 2012-05-24 07:35
Wayne, thank you for another great tutorial. Given that you use the Wire library here and IIRC this does not work with your MasterI2C library (which works much better than the Wire library) I was wondering if you have any experience/idea s/suggestions on how to connect 2 Arduinos (or more) via I2C using your MasterI2C library and some "slave" code so that the MasterI2C library can receive data from another Arduino as opposed to a I2C sensor. Thanks so much for your kind help! (I'm working on a space related project and I'd be happy to tell you more offline, just send me an email in case you are interested) Cheers, Peter
# Wayne Truchsess 2012-05-24 13:40
Quoting Peter Wonder:
Wayne, thank you for another great tutorial. Given that you use the Wire library here and IIRC this does not work with your MasterI2C library (which works much better than the Wire library) I was wondering if you have any experience/ideas/suggestions on how to connect 2 Arduinos (or more) via I2C using your MasterI2C library and some "slave" code so that the MasterI2C library can receive data from another Arduino as opposed to a I2C sensor. Thanks so much for your kind help! (I'm working on a space related project and I'd be happy to tell you more offline, just send me an email in case you are interested) Cheers, Peter

Hi Peter. Unfortunately the I2C master library does not support slave functions so there really is no way to do it with the current library. You could use Wire on the slave device and I2C on the master but you would need to use the separate write and read functions because Wire does not support repeated starts.
# tomega3 2012-08-04 18:31
Hi, terrific article on how to set up an i2c slave using the wire library.
I am learning about i2c and the wire library and have some questions about which arduino uno resources the i2c interface uses/requires that they be available.
Do you you if the WIRE library uses any of the following: timer0, timer1, timer2, int0, an or int1 ?
My sketch current uses timer0 (aka millis() function, timer1 as an timer interrupt, int0 hardware interrupt, int1 hardware interrupt, and the uart. so only timer2 is available.

Do the Wire.onRequest( requestEvent) and or the Wire.onReceive( receiveEvent) functions use any of the arduinos timer or hardware interrupts?

Do the Wire.receive and or Wire.send functions use any of the arduinos timer or hardware interrupts?

What is the purpose, in your example, is toggleinterrupt doing. What code acts upon setting it high or low? I thoiught pin 2 aka int0 was an input and was set up with the attachinterrupt function with a callback function name.

Thanks
# tomega3 2012-08-04 18:32
THis is a repeat of my last post because I forgot to click the Notify me of follow up comments check box. Sorry.

Hi, terrific article on how to set up an i2c slave using the wire library.
I am learning about i2c and the wire library and have some questions about which arduino uno resources the i2c interface uses/requires that they be available.
Do you you if the WIRE library uses any of the following: timer0, timer1, timer2, int0, an or int1 ?
My sketch current uses timer0 (aka millis() function, timer1 as an timer interrupt, int0 hardware interrupt, int1 hardware interrupt, and the uart. so only timer2 is available.

Do the Wire.onRequest( reque stEvent) and or the Wire.onReceive( recei veEvent) functions use any of the arduinos timer or hardware interrupts?

Do the Wire.receive and or Wire.send functions use any of the arduinos timer or hardware interrupts?

What is the purpose, in your example, is toggleinterrupt doing. What code acts upon setting it high or low? I thoiught pin 2 aka int0 was an input and was set up with the attachinterrupt function with a callback function name.

Thanks
# Wayne Truchsess 2012-08-04 19:48
Quoting tomega3:
THis is a repeat of my last post because I forgot to click the Notify me of follow up comments check box. Sorry.

Hi, terrific article on how to set up an i2c slave using the wire library.
I am learning about i2c and the wire library and have some questions about which arduino uno resources the i2c interface uses/requires that they be available.
Do you you if the WIRE library uses any of the following: timer0, timer1, timer2, int0, an or int1 ?
My sketch current uses timer0 (aka millis() function, timer1 as an timer interrupt, int0 hardware interrupt, int1 hardware interrupt, and the uart. so only timer2 is available.

Do the Wire.onRequest(reque stEvent) and or the Wire.onReceive(recei veEvent) functions use any of the arduinos timer or hardware interrupts?

Do the Wire.receive and or Wire.send functions use any of the arduinos timer or hardware interrupts?

What is the purpose, in your example, is toggleinterrupt doing. What code acts upon setting it high or low? I thoiught pin 2 aka int0 was an input and was set up with the attachinterrupt function with a callback function name.

Thanks

You should be fine. The only interrupt that Wire uses (to the best of my knowledge) is the TWI interrupt which is vector 25. If you look at the ATMega328p datasheet on pages 58-59 it shows you all the interrupts available and you'll see the TWI is separate from the others.

As for the toggleinterrupt () function, that was just used as optional item. You'll see several I2C sensors on the market that will generate an external signal (that you would tie to your Interrupt pin) when new data is available, so this was just an example to show you how could generate that same type of signal to mimic some of the typical I2C sensors on the market.

Hope that helps!
# tomega3 2012-08-04 21:13
Thanks for your reply,
Doesn't I2C need a timer to bit bang the pins?
I should probably use your I2C library instead of wire. Does yours use a timer?
Thanks again.
-1 # Wayne Truchsess 2012-08-04 23:18
Quoting tomega3:
Thanks for your reply,
Doesn't I2C need a timer to bit bang the pins?
I should probably use your I2C library instead of wire. Does yours use a timer?
Thanks again.

You're not at actually bit banging I2C, the ATmega328 IC uses dedicated hardware for I2C communication so there's no need to bit bang through software. My library uses millis() only if you enable the timeout feature of the library.
# tomega3 2012-08-05 00:07
Hi Wayne, your the first person to explain arduino ic2 to me. I now understand that it uses hardware resource on the chip (twi interrupt and some other timer object to drive the ic2 pins) that are not related to the timers and interrupt pins that are available to the user. I just ordered two ic2 devices and will use your library instead of the wire lib.
Is rev 5 still that latest version of your ic2 lib?
Thanks for all your help.
# Lucas Pires Camargo 2013-01-23 00:24
This is a wonderful tutorial, and helped me a lot. Thanks! If you don't mind the registers being little-endian, I suggest a minor improvement: combining the temporary register area with the other variables using a union with a struct, and relying on the AVR's 1-byte memory alignment. This way we can get rid of the storeData() function altogether and have a tighter main loop. I can send code if you want to.
# Jelbert 2013-02-03 17:56
Hi Lucas,

Can you tell me how to do this improvement you sugest?
Thanks,

Jelbert
# Lucas Pires Camargo 2013-02-04 19:20
Sure. Look at this code, straight from my implementation:
Quoting C++:

struct WorkspaceVariablesStruct {
octet status;

unsigned int voltageVccSupply;
unsigned int voltageVppSupply;
unsigned int voltagePinA0;
octet fill[9];

// up to this point we need 16 bytes
octet mode;
octet config;
};

union WorkspaceUnion {
struct WorkspaceVariablesStruct variables;
octet registerMapTemp[REG_MAP_SIZE_NO T_CONSTANT];
};

union WorkspaceUnion workspace;


"octet" is just a typedef of unsigned char. Now, accessing workspace.varia bles.something means altering the corresponding memory area in workspace.regis terMapTemp, with no data duplication. So no data copying is required.
REG_MAP_SIZE_NO T_CONSTANT is just REG_MAP_SIZE - 1. "fill[9]" is used to reserve space for more registers and make the struct's memory layout match the registers' addresses.
No byte-swapping will occur, and I find that very useful, since most architectures used nowadays, including ARM and x86, are little-endian anyway.
# Jelbert 2013-02-10 18:20
Thanks,
This Union command is new to me. It works nice.
# Lucas Pires Camargo 2013-02-10 18:25
An important thing i forgot to mention: you have to make sure to not access the device while its reading sensors or otherwise updating the data workspace, or you could be reading incomplete or corrupt data. If reading intervals are not predictable I suggest that you stick to the original implementation.
# Jelbert 2013-02-03 18:04
Thanks Wayne for this great tutorial. In the project I'm doing I have two SHT15 senors that are slow to read and an internet connection that needs to be serviced. Now I moved the sensors and the variable resistors (SPI) to a seperate arduino. And just figured out that it would become difficult to service everything in time with the serial connection.
So now I can use your example to make my sensor controller.
# scott216 2013-04-07 00:29
This is a great article, I was looking for something just like this.

I assume with Arduino IDE 1.x you woould use Wire.write() instead of Wire.send()
# Wayne Truchsess 2013-04-07 00:59
Quoting scott216:
This is a great article, I was looking for something just like this.

I assume with Arduino IDE 1.x you woould use Wire.write() instead of Wire.send()

Yes.
# scott216 2013-04-10 12:18
Can you tell me what the sketch for the master would look like if I used your i2c.h library instead of wire.h