.net, .net core, Raspberry Pi 3

Using .NET Core 2 to read from an I2C device connected to a Raspberry Pi 3 with Ubuntu 16.04

I’ve bought a lot of hardware devices – often I2C devices – to attach to my Raspberry Pi devices over the years – things like thermometers, gyroscopes, light intensity sensors and so on. And usually there’s a library supplied by the manufacturer of a device breakout board which shows me how to use the device in the .NET framework.

But what if there isn’t a library for my device? Or what if the library isn’t in .NET – or if it is in .NET, what if it isn’t compatible with .NET Core 2? I wanted to know if I could find a way to still read from my I2C devices while coding from scratch using .NET Core 2, and not depend on someone else writing the library for me.

PInvoke

I’ve written a couple of posts recently (one for Windows 10 IoT Core and one for Ubuntu 16.04) about how to create simple platform invocation service (also known as PInvoke) applications in .NET Core 2 – these posts describe calling native methods to capitalise some text, and deploy the application to a Raspberry Pi 3.

So since I found that it was so easy to use PInvoke with .NET Core and Ubuntu on the Raspberry Pi, I thought I’d try something more ambitious – accessing hardware device registers over an I2C bus using native libraries.

What is I2C?

I2C is a protocol often used to connect peripheral hardware devices (such as a thermometer) to a processor device such as a Raspberry Pi or an Arduino. Typically I find there are four wires needed to connect the Raspberry Pi to an I2C device – one for power (usually 3.3V or 5V), one for ground, one for a serial data line (sometimes labelled as SDA), and one for a serial clock line (sometimes labelled SCL).

As a software developer, I don’t need to worry too much about these wires – I just need to connect the correct 5V/3.3V and 0V wires, and connect the SDA wire to Pin 3 on my Raspberry Pi, and connect the SCL wire to Pin 5 on my Pi.

How can I set up my Ubuntu Raspberry Pi 3 to use I2C?

My Ubuntu installation on my Raspberry Pi 3 didn’t have I2C enabled out of the box – I needed to make a few simple changes.

  • I opened the file “/etc/modules” as sudo and added a couple of lines to the end:
i2c-dev
i2c-bcm2708
  • I opened the “/boot/config.txt” file as sudo and added a couple of lines to the end:
dtparam=i2c_arm=on
dtparam=i2c1=on
  • I then ran the command below:
sudo apt-get install -y i2c-tools

At this point I was able to run the command below:

i2cdetect -y 1

This command scans the I2C bus for attached devices. The “-y” switch means it doesn’t prompt me to type ‘Yes’ to confirm, and the “1” means I’m scanning the I2C-1 bus.

screenshot.1494270470

This showed me that my I2C bus is configured correctly, and highlighted that an external device is connected to my I2C-1 bus, and is accessable at address 0x48.

The device is actually a TMP102 temperature sensor – which I’ve written about before when I used a UWP application to read from this device.

How can I read from a device connected to my Raspberry Pi 3?

I happen to know for this device that the temperature is written into the first two bytes of the TMP102 device (from the datasheet), so I want my code to read these bytes.

Once I’ve connected my I2C device correctly to my Raspberry Pi 3, there are three steps to the code:

  • Open the I2C bus,
  • Specify the address of the device we want to control and read from, and
  • Read from the device.

Whereas this isn’t possible in standard .NET Core 2, there are three functions available in the GNU C library which will do this for us.

I’ve pasted the invocation signatures below to access these functions.

[DllImport("libc.so.6", EntryPoint = "open")]
public static extern int Open(string fileName, int mode);
 
[DllImport("libc.so.6", EntryPoint = "ioctl", SetLastError = true)]
private extern static int Ioctl(int fd, int request, int data);
 
[DllImport("libc.so.6", EntryPoint = "read", SetLastError = true)]
internal static extern int Read(int handle, byte[] data, int length);

So we can open the I2C-1 bus with the .NET code below:

int OPEN_READ_WRITE = 2; // constant, even for different devices
var i2cBushandle = Open("/dev/i2c-1", OPEN_READ_WRITE);

We can control the I2C slave device with address 0x48 on the I2C-1 device with the .NET code below:

int I2C_SLAVE = 0x0703; // constant, even for different devices
int registerAddress = 0x48; // different address for each I2C device
var deviceReturnCode = Ioctl(i2cBushandle, I2C_SLAVE, registerAddress);

And finally we can read two bytes into a byte array from the device with the code below:

var deviceDataInMemory = new byte[2];
Read(i2cBushandle, deviceDataInMemory, deviceDataInMemory.Length);

Putting it all together

First install .NET Core 2 using the executable from here, and then install the template for .NET Core 2 IOT projects using the command below:

dotnet new -i RaspberryPi.Template::*

Next create a project (for the TMP102 device) using the command

dotnet new coreiot -n Tmp102

Open the project, and replace the code in the Program.cs file with the code below:

using System;
using System.Runtime.InteropServices;
 
namespace RaspberryPiCore
{
    class Program
    {
        private static int OPEN_READ_WRITE = 2;
        private static int I2C_SLAVE = 0x0703;
 
        [DllImport("libc.so.6", EntryPoint = "open")]
        public static extern int Open(string fileName, int mode);
 
        [DllImport("libc.so.6", EntryPoint = "ioctl", SetLastError = true)]
        private extern static int Ioctl(int fd, int request, int data);
 
        [DllImport("libc.so.6", EntryPoint = "read", SetLastError = true)]
        internal static extern int Read(int handle, byte[] data, int length);
		
        static void Main(string[] args)
        {
            // read from I2C device bus 1
	    var i2cBushandle = Open("/dev/i2c-1", OPEN_READ_WRITE);
 
            // open the slave device at address 0x48 for communication
	    int registerAddress = 0x48;
	    var deviceReturnCode = Ioctl(i2cBushandle, I2C_SLAVE, registerAddress);
 
            // read the first two bytes from the device into an array
	    var deviceDataInMemory = new byte[2];
	    Read(i2cBushandle, deviceDataInMemory, deviceDataInMemory.Length);
 
            Console.WriteLine($"Most significant byte = {deviceDataInMemory[0]}");
            Console.WriteLine($"Least significant byte = {deviceDataInMemory[1]}");
        }
    }
}

Now build and publish using the commands below:

dotnet build
dotnet publish -r ubuntu.16.04-arm

And copy the published code (inside the “.\bin\Debug\netcoreapp2.0\ubuntu.16.04-arm\publish\” directory) to your Raspberry Pi 3 running Ubuntu.

Now if you run the Tmp102 executable (you might need to chmod it to have execute privileges), it’ll write the contents of the first two bytes to the console, which proves we’ve successfully connected to the device over the I2C bus and read from it.

screenshot.1494274133

Wrapping up

There’s obviously a lot more to I2C than this post, but it proves that we can use PInvoke and .NET Core 2 to read from devices using the I2C protocol. With this knowledge, I’m not dependent on hardware vendors supplying working .NET code for my I2C devices (although that obviously makes things easier!)