In my last blog post I showed how to transfer IoT data over the network using the M2MQTT library. But how, you may ask, do you acquire the data in the first place? Well, chances are you have some kind of sensor, be it temperature, pressure, accelerometer or what have you. These sensors, more than likely, interface to the IoT node processor via some kind of bus. It could be I2C, SPI, CAN, LIN or any number of others in the alphabet soup. By far the two most popular low level sensor buses are I2C and SPI. I2C stands for Inter-Integrated Circuit and was invented by Philips Semiconductor (now NXP Semiconductor), SPI stand for Serial Peripheral Interface invented by Motorola.

In this example I will show how we can interface to these buses using managed code (C#) in Visual Studio 2013. I will use the BeagleBone Black as the platform running Windows Embedded Compact 2013. A prebuilt demo image is available at my Codeplex site along with all the source code for the example.

To start I wired a prototype A/D board I acquired from Adafruit Industries (by the way a great place for the DIY electronics enthusiast) to a Beaglebone prototyping board. This module uses a Texas Instruments ADS1015 12bit A/D converter and uses the I2C bus as its interface. The BeagleBone connectors breakout all the needed interface signals and supply voltages needed to connect up the sensor. I am using a pair of pressure transducers connected to two channels on the A/D converter module. I won’t go into the full hardware details as this posting is focusing on the software interface. Here is a picture of the setup:

20140801_150615

The BSP for the BeagleBone comes with low level drivers for both the I2C and SPI buses available on the AM335X processor, which the Beaglebone incorporates. These drivers are written in native “C” code and implement a “stream driver” as discussed quite a bit on this site. The driver does the low level work talking to the I2C controller on the AM335X part. Because this driver is implemented as a stream driver it has a well defined interface set much like our standard file I/O calls. Things like XXX_Init, XXX_Read, XXX_Write and XXX_IOCTRL are all very familiar calls we see again and again. As you might have assumed the XXX_Read and XXX_Write calls will be used to actually read and write data but how do you do things that are I2C bus specific, for example setting the slave device’s bus address? Well this is where the XXX_IOCTRL function comes into play.

So how do we get at these interfaces in managed code? Well the traditional way is to P/Invoke the WIN32 API file I/O calls. We need to do this because the managed interfaces supplied in the Compact Framework are just not rich enough (for example no XXX_IOCTRL). Now comes the tricky part, I would normally just include a third party managed assembly, like the OpenNETCF library, which has great utility and already implements a stream driver wrapper. Unfortunately, as you may have heard, we loose binary compatibly with Windows Embedded Compact 2013 and we can not use older managed assemblies for the most part. This is because the managed to native low level calls are marshaled differently in WEC2013 and this stings us when using P/Invoke type calls.

Not to worry though, we can recompile the source against the new Compact Framework 3.9 and for the most part everything should work as it did under CF 3.5 and before as the basic WIN32 API have not really changed. This is what I had to do as the first exercise. Now, equipped with a fine managed stream driver interface, we can move on to the task at hand, interfacing and talking on the I2C bus.

I created an I2CSensorApp in Visual Studio 2013, added the newly recompiled OpenNETCF assembly reference and proceeded to add the I2C class:

using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using OpenNETCF.IO;
 
namespace Embedded101.I2C
{
    public enum I2CPort : uint
    {
        I2C0 = 1,
        I2C1,
        I2C2
    }
 
     
    public class I2C : StreamInterfaceDriver
    {
 
        public enum SubAddressMode
        {
            MODE_0,
            MODE_8,
            MODE_16,
            MODE_24,
            MODE_32
        }
 
        public enum Speed
        {
            SPEED100KHZ,
            SPEED400KHZ,
            SPEED1P6MHZ,
            SPEED2P4MHZ,
            SPEED3P2MHZ
        }
 
  
        #region I2C device IOCTL codes
 
        private const Int32 CODE_IOCTL_I2C_SET_SLAVE_ADDRESS =      0x0200;
        private const Int32 CODE_IOCTL_I2C_SET_SUBADDRESS_MODE =    0x0201;
        private const Int32 CODE_IOCTL_I2C_SET_BAUD_INDEX =         0x0202;
 
 
        private const Int32 FILE_DEVICE_UNKNOWN = 0x00000022;
        private const Int32 FILE_ANY_ACCESS = 0x0;
        private const Int32 METHOD_BUFFERED = 0x0;
 
 
        private const Int32 IOCTL_I2C_SET_SLAVE_ADDRESS =
            ((FILE_DEVICE_UNKNOWN) << 16) | ((FILE_ANY_ACCESS) << 14)
            | ((CODE_IOCTL_I2C_SET_SLAVE_ADDRESS) << 2) | (METHOD_BUFFERED);
 
        private const Int32 IOCTL_I2C_SET_SUBADDRESS_MODE =
            ((FILE_DEVICE_UNKNOWN) << 16) | ((FILE_ANY_ACCESS) << 14)
            | ((CODE_IOCTL_I2C_SET_SUBADDRESS_MODE) << 2) | (METHOD_BUFFERED);
 
        private const Int32 IOCTL_I2C_SET_BAUD_INDEX =
            ((FILE_DEVICE_UNKNOWN) << 16) | ((FILE_ANY_ACCESS) << 14)
            | ((CODE_IOCTL_I2C_SET_BAUD_INDEX) << 2) | (METHOD_BUFFERED);
 
 
        #endregion
         
        #region ctor / dtor
        /// <summary>
        /// Provides access to the I2C bus on the OMAP.
        /// </summary>
        public I2C(I2CPort port) : base("I2C" + Convert.ToString((uint)port) + ":")
        {
            // open the driver
            Open(FileAccess.ReadWrite, FileShare.ReadWrite);
        }
 
        ~I2C()
        {
            // close the driver
            Close();
        }
        #endregion
 
        public void SetSlaveAddress(UInt16 slaveAddress)
        {
            try
            {
                UInt32 SA = (UInt32)slaveAddress;
                this.DeviceIoControl(IOCTL_I2C_SET_SLAVE_ADDRESS, SerializeToByteArray(SA));
            }
            catch (Exception)
            {
                throw new Exception("Unable to complete SetSlaveAddress DeviceIoControl:" + Marshal.GetLastWin32Error());
            }
        }
 
        public void SetSubAddressMode(SubAddressMode subAddressMode)
        {
            try
            {
                UInt32 SAM = (UInt32)subAddressMode;
                this.DeviceIoControl(IOCTL_I2C_SET_SUBADDRESS_MODE, SerializeToByteArray(SAM));
            }
            catch (Exception)
            {
                throw new Exception("Unable to complete SetSubAddressMode DeviceIoControl:" + Marshal.GetLastWin32Error());
            }
        }
 
        public void SetBaudRate(Speed speed)
        {
            try
            {
                UInt32 SPD = (UInt32)speed;
                this.DeviceIoControl(IOCTL_I2C_SET_BAUD_INDEX, SerializeToByteArray(SPD));
            }
            catch (Exception)
            {
                throw new Exception("Unable to complete SetBaudRate DeviceIoControl:" + Marshal.GetLastWin32Error());
            }
        }
 
        public int Write(byte register, byte value)
        {
            base.Seek((int)register, SeekOrigin.Current);
            return base.Write(SerializeToByteArray(value));
        }
 
        public int Write(byte register, UInt16 value)
        {
            base.Seek((int)register, SeekOrigin.Current);
            return base.Write(SerializeToByteArray(value));
        }
 
        public int Write(byte register, UInt32 value)
        {
            base.Seek((int)register, SeekOrigin.Current);
            return base.Write(SerializeToByteArray(value));
        }
 
        public int Write(byte register, byte data)
        {
            base.Seek((int)register, SeekOrigin.Current);
            return base.Write(data);
        }
         
        public byte ReadByte(byte register)
        {
            base.Seek((int)register, SeekOrigin.Current);
            return (byte)DeserializeFromByteArray(base.Read(sizeof(byte)),typeof(byte));
        }
 
        public Int32 ReadInt24(byte register)
        {
            base.Seek((int)register, SeekOrigin.Current);
            return (Int32)DeserializeFromByteArray24(base.Read(3), typeof(Int32));
        }
 
        public Int16 ReadInt16(byte register)
        {
            base.Seek((int)register, SeekOrigin.Current);
            return (Int16)DeserializeFromByteArray(base.Read(sizeof(Int16)), typeof(Int16));
        }
 
        public UInt32 ReadUInt32(byte register)
        {
            base.Seek((int)register, SeekOrigin.Current);
            return (UInt32)DeserializeFromByteArray(base.Read(sizeof(UInt32)), typeof(UInt32));
        }
 
        public void Read(byte register, ref byte data)
        {
            base.Seek((int)register, SeekOrigin.Current);
            data = base.Read(data.Length);
        }
 
 
        #region P/Invoke helpers
 
        /// <summary>
        /// Byte array serializer
        /// </summary>
        /// <param name="anything"></param>
        /// <returns></returns>
        private static byte SerializeToByteArray(object anything)
        {
            int rawsize = Marshal.SizeOf(anything);
            IntPtr buffer = Marshal.AllocHGlobal(rawsize);
            Marshal.StructureToPtr(anything, buffer, false);
            byte rawdatas = new byte[rawsize];
            Marshal.Copy(buffer, rawdatas, 0, rawsize);
            Marshal.FreeHGlobal(buffer);
            return rawdatas;
        }
 
        /// <summary>
        /// De-serializer from byte array
        /// </summary>
        /// <param name="rawdatas"></param>
        /// <param name="anytype"></param>
        /// <returns></returns>
        private static object DeserializeFromByteArray(byte rawdatas, Type anytype)
        {
            int rawsize = Marshal.SizeOf(anytype);
            if (rawsize > rawdatas.Length)
                return null;
            IntPtr buffer = Marshal.AllocHGlobal(rawsize);
            Marshal.Copy(rawdatas, 0, buffer, rawsize);
            object retobj = Marshal.PtrToStructure(buffer, anytype);
            Marshal.FreeHGlobal(buffer);
            return retobj;
        }
 
        private static object DeserializeFromByteArray24(byte rawdatas, Type anytype)
        {
            int rawsize = Marshal.SizeOf(anytype);
            IntPtr buffer = Marshal.AllocHGlobal(rawsize);
            Marshal.Copy(rawdatas, 0, buffer, rawdatas.Length);
            object retobj = Marshal.PtrToStructure(buffer, anytype);
            Marshal.FreeHGlobal(buffer);
            return retobj;
        }
 
         
        #endregion
     
    }
}

 Next comes the transducer class. This class will setup and configure the registers on the A/D converter using its slave address and the I2C bus specific instance.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Threading;
 
namespace Embedded101.I2C
{
    public static class Xducer
    {
        static I2C XducerBus;
        static Int32 Xducer1Offset = 0;
        static Int32 Xducer2Offset = 0;
 
 
        #region ctor /dtor
        /// <summary>
        /// Access to the pressure xducers on I2C bus
        /// </summary>
        public static void Init()
        {
            XducerBus = new I2C(I2CPort.I2C1);
            XducerBus.SetBaudRate(I2C.Speed.SPEED400KHZ);
            XducerBus.SetSlaveAddress(0x48);
            XducerBus.SetSubAddressMode(I2C.SubAddressMode.MODE_8);
            XducerBus.Write(0x02, unchecked((UInt16)0x00028000));      // lo thresh register
            XducerBus.Write(0x03, unchecked((UInt16)0x00037fff));      // hi thresh register
            XducerBus.Write(0x01, unchecked((UInt16)0x00010480));
            XducerBus.SetSubAddressMode(I2C.SubAddressMode.MODE_0);
            XducerBus.Write(0x48, 0);      // results register
        }
 
        #endregion
 
 
        #region Raw xducer readings
 
        public static Int32 Xducer1Raw
        {
            get
            {
                byte data = new byte[2];
                byte config = new byte[3];
 
                config[0] = 0x01;                   // point to configuration register
                config[1] = 0x34;                   // mux channel (AN2/AN3 = Xducer1)
                config[2] = 0x80;
                XducerBus.Write(0x48, config);      // config registers
                XducerBus.Write(0x48, 0);
                XducerBus.Read(0x48, ref data);
                Thread.Sleep(10);
                XducerBus.Read(0x48, ref data);
                data[0] ^= 0x80;
                return (256 * data[0] + data[1]);
             }
        }
 
        public static Int32 Xducer2Raw
        {
            get
            {
                byte data = new byte[2];
                byte config = new byte[3];
 
                config[0] = 0x01;                   // point to configuration register
                config[1] = 0x04;                   // mux channel (AN0/AN1 = Xducer2)
                config[2] = 0x80;
                XducerBus.Write(0x48, config);      // config registers
                XducerBus.Write(0x48, 0);
                XducerBus.Read(0x48, ref data);
                Thread.Sleep(10);
                XducerBus.Read(0x48, ref data);
                data[0] ^= 0x80;
                return (256 * data[0] + data[1]);
            }
        }
 
        #endregion
 
        /// <summary>
        /// Returns O2 pressure in mmHg x 10
        /// </summary>
        public static Int32 Xducer1Reading
        {
            get
            {
                double scale;
                scale = 0.128 * (double)(Xducer1Raw - 32768 + Xducer1Offset);
                return (Int32)scale;
            }
        }
 
        /// <summary>
        /// Returns scaled xducer reading
        /// </summary>
        public static Int32 Xducer2Reading
        {
            get
            {
                double scale;
                scale = 0.128 * (double)(Xducer2Raw - 32768 + Xducer2Offset);
                return (Int32)scale;
            }
        }
 
        public static void ZeroXducers()
        {
            Xducer1Offset = -(Xducer1Raw - 32768);
            Xducer2Offset = -(Xducer2Raw - 32768);
        }
    }
}

And finally, the form displaying the sensor reading:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Embedded101.I2C;
 
namespace I2CSensorApp
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            Xducer.Init();
            tmrUPDATE.Interval = 500;
            tmrUPDATE.Enabled = true;
        }
 
        private void tmrUPDATE_Tick(object sender, EventArgs e)
        {
            lblXDUCER1READING.Text = Xducer.Xducer1Reading.ToString() + " mmHg";
            lblXDUCER2READING.Text = Xducer.Xducer2Reading.ToString() + " mmHg";
        }
    }
}

 

You can see the initializing call Xducer.Init() which will instantiate the I2C bus and then configure its parameters by setting its bus speed and slave address. It then initializes the A/D converter by writing to some of its configuration registers. From here on we can read and display the transducer values. A read operation will automatically trigger an A/D convert cycle as well as a scaling operation to present the values in meaningful units. In this case pressure in millimeters of mercury (mmHg).

xducer

 

So we have come full circle. Now, with the reading local in memory we can send the data out to the world using our IoT transport of choice. Which loops us back to my previous post.