A Better Way to Return Success Status and Data From I2C Transactions

I’ll try to explain what I have so far and what I think is flawed about it.

I have multiple devices connected to an I2C bus which I will be reading and writing to using a G30 and NETMF 4.3.

I have at this point elected to give control of the I2C bus to one thread.

A thread responsible for controlling an I2C device can create an I2CNode which includes all of the information necessary to make the transaction as well as handle for a callback function. It will then place this Node into a Queue.

When the I2C Thread runs it grabs a Node from the queue, makes the transaction happen, Executes the Callback to deliver whether the transaction was successful or not and any byte array that needs to be returned from the transaction.

My problem with this is that I believe this means that the I2C thread will actually be responsible for running the code contained within the callbacks. This becomes a throughput issue as the Callbacks become lengthy.

I do like the idea of the I2C Thread makes sure that the byte arrays returned get a valid reference that the other thread can hold on to. I like this because I don’t have to worry about the data being lost when I Dequeue the Node after the transaction. But I want the I2C Thread to end its responsibility there and get back to the queue for the next transaction.

Should I have another thread to handle lengthy logic that currently resides in these callbacks?

How would I then get the signal that this thread is finished?

Or is there a nice clean way that the thread which is handling the device can know that the data is ready and can safely access data returned by the I2C Thread?

Any advice?

You’re doing a lot of marshalling of data from thread to thread, and that eats RAM, which is a precious commodity on the G30. Is there any reason why you can’t just make your I2C classes thread-safe? For instance, in the past I have used a bus-arbitration mechanism based on locks that allows a thread to claim access to the I2C (or SPI) bus for the duration of a transaction. That way, you can use any thread without the time and mem overhead of marshalling or queuing of data.

I think there was even a codeshare entry for an I2C or SPI bus arbiter class that allowed you to even have contextual setting (speed, polarity, etc for SPI) on a per-caller basis. That’s the class that I based my own work on.

For instance :

[MethodImplAttribute(MethodImplOptions.Synchronized)]
internal byte RegisterRead(Registers reg)
{
    _buffer[0] = (byte)(((uint)reg >> 16) & 0xff);
    _buffer[1] = (byte)(((uint)reg >> 8) & 0xff);
    _buffer[2] = (byte)(((uint)reg & 0xf8) | SPI_READ);
    using (var releaser = _spiCM.ObtainExclusiveAccess(_spiConfig))
    {
        _spi.WriteRead(_buffer, 0, 3, _buffer, 3, 1, 3);
    }
    return _buffer[3];
}

The _spiCM obtains an exclusive lock and then applies the config desired by that SPI consumer (polarity, timing, etc). I use the same sort of mechanism with I2C. Using this, you can keep all of your data on the stack and it only takes space during the actual call.

(fwiw, this particular class ran at a very high polling rate against an accelerometer on a G30 and uses a single _buffer for all transactions, which is why the methods are using the ‘Synchronized’ attribute - so that no two in that class run at the same time. I could have accomplished the same effect with more use of locks)

Thank you for the suggestion.

Can you elaborate on what you mean by “marshalling data from thread to thread” and how I am doing that?

I was under the impression that since I am passing things by reference that I wouldn’t be expending unnecessary memory. The Queue and it’s Nodes cost some memory but I didn’t think it is very much.

I used ‘marshalling’ just to refer to passing things from thread to thread. In addition to the overhead of two queues, you are changing the lifespan of the buffers and increasing the possibility that pending requests or responses will back up in memory if you (even temporarily) generate more than can be sent or receive more than can be processed. To keep that from consuming all of the available memory, you would need to create a bounded queue - all of which is a bunch of extra complexity.

By using fixed buffers and thread locks, you are removing the cost of the queues, the possibility of unbounded consumption, the number of threads fighting for cycles (context switch cost), the cost of allocation/GC for elements in the queue, and trading that for a fixed overhead, simpler code and no allocations.

As you clearly realize, mem in the G30 is at a premium, so avoiding fragmentation and GCs due to ‘new’ allocations, and being frugal with your data structures is key. Avoiding context switches and allocations also goes toward faster and more deterministic performance. That’s why I opted for same-thread I2C and SPI with a bus arbiter instead of a dedicated IO thread.

I’m just getting back to this. I fleshed out a whole threaded method, and honestly i think it’s pretty cool, but the G30 absolutely hates it. Can’t really use the debugger at all. There’s just not enough memory. Strange thing is that it works well without debugging or even by adjusting the speed that I step through it changes how far it gets before problems… but I can’t see if I’m getting soft out-of-mem errors without the debugger. So I gotta scrap it and go with your advice.

At least it was a good exercise in doing some heavy multi-threading.

1 Like