blob: 54989dcd32cd0bcd8d85ab741fe3ce9b420ae6cf [file] [log] [blame]
/*
* Copyright (c) 2017 Oticon A/S
* Copyright (c) 2023 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
/*
* Native simulator CPU emulator,
* an *optional* module provided by the native simulator
* the hosted embedded OS / SW can use to emulate the CPU
* being started and stopped.
*
* Its mode of operation is that it step-locks the HW
* and SW operation, so that only one of them executes at
* a time. Check the docs for more info.
*/
#include <pthread.h>
#include <stdbool.h>
#include <unistd.h>
#include <stdlib.h>
#include "nce_if.h"
#include "nsi_safe_call.h"
struct nce_status_t {
/* Conditional variable to know if the CPU is running or halted/idling */
pthread_cond_t cond_cpu;
/* Mutex for the conditional variable cond_cpu */
pthread_mutex_t mtx_cpu;
/* Variable which tells if the CPU is halted (1) or not (0) */
bool cpu_halted;
bool terminate; /* Are we terminating the program == cleaning up */
void (*start_routine)(void);
};
#define NCE_DEBUG_PRINTS 0
#define PREFIX "NCE: "
#define ERPREFIX PREFIX"error on "
#define NO_MEM_ERR PREFIX"Can't allocate memory\n"
#if NCE_DEBUG_PRINTS
#define NCE_DEBUG(fmt, ...) nsi_print_trace(PREFIX fmt, __VA_ARGS__)
#else
#define NCE_DEBUG(...)
#endif
extern void nsi_exit(int exit_code);
/*
* Initialize an instance of the native simulator CPU emulator
* and return a pointer to it.
* That pointer should be passed to all subsequent calls to this module.
*/
void *nce_init(void)
{
struct nce_status_t *this;
this = calloc(1, sizeof(struct nce_status_t));
if (this == NULL) { /* LCOV_EXCL_BR_LINE */
nsi_print_error_and_exit(NO_MEM_ERR); /* LCOV_EXCL_LINE */
}
this->cpu_halted = true;
this->terminate = false;
NSI_SAFE_CALL(pthread_cond_init(&this->cond_cpu, NULL));
NSI_SAFE_CALL(pthread_mutex_init(&this->mtx_cpu, NULL));
return (void *)this;
}
/*
* This function will:
*
* If called from a SW thread, release the HW thread which is blocked in
* a nce_wake_cpu() and never return.
*
* If called from a HW thread, do the necessary clean up of this nce instance
* and return right away.
*/
void nce_terminate(void *this_arg)
{
struct nce_status_t *this = (struct nce_status_t *)this_arg;
/* LCOV_EXCL_START */ /* See Note1 */
/*
* If we are being called from a HW thread we can cleanup
*
* Otherwise (!cpu_halted) we give back control to the HW thread and
* tell it to terminate ASAP
*/
if (this == NULL || this->cpu_halted) {
/*
* Note: The nce_status structure cannot be safely free'd up
* as the user is allowed to call nce_clean_up()
* repeatedly on the same structure.
* Instead we rely of on the host OS process cleanup.
* If you got here due to valgrind's leak report, please use the
* provided valgrind suppression file valgrind.supp
*/
return;
} else if (this->terminate == false) {
this->terminate = true;
NSI_SAFE_CALL(pthread_mutex_lock(&this->mtx_cpu));
this->cpu_halted = true;
NSI_SAFE_CALL(pthread_cond_broadcast(&this->cond_cpu));
NSI_SAFE_CALL(pthread_mutex_unlock(&this->mtx_cpu));
while (1) {
sleep(1);
/* This SW thread will wait until being cancelled from
* the HW thread. sleep() is a cancellation point, so it
* won't really wait 1 second
*/
}
}
/* LCOV_EXCL_STOP */
}
/**
* Helper function which changes the status of the CPU (halted or running)
* and waits until somebody else changes it to the opposite
*
* Both HW and SW threads will use this function to transfer control to the
* other side.
*
* This is how the idle thread halts the CPU and gets halted until the HW models
* raise a new interrupt; and how the HW models awake the CPU, and wait for it
* to complete and go to idle.
*/
static void change_cpu_state_and_wait(struct nce_status_t *this, bool halted)
{
NSI_SAFE_CALL(pthread_mutex_lock(&this->mtx_cpu));
NCE_DEBUG("Going to halted = %d\n", halted);
this->cpu_halted = halted;
/* We let the other side know the CPU has changed state */
NSI_SAFE_CALL(pthread_cond_broadcast(&this->cond_cpu));
/* We wait until the CPU state has been changed. Either:
* we just awoke it, and therefore wait until the CPU has run until
* completion before continuing (before letting the HW models do
* anything else)
* or
* we are just hanging it, and therefore wait until the HW models awake
* it again
*/
while (this->cpu_halted == halted) {
/* Here we unlock the mutex while waiting */
pthread_cond_wait(&this->cond_cpu, &this->mtx_cpu);
}
NCE_DEBUG("Awaken after halted = %d\n", halted);
NSI_SAFE_CALL(pthread_mutex_unlock(&this->mtx_cpu));
}
/*
* Helper function that wraps the SW start_routine
*/
static void *sw_wrapper(void *this_arg)
{
struct nce_status_t *this = (struct nce_status_t *)this_arg;
/* Ensure nce_boot_cpu has reached the cond loop */
NSI_SAFE_CALL(pthread_mutex_lock(&this->mtx_cpu));
NSI_SAFE_CALL(pthread_mutex_unlock(&this->mtx_cpu));
#if (NCE_DEBUG_PRINTS)
pthread_t sw_thread = pthread_self();
NCE_DEBUG("SW init started (%lu)\n",
sw_thread);
#endif
this->start_routine();
return NULL;
}
/*
* Boot the emulated CPU, that is:
* * Spawn a new pthread which will run the first embedded SW thread <start_routine>
* * Hold the caller until that embedded SW thread (or a child it spawns)
* calls nce_halt_cpu()
*
* Note that during this, an embedded SW thread may call nsi_exit(), which would result
* in this function never returning.
*/
void nce_boot_cpu(void *this_arg, void (*start_routine)(void))
{
struct nce_status_t *this = (struct nce_status_t *)this_arg;
NSI_SAFE_CALL(pthread_mutex_lock(&this->mtx_cpu));
this->cpu_halted = false;
this->start_routine = start_routine;
/* Create a thread for the embedded SW init: */
pthread_t sw_thread;
NSI_SAFE_CALL(pthread_create(&sw_thread, NULL, sw_wrapper, this_arg));
/* And we wait until the embedded OS has send the CPU to sleep for the first time */
while (this->cpu_halted == false) {
pthread_cond_wait(&this->cond_cpu, &this->mtx_cpu);
}
NSI_SAFE_CALL(pthread_mutex_unlock(&this->mtx_cpu));
if (this->terminate) {
nsi_exit(0);
}
}
/*
* Halt the CPU, that is:
* * Hold this embedded SW thread until the CPU is awaken again,
* and release the HW thread which had been held on
* nce_boot_cpu() or nce_wake_cpu().
*
* Note: Can only be called from embedded SW threads
* Calling it from a HW thread is a programming error.
*/
void nce_halt_cpu(void *this_arg)
{
struct nce_status_t *this = (struct nce_status_t *)this_arg;
if (this->cpu_halted == true) {
nsi_print_error_and_exit("Programming error on: %s ",
"This CPU was already halted\n");
}
change_cpu_state_and_wait(this, true);
}
/*
* Awake the CPU, that is:
* * Hold this HW thread until the CPU is set to idle again
* * Release the SW thread which had been held on nce_halt_cpu()
*
* Note: Can only be called from HW threads
* Calling it from a SW thread is a programming error.
*/
void nce_wake_cpu(void *this_arg)
{
struct nce_status_t *this = (struct nce_status_t *)this_arg;
if (this->cpu_halted == false) {
nsi_print_error_and_exit("Programming error on: %s ",
"This CPU was already awake\n");
}
change_cpu_state_and_wait(this, false);
/*
* If while the SW was running it was decided to terminate the execution
* we stop immediately.
*/
if (this->terminate) {
nsi_exit(0);
}
}
/*
* Return 0 if the CPU is sleeping (or terminated)
* and !=0 if the CPU is running
*/
int nce_is_cpu_running(void *this_arg)
{
struct nce_status_t *this = (struct nce_status_t *)this_arg;
if (this != NULL) {
return !this->cpu_halted;
} else {
return false;
}
}
/*
* Notes about coverage:
*
* Note1: When the application is closed due to a SIGTERM, the path in this
* function will depend on when that signal was received. Typically during a
* regression run, both paths will be covered. But in some cases they won't.
* Therefore and to avoid confusing developers with spurious coverage changes
* we exclude this function from the coverage check
*/