Add PICO_MULTICORE_LOCKOUT_BEFORE_CORE1_STARTED (defaults to 1) to allow mutlicore_lockout to work before core 1 is started (#3019)

* Add PICO_MULTICORE_LOCKOUT_BEFORE_CORE1_STARTED (defaults to 1) to allow mutlicore_lockout to work before core 1 is started

* Add multicore_lockout_ready() and add a pico_minimize_runtime to turn off new support to save the extra bit of code needed

* add missing (void)

* comment change

* Fix issues with pico_minimize_runtime

CRT0_FAR_CALLS and MULTICORE_LOCKOUT_BEFORE_CORE1_STARTED hadn't been implemented correctly (so were always excluded), and THREAD_LOCAL was missing from docs

* add another missing (void)

* Apply suggestions from code review

Co-authored-by: Andrew Scheller <lurch@durge.org>

* comment change

* fix typo

Co-authored-by: Andrew Scheller <andrew.scheller@raspberrypi.com>

---------

Co-authored-by: William Vinnicombe <william.vinnicombe@raspberrypi.com>
Co-authored-by: Andrew Scheller <lurch@durge.org>
Co-authored-by: will-v-pi <108662275+will-v-pi@users.noreply.github.com>
Co-authored-by: Andrew Scheller <andrew.scheller@raspberrypi.com>
diff --git a/src/rp2_common/pico_flash/flash.c b/src/rp2_common/pico_flash/flash.c
index 1dc68c7..efbd023 100644
--- a/src/rp2_common/pico_flash/flash.c
+++ b/src/rp2_common/pico_flash/flash.c
@@ -6,7 +6,7 @@
 
 #include "pico/flash.h"
 #include "hardware/sync.h"
-#if PICO_FLASH_SAFE_EXECUTE_PICO_SUPPORT_MULTICORE_LOCKOUT
+#if PICO_FLASH_SAFE_EXECUTE_SUPPORT_MULTICORE_LOCKOUT
 #include "pico/multicore.h"
 #endif
 #include "pico/time.h"
@@ -30,7 +30,7 @@
 // 2. Regular pico_multicore - we need to use multicore lockout.
 // 3. FreeRTOS on core 0, no use of core 1 - we just want to disable IRQs
 // 4. FreeRTOS SMP on both cores - we need to schedule a high priority task on the other core to disable IRQs.
-// 5. FreeRTOS on one core, but application is using the other core. ** WE CANNOT SUPPORT THIS TODAY ** without
+// 5. FreeRTOS on one core, but application is using the other core. ** WE CANNOT SUPPORT THIS TODAY on RP2040 ** without
 //    the equivalent PICO_FLASH_ASSUME_COREx_SAFE (i.e. the user making sure the other core is fine)
 
 static bool default_core_init_deinit(bool init);
@@ -93,7 +93,7 @@
 #if PICO_FLASH_SAFE_EXECUTE_USE_FREERTOS_SMP
     return true;
 #endif
-#if PICO_FLASH_SAFE_EXECUTE_PICO_SUPPORT_MULTICORE_LOCKOUT
+#if PICO_FLASH_SAFE_EXECUTE_SUPPORT_MULTICORE_LOCKOUT
     if (!init) {
         return false;
     }
@@ -171,17 +171,17 @@
         }
         // todo we may get preempted here, but I think that is OK unless what is pre-empts requires
         //      the other core to be running.
-#elif PICO_FLASH_SAFE_EXECUTE_PICO_SUPPORT_MULTICORE_LOCKOUT
-        // we cannot mix multicore_lockout and FreeRTOS as they both use the multicore FIFO...
-        // the user, will have to roll their own mechanism in this case.
-#if LIB_FREERTOS_KERNEL
+#elif PICO_FLASH_SAFE_EXECUTE_SUPPORT_MULTICORE_LOCKOUT
+        // we cannot mix multicore_lockout and FreeRTOS on RP2040 as they both use the multicore FIFO...

+        // the user will have to roll their own mechanism in this case. FreeRTOS on RP2350 uses doorbells

+#if LIB_FREERTOS_KERNEL && PICO_RP2040
 #if PICO_FLASH_ASSERT_ON_UNSAFE
         assert(false); // we expect the other core to have been initialized via flash_safe_execute_core_init()
                        // unless PICO_FLASH_ASSUME_COREX_SAFE is set
 #endif
         rc = PICO_ERROR_NOT_PERMITTED;
 #else // !LIB_FREERTOS_KERNEL
-        if (multicore_lockout_victim_is_initialized(get_core_num()^1)) {
+        if (multicore_lockout_ready()) {
             if (!multicore_lockout_start_timeout_us(timeout_ms * 1000ull)) {
                 rc = PICO_ERROR_TIMEOUT;
             }
@@ -220,7 +220,7 @@
         if (lockout_state[core_num] != FREERTOS_LOCKOUT_LOCKEE_DONE) {
             return PICO_ERROR_TIMEOUT;
         }
-#elif PICO_FLASH_SAFE_EXECUTE_PICO_SUPPORT_MULTICORE_LOCKOUT
+#elif PICO_FLASH_SAFE_EXECUTE_SUPPORT_MULTICORE_LOCKOUT
         return multicore_lockout_end_timeout_us(timeout_ms * 1000ull) ? PICO_OK : PICO_ERROR_TIMEOUT;
 #endif
     }
diff --git a/src/rp2_common/pico_flash/include/pico/flash.h b/src/rp2_common/pico_flash/include/pico/flash.h
index fd43946..1d23ad8 100644
--- a/src/rp2_common/pico_flash/include/pico/flash.h
+++ b/src/rp2_common/pico_flash/include/pico/flash.h
@@ -108,10 +108,13 @@
 #endif
 #endif
 
-// PICO_CONFIG: PICO_FLASH_SAFE_EXECUTE_PICO_SUPPORT_MULTICORE_LOCKOUT, Support using multicore_lockout functions to make the other core safe during flash_safe_execute, type=bool, default=1 when using pico_multicore, group=pico_flash
-#ifndef PICO_FLASH_SAFE_EXECUTE_PICO_SUPPORT_MULTICORE_LOCKOUT
-#if LIB_PICO_MULTICORE
-#define PICO_FLASH_SAFE_EXECUTE_PICO_SUPPORT_MULTICORE_LOCKOUT 1
+// PICO_CONFIG: PICO_FLASH_SAFE_EXECUTE_SUPPORT_MULTICORE_LOCKOUT, Support using multicore_lockout functions to make the other core safe during flash_safe_execute, type=bool, default=1 when using pico_multicore, group=pico_flash
+#ifndef PICO_FLASH_SAFE_EXECUTE_SUPPORT_MULTICORE_LOCKOUT
+#ifdef PICO_FLASH_SAFE_EXECUTE_PICO_SUPPORT_MULTICORE_LOCKOUT
+// support old misspelling
+#define PICO_FLASH_SAFE_EXECUTE_SUPPORT_MULTICORE_LOCKOUT PICO_FLASH_SAFE_EXECUTE_PICO_SUPPORT_MULTICORE_LOCKOUT
+#elif LIB_PICO_MULTICORE
+#define PICO_FLASH_SAFE_EXECUTE_SUPPORT_MULTICORE_LOCKOUT 1
 #endif
 #endif
 
diff --git a/src/rp2_common/pico_multicore/include/pico/multicore.h b/src/rp2_common/pico_multicore/include/pico/multicore.h
index 6c997cb..9fc7f5d 100644
--- a/src/rp2_common/pico_multicore/include/pico/multicore.h
+++ b/src/rp2_common/pico_multicore/include/pico/multicore.h
@@ -38,6 +38,11 @@
 #endif
 #endif
 
+// PICO_CONFIG: PICO_MULTICORE_LOCKOUT_BEFORE_CORE1_STARTED, Allow multicore_lockout functions called from core 0 to succeed if core1 has not been started, type=bool, default=1, group=pico_multicore

+#ifndef PICO_MULTICORE_LOCKOUT_BEFORE_CORE1_STARTED
+#define PICO_MULTICORE_LOCKOUT_BEFORE_CORE1_STARTED 1
+#endif
+
 /**
  * \def SIO_FIFO_IRQ_NUM(core)
  * \ingroup pico_multicore
@@ -424,7 +429,7 @@
  * system that is not sufficient, and unless the other core is polling in some way, then it will need to be interrupted
  * in order to cooperatively enter a blocked state.
  *
- * These "lockout" functions use the inter core FIFOs to cause an interrupt on one core from the other, and manage
+ * These "lockout" functions use the inter-core FIFOs to cause an interrupt on one core from the other, and manage
  * waiting for the other core to enter the "locked out" state.
  *
  * The usage is that the "victim" core ... i.e the core that can be "locked out" by the other core calls
@@ -440,6 +445,12 @@
  * \ref multicore_lockout_end_timeout_us to release the lockout.
  *
  * \note Because multicore lockout uses the intercore FIFOs, the FIFOs <b>cannot</b> be used for any other purpose
+ *
+ * \note By default, for convenience, multicore_lockout_start_ functions will succeed on core 0, if core 1 has either not been started
+ * via multicore_launch_core1 functions, or has subsequently been reset via multicore_reset_core1. Therefore, it is not safe to
+ * (though equally not very likely that you would) call multicore_launch_core1 while core 0 is inside of a multicore_lockout_ function.

+ * This default behavior can be disabled by setting PICO_MULTICORE_LOCKOUT_BEFORE_CORE1_STARTED=0 in which case core 1 must be running
+ * and in the "victim initialized" state before multicore_lockout_start functions can be called on core 0
  */
 
 /*! \brief Initialize the current core such that it can be a "victim" of lockout (i.e. forced to pause in a known state by the other core)
@@ -459,8 +470,8 @@
 /*! \brief Determine if \ref multicore_lockout_victim_init() has been called on the specified core.
  *  \ingroup multicore_lockout
  *
- * \note this state persists even if the core is subsequently reset; therefore you are advised to
- * always call \ref multicore_lockout_victim_init() again after resetting a core, which had previously
+ * \note this state persists even if the core is subsequently reset (other than via  reset via pico_multicore_reset_core1);
+ * therefore you are advised to always call \ref multicore_lockout_victim_init() again after resetting a core, which had previously
  * been initialized.
  *
  * \param core_num the core number (0 or 1)
@@ -468,6 +479,18 @@
  */
 bool multicore_lockout_victim_is_initialized(uint core_num);
 
+/*! \brief Determine whether it is safe to call multicore_lockout_start functions from this core.
+ *  \ingroup multicore_lockout
+ *
+ * \return true if \ref multicore_lockout_start_blocking() and \ref multicore_lockout_start_timeout_us() may safely be called from this core
+ *
+ * \note that when PICO_MULTICORE_LOCKOUT_BEFORE_CORE1_STARTED=1 this returns true when called from core 0 if core 1 has
+ * not been launched via a multicore_launch_core1 function, or has since been reset via \ref multicore_reset_core1. Otherwise, it returns
+ * the same value as `multicore_lockout_victim_is_initialized(other_core)`. This behavior is intended to make it easier
+ * for applications which may want to perform operations on core 0, but may or may not yet have launched core 1.
+ */
+bool multicore_lockout_ready(void);
+
 /*! \brief Request the other core to pause in a known state and wait for it to do so
  *  \ingroup multicore_lockout
  *
diff --git a/src/rp2_common/pico_multicore/multicore.c b/src/rp2_common/pico_multicore/multicore.c
index ef9a9b0..759d31e 100644
--- a/src/rp2_common/pico_multicore/multicore.c
+++ b/src/rp2_common/pico_multicore/multicore.c
@@ -28,7 +28,19 @@
 // and is a no-op if set. We DO have a new `multicore_lockout_victim_deinit()` method, which can be called in a pinch after
 // the reset before calling `multicore_lockout_victim_init()` again, so that is good. We will reset the flag
 // for core1 in `multicore_reset_core1()` though as a convenience since most people will use that to reset core 1.
-static bool lockout_victim_initialized[NUM_CORES];
+
+#define CORE_STATUS_NOT_RUNNING         ((uint8_t)0)
+#if PICO_MULTICORE_LOCKOUT_BEFORE_CORE1_STARTED
+#define CORE_STATUS_LOCKOUT_DISABLED    ((uint8_t)1)
+#define CORE_STATUS_LOCKOUT_ENABLED     ((uint8_t)2)
+#else
+// we don't care about the distinction between NOT_RUNNING & LOCKOUT_DISABLED
+// when PICO_MULTICORE_LOCKOUT_BEFORE_CORE1_STARTED == 0
+#define CORE_STATUS_LOCKOUT_DISABLED    CORE_STATUS_NOT_RUNNING
+#define CORE_STATUS_LOCKOUT_ENABLED     ((uint8_t)1)
+#endif
+
+static uint8_t core_status[NUM_CORES];
 
 void multicore_fifo_push_blocking(uint32_t data) {
     multicore_fifo_push_blocking_inline(data);
@@ -123,7 +135,7 @@
     irq_set_enabled(irq_num, false);
 
     // Core 1 will be in un-initialized state
-    lockout_victim_initialized[1] = false;
+    core_status[1] = CORE_STATUS_NOT_RUNNING;
 
     // Bring core 1 back out of reset. It will drain its own mailbox FIFO, then push
     // a 0 to our mailbox to tell us it has done this.
@@ -176,7 +188,7 @@
 void multicore_launch_core1_raw(void (*entry)(void), uint32_t *sp, uint32_t vector_table) {
     // Allow for the fact that the caller may have already enabled the FIFO IRQ for their
     // own purposes (expecting FIFO content after core 1 is launched). We must disable
-    // the IRQ during the handshake, then restore afterwards.
+    // the IRQ during the handshake, then restore afterward.
     uint irq_num = SIO_FIFO_IRQ_NUM(0);
     bool enabled = irq_is_enabled(irq_num);
     irq_set_enabled(irq_num, false);
@@ -189,6 +201,9 @@
     const uint32_t cmd_sequence[] =
             {0, 0, 1, (uintptr_t) vector_table, (uintptr_t) sp, (uintptr_t) entry};
 
+#if PICO_MULTICORE_LOCKOUT_BEFORE_CORE1_STARTED
+    core_status[1] = CORE_STATUS_LOCKOUT_DISABLED; // we'll assume up front
+#endif
     uint seq = 0;
     do {
         uint cmd = cmd_sequence[seq];
@@ -249,23 +264,31 @@
     uint fifo_irq_this_core = SIO_FIFO_IRQ_NUM(core_num);
     irq_set_exclusive_handler(fifo_irq_this_core, multicore_lockout_handler);
     irq_set_enabled(fifo_irq_this_core, true);
-    lockout_victim_initialized[core_num] = true;
+    core_status[core_num] = CORE_STATUS_LOCKOUT_ENABLED;
 }
 
 void multicore_lockout_victim_deinit(void) {
     uint core_num = get_core_num();
-    if (lockout_victim_initialized[core_num]) {
+    if (core_status[core_num] == CORE_STATUS_LOCKOUT_ENABLED) {
         // On platforms other than RP2040, these are actually the same IRQ number
         // (each core only sees its own IRQ, always at the same IRQ number).
         uint fifo_irq_this_core = SIO_FIFO_IRQ_NUM(core_num);
         irq_remove_handler(fifo_irq_this_core, multicore_lockout_handler);
         irq_set_enabled(fifo_irq_this_core, false);
-        lockout_victim_initialized[core_num] = false;
+        core_status[core_num] = CORE_STATUS_LOCKOUT_DISABLED;
     }
 }
 
 static bool multicore_lockout_handshake(uint32_t request_id, absolute_time_t until) {
-    uint irq_num = SIO_FIFO_IRQ_NUM(get_core_num());
+    uint core_num = get_core_num();
+#if PICO_MULTICORE_LOCKOUT_BEFORE_CORE1_STARTED
+    if (!core_num && core_status[1] == CORE_STATUS_NOT_RUNNING) {
+        return true;
+    }
+#else
+    ((void)core_num); // SIO_FIFO_IRQ_NUM doesn't necessarily reference it
+#endif
+    uint irq_num = SIO_FIFO_IRQ_NUM(core_num);
     bool enabled = irq_is_enabled(irq_num);
     if (enabled) irq_set_enabled(irq_num, false);
     bool rc = false;
@@ -345,7 +368,17 @@
 }
 
 bool multicore_lockout_victim_is_initialized(uint core_num) {
-    return lockout_victim_initialized[core_num];
+    return core_status[core_num] == CORE_STATUS_LOCKOUT_ENABLED;
+}
+
+bool multicore_lockout_ready(void) {
+    uint core_num = get_core_num();
+#if PICO_MULTICORE_LOCKOUT_BEFORE_CORE1_STARTED
+    if (!core_num && core_status[1] == CORE_STATUS_NOT_RUNNING) {
+        return true;
+    }
+#endif
+    return multicore_lockout_victim_is_initialized(core_num ^ 1);
 }
 
 #if NUM_DOORBELLS
diff --git a/src/rp2_common/pico_runtime/CMakeLists.txt b/src/rp2_common/pico_runtime/CMakeLists.txt
index 07adbab..cdbe0cd 100644
--- a/src/rp2_common/pico_runtime/CMakeLists.txt
+++ b/src/rp2_common/pico_runtime/CMakeLists.txt
@@ -63,6 +63,8 @@
 # PANIC                 - default panic impl which brings in stdio;
 # AUTO_INIT_MUTEX       - auto init mutexes, without this you get no printf mutex either;
 # CRT0_FAR_CALLS        - use blx not bl for calls from crt0 to user overridable functions;
+# THREAD_LOCAL          - support for thread locals;
+# MULTICORE_LOCKOUT_BEFORE_CORE1_STARTED - allow multicore_lockout_start to be called on core 0 before core 1 is started
 #
 # \param\ INCLUDE The items to include
 # \param\ EXCLUDE The items to exclude
@@ -75,7 +77,9 @@
             FPGA_CHECK
             PANIC
             AUTO_INIT_MUTEX
-            THREAD_LOCAL)
+            CRT0_FAR_CALLS
+            THREAD_LOCAL
+            MULTICORE_LOCKOUT_BEFORE_CORE1_STARTED)
     cmake_parse_arguments(RUNTIME "" ""
             "INCLUDE;EXCLUDE" ${ARGN} )
     foreach (INCL_EXCL IN ITEMS INCLUDE EXCLUDE)
@@ -147,7 +151,10 @@
     if (NOT RUNTIME_INCLUDE_FPGA_CHECK)
         target_compile_definitions(${TARGET} PRIVATE PICO_NO_FPGA_CHECK=1)
     endif()
-    if (NOT RUNTIME_CRT0_FAR_CALLS)
+    if (NOT RUNTIME_INCLUDE_CRT0_FAR_CALLS)
         target_compile_definitions(${TARGET} PRIVATE PICO_CRT0_NEAR_CALLS=1)
     endif()
+    if (NOT RUNTIME_INCLUDE_MULTICORE_LOCKOUT_BEFORE_CORE1_STARTED)
+        target_compile_definitions(${TARGET} PRIVATE PICO_MULTICORE_LOCKOUT_BEFORE_CORE1_STARTED=0)
+    endif()
 endfunction()