Fast code in ISR - Page 1
Forum section: electronics / general

author: Jester
author_rank: Frequent Contributor
timestamp: July 23, 2023, 03:04:29 pm
content: I'm tweaking some existing code and want to avoid slowing this section of code down because it's in an ISR. This question pertains to the GPIO_SET_STEP; line of code below.The I/O pin being manipulated is used to drive the step pin of an ac servo controller. Presently the active state of this pin is #defined, so pre compile time defined. Need to make the active state of this pin user configurable. The user configurable aspect is now coded and a bool invertStep gets set or cleared during the initialization portion of the code and is then available for the code below. I want to avoid adding an if statement in the ISR where GPIO_SET_STEP; is (if possible). The actual pin manipulation code required is shown in the comments. Is there a way to substitute the required code and avoid the if statement in the ISR?EDIT: uC = TMS320F280049Code:inline void ServoDrive :: ISR(void){switch( this->state ) {case 0:if( this->desiredPosition < this->currentPosition ) {GPIO_SET_STEP; // clear if inverted and set if not inverted GpioDataRegs.GPACLEAR.bit.GPIO0 = 1; OR GpioDataRegs.GPASET.bit.GPIO0 = 1; /// this linethis->state = 2;}else if( this->desiredPosition > this->currentPosition ) {

author: langwadt
author_rank: Super Contributor
timestamp: July 23, 2023, 03:27:03 pm
content: have you looked at the assembly code and determined that it actually makes enough of a difference to be worth worrying about?I guess you could make a pointer to the set/clear address, but that also needs to be read so it might be worse

author: DavidAlfa
author_rank: Super Contributor
timestamp: July 23, 2023, 03:42:01 pm
content: Why so obsessed to avoid the if/else?The switch will make a branch anyways, one more won't hurt.Lacking actual code, first do what you need, then optimize.If invertStep is #defined then you can simply do:Code:GpioDataRegs.GPACLEAR.bit.GPIO0 = invertStep && 1;OrCode:GpioDataRegs.GPACLEAR.bit.GPIO0 = !invertStep;Though I barely understood the message.

author: radiolistener
author_rank: Super Contributor
timestamp: July 23, 2023, 03:55:24 pm
content: Quote from: Jester on July 23, 2023, 03:04:29 pmYes. It is possible, you can have two version of ISR handlers for each if statement state and write proper handler into a vector table when statement state is changed. But be careful, because interrupt may happens when you update vector table.Additional suggestions to improve performance:1) Do not use OOP and objects, because it requires to involve object pointer as context and as result needs to do additional operations2) Enable max speed optimizations in your compiler3) If your ISR code uses floating point operation, make sure that hardware acceleration for floating point is enabled.

author: Jester
author_rank: Frequent Contributor
timestamp: July 23, 2023, 05:30:17 pm
content: Quote from: radiolistener on July 23, 2023, 03:55:24 pmThat's interesting. Are you implying that if I were to rewrite this as a conventional C function that it would run a tiny bit quicker?I have actually been converting other portions of this code to conventional C for simplicity.

author: langwadt
author_rank: Super Contributor
timestamp: July 23, 2023, 06:03:51 pm
content: Quote from: radiolistener on July 23, 2023, 03:55:24 pmare you sure? depending on platform I'm not sure a pointer makes much of a difference

author: jnk0le
author_rank: Regular Contributor
timestamp: July 23, 2023, 06:10:53 pm
content: Unless virtual inheritance is used, this "OOP" code should perform exactly the same as C, given that all variables are kept in struct.Otherwise "simple C" approach could get worse like in this thread:https://www.eevblog.com/forum/microcontrollers/stm32g431-how-can-it-be-that-the-amount-of-lines-of-code-in-main()-affect-isr/(globals:2.56us vs structure:1.96us)

author: Doctorandus_P
author_rank: Super Contributor
timestamp: July 23, 2023, 06:55:20 pm
content: I've always been uncomfortable with an ISR as a part of a class.Classes are built around having the ability to have multiple instances around, while having multiple instances of an ISR in general does not make much sense.In a lot of ways the code looks "cleaner" when the ISR is part of a class, but I'm always struggling to attempt to do so without generating compiler errors or warnings, especially when compiling under the later C++ standards.When writing the ISR as a generic C function, you can drop all the references to the this pointer, and use generic simple variables instead. Global variables are also "ugly", but you can use either automatic or static variables in your C ISR function.

author: nctnico
author_rank: Super Contributor
timestamp: July 23, 2023, 07:01:15 pm
content: Quote from: jnk0le on July 23, 2023, 06:10:53 pmIndeed. With OOP the C++ compiler can do a much better job at optimising compared to functions that deal with function pointers. Also, typically ARM code fetches constants from PC+offset so you'll be looking at a pointer operation anyway because it is the fastest solution.

author: jnk0le
author_rank: Regular Contributor
timestamp: July 23, 2023, 07:16:16 pm
content: Quote from: Doctorandus_P on July 23, 2023, 06:55:20 pmIt's usually something like:Code:ServoDrive instance1();extern "C"void TIM16_IRQHandler(void) {TIM16->SR = 0; // clear pendinginstance1.ISR();}

author: ejeffrey
author_rank: Super Contributor
timestamp: July 23, 2023, 07:43:34 pm
content: Quote from: Jester on July 23, 2023, 05:30:17 pmIf you have C++ virtual function calls and you replace them direct C calls, it may be faster, but if you replace C++ objects with actually equivalent C it will probably be slower. That's because C++ compilers are much more able to statically analyze the types and replace virtual function calls with inline code to the concrete method than they are to do the same with C using function pointers.

author: ajb
author_rank: Super Contributor
timestamp: July 23, 2023, 07:45:40 pm
content: Since the inversion is configured at startup and presumably fixed, the fastest "branchless" implementation would be to set up a pointer to whichever register should be used, as langwadt suggested in the first reply.A branchless implementation of the inversion logic in the ISR would just be a more complicated way of doing the same thing. You might do something like:Code:*(uint32_t*)(( (!outputInverted) * (uint32_t)&outSet ) + ( outputInverted * (uint32_t)&outClr )) = pinMask;orCode:const int32_t offset = ((uint32_t)&outClr) - ((uint32_t)&outSet);*(uint32_t*)((uint32_t)&outSet + (offset * outputInverted)) = pinMask;Some ugly casts are necessary to allow arbitrary arithmetic on pointer addresses, the specific types cast to will depend on the platform and register sizes. This incurs 3-4 arithmetic operations not counting the final assignment, plus some of the arguments may need to be loaded with separate instructions, versus a single assignment from a constant. The harder question is if that will be faster than a branch, which depends entirely on the platform. An equally important question is what you care the most about: minimum latency, most consistent latency, or minimum time spent in the ISR?

author: SiliconWizard
author_rank: Super Contributor
timestamp: July 23, 2023, 07:53:08 pm
content: Use uintptr_t to cast pointers to integers and conversely in a portable way.

author: NorthGuy
author_rank: Super Contributor
timestamp: July 23, 2023, 09:07:27 pm
content: If you can limit your operations to pin toggling (that is pin always change the state when you manipulate it), then you simply toggle it (Many MCUs have registers to toggle pins). In such case, the polarity is only needs to be set once at the beginning.Otherwise, if you MCU has SET and CLR registers for the pin, then store their addresses in pointer variables at the beginning, sayCode:if (want_to_invert) {clear_reg = &SET;set_reg = &CLR;} else {set_reg = &SET;clear_reg = &CLR;}Then you simply write a mask into the registers using these pointers.Code:*set_reg = MASK; // to set (clear if inverted)*clear_reg = MASK; // to clear (set if inverted)This is probably the fastest.But the most important thing is this: How fast does it need to be? How fast it is now? Until you know the answer to these questions, trying to make it faster is simply waste of your time.Then, try to optimize your whole ISR. Often it'll give you more gain than the micro level.

author: radiolistener
author_rank: Super Contributor
timestamp: July 23, 2023, 10:42:56 pm
content: Quote from: Jester on July 23, 2023, 05:30:17 pmYes, but it doesn't means that any code written in C is faster, you're needs to write it properly in order to get max speed. If you wanna even higher speed optimization it requires to write in ASM, but it takes a lot of time and needs to take into account a lot of things, so it has a sense only if you're really want to be on the cutting edge of maximum performance. Using ASM will be time waste if you write usual code which is not performance critical.Quote from: langwadt on July 23, 2023, 06:03:51 pmyes, when even 1 extra CPU tact is critical it makes a big difference. But for a usual task where speed is not critical there is no big difference if you're using plain C or C++ with objects.Using low level language is much more complicated and needs to keep in mind a lot of thigs, but it allows to be more flexible. On the other hand higher level language make things more easy and simple, but less flexibility for performance tuning. Both ways have pros and cons, and the best solution depends on your actual needs.

author: langwadt
author_rank: Super Contributor
timestamp: July 23, 2023, 11:03:28 pm
content: Quote from: radiolistener on July 23, 2023, 10:42:56 pmyou don't know it if it takes more or less cycles unless you try it ...

author: radiolistener
author_rank: Super Contributor
timestamp: July 23, 2023, 11:11:13 pm
content: Quote from: langwadt on July 23, 2023, 11:03:28 pmif you know that it should do more things (like get object context pointer, etc), then you can be sure that it cannot take less cycles and will require more cycles. At best, the same amount of cycles, but only with a successful combination of circumstances. If it was written properly of course. Usually it will be slower and as practice shows, OOP code requires more memory, which is bad for MCU, where code FLASH is just 128 kB and RAM is just 4-32 kB.Of course you can write functional code in a plain C which can be slower than object oriented code in C++. And can write functional code in a plain C which will be more faster. It all depends on how you write it. The main idea here is to use lower level coding in order to make sure that performance critical code (such as ISR handler) doesn't use extra operations (such as object context pointers, vtables, etc).For example, I just bought Chineseblackpill board with STM32G431CBU6and played with it's DAC. Adding just one assign operation in ISR handler leads to the fact that I need to reduce DAC sampling rate for about 300 kHz so that the TMR ISR handler has time to update the DAC data in time. So I get 3.8 MHz DAC sample rate without that assignment operation and 3.5 MHz DAC sample rate with adding just one assignment operation.Tried to generate sine on itWorks pretty good, it's 12 bit DAC produce pretty clean sine.

author: Jester
author_rank: Frequent Contributor
timestamp: July 23, 2023, 11:29:45 pm
content: Quote from: NorthGuy on July 23, 2023, 09:07:27 pmMy goal is to not add any additional time while in the ISR. Latency is not important.Your suggestion seems like what I'm looking for.Bear with me I'm not a programmer. If I understand what your suggesting I need to determine the addresses of:address 1: GpioDataRegs.GPACLEAR.bit.GPIO0 = 1address 2: GpioDataRegs.GPASET.bit.GPIO0 = 1Assuming:1) I have a global bool InvertStepPin that is set at startup and does not changeThen in the initialization code I use:uint16_t clear_reg, set_reg;Code:if (InvertSetPin) {clear_reg = &GpioDataRegs.GPASET.bit.GPIO0 = 1;set_reg = &GpioDataRegs.GPACLEAR.bit.GPIO0 = 1;} else {set_reg = &GpioDataRegs.GPASET.bit.GPIO0 = 1;clear_reg = &GpioDataRegs.GPACLEAR.bit.GPIO0 = 1;}assuming InvertSetPin is true, then this line: GPIO_SET_STEP; // executes: GpioDataRegs.GPACLEAR.bit.GPIO0 = 1 if InvertStepPin is true, or exceutes: GpioDataRegs.GPASET.bit.GPIO0 = 1 if Invgets replaced with: set_reg;Does that make sense?Did I get the syntax correct?

author: langwadt
author_rank: Super Contributor
timestamp: July 23, 2023, 11:50:26 pm
content: Quote from: radiolistener on July 23, 2023, 11:11:13 pmmaybe..Quote from: radiolistener on July 23, 2023, 11:11:13 pmsure, but that doesn't show that one language will generate more code than the other

author: HwAoRrDk
author_rank: Super Contributor
timestamp: July 24, 2023, 12:18:18 am
content: Quote from: Jester on July 23, 2023, 11:29:45 pmYou'll probably find that doesn't work, because it's likely that the.bit.GPIOxmembers are bitfields, and you can't take the address of a bitfield with the '&' operator. You'll need to use.allwith a bitmask as suggested.

author: ejeffrey
author_rank: Super Contributor
timestamp: July 24, 2023, 01:18:04 am
content: Quote from: radiolistener on July 23, 2023, 11:11:13 pmIn the code discussed here, there is no reason C++ code using objects should need to do any more than a C function. If there is no state data, the C++ code won't have to fetch it and if there is state data you would have to fetch it in C as well.There are constructs in C++ that do have overhead, and some that have surprisingly high overhead (exceptions and RTTI being the usual examples). But just putting your data in objects isn't usually one of them, and nothing about the specific code shown suggests that there would be any (performance) advantage to rewriting in C.

author: NorthGuy
author_rank: Super Contributor
timestamp: July 24, 2023, 01:30:51 am
content: Quote from: Jester on July 23, 2023, 11:29:45 pmNot exactly. The pointer must point to whole bytes, so you doCode:uint16_t *clear_reg, *set_reg; // pointers, not integersif (InvertSetPin) {clear_reg = &GpioDataRegs.GPASET; // whole registers, not bitsset_reg = &GpioDataRegs.GPACLEAR;} else {set_reg = &GpioDataRegs.GPASET;clear_reg = &GpioDataRegs.GPACLEAR;}...*clear_reg = 1 << pin_number;*clear_reg = 0x01; // clearing (setting if inverted) pin 0

author: radiolistener
author_rank: Super Contributor
timestamp: July 24, 2023, 01:59:24 am
content: Quote from: ejeffrey on July 24, 2023, 01:18:04 amnobody said that any kind of rewriting code in C is enough to give you speed advantage. It depends on how you rewrite it. You can use object oriented style and objects in C code, but it doesn't means that it will run faster. But usual functional C code style has better performance and less memory usage than object oriented C++ code style.The same thing with ASM, rewriting code in ASM doesn't means that it give you any performance advantage. On the contrary, in most cases your first code rewritten in ASM will be worse than code in C. But ASM allows you more ways for optimizations and if you take into account all details and utilize all available options in ASM, then you can get the code which will be faster than code written in any high level language. But it will be much more complicated to write it in ASM and such ASM code will be almost non-readable for human.

author: ajb
author_rank: Super Contributor
timestamp: July 24, 2023, 02:14:15 am
content: Quote from: NorthGuy on July 24, 2023, 01:30:51 amIf you really want to retain the bitfield syntax, you could find the type definition for the bitfield from the device header files and use that as the pointer type. That would be sort of silly for sequential gpio bits, but might be more valuable for a more complex peripheral, like switching between UARTs or something.

author: cv007
author_rank: Frequent Contributor
timestamp: July 24, 2023, 03:33:59 am
content: As already mentioned, the use of a bitfield for a SET/CLR/TOGGLE type register makes no sense. You end up doing a read/modify which is not needed. The bitfields lead you to believe you are acting on a single bit, but that is not what actually happens when the compiler generates the code.GpioDataRegs.GPASET.bit.GPIO0 = 1; //read, modify, write, although the write is atomic, the read/modify is not neededGpioDataRegs.GPASET.all = 1<<0; //write pin bitmask, no need read/modifyIf you want to tweak, there is no way to know what is better/worse until you see the asm output. If you are not viewing the asm output, then you are simply guessing and may still end up with something worse. I have a hard time believing just about any way you make a pin change would make any difference. Write readable code, use the registers in a optimal way as describe above, and forget about trying to shave a handful of cpu cycles in every piece of code as its not worth the effort.

author: SiliconWizard
author_rank: Super Contributor
timestamp: July 24, 2023, 05:53:25 am
content: But if timing is really ultra-critical, just write a small section in assembly rather than try to guess what kind of C code will yield the best result.If you can understand the assembly code that the compiler spits out, then you should be able to write assembly code for the critical section instead.

author: gnuarm
author_rank: Super Contributor
timestamp: July 24, 2023, 06:40:54 am
content: Quote from: Jester on July 23, 2023, 03:04:29 pmIf statements are not always the best way to implement logic in MCUs. I recall a homework assignment in college which involved testing multiple conditions, with a goal of the code running the fastest possible. I set bits in a single byte with appropriate logic operations, then tested the result for equality to zero, a flag that is automatically set in the final operation. It was one cycle faster than the instructor's code!Add a variable with the state you wish to use, and XOR it with the other value. Logic operations are fast and consistent in timing.

author: mfro
author_rank: Regular Contributor
timestamp: July 24, 2023, 09:09:13 am
content: inline specifier doesn't make sense for an interrupt handler

author: gnuarm
author_rank: Super Contributor
timestamp: July 24, 2023, 09:38:02 am
content: Quote from: mfro on July 24, 2023, 09:09:13 amAre you talking to anyone in particular? What are you responding to?

author: HwAoRrDk
author_rank: Super Contributor
timestamp: July 24, 2023, 03:32:58 pm
content: And by its very nature an ISRcannotbe inlined because it is never called by any other piece of code.

author: ejeffrey
author_rank: Super Contributor
timestamp: July 24, 2023, 05:23:33 pm
content: Quote from: mfro on July 24, 2023, 09:09:13 amThe ISR itself is an extern C function that is calling a method on a global variable as shown in comment #9. That method is marked inline. This is a standard way to call C++ code from functions that need to be pure C linkage for some reason, and it generally has zero overhead as long as the optimizer is on. It's also an OK practice in C if you need to manually call an ISR handler for some reason. While ARM uses the native C abi for ISRs, other platforms do not so you can't directly call them. But if you make an ISR stub that just calls the actual interrupt handler using the native calling convention it works. You shouldn't normally need to call an ISR manually but there are cases it makes sense.

author: radiolistener
author_rank: Super Contributor
timestamp: July 24, 2023, 05:40:15 pm
content: Quote from: HwAoRrDk on July 24, 2023, 03:32:58 pmyou can call function directly from your code. I'm not sure if it have sense, but you can do itUsually ISR handler stubs are declared in runtime library with a weak reference. In such way when you declare function with the same name (as existing ISR stub), the linker will replace weak reference with your function, so the pointer on your function appears in ISR vector table and your function will be called when interrupt happens. But nothing prevents you to call that function directly from your code

author: langwadt
author_rank: Super Contributor
timestamp: July 24, 2023, 06:39:52 pm
content: Quote from: gnuarm on July 24, 2023, 06:40:54 ambut here it is not a matter of flipping an bit, it is choosing whether to the write to the mcu's gpio peripheral write-to-set or write-to-clear register

author: gnuarm
author_rank: Super Contributor
timestamp: July 24, 2023, 08:44:44 pm
content: Quote from: langwadt on July 24, 2023, 06:39:52 pmWhich is a decision based on more than one variable.

author: langwadt
author_rank: Super Contributor
timestamp: July 24, 2023, 09:00:21 pm
content: Quote from: gnuarm on July 24, 2023, 08:44:44 pmthe point was to avoid the decision in every ISR since it only changes at at start upand trying to get clever with logic operations in high level code might result in bigger slower code since the compiler might not recognize what it is you are trying to do

author: ozcar
author_rank: Frequent Contributor
timestamp: July 24, 2023, 09:17:35 pm
content: Quote from: NorthGuy on July 23, 2023, 09:07:27 pmI was wondering if, as for some processors I have used, there might be a single GPIO register to both set or clear an output. It seems not, but there is indeed a toggle register.In looking at this, I notice that it might be possible for GPIO to be read and manipulated by "CLA". Perhaps the CPU does not have to be involved at all?

author: gnuarm
author_rank: Super Contributor
timestamp: July 24, 2023, 09:33:46 pm
content: Quote from: langwadt on July 24, 2023, 09:00:21 pmI don't know so much about high level code. I design processors in FPGAs with interrupt structures that reach the ISR in one clock cycle. Not sure what issues you might have with your high level code.

author: langwadt
author_rank: Super Contributor
timestamp: July 24, 2023, 09:57:16 pm
content: Quote from: gnuarm on July 24, 2023, 09:33:46 pmif you are using verilog/VHDL then unless you are instantiating and wiring up individual primitive gates it is the same, you describe what you want and the compiler(synthesizer) figures out a way to implement that, if you do that in a straight forward way the synthesizer might easier recognize that it and do a better job

author: gnuarm
author_rank: Super Contributor
timestamp: July 24, 2023, 10:03:26 pm
content: Quote from: langwadt on July 24, 2023, 09:57:16 pmSorry, you've completely lost me. My processors don't run the HDL. They run assembly language.

author: langwadt
author_rank: Super Contributor
timestamp: July 24, 2023, 10:14:56 pm
content: Quote from: gnuarm on July 24, 2023, 10:03:26 pmHDL gets turned into HW by a synthesizer, so you don't twiddle around with individual primitive gates, you describe what you want it to doc code gets turned into assembly by a compiler,so you don't twiddle around with individual instructions, you describe what you want it to do

author: gnuarm
author_rank: Super Contributor
timestamp: July 24, 2023, 10:42:19 pm
content: Quote from: langwadt on July 24, 2023, 10:14:56 pmThere is no C in my designs. I thought I made that clear. Assembly language.

author: langwadt
author_rank: Super Contributor
timestamp: July 24, 2023, 10:54:29 pm
content: Quote from: gnuarm on July 24, 2023, 10:42:19 pmso none of it relevant to OPs question about code written in C

author: gnuarm
author_rank: Super Contributor
timestamp: July 24, 2023, 11:34:12 pm
content: Quote from: langwadt on July 24, 2023, 10:54:29 pmPerhaps that's a question for the OP? Certainly, there was no point in the nearly dozen post exchange discussing this minutia.

author: nctnico
author_rank: Super Contributor
timestamp: July 25, 2023, 12:26:14 am
content: Quote from: ozcar on July 24, 2023, 09:17:35 pmFrom what I've seen the basic GPIO port has a data output register for an entire port that lets you do a read modify-write. In addition to that seperate clear / set registers or even registers that control a single pin may exist.

author: ozcar
author_rank: Frequent Contributor
timestamp: July 25, 2023, 12:54:39 am
content: Quote from: nctnico on July 25, 2023, 12:26:14 amThe Fine Manual I turned up says this about the GPADAT register:"Due to the difference between the read and write values, sequential read-modify-write operations on this register may corrupt the state of the output latches. The GPASET, GPACLEAR, and GPATOGGLE registers should be used to safely control the output latches."If you could do a 32-bit write to an address not on a 4-byte boundary you could write to both the SET and CLEAR register, but that is likely too much to expect.But I know nothing about this series of TI processors, I might even have been looking in the wrong manual altogether.