Controlling an Arduino connected to the PINE64 over I2C with C and C#

In a previous post, we connected an Arduino Mega to the PINE64 and wrote a sketch for the Mega for data communication. The data to be sent and received will follow the simple rules which were listed in the post. Next, we’re going to write a shared library with some C code to interface with I2C natively, and then a C# class which will call the shared library and bring everything together. Just a quick recap of the data rules:

  • Maximum length: 16 bytes.
  • First byte: Number of bytes sent/received.
  • Second byte: Control command. CMD_DIGITAL_WRITE (0x01 or 1), CMD_DIGITAL_READ (0x02 or 2) and CMD_ANALOG_WRITE (0x03 or 3).
  • Third byte: Pin number.
  • Fourth byte: Value of either 1 (for high) or 0 (for low) for CMD_DIGITAL_WRITE, or
  • Fourth to seventh bytes: Integer value for CMD_ANALOG_WRITE.

Linux has native I2C support which lets us communicate directly with any device connected to the bus. Since native communication is possible, we create a shared library with methods which can be invoked from our C# code. Since we can open the connection to a device on the I2C bus as a file descriptor, we will be making use of fcntl.h for the descriptor. Let’s take a look at the C library.

The first thing we do is include required headers and define a constant specifying the maximum number of bytes, which is 16. Then we create the nanite_i2c_open function try to open the specified device file (usually /dev/i2c-1). Next we call ioctl so that we will be able to control the device using a reference to the file descriptor, and the specified slave address. The file descriptor is returned for reference in other function calls. If any of the steps in the function fails, -1 is returned to indicate an error condition. Other functions in the library will handle the failure the same way.

The nanite_i2c_close function is fairly simple. It takes the file descriptor as an argument and checks if it is open in order to close it.

Next is the nanite_i2c_send function which takes the file descriptor and the data to be sent as arguments. The bytes are written to the open file descriptor, and the number of bytes written is verified. If it does not match the length defined in the first byte, -1 is returned to indicate an error condition.

The nanite_i2c_read function will be used to read data over I2C. The first byte is retrieved in order to determine how many more bytes to read. Then we validate the number of bytes that were received with the expected length. 0 is returned if the operation was successful.

The final function in the library is nanite_i2c_max_bytes() which returns MAX_BYTES. This gives us a complete library that we can use for I2C data communication. You can create the shared library using gcc -shared -o libnanitei2c.so -fPIC main.c. The full code listing for the library is available at https://gitlab.com/akinwale/nanitei2c/blob/master/main.c.

Using DllImport, we can call functions from the shared library in the C# code which we’re going to look at next. We create our class, define the supported commands as an enumeration and specify the private members. We also define a couple of constructors with the default constructor using the default I2C device file which is /dev/i2c-1 on the PINE.

The DllImport calls are defined within the class and are used to map the functions from the library to functions that we can call in the I2CExtendedIO class. For instance, to call the nanite_i2c_send in the class, we’ll make use of I2CSendBytes.

Here, we’ve defined our class dispose function which closes the file descriptor if it is open. We also have a bunch of simple helper functions which will be called within the class.

The Open function is simple as it delegates to the OpenI2C function with the specified device filename and the slave address, and assigns the returned file descriptor to a private member.

With DigitalWrite, we build the data to be sent based on the specified rules. Then we delegate to I2CSendBytes using the specified arguments, which calls the corresponding library function.

AnalogWrite is similar to DigitalWrite, except that we convert the integer value to 4 bytes instead of the single byte for low (0) or high (1). Valid values are between 0 and 255 inclusive.

To wrap it all up, we have the DigitalRead function which is a little different because we have to send data and then immediately receive a response. We obtain a mutual-exclusion lock so that the send and receive process completes before any subsequent operations are run. Then we validate the received data and return the value for the pin that was read.

You can obtain the full code listing for the C# class at https://gitlab.com/akinwale/NaniteIo/blob/master/Nanite/IO/I2CExtendedIO.cs.

We can put this all together and test with a simple interactive console application.

The console application accepts inputs like 13 on, 18 off or 9 analog 172 which makes it easy to test the Arduino pins. Although this is practically a complete solution for most requirements with respect to controlling an Arduino connected to the PINE (or Pi or any other SBC) over I2C using C#, you could choose to implement an additional command for analogRead. All you would have to do is follow the logic for digitalRead and add the necessary code to the sketch and the C# class.