When you want to build complex microcontroller projects in which data needs to be exchanged between different devices, you need a fast and reliable way to exchange data. In the last article, we investigated serial UART connection, a direct one-to-one interface. This article continues the series with the I2C protocol, a half-duplex, bidirectional communication system with many-to-many servers and clients. We will see how to wire a Raspberry Pi and an Arduino Uno to form an I2C connection and exchange data between the two systems.
This article originally appeared at my blog admantium.com.
Setup
In the following setup, the Raspberry Pi will be the controller, and the Arduino Uno will be the client.
Wiring
We first wire the two devices as follows:
- Connect Raspberry GPIO2 => Arduino D18 SDA
- Connect Raspberry GPIO3 => Arduino D19 SCL
- Connect Raspberry Ground PIN => Arduino Ground
If you are unsure about the pin numbering and configuration, see the Raspberry Pi Pin Layout and Arduino Pin Layout, or read my earlier articles.
Software Libraries
On the Raspberry Pi, we need to install a I2C Raspian package, and a library for Python. The library of choice is SMBus, an I2C-based protocol. To install all required software, execute the following commands to install the required libraries.
apt-install i2c-tools
sudo pip3 install smbus
For the Arduino, no additional setup is required. The library of choice is Wire.h, and it comes bundled with the Arduino IDE or a third-party IDE like plattform.io.
Arduino: I2C Client Configuration
The Arduino program will import the <Wire.h>
library, a wrapper for basic I2C communications. With the simple call of Wire.begin()
it will start an IC2 client that can react on messages.
The following program implements a basic I2C client:
#include <Arduino.h>
#include <Wire.h>
#define I2C_DEVICE_ADDRESS 0x44
void setup() {
Wire.begin(I2C_DEVICE_ADDRESS);
Wire.onReceive(receiveMsg);
Serial.begin(9600);
Serial.println("Listening for Input");
}
void loop() {
Serial.print(".");
delay(500);
}
The program works as follows:
- Line 2: Import the
<Wire.h>
library - Line 4: Define the I2C client address with which the Arduino can be reach, here its hex
0x44
(decimal 68) - be careful to choose a suitable 7bit address that is not used by any other device on the same bus - Line 7: To create the I2C client, execute
Wire.begin
with the chosen address - Line 8: When the server sends a message to this client, the callback function
receiveMsg
will be executed
The callback function is defined as follows:
void receiveMsg() {
if (Wire.available()) {
char c = Wire.read();
Serial.print(c);
}
}
This function work as explained here:
- Line 2: Check that there are is an active, not consumed message on the I2C bus for this particular client
- Line 3: Read the first byte of the message, and store it as a
char
- Line 4: Print the char
Let’s continue with the Raspberry Pi setup.
Raspberry Pi
The Raspberry Pi will start I2C node in the server role. In this role, it can actively write messages to the bus, and read data from the clients.
The following program will open a small terminal, waiting for user input, and then send this data to the client.
from smbus import SMBus
clientAddr = 0x44
bus = SMBus(1)
def i2cWrite(msg):
for c in msg:
bus.write_byte(clientAddr, ord(c))
return -1
def main():
print("Send msg to Arduino")
while True:
msg = input("$> ")
print("...")
i2cWrite(msg)
if __name__ == "__main__":
main()
Lets explain the details:
- Line 1: Import the SMBus library
- Line 3: Define the address of the I2C client that will receive the messages, we specify hex
0x44
, decimal 144 - Line 4: Create an instance of the SMBus class
- Line 6: The custom method
ic2Write
receives amsg
string, and in Line 8 it will send each character of this string to the client - Line 10: The
main
method will start an infinite loop (Line 12), which opens the prompt$>
to the user and reads the answer (Line 14), and will then callic2Write
with the complete message.
Ok, we are read to go.
Exchange I2C messages
First of all, check the wiring of the two devices. Then, upload the Arduino program via the Arduino IDE or a third-party IDE such as Plattform IO.
On the Raspberry Pi, start the Python program. And on another terminal, check that a new I2C hardware device is registered.
$> ls /dev/*i2c*
/dev/i2c-1
If you do not see a device, then check the program source code. Then, if all is well, Finally, use an I2C helper program to check that the Arduino is properly connected:
$> i2cdetect -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- 44 -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
This command prints a table of all 7Bit - that is max 144 - connected IC2 devices. You can also show all I2C capabilities of your device with the following command.
$> sudo i2cdetect -F 1
Functionalities implemented by /dev/i2c-1:
I2C yes
SMBus Quick Command yes
SMBus Send Byte yes
SMBus Receive Byte yes
SMBus Write Byte yes
SMBus Read Byte yes
SMBus Write Word yes
SMBus Read Word yes
SMBus Process Call yes
SMBus Block Write yes
SMBus Block Read no
SMBus Block Process Call no
SMBus PEC yes
I2C Block Write yes
On the terminal in which you started the Python program, type any input. Then, in the Arduinos serial console, you should see the received messages
Listening for Input
.......................Hello from Raspberry Pi!................
Excellent! We can exchange I2C messages between the Raspberry Pi and Arduino.
But what about sending data from the client to the server? In I2C, the server controls all the communication, it actively requests data from its clients, and only when requested, are the clients answering. It is not possible to actively send data from clients to the server. To quote StackExchange:
All communication is controlled by the server. The clients does nothing, the server doesn't want it to do. The server controls the speed of the clock (clock stretching not withstanding) and how many bytes are read. At no time should the clients try forcing the data line when the server did not tell it to. The data structure should be known beforehand.
Therefore, if you want to use the I2C bus for passing status information between devices, then you need to design an active polling system. First, each client needs to buffer its status messages. Second, the server needs to call the clients periodically, collect the status information, and act on this information.
Conclusion
This article showed the essential steps to establish an I2C connection from a Raspberry Pi, acting as the server, to and Arduino Uno, acting as the client. For the Arduino, we use the built-in library Wire.h, which handles the concrete I2C message details, and exposes methods to start, listen and handle I2C communications from the server. On the Raspberry Pi, we use the Python SMBus library, with which it is easy to start an IC2 server bus and actively send messages to its connected clients.
Top comments (0)