1
\$\begingroup\$

I have an ESP32 using an 8 bit I2C multiplexer TCA9548 IC. TCA9548 IC is connected to my ESP32 SCL and SCA pins.

I have the following I2C devices connected to the I2C TCA9548 expander so I can choose on code what device to access and control (in the future I will have other 6 I2C devices): I2C I/O 16 bit expander TCA6416 --- Connected to SC0/SD0 of TCA9548 MAX31875 I2C Temp Sensor --- Connected to SC1/SD1 of TCA9548 The TCA6416 I/O expander IC has its interrupt output pin connected to my ESP32 35 pin.

In order to get any event that happens in the TCA6416 I/O expander I created a FreeRTOS task that handles the interrupt. Here is the code:

// function for selecting the I2C Device
void TCA9548A(uint8_t bus)
{

  if (xSemaphoreTake(I2CMutexSemaphore, 100) == pdTRUE)
  {
    Wire.beginTransmission(0x70); // TCA9548A address
    Wire.write(1 << bus);         // send byte to select bus
    Wire.endTransmission();
    xSemaphoreGive(I2CMutexSemaphore);
  }
}

void setup()
{
   ... some code here
     
     // create task that will get any interrupt event on ESP32 35 pin.
      xTaskCreatePinnedToCore(&readInterruptEventTask, "interrupt taskr", 2048, NULL, 20, &TaskHandle_InterruptEvent, 0);
      
      // attach the interrupt with the initCallBack function
     attachInterrupt(35, intCallBack, FALLING);
   
}

void IRAM_ATTR intCallBack()
{
  xSemaphoreGiveFromISR(interruptISRSemaphore, &xHigherPriorityTaskWoken);
  if (xHigherPriorityTaskWoken)
  {
     portYIELD_FROM_ISR();
  }
  
}

void readInterruptEventTask(void *pArgs)
{
  (void)pArgs;
  
  while (true)
  {
    if (xSemaphoreTake(interruptISRSemaphore, portMAX_DELAY) == pdPASS)
    {
      handleInterrupt();
    }
  }
}

void handleInterrupt()
{
    TCA9548A(0); // select I2C Device 0, in this case the TCA6416 
    
    uint16_t gpioAB = mcp.readINTCAPAB(); // mcp is my TCA6416 I/O expander 
    
    ... some code here to process the result.
}

The above code works fine because when I push any button connected to my TCA6416 I/O expander I get into the handleInterrupt function and I can get the button that was pressed from other logic that reads the bytes of the TCA6416 I/O expander.

The problem comes here:

I have another task that every 500ms it gets the temperature from my MAX Temp Sensor.

Insise my setup() function I have this:

 xTaskCreate(
      requestTemp,
      "Temp Reading",
      4096,
      NULL,
      3,
      &TaskHandle_Temp);

Then my task does this:

void requestTemp(void *params)
{
    (void)params;

     for (;;)
    {
       TCA9548A(1); // Selects the MAX31875 I2C device
        tmp.getReading(); // gets the reading from the device.
        vTaskDelay(500 / portTICK_PERIOD_MS);
    }
     
}

If you see my requestTemp task is always running accessing on every loop the TCA9548A I2C device, at the same time my interrupt task could get an event from my TCA6416 I/O and then will also communicate to my TCA9548A I2C device in order to get the bytes and see which button was pressed. This is the reason of my reboot and problem.

What could I do? My interrupt method has of course a higher priority because I don't want to lose any event from my TCA6416 I/O IC.

Ideally what should happen is that when I get any interrupt action I should stop the requestTemp task before communication to the TCA9548A I2C device and when finish the interrupt code then I resume the requestTemp task so no conflict on communication at the same time to the TCA9548A I2C device but I already did that I get reboots randomly.

Appreciate any help please!!

\$\endgroup\$
8
  • 1
    \$\begingroup\$ Use a Mutex in your TCA9548A() function so that any task which calls it will block & wait if there's already another task using that function. \$\endgroup\$
    – brhans
    May 23 at 13:55
  • \$\begingroup\$ I already have that @brhans \$\endgroup\$
    – VAAA
    May 23 at 14:38
  • \$\begingroup\$ The problem is that when you have selected the right I2C device, then I have code that could be trying to get/set data to a device that might not be the right one at that moment, because maybe other task just changed the device. \$\endgroup\$
    – VAAA
    May 23 at 14:42
  • \$\begingroup\$ Sorry - didn't look that closely at your code - but the solution is roughly the same. You need a mutex to prevent your 2 different tasks from trying to access the I2C port at the same time. So both mcp.readINTCAPAB() and tmp.getReading() should wait on a common "I2C access" mutex before being allowed to continue. \$\endgroup\$
    – brhans
    May 23 at 14:44
  • \$\begingroup\$ The way you've written your code, if the attempted mutex lock in TCA9548A() fails then your other code just carries on and executes anyway. \$\endgroup\$
    – brhans
    May 23 at 14:53

2 Answers 2

0
\$\begingroup\$

Based on our discussion in the comments below your question, I think your problem is that you don't prevent your 2 different tasks from attempting to use the I2C port at the same time.
Your mutex lock in TCA9548A() only protects the resource while it's configuring the TCA9548A port expander - after that there's nothing preventing tmp.getReading() and mcp.readINTCAPAB() from both trying to use the I2C port at the same time.

My 1st suggestion is that you remove the xSemaphoreGive(I2CMutexSemaphore); line out of TCA9548A() and instead put it after the uint16_t gpioAB = mcp.readINTCAPAB(); line in handleInterrupt() and also after the tmp.getReading(); line in requestTemp().
This will have the effect of locking the I2C mutex for the entire operation rather than only locking it during TCA9548A().

My 2nd suggestion is that you modify TCA9548A() to return a bool which tells the calling code whether or not the mutex lock actually succeeded - something like:

bool TCA9548A(uint8_t bus)
{
  if (xSemaphoreTake(I2CMutexSemaphore, 100) == pdTRUE)
  {
    Wire.beginTransmission(0x70); // TCA9548A address
    Wire.write(1 << bus);         // send byte to select bus
    Wire.endTransmission();

    return true;
  }

  return false;
}

and then in handleInterrupt() you would do:

if (TCA9548A(0))
{
  // do stuff
}

and similarly in requestTemp().

You also indicate that tmp.getReading() can take as long as 15 seconds to execute. I'm guessing that you're performing multiple operations in that function (since there's no reason a simple I2C transaction to read a temperature register from a sensor should take that long) - so if that's the case then you could implement some kind of system to send a message to tmp.getReading() to tell it to abort its operation and allow the other task to process the interrupt handling code instead.
I'd suggest that you use a new semaphore for this - maybe call it something like requestTempAbortSemaphore and then in between each of the many operations inside tmp.getReading() you should check the semaphore with a wait time of 0. If the result of the check is pdTRUE then abort the read and exit tmp.getReading(), or if the result is pdFALSE then continue with the next operation.
You could implement this as a set of nested if() statements - something like:

if (xSemaphoreTake(requestTempAbortSemaphore, 0) == pdFALSE)
{
  // do 1st thing

  if (xSemaphoreTake(requestTempAbortSemaphore, 0) == pdFALSE)
  {
    // do 2nd thing

    if (xSemaphoreTake(requestTempAbortSemaphore, 0) == pdFALSE)
    {
      // do 3rd thing

      // etc ...
    }
  }
}

although this method would be a bit crude and unwieldy.
I'd prefer to implement it more like a simple state machine like:

bool readTempDone = false;
int readTempSeq = 0;

while (!readTempDone && (xSemaphoreTake(requestTempAbortSemaphore, 0) == pdFALSE))
{
  switch (readTempSeq)
  {
    case 0:
      // do 1st thing
      break;

    case 1:
      // do 2nd thing
      break;

    // more cases for more stuff

    default:
      // everything done
      readTempDone  = true;
      break;
  }
}

Then if an interrupt occurs and needs to be handled, do a xSemaphoreGive(requestTempAbortSemaphore); from handleInterrupt() before calling TCA9548A(0) to tell tmp.getReading() to abort and release the I2C port so that handleInterrupt() can use it instead.

\$\endgroup\$
0
\$\begingroup\$

The ESP32 has two I2C interfaces. The best way would be to dedicate one of them for the exclusive use of your I/O expander (TCA6416) and connect it directly to the ESP32. The rest of your devices could be connected to the other one via the TCA5948. That way the tasks/interrupts won't interfere with each other and no magic tricks will be needed in your code.

If you can't change the schematic at this stage, then I would have a single task handle all I2C communications. They have to be serialised anyways after all! The interrupt would signal to this task via a flag or semaphore.

As long as your I/O events are reasonably slow compared to the maximum time taken by any one sensor, that should work without having to cancel ongoing I2C transactions, which is always going to be a little tricky. What's more important is to interleave reading the I/O expander between the other I2C tasks.

In pseudo-code:

for n in sensors:
   if semaphore_wait(io_interrupt, timeout):
      read_expander()
   read_sensor(n)

If some of your sensors take a lot of time, eg. because the readout requires multiple I2C commands with delays between them, them you'll have to rewrite these to use a custom delay function that returns early when the abort flag is set.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.