Questions About I2CDevice

I have completed a BME280Device class which provides a driver for the Bosch Sensortec BME280 device supporting I2C and SPI. My driver only supports an I2C connection. The driver works well. I want to make it available by posting it.

However, I started thinking about some issues on which I need some clarification. Because the BME280 consists of a temperature/pressure/humidity sensor, it lends itself to being paired (on the same I2C bus but with different addresses) for an indoor/outdoor data measuring application. As such, this means two (or more) BME280 I2C devices (plus potentially I2C devices of any other kind) on the I2C bus.

Because the I2CDevice uses a shared resource i.e., the I2C bus, (really the two ports that control the SDA and SCK lines for the I2C bus), what prevents attempts by other threads in an application to attempt to access more than one device on the I2C device at the same time?

When I first looked at the I2CDevice.Execute method:
public int Execute ( I2CTransaction[] xActions, int timeout)

My naive thought about the reason for the timeout parameter was that it comprised a timeout that would return (without exception) if the device failed to complete the handshakes required to implement the actions in the I2CTransaction array within the timeout period. But the help information about the Execute method says the following:

Executes a transaction by scheduling the transfer of the data involved.

So the I2CDevice.Execute method [em]schedules[/em] the execution of the transaction. Does this mean that the Execute method waits for any existing I2C bus transaction on the shared I2C bus to complete before starting the execution of the designated transaction, and only for the duration of the timeout? If so, then my driver does not need to concern itself with competition for use of the shared I2C bus with other threads in the program.

If the I2CDevice does not serialize the flow of data on the shared I2C bus, it means I must add more logic into the driver to serialize access the the shared I2C bus (at least for threads accessing BME280 devices).

The help documentation does not say what will happen when a call to I2CDevice.Execute fails to properly execute at the bus level. The Execute method reports the number of bytes transferred on the bus when returning from the command. Is this how one determines whether the call to the Execute method succeeded or failed? Or is an exception thrown? The documentation does not mention anything about an exception from the Execute method.

Without any definitive guidance from the help information on the I2CDevice.Execute method, should I assume that the timeout parameter controls how long the I2CDevice object will wait for the shared I2C bus to become idle before executing the Execute method? Should I assume that I2CDevice.Execute never throws an exception? Should I assume that successful execution of the Execute method occurs only when the result of that method consists of the sum of the bytes in all transactions found in the xActions parameter, i.e., the array of I2CTransactions?

@ srogers - Looking at CLR\Libraries\SPOT_Hardware\spot_hardware_native_Microsoft_SPOT_Hardware_I2CDevice.cpp in the porting kit, if you try to call Execute before the first call finished, you will get an invalid operation exception. Timeout is how long the system will wait for the transactions to finish before returning. The number of bytes returned by the call is the traditional method to finding out what succeeded.

1 Like

@ srogers

Locking to prevent contention. Have a static method that does all i2c operations.


    static object callMeI2CLockObject = new object();
    public static byte[] CallMei2C(byte someRegisterAddress, int bytesExpected) {
        byte[] myData = new byte[bytesExpected];
        lock (callMeI2CLockObject) {
            //do I2C stuff, on bus
            //that must not be interrupted by some
            //other thread!
        }
        //do other stuff that can be pre empted
        return myData;
    }

    public void thread1() {
        byte[] _myRegisterData = CallMei2C(255, 2); //cannot cause bus contention, because of lock
    }
    public void thread2() {
        byte[] _myRegisterData = CallMei2C(254, 2); //cannot cause bus contention, because of lock
    }

1 Like

I have two I2C ADC’s in my design and an I2C based touch LCD and I use the same method Mr John Smith suggests and that is a lock object.

With the 2 ADC’s being sampled around 15 times per second and the LCD only when someone touches it, this has worked flawlessly for years.

1 Like

@ Mr. John Smith -

Thank you very much for this information. I was wondering how this should be done correctly. I certainly don’t want to mess it up.

I am surprised that there is so little information about this in the I2CDevice help files. There is no mention of any kind of an exception that might be thrown upon any contention for the I2C bus.

I ended up making an extension method, Transact, for I2CDevice. Seems to work well. Probably should have been implemented this way from the beginning.


public static class I2DeviceExtensions
{
    /// <summary>
    /// Object I2CBusLock consists of a locking object for the I2C Bus.
    /// </summary>
    private static object I2CBusLock = new object();

    #region Transact extension method of I2CDevice (PUBLIC)
    /// <summary>
    /// <para>Transact extension method of I2CDevice accepts I2C trnasactions and a timeout</para>
    /// <para>value and calls upon I2CDevice to execute the I2C bus transactions in a thread</para>
    /// <para>save manner.</para>
    /// </summary>
    /// <param name="this">blind this parameter</param>
    /// <param name="transactions">An array of I2CTransaction to perform</param>
    /// <param name="timeout">A timeout value to perform the transactions</param>
    /// <exception cref="I2CException"></exception>
    public static void Transact(
        this I2CDevice @ this, 
        I2CDevice.I2CTransaction[] transactions, 
        int timeout)
    {
        int bytesToTransfer = 0;
        int bytesTransferred = 0;
        // Calculate bytesToTransfer
        foreach (var transaction in transactions) 
            bytesToTransfer += transaction.Buffer.Length;

        // Execute the transactions in a thread safe manner
        lock (I2CBusLock) 
        { 
            bytesTransferred = @ this.Execute(transactions, timeout); 
        }
        // Validate successful completion of transaction
        if (bytesToTransfer != bytesTransferred)
            throw new I2CException(
                "Error:  " +
                "I2C transaction of " + bytesToTransfer.ToString() +
                " byte(s) transferred " + bytesTransferred.ToString() +
                " byte(s).");
    }
    #endregion
}


public class I2CException : Exception
{
    public I2CException() { }
    public I2CException(string message) : base(message) { }
    public I2CException(string message, Exception inner) : base(message, inner) { }
}

@ srogers - There is no way to know if the programmer is intending to send those bytes down the bus or not. I’m not sure if the hardware I2C is atomic (i.e. cannot be interrupted) but I’m very sure the software one isn’t atomic (and can cause contentions). Just make sure you don’t put any infinite loops in the I2C stuff or else a deadlock condition will ocour.

@ Mr. John Smith -
John, I just updated my post with the code I’m proposing to use. I don’t know about whether you are commenting on my original post, or the one containing my extension method to I2CDevice. Do you see any vulnerabilities in my I2cDevice.Transact method?

@ srogers - The Transact method looks fine now. I noticed that you don’t use curly braces {} in that foreach and if statements. That’s a bad habit dude.

They don’t do anything of course and the code will work fine but I do agree that they make it far more easier to read and see the flow so they should always be used.

In fact, the Google Android developers frown upon any one who does not use them for all instances where the code would be indented.

1 Like