I2C cross-chatter best practices?

For Adafruit customers who seek help with microcontrollers

Moderators: adafruit_support_bill, adafruit

Please be positive and constructive with your questions and comments.
Locked
User avatar
pjforde1978
 
Posts: 21
Joined: Thu Oct 30, 2014 12:48 am

I2C cross-chatter best practices?

Post by pjforde1978 »

I have an Adafruit rotary encoder backpack on the same I2C as an EEPROM and a 7-segment backpack. My intention is to write a value to the EEPROM and update the 7-segment display when you turn the encoder.

In practice, it's very easy to turn the encoder just fast enough to interfere with the writes to the EEPROM and 7-segment display. I believe that this is because the seesaw library sets up an interrupt (via the enableEncoderInterrupt() method) which means the I2C channel is susceptible to simultaineous write instructions from the interrupt and statements in my loop() function.

I have not been able to figure out where/how the interrupt seesaw enables is defined.

Bigger picture, I figure that updating a display when an encoder is turned is such a common use case that there must be a way people do this which doesn't result in I2C conflicts:
  • there could be a way to define my own encoder interrupt, which would allow me to update the other I2C devices in the same function
  • there could be a method exposed on the Wire object which would allow me to wait until the interface is free before attempting to update the other devices
  • there could be a way in which I could create a callback or "interrupt on the interrupt" so that my interrupt could run after the seesaw interrupt
  • I might just have to run the encoder on a seperate I2C interface, but this feels like the coward's approach (and it seems like you'd run into the same BS if you have more than one rotary encoder, anyhow)
The real issue with any kind of "wait until I2C isn't busy" strategy is that my loop() could dutifully block until the interface is idle, only to be interrupted mid-write. The interrupt is a honey badger.

Any war stories are appreciated. Working on a QT Py 2040 with the Arduino 2 IDE.

User avatar
adafruit_support_mike
 
Posts: 67391
Joined: Thu Feb 11, 2010 2:51 pm

Re: I2C cross-chatter best practices?

Post by adafruit_support_mike »

The general rule of thumb is, "if multiple devices need to play nicely together, don't allow anything to update automatically."

The general subject of interaction between automatic processes is subtle and full of pain. In almost all cases, it's both easier and more reliable to use flags and a periodic update function. If you have a display, an EEPROM, and an encoder, write a single routine that reads/updates each of them, one at a time, under control of a boolean flag:

Code: Select all

    if ( eeprom_needs_update ) {
    	// eeprom code
    	eeprom_needs_update = false;
    }
    if ( encoder_needs_update ) {
    	// encoder code
    	encoder_needs_update = false;
    }
    if ( display_needs_update ) {
        // display code
        display_needs_update = false;
    }
Then the rest of your code only has the option to fill data structures those blocks of code will use, and to set those three flags true.

That gives you a system that's mostly commutative: insensitive to the order of operations. It doesn't matter how many times you set a single flag true, or which 'set the flag true' operation happens first. They all end with the flag being set to true. Likewise, it doesn't matter what order you call operations that set the EEPROM, display, and encoder flags true, they all end with the flags being true and the handler function reading the flags in the same order.

The system isn't perfectly commutative because a later update can replace data written by an earlier one. There are some risks to that, but there are also straightforward ways to make things behave properly.. making it impossible for a later update to replace information from an earlier one if the 'needs_update' flag is true, or making sure a later update replaces everything that could have been saved by a previous one.. an update whose data is no longer available to the program is functionally identical to an update that never happened.

User avatar
pjforde1978
 
Posts: 21
Joined: Thu Oct 30, 2014 12:48 am

Re: I2C cross-chatter best practices?

Post by pjforde1978 »

Thanks for this.

Your advice is eminently sensible, but I am still a bit stuck on how the enableEncoderInterrupt() method of the seesaw library is intended to work.

All of the example code on the Adafruit Learn page for the encoder backback contains the following in the setup() function:

Code: Select all

  ss.setGPIOInterrupts((uint32_t)1 << SS_SWITCH, 1);
  ss.enableEncoderInterrupt();
It's never really explained what each of these methods do. Are they both explicitly necessary? Are they connected or two different things which both concern interrupts? Unfortunately, the source code comments are of the overly literal variety, without addressing their purpose or larger context.

This does prompt what seems like a really good question, though: why do I need to make those calls in setup() if I ultimately end up calling

Code: Select all

  pressed = !ss.digitalRead(SS_SWITCH);
  newPosition = ss.getEncoderPosition();
in my loop(), anyhow? What is this mystery interrupt even doing?

For what it's worth, commenting-out the enableEncoderInterrupt() call appears to solve the crosstalk problem. I just don't know what this functionality is intended to do, and what I might be giving up by not calling it. After all, it wouldn't be in the code example if it wasn't meaningful and useful, right? *cough*

User avatar
pjforde1978
 
Posts: 21
Joined: Thu Oct 30, 2014 12:48 am

Re: I2C cross-chatter best practices?

Post by pjforde1978 »

I found this post from a few years ago, and it provides some new leads.

It seems as though the setGPIOInterrupts() and enableEncoderInterrupt() method calls are only applicable if you have a digital output connected to the INT pin on the encoder backpack. My understanding from reading the interrupts wiki page a few times is that you could use this to aggressively interrupt the encoder so that your code could take priority. While I'm sure that there are use cases for this, it seems like an extremely edge case for folks who just want to rotate and click a wheel.

Hot take: if these two calls were superfluous and just a little confusing, I would shrug and get on with my day. However, they appear to be actively harmful for the default use case of the encoder backpack, given that they will cause conflicts with other I2C devices on the chain and [in my experience] dramatically increase the possibility that a rotation step will not be detected. Since I removed both statements from my sketch, the performance and stability of my project have conservatively 10x'd.

It is will all of this in mind that I strongly urge you to remove the offending calls from the getting started example on the Learn page for the encoder backpack. At least consider commenting them out?

They have no impact on the example provided, and without any explanation, they make the DevEx actively worse.

Locked
Please be positive and constructive with your questions and comments.

Return to “Microcontrollers”