-
-
Notifications
You must be signed in to change notification settings - Fork 7k
Support extra parameter on attachInterrupt()
#4519
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
A very simple example of how this can be used to writing components:
|
Doesnt this break the current API with no extra param? I am not sure if the code works in this case. Have you tested it? Also this adds a lot of overhead, even though it could be very useable. Not sure about that. However I like the compact definitions in If you ask me I'd rethink the whole API in general. I consumes quite some flash, even though only a singe PinInterrupt is used. And I'd also change those:
to
So you have bit0 indicate falling, bit1 rising and both together a change. |
The code looks good to me. The compatibility wrapper is a clever way to still support the existing parameterless version (@NicoHood, I think that still works), though it might be better for performance to not have the extra function call. One could instead store The introduction of |
Has anyone tested the performance impact? |
Thanks for the feedback. I'll split the @PaulStoffregen: How can I measure the performance impact? @matthijskooijman, what do you think of using a poison value on voidFuncPtrParam (Maybe
The tradeoff is that ISR functions will take a little more flash to handle the condition inline @NicoHood, Although I agree the current implementation wastes some flash with unused interrupts, I really don't know how to rewrite it in a more efficient way. I'm taking suggestions :D
About the constant values, I agree with you, that makes more sense, but looks like the values are defined by the hardware |
That sounds reasonable, checking a pointer is probably a lot faster than calling a function. There is still overhead, of course, from loading the parameter and checking it. As for measuring the overhead - disassembling and counting cycles would work, or doing some kind of runtime timekeeping (though that's more tricky to get accurate). |
Wait, that's what I proposed with letting "NULL" to mean "no parameter". However, this will remove one value from the available parameter values, which I think is not a good plan (there is a fair chance that a user will pass that value anyway, especially if the void* argument is used to pass integers instead of pointers, and because function and data pointers can overlap). What I thought you were saying, was to use a poisoned as the function pointer value, which means that the parameter contains the actual (parameterless) function pointer to call. This is more reliable because valid values for the function pointer array are only pointers into the flash, and never pointers into RAM or plain integers. |
You are right. Something like this?
|
Yup, that's what I had in mind indeed. You should probably get |
The current code is very verbose and a painful to maintain (Change ISR implementation in 20 different places? No Thanks!). (This was originally part of arduino#4519, but we all agreeded it deserved it's own PR)
I've made a separate PR for the |
I haven't compared the performance yet, but the memory use on a pro micro (atmega32u4) is
|
Current code:
ISR: Executes 3 instructions (7 cycles) from code + 34 instructions of boilerplate
My code, using
|
91a68d9
to
847bc9e
Compare
@paulo-raca, thanks for the thorough comparison :-D I'm actually quite surprised about the mess the compiler makes when compiling my suggestion. There's a lot of overhead (12 instructions from my count) from storing and reloading variables on the stack, when they could just as well be kept around in registers (AFAICS all of the call-clobbered registers are pushed by the boilerplate, so they should be available). I wonder if it would be worth implementing this in hand-coded assembly perhaps... Also interesting is that compatfunction has something similar going on, which is even more silly since it stores r24 and r25 on the stack and reloads them immediately to the same registers twice. It's almost as if you compiled without optimizations enabled? |
You are right, I completely forgot to enable optimizations :/ I'll update it later (I hope most of the ISR boilerplate goes away with it) |
@paulo-raca, that suggests you compiled with avr-gcc manually? For a fair comparison, it would seem best to actually use the Arduino IDE for compilation, so you get the "official" compiler flags and compilation process. To extract the .elf file from the IDE for disassembly, just look through the verbose compilation output to find where it is stored. |
@matthijskooijman, I updated the previous comment with optimized disassembly generated from the IDE. Turns out that using |
I looked up the number of cycles in addition to the number of instructions, and I took the liberty of adding them in your comment. I believe the last code snippet could be optimized (by hand-coding in assembly, or perhaps by making the compiler smarter) to remove one To me the So, your original approach seems the most efficient. Any users that do not need this new parameter feature will pay an additional 7 cycles. The current latency is 17*2+7 (assuming all 17 preamble boilerplate instructions are push at 2 cycles per instruction) 41 cycles. This new feature brings a measureable, but not enormous, cost. On additional option we could consider is to simply pass the parameter, whether it is valid or not. Knowing AVR's calling conventions, we know that passing a parameter will simply occupy two registers. If the called function does not expect these parameters, it will just ignore them, without any additional problems. I don't see any way to actually verify this during compilation (e.g. by writing things in such a way that the compiler will see this optimization opportunity and exploit it, and/or an error be thrown if the compiler at some point handles calling things differently), but that does not need to be a problem necessarily (a decent comment should be written, though, to ensure this does not break when ported to other platforms). If you would just pass the parameter, the additional overhead is two |
I like the idea! |
847bc9e
to
c926025
Compare
I've implemented it, and everything is looking good. Thanks you for help, @matthijskooijman |
Is there anything else I can do about this? As soon as it gets settled, I plan to do the same on Due |
Hi @paulo-raca , |
c926025
to
c26f0ca
Compare
Rebased. Thanks! |
It's been a while... Should we merge or discard? |
On ARM Cortex M4, invoking that callback is 5 vs. 4 instructions. The void* parameter would be passed in a register, so a function(void) could be called as function(void*). \\ The void* parameter works fine with lamdas, e.g.:
|
I planned to follow up this patch with an ARM versions, but seems like it's not going anywhere :( Anyway, I find that 5 instructions is still a pretty low overhead for what (I think) is a pretty useful feature. Not sure about the lamdas, but I think it probably won't support lamdas with a capture list -- Just guessing, can you try it? |
No, you can't use lambda captures. They need storage that a function pointer doesn't have (you would need to use std::function). Lambdas without captures decay to function pointers. The Cortex M4 overhead is 1 extra instruction. :-) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sort of dropped off my radar, but a post on the mailing list got my attention again.
I just had another look at the code, and it looks good and acceptable for mergingm to me. @cmaglie @facchinm, would you have a look?
@paulo-raca, could you perhaps update your comment with assembly comparison with the assembly for the current approach?
This would be extremely useful for me as well. It works well with C++ since I can just pass the this pointer as a parameter. I didn't know about this thread. In my implementation for the ESP32 I also pass the interrupt (pin) number to the handler, though with a bit more overhead I could live without that. It just means that I need to extract the pin number from my class instead. |
When writing an arduino library, it is difficult to connect interrupt handlers with the component instance that should be notified. In C, the common pattern to fix this is to specifying a `void*` parameter with the callback, where the listener can store any context necessary. This patch adds the new function `attachInterruptParam()` to implement this pattern.
@matthijskooijman, the current approach adds 2 instructions / 4 cycles on AVR: Before:
After:
On an Arduino Pro Micro (Atmega32U4) it adds 56 bytes of Flash and 10 bytes of RAM |
c26f0ca
to
9eb8ba0
Compare
@aaronw2, thanks for bringing it up again :) When it gets merged I'll also make patches for ESP8266, ESP32 and Due |
@paulo-raca |
I haven't tried, but I believe it does. According to @tni, the overhead on arm is only 1 instruction. |
Also, even if ARM needs a bit more instructions, that's probably less of a problem given the usually faster MCUs. And since this is just an implementation detail, that does not affect the API, this should not hinder the changes on AVR. |
Ok, may you explain why calling a function with extra parameters is safe in the AVR case?
The point is how much is this "bit more" on the ARM side, so I'm going to give it a try to check it out. |
On AVR, the extra arguments are sent on registers The big red warning mostly applies to ABIs where arguments are stored on the stack. |
In C, the common pattern to fix this is to specifying a void* parameter with the callback, where the listener can store any context necessary. This patch adds the new function attachInterruptParam() to implement this pattern. This is a port of arduino/Arduino#4519, but for Arduino Due.
I've just written an equivalent PR for Arduino Due. The same trick also worked on ARM, and the ISR overhead is only 2 instructions. |
Is this really true? I believe the caller has to clean up the stack or else one could not realise functions with variable count of parameters like At least that's how it's done in C on x86. |
@dplasa, this is true for variadic functions, but the compiler might use a different calling convention on them. E.g., on AVR:
compiles to
On x86 there is a number of calling conventions where the callee should clean up the task: https://en.wikipedia.org/wiki/X86_calling_conventions#Callee_clean-up |
Still my argument holds :)
If an void ISR() without argument was called the compiler would probably just rcall the ISR code when the interrupt occurs. If we change this to ISR(void* arg) then arg is either passed by register which is no deal to an ISR(void) function which just ignores the register... |
Hello, There is also a version for SAM: arduino/ArduinoCore-sam#44 If these are merged in the official API, I'll also port this feature for ESP8266 / ESP32 |
I'm not aware of any Arduino compatible core that would have a problem with this. In every ABI I see supported, the first argument is always passed in a register. |
I'm not aware either, but it's better to make it very explicit, since it is an abuse of the ABI |
I might also add that in C, it is always the caller's responsibility to clean up the stack from any parameters that are passed. Passing an extra parameter will not cause a function call to fail. If passed on a stack, parameters are passed in a right to left order so that extra parameters on the stack will not cause a function to fail either. If the Pascal calling convention were used instead of C then this would be an issue since it is up to the callee to clean up the stack, but since this is C it is up to the caller to clean it up. |
Ok, so what if I replace the comment with this?
|
That sounds good to me. |
When writing an arduino library, it is difficult to connect interrupt handlers with the component instance that should be notified.
In C, the common pattern to fix this is to specifying a
void*
parameter with the callback, where the listener can store any context necessary.This patch adds the new function
attachInterruptParam()
to implement this pattern.