From aafa15d02432a3d68e378b1c052a34c11aa45d3c Mon Sep 17 00:00:00 2001 From: Nicolas Rabault Date: Tue, 2 Jun 2026 10:27:18 +0200 Subject: [PATCH 1/4] feat(luos_engine): add ZEPHYR core LuosHAL port (timing + IRQ) Co-Authored-By: Claude Opus 4.8 (1M context) --- engine/HAL/ZEPHYR/luos_hal.c | 40 +++++++++++++++++++++++++++++ engine/HAL/ZEPHYR/luos_hal.h | 35 +++++++++++++++++++++++++ engine/HAL/ZEPHYR/luos_hal_config.h | 13 ++++++++++ 3 files changed, 88 insertions(+) create mode 100644 engine/HAL/ZEPHYR/luos_hal.c create mode 100644 engine/HAL/ZEPHYR/luos_hal.h create mode 100644 engine/HAL/ZEPHYR/luos_hal_config.h diff --git a/engine/HAL/ZEPHYR/luos_hal.c b/engine/HAL/ZEPHYR/luos_hal.c new file mode 100644 index 000000000..f55fc8b84 --- /dev/null +++ b/engine/HAL/ZEPHYR/luos_hal.c @@ -0,0 +1,40 @@ +/****************************************************************************** + * @file luosHAL + * @brief Luos Hardware Abstraction Layer for Zephyr (Cortex-M). + ******************************************************************************/ +#include "luos_hal.h" +#include +#include + +void LuosHAL_Init(void) +{ + /* Zephyr owns the system tick and cycle counter; nothing to start. */ +} + +/* Mask all interrupts (including the radio IRQ) so the engine can guard its RX + * ring against Serial_ReceptionWrite running from the radio ISR. Paired + * (false then true) by the engine; not nested. */ +void LuosHAL_SetIrqState(bool Enable) +{ + if (Enable) + { + __enable_irq(); + } + else + { + __disable_irq(); + } +} + +uint32_t LuosHAL_GetSystick(void) +{ + return k_uptime_get_32(); /* milliseconds */ +} + +uint64_t LuosHAL_GetTimestamp(void) +{ + return k_cyc_to_ns_floor64(k_cycle_get_64()); /* nanoseconds */ +} + +void LuosHAL_StartTimestamp(void) {} +void LuosHAL_StopTimestamp(void) {} diff --git a/engine/HAL/ZEPHYR/luos_hal.h b/engine/HAL/ZEPHYR/luos_hal.h new file mode 100644 index 000000000..574e9dfc7 --- /dev/null +++ b/engine/HAL/ZEPHYR/luos_hal.h @@ -0,0 +1,35 @@ +/****************************************************************************** + * @file luosHAL + * @brief Luos Hardware Abstraction Layer for Zephyr (Cortex-M). + * @MCU Family Zephyr / nRF54L15 + ******************************************************************************/ +#ifndef _LUOSHAL_H_ +#define _LUOSHAL_H_ + +#include +#include +#include "luos_hal_config.h" + +#define _CRITICAL + +#define BOOT_MODE_MASK 0x000000FF +#define BOOT_MODE_OFFSET 0 +#define NODE_ID_MASK 0x00FFFF00 +#define NODE_ID_OFFSET 8 + +typedef struct ll_timestamp +{ + uint32_t lower_timestamp; + uint64_t higher_timestamp; + uint32_t start_offset; +} ll_timestamp_t; + +void LuosHAL_Init(void); +void LuosHAL_SetIrqState(bool Enable); +uint32_t LuosHAL_GetSystick(void); + +uint64_t LuosHAL_GetTimestamp(void); +void LuosHAL_StartTimestamp(void); +void LuosHAL_StopTimestamp(void); + +#endif /* _LUOSHAL_H_ */ diff --git a/engine/HAL/ZEPHYR/luos_hal_config.h b/engine/HAL/ZEPHYR/luos_hal_config.h new file mode 100644 index 000000000..3a0f1be22 --- /dev/null +++ b/engine/HAL/ZEPHYR/luos_hal_config.h @@ -0,0 +1,13 @@ +/****************************************************************************** + * @file luos_hal_config + * @brief Luos HAL config for Zephyr. No bus peripheral is reserved here; the + * serial network's radio HAL owns the physical layer. + ******************************************************************************/ +#ifndef _LUOSHAL_CONFIG_H_ +#define _LUOSHAL_CONFIG_H_ + +#ifndef MCUFREQ + #define MCUFREQ 128000000 /* nRF54L15 application core max */ +#endif + +#endif /* _LUOSHAL_CONFIG_H_ */ From a025d3c65c94b030bb1bc63583ec20eda4874169 Mon Sep 17 00:00:00 2001 From: Nicolas Rabault Date: Tue, 2 Jun 2026 10:27:44 +0200 Subject: [PATCH 2/4] feat(luos_engine): add ZEPHYR serial network HAL bound to the radio byte PHY Co-Authored-By: Claude Opus 4.8 (1M context) --- .../HAL/ZEPHYR/serial_network_hal.c | 53 +++++++++++++++++++ .../HAL/ZEPHYR/serial_network_hal.h | 15 ++++++ 2 files changed, 68 insertions(+) create mode 100644 network/serial_network/HAL/ZEPHYR/serial_network_hal.c create mode 100644 network/serial_network/HAL/ZEPHYR/serial_network_hal.h diff --git a/network/serial_network/HAL/ZEPHYR/serial_network_hal.c b/network/serial_network/HAL/ZEPHYR/serial_network_hal.c new file mode 100644 index 000000000..244414929 --- /dev/null +++ b/network/serial_network/HAL/ZEPHYR/serial_network_hal.c @@ -0,0 +1,53 @@ +/****************************************************************************** + * @file serial_network_hal (ZEPHYR / radio) + * @brief The radio is a best-effort byte pipe; the serial network handles all + * framing (0x7E/size/0x81) and reassembles from the engine RX ring. + * One Luos frame (<=139 B) fits one radio packet (<=255 B): SerialHAL_Send + * issues exactly one radio_send(). + ******************************************************************************/ +#include "serial_network_hal.h" +#include "_serial_network.h" +#include "radio.h" + +static volatile bool tx_done_pending; + +/* Radio IRQ context: push received bytes straight into the engine RX ring. */ +static void on_radio_rx(const uint8_t *data, uint8_t len) +{ + Serial_ReceptionWrite((uint8_t *)data, (uint32_t)len); +} + +/* Radio IRQ context: defer TX completion out of the ISR (see SerialHAL_Loop). */ +static void on_radio_tx_done(void) +{ + tx_done_pending = true; +} + +void SerialHAL_Init(uint8_t *rx_buffer, uint32_t buffer_size) +{ + (void)rx_buffer; /* the engine owns the ring; bytes arrive via Serial_ReceptionWrite */ + (void)buffer_size; + tx_done_pending = false; + radio_init(on_radio_rx, on_radio_tx_done); +} + +/* Called from Serial_Loop() in main context. Calling Serial_TransmissionEnd here + * (rather than from on_radio_tx_done) keeps radio_send out of the radio ISR. */ +void SerialHAL_Loop(void) +{ + if (tx_done_pending) + { + tx_done_pending = false; + Serial_TransmissionEnd(); + } +} + +void SerialHAL_Send(uint8_t *data, uint16_t size) +{ + radio_send(data, (uint8_t)size); /* size <= 139; best-effort single packet */ +} + +uint8_t SerialHAL_GetPort(void) +{ + return 0; +} diff --git a/network/serial_network/HAL/ZEPHYR/serial_network_hal.h b/network/serial_network/HAL/ZEPHYR/serial_network_hal.h new file mode 100644 index 000000000..7603d270e --- /dev/null +++ b/network/serial_network/HAL/ZEPHYR/serial_network_hal.h @@ -0,0 +1,15 @@ +/****************************************************************************** + * @file serial_network_hal (ZEPHYR / radio) + * @brief Binds the Luos serial network to the 2.4 GHz radio byte PHY. + ******************************************************************************/ +#ifndef _SERIAL_NETWORK_HAL_H_ +#define _SERIAL_NETWORK_HAL_H_ + +#include + +void SerialHAL_Init(uint8_t *rx_buffer, uint32_t buffer_size); +void SerialHAL_Loop(void); +void SerialHAL_Send(uint8_t *data, uint16_t size); +uint8_t SerialHAL_GetPort(void); + +#endif /* _SERIAL_NETWORK_HAL_H_ */ From c5e9836e2067283fa533f29e860dbd1e57d6d295 Mon Sep 17 00:00:00 2001 From: Nicolas Rabault Date: Tue, 2 Jun 2026 10:33:57 +0200 Subject: [PATCH 3/4] fix(HAL/ZEPHYR): define no-op mutex macros for single-loop engine The ZEPHYR luos_hal_config.h omitted MSGALLOC_MUTEX_LOCK/UNLOCK and LUOS_MUTEX_LOCK/UNLOCK that every other HAL defines, causing build errors in luos_engine.c, service.c, luos_io.c and luos_phy.c. Co-Authored-By: Claude Opus 4.8 (1M context) --- engine/HAL/ZEPHYR/luos_hal_config.h | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/engine/HAL/ZEPHYR/luos_hal_config.h b/engine/HAL/ZEPHYR/luos_hal_config.h index 3a0f1be22..11247af17 100644 --- a/engine/HAL/ZEPHYR/luos_hal_config.h +++ b/engine/HAL/ZEPHYR/luos_hal_config.h @@ -10,4 +10,22 @@ #define MCUFREQ 128000000 /* nRF54L15 application core max */ #endif +/******************************************************************************* + * DEFINE THREAD MUTEX LOCKING AND UNLOCKING FUNCTIONS + * Engine runs in a single cooperative loop here; no-op like the bare-metal HALs. + ******************************************************************************/ +#ifndef MSGALLOC_MUTEX_LOCK + #define MSGALLOC_MUTEX_LOCK +#endif +#ifndef MSGALLOC_MUTEX_UNLOCK + #define MSGALLOC_MUTEX_UNLOCK +#endif + +#ifndef LUOS_MUTEX_LOCK + #define LUOS_MUTEX_LOCK +#endif +#ifndef LUOS_MUTEX_UNLOCK + #define LUOS_MUTEX_UNLOCK +#endif + #endif /* _LUOSHAL_CONFIG_H_ */ From 9519126f12523713cdd09bfb062e3d0d5fd8e275 Mon Sep 17 00:00:00 2001 From: Nicolas Rabault Date: Tue, 2 Jun 2026 11:54:22 +0200 Subject: [PATCH 4/4] fix(HAL/ZEPHYR): synchronous serial TX to avoid sending-flag deadlock serial_network.c busy-waits on `sending` in spots that never pump the HAL (the `while (sending == true);` spin at the top of Serial_RunTopology), and every other Luos serial HAL clears `sending` from the TX-complete path. The deferred-via-SerialHAL_Loop design deadlocked there: tx_done fired (IRQ) but Serial_TransmissionEnd was never reached, so `sending` stayed true forever. SerialHAL_Send now transmits, waits for the radio TX-done IRQ, then calls Serial_TransmissionEnd() from caller context (never the ISR, which would re-enter radio_send). Bench-verified: node detected + LED driven over the air. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../HAL/ZEPHYR/serial_network_hal.c | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/network/serial_network/HAL/ZEPHYR/serial_network_hal.c b/network/serial_network/HAL/ZEPHYR/serial_network_hal.c index 244414929..9323ece66 100644 --- a/network/serial_network/HAL/ZEPHYR/serial_network_hal.c +++ b/network/serial_network/HAL/ZEPHYR/serial_network_hal.c @@ -4,12 +4,21 @@ * framing (0x7E/size/0x81) and reassembles from the engine RX ring. * One Luos frame (<=139 B) fits one radio packet (<=255 B): SerialHAL_Send * issues exactly one radio_send(). + * + * TX completion is SYNCHRONOUS: serial_network.c busy-waits on its + * `sending` flag in places that never pump this HAL (notably the + * `while (sending == true);` spin at the top of Serial_RunTopology), and + * every other Luos serial HAL clears `sending` from the TX-complete IRQ. + * So SerialHAL_Send must leave `sending` cleared on return: it transmits, + * waits for the radio TX-done IRQ, then calls Serial_TransmissionEnd() + * from this (caller) context. It must NOT be called from the radio ISR, + * which would re-enter radio_send and corrupt the radio state machine. ******************************************************************************/ #include "serial_network_hal.h" #include "_serial_network.h" #include "radio.h" -static volatile bool tx_done_pending; +static volatile bool tx_done; /* Radio IRQ context: push received bytes straight into the engine RX ring. */ static void on_radio_rx(const uint8_t *data, uint8_t len) @@ -17,34 +26,37 @@ static void on_radio_rx(const uint8_t *data, uint8_t len) Serial_ReceptionWrite((uint8_t *)data, (uint32_t)len); } -/* Radio IRQ context: defer TX completion out of the ISR (see SerialHAL_Loop). */ +/* Radio IRQ context: a transmit has completed. */ static void on_radio_tx_done(void) { - tx_done_pending = true; + tx_done = true; } void SerialHAL_Init(uint8_t *rx_buffer, uint32_t buffer_size) { (void)rx_buffer; /* the engine owns the ring; bytes arrive via Serial_ReceptionWrite */ (void)buffer_size; - tx_done_pending = false; + tx_done = false; radio_init(on_radio_rx, on_radio_tx_done); } -/* Called from Serial_Loop() in main context. Calling Serial_TransmissionEnd here - * (rather than from on_radio_tx_done) keeps radio_send out of the radio ISR. */ void SerialHAL_Loop(void) { - if (tx_done_pending) - { - tx_done_pending = false; - Serial_TransmissionEnd(); - } + /* TX completion is handled synchronously in SerialHAL_Send. */ } void SerialHAL_Send(uint8_t *data, uint16_t size) { - radio_send(data, (uint8_t)size); /* size <= 139; best-effort single packet */ + tx_done = false; + while (!radio_send(data, (uint8_t)size)) + { + /* a previous transmit is still draining; wait for the radio to free up */ + } + while (!tx_done) + { + /* wait for this transmit to complete (tx_done set in the radio IRQ) */ + } + Serial_TransmissionEnd(); } uint8_t SerialHAL_GetPort(void)