From 0377cc49e19e22ea04936a088b9d8273ea1d7b25 Mon Sep 17 00:00:00 2001
From: Serge Bazanski <q3k@q3k.org>
Date: Thu, 3 Aug 2023 01:34:54 +0200
Subject: [PATCH] main: move interrupts around, explain reasoning

---
 main/main.c | 102 ++++++++++++++++++++++++++++++++++++++--------------
 1 file changed, 76 insertions(+), 26 deletions(-)

diff --git a/main/main.c b/main/main.c
index 12a0e09571..3b68057263 100644
--- a/main/main.c
+++ b/main/main.c
@@ -15,27 +15,45 @@
 
 #include "esp_log.h"
 #include "freertos/FreeRTOS.h"
+#include "freertos/semphr.h"
 #include "freertos/task.h"
 #include "nvs_flash.h"
 
-// Called by micropython via MICROPY_BOARD_STARTUP.
-void flow3r_startup(void) {
-    // Initialize display first as that gives us a nice splash screen.
-    st3m_gfx_init();
-    // Submit splash a couple of times to make sure we've fully flushed out the
-    // initial framebuffer (both on-ESP and in-screen) noise before we turn on
-    // the backlight.
-    for (int i = 0; i < 4; i++) {
-        st3m_gfx_splash("");
-    }
-    // Display should've flushed by now. Turn on backlight.
-    flow3r_bsp_display_set_backlight(100);
+// Initialization is unfortunately quite complicated.
+//
+// In a perfect world, we'd just init everything in the main function in order.
+// Unfortunately, the ESP32's interrupt routing is per-core, and the ESP-IDF has
+// no API to configure interrupt handling to be performed by a core different
+// than the one which performed the interrupt setup.
+//
+// Since calls to flow3r_bsp_*/st3m_* perform calls to initialize various ESP32
+// peripherals via the ESP-IDF, it means these calls set up interrupts to be
+// handled by whatever core is executing the initialization code.
+//
+// Thus, we must perform a complicated dance in which we set up parts of
+// st3m/flow3r_bsp from one core, and part from another. Since flow3r_startup is
+// called on core0, we set up another task whose only job is to initialize
+// interrupts from core1.
+//
+// In the future we might refactor the codebase to use core pinning for all
+// tasks, and then initiazlize interrupts from within tasks, effectively keeping
+// interrupts local to handler tasks. But for now, we employ the strategy of
+// generally trusting the scheduler and instead selecting which interrupts is
+// handled by which core by performing initialization here.
+//
+// And we have to actually distribute these interrupts because each Xtensa core
+// simply doesn't have enough interrupt slots to easily handle all of our
+// external peripherals. Especially with the way the ESP-IDF interrupt
+// allocation algorithm works. Core0 is already quite polluted by micropython
+// doing its own initialization there, so we effectively push most interrupts to
+// core1.
+//
+// TODO(q3k): revisit this mess.
 
-    st3m_mode_init();
-    st3m_mode_set(st3m_mode_kind_starting, "st3m");
-    st3m_mode_update_display(NULL);
+// Populated by the core1 init task whenever core1 init is done.
+static QueueHandle_t _core1_init_done_q;
 
-    // Initialize USB and console so that we get logging as early as possible.
+void _init_core1(void *unused) {
     st3m_usb_init();
     st3m_console_init();
     st3m_usb_app_conf_t app = {
@@ -59,21 +77,12 @@ void flow3r_startup(void) {
     vTaskDelay(100 / portTICK_PERIOD_MS);
 
     flow3r_bsp_i2c_init();
-    st3m_mode_set(st3m_mode_kind_starting, "fs");
-    st3m_mode_update_display(NULL);
+
     flow3r_fs_init();
-    st3m_mode_set(st3m_mode_kind_starting, "audio");
-    st3m_mode_update_display(NULL);
-    st3m_scope_init();
-    st3m_audio_init();
-    st3m_audio_set_player_function(bl00mbox_player_function);
-    st3m_mode_set(st3m_mode_kind_starting, "io");
-    st3m_mode_update_display(NULL);
     st3m_leds_init();
     st3m_io_init();
     st3m_captouch_init();
     st3m_imu_init();
-    st3m_mode_set(st3m_mode_kind_starting, "nvs");
     esp_err_t ret = nvs_flash_init();
     if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
         ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
@@ -81,6 +90,47 @@ void flow3r_startup(void) {
         nvs_flash_init();
     }
 
+    // Let the main startup code know that core1 init is done. Then sleep
+    // forever. What a waste of stack space.
+    bool done = true;
+    xQueueSend(_core1_init_done_q, &done, portMAX_DELAY);
+    for (;;) {
+        vTaskDelay(10000 / portTICK_PERIOD_MS);
+    }
+}
+
+// Called by micropython via MICROPY_BOARD_STARTUP.
+void flow3r_startup(void) {
+    _core1_init_done_q = xQueueCreate(1, sizeof(bool));
+
+    // Initialize display first as that gives us a nice splash screen.
+    st3m_gfx_init();
+    // Submit splash a couple of times to make sure we've fully flushed out the
+    // initial framebuffer (both on-ESP and in-screen) noise before we turn on
+    // the backlight.
+    for (int i = 0; i < 4; i++) {
+        st3m_gfx_splash("");
+    }
+    // Display should've flushed by now. Turn on backlight.
+    flow3r_bsp_display_set_backlight(100);
+
+    st3m_mode_init();
+    st3m_mode_set(st3m_mode_kind_starting, "core1");
+    st3m_mode_update_display(NULL);
+
+    xTaskCreatePinnedToCore(_init_core1, "core1-init", 4096, NULL,
+                            configMAX_PRIORITIES - 1, NULL, 1);
+
+    bool done;
+    xQueueReceive(_core1_init_done_q, &done, portMAX_DELAY);
+
+    st3m_mode_set(st3m_mode_kind_starting, "core0");
+    st3m_mode_update_display(NULL);
+
+    st3m_scope_init();
+    st3m_audio_init();
+    st3m_audio_set_player_function(bl00mbox_player_function);
+
     st3m_mode_set(st3m_mode_kind_starting, "micropython");
     st3m_mode_update_display(NULL);
 }
-- 
GitLab