c++embeddedembedded-linuxlibgpiod

Using C++ libgpiod to calculate interval and speed between events


I'm using libpgidon C++ in order to calculate RPM from GPIO events.

The idea is to read all RISING_EDGE events from a given GPIO line and increment a counter (pulse counter). The counter has 3 properties:

typedef struct counter_t
{
    unsigned long count;           // Incremented on every RISING_EDGE pulse
    unsigned long start_timestamp; // Timestamp of the first event pulse read
    unsigned long last_timestamp;  // Timestamp of the last event pulse read
}

So, on every event read from gpiod I do:

auto event = line.event_read();
if (event.event_type == GPIOD_LINE_EVENT_RISING_EDGE)
{
    counter->count++;
    if (counter->start_timestamp == 0) counter->start_timestamp = event.timestamp;
    counter->last_timestamp = event.timestamp;
}

That runs on a thread that listen for new events and updates the counter. All fine working - and I'm getting the counter being incremented properly.

My problem arise at RPM calculation.

To do it, I run another thread at a fixed interval of 100ms, that reads the counter, calculate the RPM and reset the counter, as follows:

void thread_loop()
{
    std::cout << "Start thread" << std::endl;

    bool first_run = true;

    while (true)
    {
        if (!first_run)
        {
            // Get interval
            unsigned long interval = counter->last_timestamp - counter->start_timestamp;

            // Calculate speed
            double speed = (counter->count / interval) * 600000000;  //calculates speed

            // Print results
            std::cout << "SPEED: " << speed << std::endl;
            std::cout << "COUNTER: " << counter->count << std::endl;
            std::cout << "COUNTER START TIMESTAMP: " << counter->start_timestamp << std::endl;
            std::cout << "COUNTER: LAST TIMESTAMP: " << counter->last_timestamp << std::endl;
            std::cout << "INTERVAL: " << interval << std::endl;

            // Reset counter
            counter->reset(); // Sets count, start_timestamp and last_timestamp to 0
        }

        first_run = false;

        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

I am getting inconsistent speeds, as for the given output:

SPEED: 0
COUNTER: 40
COUNTER START TIMESTAMP: 13953904635016
COUNTER: LAST TIMESTAMP: 13956976823743
INTERVAL: 3072188727

From my understanding, all libgpiod events are nanosecond based, so 3072188727 is equivalent to 3072 milliseconds, but my thread is running at 100ms intervals (I expect not exactly that value, but something around this, not 30 times). There is nothing else running in the machine, no overload on the code.

Also, the speed calculation is always zero.

I imagine there is something wrong with my understanding or conversion of the libgpiod conversion, but no idea what is wrong. I've searched the libgpiod docs and cannot find any detailed explanation.

So, here are my questions:


Solution

  • Part of your problem is that the documentation you reference is libgpiod v2, but the line.event_read() call you are using is from libgpiod v1. And you are mixing C and C++, as GPIOD_LINE_EVENT_RISING_EDGE is from the C API. You might want to pick one API and stick to it.

    a) With libgpiod v2, you can select the clock source using gpiod::line_settings::set_event_clock(). By default it is gpiod::line::clock::MONOTONIC, i.e. CLOCK_MONOTONIC, which is what you want for this application. For libgpiod v1 the clock source depends on the kernel version - it was CLOCK_REALTIME prior to Linux 5.7, and CLOCK_MONOTONIC from 5.7 onwards.

    b) Given CLOCK_MONOTONIC, the difference between two timestamps is the interval in nanoseconds. Using CLOCK_REALTIME for this application could introduce errors, as it is susceptible to corrections to system time.

    c) Firstly, your multiplier is wrong. The interval is measured in nanoseconds, so the multiplier should be 60000000000 nanoseconds/minute (6e+10), not 600000000 (6e+8).

    Your interval calculation is wrong as start_timestamp marks the first ever interrupt, not the beginning of the interval, whereas you reset the counter every interval.

    Also reading the counter, calculating, printing and then resetting the counter introduces a potential error - the count may have have incremented in the background by the other thread. Fixing that could involve adding functions to increment and read/reset the counter, with locking to prevent the threads contending. But better to never reset the count - have the thread_loop() store the previous count and determine the number of new counts since the last interval.

    So, to address timestamp and count issues, drop the start_timestamp from struct counter_t, and have thread_loop() keep a snapshot of the previous value of counter and compare it with the latest. The difference in the count and timestamp will give you the average RPM during that interval.

    Your "always zero" problem is integer math - (counter->count / interval) is 0 in integer math, given interval > count, which it almost certainly will be.

    You need to cast at least one of them to a float to enable floating point math:

    double speed = (double(count) / interval) * 60000000000ULL;
    

    Alternatively you could keep everything in integer math by rearranging the order so the numerator is larger than the denominator:

    unsigned long speed = (count * 60000000000ULL) / interval;
    

    That could be prone to overflow if count gets large, but for reasonable interrupt rates that seems unlikely.

    Also note that libgpiod v2 provides a seqno on the events through gpiod::edge_event::line_seqno() so you don't need to maintain your own counter - just copy that one. Using that would also correct for any lost interrupts if your app is blocked to the point that the kernel event buffer overflows. It won't help if the interrupts arrive faster than the kernel interrupt handler can deal with them, but you aren't likely to be dealing with RPMs that high.

    Finally, you don't show how you requested the line, but if you request to only receive rising edge events then there is no need to check the event type when you read the events. This example from libgpiod v2 does that, and you can do similar with v1.