Who is up for a challenge? Software I2C

Your application may require multiple I2C devices or you may need I2C slave then what would you do? What about if you need I2C to work on any of the pins not just what is defined by hardware?

You can use the one wire support from GHI that runs on any IO. There are one wire chips to do most the things I2C chips do and it only needs one wire! But let us assume you still want I2C.

The answer is, I2C software emulation. This will be much slower than using the real hardware to run I2C but in many applications, this is very well okay. Especially that most I2C slave chips are used to control simple things.

Here is a class to emulate I2C. I didn’t test it but I went I am 90% sure it will just work. I ran out of time and so I am asking if someone in this community would be interested in testing it or adding to it then posting it on fezzer…you still get the points for doing so :wink:

I have tested the code on C++ before so I only ported it to C#. The code came from here I²C - Wikipedia

Here is how it works

SoftwareI2C i2c = new SoftwareI2C(Cpu.Pin.GPIO_Pin0, Cpu.Pin.GPIO_Pin1, 100);
            i2c.i2c_tx(true, false, 0x55);//send start and address
            i2c.i2c_rx(true, true);//read a byte with nack then stop

Here you go


using System;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;

namespace GHIElectronics.NETMF.Hardware.Emulation
{
    class SoftwareI2C
    {
        TristatePort _SCL, _SDA;
        int I2CSPEED = 100;///////////
        public SoftwareI2C(Cpu.Pin SCL, Cpu.Pin SDA, int clock)
        {
            _SCL = new TristatePort(SCL, true, false, Port.ResistorMode.PullUp);
            _SDA = new TristatePort(SDA, true, false, Port.ResistorMode.PullUp);
            _SCL.Write(false);
            _SDA.Write(false);
            MakePinInput(_SCL);
            MakePinInput(_SDA);
            //I2CSPEED = clock/ 1111????????????????
        }
        private void MakePinOutput(TristatePort port)
        {
            if (port.Active == false)
                port.Active = true;
        }
        private void MakePinInput(TristatePort port)
        {
            if (port.Active == true)
                port.Active = false;
        }
        void I2CDELAY(int delay)
        {
            //add code for delay
        }

        void CLRSCL()
        {
            _SCL.Write(false);
            MakePinOutput(_SCL);
        }
        void CLRSDA()
        {
            _SDA.Write(false);
            MakePinOutput(_SDA);
        }

        bool READSCL()
        {
            MakePinInput(_SCL);

            return _SCL.Read();
        }
        bool READSDA()
        {
            MakePinInput(_SDA);
            return _SDA.Read();
        }
        private bool start = false;
        bool read_bit()
        {
            bool bit;

            /* lets the slave drive data */
            READSDA();
            I2CDELAY(I2CSPEED);
            /* Clock stretching */
            while (READSCL() == false) ;
            /* SCL is high, now data is valid */
            bit = _SDA.Read();
            I2CDELAY(I2CSPEED);
            CLRSCL();
            return bit;
        }

        int write_bit(bool bit)
        {
            if (bit)
                READSDA();
            else
                CLRSDA();
            I2CDELAY(I2CSPEED);
            /* Clock stretching */
            while (READSCL() == false) ;
            /* SCL is high, now data is valid */
            /* check that nobody is driving SDA */
            if (bit && READSDA() == false)
                return 0;//lost arbitration
            I2CDELAY(I2CSPEED);
            CLRSCL();

            return 1;
        }

        int start_cond()
        {
            if (start)
            {
                /* set SDA to 1 */
                READSDA();
                I2CDELAY(I2CSPEED);
                /* Clock stretching */
                while (READSCL() == false) ;
            }
            if (READSDA() == false)
                return 0;
            /* SCL is high, set SDA from 1 to 0 */
            CLRSDA();
            I2CDELAY(I2CSPEED);
            CLRSCL();
            start = true;
            return 1;
        }
        int stop_cond()
        {
            /* set SDA to 0 */
            CLRSDA();
            I2CDELAY(I2CSPEED);
            /* Clock stretching */
            while (READSCL() == false) ;
            /* SCL is high, set SDA from 0 to 1 */
            if (READSDA() == false)
                return 0;
            I2CDELAY(I2CSPEED);
            start = false;

            return 1;
        }
        public bool i2c_tx(bool send_start, bool send_stop, byte d)
        {
            uint bit;
            bool nack;

            if (send_start)
                start_cond();
            for (bit = 0; bit < 8; bit++)
            {
                write_bit((d & 0x80) != 0);
                d <<= 1;
            }
            nack = read_bit();
            if (send_stop)
                stop_cond();

            return nack;
        }
        public byte i2c_rx(bool nak, bool send_stop)
        {
            byte d = 0;
            uint bit;

            for (bit = 0; bit < 8; bit++)
            {
                d <<= 1;
                if (read_bit())
                    d |= 1;
            }
            write_bit(nak);
            if (send_stop)
                stop_cond();
            return d;
        }

    }
}

I would like to help with that.

Thanks,
Valentin

good, please let me know if you need help

I refactored it to make things a little clearer and remove all the underscores and bad casing :stuck_out_tongue:


using System;
using Microsoft.SPOT.Hardware;
 
namespace GHIElectronics.NETMF.Hardware.Emulation
{
    class SoftwareI2C
    {
        TristatePort scl, sda;
        int i2cSpeed = 100;

        bool start;

        public SoftwareI2C(Cpu.Pin SCL, Cpu.Pin SDA, int clock)
        {
            start = false;

            scl = new TristatePort(SCL, true, false, Port.ResistorMode.PullUp);
            sda = new TristatePort(SDA, true, false, Port.ResistorMode.PullUp);
            scl.Write(false);
            sda.Write(false);
            MakePinInput(scl);
            MakePinInput(sda);
            //I2CSPEED = clock/ 1111????????????????
        }

        static void MakePinOutput(TristatePort port)
        {
            if (!port.Active)
                port.Active = true;
        }

        static void MakePinInput(TristatePort port)
        {
            if (port.Active)
                port.Active = false;
        }

        void I2CDELAY(int delay)
        {
            //add code for delay
        }
 
        void ClearSCL()
        {
            scl.Write(false);
            MakePinOutput(scl);
        }

        void ClearSDA()
        {
            sda.Write(false);
            MakePinOutput(sda);
        }
 
        bool ReadSCL()
        {
            MakePinInput(scl);
 
            return scl.Read();
        }

        bool ReadSDA()
        {
            MakePinInput(sda);
            return sda.Read();
        }

        bool ReadBit()
        {
            // lets the slave drive data
            ReadSDA();

            I2CDELAY(i2cSpeed);
            
            // Clock stretching 
            while (!ReadSCL())
            {
                // do nothing
            }
            
            // SCL is high, now data is valid
            bool bit = sda.Read();

            I2CDELAY(i2cSpeed);

            ClearSCL();
            
            return bit;
        }
 
        bool WriteBit(bool bit)
        {
            if (bit)
            {
                ReadSDA();
            }
            else
            {
                ClearSDA();
            }

            I2CDELAY(i2cSpeed);

            // Clock stretching
            while (!ReadSCL())
            {
                // do nothing
            }

            // SCL is high, now data is valid
            // check that nobody is driving SDA
            if (bit && !ReadSDA())
            {
                return false;//lost arbitration
            }

            I2CDELAY(i2cSpeed);
            ClearSCL();
 
            return true;
        }
 
        bool SendStartCondition()
        {
            if (start)
            {
                // set SDA to 1 
                ReadSDA();
                I2CDELAY(i2cSpeed);

                // Clock stretching 
                while (!ReadSCL())
                {
                    // do nothing
                }
            }

            if (!ReadSDA())
            {
                return false;
            }

            // SCL is high, set SDA from 1 to 0 
            ClearSDA();
            I2CDELAY(i2cSpeed);
            ClearSCL();

            start = true;

            return true;
        }

        bool SendStopCondition()
        {
            // set SDA to 0 
            ClearSDA();
            I2CDELAY(i2cSpeed);

            // Clock stretching 
            while (!ReadSCL())
            {
                // do nothing
            }

            // SCL is high, set SDA from 0 to 1 
            if (!ReadSDA())
            {
                return false;
            }

            I2CDELAY(i2cSpeed);
            start = false;
 
            return true;
        }

        /// <summary>
        /// Sends data to the remote device
        /// </summary>
        /// <param name="sendStartCondition">Perform a HIGH to LOW transition of SDA line while the SCL is high.</param>
        /// <param name="sendStopCondition">Perform a LOW to HIGH transition of SDA line while the SCL is high.</param>
        /// <param name="byteToSend">Byte to transmit</param>
        /// <returns></returns>
        public bool Transmit(bool sendStartCondition, bool sendStopCondition, byte byteToSend)
        {
            if (sendStartCondition)
            {
                SendStartCondition();
            }

            for (int bit = 0; bit < 8; bit++)
            {
                WriteBit((byteToSend & 0x80) != 0);
                byteToSend <<= 1;
            }

            bool nack = ReadBit();

            if (sendStopCondition)
            {
                SendStopCondition();
            }
 
            return nack;
        }

        /// <summary>
        /// Receive data from remote device.
        /// </summary>
        /// <param name="acknowledgeBit">
        /// Each device when addressed to has to generate an acknowledge signal after the reception of each byte. 
        /// The master generates an extra clock pulse which is associated with the <c>acknowledgeBit</c>. 
        /// The device that acknowledges pulls down the SDA line during the acknowledge clock pulse.
        /// </param>
        /// <param name="sendStopCondition">Perform a LOW to HIGH transition of SDA line while the SCL is high.</param>
        /// <returns></returns>
        public byte Receive(bool acknowledgeBit, bool sendStopCondition)
        {
            byte d = 0;
 
            for (int bit = 0; bit < 8; bit++)
            {
                d <<= 1;

                if (ReadBit())
                    d |= 1;
            }

            WriteBit(acknowledgeBit);

            if (sendStopCondition)
            {
                SendStopCondition();
            }

            return d;
        }
 
    }
}

Hi Gus,

I don’t know if you are still interested in this topic, but I spent some time over the weekend looking at the software I2C code. There appear to be a few problems with the versions posted on the forum, but nothing too serious. For example, in the constructor … by default, scl and sda are created as inputs. So the calls to scl.Write and sda.Write immediately after creating the new TristatePorts cause exceptions. Similar things happen in ClearSCL and ClearSDA … exceptions will occur unless MakePinOutput is called before the writes. These problems are easy to fix.

The biggest problem I’ve found so far is that glitches are appearing on the outputs … especially on the serial clock. I think that they are associated with changing the pin between input and output, but I’m not sure about that yet. The glitches are about 0.5 uS wide and are seen by the peripheral as extraneous clock transitions.

Al

I am sure someone will need it sooner or later and we always appreciated contributions :slight_smile:

This can go on http://code.tinyclr.com/ for everyone’s benefit

Gus,

I’ll be glad to share the code … if I can figure out a way to deal with the problems I’m having. :frowning: Additional pulses are appearing on both of the pins that I’m using for the software I2C port. Below is a code snippet that generates this behavior -


using System;
using System.Threading;

using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;

using GHIElectronics.NETMF.FEZ;

namespace FEZ_Domino_Application1
{
    public class Program
    {
        public static void Main()
        {
            TristatePort scl = new TristatePort((Cpu.Pin)FEZ_Pin.Digital.Di3, true, false, Port.ResistorMode.PullUp);

            while (true)
            {
                Thread.Sleep(20);
                scl.Active = true;
                scl.Write(false);
                Thread.Sleep(20);
                scl.Active = false;
            }
        }
    }
}

Rising edges (where the processor just lets go of the pin and allows the internal pull up resistor to do its job) are clean. Falling edges (where the processor actively pulls the pin down) are always preceded by a 0.5 usec wide glitch as shown in the image.

Any ideas?

If you see those on SDA then it is okay. These do not mean anything if clock is not active.

Yes, I agree … they’re not a problem if they are on the SDA line. The problem is that they are on the clock too.

Your clock is outputport right?

It’s a tristate port … I switch from op to ip by setting scl.Active true or false.

We need it to support clock streching right? So if we do not support that then we can use outputport and no gliches?

Yes … I believe that’s correct. If we simply the code so the clock pin is always an output we should be OK.

Ok good that is an option. Now we need to know why switching tristate causes the pin to go low. Can you please demo this in couple lines of code so I can pass this on to the experts at GHI?

The piece of code that I posted this afternoon is about as short as it can get and still be complete and executable. It just toggles a pin over and over again. The glitch is shown in the screen grab from the logic analyzer. I’ll be interested to hear what the experts have to say … I have an application in mind for software I2C.

Oh yes that is good

I got an answer back…

there is actually no problem but the way you have your code will cause the pin to change in its high level state which is probably confusing your digital analyzer.

Still, that is not important as the code should be written differently. You should always keep the pin default to low and actually set that right on you create the object and then you only set pin to input and output

Try this on your end and see logic analyzer (change the pin number)


 TristatePort scl = new TristatePort((Cpu.Pin)18, false, false, Port.ResistorMode.PullUp);
                
            while (true)
            {
                scl.Active = true;
                Thread.Sleep(10);

                scl.Active = false;
                Thread.Sleep(10);
                
            }

Now we need to try this on an actual I2C device and make sure it is working as expected.

Gus,
I just tried the code you posted this afternoon. That solved the problem! :slight_smile: The glitches are all gone. I’ll change the software I2C code tomorrow and do some testing with real devices … I have a couple of different sensors that I can try. Thanks!

It works! I have the code running on a Domino, reading the temperature and relative humidity from an SHT21. I want to clean the code up a bit and test it with a few more devices before I post it.

Great :clap: