SYSTEM_THREAD(ENABLED) and Shared I2C resource

I’m running 1.4.4 on a Boron.

I’ve got a port expander (TCA9535) attached to Wire that does multiple things:

  • Controls 2 relays
  • Controls 8 LEDs
  • Controls 6 other peripherals

The LED controls are run inside a Timer that fires every 63ms. The relay controls and other peripherals are done through the application loop on an ad-hoc basis. The other peripherals are turned on and off every second or two.

In testing I’ve found that the Relays will turn on or off for no reason I can tell. I’ve done enough testing to know its not my application code.

The issue only started happening when I introduced the LED Timer. I’ve had issues with Timers or other multithreading in the past, so now I think I could be having a collision on the port expander when the Timer fires.

This prompted me to try SINGLE_THREADED_BLOCK around I2C calls. This hung my application within 1 second of resetting.

I’m now using WITH_LOCK(Wire). It seems to be working, but as far as I can tell from the forum and documentation, this is not supported. Can someone chime in to say whether this is a supported approach? Is there some other approach that might be better?

You would be better off having an “update the TCA9535” function called from a timer. This function will read values defined elsewhere in variables (like a byte array) and apply them to the TCA9535 (or read the ports on the TCA9535 and update the array).

Then your LED timer and other functions only have to set or read the byte array and the update function (which must run faster that the led timer) will update/read the TCA9535 to your desired state. This way only one function ever calls the I2C functions and therefore you should have a much more stability.

WITH_LOCK(Wire) is the correct approach.

SINGLE_THREADED_BLOCK should be avoided for any mutex protected resource because it can very easily cause deadlock as you discovered.

The other option is to not use a Timer, which runs as a separate thread and introduces possibilities for unsafe thread operations. Handling all operations from loop, using millis() instead of a Timer, prevents thread safety issues can be an option in some cases.

Using a Finite State Machine (FSM) model in the loop() is a good idea and just using either millis() based timing or have the Timer set a volatile bool which you then test for in the loop() - so you keep all calls to Wire() from the application thread. Using these approaches I have never had an issue with controlling GPIO expanders - some devices have 32 GPIO ports and they are very time critical. This approach does mean that loop() cadence has to be kept high so no delay() use!

1 Like

@rickkas7 – Thanks for confirmation here. Just so I am sure I understand your response and the mechanisms behind it, can you confirm how WITH_LOCK works? My understanding is that:

  • I2C can be locked and unlocked through Wire.lock and Wire.unlock functions found in the device-os/wiring/src/spark_wiring_i2c.cpp library
  • These functions obtain a mutex lock / unlock from the rtos
  • WITH_LOCK(Wire) {} is a helpful macro with which to call these functions
    • Alternatively I could call Wire.lock and Wire.unlock

Is this all right or am I missing something?

@armor – I agree that an FSM type model might be foolproof, but I’d rather understand a use the built-in rtos mechanism unless it is inherently buggy. Further, I’d be concerned that I would still have this issue with device-os, as who knows how my FSM might collide with Particle’s device-os calls to Wire. For example, I’m pretty sure device-os is using an interrupt driven approach to communicate via Wire to the PMIC as of v1.1.0 or later.

I was replying to this question you posted. IMHO avoiding such problems in the application is about adopting a model or schema that is suited to what you are trying to do. You have raised an interesting point about the I2C comms with the PMIC but where are calls initiated from? I would put them in setup() and not from loop() function hence there could be no collide with the FSM running in loop().

Here is one place where device-os is communicating via I2C to the fuel gauge and PMIC. I was mistaken that it is purely interrupt driven, but it does seem like it would operate on a separate thread from my application. It does not use WITH_LOCK. Maybe @rickkas7 can clarify.

That being said – it seems to me like it would only make sense to adopt a fully FSM model for my application if I did not use SYSTEM_THREAD(ENABLED). That would be the only way I can think to insulate myself against multi-threaded issues with a shared resource like I2C. If I do want to use SYSTEM_THREAD(ENABLED), I need to use the WITH_LOCK functions and get to the bottom of these bugs, even if I used FSM.

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.