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:
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:
|0x01||Latitude - MSB|
|0x04||Latitude - LSB|
|0x05||Longitude - MSB|
|0x08||Longitude - LSB|
|0x09||Speed - MSB|
|0x0A||Speed - LSB|
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:
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) . The next variable we need is also going to be an array to temporarily hold the same register data (byte registerMapTemp). 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 ). 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.
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.
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.
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:
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 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:
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.
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.
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 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.
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.