MPR121 Driver for Windows IoT Core/Raspberry Pi 2

RPi and MPR121

I had a MPR121 in my tool box. After getting my Raspberry Pi 2 installed with Windows IoT Core, I wanted to get this MPR121 Capacitive touch break out board working with it. While Adafruit had a library for Arduino, there wasn’t an MPR121 Driver for Windows IoT Core readily available. So I wrote this Windows.IoT.Core.HWInterfaces.MPR121 library in C# that can be used with the Raspberry Pi. In addition to the sample app available in the Github repo,  I also  created a Touch sensitive Christmas tree using this library. This post describes a few things about the MPR121 library.

About MPR121

MPR121 is a proximity and capacitive touch controller from NXP Semiconductors. Adafruit has this chip on a breakout board that can be interfaced with micro controllers. It has 12 pins that can be utilized as Capacitive touch sensing electrodes. A partial set of these pins can also be used as GPIO pins. This first version of the library is geared towards capacitive touch configuration only.

MPR121 Breakout from Adafruit
MPR121 Breakout from Adafruit

Note the pin configuration related to the ADDR pin. In case you have other I2C devices and their address conflicts with the default I2C address of MPR121 (0x5A), then address can be changed by connecting the ADDR pin to VSS, VDD, SDA or SCL line. The library i created gives the options to use any of the addresses while it defaults to 0x5A

MPR121 Pin Description
MPR121 Pin Description

Wiring

The hookup to Raspberry Pi is pretty straightforward. Apart from SCL/SDA and the Power/Ground hookups, only other hook up is the IRQ Pin to the GPIO Pin#5 on the Raspberry Pi 2. I am using an Adafruit Pi Cobbler but it can be hooked up directly to the PI. The Capacitive sensing pins are connected to some cardboard wrapped in aluminum foil.

RPi and MPR121
MPR121 interfaced to Raspberry Pi with a Pi Cobbler

Interfacing

MPR121 board can be interfaced using I2C. Once I2C connection is established, specific registers can be read to get the status of the pins.  The touch status for each pin is maintained as a bit. 1 to indicate touch and 0 to indicate no touch. By reading a byte at register 0x00, the touch status of the first 8 pins can be obtained. Five bits on the next register 0x01 indicate the status of the remaining 5 pins. Here is touch the register map:

Touch Registers
MPR121 Touch Registers

All the register information can be found in this datasheet.

IRQ Pin

Brute force method is to read this register in a loop every few milliseconds to check if they are touched or not. Fortunately MPR121 provides an elegant way of notifying the status. Its has an IRQ pin that can be connected to any GPIO digital input pin on the micro controllers. The MPR121 pulls this pin low when there is a status change on any of the capacitive pins. So instead of looping to detect change, the micro controller can read the register only when the IRQ status changes. This library uses the IRQ pin.

MPR121 Driver for Windows IoT Core Code Walkthrough

The code for this driver can be found at my Github repository in Windows.IoT.Core.HWInterfaces .MPR121 folder.

  1. Its a simple Universal Windows App class library. Since it needs to work with GPIO and I2C, I added the Windows IoT Extensions for the UWP.
  2. The library contains two constructors. The default one and the overloaded constructor to change the I2C address and the IRQ pin.
            public MPR121():this(MPR121_I2CADDR_DEFAULT, IRQ_HOOKUPPIN_DEFAULT){}
    
            public MPR121(byte mprAddress, int mprIRQHookupPin)
            {
                __i2cAddress = mprAddress;
                __mpr121IRQHookupPIN = mprIRQHookupPin;
    
                __initPins();
            }
    
  3. The I2C connection is established by calling the OpenConnection method. This method accepts a Windows IoT core device descriptor id sting of the I2C master on the Raspberry PI. Once connection is established, a soft reset command is issued. After setting up the capacitive sense thresholds as defined in the Application note, i hook up the Interrupt pin.
            public async Task<bool> OpenConnection(string i2cMasterId)
            {
    
                //Establish I2C connection
                __connection = await I2cDevice.FromIdAsync(i2cMasterId, new I2cConnectionSettings(this.__i2cAddress));
                
                // soft reset
                I2cTransferResult result = __connection.WritePartial(new byte[] { Registers.MPR121_SOFTRESET, 0x63 });
    
                if (result.Status == I2cTransferStatus.SlaveAddressNotAcknowledged)
                {
                    throw new Exception(string.Format("MPR121 at address {0} not responding.", this.__i2cAddress));
                }
    
                await Task.Delay(1);
    
                writeRegister(Registers.MPR121_ECR, 0x0);
    
                byte c = readRegister8(Registers.MPR121_CONFIG2);
    
                if (c != 0x24) return false;
    
    
                SetThresholds(12, 6);
    
                //Section A Registers - Ref: AN3944, MPR121 Quick Start Guide
                //This group of setting controls the filtering of the system when the data is greater than the baseline. 
                //Settings from Adafruit Libary.. Most probably callibrated for the Adafruit's MPR121 breakout board.
                writeRegister(Registers.MPR121_MHDR, 0x01);
                writeRegister(Registers.MPR121_NHDR, 0x01);
                writeRegister(Registers.MPR121_NCLR, 0x0E);
                writeRegister(Registers.MPR121_FDLR, 0x00);
    
                //Section B Registers - Ref: AN3944, MPR121 Quick Start Guide
                writeRegister(Registers.MPR121_MHDF, 0x01);
                writeRegister(Registers.MPR121_NHDF, 0x05);
                writeRegister(Registers.MPR121_NCLF, 0x01);
                writeRegister(Registers.MPR121_FDLF, 0x00);
    
                writeRegister(Registers.MPR121_NHDT, 0x00);
                writeRegister(Registers.MPR121_NCLT, 0x00);
                writeRegister(Registers.MPR121_FDLT, 0x00);
    
                writeRegister(Registers.MPR121_DEBOUNCE, 0);
                writeRegister(Registers.MPR121_CONFIG1, 0x10); // default, 16uA charge current
                writeRegister(Registers.MPR121_CONFIG2, 0x20); // 0.5uS encoding, 1ms period
    
                writeRegister(Registers.MPR121_ECR, 0x8F);  // start with first 5 bits of baseline tracking
    
                InitMPR121TouchInterrupt();
    
                return true;
            }
    
  4. One of the great advantages of the Windows IoT core is the native event framework. It makes it quite easy to design the software to respond to events on the connected GPIO pins. So the GPIO pin is simply initialized and hooked up to the ValueChanged event. Whenever IRQ pin value changes, the GPIO pin 5 will raise the event and execute the attached event handler. Note that the event execution happens in a different thread than the UI thread. So the event handler should account for it. You can see the Mpr122IRQpin_ValueChanged event handler below.
            private void InitMPR121TouchInterrupt()
            {
                GpioController gpio = GpioController.GetDefault();
    
                __mpr122IRQpin = gpio.OpenPin(__mpr121IRQHookupPIN);
    
                __mpr122IRQpin.SetDriveMode(GpioPinDriveMode.Input); //Adafruit IRQ already has a pull up. When MPR121 detects touch it pulls IRQ low.
    
                __mpr122IRQpin.ValueChanged += Mpr122IRQpin_ValueChanged; //hook up the interrupt event.
            }
    
  5. Before we look at the ValueChanged event handler, a word on the PinId enum flag that i am using to identify the pins. If you notice the touch status register information above, one bit is used to indicate each pins status. So having them represented with flags Enum makes it natural to translate the data read from the registers. The Flags enum also has an HasFlags method that makes it quite easy to check which pins are touched/released.
        [Flags]
        public enum PinId
        {
            None = 0,
            PIN_0 = 1 << 0, // = 1
            PIN_1 = 1 << 1, // = 2
            PIN_2 = 1 << 2, // = 4
            PIN_3 = 1 << 3, // = 8
            PIN_4 = 1 << 4, // = 16
            PIN_5 = 1 << 5, // = 32
            PIN_6 = 1 << 6, // = 64
            PIN_7 = 1 << 7, // = 128
            PIN_8 = 1 << 8, // = 256
            PIN_9 = 1 << 9, // = 512
            PIN_10 = 1 << 10, // = 1024
            PIN_11 = 1 << 11  // = 2048
        }
    
  6. When the value changed event handler is invoked, the MPR121 pulls the IRQ pin low. So i track for the FallingEdge on the GPIO pin. Also MPR121 expects the status registers to be read immediately. The status register will hold the data for that moment. If two pins are touched at the same time, the register will have the bits for both pins turned on. So by simply reading the two bytes from address 0x00 and casting it in to PinId enum, I get the list of pins that are currently touched. Additionally i want to expose both the touched and released events. So i compare it with the previous touched event and do necessary checks to obtain newly touched pins and newly released pins.
    In order for the consumer hook up to simple Touched and Released events, I have included two events in the MPR121 class and invoke those two events with the appropriate event arguments.In addition I also update a Pins list the represents the continuous state of all the pins on the MPR121.
            PinId currentReading = PinId.None; 
            PinId lastReading = PinId.None;
    
            private void Mpr122IRQpin_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs args)
            {
                if (args.Edge == GpioPinEdge.FallingEdge)
                {
                    //read the touch status register and convert it in to CapsenseElectrode Enum
                    int rawTouchRegisterData = readTouchStatusRegister();
                    currentReading = (PinId)rawTouchRegisterData;
    
                    List<PinId> allTouchedPins = new List<PinId>();
                    List<PinId> allReleasedPins = new List<PinId>();
    
                    foreach (PinId val in Enum.GetValues(typeof(PinId))) //loop through all CapSense Pins
                    {
                        if (val != PinId.None)
                        {
                            if (currentReading.HasFlag(val) && !lastReading.HasFlag(val))
                            {
                                allTouchedPins.Add(val);
                                this.__pins.Find(p => p.PinId == val).SetTouched(true);
                            }
    
                            if (!currentReading.HasFlag(val) && lastReading.HasFlag(val))
                            {
                                allReleasedPins.Add(val);
                                this.__pins.Find(p => p.PinId == val).SetTouched(false);
                            }
                        }
                    }
    
                    allTouchedPins.TrimExcess();
                    allReleasedPins.TrimExcess();
                    if (allTouchedPins.Count > 0)
                    {
                        PinTouchedEventArgs touchedData = new PinTouchedEventArgs(allTouchedPins);
                        OnPinTouched(touchedData);
                    }
    
                    if (allReleasedPins.Count>0)
                    {
                        PinReleasedEventArgs releasedData = new PinReleasedEventArgs(allReleasedPins);
                        OnPinReleased(releasedData);
                    }
    
                    lastReading = currentReading;
                }
            }
    

Sample Usage

I have created a Nuget package  Windows.IoT.Core.HWInterfaces .MPR121. So it can be installed to your Universal Windows app targeting Raspberry Pi from Nuget using:

Install-Package Windows.IoT.Core.HWInterfaces.MPR121

Once Installed,  initialize the mpr121 , open connection and hook up the events.

using Windows.Devices.Enumeration;
using Windows.Devices.I2c;
using Windows.IoT.Core.HWInterfaces.MPR121;
......
        private MPR121 __mpr121 = null;
        private async void __initMPR121()
        {
            //Get the I2C device list on the Raspberry Pi.
            string aqs = I2cDevice.GetDeviceSelector(); //get the device selector AQS  (adavanced query string)
            var i2cDeviceList = await DeviceInformation.FindAllAsync(aqs); //get the I2C devices that match the device selector aqs

            //if the device list is not null, try to establish I2C connection between the master and the MPR121
            if (i2cDeviceList != null && i2cDeviceList.Count > 0)
            {
                bool connected = await __mpr121.OpenConnection(i2cDeviceList[0].Id);
                if (connected)
                {
                    this.txtStatus.Text = "Connected..";
                    //MPR121 will raise Touched and Released events if the IRQ pin is connected and configured corectly.. 
                    //Adding event handlers for those events
                    __mpr121.PinTouched += __mpr121_PinTouched;
                    __mpr121.PinReleased += __mpr121_PinReleased; ;
                }
            }
        }

        private void __mpr121_PinTouched(object sender, PinTouchedEventArgs e)
        {
            var task=Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
            {
                this.txtStatus.Text = e.Touched[0].ToString() + " Touched"; //just the first touched pin
                __updatePinStatusUI(e.Touched, true);
            });
        }

        private void __mpr121_PinReleased(object sender, PinReleasedEventArgs e)
        {
            var task = Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
            {
                this.txtStatus.Text = e.Released[0].ToString() + " Released"; //just the first released pin
                __updatePinStatusUI(e.Released,false);
            });
        }

        private void __updatePinStatusUI(List<PinId> pins, bool turnOn)
        {
            SolidColorBrush pinBrush = new SolidColorBrush(Windows.UI.Colors.Red) ;
            if (turnOn)
                pinBrush = new SolidColorBrush(Color.FromArgb(100, 38, 247, 5));

            foreach (PinId pin in pins)
            {
                (pinStatusUIElements[(Array.IndexOf(Enum.GetValues(typeof(PinId)), pin) - 1)] as Ellipse).Fill = pinBrush;
            }
        }
Here is simple UI the tracks the pins pressed. The touch electrodes are connected to cardboard wrapped in aluminium foils. The complete sample is available at the Github repository.
captouch

Future Considerations

The code might not be very tight. There is lot of room for refactoring. In addition,  when time permits I hope to update this library to expose the GPIO configuration and the proximity sensing configuration. But all in all I ended up learning quite a few things when putting this together.


Leave a Reply

Your email address will not be published. Required fields are marked *