micropython-async icon indicating copy to clipboard operation
micropython-async copied to clipboard

Rotary encoder misses click when changing directions

Open ilium007 opened this issue 3 years ago • 41 comments

At random times (the latest being when the encoders where sitting for a while) the code will not register a change in direction change.

I am using a hardware debounce circuit that uses a simple RC circuit and inverted Schmitt trigger outputs:

>>>
MPY: sync filesystems
MPY: soft reboot
result = 1   <-- one click clockwise
result = 2   <-- one click clockwise
result = 3   <-- one click clockwise
result = 4   <-- one click clockwise
result = 5   <-- one click clockwise
                   <-- one click anti-clockwise, **does nothing**
result = 4   <-- one click anti-clockwise
result = 3   <-- one click anti-clockwise
result = 2   <-- one click anti-clockwise
result = 1   <-- one click anti-clockwise
result = 0  <-- one click anti-clockwise

I have had a similar issue using the Mike Teachman class (https://github.com/miketeachman/micropython-rotary) but was able to fix it with an additional state in the state table (I know this class does not use the state table approach). On this other library, with the state table modified, it worked flawlessly. I raised an issue and documented over in that repo: https://github.com/miketeachman/micropython-rotary/issues/10

ilium007 avatar Apr 09 '22 08:04 ilium007

This behaviour does not happen all the time, only sometimes. When I had the issue with the state table class version it was consistent, the behaviour is different.

ilium007 avatar Apr 09 '22 08:04 ilium007

The time constant of 10nF and 10KΩ is 100μs. Depending on the threshold levels of the Schmitt triggers, pulses rather shorter than that could get through. You haven't mentioned what platform you're using or whether you're using hard or soft IRQ's but it's possible that pulses shorter than the interrupt latency are getting through. Bear in mind that latency with soft IRQ's is measured in ms (hundreds of ms with SPIRAM).

peterhinch avatar Apr 09 '22 10:04 peterhinch

Thanks for replying. I’m using an STM32F405 Adafruit Feather STM32F405 Express. I’ll have to work out how I check for hard / sort IRQ’s, I saw that check in the library.

ilium007 avatar Apr 09 '22 10:04 ilium007

Just checked, the board is using hard IRQ's

ilium007 avatar Apr 09 '22 11:04 ilium007

If it were a timing issue surely I would see the glitch for one reverse direction attempt but when this decides its going to change behaviour it remains that way until I do a reset (soft reset from REPL will fix it). ie. I turn one click clockwise and it increments, turn one click back it does not register, one click back again it decrements. Then the same in the other direction, one click clockwise - nothing, one more click it increments. Now I continue truing clockwise, I get an instant increment but when I turn back once click I get nothing. This continues all the way around the encoder. When I do the reset I can click back and forth on the same two detents that were erroring before the reset. So I don't think its timing or encoder related, something else is causing it.

ilium007 avatar Apr 09 '22 11:04 ilium007

I captured some images from my cheap (don't laugh) oscilloscope directly from the rotary encoder schmitt trigger outputs.

Working as it should:

clockwise turn image

immediate anticlockwise turn image

Then when it stopped working:

clockwise turn image

immediate anticlockwise turn image

I have noticed also that when it starts happening it affects both encoders exactly the same way. Once reset they both start working again.

This is the rough code used to read the encoder and produce the CANbus message: https://pastebin.com/CFgWDxKa

This is the code that runs on the other end receiving the CANbus message: https://pastebin.com/qTAMUH9x

ilium007 avatar Apr 09 '22 12:04 ilium007

Came back to this after it running overnight, fwd/rev clicks as expected. 15min later it reverts to the behaviour described above. Within minutes, no hardware change or REPL reset it is working fwd/rev as expected. I can't help think that this is somehow timer related.

ilium007 avatar Apr 10 '22 00:04 ilium007

The waveforms look OK - 4ms between edges should be no problem with hard IRQ's.

The callback runs synchronous code. How long does that block for?

Another thought is that you are using div=4. This means that you will miss switch positions: the idea of div > 1 is that resolution is discarded. I found this useful in micro-gui when using an encoder to adjust analog values. By default the Adafruit encoder was too sensitive and being able to dial it down helped. However this can introduce hysteresis when changing direction. With a value of 4 you might move forward 3 edges from the last callback: no callback will occur. If you then move backwards, you'll need to move through 3 edges before the callback will run. To ensure this isn't causing confusion you might try the default of 1.

peterhinch avatar Apr 10 '22 08:04 peterhinch

The callback sets the threadsafeflag and the synchronous method runs to process the canbus message. When I put a print(time.ticks_us()) at the start and end of that function it takes between 937 and 1087 us.

I changed the div back to 4 and the encoder steps up and down in counts of 4. I then divide by 4 in the code that sends the canbus message but now I get duplicate counts when turning the encoder.

Output below when turning encoder slowly:

input: encoder  msg_id: 0x1EB00103      nid: 0x75       cid: 0x103      bus_state: 0x1  input_id: 0x0   sign: 0x0       count: 13
input: encoder  msg_id: 0x1EB00103      nid: 0x75       cid: 0x103      bus_state: 0x1  input_id: 0x0   sign: 0x0       count: 13
input: encoder  msg_id: 0x1EB00103      nid: 0x75       cid: 0x103      bus_state: 0x1  input_id: 0x0   sign: 0x0       count: 14
input: encoder  msg_id: 0x1EB00103      nid: 0x75       cid: 0x103      bus_state: 0x1  input_id: 0x0   sign: 0x0       count: 15
input: encoder  msg_id: 0x1EB00103      nid: 0x75       cid: 0x103      bus_state: 0x1  input_id: 0x0   sign: 0x0       count: 16
input: encoder  msg_id: 0x1EB00103      nid: 0x75       cid: 0x103      bus_state: 0x1  input_id: 0x0   sign: 0x0       count: 17
input: encoder  msg_id: 0x1EB00103      nid: 0x75       cid: 0x103      bus_state: 0x1  input_id: 0x0   sign: 0x0       count: 17
input: encoder  msg_id: 0x1EB00103      nid: 0x75       cid: 0x103      bus_state: 0x1  input_id: 0x0   sign: 0x0       count: 18
input: encoder  msg_id: 0x1EB00103      nid: 0x75       cid: 0x103      bus_state: 0x1  input_id: 0x0   sign: 0x0       count: 19
input: encoder  msg_id: 0x1EB00103      nid: 0x75       cid: 0x103      bus_state: 0x1  input_id: 0x0   sign: 0x0       count: 20
input: encoder  msg_id: 0x1EB00103      nid: 0x75       cid: 0x103      bus_state: 0x1  input_id: 0x0   sign: 0x0       count: 21
input: encoder  msg_id: 0x1EB00103      nid: 0x75       cid: 0x103      bus_state: 0x1  input_id: 0x0   sign: 0x0       count: 22
input: encoder  msg_id: 0x1EB00103      nid: 0x75       cid: 0x103      bus_state: 0x1  input_id: 0x0   sign: 0x0       count: 22
input: encoder  msg_id: 0x1EB00103      nid: 0x75       cid: 0x103      bus_state: 0x1  input_id: 0x0   sign: 0x0       count: 23
input: encoder  msg_id: 0x1EB00103      nid: 0x75       cid: 0x103      bus_state: 0x1  input_id: 0x0   sign: 0x0       count: 24
input: encoder  msg_id: 0x1EB00103      nid: 0x75       cid: 0x103      bus_state: 0x1  input_id: 0x0   sign: 0x0       count: 25
input: encoder  msg_id: 0x1EB00103      nid: 0x75       cid: 0x103      bus_state: 0x1  input_id: 0x0   sign: 0x0       count: 25
input: encoder  msg_id: 0x1EB00103      nid: 0x75       cid: 0x103      bus_state: 0x1  input_id: 0x0   sign: 0x0       count: 26

ilium007 avatar Apr 10 '22 08:04 ilium007

The state table approach that I forked and modified here does not exhibit this same behaviour.

https://github.com/MicroTechAU/micropython-rotary

I added this line to _transition_table = [

[_R_CW_2, _R_START, _R_CW_1, _R_START | _DIR_CCW], # _R_CW_1

Based on the approach taken here: https://github.com/mathertel/RotaryEncoder

I know your code is different from the state table approach but that method seems to work better for the encoders I have tested. No idea why.

ilium007 avatar Apr 10 '22 08:04 ilium007

I am confident of the integrity of the exclusive or algorithm for these reasons:

  1. The theory detailed here.
  2. I implemented it in hardware for an industrial NC machine: the slightest accumulated error in hours of running would have been apparent.
  3. Very careful testing of encoder_portable with hard IRQ's and an optical encoder did show some evidence of drift. However provoking this required some effort and can be explained by the interrupt issues explained in the above doc.

Re issue 3 Mike Teachman's code has the same vulnerability; I don't believe there is any way to avoid it with current firmware. IRQ's are necessarily raised on every edge. It is necessary to determine which edge launched the ISR. Detection happens after the edge, by which time another edge may have occurred. In your case the rate limit implied by your circuit should preclude this fault in both versions.

I think the cause lies elsewhere than in the decoding algorithm, and may be a fault in the asynchronous primitives.encoder.

I haven't tested it with the same degree of rigour as the underlying interrupt driven code. I'm puzzled by your getting two runs of the asynchronous callback with the same count. In my usage in micro-gui if this occurred it would probably be undetectable. It is an effect that I haven't looked for and may represent a bug in its .run method.

I will attempt a more rigorous test of primitives.encoder using an optical encoder to ensure clean logic levels to see if I can replicate this effect. I'll report back.

peterhinch avatar Apr 10 '22 11:04 peterhinch

Thanks for your dedication! I’m just trying to get a rotary encoder to work for a control panel. I see so many consumer products with rotary encoders and not running micropython 😉 eg. aviation gps units where I have never seen a skipped step, they are obviously better quality encoders. I may have to ditch the rotary encoder and go back to an old potentiometer.

ilium007 avatar Apr 10 '22 11:04 ilium007

The code pasted above in pastebin is what I’ve been testing with except for the div=4

ilium007 avatar Apr 10 '22 11:04 ilium007

So I just tested the bare Mike Teachman example code and the encoder runs without fault, never ever a skipped click and fwd / rev works without issue.

import sys
from rotary_irq_pyb import RotaryIRQ
import uasyncio as asyncio
from machine import Pin

# Use heartbeat to keep event loop not empty
async def heartbeat():
    while True:
        await asyncio.sleep_ms(10)

event = asyncio.Event()

def callback():
    event.set()

pin_b10 = Pin(Pin.cpu.B10, Pin.IN)
pin_b11 = Pin(Pin.cpu.B11, Pin.IN)

async def main():
    r = RotaryIRQ(pin_num_clk=pin_b11,
                  pin_num_dt=pin_b10)
    r.add_listener(callback)
    
    asyncio.create_task(heartbeat())
    
    while True:
        await event.wait()
        print('result =', r.value())
        event.clear()

try:
    asyncio.run(main())
except (KeyboardInterrupt, Exception) as e:
    print('Exception {} {}\n'.format(type(e).__name__, e))
finally:
    ret = asyncio.new_event_loop()  # Clear retained uasyncio state

Results

MicroPython v1.18 on 2022-01-17; Adafruit Feather STM32F405 with STM32F405RG
Type "help()" for more information.
>>> 
MPY: sync filesystems
MPY: soft reboot
result = 1
result = 2
result = 3
result = 4
result = 5
result = 6
result = 7
result = 8
result = 9
result = 8
result = 7
result = 6
result = 5
result = 4
result = 3
result = 2
result = 1
result = 0

When I print the state at each click on the encoder.

3 clicks clockwise, 3 clicks anticlockwise, 3 clicks clockwise

MicroPython v1.18 on 2022-01-17; Adafruit Feather STM32F405 with STM32F405RG
Type "help()" for more information.
>>>
MPY: sync filesystems
MPY: soft reboot
4
16
result = 1
1
2
3
16
result = 2
1
2
3
16
result = 3
1
2
1
32
result = 2
4
5
4
5
6
32
result = 1
4
5
6
32
result = 0
4
5
4
5
4
16
result = 1
1
2
3
16
result = 2
1
2
3
16
result = 3
1
2
1
2

These are the int values of the states at each rotation, I didn't know how to change to the hex values in the callback.

ilium007 avatar Apr 10 '22 12:04 ilium007

You can see fro the above the the clockwise to anticlockwise rotation is a different state sequence but it is handled

ilium007 avatar Apr 10 '22 12:04 ilium007

I've now run this test (with pin nos. adapted) on a Pyboard 1.1. This is on the basis that it uses the same chip as your board. With div=4 I couldn't fault its behaviour: I never saw duplicate counts however slowly I rotated it. There is some hysteresis when you change direction, but this is by design.

With div=1 it is very sensitive and I found it hard to turn it slowly and consistently enough to ensure the callback occurs at one step intervals - it would occasionally skip a couple of steps. It's hard to prove but I'm pretty sure it's down to slightly shaky hands. Again I never saw duplicate counts.

I'm none the wiser as to what is going on. You might like to try the test script with your mechanical encoder and input circuit.

peterhinch avatar Apr 10 '22 13:04 peterhinch

Where you using any hardware debouncing at all?

ilium007 avatar Apr 10 '22 13:04 ilium007

This is turning clockwise to 100 and back again:

Running encoder test. Press ctrl-c to teminate.
4 4
8 4
9 1
8 -1
9 1
12 3
14 2
17 3
21 4
25 4
29 4
33 4
37 4
39 2
41 2
45 4
49 4
53 4
57 4
60 3
61 1
62 1
63 1
67 4
70 3
72 2
74 2
77 3
78 1
82 4
86 4
89 3
90 1
94 4
98 4
100 2
96 -4
92 -4
87 -5
86 -1
83 -3
82 -1
79 -3
76 -3
75 -1
74 -1
71 -3
69 -2
67 -2
63 -4
59 -4
55 -4
51 -4
49 -2
47 -2
44 -3
43 -1
41 -2
39 -2
35 -4
31 -4
27 -4
23 -4
21 -2
19 -2
15 -4
11 -4
7 -4
3 -4
0 -3

ilium007 avatar Apr 10 '22 13:04 ilium007

This is with div=4 and delay reduced to 25ms in encoder.py - worked faultlessly.

Could my issues be related to another class in my code causing issues. I am running the pub.CAN class. Not sure if there is something at the interrupt level causing problems.

MPY: sync filesystems
MPY: soft reboot
Running encoder test. Press ctrl-c to teminate.
1 1
2 1
3 1
4 1
5 1
6 1
7 1
8 1
9 1
10 1
11 1
12 1
13 1
14 1
15 1
16 1
17 1
18 1
19 1
20 1
21 1
22 1
21 -1
20 -1
19 -1
18 -1
17 -1
16 -1
15 -1
14 -1
13 -1
12 -1
11 -1
10 -1
9 -1
8 -1
7 -1
6 -1
5 -1
4 -1
3 -1
2 -1
1 -1
0 -1
1 1
0 -1
1 1
0 -1
1 1
0 -1

ilium007 avatar Apr 10 '22 13:04 ilium007

I probably should have added that my use for the rotary encoder is an input to a speed controller so I really would like a detent to equate to a speed state change.

ilium007 avatar Apr 10 '22 13:04 ilium007

So just running the test again I can get it to fail on the direction change and not register a reading. Its random as to when it occurs but its doing it in this test code. CTRL+C and restart REPL without touching the encoder or removing power from the circuit and the direction change is registered again on each fwd/rev detent. It just feels like a code issue to me vs a hardware issue.

ilium007 avatar Apr 10 '22 13:04 ilium007

No debouncing. Optical encoders can produce jitter due to vibration, but they don't produce nasty logic levels.

I assume you're running with div=1?

I wonder if there is a mismatch between the mechanical click detents on the switch and the location of its electrical transitions. You seem commonly to get four changes for each click. To be honest I've not studied mechanical encoders in detail although I use them in micro-gui. However if there are such irregularities you should be able to minimise their effect with higher values of div.

FWIW in my micro-gui test rigs I use no pre-conditioning and div=5. But the nature of GUI applications is such that the odd extra or missed count passes largely un-noticed because of the visual feedback. There is no question that your approach with Schmitt triggers is superior.

peterhinch avatar Apr 10 '22 13:04 peterhinch

I am using div=4, that's were I get the almost perfect response but every now and again the test code above fails and the fwd/rev transition is missed.

I am using brand new Bourns encoders that have (as is common) 4 transitions per physical detent.

Not sure what to do next...

ilium007 avatar Apr 10 '22 13:04 ilium007

If I ditched the encoders with physical detents the issue would be un-noticed as the user would never associate a missed physical click.

ilium007 avatar Apr 10 '22 13:04 ilium007

All of these fwd/rev transitions below worked perfectly, just every now and again that stops working and it need two clicks on the direction change to register a change. CTRL-C and restart code fixes it, now physical power off/on. Surely this has to be code.

6 -1
7 1
6 -1
7 1
6 -1
7 1
6 -1
7 1
6 -1
7 1
6 -1
7 1
6 -1
7 1
6 -1
7 1
6 -1
7 1
6 -1
7 1
6 -1
7 1
6 -1
7 1
6 -1
7 1
6 -1
7 1
6 -1
7 1
6 -1
7 1
6 -1
7 1
6 -1
7 1
8 1
9 1
10 1
11 1
10 -1
9 -1
8 -1
7 -1
8 1
7 -1
8 1
7 -1
8 1
7 -1
8 1
7 -1

ilium007 avatar Apr 10 '22 13:04 ilium007

My efforts so far have been on approaching bit-perfect results with a detent-less optical encoder and "good enough for GUI" results with a mechanical one without pre-conditioning. I never considered the impact of detents.

I suspect that keeping perfect sync between clicks and count may involve more than setting div=4. Contact bounce - which will get through your 100μs pre-conditioning - will cause a one-bit uncertainty in the count maintained by the hardware ISR. This is the value before it is divided down. The division process introduces hysteresis. I haven't fully thought this through but I think the count prior to division would need to be initialised so that it was in the middle of the hysteresis band. You need to be sure that a one-bit change brought on by contact bounce can't trigger the hysteresis to flip to the next state.

You might like to try changing this line to

self._v = div // 2

to see if this improves things.

Meanwhile I'll build a circuit on the lines of yours and test a mechanical encoder.

peterhinch avatar Apr 10 '22 17:04 peterhinch

I've thought some more about this and I think accurately tracking the mechanical detents is likely to be difficult. This is because, as a process, it has no fault-tolerance. If you ever miss interrupts (e.g. because the latency exceeds 100μs and a brief pulse gets through) the error will accumulate.

Tthe underlying count which is maintained by the IRQ's needs to be bit-perfect, because there is no means of correcting it if it goes adrift. Or at least, I haven't thought of one yet. If it does go adrift you'll get the results you observe, where either two clicks are required or one click gives two callback calls.

It may be that the best hope of success is to increase the time constant of your pre-conditioning circuit so that it acts as a true debouncer. This would prevent rogue pulses getting through and as a side effect would allow very long IRQ latency. Your measurements show about 10ms between edges, but contact bounce can go on for longer than that: see this doc. It might be worth experimenting with times in the range 5-10ms. To be honest I have real doubts whether perfect detent syncing is practical with a mechanical encoder.

peterhinch avatar Apr 11 '22 12:04 peterhinch

Thanks for your continued help. I have ripped apart the Mike Teachman state table library and modified it to look more like your asyncio library and its working flawlessly. Tracks detent changes perfectly without issue. I think I will continue to run with that for the time being so I can move on with the rest of my project.

ilium007 avatar Apr 12 '22 04:04 ilium007

OK. I don't yet understand why that works, but I'm glad you have something that meets your needs.

I'll build some hardware and investigate to satisfy my curiosity about mechanical encoders. I may contact Mike Teachman for his views.

I also have an idea for a design specifically targeted on tracking detents which eliminates the hysteresis. If I come up with anything useful I'll update my code and/or docs and post a comment here.

peterhinch avatar Apr 12 '22 08:04 peterhinch

Thanks again.

My mashed up code here: https://pastebin.com/L3N1DBUZ

ilium007 avatar Apr 12 '22 08:04 ilium007

Thank you. I've downloaded it and will study it in detail. Your ISR's are commendably concise :+1:

peterhinch avatar Apr 12 '22 08:04 peterhinch

Well everything says they should be 😄

ilium007 avatar Apr 12 '22 08:04 ilium007

I can't argue with code that works, but I think it relies on the specific nature of your application which never blocks for more than 1ms or so.

The reason is that ThreadSafeFlag cannot schedule execution until the current task has yielded; there may also be other tasks in the queue which get to run first. (Current uasyncio has no priority mechanism.) As an example my GUI code has typical latency of ~50ms when the display is updated. In a more typical uasyncio application, the worst case latency of ThreadSafeFlag would substantially exceed the time between consecutive edges.

This is why I maintain the count in the ISR context. I do the division in the uasyncio context to minimise ISR code.

I'll be back in a few days when I've figured out the problem with my offering.

peterhinch avatar Apr 12 '22 10:04 peterhinch

Could I move all of the encoder logic out of the coro and remove the threadsafeflags and just run the user callback as a coro and create_task? I couldn't get create_task to work with the user supplied callback and its args.

ilium007 avatar Apr 12 '22 10:04 ilium007

I'm afraid I don't follow what you intend here, please explain.

I'm now very puzzled. I built conditioning circuits with 1.2ms time constants and Scmitt triggers and examined the output waveforms. They normally resembled yours closely. Sometimes there was clear evidence of contact bounce (in one case lasting 75ms!). On occasion one or both signals did not return to zero after a click. So I did not have high expectations.

I ran this script on a Pyboard 1.1. It is slightly adapted from encoder_test to make errors obvious.

from machine import Pin
import uasyncio as asyncio
from primitives.encoder import Encoder

px = Pin('X1', Pin.IN)
py = Pin('X2', Pin.IN)
old_delta = 0
def cb(pos, delta):
    global old_delta
    print(pos, delta)
    if abs(delta) != 1:
        print('***** FAIL *****')
    elif delta != old_delta:
        print('***** DIRECTION CHANGE *****')
        old_delta = delta

async def main():
    while True:
        await asyncio.sleep(1)

def test():
    print('Running encoder test. Press ctrl-c to teminate.')
    enc = Encoder(px, py, v=0, vmin=0, vmax=100, div=4, callback=cb)
    enc.delay = 0
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print('Interrupted')
    finally:
        asyncio.new_event_loop()

test()

Despite my best efforts to make it fail it tracked the detents perfectly through numerous direction changes and running past the 0 and 100 "end stops". I tried rotating fast and very slow and never saw a FAIL message or an unintended direction change. Two things differentiate my test setup from yours:

  1. I set enc.delay = 0. Without this it worked fine rotating slowly but changes were missed rotating fast. This is unsurprising.
  2. My preconditioning delay is 1.2ms.

For comparison I tried eliminating the preconditioning. Errors were common.

Have you checked that you are actually getting 100μs delays from your circuit? It might be worth increasing those to see if it has any effect.

peterhinch avatar Apr 12 '22 17:04 peterhinch

Oh boy… this is getting more interesting!

I have smd components and have soldered my encoders to a PCB. I think in have enough THC to breadboard and measure the time constant.

Can you explain your pre-conditioning delay?

ilium007 avatar Apr 12 '22 19:04 ilium007

I set up the test circuit on a breadboard.

First I tried with the 10k / 0.01uF RC circuit and no Schmitt Trigger IC:

>>>
MPY: sync filesystems
MPY: soft reboot
MPY: can't mount SD card
Running encoder test. Press ctrl-c to teminate.
3 3
***** FAIL *****
4 1
***** DIRECTION CHANGE *****
5 1
6 1
9 3
***** FAIL *****
10 1
11 1
12 1
13 1
19 6
***** FAIL *****
20 1
19 -1
***** DIRECTION CHANGE *****
16 -3
***** FAIL *****
15 -1
13 -2
***** FAIL *****
12 -1
11 -1
10 -1
9 -1
8 -1
7 -1
6 -1
5 -1
4 -1
5 1
***** DIRECTION CHANGE *****
11 6
***** FAIL *****
12 1
13 1
16 3
***** FAIL *****
18 2
***** FAIL *****
19 1
20 1
21 1
22 1
23 1

Then with 10k / 0.01uF RC and 74HC14N Schmitt Trigger. It worked but could still generate the direction change issue after a while. It seemed to get into that state if I generated the ***** FAIL ***** condition a number of times. It would just sit there on fwd/rev clicks doing nothing. I could do that 1 hundred times and it would not leave that state. If I CTRL-C and CTRL-D leaving the encoder in the exact same position it would then do the fwd/rev perfectly. I just do not think it is hardware related.

>>>
MPY: sync filesystems
MPY: soft reboot
MPY: can't mount SD card
Running encoder test. Press ctrl-c to teminate.
1 1
***** DIRECTION CHANGE *****
2 1
3 1
4 1
5 1
6 1
7 1
8 1
9 1
10 1
9 -1
***** DIRECTION CHANGE *****
8 -1
7 -1
6 -1
5 -1
4 -1
3 -1
2 -1
1 -1
0 -1

Then with the 10k / 0.1uF RC and 74HC14N Schmitt Trigger. This one was different, I went 0-10, reversed 10-1 a number of times with no fwd/rev issue at the 'click'. Then continued 0-15 and tried the rev change, nothing

MicroPython v1.18 on 2022-01-17; Adafruit Feather STM32F405 with STM32F405RG
Type "help()" for more information.
>>>
MPY: sync filesystems
MPY: soft reboot
MPY: can't mount SD card
Running encoder test. Press ctrl-c to teminate.
1 1
***** DIRECTION CHANGE *****
2 1
3 1
4 1
5 1
6 1
7 1
8 1
9 1
10 1
9 -1
***** DIRECTION CHANGE *****
8 -1
7 -1
6 -1
5 -1
4 -1
3 -1
2 -1
1 -1
0 -1
1 1
***** DIRECTION CHANGE *****
2 1
3 1
4 1
5 1
6 1
7 1
8 1
9 1
10 1
9 -1
***** DIRECTION CHANGE *****
8 -1
7 -1
6 -1
5 -1
4 -1
3 -1
2 -1
1 -1
0 -1
1 1
***** DIRECTION CHANGE *****
2 1
3 1
4 1
5 1
6 1
7 1
8 1
9 1
10 1
11 1
12 1
13 1
14 1
15 1         <---------- went fwd/rev here 10 times with no state change

The n did CTRL-C and CTRL-D in REPL without changing encoder position and got the following perfect fwd/rev direction changes.

MicroPython v1.18 on 2022-01-17; Adafruit Feather STM32F405 with STM32F405RG
Type "help()" for more information.
>>>
MPY: sync filesystems
MPY: soft reboot
MPY: can't mount SD card
Running encoder test. Press ctrl-c to teminate.
1 1
***** DIRECTION CHANGE *****
0 -1
***** DIRECTION CHANGE *****
1 1
***** DIRECTION CHANGE *****
0 -1
***** DIRECTION CHANGE *****
1 1
***** DIRECTION CHANGE *****
0 -1
***** DIRECTION CHANGE *****
1 1
***** DIRECTION CHANGE *****
0 -1
***** DIRECTION CHANGE *****
1 1
***** DIRECTION CHANGE *****
0 -1
***** DIRECTION CHANGE *****
1 1
***** DIRECTION CHANGE *****
0 -1
***** DIRECTION CHANGE *****
1 1
***** DIRECTION CHANGE *****
0 -1
***** DIRECTION CHANGE *****
1 1
***** DIRECTION CHANGE *****
0 -1
***** DIRECTION CHANGE *****

ilium007 avatar Apr 13 '22 01:04 ilium007

Just for giggles... Using the exact same hardware and the Mike Teachman library I got this result:

MicroPython v1.18 on 2022-01-17; Adafruit Feather STM32F405 with STM32F405RG
Type "help()" for more information.
>>>
>>>
MPY: sync filesystems
MPY: soft reboot
MPY: can't mount SD card
result = 1
result = 2
result = 3
result = 4
result = 5
result = 4
result = 3
result = 2
result = 1
result = 0
result = 1
result = 2
result = 3
result = 4
result = 5
result = 4
result = 3
result = 2
result = 1
result = 0
result = 1
result = 2
result = 3
result = 4
result = 5
result = 4
result = 3
result = 2
result = 1                         <-----------------  no skipped detents to here and then proceeded to go back and forth
result = 0
result = 1                         <-----------------  no skipped detents on the rest of these direction changes, one detent each
result = 0
result = 1
result = 0
result = 1
result = 0
result = 1
result = 0
result = 1
result = 0
result = 1
result = 0
result = 1
result = 0
result = 1
result = 0
result = 1
result = 0
result = 1
result = 0
result = 1
result = 0
result = 1
result = 0
result = 1
result = 0
result = 1
result = 0
result = 1
result = 0
result = 1
result = 0
result = 1
result = 0
result = 1
result = 0

I then ran again but this time wound the encoder as fast as I could (knowing that this would induce errors but when I got to count -13 I started just going fwd/rev continuously and got zero skipped detents or state issues.

MicroPython v1.18 on 2022-01-17; Adafruit Feather STM32F405 with STM32F405RG
Type "help()" for more information.
>>>
MPY: sync filesystems
MPY: soft reboot
MPY: can't mount SD card
result = 1
result = 2
result = 3
result = 4
result = 5
result = 6
result = 5
result = 4
result = 3
result = 2
result = 1
result = 0
result = -1
result = -2
result = -1
result = 0
result = 2
result = 3
result = 4
result = 5
result = 4
result = 3
result = 2
result = 1
result = 0
result = 1
result = 2
result = 4
result = 5
result = 6
result = 7
result = 6
result = 5
result = 4
result = 3
result = 2
result = 1
result = 0
result = 1
result = 2
result = 3
result = 4
result = 5
result = 6
result = 5
result = 4
result = 3
result = 2
result = 1
result = 0
result = -1
result = -2
result = -1
result = 0
result = 1
result = 2
result = 1
result = 0
result = -1
result = -2
result = -3
result = -4
result = -3
result = -2
result = -1
result = 0
result = 1
result = 2
result = 3
result = 4
result = 5
result = 6
result = 7
result = 8
result = 9
result = 10
result = 11
result = 10
result = 9
result = 8
result = 6
result = 5
result = 4
result = 3
result = 2
result = 3
result = 4
result = 6
result = 7
result = 9
result = 8
result = 6
result = 4
result = 3
result = 2
result = 3
result = 4
result = 5
result = 7
result = 8
result = 9
result = 10
result = 9
result = 8
result = 6
result = 5
result = 4
result = 5
result = 6
result = 7
result = 9
result = 10
result = 9
result = 7
result = 6
result = 5
result = 4
result = 3
result = 2
result = 1
result = 0
result = -1
result = -2
result = -3
result = -4
result = -5
result = -6
result = -4
result = -3
result = -2
result = -1
result = -2
result = -3
result = -4
result = -6
result = -7
result = -8
result = -7
result = -6
result = -5
result = -4
result = -5
result = -6
result = -8
result = -9
result = -10
result = -11
result = -12
result = -11
result = -9
result = -8
result = -7
result = -6
result = -5
result = -4
result = -3
result = -4
result = -6
result = -7
result = -8
result = -10
result = -11
result = -10
result = -9
result = -8
result = -7
result = -6
result = -5
result = -4
result = -3
result = -4
result = -6
result = -7
result = -8
result = -9
result = -10
result = -11
result = -10
result = -9
result = -8
result = -7
result = -6
result = -5
result = -6
result = -7
result = -9
result = -10
result = -11
result = -12
result = -13
result = -12
result = -11
result = -10
result = -9
result = -8
result = -6
result = -5
result = -6
result = -7
result = -9
result = -11
result = -12
result = -13
result = -14
result = -16
result = -17
result = -18
result = -19
result = -20
result = -21
result = -20
result = -19
result = -18
result = -17
result = -16
result = -15
result = -14
result = -13
result = -12
result = -14
result = -15
result = -16
result = -18
result = -19
result = -18
result = -17
result = -16
result = -15
result = -16
result = -17
result = -19
result = -20
result = -21
result = -22
result = -21
result = -19
result = -18
result = -16
result = -15
result = -14
result = -15
result = -16
result = -17
result = -18
result = -19
result = -20
result = -21
result = -19
result = -18
result = -17
result = -16
result = -15
result = -14
result = -13
result = -14                         <-----------------  no skipped detents from here on fwd/rev direction change, one detent each
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15
result = -14
result = -15

Inducing the errors on the micropython-async library would have got the microcontroller into a state where it could no longer distinguish the fwd/rev direction change in one detent, it would take two detents to indicate a direction change. Obviously not what I want for a UI.

ilium007 avatar Apr 13 '22 01:04 ilium007

I can't understand why we're getting such different results. I'll try again to break it, but I don't hold out much hope; I can't fix a fault unless I can replicate it.

All I can think to do is to further examine the waveforms from your pre-conditioner and the STM32F405 to see if you can see anything seriously challenging (e.g. pulses <100μs, dodgy levels).

For completeness here is my implementation of the pre-conditioner, which reflects the parts I had available. I don't believe there is any real difference between our solutions, but you might like to see if you can see anything of significance. Image

peterhinch avatar Apr 13 '22 11:04 peterhinch

Unfortunately I don't have a proper scope so can't reliably create the waveforms you need. I'm using the Teachman library for now as I can't get it to fault at all but I am equally interested in seeing this code work for me as well.

ilium007 avatar Apr 19 '22 09:04 ilium007

I have pursued this further with these outcomes:

  1. I remain baffled as to why you didn't have success with your Schmitt pre-conditioner.
  2. I have significantly improved my code and have pushed an update. It now tracks detents on platforms with hard IRQ's.

The issue was with my ISR. As I stated in my docs, a problem arises if a pulse occurs which is shorter than the interrupt latency. In that instance, the old code used the changed value causing the count to change in the wrong direction. Having thought further about this, it can be fixed with this ISR:

    def _x_cb(self, pin_x):
        if (x := pin_x()) != self._x:  # Only process the IRQ if pin value has changed
            self._x = x
            self._v += 1 if x ^ self._pin_y() else -1
            self._tsf.set()

The short pulse will cause a second ISR call on the trailing edge, but with this logic the second call will also do nothing.

I tested with a Pyboard and the encoder connected directly to the pins and using only the internal pullups. With div=4 it tracked the detents perfectly.

I also tested with an ESP32. This cannot deliver perfect results because the latency exceeds the time between actual edges of the encoder, but it definitely performs better than the old code.

I'll be interested to hear any results of testing.

peterhinch avatar Apr 19 '22 12:04 peterhinch