5
\$\begingroup\$

This is strange behavior and I am very curious.

It's a fairly large project, but basically this section of code is designed to quickly save all of the user settings on power down. There's plenty of time if I do it quickly.

There's a global variable psup that monitors power supply voltage. If it goes below a threshold, I want to call the shutdown() routine. So:

ISR(TIMER0_COMPA_vect)  
{
    if ((psup<0x76) && allow_shutdown_detection) 
    {
       PINONC(3);
       shutdown();
    } 
...
}

The code crashes (watchdog reset) even though this code is never executed. I've checked with the debugger and monitoring pin C3. The psup variable never decreases to trigger the condition.

If I comment out the shutdown(), it executes fine.

What is it about the mere inclusion of a function that's causing the crash, even if it's never executed?

There seems to be plenty of data memory left. Such a strange thing.

Putting this check inside the ADC interrupt DOES work but I'd like to have it here in the timer instead for reasons. But I'd really like to understand what's going on.

(Part: ATmega328PB)

\$\endgroup\$
6
  • \$\begingroup\$ Limited stack space? Registers (Instruction pointer mainly) might be overwritten? Generally, you want to avoid function calls inside an ISR. The ISR should do the minimum required to handle the situation and return to the main code as quickly as possible. \$\endgroup\$
    – Lior Bilia
    Commented Dec 19, 2022 at 20:27
  • 2
    \$\begingroup\$ The problem might be not where you are looking. If you are using the Arduino core, it might be using that timer and causing your problem. How do you know the code is never executed? Formulate some tests to prove/disprove this assumption. \$\endgroup\$
    – Kartman
    Commented Dec 19, 2022 at 20:30
  • \$\begingroup\$ Most micros have a very limited ISR "space." Some only allow a few instructions at most; in general, ISRs should be as short as possible. Consider setting a flag bit in the ISR, then handle that in your main loop. Or just check psup straight from the main loop (if there is time.) \$\endgroup\$
    – rdtsc
    Commented Dec 19, 2022 at 22:12
  • \$\begingroup\$ How often are you calling the interrupt? \$\endgroup\$ Commented Dec 19, 2022 at 22:15
  • 1
    \$\begingroup\$ Does this interrupt-service-routine reset the TIMER0 interrupt flag? If not, the interrupt runs continuously. \$\endgroup\$
    – glen_geek
    Commented Dec 19, 2022 at 22:52

1 Answer 1

6
\$\begingroup\$

What makes an unused call for difference?

Generating code which calls an external routine (which is not or can not be inlined) from ISR means that compiler has to save and restore all (!) call-used registers (see documentation, or here) in ISR entry code. This is not done in specific branch, but before control is passed to your code regardless actual branching. (Only if compiler could determine the branch is never true, it could be optimized out together wit all register stuff).

When you comment out shutdown(), the routine does not anymore contain any hidden code (from compiler perspective). All used registers can be listed out (which in this case will be a few or maybe only one), so not so much data to save and restore.

The registers to save end up on top of stack. Combined with the fact timer ISR is called often all the time, there is a good chance it gets sooner or later triggered when stack usage is already high.

Actual example

I made your code to a minimal compile-able form:

#include <avr/io.h>
#include <avr/interrupt.h>

extern void shutdown(void);

extern uint8_t allow_shutdown_detection;
extern uint8_t psup;

#define PINONC(x) do {PORTC |= _BV((x));} while(0)

ISR(TIMER0_COMPA_vect)
{   
    if( (psup<0x76) && allow_shutdown_detection)
    {
       PINONC(3);
       shutdown();
    } 
}   

And compiled it by avr-gcc -S -Os -mmcu=atmega328p test.c

Resulting ISR code looks like this:

.global __vector_14
    .type   __vector_14, @function
__vector_14:
    push r1
    push r0
    in r0,__SREG__
    push r0
    clr __zero_reg__
    push r18
    push r19
    push r20
    push r21
    push r22
    push r23
    push r24
    push r25
    push r26
    push r27
    push r30
    push r31
/* prologue: Signal */
/* frame size = 0 */
/* stack size = 15 */
.L__stack_usage = 15
    lds r24,psup
    cpi r24,lo8(118)
    brsh .L1
    lds r24,allow_shutdown_detection
    tst r24
    breq .L1
    sbi 0x8,3
    call shutdown
.L1:
/* epilogue start */
    pop r31
    pop r30
    pop r27
    pop r26
    pop r25
    pop r24
    pop r23
    pop r22
    pop r21
    pop r20
    pop r19
    pop r18
    pop r0
    out __SREG__,r0
    pop r0
    pop r1
    reti
    .size   __vector_14, .-__vector_14

Your own code starts at the line lds r24,psup (the psup would be later replaced by actual address in linker), but before reaching there, compiler has to generate preamble which takes 15 bytes off the stack. Despite the visible code here using only two registers (r24 and sreg).

If I comment out call to shutdown(), generated code shrinks using only 4 bytes of stack (and it could be hand-optimized down to two):

.global __vector_14
    .type   __vector_14, @function
__vector_14:
    push r1
    push r0
    in r0,__SREG__
    push r0
    clr __zero_reg__
    push r24
/* prologue: Signal */
/* frame size = 0 */
/* stack size = 4 */
.L__stack_usage = 4
    lds r24,psup
    cpi r24,lo8(118)
    brsh .L1
    lds r24,allow_shutdown_detection
    cpse r24,__zero_reg__
    sbi 0x8,3
.L1:
/* epilogue start */
    pop r24
    pop r0
    out __SREG__,r0
    pop r0
    pop r1
    reti 
    .size   __vector_14, .-__vector_14

How to fix this?

  • Decrease memory usage in your code. If 10 bytes of stack space makes all the difference, you are likely using too much RAM already. Are you sure there is no possible function call chain which will take even more stack. Or other interrupt which could come in wrong moment too? Check static ram usage (Do you have lot of strings in memory maybe? Or some big data?) and stack usage (Many nested function calls? Lot of local variables in (such) functions?).
  • Make your ISR more simple. That is do not force compiler to save all the registers. This can help only as much as many stack bytes current ISR uses. Moreover the typical approach where you set a flag (semaphore) in the ISR and handle it in main loop is not useful for you (I assume) because you need to run whole shutdown() immediately. If the routine itself is rather simple, you could save some stack by asking compiler to inline it directly into the ISR.
  • Use dirty tricks. Because shutdown() is apparently no-return function, there is place for some hacks if you really need to use stack up to the last byte. Change the function call in the ISR to assembler jump instruction instead. This will prevent compiler form storing all the registers. Then the shutdown() itself can be made a naked function with your own preamble where you reset stack pointer and registers to safe state regardless where you jumped from. Of course doing this there will be no return to the interrupted code, but I assume it is not intended anyway.
\$\endgroup\$
4
  • \$\begingroup\$ Thanks very much for the thorough answer. It seems almost certain that I am running out of stack, and I will have to dig deeper to learn why. There are other interrupts but not much data. I'm using Atmel Studio so I'll have to figure out how to view relevant RAM and stack usage during debug. The dirty trick is probably workable, as shutdown() is indeed terminal. Perhaps a goto command in C? \$\endgroup\$
    – rspaudio
    Commented Dec 20, 2022 at 10:11
  • 1
    \$\begingroup\$ Goto is not going to work (and neither longjmp and co.). All C-language constructions will save registers and such as necessary, because C code won't break itself. :-) You simply need asm block (so compiler do not "see" what happens inside) with a jmp to function address. Then either you want to reset stack pointer in ISR before jmp (again in asm) or you need to replace function preamble generated by avr-gcc by your own (that's the reference to naked function). But this starting to be an entirely different question now. \$\endgroup\$
    – Martin
    Commented Dec 20, 2022 at 21:44
  • \$\begingroup\$ Thank you very much. Is there a way that I can use at Atmel Studio to characterize how much stack I am using? It doesn't seem to be very clear. From looking at just the raw memory it looks like I should be in the clear but clearly I am not. \$\endgroup\$
    – rspaudio
    Commented Dec 21, 2022 at 22:05
  • \$\begingroup\$ Please ask this as a new question. You will have greater chance to get a good answer and it will be easier to find for others later. (In short, I do not know if Atmel studio provides any built-in stack usage analysis, possibly not, but universal easy approach, usually good enough to see just usage level, is to prefill stack with known value/pattern before running your code, and then check (periodically) at runtime how much of the pattern is still remaining in memory. There is definitively more alternatives, but again, ask a new question!) \$\endgroup\$
    – Martin
    Commented Dec 25, 2022 at 21:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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