Skip to content

Conversation

edgar-bonet
Copy link
Contributor

The firmware can end the simulation by sleeping with interrupts disabled. While this way of hanging the CPU makes a lot of sense, it is not the most intuitive.

The standard way to terminate a C program is by either calling exit() or returning from main(). It turns out the AVR C runtime supports stopping the firmware in both these standard ways. Returning from main() jumps to exit(), which disables interrupts and hangs the CPU in an infinite loop. The implementation is roughly like this:

; after the C runtime initializations:
.section .init9
    call main  ; run the firmware
    jmp exit   ; if main() returns, go to exit()

exit:
_exit:
    cli        ; disable interrupts
    rjmp .-2   ; hang the CPU

This pull request makes run_avr gracefully quit if the firmware executes the CPU-hanging instruction rjmp .-2 with interrupts disabled.

The actual condition used in the code is:

if (avr->state == cpu_Running && new_pc == avr->pc
    && avr->flash[new_pc] == 0xff && avr->flash[new_pc+1] == 0xcf
    && !avr->sreg[S_I])

Some of those tests may be redundant but, as I am not sure to understand all the subtleties of the simulation, I opted for defensive programming.

The feature has been tested with the following program:

#include <stdlib.h>
#include <avr/io.h>
#include <avr/sleep.h>
#include "avr_mcu_section.h"

AVR_MCU(F_CPU, "atmega328p");
AVR_MCU_SIMAVR_CONSOLE(&GPIOR0);

int main(void)
{
    const char *s = "Exiting.\r\n";
    for (const char *t = s; *t; t++)
        GPIOR0 = *t;
#if defined(QUIT_BY_SLEEP)
    sleep_mode();
#elif defined(QUIT_BY_EXIT)
    exit(0);
#endif
}

When the firmware calls exit() or returns from main(), it ends up
running _exit(), which disables interrupts and hangs the CPU in an
infinite, empty loop. run_avr now terminates the simulation if the
CPU-hanging instruction (rjmp .-2) is executed with interrupts disabled.
@edgar-bonet
Copy link
Contributor Author

I checked that, with this pull request, abort() is also a valid way of terminating the simulation. According to the avr-libc documentation, abort() is implemented as:

cli();
_exit(1);

@kittennbfive
Copy link
Contributor

+1 for this PR, i wondered why cli();while(1); does not stop the simulation.

@mofosyne
Copy link

mofosyne commented Oct 6, 2025

Oh cool does _exit(1); return 1 on simavr? Was hoping for a canonical way to use simavr for CI and having a way to return unit test results would be good.

@edgar-bonet
Copy link
Contributor Author

@mofosyne: No. The value passed to exit() (or _exit()) is not returned by simavr, but you could add the feature as an extension to this pull request. Quoting my own comment on PR #555:

Note that it should be possible to grab the value returned by main() (and passed to exit()) from r25:r24, and have run_avr return that. The above-mentioned pull request does not do that.

@edgar-bonet
Copy link
Contributor Author

@mofosyne: Here is a quick and dirty way to get simavr return the exit status of the firmware. Apply this pull request, then:

diff --git a/simavr/sim/run_avr.c b/simavr/sim/run_avr.c
index 863fc8e..0b4fd50 100644
--- a/simavr/sim/run_avr.c
+++ b/simavr/sim/run_avr.c
@@ -334,5 +334,7 @@ main(
 			break;
 	}
 
+	int exit_status = avr->data[24];  // r24 at program termination
 	avr_terminate(avr);
+	return exit_status;
 }

There is a caveat: if the firmware terminates by crashing, or by sleeping with interrupts off, then the returned value is meaningless. In such a case, it would be wiser for simavr to return 0, or maybe 1 on a crash. For this reason, I am not submitting this as a pull request, but you can use this solution if you are not concerned about this caveat.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants