miketeachman / micropython-rotary

MicroPython module to read a rotary encoder.
MIT License
269 stars 56 forks source link

It's counting every 2nd step or skipping steps #2

Closed gampam2000 closed 2 years ago

gampam2000 commented 5 years ago

Hi,

first of all i like the implementation, great work. I tested it on a ESP32 and I experienced that I have to turn the encoder 2 steps so that it is counted +1 or -1. Have you experienced this behaviour? But maybe it is just my encoder (30steps per Revolution).

A second issue is, that if i turn the encoder faster it skips steps. Is there a solution to this problem?

miketeachman commented 5 years ago

Thanks for trying out the encoder library! I appreciate the feedback. I have some time in the next couple of days to look into this problem.

Can you tell me what encoder you are using. e.g. purchase link? or name?

Also, do you know the approximate turning speed where you start to see missed steps. e.g. 2 turns per second? That will help me to test with the similar conditions.

gampam2000 commented 5 years ago

Hi, its an alps encoder. And it generate only 15pulses and has 30 steps, so I think your implementation is correct. https://www.alps.com/prod/info/E/HTML/Encoder/Incremental/EC11/EC11E15244G1.html

About the missing steps, when I turn it slighty faster it skippes steps. I will test it with a different encoder, because the only thing this could happen if the interrupt callback takes longer than the next interrupt, but I doubt thats the case. Especially on the ESP32 at full clock.

hpirila commented 5 years ago

Hi,

I experience same behavior.

My encoder is 100 step CNC rotary encoder, https://tinyurl.com/y38vyyu7 . I am using ESP32 and even it says 5V encoder, it seems to work fine in ESP32 3.3 V.

I have used same encoder with Arduino and this library https://www.pjrc.com/teensy/td_libs_Encoder.html , it does not lose steps ever.

With ESP32 and this library, it works but has three problems.

  1. This is very constant problem. When I change direction it needs always two steps before value is changed. When I change direction and do only one step, value is same as previous value. If I move one step back and forw continuously, value does not change at all.

  2. Similar to problem 1. After ESP32 reset, it needs two steps before the value changes. This happens always.

  3. This is less constant, but sometimes it loses steps. It is rare and not depending on speed I rotate. I can rotate very fast and in most cases it is fine.

Thanks for this library, it is very useful.

hpirila commented 5 years ago

Hi,

I added this debug line and did some testing to clarify my previous post

    def _process_rotary_pins(self, pin):
        print(str(self._hal_get_clk_value())+" "+str(self._hal_get_dt_value()))

Encoder was initialized with this command, rest is example.py

r = RotaryIRQ(pin_num_clk=19, pin_num_dt=23)

See below log of the test. I added manually comments to log starting with //

// ------ reset
0 1
1 1
1 0
0 0
// ------ rotated right 1: note value did not change
0 1
1 1
1 0
result = 1
0 0
// ------ rotated right 1
0 1
1 1
result = 2
1 0
0 0
// ------ rotated right 1
0 1
1 1
1 0
0 0
result = 3
// ------ rotated right 1
0 1
1 1
result = 4
1 0
0 0
// ------ rotated right 1
0 1
1 1
1 0
0 0
result = 5
// ------ rotated right 1
0 1
1 1
1 0
result = 6
0 0
// ------ rotated right 1
0 1
1 1
1 0
0 0
result = 7
// ------ rotated right 1
1 1
result = 8
1 0
0 0
// ------ rotated right 1
1 0
1 1
0 1
0 0
// ------ rotated left 1, note: value did not change
1 0
1 1
0 1
result = 7
0 0
// ------ rotated left 1, note: value changed when rotated two steps left
1 0
1 1
0 1
0 0
1 0
result = 6
0 0
// ------ rotated left 1, it went slightly over the slot and then back to slot
1 0
1 1
0 1
0 0
result = 5
// ------ rotated left 1
0 1
1 1
1 0
0 0
// ------ rotated right 1, note: value did not change
1 0
1 1
0 1
0 0
// ------ rotated left 1, note: value did not change
0 1
1 1
1 0
0 0
// ------ rotated right 1, note: value did not change
0 1
1 1
1 0
0 0
result = 6
// ------ rotated right 1, note: value changed only after rotated 2 steps right
hpirila commented 5 years ago

And for losing steps problem.

I tested more and it is not losing steps at all, no matter if i rotate fast (>200 steps/s) or slow.

Only problems are related to direction change and first step after reset.

miketeachman commented 5 years ago

@gampam2000

I will test it with a different encoder

That would be great. I don't own the type of encoder that you mention.

because the only thing this could happen if the interrupt callback takes longer than the next interrupt

I wanted to share some oscilloscope captures that might give some insight into the limitations of the ESP32 and this MicroPython based implementation of an encoder.

Signals: Yellow: CLK, Aqua: DT, Purple: ISR duration (_process_rotary_pins() in rotary.py)

Scope plot#1 shows CLK/DT signals for one encoder click and the resulting calls to the ISR

DS1Z_QuickPrint1

Scope plot#2 zooms in on the fall of DT and shows 7 calls to the ISR. The multiple calls to the ISR are caused by bouncing of the DT contact. Note that interrupts are queued by the ESP32 and result in multiple calls to the ISR. Running the ISR 7 times back-to-back takes 1.920ms DS1Z_QuickPrint3

Scope plot#3 zooms in on the first call to the ISR. This capture shows the bouncing of DT, which triggers the ISR multiple times. The ISR takes 240us to complete, and the ISR latency is 83us (DT falling to ISR start)

DS1Z_QuickPrint4

These oscilloscope captures show that:

These performance reducing factors result in missed steps when the encoder is rotated quickly.

To illustrate this point, here is a final scope capture when the encoder is rapidly turned 6 steps in 50.6ms - it shows an almost continuous operation of the ISR. In this case 4 steps were missed.

DS1Z_QuickPrint6

Hope this helps to understand what is happening "under the hood" of this rotary encoder implementation

miketeachman commented 5 years ago

@hpirila

See below log of the test. I added manually comments to log starting with //

Your debug print and comments helped a lot. I'm fairly certain that I see the issue - the encoder you are using has different resting values for CLK and DT than the encoder I used when developing the Rotary module.

At rest, your CNC encoder has CLK=0 and DT=0. But, the state table is designed to work with an encoder that has CLK=1 and DT=1 in the resting state. e.g. if you put an oscilloscope (or multimeter) on DT and CLK you will see that CLK=0V and DT=0V when it is at rest.

The different resting values for CLK/DT changes how the transition table operates.

Here is a workaround that I think will make your encoder function properly - it involves changing one line in the ISR as shown below to invert the CLK/DT values.

def _process_rotary_pins(self, pin):
     clk_dt_pins = ((not self._hal_get_clk_value()) << 1) | (not self._hal_get_dt_value())

Is it possible to try this workaround?

One other observation: the CLK and DT signals coming from your encoder appear to have no contact bounce. This is a good thing as it will allow you to have a higher rotation rate before counts are lost.

hpirila commented 5 years ago

I tried the workaround and it works very well. Both problems, first step after reset and direction change are now working as expected.

I also did some stress test to rapidly move back and forth as fast as I can. It never loses a single step. If I start from slot 0, result is always 0 when I move back to slot 0. Also tested with two encoders moving simultaneously and rapidly, still works fine.

This encoder has 6 connectors, 0V, +5V, A, B, A- and B-. I am currently connected to A and B, likely the library would have work straight away had I used A- and B- connectors instead. Not tested that yet.

gampam2000 commented 5 years ago

So I quess my encoder bounces a lot.

So it bounces more at the beginning, maybe the problem can be reduced by disable the interrupts when entering the ISR and enable it at the end. So during the time of the ISR no more Interrupts can be queued. When looking at your Plot #2 the 7 ISR calls are all the result of the spikes in small time left of the first white vertical line.

Than the max. rotation speed without losing steps is limited by the time the ISR takes to complete.

Or did I miss something?

hpirila commented 5 years ago

You can try to add capacitors between A, B and ground as seen here https://hifiduino.wordpress.com/2010/10/20/rotaryencoder-hw-sw-no-debounce/

gampam2000 commented 5 years ago

I've now tested it with a different encoder. A Honeywell 600EN-128-CN1 (https://datasheet.octopart.com/600128CN1-Honeywell-datasheet-11736964.pdf)

This Encoder has 128 steps per revolution. If i turn it really slowy (4s/revolution) it counts only 80 steps out of the 128. So here I also loose Steps.

If I turn it with 1rev/s one step equals to 7,8ms. I will test it with the debouncing circuit.

miketeachman commented 5 years ago

@gampam2000
about the max rotation speed. As you mentioned, max speed without losing steps is limited by the ISR duration .. and, also the number of interrupts caused by contact bounce. I'm interested in the results of the debounce capacitor test. I haven't tried that yet. It might be a good way to improve the performance of bouncy encoders.

miketeachman commented 5 years ago

@hpirila glad that the workaround helped. I'll look at adding an option to the constructor, to support encoders that have DT/CLK=0 on start. Thanks for the link on the capacitor technique to reduce contact bounce. I'll try it out the next time I'm using encoders.

miketeachman commented 2 years ago

The new invert argument in the commit https://github.com/miketeachman/micropython-rotary/commit/e7673121e7c6071033ff13377e71ee063fa63b25 replaces the need for this workaround