Table of Contents
Introduction
In the first part of the series on using I2C we discussed what I2C is and how it is implemented. We also hooked up a couple of Arduino’s and sent data between them using I2C and the Arduino Wire Library.
Today we will continue the discussion about I2C.
We will look into the protocol that I2C uses to transfer data and to establish slave addresses. We will also build our own I2C sensor.
Let’s get started!
Inside I2C
As you’ll recall from the last article and video, I2C is a method of exchanging data between integrated circuits, sensors, microcomputers and microcontrollers. It uses two wires for communications and another two for power and ground.
The two communications connections are as follows:
- SDA – This is the serial data line. I2C is a unidirectional system, so data can only travel in one direction at any given moment.
- SCL – The clock signal, which synchronizes the data.
I2C works on the principle of Masters and Slaves, the Master provides the clock signal and orchestrates all of the communications. Only one device can be master at any given moment, there can be several Slaves.
I2C Addressing
Every I2C Slave has an address that is unique on the I2C bus. Masters do not have addresses. The address is used by the Master to communicate with a Slave, one device at a time.
Most I2C devices use a 7-bit addressing scheme, some newer devices use a 10-bit address.
In the more common 7-bit addressing system the lower bit (bit 0) is used to determine if the master wants to send data to the slave or read data from the slave.
- If Bit 0 is HIGH then the master is requesting to read data from the slave.
- If Boit 0 is LOW then the master is going to write data to the slave.
The remaining 7-bits are the I2C address of the slave that the master wants to communicate with.
Reserved Addresses
Using 7-bits for addressing will, in theory, allow a maximum of 128 addresses. Probably more than you’ll need for your project.
However, that isn’t actually true.
There are really “only” 112 addresses available on an I2C bus, as there are several reserved addresses. A couple of these addresses are used by the Master when making a call to all I2C devices, one is used to change into 10-bit addressing mode and several are reserved for future purposes.
The I2C Bus Organization has provided the date for the following table. If you are building an I2C device you should avoid these addresses, as they are reserved:
Address in Binary, MSB on the left | Purpose |
0000000 0 | General Call |
0000000 1 | Start Byte |
0000001 X | CBUS Addresses |
0000010 X | Reserved for Different Bus Formats |
0000011 X | Reserved for future purposes |
00001XX X | High-Speed Master Code |
11110XX X | 10-bit Slave Addressing |
11111XX X | Reserved for future purposes |
Assigned Addresses
In addition to the reserved addresses, you need to avoid using the address of another I2C device that is used on your I2C bus implementation.
Usually, this is just a matter of determining what addresses are in use, what ones you might possibly use in the future and then just picking an address for your home-brew device that doesn’t conflict.
If, however, you are creating an I2C device that will be used by other people then address selection becomes a bit more challenging.
If you are building a commercial product then the I2C Bus Committee at NXP can assign you an address, for a fee.
Otherwise for hobby and non-profit projects you are pretty well on your own.
Oddly enough, there is no “master list” of I2C bus assignments. The closest thing I have been able to find is Adafruit’s I2C Address Compilation.
I2C Protocol
As I’ve already mentioned, the Master device initiates the communications and supplies the clock signal. It is not possible for a Slave device to initiate communications, it needs to wait until it is called by the Master.
The sequence of communications is as follows:
Normal State
When the I2C bus is idle both the SDA (data) and SCL (clock) lines are held HIGH by the Master.
Start Condition
In order the begin exchanging data the Master puts the bus into Start Condition. It signifies this by changing the state of the DSDA line from HIGH to LOW. At this point, the SCL line is still held HIGH.
In the second phase of the Start Condition, the SCL line drops LOW. The SDA line now transmits the first byte of data, which is the beginning of the address.
Address Sent
The Address is sent and is received by all of the attached slaves. It can be one of two types of addresses:
- A General Call (all zeros), which is a broadcast message to all of the slave devices.
- A specific slave address.
The first bit of the address byte is examined to see if the master is about to send data or if it is requesting data. This will determine the source of data for the next several operations.
Sending Data
The data is now transferred on the I2C bus, it’s origin depends upon the previous address byte.
This may be one or many bytes of data sent, these bytes are synchronized by the clock signal provided by the Master on the SCL line.
The last byte of data is no different than the previous bytes. If the slave is sending data them the Master should have already determined how much data it expects, so it knows when it is the last byte. The number of bytes will vary depending upon the slave device.
After the last byte is sent the SDA line will be held LOW.
Stop Condition
The final stage is the Stop Condition. This is signified by the SDA line transitioning from LOW to HIGH, the opposite sequence from the Start Condition. The SCL clock line will go HIGH.
Ultrasonic Sensor Array
Now let’s construct our own I2C sensor device.
We are going to construct an I2C ultrasonic distance sensor. This is not by itself unique, as there are commercial I2C ultrasonic distance sensors available, such as the URM07 from dfRobot.
What is unique about our sensor is that it won’t just consist of one sensor, it will have four of them. You could use this device to measure the distance to the four corners of the room.
Our sensor will be accurate to a distance of 254 cm and will have a 1 cm resolution.
I’m going to build the sensor on a breadboard using an Arduino Uno, however in a “real life” application I would use a smaller Arduino, like the Arduino Pro Mini, or an ATmega328 chip.
HC-SR04 Ultrasonic Sensor
Our sensor will make use of the popular HC-SR04 ultrasonic sensors.
These devices are inexpensive and easily obtained through just about any electronics parts store. They are reasonably accurate and have become a staple in robotics projects.
As with most ultrasonic sensors, the HC-SR04 consists of a transmitter and receiver. The transmitter sends pulses of ultrasonic sound towards the target, and the receiver picks up the reflected signal.
As the pulses travel at the speed of sound it is just a matter of measuring the time delay between sending and receiving the pulse. This can be used to calculate the distance.
If you want to really learn everything about using the HC-SR04 with an Arduino you can check out the article and video I did on this sensor a while back – Using the HC-SR04 Ultrasonic Distance Sensor with Arduino.
Arduino Hookup
The HC-SR04 has four pins. Two are used for power and ground, the other two for the transmit (Trigger) and receive (Echo).
To save on wiring you can run the sensor in “1-pin mode”, tying the Trigger and Echo pins together. Your code just needs to flip between sending and receiving, and you save some wiring.
Otherwise, the wiring is fairly simple. In addition to the sensor wiring, we will use two of the Arduino’s analog pins, A4 and A5, as I2C connections. A4 is the SDA connection while A5 is the SCL.
Ultrasonic Sensor Test Sketch
Before we work on our I2C sensor code I thought it would be a good idea to make sure that our four ultrasonic sensors were working correctly. So I wrote a test sketch to test everything out, without I2C.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
/* Ultrasonic 4 Sensors Test ultrasonic-4sense-test.ino Uses 4 x HC-SR04 Ultrasonic Range Finders Uses NewPing Library DroneBot Workshop 2019 http://dronebotworkshop.com */ // Include NewPing Library for HC-SR04 sensor #include <NewPing.h> // Hook up 4 HC-SR04 sensors in 1-pin mode // Sensor 0 #define TRIGGER_PIN_0 8 #define ECHO_PIN_0 8 // Sensor 1 #define TRIGGER_PIN_1 9 #define ECHO_PIN_1 9 // Sensor 2 #define TRIGGER_PIN_2 10 #define ECHO_PIN_2 10 // Sensor 3 #define TRIGGER_PIN_3 11 #define ECHO_PIN_3 11 // Maximum Distance is 400 cm #define MAX_DISTANCE 400 // Create objects for ultrasonic sensors NewPing sensor0(TRIGGER_PIN_0, ECHO_PIN_0, MAX_DISTANCE); NewPing sensor1(TRIGGER_PIN_1, ECHO_PIN_1, MAX_DISTANCE); NewPing sensor2(TRIGGER_PIN_2, ECHO_PIN_2, MAX_DISTANCE); NewPing sensor3(TRIGGER_PIN_3, ECHO_PIN_3, MAX_DISTANCE); // Variables to represent distances float distance0, distance1, distance2, distance3; void setup() { // Serial monitor for testing Serial.begin (9600); } void loop() { // Read sensors in CM // Sensor 0 distance0 = sensor0.ping_cm(); delay(20); // Sensor 1 distance1 = sensor1.ping_cm(); delay(20); // Sensor 2 distance2 = sensor2.ping_cm(); delay(20); // Sensor 3 distance3 = sensor3.ping_cm(); delay(20); // Send results to Serial Monitor // Sensor 0 Serial.print("Distance 0 = "); if (distance0 >= 400 || distance0 <= 2) { Serial.println("Out of range"); } else { Serial.print(distance0); Serial.println(" cm"); delay(500); } // Sensor 1 Serial.print("Distance 1 = "); if (distance1 >= 400 || distance1 <= 2) { Serial.println("Out of range"); } else { Serial.print(distance1); Serial.println(" cm"); delay(500); } // Sensor 2 Serial.print("Distance 2 = "); if (distance2 >= 400 || distance2 <= 2) { Serial.println("Out of range"); } else { Serial.print(distance2); Serial.println(" cm"); delay(500); } // Sensor 3 Serial.print("Distance 3 = "); if (distance3 >= 400 || distance3 <= 2) { Serial.println("Out of range"); } else { Serial.print(distance3); Serial.println(" cm"); delay(500); } delay(500); } |
The sketch uses the NewPing library to simplify working with the HC-SR04 sensors. You can use the Library Manager in your Arduino IDE to install this library.
After including the library we define constants to represent the pins on the HC-SR04. Since we are using 1-pin mode the same pins are defined for both Trigger and Echo on each of the four sensors.
We also define a maximum distance. In this test, it will be 400 cm, which is near the maximum range of the ultrasonic sensors.
We use the NewPing library to create objects to represent each sensor and also define variables to hold the distance values we receive.
In the Setup, all that we need to do is to set up the serial monitor.
Now to the Loop.
We read each sensor value in centimeters and place it into the sensors distance variable. Then we add a very short time delay, this is done to (hopefully) prevent the sensors from interfering with one another.
We then check to see if the value falls between 2 and 400. If it is out of this range we print an “out of range” message to the monitor. If it is within the range then we print the value.
After a half-second time delay, we repeat the loop.
Load the sketch to your Arduino and observe it in action. Try and orient your sensors so they are pointing in different directions if possible. I found the easiest way to test them was ti place an object very close to them, which will cause an “out of range” message if the object is less than 2 cm away.
Once you have determined that your sensors are working leave the breadboard wired up the way it is and proceed to the next sketches.
Remote I2C Sensor Array
Now that we have established that our sensors are working correctly it’s time to convert them into an I2C device.
In order to do this, we will need a second Arduino to serve as the Master. Our existing Arduino is already hooked up correctly to act as the Slave, all that will be required is to change the code so that the results are sent over the I2C bus when a request is received from the Master.
Hooking up the Master
The following diagram only shows the hookup between the Master and Slave Arduino’s. Remember to keep all of the existing wiring to the four HC-SR04 ultrasonic sensors intact!
As you can see the hookup is very simple. I am showing two 10k resistors used as pull-ups for the SDA and SCL lines, but you may not even need to use them.
On both Arduino’s the A4 line is also the SDA line, and A5 is SCL. Just connect both of those together, you’ll also need to connect the ground from each of the Arduino’s together.
If you do use the two pull-up resistors then use the 5-volt supply from the Master Arduino to act as the reference voltage. Do not tie the 5-volt lines from each Arduino together!
Once you have it all hooked up you’ll need to load two sketches, one on the Slave and one on the Master.
Arduino Sensor (Slave) Sketch
Both the Slave and Master sketches borrow heavily from code written by Andreas Spiess and presented in a video on his excellent YouTube channel. In his video, he used an ATtiny85 and three sensors.
The sketch used on the slave Arduino, the one with the four sensors, is shown here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
/* I2C Ultrasonic 4 Sensors Slave ultrasonic-4sense-i2c-slave.ino Uses 4 x HC-SR04 Ultrasonic Range Finders Uses I2C Interface Uses NewPing Library DroneBot Workshop 2019 http://dronebotworkshop.com */ // Include NewPing Library for HC-SR04 sensor #include <NewPing.h> // Include Arduino Wire library for I2C #include <Wire.h> // Define Slave I2C Address #define SLAVE_ADDR 9 // Hook up 4 HC-SR04 sensors in 1-pin mode // Sensor 0 #define TRIGGER_PIN_0 8 #define ECHO_PIN_0 8 // Sensor 1 #define TRIGGER_PIN_1 9 #define ECHO_PIN_1 9 // Sensor 2 #define TRIGGER_PIN_2 10 #define ECHO_PIN_2 10 // Sensor 3 #define TRIGGER_PIN_3 11 #define ECHO_PIN_3 11 // Maximum Distance is 260 cm #define MAX_DISTANCE 260 // Create objects for ultrasonic sensors NewPing sensor0(TRIGGER_PIN_0, ECHO_PIN_0, MAX_DISTANCE); NewPing sensor1(TRIGGER_PIN_1, ECHO_PIN_1, MAX_DISTANCE); NewPing sensor2(TRIGGER_PIN_2, ECHO_PIN_2, MAX_DISTANCE); NewPing sensor3(TRIGGER_PIN_3, ECHO_PIN_3, MAX_DISTANCE); // Define return data array, one element per sensor int distance[4]; // Define counter to count bytes in response int bcount = 0; void setup() { // Initialize I2C communications as Slave Wire.begin(SLAVE_ADDR); // Function to run when data requested from master Wire.onRequest(requestEvent); } void requestEvent() { // Define a byte to hold data byte bval; // Cycle through data // First response is always 255 to mark beginning switch (bcount) { case 0: bval = 255; break; case 1: bval = distance[0]; break; case 2: bval = distance[1]; break; case 3: bval = distance[2]; break; case 4: bval = distance[3]; break; } // Send response back to Master Wire.write(bval); // Increment byte counter bcount = bcount + 1; if (bcount > 4) bcount = 0; } void readDistance() { distance[0] = sensor0.ping_cm(); if (distance[0] > 254 ) { distance[0] = 254; } delay(20); distance[1] = sensor1.ping_cm(); if (distance[1] > 254 ) { distance[1] = 254; } delay(20); distance[2] = sensor2.ping_cm(); if (distance[2] > 254 ) { distance[2] = 254; } delay(20); distance[3] = sensor3.ping_cm(); if (distance[3] > 254 ) { distance[3] = 254; } delay(20); } void loop() { // Refresh readings every half second readDistance(); delay(500); } |
You‘ll notice many similarities to the previous sketch, it uses the NewPing library and defines the pins and sensor objects in the same way as before. It also uses the Arduino Wire library for I2C communications, this library is part of your Arduino IDE so you won’t need to install it.
We need to pick an address for our device, I chose 9 but you can pick anything really. If you plan to implement a permanent version of this in one of your designs you should choose an address that won’t conflict with any other I2C devices that you may be using.
The maximum distance has been reduced here, this is because we really are not planning to measure anything over 254 cem. This is because we want to fit our data into one byte.
Instead of using four separate variables to hold the four distance measurements we will put them into a 4-element array.
We are going to send the four distance variables one after another, preceded by a “marker byte”, so we also need to define a counter to keep track of where we are in the response.
In the Setup, we set the device up as a Slave by passing the address to the Wire library.
We also need to specify a function that we will call every time we get a request from the Master. We will call the function requestEvent.
Before we examine the requestEvent function it would be a good idea to look at another function, readDistance, as this is used in the requestEvent function.
The readDistance function simply reads all four sensors and assigns their value to the elements in the array. As with our stand-alone test, there is a small delay between readings to prevent interference between sensors.
In the Loop, we execute the readDistance function every half-second. This means that when the Master asks for a value it will already be there waiting for it.
Now back up to the requestEvent function. This is the function that is called when the Master requests data.
Our response will be sent to the Master over the I2C bus as a string of individual bytes. To allow the Master to distinguish which byte belongs to each sensor we will start the sequence with “start byte” which always has a value of 255. This is why we need to limit our distance reading to 254 cm.
We use the counter we established earlier to cycle through the start byte and the four data values. For each of them, we use the Wire.write method to send the data back to the Master over the I2C bus.
Now that you have seen the Slave sketch it’s time to examine the code we will use on the Master.
Arduino Master Sketch
Here is the sketch used on the Master, it’s actually simpler than the one we just looked at:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
/* I2C Ultrasonic 4 Sensors Master ultra-4sense-i2c-master.ino Receives data from 4 remote Ultrasonic Sensors Uses I2C Interface DroneBot Workshop 2019 http://dronebotworkshop.com */ // Include Arduino Wire library for I2C #include <Wire.h> // Define Slave I2C Address #define SLAVE_ADDR 9 // Define counter to count bytes in response int bcount; // Define array for return data byte distance[4]; void setup() { Wire.begin(); Serial.begin(9600); } byte readI2C(int address) { // Define a variable to hold byte of data byte bval ; long entry = millis(); // Read one byte at a time Wire.requestFrom(address, 1); // Wait 100 ms for data to stabilize while (Wire.available() == 0 && (millis() - entry) < 100) Serial.print("Waiting"); // Place data into byte if (millis() - entry < 100) bval = Wire.read(); return bval; } void loop() { while (readI2C(SLAVE_ADDR) < 255) { // Until first byte has been received print a waiting message Serial.print("Waiting"); } for (bcount = 0; bcount < 4; bcount++) { distance[bcount] = readI2C(SLAVE_ADDR); } for (int i = 0; i < 4; i++) { Serial.print(distance[i]); Serial.print("\t"); } Serial.println(); delay(200); } |
Once again we will use the Arduino Wire Library to handle all of the I2C functions. We will define an address for the Slave that we want to communicate with, in this case, I chose 9 to match the Slave sketch.
As with the previous sketch, we will define a counter to count the incoming bytes and an array to hold the four distance values we receive.
In the Setup, we initialize the I2C bus. Note that we do not specify an address this time, this lets the Wire library know that we wish to operate as a Master.
We also set up the serial monitor so we can look at the incoming data.
Next, we create a function called readI2C. It has one parameter, the address of the device we want to read, and it outputs a byte of data.
The function makes use of the Arduino millis function, this is a function that outputs the number of milliseconds that have elapsed since the Arduino was powered up or reset. It will be used as a timer, to allow the data to settle.
A Wire.request is used to request data from the slave. Remember, in this application, this will bring back one byte of data. We return that byte as the function’s output.
In the Loop, we call the readI2C function and read the data. We wait until it equals 255, as this is the start of our data sequence.
The next four reads are going to be the data from the four ultrasonic sensors. We put each value into our array.
Finally, we print the array values to the serial monitor. They are spaced with a tab character toi make it easier to read.
Load the sketch and keep the computer plugged into the Master Arduino so you can observe the readings on the serial monitor.
You can power the Slave Arduino with a power supply or USB adapter.
The test is pretty well the same test you performed for the stand-alone version, the difference is that the data is now being delivered over the I2C bus.
There are a number of ways you can expand upon your new sensor. You could, of course, add more sensors. And you can rewrite the code to send all of the data at once, instead of as individual bytes.
But this should be enough to get you started building your own I2C sensor devices.
Conclusion
As you can see it is not that difficult to build your own I2C sensors, especially now that you understand the protocol used on the I2C bus. You can make them a lot smaller and more efficient by using an Arduino Pro Mini or ATmega328 chip in place of the Arduino Uno. And you can certainly add more functions to them than this simple experiment.
The next time we look at I2C we will interface a Raspberry Pi and an Arduino. This is a bit tricky as the Raspberry Pi uses 3.3-volt logic and the Arduino uses 5-volt logic. But it is a common problem and there is more than one way to solve it.
Until then go out and sense something!
Parts List
Here are some components that you might need to complete the experiments in this article. Please note that some of these links may be affiliate links, and the DroneBot Workshop may receive a commission on your purchases. This does not increase the cost to you and is a method of supporting this ad-free website.
COMING SOON!
Resources
Sketches used in this Article – All of the sketches in a ZIP file, for your convenience.
Ultrasonic Distance Sensors Arduino Tutorial – Excellent video by Andreas Spiess.
Arduino Wire Library – The built-in library for working with I2C.
Bill, To follow up on a response to this video, by Marcus Brooks, . There is a considerable error in the description of the I2C protocol and timing diagrams. I cannot say that the error will lead to any immediate problems with deploying the Real Robot, as the I2C functionality is managed via software libraries. However, should you need to debug a problem at the hardware level, you will find that your timing diagrams do not reflect reality. As I mentioned I don’t believe that this is a serious problem, but I wanted to pass on my concerns. Also… I… Read more »
question about i2c in relation to dht 11/22
does the dht sensor have an i2c address?
awesome info for starters
Cool project.,,
Hi, Thanks for this great article. I hope you can help me understand more. 1) Arduino Sensor (Slave) Sketch In the Line 59, Wire.onRequest(requestEvent);, Does this instruction set the slave into continuous state of polling or listening so that it need not appear in the loop() function? I am not sure if I understand the code correctly. 2) In <Arduino Master Sketch> 2.1) Line 38, if “(millis() – entry < 100) bval = Wire.read();” Should comparing operation be “entry > 100) “? 2.2) In line 45, while (readI2C(SLAVE_ADDR) < 255)”, should comparison be “>255”? Sorry to bother you that the question… Read more »
can anyone tell me how can I find the register address in the arduino uno please help me
What if the sensors that are being used to gather data are themselves I2C devices? I want to create a “sub” circuit that does continuous data reads from the I2C sensors, and responds with the current set of data when requested. So, the Uno in your example would be a slave on one I2C bus and would also manage it’s own I2C bus as a master. Do I need to look at using more complex Wire replacements to do this properly?
Love your channel. Everything is well explained.
Nicely done explanation of I2C. A piece of information that would be handy to get out to folks is that NOT ALL BOARDS PROGRAMMABLE BY ARDUINO IDE are capable of being configured as I2C Slave… Pretty much all example blog posts and tutorial videos avoid this trap by always using boards that do support it — but without mentioning that many popular boards do not. For instance I have found that ESP32, ESP8266, STM32f103c8t6 DO NOT IMPLEMENT I2C SLAVE in their Wire (a.k.a. TwoWire) libraries… You can determine if your desired target board has a chance of working by selecting… Read more »