MPSL Timeslot radio bridge between two nRF52840 – can I get below 8ms end-to-end latency?

Hi,

I am working on a project with two nRF52840 devices (nRF Connect SDK 3.2.4 / Zephyr RTOS) that are already connected via
BLE. On top of the BLE link I need a low-latency out-of-band signalling channel using the MPSL Timeslot API.

Setup:
- Sensor (peripheral): when an application event fires, it requests a single EARLIEST timeslot (NORMAL priority, ~22
ms long) and transmits 10 short beacons back-to-back inside it using a TIMER0 loop, without chaining separate NORMAL
slots.
- Dongle (central, USB host): requests an EARLIEST slot at HIGH priority and keeps extending it indefinitely via
ACTION_EXTEND until the beacon is received. It also enables RADIO_INTENSET_CRCOK_Msk to get SIGNAL_RADIO immediately
on reception and toggles a GPIO directly inside the SWI1 ISR, before any k_work is involved. Before starting, the
dongle widens the BLE connection interval to 1000 ms to give MPSL more room.

I am measuring the time between a GPIO toggle on the sensor (inside the timer callback that triggers the TX) and a
GPIO toggle on the dongle (inside the SWI1 ISR on beacon reception) with an oscilloscope. I consistently get ~8 ms.

I will attach the full source of both timeslot_radio.c files below.

Is there anything wrong or suboptimal in the approach that is causing this 8 ms? Is it possible to get below that,
given that both devices must maintain the BLE connection?

Thanks.

dongle:

#include "timeslot_radio.h"
#include "system_logging.h"

#include <zephyr/kernel.h>
#include <zephyr/sys/ring_buffer.h>
#include <zephyr/bluetooth/conn.h>
#include <mpsl_timeslot.h>
#include <nrfx.h>
#include <string.h>

LOG_MODULE_REGISTER( timeslot_radio );


/*******************************************************************************
 *  Costanti
 ******************************************************************************/
/* Slot da 100ms (massimo MPSL per singola richiesta).
 * L'approccio con ESTENSIONE prolunga il timeslot corrente invece di richiedere
 * slot NORMAL separati, evitando il SIGNAL_BLOCKED tipico dei slot periodici.
 * Se l'estensione fallisce (evento BLE urgente) si richiede subito un nuovo EARLIEST. */
#define RX_SLOT_LENGTH_US           100000U  /* 100ms – MPSL_TIMESLOT_LENGTH_MAX_US */
#define RX_SLOT_TIMER_MARGIN_US       2000U  /* TIMER0 scatta 2ms prima della fine slot */
#define RX_SLOT_EXTEND_US           100000U  /* incremento di ogni estensione (100ms) */
#define RX_TIMEOUT_MS               ( 30U * 1000U )    /* 30 secondi */

/* Parametri CI durante timeslot: 800 × 1.25ms = 1000ms, supervision 60s.
 * Il dongle (central) invia LL_CONNECTION_UPDATE_IND; il sensore deve accettare. */
#define RX_CI_TIMESLOT_MIN          800U
#define RX_CI_TIMESLOT_MAX          800U
#define RX_CI_TIMESLOT_LATENCY      0U
#define RX_CI_TIMESLOT_TIMEOUT      3200U   /* 3200 × 10ms = 32s (massimo BLE spec) */

/* Parametri CI idle (ripristino post-timeslot): 40 × 1.25ms = 50ms */
#define RX_CI_IDLE_MIN              40U
#define RX_CI_IDLE_MAX              40U
#define RX_CI_IDLE_LATENCY          0U
#define RX_CI_IDLE_TIMEOUT          400U    /* 400 × 10ms = 4s */

#define RADIO_FREQ_OFFSET           80U          /* 2480 MHz */
/* AA custom – NON usare 0x8E89BED6 (BLE advertising AA): causerebbe la
 * ricezione di ogni beacon BLE pubblicitario su ch.39, disabilitando la
 * radio prima del nostro beacon. */
#define RADIO_ACCESS_ADDRESS        0xDEADBEEFUL
#define BEACON_MAGIC                0xBEAC0001UL

#define MPSL_THREAD_STACKSIZE       1024U
#define MPSL_MSGQ_SIZE              4U
#define SLOT_RING_BUF_SIZE          16U


/*******************************************************************************
 *  Tipo beacon ricevuto
 ******************************************************************************/
/* Payload fisso 8 byte: niente campo LENGTH in testa (STATLEN=8 in PCNF1). */
typedef struct __attribute__(( packed ))
{
    uint32_t magic;
    uint8_t  serial[ 4 ];

} timeslot_radio_beacon_t;


/*******************************************************************************
 *  Stato macchina RX
 ******************************************************************************/
typedef enum
{
    RX_STATE_IDLE = 0,
    RX_STATE_REQUESTING,
    RX_STATE_POLLING,
    RX_STATE_BEACON_RECEIVED,

} rx_state_t;

typedef enum
{
    MPSL_MSG_OPEN_SESSION = 0,
    MPSL_MSG_MAKE_REQUEST,
    MPSL_MSG_CLOSE_SESSION,

} mpsl_msg_t;

typedef enum
{
    SLOT_EVT_SESSION_IDLE = 0,
    SLOT_EVT_BEACON_RECEIVED,
    SLOT_EVT_SLOT_ENTERED,
    SLOT_EVT_TIMER0_FIRED,
    SLOT_EVT_TIMER0_CRCOK_MAGIC_OK,
    SLOT_EVT_TIMER0_CRCOK_MAGIC_FAIL,
    SLOT_EVT_EXTENDED,       /* EXTEND_SUCCEEDED: slot prolungato */
    SLOT_EVT_EXTEND_FAILED,  /* EXTEND_FAILED: nuovo EARLIEST richiesto */
    SLOT_EVT_BLOCKED,        /* BLOCKED: richiesta bloccata, retry EARLIEST */
    SLOT_EVT_CANCELLED,      /* CANCELLED: slot revocato, retry EARLIEST */

} slot_evt_t;


/*******************************************************************************
 *  Parametri connessione BLE
 ******************************************************************************/
static const struct bt_le_conn_param *timeslotConnParam =
    BT_LE_CONN_PARAM( RX_CI_TIMESLOT_MIN, RX_CI_TIMESLOT_MAX,
                      RX_CI_TIMESLOT_LATENCY, RX_CI_TIMESLOT_TIMEOUT );

static const struct bt_le_conn_param *idleConnParam =
    BT_LE_CONN_PARAM( RX_CI_IDLE_MIN, RX_CI_IDLE_MAX,
                      RX_CI_IDLE_LATENCY, RX_CI_IDLE_TIMEOUT );


/*******************************************************************************
 *  Variabili globali
 ******************************************************************************/
static volatile rx_state_t rxState = RX_STATE_IDLE;
static uint8_t             targetDeviceId;
static struct bt_conn     *targetConn;
static timeslot_radio_beacon_cb_t beaconCallback;

/* Fine slot corrente in µs (da start slot): aggiornato ad ogni estensione */
static volatile uint32_t currentSlotEndUs;

/* Contatori debug – aggiornati dalla callback MPSL (ISR), letti da SWI1/work */
static volatile uint32_t dbgSlotCount;
static volatile uint32_t dbgTimer0Count;    /* SIGNAL_TIMER0 ricevuti */
static volatile uint32_t dbgExtendCount;    /* estensioni concesse da MPSL */
static volatile uint32_t dbgCrcOkCount;
static volatile uint32_t dbgMagicFailCount;
static volatile uint32_t dbgLastMagic;
static volatile uint32_t dbgAddressCount;   /* AA match rilevati (EVENTS_ADDRESS) */
static volatile uint32_t dbgCrcErrorCount;  /* pacchetti con CRC errato            */
static volatile uint32_t dbgStateAtStart;   /* NRF_RADIO->STATE all'ingresso SIGNAL_START */
static volatile uint32_t dbgLastState;      /* NRF_RADIO->STATE all'ingresso SIGNAL_TIMER0 */

static timeslot_radio_beacon_t rxBuffer;
static mpsl_timeslot_session_id_t sessionId;

/* Strutture MPSL */
static mpsl_timeslot_request_t            firstRequest;
static mpsl_timeslot_request_t            nextRequest;
static mpsl_timeslot_signal_return_param_t returnParam;

/* Ring buffer ISR → SWI */
static uint8_t slotRingBufData[ SLOT_RING_BUF_SIZE ];
static struct ring_buf slotEventRingBuf;

/* Timer timeout RX */
static struct k_timer rxTimeoutTimer;

/* Timer ritardo prima della prima request MPSL: aspetta che il CI update
 * (50ms → 1000ms) sia applicato prima che MPSL cerchi lo slot da 100ms. */
static struct k_timer mpslRequestDelayTimer;

/* Thread MPSL non-preemptible */
static struct k_thread mpslThread;
static K_THREAD_STACK_DEFINE( mpslStackArea, MPSL_THREAD_STACKSIZE );
static k_tid_t mpslThreadId;

/* Message queue */
K_MSGQ_DEFINE( mpslMsgq, sizeof( mpsl_msg_t ), MPSL_MSGQ_SIZE, 4 );

/* Work handlers */
static struct k_work sessionIdleWork;
static struct k_work beaconReceivedWork;
static struct k_work restoreConnParamWork;

static const struct gpio_dt_spec signalSwitchButton =
    GPIO_DT_SPEC_GET( DT_NODELABEL( button0 ), gpios );


/*******************************************************************************
 *  Forward declarations
 ******************************************************************************/
static mpsl_timeslot_signal_return_param_t *Timeslot_Callback(
    mpsl_timeslot_session_id_t session_id, uint32_t signal_type );
static void Mpsl_Thread( void *p1, void *p2, void *p3 );
static void Session_Idle_Work_Handler( struct k_work *work );
static void Beacon_Received_Work_Handler( struct k_work *work );
static void Restore_Conn_Param_Work_Handler( struct k_work *work );
static void Rx_Timeout_Expiry( struct k_timer *timer );
static void Mpsl_Request_Delay_Expiry( struct k_timer *timer );

/*******************************************************************************
 *  Configurazione radio RX
 ******************************************************************************/
static void Radio_Configure_Rx( void )
{
    /* Assicura che la radio sia alimentata. */
    NRF_RADIO->POWER    = 1U;
    NRF_RADIO->FREQUENCY = RADIO_FREQ_OFFSET;
    NRF_RADIO->MODE      = RADIO_MODE_MODE_Nrf_1Mbit;

    /* Access address 0x8E89BED6 con BALEN=3 */
    NRF_RADIO->BASE0   = ( RADIO_ACCESS_ADDRESS << 8U )  & 0xFFFFFF00UL;
    NRF_RADIO->PREFIX0 = ( RADIO_ACCESS_ADDRESS >> 24U ) & 0xFFUL;
    NRF_RADIO->RXADDRESSES = 1U;

    /* Nessun header (S0/LENGTH/S1 = 0): payload fisso 8 byte via STATLEN. */
    NRF_RADIO->PCNF0 = 0UL;

    /* PCNF1: MAXLEN=8, STATLEN=8, BALEN=3, little-endian */
    NRF_RADIO->PCNF1 =
        ( 8UL << RADIO_PCNF1_MAXLEN_Pos  ) |
        ( 8UL << RADIO_PCNF1_STATLEN_Pos ) |
        ( 3UL << RADIO_PCNF1_BALEN_Pos   ) |
        ( RADIO_PCNF1_ENDIAN_Little << RADIO_PCNF1_ENDIAN_Pos );

    /* CRC: 3 byte, skip addr, poly 0x100065B, init 0x555555 */
    NRF_RADIO->CRCCNF =
        ( RADIO_CRCCNF_LEN_Three    << RADIO_CRCCNF_LEN_Pos      ) |
        ( RADIO_CRCCNF_SKIPADDR_Skip << RADIO_CRCCNF_SKIPADDR_Pos );
    NRF_RADIO->CRCPOLY = 0x100065BUL;
    NRF_RADIO->CRCINIT = 0x555555UL;

    /* Shorts: READY→START, END→DISABLE */
    NRF_RADIO->SHORTS =
        RADIO_SHORTS_READY_START_Msk |
        RADIO_SHORTS_END_DISABLE_Msk;

    /* Resetta tutti gli eventi rilevanti per questo slot */
    NRF_RADIO->EVENTS_CRCOK     = 0U;
    NRF_RADIO->EVENTS_CRCERROR  = 0U;
    NRF_RADIO->EVENTS_DISABLED  = 0U;
    NRF_RADIO->EVENTS_ADDRESS   = 0U;
    NRF_RADIO->EVENTS_END       = 0U;
    NRF_RADIO->PACKETPTR = ( uint32_t )&rxBuffer;

    /* Abilita interrupt CRCOK: genera SIGNAL_RADIO appena un pacchetto valido è ricevuto.
     * Permette rilevamento immediato del beacon (latenza ~µs invece di ~100ms). */
    NRF_RADIO->INTENSET = RADIO_INTENSET_CRCOK_Msk;

    /* Avvia ricezione */
    NRF_RADIO->TASKS_RXEN = 1U;
}


/*******************************************************************************
 *  Helper interno: prepara nextRequest come EARLIEST per recovery
 ******************************************************************************/
static void Prepare_Earliest_Recovery( void )
{
    nextRequest.request_type                   = MPSL_TIMESLOT_REQ_TYPE_EARLIEST;
    nextRequest.params.earliest.hfclk          = MPSL_TIMESLOT_HFCLK_CFG_XTAL_GUARANTEED;
    nextRequest.params.earliest.priority       = MPSL_TIMESLOT_PRIORITY_HIGH;
    nextRequest.params.earliest.length_us      = RX_SLOT_LENGTH_US;
    nextRequest.params.earliest.timeout_us     = 4000000U;  /* timeout 4s */
}


/*******************************************************************************
 *  MPSL callback – ISR di massima priorità
 *
 *  Strategia: estensione slot invece di slot NORMAL separati.
 *  - SIGNAL_TIMER0 scatta 2ms prima della fine: se nessun beacon → ACTION_EXTEND
 *  - EXTEND_SUCCEEDED: ri-arma CC[0] per il prossimo allarme
 *  - EXTEND_FAILED: disabilita radio, richiede nuovo EARLIEST
 *  - BLOCKED / CANCELLED: richiede nuovo EARLIEST (retry immediato)
 *  - Beacon trovato: ACTION_END → SESSION_IDLE → session_close
 ******************************************************************************/
static mpsl_timeslot_signal_return_param_t *Timeslot_Callback(
    mpsl_timeslot_session_id_t session_id, uint32_t signal_type )
{
    ARG_UNUSED( session_id );

    switch ( signal_type )
    {
        case MPSL_TIMESLOT_SIGNAL_START:
        {
            rxState = RX_STATE_POLLING;
            dbgSlotCount++;

            /* Cattura stato radio PRIMA di configurarla */
            dbgStateAtStart = NRF_RADIO->STATE;

            /* Configura radio e avvia RX */
            Radio_Configure_Rx();

            /* Arma TIMER0: scatta 2ms prima della fine per richiedere l'estensione.
             * TIMER0 parte da 0 all'inizio dello slot e gira a 1 MHz. */
            currentSlotEndUs              = RX_SLOT_LENGTH_US;
            NRF_TIMER0->TASKS_CLEAR       = 1U;
            NRF_TIMER0->CC[ 0 ]           = RX_SLOT_LENGTH_US - RX_SLOT_TIMER_MARGIN_US;
            NRF_TIMER0->EVENTS_COMPARE[0] = 0U;
            NRF_TIMER0->INTENSET          = TIMER_INTENSET_COMPARE0_Msk;
            NRF_TIMER0->TASKS_START       = 1U;

            {
                uint8_t evt = ( uint8_t )SLOT_EVT_SLOT_ENTERED;
                ring_buf_put( &slotEventRingBuf, &evt, 1 );
                NVIC_SetPendingIRQ( SWI1_EGU1_IRQn );
            }

            returnParam.callback_action = MPSL_TIMESLOT_SIGNAL_ACTION_NONE;
            break;
        }

        case MPSL_TIMESLOT_SIGNAL_RADIO:
        {
            /* Scatta appena la radio genera EVENTS_CRCOK: rilevamento immediato del beacon.
             * Latenza: ~µs dal completamento del pacchetto RX. */
            if ( NRF_RADIO->EVENTS_CRCOK == 1U )
            {
                NRF_RADIO->EVENTS_CRCOK = 0U;
                dbgCrcOkCount++;

                bool magicOk = ( rxBuffer.magic == BEACON_MAGIC );

                if ( magicOk )
                {
                    /* Beacon valido: attendi DISABLED (END→DISABLE short in corso) e termina. */
                    while ( NRF_RADIO->EVENTS_DISABLED == 0U ) {}
                    NRF_RADIO->EVENTS_DISABLED = 0U;

                    /* Disabilita ulteriori interrupt radio e TIMER0 (non più necessari) */
                    NRF_RADIO->INTENCLR   = RADIO_INTENCLR_CRCOK_Msk;
                    NRF_TIMER0->INTENCLR  = TIMER_INTENCLR_COMPARE0_Msk;

                    rxState = RX_STATE_BEACON_RECEIVED;

                    uint8_t evt = ( uint8_t )SLOT_EVT_BEACON_RECEIVED;
                    ring_buf_put( &slotEventRingBuf, &evt, 1 );
                    evt = ( uint8_t )SLOT_EVT_TIMER0_CRCOK_MAGIC_OK;
                    ring_buf_put( &slotEventRingBuf, &evt, 1 );
                    NVIC_SetPendingIRQ( SWI1_EGU1_IRQn );

                    returnParam.callback_action = MPSL_TIMESLOT_SIGNAL_ACTION_END;
                }
                else
                {
                    /* Magic errato: attendi DISABLED e riavvia RX immediatamente. */
                    dbgMagicFailCount++;
                    dbgLastMagic = rxBuffer.magic;

                    while ( NRF_RADIO->EVENTS_DISABLED == 0U ) {}
                    NRF_RADIO->EVENTS_DISABLED = 0U;

                    NRF_RADIO->EVENTS_CRCOK    = 0U;
                    NRF_RADIO->EVENTS_CRCERROR = 0U;
                    NRF_RADIO->EVENTS_ADDRESS  = 0U;
                    NRF_RADIO->EVENTS_END      = 0U;
                    NRF_RADIO->TASKS_RXEN      = 1U;

                    uint8_t evt = ( uint8_t )SLOT_EVT_TIMER0_CRCOK_MAGIC_FAIL;
                    ring_buf_put( &slotEventRingBuf, &evt, 1 );
                    NVIC_SetPendingIRQ( SWI1_EGU1_IRQn );

                    returnParam.callback_action = MPSL_TIMESLOT_SIGNAL_ACTION_NONE;
                }
            }
            else
            {
                returnParam.callback_action = MPSL_TIMESLOT_SIGNAL_ACTION_NONE;
            }
            break;
        }

        case MPSL_TIMESLOT_SIGNAL_TIMER0:
        {
            /* SIGNAL_RADIO gestisce il rilevamento rapido; SIGNAL_TIMER0 si occupa di:
             *   a) chiudere il slot se il beacon è già stato rilevato (race con SIGNAL_RADIO)
             *   b) richiedere l'estensione se ancora nessun beacon                         */
            dbgTimer0Count++;
            dbgLastState = NRF_RADIO->STATE;

            if ( NRF_RADIO->EVENTS_ADDRESS  == 1U ) { dbgAddressCount++;  }
            if ( NRF_RADIO->EVENTS_CRCERROR == 1U ) { dbgCrcErrorCount++; }

            {
                uint8_t evt = ( uint8_t )SLOT_EVT_TIMER0_FIRED;
                ring_buf_put( &slotEventRingBuf, &evt, 1 );
                NVIC_SetPendingIRQ( SWI1_EGU1_IRQn );
            }

            if ( rxState == RX_STATE_BEACON_RECEIVED )
            {
                /* Beacon già rilevato da SIGNAL_RADIO: termina il slot. */
                if ( NRF_RADIO->STATE != 0U )
                {
                    NRF_RADIO->TASKS_DISABLE = 1U;
                    while ( NRF_RADIO->EVENTS_DISABLED == 0U ) {}
                    NRF_RADIO->EVENTS_DISABLED = 0U;
                }
                returnParam.callback_action = MPSL_TIMESLOT_SIGNAL_ACTION_END;
            }
            else
            {
                /* Nessun beacon: richiedi estensione. */
                /* Fallback: controlla CRCOK nel caso raro in cui SIGNAL_RADIO
                 * non sia ancora stato consegnato (race CRCOK/TIMER0). */
                bool crcOk   = ( NRF_RADIO->EVENTS_CRCOK == 1U );
                bool magicOk = ( rxBuffer.magic == BEACON_MAGIC );

                if ( crcOk && magicOk )
                {
                    if ( NRF_RADIO->STATE != 0U )
                    {
                        NRF_RADIO->TASKS_DISABLE = 1U;
                        while ( NRF_RADIO->EVENTS_DISABLED == 0U ) {}
                        NRF_RADIO->EVENTS_DISABLED = 0U;
                    }
                    dbgCrcOkCount++;
                    rxState = RX_STATE_BEACON_RECEIVED;

                    uint8_t evt = ( uint8_t )SLOT_EVT_BEACON_RECEIVED;
                    ring_buf_put( &slotEventRingBuf, &evt, 1 );
                    evt = ( uint8_t )SLOT_EVT_TIMER0_CRCOK_MAGIC_OK;
                    ring_buf_put( &slotEventRingBuf, &evt, 1 );
                    NVIC_SetPendingIRQ( SWI1_EGU1_IRQn );

                    returnParam.callback_action = MPSL_TIMESLOT_SIGNAL_ACTION_END;
                }
                else
                {
                    if ( crcOk && !magicOk )
                    {
                        /* Magic errato: radio DISABLED, riavvia per l'estensione */
                        dbgMagicFailCount++;
                        dbgLastMagic = rxBuffer.magic;
                        NRF_RADIO->EVENTS_CRCOK    = 0U;
                        NRF_RADIO->EVENTS_CRCERROR = 0U;
                        NRF_RADIO->EVENTS_DISABLED = 0U;
                        NRF_RADIO->EVENTS_ADDRESS  = 0U;
                        NRF_RADIO->EVENTS_END      = 0U;
                        NRF_RADIO->TASKS_RXEN      = 1U;
                    }
                    /* Richiedi estensione del timeslot corrente */
                    returnParam.callback_action         = MPSL_TIMESLOT_SIGNAL_ACTION_EXTEND;
                    returnParam.params.extend.length_us = RX_SLOT_EXTEND_US;
                }
            }
            break;
        }

        case MPSL_TIMESLOT_SIGNAL_EXTEND_SUCCEEDED:
        {
            /* Estensione concessa: aggiorna la fine slot e ri-arma CC[0].
             * TIMER0 continua a girare dal T=0 dello slot originale. */
            dbgExtendCount++;
            currentSlotEndUs += RX_SLOT_EXTEND_US;
            NRF_TIMER0->EVENTS_COMPARE[0] = 0U;
            NRF_TIMER0->CC[ 0 ]           = currentSlotEndUs - RX_SLOT_TIMER_MARGIN_US;

            {
                uint8_t evt = ( uint8_t )SLOT_EVT_EXTENDED;
                ring_buf_put( &slotEventRingBuf, &evt, 1 );
                NVIC_SetPendingIRQ( SWI1_EGU1_IRQn );
            }

            returnParam.callback_action = MPSL_TIMESLOT_SIGNAL_ACTION_NONE;
            break;
        }

        case MPSL_TIMESLOT_SIGNAL_EXTEND_FAILED:
        {
            /* Estensione negata (evento BLE urgente imminente).
             * Disabilita radio e richiedi subito un nuovo slot EARLIEST. */
            if ( NRF_RADIO->STATE != 0U )
            {
                NRF_RADIO->TASKS_DISABLE = 1U;
                while ( NRF_RADIO->EVENTS_DISABLED == 0U ) {}
                NRF_RADIO->EVENTS_DISABLED = 0U;
            }

            {
                uint8_t evt = ( uint8_t )SLOT_EVT_EXTEND_FAILED;
                ring_buf_put( &slotEventRingBuf, &evt, 1 );
                NVIC_SetPendingIRQ( SWI1_EGU1_IRQn );
            }

            Prepare_Earliest_Recovery();
            returnParam.callback_action       = MPSL_TIMESLOT_SIGNAL_ACTION_REQUEST;
            returnParam.params.request.p_next = &nextRequest;
            break;
        }

        case MPSL_TIMESLOT_SIGNAL_BLOCKED:
        {
            /* Richiesta bloccata da evento di priorità ≥ già schedulato.
             * Retry immediato con EARLIEST: MPSL trova il primo slot libero. */
            {
                uint8_t evt = ( uint8_t )SLOT_EVT_BLOCKED;
                ring_buf_put( &slotEventRingBuf, &evt, 1 );
                NVIC_SetPendingIRQ( SWI1_EGU1_IRQn );
            }

            Prepare_Earliest_Recovery();
            returnParam.callback_action       = MPSL_TIMESLOT_SIGNAL_ACTION_REQUEST;
            returnParam.params.request.p_next = &nextRequest;
            break;
        }

        case MPSL_TIMESLOT_SIGNAL_CANCELLED:
        {
            /* Slot già schedulato ma revocato da evento a priorità superiore.
             * Retry immediato con EARLIEST. */
            {
                uint8_t evt = ( uint8_t )SLOT_EVT_CANCELLED;
                ring_buf_put( &slotEventRingBuf, &evt, 1 );
                NVIC_SetPendingIRQ( SWI1_EGU1_IRQn );
            }

            Prepare_Earliest_Recovery();
            returnParam.callback_action       = MPSL_TIMESLOT_SIGNAL_ACTION_REQUEST;
            returnParam.params.request.p_next = &nextRequest;
            break;
        }

        case MPSL_TIMESLOT_SIGNAL_SESSION_IDLE:
        {
            uint8_t evt = ( uint8_t )SLOT_EVT_SESSION_IDLE;
            ring_buf_put( &slotEventRingBuf, &evt, 1 );
            NVIC_SetPendingIRQ( SWI1_EGU1_IRQn );

            returnParam.callback_action = MPSL_TIMESLOT_SIGNAL_ACTION_NONE;
            break;
        }

        default:
        {
            returnParam.callback_action = MPSL_TIMESLOT_SIGNAL_ACTION_NONE;
            break;
        }
    }

    return &returnParam;
}


/*******************************************************************************
 *  ISR SWI1_EGU1 – lettura ring buffer e submit work
 ******************************************************************************/
ISR_DIRECT_DECLARE( Timeslot_Swi1_Isr )
{
    uint8_t evt;

    while ( ring_buf_get( &slotEventRingBuf, &evt, 1 ) == 1 )
    {
        switch ( ( slot_evt_t )evt )
        {
            case SLOT_EVT_SESSION_IDLE:
                k_work_submit( &sessionIdleWork );
                break;

            case SLOT_EVT_BEACON_RECEIVED:
                gpio_pin_toggle_dt( &signalSwitchButton );
                k_work_submit( &beaconReceivedWork );
                break;

            case SLOT_EVT_SLOT_ENTERED:
                /* stateAtStart: stato radio PRIMA di Radio_Configure_Rx()
                 *   0=DISABLED 1=RXRU 2=RXIDLE 3=RX 9=TXRU 11=TX */
                LOG_INF( "slot #%u ENTRATO stateAtStart=%u",
                    dbgSlotCount, dbgStateAtStart );
                break;

            case SLOT_EVT_TIMER0_FIRED:
                /* dbgLastState: stato radio al SIGNAL_TIMER0 (dopo 99ms di RX)
                 *   0=DISABLED (END→DISABLE short: pkt ricevuto o CRC error)
                 *   3=RX       (nessun AA match – radio ancora in ascolto)
                 *   altri      (transizione in corso) */
                LOG_INF( "slot #%u TIMER0 state=%u addr=%u crcOk=%u crcErr=%u magicFail=%u",
                    dbgTimer0Count, dbgLastState,
                    dbgAddressCount, dbgCrcOkCount,
                    dbgCrcErrorCount, dbgMagicFailCount );
                break;

            case SLOT_EVT_TIMER0_CRCOK_MAGIC_OK:
                LOG_INF( "CRCOK + magic OK -> beacon valido" );
                break;

            case SLOT_EVT_TIMER0_CRCOK_MAGIC_FAIL:
                LOG_WRN( "CRCOK ma magic errato 0x%08X (atteso 0x%08X) – riavvio RX",
                    dbgLastMagic, BEACON_MAGIC );
                break;

            case SLOT_EVT_EXTENDED:
                LOG_DBG( "ext #%u: slotEnd=%ums",
                    dbgExtendCount, ( unsigned )( currentSlotEndUs / 1000U ) );
                break;

            case SLOT_EVT_EXTEND_FAILED:
                LOG_WRN( "EXTEND_FAILED dopo %ums – nuovo EARLIEST (ext=%u slot=%u)",
                    ( unsigned )( currentSlotEndUs / 1000U ),
                    dbgExtendCount, dbgSlotCount );
                break;

            case SLOT_EVT_BLOCKED:
                LOG_WRN( "BLOCKED – retry EARLIEST (slot=%u)", dbgSlotCount );
                break;

            case SLOT_EVT_CANCELLED:
                LOG_WRN( "CANCELLED – retry EARLIEST (slot=%u)", dbgSlotCount );
                break;

            default:
                break;
        }
    }

    ISR_DIRECT_PM();
    return 1;
}


/*******************************************************************************
 *  Work handler: sessione idle → chiude sessione e resetta stato
 ******************************************************************************/
static void Session_Idle_Work_Handler( struct k_work *work )
{
    ARG_UNUSED( work );

    LOG_INF( "session idle – slot=%u STATE=%u addr=%u crcOk=%u crcErr=%u magicFail=%u stato=%d",
        dbgSlotCount, dbgLastState,
        dbgAddressCount, dbgCrcOkCount,
        dbgCrcErrorCount, dbgMagicFailCount, ( int )rxState );

    mpsl_msg_t msg = MPSL_MSG_CLOSE_SESSION;
    k_msgq_put( &mpslMsgq, &msg, K_NO_WAIT );

    /* Resetta stato e ferma il timer: la sessione è terminata */
    k_timer_stop( &rxTimeoutTimer );
    k_work_submit( &restoreConnParamWork );
    rxState = RX_STATE_IDLE;
}


/*******************************************************************************
 *  Work handler: beacon ricevuto → callback applicativa
 ******************************************************************************/
static void Beacon_Received_Work_Handler( struct k_work *work )
{
    ARG_UNUSED( work );

    k_timer_stop( &rxTimeoutTimer );

    /* La sessione MPSL viene chiusa da Session_Idle_Work_Handler quando arriva
     * SIGNAL_SESSION_IDLE in risposta all'ACTION_END restituito da SIGNAL_TIMER0.
     * Non chiamare session_close qui per evitare il doppio close (-EBUSY). */
    k_work_submit( &restoreConnParamWork );

    uint8_t devId = targetDeviceId;
    rxState = RX_STATE_IDLE;

    LOG_INF( "RADIO BEACON RICEVUTO: device %d", devId );

    if ( beaconCallback != NULL )
    {
        beaconCallback( devId );
    }
}


/*******************************************************************************
 *  Work handler: ripristina la CI BLE a idle dopo il timeslot
 ******************************************************************************/
static void Restore_Conn_Param_Work_Handler( struct k_work *work )
{
    ARG_UNUSED( work );

    if ( targetConn != NULL )
    {
        bt_conn_le_param_update( targetConn, idleConnParam );
        targetConn = NULL;
    }
}


/*******************************************************************************
 *  Timer timeout RX
 ******************************************************************************/
static void Rx_Timeout_Expiry( struct k_timer *timer )
{
    ARG_UNUSED( timer );

    if ( rxState == RX_STATE_IDLE )
    {
        return;
    }

    mpsl_msg_t msg = MPSL_MSG_CLOSE_SESSION;
    k_msgq_put( &mpslMsgq, &msg, K_NO_WAIT );

    /* Ripristina CI BLE a idle */
    k_work_submit( &restoreConnParamWork );

    rxState = RX_STATE_IDLE;
    LOG_WRN( "timeout 2min, beacon non ricevuto per device %d", targetDeviceId );
}


/*******************************************************************************
 *  Thread MPSL non-preemptible
 ******************************************************************************/
static void Mpsl_Thread( void *p1, void *p2, void *p3 )
{
    ARG_UNUSED( p1 );
    ARG_UNUSED( p2 );
    ARG_UNUSED( p3 );

    mpsl_msg_t msg;

    while ( 1 )
    {
        k_msgq_get( &mpslMsgq, &msg, K_FOREVER );

        switch ( msg )
        {
            case MPSL_MSG_OPEN_SESSION:
            {
                int err = mpsl_timeslot_session_open( Timeslot_Callback, &sessionId );
                if ( err != 0 )
                {
                    LOG_ERR( "session_open fallita: %d", err );
                    rxState = RX_STATE_IDLE;
                }
                else
                {
                    LOG_INF( "session_open OK (id=%u)", ( unsigned )sessionId );
                }
                break;
            }

            case MPSL_MSG_MAKE_REQUEST:
            {
                int err = mpsl_timeslot_request( sessionId, &firstRequest );
                if ( err != 0 )
                {
                    LOG_ERR( "timeslot_request fallita: %d (sessione=%u)", err, ( unsigned )sessionId );
                    rxState = RX_STATE_IDLE;
                }
                else
                {
                    LOG_INF( "timeslot_request OK, in attesa SIGNAL_START..." );
                }
                break;
            }

            case MPSL_MSG_CLOSE_SESSION:
            {
                int err = mpsl_timeslot_session_close( sessionId );
                if ( err != 0 )
                {
                    LOG_ERR( "session_close fallita: %d", err );
                }
                break;
            }

            default:
                break;
        }
    }
}


/*******************************************************************************
 *  Timer ritardo request MPSL – scatta dopo 1s dal CI update
 ******************************************************************************/
static void Mpsl_Request_Delay_Expiry( struct k_timer *timer )
{
    ARG_UNUSED( timer );

    LOG_INF( "CI update atteso, invio MAKE_REQUEST a MPSL..." );
    mpsl_msg_t msg = MPSL_MSG_MAKE_REQUEST;
    k_msgq_put( &mpslMsgq, &msg, K_NO_WAIT );
}


/*******************************************************************************
 *  API pubblica
 ******************************************************************************/

error_mng_t Timeslot_Radio_Initialize( void )
{
    rxState           = RX_STATE_IDLE;
    targetConn        = NULL;
    beaconCallback    = NULL;
    currentSlotEndUs  = 0U;
    dbgSlotCount      = 0U;
    dbgTimer0Count    = 0U;
    dbgExtendCount    = 0U;
    dbgCrcOkCount     = 0U;
    dbgMagicFailCount = 0U;
    dbgLastMagic      = 0U;
    dbgAddressCount   = 0U;
    dbgCrcErrorCount  = 0U;
    dbgStateAtStart   = 0U;
    dbgLastState      = 0U;

    ring_buf_init( &slotEventRingBuf, sizeof( slotRingBufData ), slotRingBufData );
    k_work_init( &sessionIdleWork,       Session_Idle_Work_Handler );
    k_work_init( &beaconReceivedWork,    Beacon_Received_Work_Handler );
    k_work_init( &restoreConnParamWork,  Restore_Conn_Param_Work_Handler );
    k_timer_init( &rxTimeoutTimer,          Rx_Timeout_Expiry,          NULL );
    k_timer_init( &mpslRequestDelayTimer,   Mpsl_Request_Delay_Expiry,  NULL );

    /* Collega ISR SWI1_EGU1 */
    IRQ_DIRECT_CONNECT( SWI1_EGU1_IRQn, 1, Timeslot_Swi1_Isr, 0 );
    irq_enable( SWI1_EGU1_IRQn );

    /* Crea il thread MPSL non-preemptible */
    mpslThreadId = k_thread_create(
        &mpslThread,
        mpslStackArea,
        K_THREAD_STACK_SIZEOF( mpslStackArea ),
        Mpsl_Thread,
        NULL, NULL, NULL,
        K_PRIO_COOP( CONFIG_MPSL_THREAD_COOP_PRIO ),
        0,
        K_NO_WAIT );

    if ( mpslThreadId == NULL )
    {
        LOG_ERR( "creazione thread MPSL fallita" );
        return SYSTEM_ERROR;
    }

    k_thread_name_set( mpslThreadId, "TH_MPSL_RX" );
    LOG_INF( "inizializzato" );
    return SYSTEM_NO_ERROR;
}


void Timeslot_Radio_Register_Beacon_Callback( timeslot_radio_beacon_cb_t callback )
{
    beaconCallback = callback;
}


error_mng_t Timeslot_Radio_Rx_Start( uint8_t deviceId, struct bt_conn *conn )
{
    int err;

    if ( rxState != RX_STATE_IDLE )
    {
        LOG_WRN( "gia in corso (stato %d)", ( int )rxState );
        return SYSTEM_ERROR;
    }

    targetDeviceId = deviceId;
    targetConn     = conn;
    rxState        = RX_STATE_REQUESTING;

    /* Aumenta la CI BLE a 1000ms per massimizzare le finestre MPSL (~90% duty cycle).
     * Nessun trasferimento BLE durante il timeslot, quindi nessun impatto applicativo. */
    if ( conn != NULL )
    {
        err = bt_conn_le_param_update( conn, timeslotConnParam );

        if ( err != 0 )
        {
            LOG_ERR( "impossibile aggiornare parametri connessione (%d)", err );
        }
    }
    else
    {
        LOG_WRN( "connessione NULL, impossibile aggiornare parametri connessione" );
    }

    /* Prima richiesta EARLIEST */
    firstRequest.request_type                    = MPSL_TIMESLOT_REQ_TYPE_EARLIEST;
    firstRequest.params.earliest.hfclk           = MPSL_TIMESLOT_HFCLK_CFG_XTAL_GUARANTEED;
    /* HIGH: MPSL può preemptare gli eventi BLE connection, necessario perché
     * uno slot da 100ms non entra mai in un gap BLE da 48ms (CI=50ms).
     * Nessun dato BLE viene trasmesso durante il timeslot → OK. */
    firstRequest.params.earliest.priority        = MPSL_TIMESLOT_PRIORITY_HIGH;
    firstRequest.params.earliest.length_us       = RX_SLOT_LENGTH_US;
    firstRequest.params.earliest.timeout_us      = 4000000U;

    /* Apre la sessione immediatamente; la MAKE_REQUEST viene ritardata di 1 secondo
     * tramite mpslRequestDelayTimer per dare tempo al CI update di applicarsi.
     * Senza il ritardo, MPSL non trova uno slot da 100ms nell'intervallo BLE da 50ms. */
    mpsl_msg_t msg = MPSL_MSG_OPEN_SESSION;
    k_msgq_put( &mpslMsgq, &msg, K_NO_WAIT );
    k_timer_start( &mpslRequestDelayTimer, K_MSEC( 1000 ), K_NO_WAIT );

    /* Avvia timer timeout 2 minuti */
    k_timer_start( &rxTimeoutTimer, K_MSEC( RX_TIMEOUT_MS ), K_NO_WAIT );

    gpio_pin_configure_dt( &signalSwitchButton, GPIO_OUTPUT_INACTIVE );

    LOG_INF( "avviato per device %d (CI -> 1000ms, request tra 1s)", deviceId );
    return SYSTEM_NO_ERROR;
}

sensor:

#include "timeslot_radio.h"
#include "memory.h"

#include <zephyr/kernel.h>
#include <zephyr/sys/ring_buffer.h>
#include <zephyr/drivers/gpio.h>
#include <mpsl_timeslot.h>
#include <nrfx.h>
#include <string.h>

LOG_MODULE_REGISTER( timeslot_radio );


/*******************************************************************************
 *  Costanti
 ******************************************************************************/
/* Slot unico contenente tutti i beacon: la radio TX rimane allocata per tutta
 * la finestra, eliminando i gap inter-slot dovuti a BLOCKED/CANCELLED.
 * TX_SLOT_DISTANCE_US: intervallo tra inizio beacon N e inizio beacon N+1.
 * TX_SLOT_LENGTH_US: deve coprire l'ultimo TIMER0 + margine.
 *   = TX_TIMER_EXPIRY_US + (TX_BEACON_COUNT-1)*TX_SLOT_DISTANCE_US + margine */
#define TX_SLOT_LENGTH_US           22000U  /* 22ms: copre 10 beacon ogni 2ms */
#define TX_TIMER_EXPIRY_US            350U  /* TIMER0 primo beacon: ~40µs ramp + ~110µs TX + margine */
#define TX_SLOT_DISTANCE_US          2000U  /* 2ms tra beacon: latenza max dongle ~2ms */
#define TX_BEACON_COUNT                10U  /* beacon nel singolo slot */

#define RADIO_FREQ_OFFSET           80U         /* 2480 MHz */
#define RADIO_ACCESS_ADDRESS        0xDEADBEEFUL
#define BEACON_MAGIC                0xBEAC0001UL

#define TX_ARM_DELAY_MS             10000U  /* 10s – simula evento applicativo random */

#define MPSL_THREAD_STACKSIZE       1024U
#define MPSL_MSGQ_SIZE              4U
#define SLOT_RING_BUF_SIZE          8U


/*******************************************************************************
 *  Tipo beacon
 ******************************************************************************/
/* Payload fisso 8 byte: niente campo LENGTH in testa (STATLEN=8 in PCNF1).
 * S0/LFLEN/S1 a zero → la radio non legge/scrive alcun header prima del payload. */
typedef struct __attribute__(( packed ))
{
    uint32_t magic;         /* = BEACON_MAGIC */
    uint8_t  serial[ 4 ];  /* primi 4 byte del seriale NVS */

} timeslot_radio_beacon_t;


/*******************************************************************************
 *  Stato macchina TX
 ******************************************************************************/
typedef enum
{
    TX_STATE_IDLE = 0,
    TX_STATE_ARMED,
    TX_STATE_REQUESTING,
    TX_STATE_IN_SLOT,

} tx_state_t;

typedef enum
{
    MPSL_MSG_OPEN_SESSION = 0,
    MPSL_MSG_MAKE_REQUEST,
    MPSL_MSG_CLOSE_SESSION,

} mpsl_msg_t;

typedef enum
{
    SLOT_EVT_SESSION_IDLE = 0,
    SLOT_EVT_TX_TIMER0,     /* SIGNAL_TIMER0 scattato: conferma per ogni beacon TX */

} slot_evt_t;


/*******************************************************************************
 *  Variabili globali
 ******************************************************************************/
static volatile tx_state_t txState = TX_STATE_IDLE;
static volatile uint8_t    txCount;
static volatile uint32_t   dbgTxEndCount;   /* EVENTS_END=1 al TIMER0: TX completata */
static volatile uint32_t   dbgTxStateAtT0;  /* NRF_RADIO->STATE al TIMER0 (11=TX,0=DISABLED) */

static timeslot_radio_beacon_t txBeacon;
static mpsl_timeslot_session_id_t sessionId;

static mpsl_timeslot_request_t             firstRequest;
static mpsl_timeslot_signal_return_param_t returnParam;

static uint8_t         slotRingBufData[ SLOT_RING_BUF_SIZE ];
static struct ring_buf slotEventRingBuf;

static struct k_thread mpslThread;
static K_THREAD_STACK_DEFINE( mpslStackArea, MPSL_THREAD_STACKSIZE );
static k_tid_t mpslThreadId;

K_MSGQ_DEFINE( mpslMsgq, sizeof( mpsl_msg_t ), MPSL_MSGQ_SIZE, 4 );

static struct k_work  sessionIdleWork;
static struct k_timer txArmTimer;

extern memory_nvs_settings_t nvsSettings;

static const struct gpio_dt_spec signalChargeWireless =
    GPIO_DT_SPEC_GET( DT_NODELABEL( chg_wls_mcu ), gpios );


/*******************************************************************************
 *  Forward declarations
 ******************************************************************************/
static mpsl_timeslot_signal_return_param_t *Timeslot_Callback(
    mpsl_timeslot_session_id_t session_id, uint32_t signal_type );
static void Mpsl_Thread( void *p1, void *p2, void *p3 );
static void Session_Idle_Work_Handler( struct k_work *work );
static void Tx_Arm_Timer_Expiry( struct k_timer *timer );

/*******************************************************************************
 *  Configurazione radio TX
 ******************************************************************************/
static void Radio_Configure_Tx( void )
{
    /* Assicura che la radio sia alimentata. */
    NRF_RADIO->POWER    = 1U;
    NRF_RADIO->FREQUENCY = RADIO_FREQ_OFFSET;
    NRF_RADIO->MODE      = RADIO_MODE_MODE_Nrf_1Mbit;

    NRF_RADIO->BASE0   = ( RADIO_ACCESS_ADDRESS << 8U )  & 0xFFFFFF00UL;
    NRF_RADIO->PREFIX0 = ( RADIO_ACCESS_ADDRESS >> 24U ) & 0xFFUL;
    NRF_RADIO->TXADDRESS   = 0U;
    NRF_RADIO->RXADDRESSES = 1U;

    /* Nessun header (S0/LENGTH/S1 = 0): payload fisso 8 byte via STATLEN. */
    NRF_RADIO->PCNF0 = 0UL;

    NRF_RADIO->PCNF1 =
        ( 8UL << RADIO_PCNF1_MAXLEN_Pos  ) |
        ( 8UL << RADIO_PCNF1_STATLEN_Pos ) |
        ( 3UL << RADIO_PCNF1_BALEN_Pos   ) |
        ( RADIO_PCNF1_ENDIAN_Little << RADIO_PCNF1_ENDIAN_Pos );

    NRF_RADIO->CRCCNF =
        ( RADIO_CRCCNF_LEN_Three    << RADIO_CRCCNF_LEN_Pos      ) |
        ( RADIO_CRCCNF_SKIPADDR_Skip << RADIO_CRCCNF_SKIPADDR_Pos );
    NRF_RADIO->CRCPOLY = 0x100065BUL;
    NRF_RADIO->CRCINIT = 0x555555UL;

    NRF_RADIO->SHORTS =
        RADIO_SHORTS_READY_START_Msk |
        RADIO_SHORTS_END_DISABLE_Msk;

    NRF_RADIO->PACKETPTR      = ( uint32_t )&txBeacon;
    NRF_RADIO->EVENTS_DISABLED = 0U;
    NRF_RADIO->TASKS_TXEN      = 1U;
}


/*******************************************************************************
 *  MPSL callback – ISR massima priorità
 ******************************************************************************/
static mpsl_timeslot_signal_return_param_t *Timeslot_Callback(
    mpsl_timeslot_session_id_t session_id, uint32_t signal_type )
{
    ARG_UNUSED( session_id );

    switch ( signal_type )
    {
        case MPSL_TIMESLOT_SIGNAL_START:
        {
            txState = TX_STATE_IN_SLOT;
            Radio_Configure_Tx();

            NRF_TIMER0->TASKS_CLEAR       = 1U;
            NRF_TIMER0->CC[ 0 ]           = TX_TIMER_EXPIRY_US;
            NRF_TIMER0->EVENTS_COMPARE[0] = 0U;
            NRF_TIMER0->INTENSET          = TIMER_INTENSET_COMPARE0_Msk;
            NRF_TIMER0->TASKS_START       = 1U;

            returnParam.callback_action = MPSL_TIMESLOT_SIGNAL_ACTION_NONE;
            break;
        }

        case MPSL_TIMESLOT_SIGNAL_TIMER0:
        {
            /* Cattura stato prima di disabilitare:
             *  0=DISABLED → TX completata (END→DISABLE short scattato)
             * 11=TX       → TX ancora in corso (improbabile a 350µs) */
            dbgTxStateAtT0 = NRF_RADIO->STATE;
            if ( NRF_RADIO->EVENTS_END == 1U ) { dbgTxEndCount++; }

            /* Forza DISABLE in ogni caso (sicurezza se lo short non è ancora scattato) */
            NRF_RADIO->TASKS_DISABLE = 1U;
            while ( NRF_RADIO->EVENTS_DISABLED == 0U ) {}
            NRF_RADIO->EVENTS_DISABLED = 0U;

            txCount++;

            /* Segnala TIMER0 per log di conferma TX */
            {
                uint8_t evt = ( uint8_t )SLOT_EVT_TX_TIMER0;
                ring_buf_put( &slotEventRingBuf, &evt, 1 );
                NVIC_SetPendingIRQ( SWI1_EGU1_IRQn );
            }

            if ( txCount < TX_BEACON_COUNT )
            {
                /* Beacon successivo nello stesso slot: ri-arma CC[0] e rilancia TX.
                 * TIMER0 continua a girare dal T=0 dello SIGNAL_START: avanziamo
                 * CC[0] di TX_SLOT_DISTANCE_US rispetto al valore corrente. */
                NRF_RADIO->EVENTS_END         = 0U;
                NRF_RADIO->EVENTS_DISABLED    = 0U;
                NRF_TIMER0->CC[ 0 ]          += TX_SLOT_DISTANCE_US;
                NRF_TIMER0->EVENTS_COMPARE[0] = 0U;
                NRF_RADIO->TASKS_TXEN         = 1U;

                returnParam.callback_action = MPSL_TIMESLOT_SIGNAL_ACTION_NONE;
            }
            else
            {
                /* Tutti i beacon trasmessi: termina lo slot. */
                returnParam.callback_action = MPSL_TIMESLOT_SIGNAL_ACTION_END;
            }
            break;
        }

        case MPSL_TIMESLOT_SIGNAL_SESSION_IDLE:
        {
            uint8_t evt = ( uint8_t )SLOT_EVT_SESSION_IDLE;
            ring_buf_put( &slotEventRingBuf, &evt, 1 );
            NVIC_SetPendingIRQ( SWI1_EGU1_IRQn );

            returnParam.callback_action = MPSL_TIMESLOT_SIGNAL_ACTION_NONE;
            break;
        }

        case MPSL_TIMESLOT_SIGNAL_CANCELLED:
        default:
            returnParam.callback_action = MPSL_TIMESLOT_SIGNAL_ACTION_NONE;
            break;
    }

    return &returnParam;
}


/*******************************************************************************
 *  ISR SWI1_EGU1
 ******************************************************************************/
ISR_DIRECT_DECLARE( Timeslot_Swi1_Isr )
{
    uint8_t evt;

    while ( ring_buf_get( &slotEventRingBuf, &evt, 1 ) == 1 )
    {
        switch ( ( slot_evt_t )evt )
        {
            case SLOT_EVT_SESSION_IDLE:
                k_work_submit( &sessionIdleWork );
                break;

            case SLOT_EVT_TX_TIMER0:
                /* STATE: 0=DISABLED(TX ok, short END→DISABLE) 11=TX(ancora in corso) */
                LOG_INF( "TX beacon #%u state=%u END=%u",
                    ( unsigned )txCount, dbgTxStateAtT0, dbgTxEndCount );
                break;

            default:
                break;
        }
    }

    ISR_DIRECT_PM();
    return 1;
}


/*******************************************************************************
 *  Work handler
 ******************************************************************************/
static void Session_Idle_Work_Handler( struct k_work *work )
{
    ARG_UNUSED( work );

    mpsl_msg_t msg = MPSL_MSG_CLOSE_SESSION;
    k_msgq_put( &mpslMsgq, &msg, K_NO_WAIT );

    txState = TX_STATE_IDLE;
    txCount = 0;
    /* dbgTxEndCount==TX_BEACON_COUNT → tutte le TX completate (EVENTS_END).
     * dbgTxStateAtT0==0 (DISABLED) → radio si era già auto-disabilitata via END→DISABLE. */
    LOG_INF( "Timeslot TX: %d beacon trasmessi, END_count=%u STATE_last=%u",
        TX_BEACON_COUNT, dbgTxEndCount, dbgTxStateAtT0 );
}


/*******************************************************************************
 *  Timer expiry – scatta dopo TX_ARM_DELAY_MS e avvia la sessione MPSL
 ******************************************************************************/
static void Tx_Arm_Timer_Expiry( struct k_timer *timer )
{
    ARG_UNUSED( timer );

    gpio_pin_toggle_dt( &signalChargeWireless );

    txState = TX_STATE_REQUESTING;
    txCount = 0;

    mpsl_msg_t msg = MPSL_MSG_OPEN_SESSION;
    k_msgq_put( &mpslMsgq, &msg, K_NO_WAIT );
    msg = MPSL_MSG_MAKE_REQUEST;
    k_msgq_put( &mpslMsgq, &msg, K_NO_WAIT );

    LOG_INF( "Timeslot TX: trasmissione beacon avviata" );
}


/*******************************************************************************
 *  Thread MPSL non-preemptible
 ******************************************************************************/
static void Mpsl_Thread( void *p1, void *p2, void *p3 )
{
    ARG_UNUSED( p1 );
    ARG_UNUSED( p2 );
    ARG_UNUSED( p3 );

    mpsl_msg_t msg;

    while ( 1 )
    {
        k_msgq_get( &mpslMsgq, &msg, K_FOREVER );

        switch ( msg )
        {
            case MPSL_MSG_OPEN_SESSION:
            {
                int err = mpsl_timeslot_session_open( Timeslot_Callback, &sessionId );
                if ( err != 0 )
                {
                    LOG_ERR( "session_open fallita: %d", err );
                    txState = TX_STATE_IDLE;
                }
                else
                {
                    LOG_INF( "session_open OK (id=%u)", ( unsigned )sessionId );
                }
                break;
            }
            case MPSL_MSG_MAKE_REQUEST:
            {
                int err = mpsl_timeslot_request( sessionId, &firstRequest );
                if ( err != 0 )
                {
                    LOG_ERR( "timeslot_request fallita: %d (sessione=%u)", err, ( unsigned )sessionId );
                    txState = TX_STATE_IDLE;
                }
                else
                {
                    LOG_INF( "timeslot_request OK, in attesa SIGNAL_START..." );
                }
                break;
            }
            case MPSL_MSG_CLOSE_SESSION:
            {
                int err = mpsl_timeslot_session_close( sessionId );
                if ( err != 0 )
                {
                    LOG_ERR( "session_close fallita: %d", err );
                }
                break;
            }
            default:
                break;
        }
    }
}


/*******************************************************************************
 *  API pubblica
 ******************************************************************************/

error_mng_t Timeslot_Radio_Initialize( void )
{
    txState          = TX_STATE_IDLE;
    txCount          = 0;
    dbgTxEndCount    = 0U;
    dbgTxStateAtT0   = 0U;

    gpio_pin_configure_dt( &signalChargeWireless, GPIO_OUTPUT_INACTIVE );

    ring_buf_init( &slotEventRingBuf, sizeof( slotRingBufData ), slotRingBufData );
    k_work_init( &sessionIdleWork, Session_Idle_Work_Handler );
    k_timer_init( &txArmTimer, Tx_Arm_Timer_Expiry, NULL );

    IRQ_DIRECT_CONNECT( SWI1_EGU1_IRQn, 1, Timeslot_Swi1_Isr, 0 );
    irq_enable( SWI1_EGU1_IRQn );

    mpslThreadId = k_thread_create(
        &mpslThread, mpslStackArea,
        K_THREAD_STACK_SIZEOF( mpslStackArea ),
        Mpsl_Thread, NULL, NULL, NULL,
        K_PRIO_COOP( CONFIG_MPSL_THREAD_COOP_PRIO ), 0, K_NO_WAIT );

    if ( mpslThreadId == NULL )
    {
        LOG_ERR( "Timeslot TX: creazione thread MPSL fallita" );
        return SYSTEM_ERROR;
    }

    k_thread_name_set( mpslThreadId, "TH_MPSL_TX" );
    LOG_INF( "Timeslot TX: inizializzato" );
    return SYSTEM_NO_ERROR;
}


error_mng_t Timeslot_Radio_Tx_Arm( void )
{
    if ( txState != TX_STATE_IDLE )
    {
        LOG_WRN( "Timeslot TX: già in corso (stato %d)", ( int )txState );
        return SYSTEM_ERROR;
    }

    /* Popola il beacon ora, prima del delay, così è pronto all'avvio MPSL */
    txBeacon.magic  = BEACON_MAGIC;
    memcpy( txBeacon.serial, &nvsSettings.serial[ MEMORY_NVS_HEADER_SIZE ], 4 );

    /* Prepara la prima richiesta EARLIEST */
    firstRequest.request_type                    = MPSL_TIMESLOT_REQ_TYPE_EARLIEST;
    firstRequest.params.earliest.hfclk           = MPSL_TIMESLOT_HFCLK_CFG_XTAL_GUARANTEED;
    firstRequest.params.earliest.priority        = MPSL_TIMESLOT_PRIORITY_NORMAL;
    firstRequest.params.earliest.length_us       = TX_SLOT_LENGTH_US;  /* 22ms: tutti i beacon in un unico slot */
    firstRequest.params.earliest.timeout_us      = 1000000U;

    txState = TX_STATE_ARMED;

    /* Avvia il timer: la trasmissione parte dopo TX_ARM_DELAY_MS */
    k_timer_start( &txArmTimer, K_MSEC( TX_ARM_DELAY_MS ), K_NO_WAIT );

    LOG_INF( "Timeslot TX: armato, trasmissione tra %u ms", TX_ARM_DELAY_MS );
    return SYSTEM_NO_ERROR;
}

Related