-
Notifications
You must be signed in to change notification settings - Fork 3
General I2C Tutorial
This tutorial will walk you through the basics of setting up and talking to a device with I2C. The goal is to help unfamiliar users get up to speed with how to use some of the cool features in Scarlet. As such, this tutorial is focused on beginners; if you are familiar with I2C this section probably won't help you much, and we recommend just using the general documentation linked below in bold.
For the I2C general overview/basic information, consult this documentation page.
So you have a device that uses I2C and you want to talk to it using Scarlet. First of all, it's good to know how I2C works. If you are unfamiliar with the platform this could get pretty confusing. Sparkfun has a good written tutorial as well as a video that includes SPI. Please take the time to understand the protocol if you are trying to use it. This Texas Instruments I2C Overview can also be very helpful!
I2C is generally an easy protocol to deal with; this tutorial should do a decent job explaining this to all levels. It goes into a lot to try and help you understand exactly what is happening, but this much information could be overwhelming, so just remember that if you follow the code in this example it will get you started. The more you use I2C, the more familiar you will become with it.
To be clear, this is not the way you should talk to a device with Scarlet. To make a new device, please refer to the documentation here. (TODO: Link this documentation)
In this tutorial, we will be demoing the MPU 6050 device from InvenSense. If you haven't already, please review the MPU 6050 Notes below (here). We go over some critical details including determining the device address and documentation details.
From that documentation, we know the address (0x68), so let's talk to it!
Preparation:
The absolute first thing we need to do is setup the I2C bus on the Raspberry Pi or BeagleBone Black by following one of these sets of instructions:
Now that we have a bus setup, we can write and read from any registers we want! Let's read the x-coordinate acceleration from this MPU.
On page 7 of the register document of the MPU, we see that ACCEL_XOUT_H and ACCEL_XOUT_L are registers 59 and 60 respectively (or 0x3B and 0x3C). The "_H" register stores the "HIGH" bits and the "_L" register defines the "LOW" or least significant bits (because the device gives us a 16bit number and the registers are only 8bits wide, they needed two of them to store this data). Now there are two ways to approach this, we will go through both.
Method 1: Read both registers (beginner)
This is the most straightforward approach, but not the most efficient, i.e. there are more lines of code. There are ways to make this code cleaner for certain, but since this approach is meant for beginners this is supposed to be as simple as possible. If you are confused, don't be discouraged, we chose an example of a device that was fairly complicated to deal with, so that we can have a more in-depth discussion about I2C.
Let's initialize an integer to store our final data
int RawAccelXData = 0;
Our I2C read method takes in a buffer so it can write to it. After we call the Read() method, this array will be filled with the returned data.
byte[] ByteBuffer = new byte[1];
This will read the 0x3B register on our device (0x68) into our ByteBuffer. Fairly straightforward.
ByteBuffer = I2C1.ReadRegister(0x68, 0x3B, 1);
Let's convert our received byte into an int and store what we got into our final data
RawAccelXData = (int)ByteBuffer[0];
Now we do another read, this time on the next register (0x3C)
ByteBuffer = I2C1.ReadRegister(0x68, 0x3C, 1);
Now we'll do this:
RawAccelXData = RawAccelXData << 8;
This is where things may get a little bit weird if you have never worked with bits before. This is a bitwise operator called a left shift, basically it moves all of the binary data into our int over to the left by 8 spots (essentially making it 16 bits, but the Integer type in C# is 32 bits- the rest are just zeros). It's just like in base ten if we multiply a number by 100000000, since it has 8 zeros all the original digits will move to the left 8 spots. But in binary instead of powers of 10, we need to multiply by powers of 2. 100000000 = 108 so it makes sense then that instead of using the left shift operator I could have just multiplied by 256. Try it if you don't believe me!
After that, we will have to do this:
RawAccelXData = RawAccelXData | (int)ByteBuffer[0];
Another weird bitwise operator, this one is almost a logical OR if you haven't noticed. Which is no coincidence since it's a called a bitwise OR. In basic terms what this does is it takes every bit in RawAccelXData and the corresponding bit in ByteBuffer, turns them into booleans, compares them with a logical OR gate, then the corresponding bit in the new number (in this case it's being assigned back to RawAccelXData) is the result of that logical OR (1 if true, 0 if false). This is a trick Engineers use to combine numbers together. Really we cannot do this explanation much justice here, take a look at this for more detailed information. If you still don't understand how this works, get a pen and paper make up two 8-bit binary numbers left shift one of them over by 8 (making it 16 bits long) then OR against the other. You'll see!
But you'll notice that this raw value doesn't mean much, this is due to the configuration of the accelerometer sensitivity. Take a look at page 29 of that Register specification sheet for more information. This number won't do anything until you divide it be some constant, by default 16384. So to get the acceleration, use this:
double ACCEL_Gs = RawAccelXData / (double)16384;
Method 2: Read one register (intermediate)
Instead of reading just one register at a time, we can actually read two, since the I2C device will just continue reading onto the next register (like the registers are all side by side and we just scan them). To do this, all we have to do is supply a byte[] buffer with length two. Theoretically we can go as far as we want with this, we could even scan them all if we have a big enough buffer.
Let's define an integer to store our final data.
int RawAccelXData = 0;
We need to construct a buffer for the data to be read into. Once we call our Read() method, this will be filled with the data.
byte[] ByteBuffer = new byte[2];
So let's read the FIRST register (so it will read into the second) and fill the buffer.
ByteBuffer = I2C1.ReadRegister(0x68, 0x3B, 2);
Now we can take our MSB data, left shift it by 8 and then OR against the LSB data to get our raw output
RawAccelXData = (ByteBuffer[0] << 8) | ByteBuffer[1];
The last paragraph of the Beginner lesson explains this process, please take a look if interested.
double ACCEL_Gs = RawAccelXData / (double)16384;
A device without registers is relatively uncommon, so be sure that there aren't any. If you are sure, then devices without registers have their own way of communicating data and you must get familiar with it. Our VEML6070 UV sensor does not use registers, it merely lets you write data (and when you do it's automatically setup to an internal register) and read data (the raw sensor data). Essentially you select which register to are accessing by whether or not you are reading or writing to the device, so take a look at the device you are using. To write to the device and not to a register, please refer to the II2CBus methods: Write() and Read()
If you have taken the time to understand I2C, you know that to use it we need to know the device address in order to communicate. Sometimes that information is hard or tricky to find on a device sometimes it's easier. The MPU-6050 is an I2C or SPI sensor where that information is relatively easy to find. Best way to find it is to look at the documentation, CTRL/CMD+F "address" and click through until you find detailed information on the I2C address because sometimes there is a nuance. In this example, the I2C address information is detailed on page 33. The address is typically b1101000 which is 0x68 in hex (104 decimal) but can be 0x69 (105) depending on if one of the pins on the chip (A0 in this case) is pulled high. This is to allow for two of the same devices on the same I2C bus. If one's A0 pin is pulled high and the other is pulled low, there will be no device collision on the bus and everyone is happy! (Notice that A0 means address bit 0) We will assume for this example that A0 is low and the address is 0x68.
In some devices, you can only talk to the device itself (i.e. you can just write or read to the device address), but on most you can choose from multiple onboard devices with their own address to talk to, these are called registers and we need to know a lot about the registers specific to the device if we plan to use it. What's nice about the MPU 6050 is that it has a whole document dedicated to its registers (this is atypical, but they do it because this device has a LOT of registers, usually devices will have their registers detailed in their main documentation).
How is it possible that a device has a bunch of onboard devices on the same I2C bus? How does that work?
Good question. It is all handled in the back end of Scarlet and by most I2C libraries, so if you are not worried about understanding this thoroughly you can skip through the rest.
Those "onboard devices" are not visible to the main I2C bus, they are only onboard the device, so when we talk to the device we need to tell it what register we are trying to deal with. To handle this, the standard is to begin an I2C communication with a register. We must write, to the device, one byte that tells the system which register to access. Then either write the data right after that transmission, or start a read command with the device. Here is a link, I recommend taking a look at page 7 if you are still confused (as you likely are if this is your first experience with I2C).
Quick Links:
NuGet
Pin Diagrams: RPi | BBB
Developers: CaiB, Baldstrom
General Info:
Home
Common Issues
Getting Started
Supported Devices
Sections:
Logging
DataLog
Filters
Hardware I/O:
- BeagleBone Black
- Raspberry Pi
- Pin Diagrams: RPi | BBB
- GPIO: Using | For Beginners
- PWM: Using | For Beginners
- ADC: Using | For Beginners
- I2C: Using | For Beginners
- SPI: Using | For Beginners
- UART: Using | For Beginners
- CAN: Using | For Beginners
Networking
Sensors
StateStore
Other: Interesting Case Studies