Wednesday, June 20, 2012

Using I²C on the Raspberry Pi

Last night I succeeded in getting my Raspberry Pi to do some basic I²C (also known as I2C, TWI, or SMBus) communication. I had to look around in a couple different places on the web to figure out how to do it, so I thought I would make this blog post to consolidate the knowledge.

The first step, of course, is to set up an SD card suitable for the Raspberry Pi. I used the official Debian image from the Raspberry Pi Foundation.

Unfortunately, this official image has no support for I²C! You need to enter Chris's Digital Realm for that. Follow the instructions on Chris Boot's site to upgrade your Raspberry Pi firmware and install his Raspberry Pi kernel, which has drivers for I²C. Now if you run lsmod you should see a lot of good stuff, such as the i2c_bcm2708 module.

Once you are running the new kernel, you will need to run sudo modprobe i2c_dev. You should also add i2c_dev to the /etc/modules.conf file so that it loads automatically the next time you boot up the Raspberry Pi. Now you should see two I2C devices:
pi@raspberrypi:~/$ ls -l /dev/i2c*
crw------- 1 root root 89, 0 Dec 31  1969 /dev/i2c-0
crw------- 1 root root 89, 1 Dec 31  1969 /dev/i2c-1

You will need to connect your device to the Raspberry Pi. I used Pololu Female-Female Premium Jumper Wires to make the following connections between the Raspberry Pi's GPIO header and my device:
  • Raspberry Pi GND to device GND
  • Raspberry Pi SCL to device SCL
  • Raspberry Pi SDA to device SDA
  • Raspberry Pi 3V3 Power to device's power input. This line can only provide a few tens of milliamps so you should look up how much current your device draws and make sure you can power it at 3.3 V. The Raspberry Pi's 5V power line is also available.
At this point, you can use simple C system calls to communicate with I2C devices. Here is some code I wrote to read the "Who Am I" register from the LSM303DLM on a Pololu MinIMU-9 Gyro, Accelerometer, and Compass (L3G4200D and LSM303DLM Carrier):
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
#include <fcntl.h>

#define MAG_ADDRESS        (0x3C >> 1)
#define LSM303_WHO_AM_I_M  (0x0F)

int main()
{
    char buf[10];
    const char * devName = "/dev/i2c-0";

    // Open up the I2C bus
    int file = open(devName, O_RDWR);
    if (file == -1)
    {
        perror(devName);
        exit(1);
    }

    // Specify the address of the slave device.
    if (ioctl(file, I2C_SLAVE, MAG_ADDRESS) < 0)
    {
        perror("Failed to acquire bus access and/or talk to slave");
        exit(1);
    }

    // Write a byte to the slave.
    buf[0] = LSM303_WHO_AM_I_M;
    if (write(file, buf, 1) != 1)
    {
        perror("Failed to write to the i2c bus");
        exit(1);
    }

    // Read a byte from the slave.
    if (read(file,buf,1) != 1)
    {
        perror("Failed to read from the i2c bus");
        exit(1);
    }

    printf("result: 0x%02X\n", buf[0]);

    return 0;
}
When I ran this code, it printed 0x3C which means that it successfully talked to the device! There might be a simpler way to read the register; see the Linux Kernel I2C Documentation.

In conclusion, it was not too hard to get I²C working on the Raspberry Pi. The biggest hurdle is that the official Debian image/kernel has no support for it. This is kind of surprising because the Raspberry Pi team purposefully brought the I²C lines out to their GPIO header and the product has been selling for several months now.

21 comments:

  1. Thank you for posting this! Did you have any luck reading data from the two sensors? I was able to read WHO_AM_I on both devices, but i can't read the sensor data -- all i'm getting back is gibberish. I was trying to modify the arduino code written for the chip.

    ReplyDelete
  2. I had more luck this evening -- i used i2c_smbus_write_byte_data to set registers and i2c_smbus_read_i2c_block_data to read all 6 bytes of acc/mag/gyro data. also, you have to watch out for the two's complement output, like so (this is rpi equivalent of the LSM303 code for arduino, inside of read())

    __s32 res;
    __u8 reg;
    reg = LSM303_OUT_X_L_A | (1 << 7) ;
    __u8 buf[6];
    res = i2c_smbus_read_i2c_block_data(file, reg, 6, (__u8 *)buf);
    if (res != 6) {
    printf("Failed to read acc data in read()\n");
    exit(1);
    }
    for (int i = 0; i < 6; i++) {
    buf[i] = ~buf[i];
    buf[i] += 0x01;
    }
    __u8 xla = buf[0];
    __u8 xha = buf[1];
    __u8 yla = buf[2];
    __u8 yha = buf[3];
    __u8 zla = buf[4];
    __u8 zha = buf[5];

    __s16 x = xha << 8 | xla;
    __s16 y = yha << 8 | yla;
    __s16 z = zha << 8 | zla;

    remember you need libi2c-dev installed to use i2c_smbus_xxx functions.

    Hope this helps somebody!!!

    ReplyDelete
  3. It helped me! Thanks a lot, Wojciech. I didn't know about libi2c-dev and i2c_smbus_read_i2c_block_data! I was trying to do something worse that made the kernel spit out scary error messages.

    My current sticking point is that the values I am reading from the magnetometer and accelerometer are all constant. I probably need to enable them somehow; I will look into it tomorrow by checking out the example Arduino code. In the mean time, you can see my code here:
    https://github.com/DavidEGrayson/minimu9-rpi-ahrs

    ReplyDelete
  4. Thanks for this. I just wanted to mention that the official Raspbian and Debian Wheezy image for RPi now support both I2C and SPI.

    ReplyDelete
  5. Hello. It's four years later and I have learned some things. I think I prefer to use the general-purpose I2C_RDWR ioctl instead of using these inline SMBus functions. See my gist here:

    https://gist.github.com/DavidEGrayson/8e84e69a24b58f65b6771593ad88a10c

    ReplyDelete
  6. Thanks for posting this information. Just wanted to let you know that you gist doesn't compile due to the #include "exceptions.h" not being available. Would you be able to add that file too?

    ReplyDelete
    Replies
    1. I'm actually pretty surprised that you found it so quickly and tried to use it! There has been no activity on this post for four years. Anyway, I fixed the Gist, so please try again and let me know if it works now.

      Delete
  7. I was trying to do i2c transaction with my eeprom at address 0x51.
    I did the initial setup by setting slave address with I2C_SLAVE ioctl.
    I am trying to write 64 bytes data (page size) into eeprom using the
    write(fd, buf, 64);
    Now i want to reset the address to 0th page where i wrote the 1st page of 64 bytes and start reading from there. I am doing this by the following:-
    ----------------------------------------------
    buffer[0] = address[0];// high byte of address to access
    buffer[1] = address[1];//low byte
    //setting the address location to read for each page of 64 bytes
    if(write(fd, buffer, 2) != 2)
    {
    printf("Error in setting new page pointer\n");
    return -1;
    }
    #endif
    printf("\nReading at address 0x %x %x \n",address[0],address[1]);

    ret = read(fd, rcvbuf, 64);
    ------------------------------------------------------------

    ReplyDelete
    Replies
    1. Is there something wrong that I am doing, my read just is not working correctly.

      Delete
    2. Nothing that you have shown here looks wrong. I would suggest that you post your full code, schematics, and your wiring diagram as a question on StackOverflow.com to get help. Be sure to decsribe exactly how you compiled and ran the program, and what expected output is and what the actual output is. If you send me a link to your question I can take a look.

      I would also suggest that you use the general-purpose I2C_RDWR ioctl instead of doing those other things you were doing to separately set the address and read and write.

      Delete
  8. Sort of a dumb question but how does one interact with multiple i2c slave devices? Do I use the same file descriptor from open and just call ioctl with a different slave address when I want to talk to a different slave device?

    I know I2C really well but how to do this in Linux is not exactly easy to find.

    ReplyDelete
    Replies
    1. First of all, make sure you see the new code I posted:

      https://gist.github.com/DavidEGrayson/8e84e69a24b58f65b6771593ad88a10c

      Now that we know how to use the I2C_RDWR ioctl, there is no need to use a separate ioctl just to set the slave address. You can have one file descriptor or multiple ones, whichever is most convenient for you.

      Delete
  9. Hi David,
    After doing an

    ioctl(file, I2C_SLAVE, MAG_ADDRESS)

    i am writing to device now, using

    write(file, buf, 1)

    but i'm getting kernel messages like these
    "i2c f0018000.i2c: controller timed out"

    ioctl was successfull. but write api failed with error no 110.
    Should there be a delay in between ioctl and read/write.

    ReplyDelete
    Replies
    1. No, I don't think you should need a delay. You might consider trying the I2C_RDWR ioctl that I mentioned above. I have a nice class for it here: i2c_bus.h i2c_bus.cpp. If that doesn't help you probably just need to get out the oscilloscope and see what's going wrong.

      Delete
  10. Hi David,
    I am not able to write the data into eeprom. upon execution of code I am getting "Failed to write to the i2c bus: Remote I/O error". My code is given below.
    #include
    #include
    #include
    #include
    #include
    #include
    #include

    #define MAG_ADDRESS (0b01010000 >> 1)
    #define LSM303_WHO_AM_I_M 0x0F

    int main()
    {
    char buf[10];
    const char * devName = "/dev/i2c-0";

    // Open up the I2C bus
    int file = open(devName, O_RDWR);
    if (file == -1)
    {
    perror(devName);
    exit(1);
    }

    // Specify the address of the slave device.
    if (ioctl(file, I2C_SLAVE, MAG_ADDRESS) < 0)
    {
    perror("Failed to acquire bus access and/or talk to slave");
    exit(1);
    }

    // Write a byte to the slave.
    buf[0] = LSM303_WHO_AM_I_M;
    if (write(file, buf, 1) != 1)
    {
    perror("Failed to write to the i2c bus");
    exit(1);
    }

    // Read a byte from the slave.
    if (read(file,buf,1) != 1)
    {
    perror("Failed to read from the i2c bus");
    exit(1);
    }

    printf("result: 0x%02X\n", buf[0]);

    return 0;
    }

    ReplyDelete
    Replies
    1. What EEPROM are you talking about? Also, please use a proper code hosting service like gist, or at least post your code in some reasonable way that preserves the indentation and include statements.

      Delete
    2. This comment has been removed by the author.

      Delete
    3. I'm going to delete any further comments with poorly-formatted code. You can use gist.github.com to post code properly, or just host it on your own website as a text file. I don't know how to access a M24C32 EEPROM on a phycore-AM57x, but you should try running "i2cdetect -y 0" and "i2cdetect -y 1" to make sure you got the address of the device right and that your board can detect the device.

      Delete
  11. This comment has been removed by the author.

    ReplyDelete
  12. This comment has been removed by the author.

    ReplyDelete
  13. This comment has been removed by a blog administrator.

    ReplyDelete

Note: Only a member of this blog may post a comment.