Encoder with GPIO interrupts

I have an EC11 encoder connected to the SCM20260D and using this to input numeric fields. It kind of works but I am seeing direction changes when I go in 1 direction depending on the speed. I’ve used this encoder before with an STM32 and it was super stable and would count up or down with no reverse pulses detected.

The datasheet says 2-3ms chatter so I set the debounce to 4ms.

Looking at the scope, the pulses look good. I have a 10K pullup and a 0.1uF cap on the input.

This is the count up direction at fast rotation speed. I have the interrupt set for falling edge on the yellow trace and check the blue trace to determine the direction. The scope shows that the pulses are all good with 8ms before the blue trace returns high.

Next is the scope from the other direction, again it all looks good with 6ms before the transition to the other state.

I’ve tried debounce 1ms, 2ms etc and I just don’t get a good response. It doesn’t look good from the operators point of view. Going slow works but that defeats the purpose of the endoder.

How do you disable debounce? Do we just leave this field unset or do we need to set this to ZERO?

Lastly, how does the debounce work? I could not see how the delay is handled in the Library files.

It has been a while but I remember some code from ages ago!! Was this a gadgeteer driver then?

Anyway, do you have some code we can use to look into this for you?

This is the init code. I was careful to ensure that none of the GPIO with the same number is used as per the documents. This is on the SCM20260D.

    encoderA = gpioController.OpenPin(SC20260.GpioPin.PF6);
    encoderB = gpioController.OpenPin(SC20260.GpioPin.PF8);
    encoderButton = gpioController.OpenPin(SC20260.GpioPin.PC13);

    encoderA.SetDriveMode(GpioPinDriveMode.Input);
    encoderB.SetDriveMode(GpioPinDriveMode.Input);
    encoderButton.SetDriveMode(GpioPinDriveMode.Input);

    encoderA.ValueChangedEdge = GpioPinEdge.FallingEdge;
    encoderA.DebounceTimeout = new TimeSpan(0, 0, 0, 0, 2);
    encoderA.ValueChanged += EncoderA_ValueChanged;

    encoderButton.ValueChangedEdge = GpioPinEdge.FallingEdge;
    encoderButton.DebounceTimeout = new TimeSpan(0, 0, 0, 0, 20);
    encoderButton.ValueChanged += EncoderButton_ValueChanged;

This is the interrupt handler.

private static void EncoderButton_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs e)
{
    onEncoderButtonPressed?.Invoke(typeof(IOClass), EventArgs.Empty);
}

private static void EncoderA_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs e)
{
    if (e.Edge == GpioPinEdge.FallingEdge)
    {
        if (encoderB.Read() == GpioPinValue.Low)
        {
            encoderCount++;
        }
        else
        {
            encoderCount--;
        }
        onEncoderValueChanged?.Invoke(typeof(IOClass), EventArgs.Empty);
    }
}

Set it to zero. Unset it will be default 20ms as I remember.

Native does it, library has nothing to do with it.

If we set it to zero and it defaults to 20ms, how do we disable it?

if you set it to zero then it is zero.

It is only 20ms default if you don’t touch on that property.

You can’t read the counterB pin from counterA event. An event may be fired too late and pin value isn’t correct.

Try below, it will work at 1ms.

       static GpioPin encoderA;
        static GpioPin encoderB;
        
        static int encoderCount;
        static int deboud = 1; // 1ms
        static long lastInteruptNativeA = 0;
        static long lastInteruptNativeB = 0;

        static void Test_Custom()
        {
            var gpioController = GpioController.GetDefault();

            encoderA = gpioController.OpenPin(SC20260.GpioPin.PC3);
            encoderB = gpioController.OpenPin(SC20260.GpioPin.PF8);          

            encoderA.SetDriveMode(GpioPinDriveMode.InputPullUp);
            encoderB.SetDriveMode(GpioPinDriveMode.InputPullUp);
          
            encoderA.ValueChangedEdge = GpioPinEdge.FallingEdge ;
            encoderA.DebounceTimeout = new TimeSpan(0, 0, 0, 0, deboud);
            encoderA.ValueChanged += EncoderA_ValueChanged;

            encoderB.ValueChangedEdge = GpioPinEdge.FallingEdge;
            encoderB.DebounceTimeout = new TimeSpan(0, 0, 0, 0, deboud);
            encoderB.ValueChanged += EncoderB_ValueChanged;

            encoderCount = 0;

            Thread.Sleep(-1);
        }

        private static void EncoderA_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs e)
        {
            lastInteruptNativeA = e.Timestamp.Ticks;

            if (lastInteruptNativeB > 0)
            {
                if (lastInteruptNativeA > lastInteruptNativeB)
                {
                    encoderCount++;
                }
                else
                    encoderCount--;

                lastInteruptNativeA = lastInteruptNativeB = 0;

                Debug.WriteLine(" encoderCount = " + encoderCount);
            }
        }

        private static void EncoderB_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs e)
        {            
            lastInteruptNativeB = e.Timestamp.Ticks;

            if (lastInteruptNativeA > 0)
            {
                if (lastInteruptNativeA > lastInteruptNativeB)
                {
                    encoderCount++;
                }
                else
                    encoderCount--;

                lastInteruptNativeA = lastInteruptNativeB = 0;

                Debug.WriteLine(" encoderCount = " + encoderCount);
            }
        }
1 Like

Still not 100%. I am still seeing down counts when I turn cw and also up counts in the ccw direction.

If I change direction without stopping, the count continues in the same direction.

Because the interrupts are event driven and not real time, this could be tricky to resolve.

In our Rotary Click driver, we are doing this way :

// First encoder
            _encA = GpioController.GetDefault().OpenPin(socket.PwmPin);
            _encA.SetDriveMode(GpioPinDriveMode.Input);
            _aLastState = _encA.Read();
            _encA.ValueChanged += EncA_ValueChanged;

            // Second encoder
            _encB = GpioController.GetDefault().OpenPin(socket.AnPin);
            _encB.SetDriveMode(GpioPinDriveMode.Input);

private void EncA_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs e)
        {
            _aState = _encA.Read();
            if (_aState != _aLastState)
            {
                if (_encB.Read() != _aState)
                {
                    InternalCounter++;
                    Direction = Directions.Clockwise;
                }
                else
                {
                    InternalCounter--;
                    Direction = Directions.CounterClockwise;
                }
                RotationEventHandler rotationEvent = RotationDetected;
                rotationEvent(this, new RotationEventArgs(Direction, InternalCounter));
            }
            _aLastState = _aState;
        }

If it can help.

1 Like

That doesn’t seem to work at all for me. I have falling edge triggering on encoderA

I can’t get any event with slow rotation. Fast rotation gives me a count up and then count down on the next event.

Not sure this code would work as it falls into the same issue that DAT mentions that reading encoderB input may not be active anymore if there is a delay before the falling edge and the event firing.

We kind of need a hardware level interrupt handler that is fast enough to capture the edge and the state of the second input. I have a hardware counter on another project that uses an Atmel ATTiny418 to handle encoder readings from a 2000 pulse encoder and I can record perfect steps up to 5 rpm with that.

Well this seems to work really well. I removed the check for the edge in my original code and using the following the count up and down is perfect.

private static void EncoderA_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs e)
{
    if(encoderB.Read() == GpioPinValue.Low)
    {
        encoderCount++;
    }
    else
    {
        encoderCount--;
    }
    Debug.WriteLine("encoder Count - " + encoderCount.ToString());

    onEncoderValueChanged?.Invoke(typeof(IOClass), EventArgs.Empty);
}

Probably I was using different firmware which is not released yet, although I don’t see how it effect.

Also

Still not 100%. I am still seeing down counts when I turn cw and also up counts in the ccw direction.

I had that issue with one encoder that has no 10K and 0.1uF. We tried another one with10K and 0.1uF then signal pulses are clean and work fine. Just more information because I think you already have them installed.

Unless I’m wrong, if you don’t test the EncoderA value, you can’t say whether you’re rotating clockwise or counter clockwise.

Here you are only testing low/high state of the 2nd output of the encoder. But low on this single 2nd encoder does not mean CCW.

Rotation direction is determined by both encoders, not only one.

If you look at the 2 scope traces I posted above for cw and ccw rotation, you will see that if you trigger on the falling edge of the encoderA signal and then read encoderB in the interrupt handler, you can determine direction as it will be either high or low for each direction. This it the exact same technique I’ve used for years with encoders and it works perfectly.
This is what I am doing now and the count seems to be perfect, even with fast rotation.