Reading encoder with interrupts

General discussions and questions abound development of code with MicroPython that is not hardware specific.
Target audience: MicroPython Users.
Post Reply
Leonti
Posts: 14
Joined: Sun Mar 28, 2021 12:29 pm

Reading encoder with interrupts

Post by Leonti » Thu Sep 22, 2022 10:30 am

I have a motor with a hall sensor encoder (JGA25-370).

I'm building automatic roller blinds and using encoder values to determine position of the blind.
However, during my testing, I noticed that there is an encoder drift.
For example, when I move the motor to position 0 then to position 100 and back a couple of times, positions shift higher off the ground.

Here is my interrupt handler:

Code: Select all

class Motor:
    
    def __init__(self, pins):
        self._enc2 = Pin(pins['enc2'], Pin.IN)
        self._enc1 = Pin(pins['enc1'], Pin.IN)
        self._enc1.irq(trigger = Pin.IRQ_FALLING, handler=self.handle_enc1)
        
    def handle_enc1(self, pin):   
        if self._enc2.value() == 1:
            self._pos += 1
        else:
            self._pos -= 1        
I also have a bunch of print statements in the application (there is a message every 40ms about the current speed settings from PID controller).

I guess I have 3 questions:
1. Does this interrupt handler look good for handling encoder values? For example, would it be better to have a second handler for `enc2` pin instead of reading it's value inside of the `enc1` pin?
2. Would print statements affect values coming from interrupt handler? My understanding is that interrupt will fire no matter what, but maybe when I have print statements and serial communication (mpremote) it affects the performance of interrupts?
3. I'm using raspberry pi pico which has 2 cores. Is it possible to offload interrupt handling to a second core, so I don't have to worry about it clashing with the rest of the code?

User avatar
Roberthh
Posts: 3656
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: Reading encoder with interrupts

Post by Roberthh » Thu Sep 22, 2022 12:23 pm

You did not mention the pulse rate of the encoder. So it's hard to tell whether interrupt is lagging. But in any case a IRQ should terminate as fast as possible. You said the you have a hall sensor encoder. That one should create clean pulses without bouncing. But if the pulse rate is faster than the time required to finish the IRQ, you will miss counts.
It's interesting that you seem to miss only counts in one direction. Could it be that the output signal of the sensor has different rise and fall times?
What you could to is to verify in the IRQ that the value of enc1 = 0 after waiting a short time, and discard the enc2 result if enc1 is not 0. That could eliminate false triggers,

Leonti
Posts: 14
Joined: Sun Mar 28, 2021 12:29 pm

Re: Reading encoder with interrupts

Post by Leonti » Fri Sep 23, 2022 12:52 am

The pulse rate is about 2 pulses every millisecond, so I'm guessing it's more than enough time to process it before the next pulse comes.
The program has a lot of print statements, so I was thinking that maybe interrupts are not processed when serial is being used.

Yeah, it's interesting that it's drifting in a single direction. One explanation I had is that because it has weight attached to it it goes down faster than it goes up. But that would mean that if its missing pulses it would actually stop lower to the ground, not higher (because it would need to travel more to count enough pulses).
I will look into false triggers, thanks for the suggestion!

Leonti
Posts: 14
Joined: Sun Mar 28, 2021 12:29 pm

Re: Reading encoder with interrupts

Post by Leonti » Fri Sep 23, 2022 4:05 am

After further investigation I can confirm that it's most likely not the CPU.
2 things I noticed:
1. If I change trigger to "Pin.IRQ_RISING" it makes readings more stable overall. They are still not as accurate as I want them to be, but at least they don't skew in a single direction as much, here are the results of testing:
Image
But they do still skew, just not as much and in the opposite direction.
2. More importantly, when I modify the code to be like this:

Code: Select all

    def handle_enc1(self, pin):   
        if self._enc2.value() == 1:
            if not self._going_down:
                self._direction_changes += 1
            self._going_down = True    
            self._pos -= 1
        else:
            if self._going_down:
                self._direction_changes += 1
            self._going_down = False
            self._pos += 1
I can now print the value of 'self._direction_changes' every second to see if there are some unexpected values.
During my tests I wind the motor up by hand and then let it go (it has a wheel with the weight attached to simulate a window roller blind) so it rotates by itself because of the weight.
I can see that the value of 'self._direction_changes' is increasing. I can kinda understand it happening when I move the motor by hand (maybe after each rotation it jitters a bit because I have to change the position of my hand).
But it also increasing when the motor is going 100% in a single direction, which shouldn't be the case.
So it seems like I have some false triggers happening.

Leonti
Posts: 14
Joined: Sun Mar 28, 2021 12:29 pm

Re: Reading encoder with interrupts

Post by Leonti » Fri Sep 23, 2022 5:04 am

I think I figured it out.
I've updated my code to be this:

Code: Select all

    def handle_enc1(self, pin):
        if pin.value() != 1:
            self._false_triggers += 1

        if self._enc2.value() == 1:
            if not self._going_down:
                self._direction_changes += 1
            self._going_down = True    
            self._pos -=  1
        else:
            if self._going_down:
                self._direction_changes += 1
            self._going_down = False
            self._pos += 1
And strangely enough I do have some false triggers when the value of 'enc1' is no longer '1' when the interrupt runs.

After googling I found out that interrupts can interfere with other interrupts.
I also have an NRF24L01 running and I'm also updating a value on filesystem every second.
As soon as I disable communication with NRF24L01 (it uses SPI), I no longer see a situation when the value of the interrupt pin is not 1 after `RISING` interrupt.
Direction was still changing for no reason, but after disabling file write it has gone too. Now when the motor is just going down direction doesn't change, meaning that I'm not losing/adding values.

Not sure how to fix it. File write is simple, I can just not do it when the motor is moving, but I still need NRF24L01 running full-time.
I wonder if I can somehow use second core just for encoder interrupts.

User avatar
jimmo
Posts: 2746
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: Reading encoder with interrupts

Post by jimmo » Fri Sep 23, 2022 5:31 am

Leonti wrote:
Fri Sep 23, 2022 5:04 am
I wonder if I can somehow use second core just for encoder interrupts.
I'm assuming this is rp2040/pico? This is not currently supported but something we do need to fix. See https://github.com/micropython/micropython/issues/9124

I was originally wondering if it was stm32 in which case you might want to consider hard IRQs (but dual core must mean you're esp32 or rp2).

Leonti
Posts: 14
Joined: Sun Mar 28, 2021 12:29 pm

Re: Reading encoder with interrupts

Post by Leonti » Fri Sep 23, 2022 7:50 am

What is a hard interrupt?
Does it mean it's a higher priority than the other ones?

I have solved my issue by not constantly reading from SPI.
I'm using [url=https://github.com/nRF24/CircuitPython_nRF24L01[]this NRF24L01/url] ported to Micropython.
It allows you to set up an "IRQ" pin, which will become low when new data is available.

So previously I had something like this:

Code: Select all

def run():
  while True:
      if nrf.available(): # this writes/reads to SPI
Now I connected the pin and configured it with:

Code: Select all

nrf.interrupt_config(data_sent=False, data_fail=False) # only interested in incoming data
which allows me to write this:

Code: Select all

def run():
  while True:
    if nrf_irc_pin.value() == 0:
      if nrf.available():
This means, that SPI writes/reads ('nrf.available()' talks to SPI) only happen when data is available and not constantly.

After this change encoder values are correct. I'm actually surprised how accurate it is now, I tested it multiple times and the results are very consistent:

Code: Select all

# start value - end value
477 - 1790
477 - 1790
477 - 1790
477 - 1790
476 - 1789
I will need to test it with a different motor with different gear ratio, which should give me 4 pulses per millisecond, but I can't imagine it would be a problem.

User avatar
Roberthh
Posts: 3656
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: Reading encoder with interrupts

Post by Roberthh » Fri Sep 23, 2022 8:46 am

What is a hard interrupt?
Does it mean it's a higher priority than the other ones?
With an pin IRQ defined with the option hard=True the callback runs in interrupt context, in contrast to the 'normal' pin irq, where the callback runs as regular scheduled task. The callback in interrupt context can only be interrupted by a higher priority interrupt (like timer tick), but not by other Python tasks. And with hard=True, the callback interrupts code in non-interrupt mode. So it may interrupt the SPI transfer.
The drawback is, that 'hard' callbacks must not allocate memory. But in your case that's easy, since you use either only small integers, and/or the objects are pre-allocated.
Edit: A 'hard' IRQ also has a shorter latency between event and the start of the callback. But that's in the µs range.

Post Reply