4
\$\begingroup\$

From my experience so far I am aware of the following to pay attention to when working with ISRs:

  • An ISR should complete quickly
  • Variables shared between an ISR and the main execution path should be declared volatile to avoid accesses to it being optimized away
  • Access to shared variables with 16 bit and more should be accessed in an ATOMIC_BLOCK because the 8 bit CPU cannot access such variables in a single cycle

But I am wondering about functions called from an ISR:

  1. Does calling a function from an ISR add any particular overhead? Looking at the disassembly, code in a function and the same code directly in the ISR yield the same instructions
  2. I suppose everything that applies to an ISR also applies to a function called from an ISR since it is the same execution path?
  3. If a function called from userspace contains an ATOMIC_BLOCK, is it valid to also call it from an ISR?
\$\endgroup\$
  • 3
    \$\begingroup\$ Your points sound about right to me. Functions calls from the ISR may add a call and return, and may use stack space, but the compiler may optimize the call away and place the called code inline. Yes, the routine called then forms part of the ISR. If you have a block of code which may do a number of operations and it is important the they are completed atomically, then you can disable interrupts at the start of the block and re-enable them at the end. \$\endgroup\$ – user1582568 Feb 23 '16 at 16:36
  • \$\begingroup\$ Isn't ATOMIC_BLOCK specific to the platform, e.g. AVR? \$\endgroup\$ – Peter Mortensen Sep 15 '17 at 23:44
  • \$\begingroup\$ @PeterMortensen I suppose it is since it sets the "Global Interrupt Status flag in SREG" which is specific to avr-gcc but to my understanding similar patterns exist in other platforms, i.e. a synchronized block in Java. \$\endgroup\$ – Torsten Römer Sep 16 '17 at 8:49
4
\$\begingroup\$

Non-AVR-specific answer:

Jumping into an ISR from a normal execution thread generally requires a context switch, where the current state of the CPU is saved somewhere (stack, shadow registers) so that the ISR can use the CPU as it needs to, and when it is finished, the ISR restores the context (so that to the main program, the CPU is in the exact state it was in prior to the ISR) and carries on. It is an asynchronous jump from normal execution - the program doesn't deliberately decide to branch, some external mechanism (a signal, some hardware, whatever) makes the decision.

An ordinary function call doesn't require a context switch per se. The code is following a known path, so there's no need to save/preserve all of the CPU registers. Depending on the function, you may need to pass arguments to it, and may read back a return value, but since you're not pre-empting normal execution (i.e. the program knows where it's going and when it's going there) and not expecting to return with everything as it was before the call, you're not doing a context switch.

If you don't see any context switching in your ISR code (could be pushes, pops, etc.) then perhaps the hardware takes care of that for you. On other devices (many Microchip parts, for example) you need to take care of the context switching in code. Generally I would expect that an ISR jump would require extra code to take care of the context switching vs. a naive void XXX(void) function call.

Calling a function from an ISR isn't so different from calling a function in userspace. The code will branch in much the same way as before, with arguments being passed and return values returned. If you are on a limited-stack device, however, beware: the function call will be pushing and popping stuff to and from the stack which likely is already maintaining the context of the code outside the ISR. Stack overflow is always a possibility with nested function calls (recursion is really bad for this).

An ATOMIC_BLOCK wrapper blocks interrupts from firing while a segment of code is being executed. If you're already in an ISR, I presume this would prevent the ISR from being pre-empted by another higher-priority ISR. I can understand why you may want do to this, so it seems valid to me (note: I am not an ARM expert)

\$\endgroup\$
  • \$\begingroup\$ Great. Just one more thing: I have a function including an ATOMIC_BLOCK that is called from userspace but also from an ISR. It works fine but is this a valid thing to do? \$\endgroup\$ – Torsten Römer Feb 23 '16 at 17:11
  • \$\begingroup\$ Please update your original question with this addtional detail. Comments aren't the right place for this. \$\endgroup\$ – Adam Lawrence Feb 23 '16 at 18:00
  • 1
    \$\begingroup\$ Why do you explicitly mention ARM? I can't find any notion of it in the question. Regarding the "If you're already in an ISR, I presume this would prevent the ISR from being pre-empted by another higher-priority ISR." - The most common default behavior is, that the global interrupt flag is disabled anyway while an ISR is executed. You can explicitly enable it to allow nested interrupts. ATOMIC_BLOCK inside an ISR would be a somewhat weird use case. \$\endgroup\$ – Rev1.0 Feb 23 '16 at 18:14
  • \$\begingroup\$ @AdamLawrence - Thanks, question edited. @Rev1.0 - I agree it is weird but the function is also called from userspace, that's why it contains an ATOMIC_BLOCK. The function contains a few lines of code that is called from the main execution path as well as from the ISR under a certain condition. \$\endgroup\$ – Torsten Römer Feb 23 '16 at 19:31
  • \$\begingroup\$ @TorstenRömer: Ah OK, then it is totally appropriate to have it there. \$\endgroup\$ – Rev1.0 Feb 23 '16 at 21:03
1
\$\begingroup\$

I would like to emphasize the difference between atomic blocks and functions called from user & ISR code. Just look up the Wikipedia articles for Atomic Operation and Reentrant.

Atomic at the embedded level is best illustrated by Read-Modify-Write operations. Some opcodes like INC or DEC are atomic by hardware design; because any ISR will fire only before or after the opcode has completed.

However, some operations are too complex. Or some 32-bit MCU's like PIC32 or ARM cannot perform these operations on the (I/O) memory space. In this case a read-modify-write operation needs to be completed. An issue may occur if an interrupt or context switch (e.g. RTOS) occurs. The value may already be read but not yet written out, while the other context changes the value.

An atomic block usually disables interrupts. This will prevent context switches and allow operations on data to complete. It is best to keep atomic blocks short, so interrupts are serviced as soon as possible.

Atomic blocks may be useful if your micro has a nested interrupt controller. A nested interrupt controller may pre-empt execution of a low-priority ISR with a higher priority one.

ARM microcontrollers have ldrex and strex that load&store variables with a hardware exclusive monitor. Upon store it is possible to check if access to the variable was exclusive - if not the software can re-do computation and try again.

I would like to add that even though your hardware operations are atomic, it does not mean the function is re-entrant or thread-safe. Re-entrant means that a function can be called again within the execution of that function.

Important to watch out for is where you store the function state. Do not work on global or local static variables. If you're writing ISR routines this is unavoidable as it's the only means of communicating with the main code. In this case I would make sure each variable will only be written from 1 source (only written from ISR or from main code) and are defined as static-global (not accessible outside the file).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.