diff --git a/epicardium/api/caller.c b/epicardium/api/caller.c index 7c52d28051250850009871c460ce29561caaaf48..f2f5643abb62e6497c19e7e831ccd1ad1a60525f 100644 --- a/epicardium/api/caller.c +++ b/epicardium/api/caller.c @@ -54,6 +54,47 @@ void *_api_call_transact(void *buffer) return API_CALL_MEM->buffer; } +__attribute__((noreturn)) void epic_exit(int ret) +{ + /* + * Call __epic_exit() and then jump to the reset routine/ + */ + void *buffer; + + buffer = _api_call_start(API_SYSTEM_EXIT, sizeof(int)); + *(int *)buffer = ret; + _api_call_transact(buffer); + + API_CALL_MEM->reset_stub(); + + /* unreachable */ + while (1) + ; +} + +int epic_exec(char *name) +{ + /* + * Call __epic_exec(). If it succeeds, jump to the reset routine. + * Otherwise, return the error code. + */ + void *buffer; + + buffer = _api_call_start(API_SYSTEM_EXEC, sizeof(char *)); + *(char **)buffer = name; + int ret = *(int *)_api_call_transact(buffer); + + if (ret < 0) { + return ret; + } + + API_CALL_MEM->reset_stub(); + + /* unreachable */ + while (1) + ; +} + int api_fetch_args(char *buf, size_t cnt) { if (API_CALL_MEM->id != 0) { diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h index 2c65e376d4f3741592d34e1e64f57cd3c9714db2..c680d55926adf7af5202c7e498873e7ea6d10472 100644 --- a/epicardium/epicardium.h +++ b/epicardium/epicardium.h @@ -29,8 +29,8 @@ typedef _Bool bool; */ /* clang-format off */ -#define API_SYSTEM_EXIT 0x1 /* TODO */ -#define API_SYSTEM_EXEC 0x2 /* TODO */ +#define API_SYSTEM_EXIT 0x1 +#define API_SYSTEM_EXEC 0x2 #define API_INTERRUPT_ENABLE 0xA #define API_INTERRUPT_DISABLE 0xB @@ -116,7 +116,7 @@ API(API_INTERRUPT_DISABLE, int epic_interrupt_disable(api_int_id_t int_id)); */ /* clang-format off */ -/** Reset Handler? **TODO** */ +/** Reset Handler */ #define EPIC_INT_RESET 0 /** ``^C`` interrupt. See :c:func:`epic_isr_ctrl_c` for details. */ #define EPIC_INT_CTRL_C 1 @@ -129,8 +129,59 @@ API(API_INTERRUPT_DISABLE, int epic_interrupt_disable(api_int_id_t int_id)); #define EPIC_INT_NUM 4 /* clang-format on */ -API_ISR(EPIC_INT_RESET, epic_isr_reset); +/* + * "Reset Handler*. This isr is implemented by the API caller and is used to + * reset the core for loading a new payload. + * + * Just listed here for completeness. You don't need to implement this yourself. + */ +API_ISR(EPIC_INT_RESET, __epic_isr_reset); + +/** + * Core API + * ======== + * The following functions control execution of code on core 1. + */ + +/** + * Stop execution of the current payload and return to the menu. + * + * :param int ret: Return code. + * :return: :c:func:`epic_exit` will never return. + */ +void epic_exit(int ret) __attribute__((noreturn)); + +/* + * The actual epic_exit() function is not an API call because it needs special + * behavior. The underlying call is __epic_exit() which returns. After calling + * this API function, epic_exit() will enter the reset handler. + */ +API(API_SYSTEM_EXIT, void __epic_exit(int ret)); +/** + * Stop execution of the current payload and immediately start another payload. + * + * :param char* name: Name (path) of the new payload to start. This can either + * be: + * + * - A path to an ``.elf`` file (l0dable). + * - A path to a ``.py`` file (will be loaded using Pycardium). + * - A path to a directory (assumed to be a Python module, execution starts + * with ``__init__.py`` in this folder). + * + * :return: :c:func:`epic_exec` will only return in case loading went wrong. + * The following error codes can be returned: + * + * - ``-ENOENT``: File not found. + * - ``-ENOEXEC``: File not a loadable format. + */ +int epic_exec(char *name); + +/* + * Underlying API call for epic_exec(). The function is not an API call itself + * because it needs special behavior when loading a new payload. + */ +API(API_SYSTEM_EXEC, int __epic_exec(char *name)); /** * UART/Serial Interface diff --git a/epicardium/main.c b/epicardium/main.c index 1ae2dde0ea54abef8bbab248502d5537b47e243c..c70ea936532c0bf85ce913186e90ca0241fcf8b3 100644 --- a/epicardium/main.c +++ b/epicardium/main.c @@ -12,7 +12,6 @@ #include "pmic.h" #include "leds.h" #include "api/dispatcher.h" -#include "l0der/l0der.h" #include "modules/modules.h" #include "modules/log.h" #include "modules/stream.h" @@ -73,6 +72,9 @@ int main(void) api_interrupt_init(); stream_init(); + LOG_INFO("startup", "Initializing dispatcher ..."); + api_dispatcher_init(); + LOG_INFO("startup", "Initializing tasks ..."); /* Serial */ @@ -127,39 +129,22 @@ int main(void) abort(); } - LOG_INFO("startup", "Initializing dispatcher ..."); - api_dispatcher_init(); - /* light sensor */ LOG_INFO("startup", "starting light sensor ..."); epic_light_sensor_run(); - core1_boot(); - - /* - * See if there's a l0dable.elf to run. If not, run pycardium. - * This is temporary until epicardium gets a l0dable API from pycardium. - */ - const char *l0dable = "l0dable.elf"; - if (f_stat(l0dable, NULL) == FR_OK) { - LOG_INFO("startup", "Running %s ...", l0dable); - struct l0dable_info info; - int res = l0der_load_path(l0dable, &info); - if (res != 0) { - LOG_ERR("startup", "l0der failed: %d\n", res); - } else { - LOG_INFO( - "startup", "Starting %s on core1 ...", l0dable - ); - core1_load(info.isr_vector, ""); - } - } else { - LOG_INFO("startup", "Starting pycardium on core 1 ..."); - core1_load((void *)0x10080000, "main.py"); + /* Lifecycle */ + if (xTaskCreate( + vLifecycleTask, + (const char *)"Lifecycle", + configMINIMAL_STACK_SIZE * 4, + NULL, + tskIDLE_PRIORITY + 1, + NULL) != pdPASS) { + LOG_CRIT("startup", "Failed to create %s task!", "Lifecycle"); + abort(); } - hardware_init(); - LOG_INFO("startup", "Starting FreeRTOS ..."); vTaskStartScheduler(); diff --git a/epicardium/modules/lifecycle.c b/epicardium/modules/lifecycle.c new file mode 100644 index 0000000000000000000000000000000000000000..13ba9704e5d068edce4dc85fb3a26c610b662964 --- /dev/null +++ b/epicardium/modules/lifecycle.c @@ -0,0 +1,315 @@ +#include "epicardium.h" +#include "modules/log.h" +#include "modules/modules.h" +#include "api/dispatcher.h" +#include "l0der/l0der.h" + +#include "card10.h" + +#include "FreeRTOS.h" +#include "task.h" +#include "semphr.h" + +#include <string.h> +#include <stdbool.h> +#include <stdbool.h> + +#define PYCARDIUM_IVT (void *)0x10080000 +#define BLOCK_WAIT pdMS_TO_TICKS(1000) +/* + * Loading an empty filename into Pycardium will drop straight into the + * interpreter. This define is used to make it more clear when we intend + * to go into the interpreter. + */ +#define PYINTERPRETER "" + +static TaskHandle_t lifecycle_task = NULL; +static StaticSemaphore_t core1_mutex_data; +static SemaphoreHandle_t core1_mutex = NULL; + +enum payload_type { + PL_INVALID = 0, + PL_PYTHON_SCRIPT = 1, + PL_PYTHON_DIR = 2, + PL_PYTHON_INTERP = 3, + PL_L0DABLE = 4, +}; + +struct load_info { + bool do_reset; + enum payload_type type; + char name[256]; +}; +static volatile struct load_info async_load = { + .do_reset = false, + .name = { 0 }, + .type = PL_INVALID, +}; + +/* Helpers {{{ */ + +/* + * Check if the payload is a valid file (or module) and if so, return its type. + */ +static int load_stat(char *name) +{ + size_t name_len = strlen(name); + + if (name_len == 0) { + return PL_PYTHON_INTERP; + } + + struct epic_stat stat; + if (epic_file_stat(name, &stat) < 0) { + return -ENOENT; + } + + if (stat.type == EPICSTAT_DIR) { + /* This might be a python module. */ + return PL_PYTHON_DIR; + } + + if (strcmp(name + name_len - 3, ".py") == 0) { + /* A python script */ + return PL_PYTHON_SCRIPT; + } else if (strcmp(name + name_len - 4, ".elf") == 0) { + return PL_L0DABLE; + } + + return -ENOEXEC; +} + +/* + * Actually load a payload into core 1. Optionally reset the core first. + */ +static int do_load(struct load_info *info) +{ + if (*info->name == '\0') { + LOG_INFO("lifecycle", "Loading Python interpreter ..."); + } else { + LOG_INFO("lifecycle", "Loading \"%s\" ...", info->name); + } + + if (xSemaphoreTake(api_mutex, BLOCK_WAIT) != pdTRUE) { + LOG_ERR("lifecycle", "API blocked"); + return -EBUSY; + } + + if (info->do_reset) { + LOG_DEBUG("lifecycle", "Triggering core 1 reset."); + core1_reset(); + + api_dispatcher_init(); + } + + switch (info->type) { + case PL_PYTHON_SCRIPT: + case PL_PYTHON_DIR: + case PL_PYTHON_INTERP: + core1_load(PYCARDIUM_IVT, info->name); + break; + case PL_L0DABLE: + /* + * Always reset when loading a l0dable to make sure the memory + * space is absolutely free. + */ + core1_reset(); + + struct l0dable_info l0dable; + int res = l0der_load_path(info->name, &l0dable); + if (res != 0) { + LOG_ERR("lifecycle", "l0der failed: %d\n", res); + xSemaphoreGive(api_mutex); + return -ENOEXEC; + } + core1_load(l0dable.isr_vector, ""); + + break; + default: + LOG_ERR("lifecyle", + "Attempted to load invalid payload (%s)", + info->name); + xSemaphoreGive(api_mutex); + return -EINVAL; + } + + xSemaphoreGive(api_mutex); + return 0; +} + +/* + * Do a synchroneous load. + */ +static int load_sync(char *name, bool reset) +{ + int ret = load_stat(name); + if (ret < 0) { + return ret; + } + + struct load_info info = { + .name = { 0 }, + .type = ret, + .do_reset = reset, + }; + + strncpy(info.name, name, sizeof(info.name)); + + return do_load(&info); +} + +/* + * Do an asynchroneous load. This will return immediately if the payload seems + * valid and call the lifecycle task to actually perform the load later. + */ +static int load_async(char *name, bool reset) +{ + int ret = load_stat(name); + if (ret < 0) { + return ret; + } + + async_load.type = ret; + async_load.do_reset = reset; + + strncpy((char *)async_load.name, name, sizeof(async_load.name)); + + if (lifecycle_task != NULL) { + xTaskNotifyGive(lifecycle_task); + } + + return 0; +} + +/* + * Go back to the menu. + */ +static void load_menu(bool reset) +{ + LOG_INFO("lifecycle", "Into the menu"); + + if (xSemaphoreTake(core1_mutex, BLOCK_WAIT) != pdTRUE) { + LOG_ERR("lifecycle", + "Can't load because mutex is blocked (menu)."); + return; + } + + int ret = load_async("menu.py", reset); + if (ret < 0) { + /* TODO: Write default menu */ + LOG_WARN("lifecycle", "No menu script found."); + load_async(PYINTERPRETER, reset); + } + + xSemaphoreGive(core1_mutex); +} +/* Helpers }}} */ + +/* API {{{ */ +/* + * This is NOT the epic_exec() called from Pycardium, but an implementation of + * the same call for use in Epicardium. This function is synchroneous and will + * wait until the call returns. + */ +int epic_exec(char *name) +{ + if (xSemaphoreTake(core1_mutex, BLOCK_WAIT) != pdTRUE) { + LOG_ERR("lifecycle", + "Can't load because mutex is blocked (epi exec)."); + return -EBUSY; + } + + int ret = load_sync(name, true); + xSemaphoreGive(core1_mutex); + return ret; +} + +/* + * This is the underlying call for epic_exec() from Pycardium. It is + * asynchroneous and will return early to allow Pycardium (or a l0dable) to jump + * to the reset handler. + * + * The lifecycle task will deal with actually loading the new payload. + */ +int __epic_exec(char *name) +{ + if (xSemaphoreTake(core1_mutex, BLOCK_WAIT) != pdTRUE) { + LOG_ERR("lifecycle", + "Can't load because mutex is blocked (1 exec)."); + return -EBUSY; + } + int ret = load_async(name, false); + xSemaphoreGive(core1_mutex); + return ret; +} + +/* + * This is the underlying call for epic_exit() from Pycardium. It is + * asynchroneous and will return early to allow Pycardium (or a l0dable) to jump + * to the reset handler. + * + * The lifecycle task will deal with actually loading the new payload. + */ +void __epic_exit(int ret) +{ + if (ret == 0) { + LOG_INFO("lifecycle", "Payload returned successfully"); + } else { + LOG_WARN("lifecycle", "Payload returned with %d.", ret); + } + + load_menu(false); +} + +/* + * This function can be used in Epicardium to jump back to the menu. + * + * It is asynchroneous and will return immediately. The lifecycle task will + * take care of actually jumping back. + */ +void return_to_menu(void) +{ + load_menu(true); +} +/* API }}} */ + +void vLifecycleTask(void *pvParameters) +{ + lifecycle_task = xTaskGetCurrentTaskHandle(); + core1_mutex = xSemaphoreCreateMutexStatic(&core1_mutex_data); + + if (xSemaphoreTake(core1_mutex, 0) != pdTRUE) { + LOG_CRIT( + "lifecycle", "Failed to acquire mutex after creation." + ); + vTaskDelay(portMAX_DELAY); + } + + LOG_INFO("lifecycle", "Booting core 1 ..."); + core1_boot(); + vTaskDelay(pdMS_TO_TICKS(10)); + + xSemaphoreGive(core1_mutex); + + /* If `main.py` exists, start it. Otherwise, start `menu.py`. */ + if (epic_exec("main.py") < 0) { + return_to_menu(); + } + + hardware_init(); + + /* When triggered, reset core 1 to menu */ + while (1) { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + if (xSemaphoreTake(core1_mutex, BLOCK_WAIT) != pdTRUE) { + LOG_ERR("lifecycle", + "Can't load because mutex is blocked (task)."); + continue; + } + + do_load((struct load_info *)&async_load); + + xSemaphoreGive(core1_mutex); + } +} diff --git a/epicardium/modules/meson.build b/epicardium/modules/meson.build index ba1a4fd47d3827fc1f2d3d85215c089564208e9f..d7c9f40b90fe174bc81cba76cd928a210a4c2b75 100644 --- a/epicardium/modules/meson.build +++ b/epicardium/modules/meson.build @@ -4,6 +4,7 @@ module_sources = files( 'fileops.c', 'hardware.c', 'leds.c', + 'lifecycle.c', 'light_sensor.c', 'log.c', 'pmic.c', diff --git a/epicardium/modules/modules.h b/epicardium/modules/modules.h index df1a9d9b477f043433225c59eec93500e386d0af..35968547f77fb7180548ec178e3484bff37ca08e 100644 --- a/epicardium/modules/modules.h +++ b/epicardium/modules/modules.h @@ -16,6 +16,10 @@ int hardware_early_init(void); int hardware_init(void); int hardware_reset(void); +/* ---------- Lifecycle ---------------------------------------------------- */ +void vLifecycleTask(void *pvParameters); +void return_to_menu(void); + /* ---------- Serial ------------------------------------------------------- */ #define SERIAL_READ_BUFFER_SIZE 128 void vSerialTask(void *pvParameters);