Read Analog data on your Raspberry PI with Windows IoT

The Raspberry 3 GPIO extension with a PCF8591 and a potentiometer

Windows IoT and Raspberry

If you installed Windows 10 IoT on your Raspberry PI and started to code some cool apps using your favorite framework, you may have found how easy it is to make some LEDs blinking from some C# code.

I did it and was not disappointed. I recently bought a Raspberry Pi 3, with some breadboards and all this fancy geeky stuff (thanks to Canakit). GPIO is really easy to use and is the first IRL extension to my code (beside screens of course). So basically, you can read and write binary data to (almost) each of these pins and therefore turn a LED on or off with a unbeatable amount of code:

GpioController gpio = GpioController.GetDefault();
GpioPin pin = gpio.OpenPin(5);
pin.SetDriveMode(GpioPinDriveMode.Output);
pin.Write(GpioPinValue.High);

However, what I didn't expect, is that with all this easy binary read/write the values on pins, you can't read (neither write) an analog value. So you can easily detect if there are 5volts on this pin or 0, but can't detect values inbetween. It will be either a 1 or a 0.

Of course, there is a solution, that came with my kit, it is called the PCF8591.

The Analog to Digital converter, aka PCF8591

This pretty piece of electronics takes 4 different analog inputs, called A0, A1, A2 and A3, read indepentantly their value from 0 to 5V (min and max) and then output these values, encoded on 8 bits (so from 0 to 255), on a clock basis (see picture below).

This is how the I2C bus works. It syncs to a clock (the output SCL pin of your PCF8591) and reads the data as it comes (the SDA pin).

Thankfully, there is a I2C bus on the Raspberry GPIO, and now this is how we can deal with it.

Reading a value from a potentiometer

I built the circuit as show here, as it is the way to go, and drew this "great" schema, thanks to the fritzing app. Basically, what we have here is the yellow cable from the potentiometer will have an output voltage from 5V to zero. This yellow cable is connected to the A0 input pin of the PCF8591. And the green and orange cable are respectively the data (SDA) and the clock (SCL), connected from the PCF8591 output to your Raspberry GPIO I2C (pins 3 and 5). 

The LED and its resistor are just here to verify it's working. It should be as bright as the potentiometer goes from zero to 1.

And now... the code!

I made for you a useful class to read the Analog value from the I2C bus of your Raspberry.

using System;
using System.Threading.Tasks;
using Windows.Devices.I2c;
namespace PhoebeCoeus.IoT.RaspberryUtility
{
    public class PCF8591 : IDisposable
 
    {
        /// <summary>
        /// The address of the device is 0x90, coded on 7 bits.
        /// To get the matching value om 8 bits, shift (>>) the bits by 1.
        /// </summary>
        private const byte addr_PCF8591 = (0x90 >> 1);
        private I2cDevice device;

        /// <summary>
        /// private constructor for internal use only.
        /// To instanciate a PCF8591 object, please use the static method PCF8591.Create();
        /// </summary>
        private PCF8591() { }

        /// <summary>
        /// Instanciate asyncronously a new PCF8591 object.
        /// </summary>
        /// <returns>A new PCF8591 object instance.</returns>
        public static Windows.Foundation.IAsyncOperation<PCF8591> Create()
        {
            return CreateAsync(I2cBusSpeed.StandardMode, I2cSharingMode.Exclusive).AsAsyncOperation();
        }

        /// <summary>
        /// Instanciate asyncronously a new PCF8591 object.
        /// </summary>
        /// <param name="BusSpeed">The I2C Bus Speed. Default value: StandardMode </param>
        /// <returns>A new PCF8591 object instance.</returns>
        public static Windows.Foundation.IAsyncOperation<PCF8591> Create(I2cBusSpeed BusSpeed)
        {
            return CreateAsync(BusSpeed, I2cSharingMode.Exclusive).AsAsyncOperation();
        }

        /// <summary>
        /// Instanciate asyncronously a new PCF8591 object.
        /// </summary>
        /// <param name="BusSpeed">The I2C Bus Speed. Default value: StandardMode </param>
        /// <param name="SharingMode">The I2C Sharing Mode. Default value is Exclusive. To use with caution </param>
        /// <returns>A new PCF8591 object instance.</returns>
        public static Windows.Foundation.IAsyncOperation<PCF8591> Create(I2cBusSpeed BusSpeed, I2cSharingMode SharingMode)
        {
            return CreateAsync(BusSpeed, SharingMode).AsAsyncOperation();
        }

        /// <summary>
        /// Instanciate asyncronously a new PCF8591 object.
        /// As System.Threading.Tasks.Task are not valid Windows Runtime type supported, this method has been set to private and is publicly exposed through the IAsyncOperation method "Create".
        /// </summary>
        /// <param name="BusSpeed">The I2C Bus Speed. Default value: StandardMode </param>
        /// <param name="SharingMode">The I2C Sharing Mode. Default value is Exclusive. To use with caution </param>
        /// <returns></returns>
        async static private Task<PCF8591> CreateAsync(I2cBusSpeed BusSpeed, I2cSharingMode SharingMode)
        {
            PCF8591 newADC = new PCF8591();
            /// advanced query syntax used to find devices on the RaspberryPi.
            string AQS = Windows.Devices.I2c.I2cDevice.GetDeviceSelector();
            var DevicesInfo = await Windows.Devices.Enumeration.DeviceInformation.FindAllAsync(AQS);
            if (DevicesInfo.Count == 0) throw new Exception("No Device Information were found with query: " + AQS);
            // I2C bus settings
            var settings = new Windows.Devices.I2c.I2cConnectionSettings(addr_PCF8591);
            settings.BusSpeed = BusSpeed;
            settings.SharingMode = SharingMode;
            // Reteives the device from the I2C bus with the given ID.
            newADC.device = await Windows.Devices.I2c.I2cDevice.FromIdAsync(DevicesInfo[0].Id, settings);
            if (newADC.device == null) throw new Exception("No I2C Device were found with ID " + DevicesInfo[0].Id);
            return newADC;
        }

        public void Dispose()
        {
            this.device.Dispose();
        }

        /// <summary>
        /// Returns an int value from 0 to 255 (included).
        /// </summary>
        /// <param name="InputPin">The Input pin on the PCF8591 to read analog value from</param>
        /// <returns></returns>
        public int ReadI2CAnalog(PCF8591_AnalogPin InputPin)
        {
            byte[] b = new byte[2];
            device.WriteRead(new byte[] { (byte)InputPin }, b);
            return b[1];
        }

        /// <summary>
        /// Returns an double value from 0 to 1.
        /// </summary>
        /// <param name="InputPin">The Input pin on the PCF8591 to read analog value from</param>
        /// <returns></returns>
        public double ReadI2CAnalog_AsDouble(PCF8591_AnalogPin InputPin)
        {
            return ReadI2CAnalog(InputPin) / 255d;
        }
    }

    /// <summary>
    /// The 4 available analog input pins on the PCF8591.
    /// Value defined by their internal address.
    /// </summary>
    public enum PCF8591_AnalogPin
    {
        A0 = 0x40,
        A1 = 0x41,
        A2 = 0x42,
        A3 = 0x43
    }
}

Et voila, ready to use, the PCF8591 class can now be used like this:

PCF8591 ADConverter = await PCF8591.Create();
double value = ADConverter.ReadI2CAnalog_AsDouble(PCF8591_AnalogPin.A0);

If needed, here is the whole code to upload to your Raspberry that creates a Timer, reads the analog value every 500 miliseconds and display the result in the console.

using System;
using System.Collections.Generic;
using Windows.ApplicationModel.Background;
using PhoebeCoeus.IoT.RaspberryUtility;

namespace testapp
{
    public sealed class StartupTask : IBackgroundTask 
    {
        BackgroundTaskDeferral deferral = null;
        PCF8591 ADConverter { get; set; }

        public async void Run(IBackgroundTaskInstance taskInstance)
        {
            try 
            {
                // set deferral to keep the Application running at the end of the method.
                deferral = taskInstance.GetDeferral();
                // creates a PCF8591 instance
                ADConverter = await PCF8591.Create();
                // set timer to be executed every 500ms.
                var timer = new System.Threading.Timer(Timer_Tick, null, 500, 300);

            }
            catch (Exception ex)
            {
                // dispose the PCF8591.
                if (ADConverter != null)
                    ADConverter.Dispose();

                // Terminates the application.
                if (deferral != null)
                    deferral.Complete();
            }

        }

        /// On Timer tick
        private void Timer_Tick(object sender)
        {
            try 
            {
                // reads the analog value from pin A0.
                double value = ADConverter.ReadI2CAnalog_AsDouble(PCF8591_AnalogPin.A0);
                // shows value in console
                System.Diagnostics.Debug.WriteLine("Analog value from A0 : " + value);
            }
            catch (Exception ex)
            {
                // dispose the PCF8591.
                ADConverter.Dispose();
                // Terminates the application.
                deferral.Complete();
            }

        }
    }
}

Aurelien Jacquot

Leave a Reply

Your email address will not be published.