dotnet / iot

This repo includes .NET Core implementations for various IoT boards, chips, displays and PCBs.
MIT License
2.13k stars 574 forks source link

Inconsistent number of events received from RegisterCallbackForPinValueChangedEvent #841

Closed nahueltaibo closed 1 year ago

nahueltaibo commented 4 years ago

Describe the bug

I'd like to know better if this is a bug, a limitation of the implementation or if it's me doing something wrong.

I subscribed to event changes on two pins (20 and 21), and counted how many events I get from each pin. The pins are connected to the quadrature encoder of a motor. I am rotating the motor in only one direction, so I would expect events to keep the same number, but this is not happening.

Steps to reproduce

  1. Connect the quadrature encoder of a motor to pins 20 and 21 of raspberry pi
  2. Turn manually the motor in only one direction
var controller = new GpioController();
var pinA = 20;
var pinB = 21;
int pinACount = 0;
int pinBCount = 0;

controller.OpenPin(pinA, PinMode.Input);
controller.OpenPin(pinB, PinMode.Input);

controller.RegisterCallbackForPinValueChangedEvent(pinB, PinEventTypes.Falling, (o, e) =>
{

        // Update how many times pinB changed
        pinBCount++;
});

controller.RegisterCallbackForPinValueChangedEvent(pinA, PinEventTypes.Falling, (o, e) =>
{
        // Update how many times pinA changed
        pinACount++;

        Console.WriteLine($"A={pinACount}; B={pinBCount};");
});

Expected behavior

pinACount and pinBCount Should be the same (or at least different by only 1)

Actual behavior

pinACount and pinBCount deffer by a lot

This is the console output of such program and the count of pins 20 and 21 (A and B)

pi@devpi:~/Dev/QuadratureEncoderTest $ sudo ./QuadratureEncoderTest
A=1; B=0;
A=2; B=1;
A=3; B=3;
A=4; B=4;
A=5; B=4;
A=6; B=4;
A=7; B=4;
A=8; B=4;
A=9; B=5;
A=10; B=7;
A=11; B=10;
A=12; B=11;
A=13; B=12;
A=14; B=14; -- ie: A grows steady, B is stuck in "14"
A=15; B=14;
A=16; B=15;
A=17; B=15;
A=18; B=15;
A=19; B=15;
A=20; B=16;
A=21; B=17;
A=22; B=18;
A=23; B=18;
A=24; B=19;
A=25; B=20;
A=26; B=22;

Versions used Latest

Add following information:

Runtime Environment: OS Name: Windows OS Version: 10.0.18362 OS Platform: Windows RID: win10-x64 Base Path: C:\Program Files\dotnet\sdk\3.1.100-preview2-014569\

Host (useful for support): Version: 3.1.0-preview2.19525.6 Commit: 5672978d91

.NET Core SDKs installed: 1.1.14 [C:\Program Files\dotnet\sdk] 2.1.202 [C:\Program Files\dotnet\sdk] 2.1.507 [C:\Program Files\dotnet\sdk] 2.1.508 [C:\Program Files\dotnet\sdk] 2.1.509 [C:\Program Files\dotnet\sdk] 2.1.604 [C:\Program Files\dotnet\sdk] 2.1.700-preview-009618 [C:\Program Files\dotnet\sdk] 2.1.700 [C:\Program Files\dotnet\sdk] 2.1.800-preview-009677 [C:\Program Files\dotnet\sdk] 2.1.800-preview-009696 [C:\Program Files\dotnet\sdk] 2.1.801 [C:\Program Files\dotnet\sdk] 2.2.109 [C:\Program Files\dotnet\sdk] 2.2.204 [C:\Program Files\dotnet\sdk] 2.2.300-preview-010067 [C:\Program Files\dotnet\sdk] 2.2.300 [C:\Program Files\dotnet\sdk] 2.2.400-preview-010195 [C:\Program Files\dotnet\sdk] 2.2.400-preview-010219 [C:\Program Files\dotnet\sdk] 2.2.401 [C:\Program Files\dotnet\sdk] 3.0.100-preview5-011568 [C:\Program Files\dotnet\sdk] 3.0.100-preview8-013656 [C:\Program Files\dotnet\sdk] 3.0.100 [C:\Program Files\dotnet\sdk] 3.1.100-preview2-014569 [C:\Program Files\dotnet\sdk]

.NET Core runtimes installed: Microsoft.AspNetCore.All 2.1.9 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All] Microsoft.AspNetCore.All 2.1.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All] Microsoft.AspNetCore.All 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All] Microsoft.AspNetCore.All 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All] Microsoft.AspNetCore.All 2.2.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All] Microsoft.AspNetCore.All 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All] Microsoft.AspNetCore.All 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All] Microsoft.AspNetCore.All 2.2.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All] Microsoft.AspNetCore.App 2.1.9 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 2.1.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 2.2.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 2.2.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 3.0.0-preview5-19227-01 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 3.1.0-preview2.19528.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.NETCore.App 1.0.16 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 1.1.13 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 2.0.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 2.1.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 2.1.11 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 2.2.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 2.2.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 3.0.0-preview5-27626-15 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 3.1.0-preview2.19525.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.WindowsDesktop.App 3.0.0-preview5-27626-15 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 3.1.0-preview2.19525.6 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

To install additional .NET Core runtimes or SDKs: https://aka.ms/dotnet-download


- `dotnet --info` on the machine where app is being run (not applicable for self-contained apps)

.NET Core SDK (reflecting any global.json): Version: 3.0.100 Commit: 04339c3a26

Runtime Environment: OS Name: raspbian OS Version: 10 OS Platform: Linux RID: linux-arm Base Path: /opt/dotnet/sdk/3.0.100/

Host (useful for support): Version: 3.0.0 Commit: 7d57652f33

.NET Core SDKs installed: 3.0.100 [/opt/dotnet/sdk]

.NET Core runtimes installed: Microsoft.AspNetCore.App 3.0.0 [/opt/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.NETCore.App 3.0.0 [/opt/dotnet/shared/Microsoft.NETCore.App]

To install additional .NET Core runtimes or SDKs: https://aka.ms/dotnet-download

- Version of `System.Device.Gpio` package

1.1.0-prerelease.19557.1

krwq commented 4 years ago

(deleting my previous comment since I confused this with Serial Port implementation) which has similarly named member.

cc: @joperezr, @buyaa-n - AFAIR Libgpiod implementation of events is much more reliable than sysfs. You might want to pass Libgpiod driver to GpioController constructor to force it.

nahueltaibo commented 4 years ago

@krwq I'll try your suggestion.

I am also thinking that events might not be exactly what is needed to implement a encoder. The best thing would probably be interrupts, and the firing an event with the TickCount or something like that, but each tick on each pin should be received ASAP.

Do you think there is any way of achieving this?

krwq commented 4 years ago

@nahueltaibo I'm not sure what you use for counting (optocoupler, rotary encoder, something else) but perhaps Something from #705 might be useful for you for counting

buyaa-n commented 4 years ago

@nahueltaibo could be some noise read on the pin, if you are not using pull up/down resistors for the pins I would suggest to use them

nahueltaibo commented 4 years ago

@nahueltaibo I'm not sure what you use for counting (optocoupler, rotary encoder, something else) but perhaps Something from #705 might be useful for you for counting

@krwq I based my code on that. In fact I started with the Quadrature encoder class in that pull request but it is not working because of the same issue. Values seem random. I am assuming that because when the even of a changed pin arrives, it is reading the values of both pi a. But the event might be to old at that time and pins might have changed already.

nahueltaibo commented 4 years ago

@nahueltaibo could be some noise read on the pin, if you are not using pull up/down resistors for the pins I would suggest to use them

@buyaa-n Is there any pin that already have that? Maybe I could try using different pins to read the Quadrature encoder values

pgrawehr commented 4 years ago

@nahueltaibo : All input pins have a 50-60k selectable pullup or pulldown resistor. It can be enabled by setting the pin mode to PinMode.InputPullUp or PinMode.InputPullDown (instead of just PinMode.Input). Note however that this currently doesn't work on the Pi4, until #823 is merged.

buyaa-n commented 4 years ago

@nahueltaibo as @pgrawehr said all GPIO pins has internal pull up/down resisters but might not supported, though you can use external resistors for that. Seems your event active signal is low, so you need to wire pull up resistor from the event pin to power

Frankenslag commented 4 years ago

@nahueltaibo How fast are you turning the encoder or more specially how many pulses per second are you seeing approx.

nahueltaibo commented 4 years ago

@Frankenslag, @krwq I am using this motor from aliexpress for testing.

It rotates at 100 rpm, and the encoder has 11 ticks per rotation. It has a reduction of 1:75. I am not sure if the encoder is attached before of after the reduction.

For the tests I was rotating it manually though, so it was a lot slower than the motor speed.

What I did noticed in one of the tests is that the number of events on each pin got to the same value, ie: after a while, I got 25 rotations on both pins, and then they unsynchronized again.

That made me think that probably I'm not getting the events as soon as they happen, and not in the order they happen.

Could this be the case? If so, is there any way to get something more real time? (like interrupts)

Frankenslag commented 4 years ago

@nahueltaibo

At first glance the loop that polls and waits for an event will be able to be triggered a maximum of 1000 time a second because of the delay in Thread.Sleep(1). However if I look at this then I see that you might only get 64 triggers a second because Thread.Sleep has a certain granularity. These are best case as well because they take no account of the loading of the system and the thread could wake up much later. I am not sure what could make the events come out of order though.

            while (_pinsToDetectEventsCount > 0)
            {
                try
                {
                    bool eventDetected = WasEventDetected(_pollFileDescriptor, -1, out int pinNumber, s_eventThreadCancellationTokenSource.Token);
                    if (eventDetected)
                    {
                        Thread.Sleep(1); // Adding some delay to make sure that the value of the File has been updated so that we will get the right event type.
                        PinEventTypes eventTypes = (Read(pinNumber) == PinValue.High) ? PinEventTypes.Rising : PinEventTypes.Falling;
                        var args = new PinValueChangedEventArgs(eventTypes, pinNumber);
                        _devicePins[pinNumber]?.OnPinValueChanged(args);
                    }
                } catch (ObjectDisposedException)
                {
                    break; //If cancellation token source is dispossed then we need to exit this thread.
                }
            }

I have now updated the code for the encoder locally to include properly implement the encoding but this doesn't disguise that if you want accuracy then perhaps using the GPIO on the PI, or any other non-realtime device could cause issues. I think that, in this case that moving the GPIO and decoding to a microcontroller might be the best. The original intent of the rotary encoder device was as a potentiometer replacement like a volume and tuning control and it seems to work fine at those low rates.

I have also seen it suggested that we use the an alternative hardware function built into the PI for a rotary encoder which may solve the issues but I haven't personally looked into that.

dpsenner commented 4 years ago

In the example of a DHT22 the rising and falling flanks need to be scanned at a rate of roughly 10 microseconds because the flanks rise and fall either after 28 or 70 microseconds.

I tried to implement this against the OnPinValueChanged() api but noted that it is impossible to detect the flanks because of the Thread.Sleep(1). The readings are further delayed by the invocation of the event handler.

I went to implement a very tight loop that calls GpioController.Read(int pinNumber), hoping to call it often enough to detect the rising and the falling flanks. This gladly works in roughly 75% - 85% of the dht22 measurements.

This is a better solution that unfortunately is tied to the UWP (universal windows platform). The idea behind this api is to:

  1. Start the polling in kernel space.
  2. The kernel detects rising/falling flanks with a high resolution and buffers the events.
  3. After some delay or a number of events, user space inspects the buffer and fetches the detected flanks to process them.
  4. Finally, userspace stops the kernel space polling.
pgrawehr commented 4 years ago

PR #914 is hopefully improving the situation greatly. It no longer requires any sleeps and can handle interrupts of several Khz. I haven't figured out how to make it 100% reliable, so still some interrupts may be lost.

@nahueltaibo Could you try to run your code with this patch and see whether it helps?

pgrawehr commented 1 year ago

Closing this old ticket as event handling is (supposedly) fine with the current version. At least I haven't seen any problems with it and I'm using this extensively.