Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interrupt issue in MicroPython when using multiple i2cencoders #22

Open
cameronprince opened this issue Jan 24, 2025 · 5 comments
Open

Comments

@cameronprince
Copy link

Hello,

I'd like to use the i2cencoders for a project that is coded in MicroPython. I wasn't able to find an existing library, so I ported the Python library to MicroPython here: https://github.com/cameronprince/i2cEncoderLibV2

Everything has been working well with a single i2cencoder, but once I added additional i2cencoders to the bus, the interrupt pin sticks low on the first interaction with any i2cencoder. This causes the interrupt handler to fire continuously until the ESP-WROOM-32 grinds to a halt.

My initial order from you was for five of the i2cencoders. I soldered four together using the castellations and have one stand alone. The stand-alone i2cencoder has the same address (0x50) as the first i2cencoder in the group of four. Both of these have the i2c bus pull-up pads soldered.

When testing the four i2cencoders together, I initially had a single set of callbacks that were being shared with all four i2cencoders. I thought this might have something to do with the problem, so I switched to separate callbacks. This made no difference. I also tried initializing each of the four individually, but this also made no difference. Additional delays were tried in various places, as well. All four have the same problem, but the stand-alone continues to work fine by itself.

Here is my test code:

import time
import struct
from machine import Pin, I2C
import i2cEncoderLibV2

I2C_BUS = 1
I2C_SCL_PIN = 33
I2C_SDA_PIN = 32
INTERRUPT_PIN = 34
# ENCODER_ADDRESSES = [0x50]
ENCODER_ADDRESSES = [0x50, 0x30, 0x60, 0x44]

# Setup the Interrupt Pin from the encoders.
INT_pin = Pin(INTERRUPT_PIN, Pin.IN)

# Initialize the I2C bus.
i2c = I2C(I2C_BUS, scl=Pin(I2C_SCL_PIN), sda=Pin(I2C_SDA_PIN))

# Initialize encoders
encoder1 = i2cEncoderLibV2.i2cEncoderLibV2(i2c, ENCODER_ADDRESSES[0])
encoder2 = i2cEncoderLibV2.i2cEncoderLibV2(i2c, ENCODER_ADDRESSES[1])
encoder3 = i2cEncoderLibV2.i2cEncoderLibV2(i2c, ENCODER_ADDRESSES[2])
encoder4 = i2cEncoderLibV2.i2cEncoderLibV2(i2c, ENCODER_ADDRESSES[3])

# Callback functions for Encoder 1
def EncoderChange1():
    encoder1.writeLEDG(100)
    valBytes = struct.unpack('>i', encoder1.readCounter32())
    print('Encoder 1 Changed: %d' % valBytes[0])
    encoder1.writeLEDG(0)

def EncoderPush1():
    encoder1.writeLEDB(100)
    print('Encoder 1 Pushed!')
    encoder1.writeLEDB(0)

# Callback functions for Encoder 2
def EncoderChange2():
    encoder2.writeLEDG(100)
    valBytes = struct.unpack('>i', encoder2.readCounter32())
    print('Encoder 2 Changed: %d' % valBytes[0])
    encoder2.writeLEDG(0)

def EncoderPush2():
    encoder2.writeLEDB(100)
    print('Encoder 2 Pushed!')
    encoder2.writeLEDB(0)

# Callback functions for Encoder 3
def EncoderChange3():
    encoder3.writeLEDG(100)
    valBytes = struct.unpack('>i', encoder3.readCounter32())
    print('Encoder 3 Changed: %d' % valBytes[0])
    encoder3.writeLEDG(0)

def EncoderPush3():
    encoder3.writeLEDB(100)
    print('Encoder 3 Pushed!')
    encoder3.writeLEDB(0)

# Callback functions for Encoder 4
def EncoderChange4():
    encoder4.writeLEDG(100)
    valBytes = struct.unpack('>i', encoder4.readCounter32())
    print('Encoder 4 Changed: %d' % valBytes[0])
    encoder4.writeLEDG(0)

def EncoderPush4():
    encoder4.writeLEDB(100)
    print('Encoder 4 Pushed!')
    encoder4.writeLEDB(0)

# Interrupt handler
def Encoder_INT(pin):
    print('Encoder_INT')
    for i, encoder in enumerate(encoders):
        status = encoder.readEncoder8(i2cEncoderLibV2.REG_ESTATUS)
        print(f'Encoder {i+1} Status 1: {status}')
        encoder.updateStatus()
        status = encoder.readEncoder8(i2cEncoderLibV2.REG_ESTATUS)
        print(f'Encoder {i+1} Status 2: {status}')
        print(" ")
        time.sleep(0.1)

# Initialize each encoder
def init_encoder(encoder, change_callback, push_callback):
    encoder.reset()
    print(f"Encoder reset")
    time.sleep(0.1)

    encconfig = (i2cEncoderLibV2.INT_DATA | i2cEncoderLibV2.WRAP_ENABLE
                 | i2cEncoderLibV2.DIRE_RIGHT | i2cEncoderLibV2.IPUP_ENABLE
                 | i2cEncoderLibV2.RMOD_X1 | i2cEncoderLibV2.RGB_ENCODER)
    encoder.begin(encconfig)
    print(f"Encoder begin with config: {encconfig}")

    reg = (i2cEncoderLibV2.PUSHP | i2cEncoderLibV2.RINC | i2cEncoderLibV2.RDEC)
    encoder.writeEncoder8(i2cEncoderLibV2.REG_INTCONF, reg)
    print(f"Encoder begin with intconfig: {reg}")

    encoder.writeCounter(0)
    encoder.writeMax(35)
    encoder.writeMin(-20)
    encoder.writeStep(1)
    encoder.writeAntibouncingPeriod(12)

    encoder.onChange = change_callback
    encoder.onButtonPush = push_callback

    print(f'Board ID code: 0x{encoder.readIDCode():X}')
    print(f'Board Version: 0x{encoder.readVersion():X}')

    encoder.writeRGBCode(0x640000)

# Initialize each encoder with their respective callback functions
init_encoder(encoder1, EncoderChange1, EncoderPush1)
init_encoder(encoder2, EncoderChange2, EncoderPush2)
init_encoder(encoder3, EncoderChange3, EncoderPush3)
init_encoder(encoder4, EncoderChange4, EncoderPush4)

# Add the encoders to the list for interrupt handling
encoders = [encoder1, encoder2, encoder3, encoder4]
# encoders = [encoder1]

# Setup the interrupt handler
INT_pin.irq(trigger=Pin.IRQ_FALLING, handler=Encoder_INT)

With the four i2cencoders, this is the output in which I rotated the first i2cencoder two detents:

MPY: soft reboot
Encoder reset
Encoder begin with config: 34
Encoder begin with intconfig: 26
Board ID code: 0x53
Board Version: 0x23
Encoder reset
Encoder begin with config: 34
Encoder begin with intconfig: 26
Board ID code: 0x53
Board Version: 0x23
Encoder reset
Encoder begin with config: 34
Encoder begin with intconfig: 26
Board ID code: 0x53
Board Version: 0x23
Encoder reset
Encoder begin with config: 34
Encoder begin with intconfig: 26
Board ID code: 0x53
Board Version: 0x23
>>> Encoder_INT
Encoder 1 Status 1: 8
Encoder 1 Status 2: 0
 
Encoder 2 Status 1: 0
Encoder 2 Status 2: 0
 
Encoder 3 Status 1: 0
Encoder 3 Status 2: 0
 
Encoder 4 Status 1: 0
Encoder 4 Status 2: 0
 
Encoder_INT
Encoder 1 Status 1: 8
Encoder 1 Status 2: 0
 
Encoder 2 Status 1: 0
Encoder 2 Status 2: 0
 
Encoder 3 Status 1: 0
Encoder 3 Status 2: 0
 
Encoder 4 Status 1: 0
Encoder 4 Status 2: 0
 
Encoder_INT
Encoder 1 Status 1: 0
Encoder 1 Status 2: 0
 
Encoder 2 Status 1: 0
Encoder 2 Status 2: 0
 
Encoder 3 Status 1: 0
Encoder 3 Status 2: 0
 
Encoder 4 Status 1: 0
Encoder 4 Status 2: 0
 
Encoder_INT
Encoder 1 Status 1: 0
Encoder 1 Status 2: 0
 
Encoder 2 Status 1: 0
Encoder 2 Status 2: 0
 
Encoder 3 Status 1: 0
Encoder 3 Status 2: 0
 
Encoder 4 Status 1: 0
Encoder 4 Status 2: 0
 
Encoder_INT
Encoder 1 Status 1: 0
Encoder 1 Status 2: 0
 
Encoder 2 Status 1: 0
Encoder 2 Status 2: 0
 
Encoder 3 Status 1: 0
Encoder 3 Status 2: 0
 
Encoder 4 Status 1: 0
Encoder 4 Status 2: 0

The Encoder_INT method fires continuously until I reset the microcontroller.

This is the output when testing with the stand-along i2cencoder:

MPY: soft reboot
Encoder reset
Encoder begin with config: 34
Encoder begin with intconfig: 26
Board ID code: 0x53
Board Version: 0x23
>>> Encoder_INT
Encoder 1 Status 1: 8
Encoder 1 Status 2: 0
 
Encoder_INT
Encoder 1 Status 1: 8
Encoder 1 Status 2: 0
 
Encoder_INT
Encoder 1 Status 1: 2
Encoder 1 Status 2: 0

This output stops after each interaction.

Would you have any idea as to what might be happening?

Thank you,
Cameron

@cameronprince cameronprince changed the title Interrupt issue in MicroPython when using multiple encoders Interrupt issue in MicroPython when using multiple i2cencoders Jan 24, 2025
@cameronprince
Copy link
Author

cameronprince commented Jan 24, 2025

This solution is a bit of a hack, but I've discovered if I check the pin status in the interrupt callback, I can determine whether or not to call updateStatus() and keep from crashing the microcontroller:

def Encoder_INT(pin):
    if pin.value() == 0:
        print('Encoder_INT')

        # Read the status of the interrupt pin
        pin_status = INT_pin.value()
        print(f'Interrupt Pin Status: {pin_status}')

        for i, encoder in enumerate(encoders):
            status = encoder.readEncoder8(i2cEncoderLibV2.REG_ESTATUS)
            print(f'Encoder {i+1} Status 1: {status}')
            encoder.updateStatus()
            status = encoder.readEncoder8(i2cEncoderLibV2.REG_ESTATUS)
            print(f'Encoder {i+1} Status 2: {status}')
            print(" ")

Do you have any ideas as to why the interrupt callback is firing after the pin returns high, and why this would only happen with multiple i2cencoders?

@cameronprince
Copy link
Author

This still seems like a hack, but I've discovered that if I reinit the interrupt pin inside the Encoder_INT function, the continuous calls to the function cease. I'm now getting solid, consistent callback triggering, like with the stand-alone i2cencoder.

import time
import struct
from machine import Pin, I2C
import i2cEncoderLibV2

I2C_BUS = 1
I2C_SCL_PIN = 33
I2C_SDA_PIN = 32
INTERRUPT_PIN = 34
ENCODER_ADDRESSES = [0x50, 0x30, 0x60, 0x44]

# Initialize the interrupt pin.
INT_pin = Pin(INTERRUPT_PIN, Pin.IN, Pin.PULL_UP)

# Initialize the I2C bus.
i2c = I2C(I2C_BUS, scl=Pin(I2C_SCL_PIN), sda=Pin(I2C_SDA_PIN))

# Create the encoders array.
encoders = [
    i2cEncoderLibV2.i2cEncoderLibV2(i2c, ENCODER_ADDRESSES[0]),
    i2cEncoderLibV2.i2cEncoderLibV2(i2c, ENCODER_ADDRESSES[1]),
    i2cEncoderLibV2.i2cEncoderLibV2(i2c, ENCODER_ADDRESSES[2]),
    i2cEncoderLibV2.i2cEncoderLibV2(i2c, ENCODER_ADDRESSES[3]),
]

# Unified callback functions.
def EncoderChange(encoder):
    encoder.writeLEDR(100)
    valBytes = struct.unpack('>i', encoder.readCounter32())
    print(f'Changed: {valBytes[0]} on Encoder with address: {hex(encoder.readI2CAdd())}')
    encoder.writeLEDR(0)

def EncoderPush(encoder):
    encoder.writeLEDR(100)
    print(f'Encoder Pushed on Encoder with address: {hex(encoder.readI2CAdd())}')
    encoder.writeLEDR(0)

# Interrupt handler.
def Encoder_INT(pin):
    if pin.value() == 0:
        # Disable the interrupt.
        INT_pin.irq(handler=None)
        # Loop over all encoders to find the triggering instance.
        for encoder in encoders:
            # Read and reset the status.
            status = encoder.readEncoder8(i2cEncoderLibV2.REG_ESTATUS)
            # Fire the appropriate callback.
            if status & (i2cEncoderLibV2.RINC | i2cEncoderLibV2.RDEC):
                EncoderChange(encoder)
            if status & i2cEncoderLibV2.PUSHP:
                EncoderPush(encoder)
        # Re-enable the interrupt.
        INT_pin.irq(trigger=Pin.IRQ_FALLING, handler=Encoder_INT)

# Initialize a specific encoder.
def init_encoder(encoder):
    encoder.reset()
    print(f"Encoder reset")
    time.sleep(0.1)

    encconfig = (i2cEncoderLibV2.INT_DATA | i2cEncoderLibV2.WRAP_ENABLE
                 | i2cEncoderLibV2.DIRE_RIGHT | i2cEncoderLibV2.IPUP_ENABLE
                 | i2cEncoderLibV2.RMOD_X1 | i2cEncoderLibV2.RGB_ENCODER)
    encoder.begin(encconfig)
    print(f"Encoder begin with config: {encconfig}")

    reg = (i2cEncoderLibV2.PUSHP | i2cEncoderLibV2.RINC | i2cEncoderLibV2.RDEC)
    encoder.writeEncoder8(i2cEncoderLibV2.REG_INTCONF, reg)
    print(f"Encoder begin with intconfig: {reg}")

    encoder.writeCounter(0)
    encoder.writeMax(35)
    encoder.writeMin(-20)
    encoder.writeStep(1)
    encoder.writeAntibouncingPeriod(12)

    print(f'Board ID code: 0x{encoder.readIDCode():X}')
    print(f'Board Version: 0x{encoder.readVersion():X}')

# Initialize each encoder.
for encoder in encoders:
    init_encoder(encoder)

# Setup the interrupt handler
INT_pin.irq(trigger=Pin.IRQ_FALLING, handler=Encoder_INT)

I did modify the i2cEncoderLibV2 library slightly to include:

# read the i2cadd #
    def readI2CAdd(self):
        return self.i2cadd

This allows the callbacks to know which i2cencoder triggered it.

I would like to have your thoughts on this approach.

Thank you,
Cameron

@Fattoresaimon
Copy link
Owner

Hello,
I never used micropython and i'm not expert of python, so i can't help you on that side. I'm sorry.
But i remember a similar issue on the Arduino uno.
The Arduno uno can mange only 1 interrupt at time, so inside on the GPIO interrupt the I2C bus doesn't work. Because also the I2C use the interrupt.
In that case the program was blocking inside the GPIO interrupt and not I2C transition.

@cameronprince
Copy link
Author

Thanks for the reply... In testing this work around mentioned above, eventually, after some number of interactions with the i2cencoder, the interrupt stops firing. So it seems I have three options:

  1. Remove the code which clears and re-adds the interrupt handler and just allow the function to be continuously called. The pin value check seems reliable to know when a true interrupt is received.
  2. Switch to polling with asyncio.
  3. Separate the i2cencoders and dedicate individual GPIO pins for each interrupt.

I'll let you know how it goes.

Cameron

@cameronprince
Copy link
Author

@Fattoresaimon,

A follow-up here... I've been pretty successful using four I2CEncoders with my project. I did settle with a separate GPIO pin for each encoder's interrupt. I was never able to figure out why the interrupt callback kept firing when trying to poll the encoders to see which fired when sharing interrupt pins.

What I'm seeing now that maybe I should create a new issue about is that during operation, several times, I've seen an encoder become unresponsive. Sometimes, the other encoders continue to work. Other times, the whole program crashes. I attribute this to I2C bus overruns. I'm currently running with encoder.writeAntibouncingPeriod(40) for each encoder. Is this the only deterrent to slow down I2C comms?

In my project, during MIDI playback, which is in it's own thread, I have four encoders controlling four output levels in the main thread. The code is set up so that the encoders scale the duty cycle of each output and the RGB LED in each encoder shows a color indicative of the output level (green, to yellow, to red). I also have an elapsed time display running. While the MIDI file is playing and adjusting the encoders, sometimes I see this problem. In these cases, the encoder adjustment is triggering back-to-back calls to readEncoder8() and writeRGBCode(), one triggering the other.

Is the solution here to queue the comms? I would certainly rather have a slight delay in the RGB color changing than the program be interrupted.

Thank you,
Cameron

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants