Enable interrupts to use Wire in ISR

I'm driving an SSD1306 display over I²C using the Adafruit_SSD1306 library. Most of time in my loop is spent transmitting the frame buffer to the display. (Source code that transmits I²C)

I would like to add eight rotary encoders using an MCP23017.
I believe using external interrupts is the only option here, because my loop is way too slow to poll.
The MCP23017 has interrupt outputs to indicate that the input registers have changed, so far so good.

However, the problem is that I cannot use I²C in the ISR. This is because the Wire library relies on interrupts to work correctly:

  • TwoWire::beginTransmission just resets the buffer indices (source)
  • TwoWire::write writes data to the buffer and increments the index (source)
  • TwoWire::endTransmission calls twi_writeTo (source)
  • twi_writeTo waits for any previous I²C transmissions to finish, configures the TWI FSM, configures the TWI interrupts, and finally waits until the transmission is complete (source)
  • ISR(TWI_vect) is where the actual transmission of the data happens, asynchronously (source)

Would it be possible to do the following:

  • OLED library is transmitting I²C
  • MCP23017 interrupt fires → inside of ISR
  • Enable interrupts
  • Wait for OLED I²C transmission to finish
  • Start I²C transmission to read MCP23017 input registers
  • Wait for MCP23017 I²C transmission to finish
  • Disable interrupts
  • Calculate encoder deltas and update positions
  • Exit ISR

I've done some research online, and all of the posts seem to end with: "Don't do the I²C transmission in the ISR, just set a flag and handle it in the main loop", which is of course a valid point, but that's just not possible in this case, because the OLED I²C transmissions take up so much time in the loop.

Do you think trying to get this working is worth it (is it even possible at all?), or am I just wasting my time?
In case of the latter, what solution would you propose to be able to read many rotary encoders?

Thanks,
Pieter

Can't say if it'll be of any use to you but, there's this. He addresses using interrupts with the MCP23017.

Don't do the I²C transmission in the ISR

Define "because my loop is way too slow to poll."?

dougp:
Can't say if it'll be of any use to you but, there's this. He addresses using interrupts with the MCP23017.

Thanks for the link! Seems very useful, I'll try it in the morning.

Paul__B:
Define "because my loop is way too slow to poll."?

Loop duration is over 20 ms. I have to send 1 KiB of data over a 400 kHz I²C bus.

PieterP:
Would it be possible to do the following:

No, that is not a reliable situation.

What is wrong with the code in your loop(). Are you reading data from a microSD card ?

It is possible to get far away from interrupt problems:
(1) Use a software I2C library for the display.
(2) Use the u8g2 library and use its software I2C code optimized for displays.

Then you have to use those software I2C calls in the loop(). They still do not belong in a ISR, because the I2C bus is slow.
The only advantage is that they do not disturb any interrupts.

The Arduino Wire library uses interrupts, but the Wire.endTransmission() and Wire.requestFrom() wait until the I2C transaction has finished. A software I2C library can be just as fast and have the same "CPU load".

Do you have a logic analyzer ? A good one is 20 dollars (LHT00SU1 + sigrok + PulseView). Then you see the overhead of the Wire library at 400kHz.

Which Arduino board do you use ? The u8g2 requires lots of memory and so does the Adafruit library.

There is a issue with the SoftwareWire library and the Adafruit library. I'm still hoping for help to fix it.

Koepel:
What is wrong with the code in your loop(). Are you reading data from a microSD card ?

I'm updating an OLED display.

Updating the display takes 20 ms. If an interrupt for the rotary encoders fires, I have to handle it as soon as possible, I cannot wait 20 ms, or I'll lose ticks.

The I²C transmission for the display transmits 32 bytes at a time, so I'm looking for a way to add my MCP23017 in between the OLED transmissions, without editing the OLED library.

I'm using an Arduino UNO right now, but it would be nice if I could eventually add support for other boards.

Sorry, I misunderstood the problem.

Using 8 rotary encoders behind a I2C I/O chip is weird, even without the display. The I2C bus is slow.
They should be connected directly to pins and trigger interrupts. The Arduino Uno has PCINT interrupts on every pin.

Do you need two pins per encoder ? So you need 16 pins ? The Arduino Uno does not have that many pins when the I2C pins are in use.

Can you use a SPI I/O chip with a interrupt ? Then you can do some code in the ISR itself. Then you can even try a software I2C library for the display to further improve the interrupt response time.

Using the I2C for the display is not ideal as well.
Can the display be set in SPI mode ?

The link provided by @dougp creates a whole world of problems in my opinion, because the I2C bus could be in use for the display.

When using the MCP23017, you have to change the Adafruit SSD1306 library to check a flag that is set by the MCP23017 in a ISR.
That is not nice.

In my opinion, the best solution is a SPI chip for the I/O to the encoders.

Other solutions are a Arduino Mega 2560 to be able to connect the encoders directly to its pins. Perhaps the Arduino Leonardo can do that as well. Or a seperate Arduino board that replaces the MCP23017. Maybe an intelligent display with serial interface will make it easier.

So what you are saying is that you are using an I2C based display and an I2C based interface for the rotary encoders - which I presume are simply manual controls.

It looks like you will need to use a "smart" software I2C library for the display on a separate bus which using a state machine, allows you to fully interleave polls for the encoders.

Koepel:
They should be connected directly to pins and trigger interrupts. The Arduino Uno has PCINT interrupts on every pin.

I'm aware of that, but I don't have enough free pins, so I started thinking about using the MCP23017.

Koepel:
Can you use a SPI I/O chip with a interrupt ? Then you can do some code in the ISR itself.

Using the I2C for the display is not ideal as well.
Can the display be set in SPI mode ?

Wouldn't that create the same problem? If both the main loop and the ISR use SPI, how can I finish the display transaction before starting my IO transaction? How do I know what CS pin to deassert after finishing the display transaction?

Koepel:
In my opinion, the best solution is a SPI chip for the I/O to the encoders.

Other solutions are a Arduino Mega 2560 to be able to connect the encoders directly to its pins. Perhaps the Arduino Leonardo can do that as well. Or a seperate Arduino board that replaces the MCP23017. Maybe an intelligent display with serial interface will make it easier.

I guess you're right :slight_smile:

The problem is that I would like to keep things "modular". Once I get this simple example working, I'd like to add it to my Control Surface library. One of the requests I get most often is "I want to use rotary encoders with IO expanders/multiplexers".
The library allows you to use SPI or I²C displays, so for the rotary encoder solution, I'd like to make no assumptions about how these interfaces are used.
Any thoughts on that?

It might be easier to just use an extra ATmega328P that uses all of its PCINTs to read 8 encoders, and communicates with the main MCU over I²C or SPI.
Do you happen to know if I²C or SPI transfers trigger the PCINT for the port of the data/clock lines?

Paul__B:
So what you are saying is that you are using an I2C based display and an I2C based interface for the rotary encoders - which I presume are simply manual controls.

It looks like you will need to use a "smart" software I2C library for the display on a separate bus which using a state machine, allows you to fully interleave polls for the encoders.

That could work, but I don't know yet if I'm prepared to put so much effort in it :slight_smile: I don't want to re-implement the entire Wire library, and I'd prefer a solution that doesn't require editing the Adafruit_SSD1306 library.
I know, I'm asking too much :wink: