Forum Rule: Always post complete source code & details to reproduce any issue!
Page 2 of 2 FirstFirst 1 2
Results 26 to 35 of 35

Thread: Lightweight C++ callbacks

  1. #26
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,917
    To use captured lambdas attachInterupt would need to have a std::function API. Fortunately this is not difficult to achieve. Copy the attached files to your sketch folder then this will work as intended:
    Code:
    #include "attachInterruptEx.h"
    
    void setup()
    {
        while(!Serial){}
    
        for (int pin = 2; pin <= 9; pin++)
        {
            pinMode(pin, INPUT_PULLUP);
            attachInterruptEx(pin, [pin] { Serial.println(pin); }, FALLING); // capture a copy of pin 
        }
    }
    
    void loop(){}
    See https://github.com/luni64/TeensyHelpers for more examples
    Attached Files Attached Files

  2. #27
    Senior Member PaulStoffregen's Avatar
    Join Date
    Nov 2012
    Posts
    27,065
    Quote Originally Posted by luni View Post
    To use captured lambdas attachInterupt would need to have a std::function API.
    But then we would burden all programs and libraries using attachInterupt() with std::function's overhead!

  3. #28
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,917
    Sure, I don't propose to do this on the normal attachInterrupt. But in case someone needs it one can just use attachInterruptEx(). It peacefully coexists with the standard version.

    Did you find time to look at the proposed callback system in #23?

  4. #29
    Senior Member PaulStoffregen's Avatar
    Join Date
    Nov 2012
    Posts
    27,065
    Tried a quick measurement just now, and I must admit, the added overhead isn't nearly as much as I has expected.

    Looks like attachInterrupt() is taking 165ns and attachInterruptEx() takes 174ns.

    Measurement was from rising edge input to observed pin change by digitalToggleFast(), so probably includes a few instructions to set up registers.

    Click image for larger version. 

Name:	file.png 
Views:	14 
Size:	32.8 KB 
ID:	29260
    Last edited by PaulStoffregen; 09-01-2022 at 12:11 AM. Reason: added scope screenshot

  5. #30
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,917
    I didn't expect much performance penalty but only 6 cycles is impressive. It also includes the additional indirection introduced by attachInteruptEx which adds a relay function which then calls the user callback. Those STL guys defintely know their trade.

    Drawback: the needed <functional> header it is a bit expensive memory wise (some 10kB).

  6. #31
    Senior Member
    Join Date
    Dec 2016
    Posts
    183
    Quote Originally Posted by PaulStoffregen View Post
    Tried a quick measurement just now, and I must admit, the added overhead isn't nearly as much as I has expected.

    Looks like attachInterrupt() is taking 165ns and attachInterruptEx() takes 174ns.

    Measurement was from rising edge input to observed pin change by digitalToggleFast(), so probably includes a few instructions to set up registers.

    Click image for larger version. 

Name:	file.png 
Views:	14 
Size:	32.8 KB 
ID:	29260
    Thanks for the analysis and the quick response to my "quick and dirty" question.

  7. #32
    Senior Member
    Join Date
    Dec 2016
    Posts
    183
    Quote Originally Posted by luni View Post
    I didn't expect much performance penalty but only 6 cycles is impressive. It also includes the additional indirection introduced by attachInteruptEx which adds a relay function which then calls the user callback. Those STL guys defintely know their trade.

    Drawback: the needed <functional> header it is a bit expensive memory wise (some 10kB).
    Thanks.

    Given that I still use some LC's that is something to consider. I hope it's flash memory and not RAM?

  8. #33
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,917
    Quote Originally Posted by AlainD View Post
    Thanks.

    Given that I still use some LC's that is something to consider. I hope it's flash memory and not RAM?
    It is both, best to give it a try. IIRC the LC is using nanoLib instead of newLib which brings down the memory requirements significantly

  9. #34
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,917
    @PaulStoffregen In case you are still interested: I meanwhile got capturing lambdas going as well without using std::function or dynamic memory of course :-). Looks like everything possible for std::function callbacks is possible with my callback helper. Syntax is the same as for std::function callbacks. Still experimental but I'm confident that it could be made into a robust tool. Code and examples can be found here: https://github.com/luni64/cb

    Here the current user API (using the simple PIT class from #21 as test implementation)
    Code:
     // use free function callback
     timer.begin(onTimer, 250'000);
    
     // use lambda to attach member function as callback
     timer.begin([] { test.myFunc2(); }, 250'000);
    
     // use member function pointer and instance to attach callback. Syntax won't get nicer. "test.myFunc1" is not possible in c++ (maybe with some makro if really necessary) 
     timer.begin(&Test::myFunc1, &test, 250'000);
    
     // attach non capturing lambda as callback
     timer.begin([] { Serial.println("called lambda"); }, 250'000);
    
     // attach capturing lambda as callback
     // this is especially useful for embedding the callback provider (e.g. IntervalTimer) in a user class without the static trick. 
     int n = 42;
     timer.begin([n] { Serial.println(n); }, 500'000 );
    Quick explanation:
    For each lambda the compiler basically generates an anonymous class with an operator(). The operator() executes the lambda code (the code between the braces). The captured variables (those between the brackets) end up as members of this class. Therefore, the size of the autogenerated class equals the size of the captured variables.

    If you want to store the generated class (i.e. the lambda), e.g. for later use as callback, you'd normaly new it up on the heap which doesn't require to know its size beforehand (I assume this is why std::function uses dynamic memory). If we want to avoid dynamic memory allocation, we can statically preallocate some buffer and use `placement new`instead of `new`to construct the object in this buffer. Of course the size of the preallocated buffer sets a limit on how much variables can be captured per callback. Usually you'd capture some numeric values or the 'this' pointer only. I configured the code to reserve 16 bytes for lambda parameters per callback. It can be increased to any value if one is willing to accept the increased memory footprint. Good thing is, the code will generate a compiletime error if the user captures too much parameters

    The whole thing is encapsulated in a CallbackHelper class which handles all of the details. A library writer can use the high level CallbackHelper API to generate callbacks from the various passed in types (function pointers, lambdas etc). He doesn't need to mess with the detais. See the PIT class for an example.

    Let me know if you actually want to use it and need a robust version with destructors and error handling. Otherwise I'd call it a very nice learning project and stop here.
    Last edited by luni; 09-02-2022 at 10:08 PM.

  10. #35
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,917
    There is some news on this:
    I meanwhile found some very interesting code in a (german) c++ forum https://www.c-plusplus.net/forum/top...signal-slot/17, which shows how to implement a tiny drop in replacement of std::function which doesn't use dynamic memory allocation. Like in my CallbackHelper, it stores the passed in objects (function pointes, lambdas, functors...) in a preallocated buffer. I defaulted the buffer size to 16 byte which is plenty, but it can be adjusted from user code if necessary.

    I had to tweak the code a bit to compile with std::c++11, but now it is pretty generic and works with a lot of boards (XIAO, Nucleo, ESP32, SAMD...). However, it doesn't compile with the old GCC5.4 (misses some utilities). So, to try it on a Teensy one currently needs to use Teensyduino beta version. Here the link to the library: https://github.com/luni64/staticFunctional (actually it is a one header only library. So, just copy the file "staticFunctional.h" to your sketch folder and you are good to go)

    The repo contains some examples, and, just for the fun of it a reworked version of the IntervalTimer. Adopting IntervalTimer only required to change the declarations of the begin(...) functions from the explicit begin(void(*func)(), ....) to begin(callback_t func,....). callback_t is defined by "using callback_t = function<void(void)>;" That was basically all I needed to change. It shouldn't break any existing code since it still accepts void(*)() callbacks.

    Here a simple usage example for the library.
    Code:
    #include "staticFunctional.h"
    using namespace staticFunctional; // save typing
    
    class MyClass
    {
     public:
        void nonStaticMemberFunction() {
            Serial.printf("nonStaticMemberFunction i=%d\n", i);
        }
    
        void operator()() const {
            Serial.printf("functor 2i=%d\n", 2*i);
        }
        int i = 42;
    };
    MyClass myClass;
    
    void freeFunc(){
        Serial.println("free function");
    }
    
    void setup()
    {
        while (!Serial) {}
    
        function<void(void)> f; // function taking no arguments and returning nothing
    
        f = freeFunc;
        f();
    
        f = [] { Serial.println("lambda expression"); };
        f();
    
        f = [] { myClass.nonStaticMemberFunction(); }; // non static member function
        f();
    
        f = myClass; // functor, f uses operator ()()
        f();
    }
    
    void loop(){
    }
    
    // prints:
    // free function
    // lambda expression
    // nonStaticMemberFunction i=42
    // functor 2i=84

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •