From 7b45c6ad8d6c00be7ff42686c251e19c4fe6f2e8 Mon Sep 17 00:00:00 2001 From: Martynas Smilingis Date: Wed, 13 May 2026 17:57:20 +0200 Subject: [PATCH 1/2] lib: bm_spi_mngr: add bare metal SPI transaction manager Add a bare metal SPI transaction manager library on top of the nrfx SPIM driver. The library serializes work on a single SPIM instance by keeping pending transactions in a FIFO queue and running them back-to-back on the bus. Each transaction is a sequence of one or more TX/RX transfers with optional begin and end callbacks. It exposes both a non-blocking API (bm_spi_mngr_schedule) and a blocking API (bm_spi_mngr_perform) on top of a shared scheduling engine. Each transaction may carry its own SPIM configuration, in which case the driver is reinitialized before the first transfer if the configuration differs from the one currently in use. The library is functionally equivalent to the legacy nRF5 SDK nrf_spi_mngr module, ported to NCS conventions. Signed-off-by: Martynas Smilingis --- CODEOWNERS | 1 + doc/nrf-bm/api/api.rst | 7 + doc/nrf-bm/libraries/bm_spi_mngr.rst | 115 ++++++ .../release_notes/release_notes_changelog.rst | 4 + include/bm/bm_spi_mngr.h | 325 +++++++++++++++ lib/CMakeLists.txt | 1 + lib/Kconfig | 1 + lib/bm_spi_mngr/CMakeLists.txt | 8 + lib/bm_spi_mngr/Kconfig | 20 + lib/bm_spi_mngr/bm_spi_mngr.c | 387 ++++++++++++++++++ 10 files changed, 869 insertions(+) create mode 100644 doc/nrf-bm/libraries/bm_spi_mngr.rst create mode 100644 include/bm/bm_spi_mngr.h create mode 100644 lib/bm_spi_mngr/CMakeLists.txt create mode 100644 lib/bm_spi_mngr/Kconfig create mode 100644 lib/bm_spi_mngr/bm_spi_mngr.c diff --git a/CODEOWNERS b/CODEOWNERS index 9fb42540f1..31634bd4c5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -63,6 +63,7 @@ /lib/bm_timer/ @nrfconnect/ncs-bm /lib/boot_banner/ @nrfconnect/ncs-bm /lib/bm_scheduler/ @nrfconnect/ncs-bm +/lib/bm_spi_mngr/ @nrfconnect/ncs-bm /lib/sensorsim/ @nrfconnect/ncs-bm /lib/zephyr_queue/ @nrfconnect/ncs-eris diff --git a/doc/nrf-bm/api/api.rst b/doc/nrf-bm/api/api.rst index 898f583696..adade0f172 100644 --- a/doc/nrf-bm/api/api.rst +++ b/doc/nrf-bm/api/api.rst @@ -145,6 +145,13 @@ Bare Metal Zephyr Memory Storage (ZMS) .. doxygengroup:: bm_zms +.. _api_bm_spi_mngr: + +SPI transaction manager +======================= + +.. doxygengroup:: bm_spi_mngr + .. _api_ble_gatt_queue: GATT Queue diff --git a/doc/nrf-bm/libraries/bm_spi_mngr.rst b/doc/nrf-bm/libraries/bm_spi_mngr.rst new file mode 100644 index 0000000000..e8cb09daba --- /dev/null +++ b/doc/nrf-bm/libraries/bm_spi_mngr.rst @@ -0,0 +1,115 @@ +.. _lib_bm_spi_mngr: + +SPI transaction manager +####################### + +.. contents:: + :local: + :depth: 2 + +This library runs SPI master (SPIM) work on a single hardware instance, one job at a time. + +Overview +******** + +Applications describe work as *transactions*, where each transaction is one or more *transfers* (TX/RX steps) that run in order on the bus. +The library keeps pending transactions in a FIFO queue and runs them one after another, so the application can keep scheduling work without waiting for the bus to be free. + +The library sits on top of the nrfx SPIM driver and offers both a non-blocking mode that queues work and returns immediately, and a blocking mode that waits until the work is done. +Both share the same queue and scheduling engine. + +Each transaction can use the default SPIM configuration or supply its own. +This makes it possible to share one SPIM instance between several devices on the same bus, for example by giving each device its own chip select pin. + +Configuration +************* + +Set the :kconfig:option:`CONFIG_BM_SPI_MNGR` Kconfig option to enable the library. + +The option depends on :kconfig:option:`CONFIG_NRFX_SPIM` and selects :kconfig:option:`CONFIG_RING_BUFFER` for the internal queue. + +Initialization +============== + +The manager instance is declared using the :c:macro:`BM_SPI_MNGR_DEF` macro, specifying the instance name, queue size, and SPIM instance. +The queue size is the number of transactions that can wait in the queue, not counting the one currently running. + +Before initializing, connect and enable the SPIM interrupt for the chosen instance, for example with :c:macro:`BM_IRQ_DIRECT_CONNECT`. +The interrupt handler must forward the event to the nrfx SPIM driver. + +To initialize the manager, call the :c:func:`bm_spi_mngr_init` function with an :c:type:`nrfx_spim_config_t` configuration, created with :c:macro:`NRFX_SPIM_DEFAULT_CONFIG` and customized as needed. + +The following example shows how to declare, connect, and initialize a manager instance: + +.. code-block:: c + + #include + + BM_SPI_MNGR_DEF(spi_mgr, 4, SPIM_INST); + + static nrfx_spim_config_t spim_cfg = NRFX_SPIM_DEFAULT_CONFIG(PIN_SCK, PIN_MOSI, PIN_MISO, PIN_CSN); + + ISR_DIRECT_DECLARE(spim_isr) + { + nrfx_spim_irq_handler(spi_mgr.spim); + return 0; + } + + BM_IRQ_DIRECT_CONNECT(NRFX_IRQ_NUMBER_GET(SPIM_INST), IRQ_PRIO_LOWEST, spim_isr, 0); + irq_enable(NRFX_IRQ_NUMBER_GET(SPIM_INST)); + + bm_spi_mngr_init(&spi_mgr, &spim_cfg); + +You can uninitialize a manager instance with the :c:func:`bm_spi_mngr_uninit` function. +Do not call it while a transaction is running or while a caller is blocked in :c:func:`bm_spi_mngr_perform`, as it does not cancel pending work or release blocked callers. + +Usage +***** + +Work is described in a :c:struct:`bm_spi_mngr_transaction` structure, holding an array of transfers and the number of transfers. +Use the :c:macro:`BM_SPI_MNGR_TRANSFER` macro to set up each transfer. + +A transaction can optionally provide a begin callback, an end callback, and a per-transaction SPIM configuration. +Both callbacks may run from the SPIM interrupt handler, so keep them short, for example setting a flag. + +.. note:: + + The transaction descriptor and any configuration it points to must stay valid until the transaction finishes, because the library stores only a pointer to it. + +The following is a list of operations you can perform with this library. + +Schedule (non-blocking) +======================= + +Use the :c:func:`bm_spi_mngr_schedule` function to add a transaction to the queue and return immediately. +The transaction starts at once if the bus is idle, otherwise it runs after the transactions ahead of it. +The completion of the transaction is reported by the optional end callback. + +Perform (blocking) +================== + +Use the :c:func:`bm_spi_mngr_perform` function to schedule a single transaction and wait until it completes. + +While waiting, the function can call an optional idle function repeatedly. +Because the function blocks until the transaction finishes, do not call it from an interrupt handler. + +Busy state +========== + +Use the :c:func:`bm_spi_mngr_is_idle` function to check whether all scheduled work has finished. + +Dependencies +************ + +This library has the following |BMshort| dependencies: + +* nrfx SPIM - :kconfig:option:`CONFIG_NRFX_SPIM` +* Zephyr ring buffer - :kconfig:option:`CONFIG_RING_BUFFER` + +API documentation +***************** + +| Header file: :file:`include/bm/bm_spi_mngr.h` +| Source files: :file:`lib/bm_spi_mngr/` + +:ref:`SPI transaction manager API reference ` diff --git a/doc/nrf-bm/release_notes/release_notes_changelog.rst b/doc/nrf-bm/release_notes/release_notes_changelog.rst index 7d5ec4d0a5..7de560325b 100644 --- a/doc/nrf-bm/release_notes/release_notes_changelog.rst +++ b/doc/nrf-bm/release_notes/release_notes_changelog.rst @@ -81,6 +81,10 @@ Libraries * An issue where the :c:func:`ble_conn_params_phy_radio_mode_get` function would incorrectly return the PHY mode mask of a pending update rather than the currently active PHY mode if a PHY update initiated by the :c:func:`ble_conn_params_phy_radio_mode_set` function was still in progress. * An issue where the SoftDevice define :c:macro:`BLE_GAP_PHYS_SUPPORTED` was used instead of the PHY preferences set with Kconfig when initiating or responding to a PHY update procedure. +* Added the :ref:`lib_bm_spi_mngr` library for queued SPI master transactions on a single SPIM instance. + Enable it with the :kconfig:option:`CONFIG_BM_SPI_MNGR` Kconfig option. + See :ref:`lib_bm_spi_mngr` for an overview and :ref:`SPI transaction manager API reference ` for the full API. + Bluetooth LE Services --------------------- diff --git a/include/bm/bm_spi_mngr.h b/include/bm/bm_spi_mngr.h new file mode 100644 index 0000000000..91ed9c1bbf --- /dev/null +++ b/include/bm/bm_spi_mngr.h @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2026 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/** + * @file + * + * @defgroup bm_spi_mngr SPI transaction manager + * @{ + * + * @brief SPI master transaction queue on top of @ref nrfx_spim. + * + * @details Transactions wait in a FIFO queue and run one after another on the bus. Each + * transaction is one or more TX and RX steps in order. You can hook @c begin_callback and + * @c end_callback, and optionally pass a different @ref nrfx_spim_config_t per + * transaction. That lets you change pins between jobs, for example a different software + * chip select line. Pins must still be valid for this SPIM instance and on the same GPIO + * port when your SoC or board wiring requires it. + * + * @ref bm_spi_mngr_schedule adds a transaction and returns right away in a nonblocking + * way. When it finishes, @c end_callback runs from the SPIM interrupt. @ref + * bm_spi_mngr_perform does the same work but waits until it is done in a blocking way. + * Only use @c idle_fn from normal code, not from an interrupt handler. + * + * Connect and enable the SPIM interrupt, for example @ref BM_IRQ_DIRECT_CONNECT, before + * @ref bm_spi_mngr_init. + */ + +#ifndef BM_SPI_MNGR_H__ +#define BM_SPI_MNGR_H__ + +#include +#include +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Macro for creating a simple SPI transfer initializer. + * + * @param[in] _tx_data Pointer to data to send, or NULL if @a _tx_length is zero. + * @param[in] _tx_length Number of bytes to send, must fit in @c uint8_t. + * @param[in] _rx_data Pointer to buffer for received data, or NULL if @a _rx_length is zero. + * @param[in] _rx_length Number of bytes to receive, must fit in @c uint8_t. + */ +#define BM_SPI_MNGR_TRANSFER(_tx_data, _tx_length, _rx_data, _rx_length) \ + { \ + .tx_data = (uint8_t const *)(_tx_data), \ + .tx_length = (uint8_t)(_tx_length), \ + .rx_data = (uint8_t *)(_rx_data), \ + .rx_length = (uint8_t)(_rx_length), \ + } + +/** + * @brief Transaction end callback. + * + * @param[in] result @c 0 on success. On failure, a negative error code from nrfx SPIM or from this + * module (for example @c -EIO). + * @param[in] user_data Pointer passed through from @ref bm_spi_mngr_transaction member + * @c user_data. + */ +typedef void (*bm_spi_mngr_callback_end_t)(int result, void *user_data); + +/** + * @brief Transaction begin callback, runs from the SPIM event handler context. + * + * @param[in] user_data Pointer passed through from @ref bm_spi_mngr_transaction member + * @c user_data. + */ +typedef void (*bm_spi_mngr_callback_begin_t)(void *user_data); + +/** + * @brief SPI transfer descriptor, one segment of a transaction. + */ +struct bm_spi_mngr_transfer { + /** + * @brief Pointer to data to send. + */ + uint8_t const *tx_data; + /** + * @brief Number of bytes to send. + */ + uint8_t tx_length; + /** + * @brief Pointer to buffer for received data. + */ + uint8_t *rx_data; + /** + * @brief Number of bytes to receive. + */ + uint8_t rx_length; +}; + +/** + * @brief SPI transaction descriptor. + * + * @note If @ref required_spim_cfg is non NULL it must remain valid until the transaction + * completes. If it is NULL, the default configuration passed to @ref bm_spi_mngr_init is + * used. + * + * @note If @ref required_spim_cfg differs from the configuration currently in use, the module + * initializes the SPIM instance again before starting the transaction. + */ +struct bm_spi_mngr_transaction { + /** + * @brief User function invoked before the first transfer of the transaction starts. + */ + bm_spi_mngr_callback_begin_t begin_callback; + /** + * @brief User function invoked when the transaction completes or aborts after an error. + */ + bm_spi_mngr_callback_end_t end_callback; + /** + * @brief Opaque pointer passed to the optional begin and end callbacks. + */ + void *user_data; + /** + * @brief Array of transfers that make up the transaction. + */ + struct bm_spi_mngr_transfer const *transfers; + /** + * @brief Number of entries in @ref transfers. + */ + uint8_t number_of_transfers; + /** + * @brief Optional SPIM configuration for this transaction; NULL selects the default. + */ + nrfx_spim_config_t const *required_spim_cfg; +}; + +/** + * @brief SPI manager control block (writable runtime state). + */ +struct bm_spi_mngr_cb { + /** + * @brief Transaction currently being executed (NULL when idle). + */ + struct bm_spi_mngr_transaction const *volatile current_transaction; + /** + * @brief Default SPIM configuration (copy of the argument to @ref bm_spi_mngr_init). + */ + nrfx_spim_config_t default_configuration; + /** + * @brief Pointer to the SPIM configuration currently applied to the instance. + */ + nrfx_spim_config_t const *current_configuration; + /** + * @brief Index of the active transfer within @ref current_transaction. + */ + uint8_t volatile current_transfer_idx; +}; + +/** + * @brief Transaction queue (backing store + Zephyr @ref ring_buf). + * + * @details @ref bm_spi_mngr::queue points here. Member @c size is the maximum number of + * pending transactions (not counting the transaction currently in progress). + * Member @c byte_storage must provide @c size contiguous pointer-sized slots, + * each @c sizeof(bm_spi_mngr_transaction const *), in bytes. + */ +struct bm_spi_mngr_queue { + /** + * @brief Maximum number of pending transactions (not counting the one in progress). + */ + size_t size; + /** + * @brief Zephyr @c ring_buf used as a FIFO of transaction pointers. + */ + struct ring_buf ring; + /** + * @brief Backing memory for @c ring, at least @c size transaction pointers. + */ + uint8_t *byte_storage; +}; + +/** + * @brief SPI transaction manager instance. + * + * @note Instantiate with @ref BM_SPI_MNGR_DEF. Do not modify fields directly. + */ +struct bm_spi_mngr { + /** + * @brief Control block for this instance. + */ + struct bm_spi_mngr_cb *cb; + /** + * @brief Pending transaction queue. + */ + struct bm_spi_mngr_queue *queue; + /** + * @brief nrfx SPIM driver instance. + */ + nrfx_spim_t *spim; +}; + +/** + * @brief Macro for defining an SPI transaction manager instance. + * + * @details This macro allocates a static buffer for the transaction queue. Therefore, use it in + * only one place in the code for a given instance name. + * + * @note The queue size is the maximum number of pending transactions not counting the one that is + * running. For an empty queue with size of for example 4 elements, it is possible to schedule + * up to 5 transactions. + * + * @param[in] _name Name of the instance to be created. + * @param[in] _queue_size Size of the transaction queue (maximum number of pending transactions). + * @param[in] _spim_inst Index of the SPIM hardware instance to be used. + */ +#define BM_SPI_MNGR_DEF(_name, _queue_size, _spim_inst) \ + static uint8_t _name##_queue_bytes[(_queue_size) * \ + sizeof(struct bm_spi_mngr_transaction const *)]; \ + static struct bm_spi_mngr_queue _name##_queue = { \ + .size = (_queue_size), \ + .byte_storage = _name##_queue_bytes, \ + }; \ + static struct bm_spi_mngr_cb _name##_cb; \ + static nrfx_spim_t _name##_spim = NRFX_SPIM_INSTANCE(_spim_inst); \ + static const struct bm_spi_mngr _name = { \ + .cb = &_name##_cb, \ + .queue = &_name##_queue, \ + .spim = &_name##_spim, \ + } + +/** + * @brief Initialize the SPI manager and the underlying SPIM driver. + * + * @details Initializes the transaction queue and calls @ref nrfx_spim_init with the internal event + * handler. On success, clears the current transaction pointer and stores this SPIM + * configuration. + * + * @param[in] mgr Manager instance to initialize. + * @param[in] default_spim_cfg Pointer to the SPIM driver configuration. This configuration + * will be used whenever the scheduled transaction has + * @c required_spim_cfg set to NULL. + * + * @retval 0 On success. + * @retval -EFAULT If @p mgr, @p default_spim_cfg, the queue, or its backing storage is NULL. + * @retval -EINVAL If the queue size is zero. + * @return Negative error code from @ref nrfx_spim_init on failure. + */ +int bm_spi_mngr_init(struct bm_spi_mngr const *mgr, nrfx_spim_config_t const *default_spim_cfg); + +/** + * @brief Uninitialize the SPI manager and SPIM. + * + * @param[in] mgr Manager instance. + */ +void bm_spi_mngr_uninit(struct bm_spi_mngr const *mgr); + +/** + * @brief Schedule an SPI transaction. + * + * @details The transaction is enqueued and started as soon as the SPI bus is available, thus when + * all previously scheduled transactions have been finished (possibly immediately). + * + * If @ref bm_spi_mngr_transaction::required_spim_cfg is set to a non-NULL value, the + * module compares it with the current configuration and reinitializes the SPIM instance + * with the new parameters if any differences are found. If + * @ref bm_spi_mngr_transaction::required_spim_cfg is NULL, the default configuration + * passed to @ref bm_spi_mngr_init is used instead. + * + * @param[in] mgr SPI transaction manager instance. + * @param[in] transaction Descriptor of the transaction to be scheduled. + * + * @retval 0 On success. + * @retval -EFAULT If @p mgr, @p transaction, or its transfers array is NULL. + * @retval -EINVAL If the transaction has zero transfers. + * @retval -ENOMEM If the queue is full. + */ +int bm_spi_mngr_schedule(struct bm_spi_mngr const *mgr, + struct bm_spi_mngr_transaction const *transaction); + +/** + * @brief Schedule a transaction and wait until it is finished. + * + * @details This function schedules a transaction that consists of one or more transfers and waits + * until it is finished. + * + * @param[in] mgr SPI transaction manager instance. + * @param[in] config SPIM configuration for this transaction. If NULL, the default configuration + * passed to @ref bm_spi_mngr_init is used. + * @param[in] transfers Array of transfers to be performed. + * @param[in] number_of_transfers Number of transfers to be performed. + * @param[in] idle_fn User function called while waiting, or NULL if not needed. Must not be called + * from ISR context. + * + * @retval 0 On success. + * @retval -EFAULT If @p mgr or @p transfers is NULL. + * @retval -EINVAL If @p number_of_transfers is zero. + * @retval -ENOMEM If the queue is full. + * @return Negative error code from the @ref nrfx_spim driver or from this module during the + * transaction. + */ +int bm_spi_mngr_perform(struct bm_spi_mngr const *mgr, nrfx_spim_config_t const *config, + struct bm_spi_mngr_transfer const *transfers, + uint8_t number_of_transfers, void (*idle_fn)(void)); + +/** + * @brief Get the current state of an SPI transaction manager instance. + * + * @param[in] mgr SPI transaction manager instance. + * + * @retval true If all scheduled transactions have been finished. + * @retval false Otherwise. + */ +static inline bool bm_spi_mngr_is_idle(struct bm_spi_mngr const *mgr) +{ + return mgr->cb->current_transaction == NULL; +} + +#ifdef __cplusplus +} +#endif + +#endif /* BM_SPI_MNGR_H__ */ + +/** @} */ diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 7dfdee0391..28f59acd59 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -12,3 +12,4 @@ add_subdirectory_ifdef(CONFIG_BM_TIMER bm_timer) add_subdirectory_ifdef(CONFIG_SENSORSIM sensorsim) add_subdirectory_ifdef(CONFIG_NCS_BARE_METAL_BOOT_BANNER boot_banner) add_subdirectory_ifdef(CONFIG_ZEPHYR_QUEUE zephyr_queue) +add_subdirectory_ifdef(CONFIG_BM_SPI_MNGR bm_spi_mngr) diff --git a/lib/Kconfig b/lib/Kconfig index ccc21e05fb..0f0d2a2c46 100644 --- a/lib/Kconfig +++ b/lib/Kconfig @@ -10,6 +10,7 @@ rsource "bm_scheduler/Kconfig" rsource "bm_buttons/Kconfig" rsource "bm_gpiote/Kconfig" rsource "bm_timer/Kconfig" +rsource "bm_spi_mngr/Kconfig" rsource "sensorsim/Kconfig" rsource "boot_banner/Kconfig" rsource "zephyr_queue/Kconfig" diff --git a/lib/bm_spi_mngr/CMakeLists.txt b/lib/bm_spi_mngr/CMakeLists.txt new file mode 100644 index 0000000000..1d87e154ee --- /dev/null +++ b/lib/bm_spi_mngr/CMakeLists.txt @@ -0,0 +1,8 @@ +# +# Copyright (c) 2026 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +zephyr_library() +zephyr_library_sources(bm_spi_mngr.c) diff --git a/lib/bm_spi_mngr/Kconfig b/lib/bm_spi_mngr/Kconfig new file mode 100644 index 0000000000..59e04d0b47 --- /dev/null +++ b/lib/bm_spi_mngr/Kconfig @@ -0,0 +1,20 @@ +# +# Copyright (c) 2026 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menuconfig BM_SPI_MNGR + bool "SPI transaction manager" + depends on NRFX_SPIM + select RING_BUFFER + help + Queue SPI master transactions (multi-transfer sequences) on one SPIM instance. + +if BM_SPI_MNGR + +module=BM_SPI_MNGR +module-str=SPI transaction manager +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +endif diff --git a/lib/bm_spi_mngr/bm_spi_mngr.c b/lib/bm_spi_mngr/bm_spi_mngr.c new file mode 100644 index 0000000000..a1783b20fb --- /dev/null +++ b/lib/bm_spi_mngr/bm_spi_mngr.c @@ -0,0 +1,387 @@ +/* Copyright (c) 2026 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include + +#include +#include + +#include +#include +#include + +/* State shared between bm_spi_mngr_perform() and its internal end callback. The callback writes + * the result and clears transaction_in_progress to release the caller from its wait loop. + */ +struct bm_spi_mngr_cb_data { + bool transaction_in_progress; + int transaction_result; +}; + +/* Add one transaction pointer to the back of the queue. IRQs are locked here internally because + * the queue is accessed both from the caller of bm_spi_mngr_schedule() and from the SPIM interrupt + * handler that starts the next queued transaction when one finishes. + */ +static int queue_push(struct bm_spi_mngr const *p_bm_spi_mngr, void const *p_src) +{ + struct ring_buf *rb = &p_bm_spi_mngr->queue->ring; + const uint32_t rec = (uint32_t)sizeof(struct bm_spi_mngr_transaction const *); + int ret; + unsigned int key = irq_lock(); + + if (ring_buf_space_get(rb) < rec) { + ret = -ENOMEM; + } else { + uint32_t w = ring_buf_put(rb, (uint8_t const *)p_src, rec); + + ret = (w == rec) ? 0 : -ENOMEM; + } + + irq_unlock(key); + return ret; +} + +/* Take one transaction pointer from the front of the queue. The caller must hold the IRQ lock, + * since the queue is accessed both from start_pending_transaction() and from the SPIM interrupt + * handler that starts the next queued transaction when one finishes. + */ +static int queue_pop(struct bm_spi_mngr const *p_bm_spi_mngr, void *p_element) +{ + struct ring_buf *rb = &p_bm_spi_mngr->queue->ring; + const uint32_t rec = (uint32_t)sizeof(struct bm_spi_mngr_transaction const *); + int ret; + + if (ring_buf_size_get(rb) < rec) { + ret = -ENOENT; + } else { + uint32_t r = ring_buf_get(rb, (uint8_t *)p_element, rec); + + ret = (r == rec) ? 0 : -ENOENT; + } + + return ret; +} + +/* Start the active segment of the current transaction via nrfx_spim_xfer(). */ +static int start_transfer(struct bm_spi_mngr const *p_bm_spi_mngr) +{ + __ASSERT_NO_MSG(p_bm_spi_mngr != NULL); + + /* Use a local copy so we do not read two volatile fields in one expression. */ + uint8_t curr_transfer_idx = p_bm_spi_mngr->cb->current_transfer_idx; + struct bm_spi_mngr_cb *p_cb = p_bm_spi_mngr->cb; + struct bm_spi_mngr_transaction const *p_txn = p_cb->current_transaction; + struct bm_spi_mngr_transfer const *p_transfer = &p_txn->transfers[curr_transfer_idx]; + + nrfx_spim_xfer_desc_t xfer = NRFX_SPIM_XFER_TRX( + p_transfer->tx_data, p_transfer->tx_length, p_transfer->rx_data, + p_transfer->rx_length); + + return nrfx_spim_xfer(p_bm_spi_mngr->spim, &xfer, 0); +} + +/* If the transaction defines begin_callback, call it before the first transfer starts. */ +static void transaction_begin_signal(struct bm_spi_mngr const *p_bm_spi_mngr) +{ + __ASSERT_NO_MSG(p_bm_spi_mngr != NULL); + + struct bm_spi_mngr_transaction const *current_transaction = + p_bm_spi_mngr->cb->current_transaction; + + if (current_transaction->begin_callback != NULL) { + void *user_data = current_transaction->user_data; + + current_transaction->begin_callback(user_data); + } +} + +/* If end_callback is set, call it with the transaction result (success or error code). */ +static void transaction_end_signal(struct bm_spi_mngr const *p_bm_spi_mngr, int result) +{ + __ASSERT_NO_MSG(p_bm_spi_mngr != NULL); + + struct bm_spi_mngr_transaction const *current_transaction = + p_bm_spi_mngr->cb->current_transaction; + + if (current_transaction->end_callback != NULL) { + void *user_data = current_transaction->user_data; + + current_transaction->end_callback(result, user_data); + } +} + +static void spim_event_handler(nrfx_spim_event_t const *p_event, void *p_context); + +/* Start the next transaction from the queue. Called from the scheduling path when the bus is + * idle, and from the SPIM interrupt handler after a transaction finishes to force a switch to + * the next one. The current transaction pointer is only cleared when the queue is empty, so + * back-to-back transactions never look idle in between. + */ +static void start_pending_transaction(struct bm_spi_mngr const *p_bm_spi_mngr, + bool switch_transaction) +{ + __ASSERT_NO_MSG(p_bm_spi_mngr != NULL); + + while (1) { + bool start_transaction = false; + struct bm_spi_mngr_cb *p_cb = p_bm_spi_mngr->cb; + + /* IRQs are locked while touching the queue and the current transaction pointer + * because both are also accessed from the SPIM interrupt handler. + */ + unsigned int key = irq_lock(); + + if (switch_transaction || bm_spi_mngr_is_idle(p_bm_spi_mngr)) { + if (queue_pop(p_bm_spi_mngr, + (void *)(&p_cb->current_transaction)) == 0) { + start_transaction = true; + } else { + /* Queue is empty, mark the manager as idle. */ + p_cb->current_transaction = NULL; + } + } + irq_unlock(key); + + if (!start_transaction) { + return; + } + + /* A transaction can carry its own SPIM configuration, otherwise the default + * configuration from bm_spi_mngr_init() is used. + */ + nrfx_spim_config_t const *p_instance_cfg; + + if (p_cb->current_transaction->required_spim_cfg == NULL) { + p_instance_cfg = &p_cb->default_configuration; + } else { + p_instance_cfg = p_cb->current_transaction->required_spim_cfg; + } + + int result; + + /* Reinitialize the SPIM instance only when this transaction needs a different + * configuration than the one currently active (different pins, frequency, mode, + * and so on). + */ + if (memcmp(p_cb->current_configuration, p_instance_cfg, + sizeof(*p_instance_cfg)) != 0) { + nrfx_spim_uninit(p_bm_spi_mngr->spim); + result = nrfx_spim_init(p_bm_spi_mngr->spim, + p_instance_cfg, + spim_event_handler, + (void *)p_bm_spi_mngr); + __ASSERT_NO_MSG(result == 0); + p_cb->current_configuration = p_instance_cfg; + } + + /* Try to start first transfer for this new transaction. */ + p_cb->current_transfer_idx = 0; + + /* Execute user code if available before starting transaction. */ + transaction_begin_signal(p_bm_spi_mngr); + result = start_transfer(p_bm_spi_mngr); + + /* If transaction started successfully there is nothing more to do here now. */ + if (result == 0) { + return; + } + + /* Transfer failed to start. Notify the user that this transaction cannot be + * started and try with the next one in the next iteration of the loop. + */ + transaction_end_signal(p_bm_spi_mngr, result); + + switch_transaction = true; + } +} + +/* Handle SPIM events. Called from the SPIM interrupt when a transfer finishes. If there are more + * transfers in the current transaction, start the next one. Otherwise, notify the user and start + * the next queued transaction (if any). + */ +static void spim_event_handler(nrfx_spim_event_t const *p_event, void *p_context) +{ + __ASSERT_NO_MSG(p_event != NULL); + __ASSERT_NO_MSG(p_context != NULL); + + int result; + struct bm_spi_mngr_cb *p_cb = ((struct bm_spi_mngr const *)p_context)->cb; + + /* This callback should be called only during a transaction. */ + __ASSERT_NO_MSG(p_cb->current_transaction != NULL); + + if (p_event->type == NRFX_SPIM_EVENT_DONE) { + result = 0; + + /* Transfer finished successfully. If there is another one to be performed in the + * current transaction, start it now. Use a local variable to avoid using two + * volatile variables in one expression. + */ + uint8_t curr_transfer_idx = p_cb->current_transfer_idx; + + ++curr_transfer_idx; + if (curr_transfer_idx < p_cb->current_transaction->number_of_transfers) { + p_cb->current_transfer_idx = curr_transfer_idx; + + result = start_transfer(((struct bm_spi_mngr const *)p_context)); + + if (result == 0) { + /* The current transaction is running and its next transfer has + * been successfully started. There is nothing more to do. + */ + return; + } + /* If the next transfer could not be started due to some error, finish + * the transaction with this error code as the result. + */ + } + } else { + result = -EIO; + } + + /* The current transaction has been completed or interrupted by some error. Notify the + * user and start the next one (if there is any). Switch transactions here so that + * current_transaction is set to NULL only if there is nothing more to do, in order to + * not generate a spurious idle status (even for a moment). + */ + transaction_end_signal(((struct bm_spi_mngr const *)p_context), result); + start_pending_transaction(((struct bm_spi_mngr const *)p_context), true); +} + +int bm_spi_mngr_init(struct bm_spi_mngr const *p_bm_spi_mngr, + nrfx_spim_config_t const *p_default_spim_config) +{ + if (p_bm_spi_mngr == NULL || p_default_spim_config == NULL) { + return -EFAULT; + } + if (p_bm_spi_mngr->queue == NULL || p_bm_spi_mngr->queue->byte_storage == NULL) { + return -EFAULT; + } + if (p_bm_spi_mngr->queue->size == 0) { + return -EINVAL; + } + + struct bm_spi_mngr_queue *queue = p_bm_spi_mngr->queue; + + /* Initialize the ring buffer that holds the queued transactions. Each slot stores one + * pointer to a transaction descriptor, so the total size in bytes is the number of queue + * slots multiplied by the size of one pointer. + */ + ring_buf_init(&queue->ring, + (uint32_t)(queue->size * sizeof(struct bm_spi_mngr_transaction const *)), + queue->byte_storage); + + int err_code = nrfx_spim_init(p_bm_spi_mngr->spim, + p_default_spim_config, + spim_event_handler, + (void *)p_bm_spi_mngr); + + if (err_code == 0) { + struct bm_spi_mngr_cb *p_cb = p_bm_spi_mngr->cb; + + p_cb->current_transaction = NULL; + p_cb->default_configuration = *p_default_spim_config; + p_cb->current_configuration = &p_cb->default_configuration; + } + + return err_code; +} + +void bm_spi_mngr_uninit(struct bm_spi_mngr const *p_bm_spi_mngr) +{ + __ASSERT_NO_MSG(p_bm_spi_mngr != NULL); + + nrfx_spim_uninit(p_bm_spi_mngr->spim); + + ring_buf_reset(&p_bm_spi_mngr->queue->ring); + p_bm_spi_mngr->cb->current_transaction = NULL; +} + +int bm_spi_mngr_schedule(struct bm_spi_mngr const *p_bm_spi_mngr, + struct bm_spi_mngr_transaction const *transaction) +{ + if (p_bm_spi_mngr == NULL || transaction == NULL || transaction->transfers == NULL) { + return -EFAULT; + } + if (transaction->number_of_transfers == 0) { + return -EINVAL; + } + + int result = queue_push(p_bm_spi_mngr, &transaction); + + if (result == 0) { + /* New transaction has been successfully added to queue, + * so if we are currently idle it's time to start the job. + */ + start_pending_transaction(p_bm_spi_mngr, false); + } + + return result; +} + +/* Internal end callback used by bm_spi_mngr_perform(). Stores the transaction result and clears + * the in-progress flag, which releases the blocking caller from its wait loop. + */ +static void spi_internal_transaction_cb(int result, void *user_data) +{ + volatile struct bm_spi_mngr_cb_data *p_cb_data = user_data; + + p_cb_data->transaction_result = result; + p_cb_data->transaction_in_progress = false; +} + +int bm_spi_mngr_perform(struct bm_spi_mngr const *p_bm_spi_mngr, + nrfx_spim_config_t const *config, + struct bm_spi_mngr_transfer const *transfers, + uint8_t number_of_transfers, void (*user_function)(void)) +{ + if (p_bm_spi_mngr == NULL || transfers == NULL) { + return -EFAULT; + } + if (number_of_transfers == 0) { + return -EINVAL; + } + + volatile struct bm_spi_mngr_cb_data cb_data = { + .transaction_in_progress = true, + }; + + /* Wrap the transfers in an internal transaction so the same scheduling path can be + * reused. The internal end callback signals completion back to this function. + */ + struct bm_spi_mngr_transaction internal_transaction = { + .begin_callback = NULL, + .end_callback = spi_internal_transaction_cb, + .user_data = (void *)&cb_data, + .transfers = transfers, + .number_of_transfers = number_of_transfers, + .required_spim_cfg = config, + }; + + int result = bm_spi_mngr_schedule(p_bm_spi_mngr, &internal_transaction); + + if (result != 0) { + return result; + } + + /* The user function may sleep the CPU waiting for an interrupt, so it must not be + * called from ISR context. This assert is a precaution against silent deadlocks. If + * the function were called from an ISR with a sleeping idle hook, the SPIM interrupt + * that ends the transaction could not run, and the system would lock up with no + * indication of what went wrong. + */ + __ASSERT_NO_MSG(user_function == NULL || !k_is_in_isr()); + + /* Block until the internal end callback runs from the SPIM interrupt and clears the + * in-progress flag. + */ + while (cb_data.transaction_in_progress) { + if (user_function) { + user_function(); + } + } + + return cb_data.transaction_result; +} From 25fd73e0fbee3cdec3cbad6a38c02e7e38046452 Mon Sep 17 00:00:00 2001 From: Martynas Smilingis Date: Wed, 13 May 2026 18:01:31 +0200 Subject: [PATCH 2/2] samples: peripherals: spi_mngr: add SPI manager NOR flash sample Add a non-blocking sample that exercises read, page program, and sector erase on the on-board external NOR flash through the SPI manager library. Buttons trigger the operations and the read result is logged as a hex dump. Wire up BOARD_EXTERNAL_MEMORY_* macros (SPIM instance, SCK/MOSI/MISO/CS and WP#/RST# strap pins) in the affected board-config.h files so the sample picks up the on-board flash automatically. Signed-off-by: Martynas Smilingis --- .../bm_nrf54l15dk/include/board-config.h | 24 ++ .../bm_nrf54lm20dk/include/board-config.h | 24 ++ ...nal_flash_memory_mcuboot_variants_s115.txt | 20 ++ ...nal_flash_memory_mcuboot_variants_s145.txt | 20 ++ ...flash_memory_non-mcuboot_variants_s115.txt | 20 ++ ...flash_memory_non-mcuboot_variants_s145.txt | 20 ++ doc/nrf-bm/libraries/bm_spi_mngr.rst | 5 + doc/nrf-bm/links.txt | 1 + .../release_notes/release_notes_changelog.rst | 4 +- samples/peripherals/spi_mngr/CMakeLists.txt | 12 + samples/peripherals/spi_mngr/Kconfig | 22 ++ samples/peripherals/spi_mngr/README.rst | 96 +++++++ samples/peripherals/spi_mngr/prj.conf | 19 ++ samples/peripherals/spi_mngr/sample.yaml | 26 ++ samples/peripherals/spi_mngr/src/main.c | 267 ++++++++++++++++++ 15 files changed, 578 insertions(+), 2 deletions(-) create mode 100644 doc/nrf-bm/includes/supported_boards_with_external_flash_memory_mcuboot_variants_s115.txt create mode 100644 doc/nrf-bm/includes/supported_boards_with_external_flash_memory_mcuboot_variants_s145.txt create mode 100644 doc/nrf-bm/includes/supported_boards_with_external_flash_memory_non-mcuboot_variants_s115.txt create mode 100644 doc/nrf-bm/includes/supported_boards_with_external_flash_memory_non-mcuboot_variants_s145.txt create mode 100644 samples/peripherals/spi_mngr/CMakeLists.txt create mode 100644 samples/peripherals/spi_mngr/Kconfig create mode 100644 samples/peripherals/spi_mngr/README.rst create mode 100644 samples/peripherals/spi_mngr/prj.conf create mode 100644 samples/peripherals/spi_mngr/sample.yaml create mode 100644 samples/peripherals/spi_mngr/src/main.c diff --git a/boards/nordic/bm_nrf54l15dk/include/board-config.h b/boards/nordic/bm_nrf54l15dk/include/board-config.h index a836a40934..eb5980309c 100644 --- a/boards/nordic/bm_nrf54l15dk/include/board-config.h +++ b/boards/nordic/bm_nrf54l15dk/include/board-config.h @@ -63,6 +63,30 @@ extern "C" { #define BOARD_CONSOLE_UARTE_PIN_CTS NRF_PIN_PORT_TO_PIN_NUMBER(7, 1) #endif +/* External SPI memory pins. */ +#ifndef BOARD_EXTERNAL_MEMORY_SPIM_INST +#define BOARD_EXTERNAL_MEMORY_SPIM_INST NRF_SPIM00 +#endif + +#ifndef BOARD_EXTERNAL_MEMORY_SPIM_PIN_SCK +#define BOARD_EXTERNAL_MEMORY_SPIM_PIN_SCK NRF_PIN_PORT_TO_PIN_NUMBER(1, 2) +#endif +#ifndef BOARD_EXTERNAL_MEMORY_SPIM_PIN_MOSI +#define BOARD_EXTERNAL_MEMORY_SPIM_PIN_MOSI NRF_PIN_PORT_TO_PIN_NUMBER(2, 2) +#endif +#ifndef BOARD_EXTERNAL_MEMORY_SPIM_PIN_MISO +#define BOARD_EXTERNAL_MEMORY_SPIM_PIN_MISO NRF_PIN_PORT_TO_PIN_NUMBER(4, 2) +#endif +#ifndef BOARD_EXTERNAL_MEMORY_SPIM_PIN_CSN +#define BOARD_EXTERNAL_MEMORY_SPIM_PIN_CSN NRF_PIN_PORT_TO_PIN_NUMBER(5, 2) +#endif +#ifndef BOARD_EXTERNAL_MEMORY_PIN_WP +#define BOARD_EXTERNAL_MEMORY_PIN_WP NRF_PIN_PORT_TO_PIN_NUMBER(3, 2) +#endif +#ifndef BOARD_EXTERNAL_MEMORY_PIN_RST +#define BOARD_EXTERNAL_MEMORY_PIN_RST NRF_PIN_PORT_TO_PIN_NUMBER(0, 2) +#endif + /* Application UART configuration */ #ifndef BOARD_APP_UARTE_INST #define BOARD_APP_UARTE_INST NRF_UARTE30 diff --git a/boards/nordic/bm_nrf54lm20dk/include/board-config.h b/boards/nordic/bm_nrf54lm20dk/include/board-config.h index b369da77f0..80a30c99a5 100644 --- a/boards/nordic/bm_nrf54lm20dk/include/board-config.h +++ b/boards/nordic/bm_nrf54lm20dk/include/board-config.h @@ -63,6 +63,30 @@ extern "C" { #define BOARD_CONSOLE_UARTE_PIN_CTS NRF_PIN_PORT_TO_PIN_NUMBER(19, 1) #endif +/* External SPI memory pins. */ +#ifndef BOARD_EXTERNAL_MEMORY_SPIM_INST +#define BOARD_EXTERNAL_MEMORY_SPIM_INST NRF_SPIM00 +#endif + +#ifndef BOARD_EXTERNAL_MEMORY_SPIM_PIN_SCK +#define BOARD_EXTERNAL_MEMORY_SPIM_PIN_SCK NRF_PIN_PORT_TO_PIN_NUMBER(1, 2) +#endif +#ifndef BOARD_EXTERNAL_MEMORY_SPIM_PIN_MOSI +#define BOARD_EXTERNAL_MEMORY_SPIM_PIN_MOSI NRF_PIN_PORT_TO_PIN_NUMBER(2, 2) +#endif +#ifndef BOARD_EXTERNAL_MEMORY_SPIM_PIN_MISO +#define BOARD_EXTERNAL_MEMORY_SPIM_PIN_MISO NRF_PIN_PORT_TO_PIN_NUMBER(4, 2) +#endif +#ifndef BOARD_EXTERNAL_MEMORY_SPIM_PIN_CSN +#define BOARD_EXTERNAL_MEMORY_SPIM_PIN_CSN NRF_PIN_PORT_TO_PIN_NUMBER(5, 2) +#endif +#ifndef BOARD_EXTERNAL_MEMORY_PIN_WP +#define BOARD_EXTERNAL_MEMORY_PIN_WP NRF_PIN_PORT_TO_PIN_NUMBER(3, 2) +#endif +#ifndef BOARD_EXTERNAL_MEMORY_PIN_RST +#define BOARD_EXTERNAL_MEMORY_PIN_RST NRF_PIN_PORT_TO_PIN_NUMBER(0, 2) +#endif + /* Application UART configuration */ #ifndef BOARD_APP_UARTE_INST #define BOARD_APP_UARTE_INST NRF_UARTE30 diff --git a/doc/nrf-bm/includes/supported_boards_with_external_flash_memory_mcuboot_variants_s115.txt b/doc/nrf-bm/includes/supported_boards_with_external_flash_memory_mcuboot_variants_s115.txt new file mode 100644 index 0000000000..19eac915c1 --- /dev/null +++ b/doc/nrf-bm/includes/supported_boards_with_external_flash_memory_mcuboot_variants_s115.txt @@ -0,0 +1,20 @@ +**S115**: + + .. list-table:: + :header-rows: 1 + + * - Hardware platform + - PCA + - Board target + * - `nRF54L15 DK`_ + - PCA10156 + - bm_nrf54l15dk/nrf54l15/cpuapp/s115_softdevice/mcuboot + * - `nRF54L15 DK`_ (emulating nRF54L10) + - PCA10156 + - bm_nrf54l15dk/nrf54l10/cpuapp/s115_softdevice/mcuboot + * - `nRF54L15 DK`_ (emulating nRF54L05) + - PCA10156 + - bm_nrf54l15dk/nrf54l05/cpuapp/s115_softdevice/mcuboot + * - `nRF54LM20 DK`_ + - PCA10184 + - bm_nrf54lm20dk/nrf54lm20a/cpuapp/s115_softdevice/mcuboot diff --git a/doc/nrf-bm/includes/supported_boards_with_external_flash_memory_mcuboot_variants_s145.txt b/doc/nrf-bm/includes/supported_boards_with_external_flash_memory_mcuboot_variants_s145.txt new file mode 100644 index 0000000000..84a05e32ed --- /dev/null +++ b/doc/nrf-bm/includes/supported_boards_with_external_flash_memory_mcuboot_variants_s145.txt @@ -0,0 +1,20 @@ +**S145**: + + .. list-table:: + :header-rows: 1 + + * - Hardware platform + - PCA + - Board target + * - `nRF54L15 DK`_ + - PCA10156 + - bm_nrf54l15dk/nrf54l15/cpuapp/s145_softdevice/mcuboot + * - `nRF54L15 DK`_ (emulating nRF54L10) + - PCA10156 + - bm_nrf54l15dk/nrf54l10/cpuapp/s145_softdevice/mcuboot + * - `nRF54L15 DK`_ (emulating nRF54L05) + - PCA10156 + - bm_nrf54l15dk/nrf54l05/cpuapp/s145_softdevice/mcuboot + * - `nRF54LM20 DK`_ + - PCA10184 + - bm_nrf54lm20dk/nrf54lm20a/cpuapp/s145_softdevice/mcuboot diff --git a/doc/nrf-bm/includes/supported_boards_with_external_flash_memory_non-mcuboot_variants_s115.txt b/doc/nrf-bm/includes/supported_boards_with_external_flash_memory_non-mcuboot_variants_s115.txt new file mode 100644 index 0000000000..470db3ef92 --- /dev/null +++ b/doc/nrf-bm/includes/supported_boards_with_external_flash_memory_non-mcuboot_variants_s115.txt @@ -0,0 +1,20 @@ +**S115**: + + .. list-table:: + :header-rows: 1 + + * - Hardware platform + - PCA + - Board target + * - `nRF54L15 DK`_ + - PCA10156 + - bm_nrf54l15dk/nrf54l15/cpuapp/s115_softdevice + * - `nRF54L15 DK`_ (emulating nRF54L10) + - PCA10156 + - bm_nrf54l15dk/nrf54l10/cpuapp/s115_softdevice + * - `nRF54L15 DK`_ (emulating nRF54L05) + - PCA10156 + - bm_nrf54l15dk/nrf54l05/cpuapp/s115_softdevice + * - `nRF54LM20 DK`_ + - PCA10184 + - bm_nrf54lm20dk/nrf54lm20a/cpuapp/s115_softdevice diff --git a/doc/nrf-bm/includes/supported_boards_with_external_flash_memory_non-mcuboot_variants_s145.txt b/doc/nrf-bm/includes/supported_boards_with_external_flash_memory_non-mcuboot_variants_s145.txt new file mode 100644 index 0000000000..0b6a2f86e3 --- /dev/null +++ b/doc/nrf-bm/includes/supported_boards_with_external_flash_memory_non-mcuboot_variants_s145.txt @@ -0,0 +1,20 @@ +**S145**: + + .. list-table:: + :header-rows: 1 + + * - Hardware platform + - PCA + - Board target + * - `nRF54L15 DK`_ + - PCA10156 + - bm_nrf54l15dk/nrf54l15/cpuapp/s145_softdevice + * - `nRF54L15 DK`_ (emulating nRF54L10) + - PCA10156 + - bm_nrf54l15dk/nrf54l10/cpuapp/s145_softdevice + * - `nRF54L15 DK`_ (emulating nRF54L05) + - PCA10156 + - bm_nrf54l15dk/nrf54l05/cpuapp/s145_softdevice + * - `nRF54LM20 DK`_ + - PCA10184 + - bm_nrf54lm20dk/nrf54lm20a/cpuapp/s145_softdevice diff --git a/doc/nrf-bm/libraries/bm_spi_mngr.rst b/doc/nrf-bm/libraries/bm_spi_mngr.rst index e8cb09daba..b2ad50d74b 100644 --- a/doc/nrf-bm/libraries/bm_spi_mngr.rst +++ b/doc/nrf-bm/libraries/bm_spi_mngr.rst @@ -98,6 +98,11 @@ Busy state Use the :c:func:`bm_spi_mngr_is_idle` function to check whether all scheduled work has finished. +Sample +****** + +The usage of this library is demonstrated in the :ref:`spi_mngr_sample` sample. + Dependencies ************ diff --git a/doc/nrf-bm/links.txt b/doc/nrf-bm/links.txt index a5ab83af7d..a7fd996c6a 100644 --- a/doc/nrf-bm/links.txt +++ b/doc/nrf-bm/links.txt @@ -91,6 +91,7 @@ .. _`How to flash an application`: https://docs.nordicsemi.com/bundle/nrf-connect-vscode/page/get_started/quick_debug.html#how-to-flash-an-application .. _`How to connect to the terminal`: https://docs.nordicsemi.com/bundle/nrf-connect-vscode/page/get_started/quick_debug.html#how-to-connect-to-the-terminal .. _`nRF Connect for Desktop`: https://www.nordicsemi.com/Software-and-Tools/Development-Tools/nRF-Connect-for-desktop +.. _Board Configurator: https://docs.nordicsemi.com/bundle/nrf-connect-board-configurator/page/index.html .. _`additional requirements`: https://docs.nordicsemi.com/bundle/nrf-connect-desktop/page/download_cfd.html .. _`nRF Connect Device Manager`: https://www.nordicsemi.com/Products/Development-tools/nRF-Connect-Device-Manager .. _`nRF Toolbox`: https://www.nordicsemi.com/Software-and-Tools/Development-Tools/nRF-Toolbox diff --git a/doc/nrf-bm/release_notes/release_notes_changelog.rst b/doc/nrf-bm/release_notes/release_notes_changelog.rst index 7de560325b..6555ba72f2 100644 --- a/doc/nrf-bm/release_notes/release_notes_changelog.rst +++ b/doc/nrf-bm/release_notes/release_notes_changelog.rst @@ -33,7 +33,7 @@ No changes since the latest nRF Connect SDK Bare Metal release. Boards ====== -No changes since the latest nRF Connect SDK Bare Metal release. +* Added ``BOARD_EXTERNAL_MEMORY_*`` macros to **bm_nrf54l15dk** and **bm_nrf54lm20dk** ``board-config.h`` (SPIM instance, SCK/MOSI/MISO/CS and WP#/RST# strap pins) for on-board SPI external flash. Other BM development kits do not include external flash memory on the board, so their ``board-config.h`` files omit these macros. Build system ============ @@ -120,7 +120,7 @@ No changes since the latest nRF Connect SDK Bare Metal release. Peripheral samples ------------------ -No changes since the latest nRF Connect SDK Bare Metal release. +* Added the :ref:`spi_mngr_sample` sample, demonstrating non-blocking read, page program, and sector erase on the on-board external NOR flash using the :ref:`lib_bm_spi_mngr` library. DFU samples ----------- diff --git a/samples/peripherals/spi_mngr/CMakeLists.txt b/samples/peripherals/spi_mngr/CMakeLists.txt new file mode 100644 index 0000000000..6f030454bc --- /dev/null +++ b/samples/peripherals/spi_mngr/CMakeLists.txt @@ -0,0 +1,12 @@ +# +# Copyright (c) 2026 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(spi_mngr) + +target_sources(app PRIVATE src/main.c) diff --git a/samples/peripherals/spi_mngr/Kconfig b/samples/peripherals/spi_mngr/Kconfig new file mode 100644 index 0000000000..85377c24a2 --- /dev/null +++ b/samples/peripherals/spi_mngr/Kconfig @@ -0,0 +1,22 @@ +# +# Copyright (c) 2026 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menu "SPI Manager sample" + +config SAMPLE_SPI_MNGR_MSG + string "Message to write to external NOR flash" + default "Hello World!" + help + Payload bytes written to the external NOR flash when a program operation is triggered. + Truncated to 64 bytes if longer. + +module=SAMPLE_SPI_MNGR +module-str=SPI manager sample +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +endmenu # "SPI Manager sample" + +source "Kconfig.zephyr" diff --git a/samples/peripherals/spi_mngr/README.rst b/samples/peripherals/spi_mngr/README.rst new file mode 100644 index 0000000000..472e597426 --- /dev/null +++ b/samples/peripherals/spi_mngr/README.rst @@ -0,0 +1,96 @@ +.. _spi_mngr_sample: + +SPI manager external memory +########################### + +.. contents:: + :local: + :depth: 2 + +The SPI manager sample demonstrates how to use the :ref:`lib_bm_spi_mngr` library with |BMlong| to perform non-blocking read, page program, and sector erase operations on the external flash memory of the development kit. + +Requirements +************ + +This sample is designed to run on a development kit with **on-board SPI external flash**, which is configured through the ``BOARD_EXTERNAL_MEMORY_*`` macros in :file:`board-config.h`. +Currently, only the `nRF54L15 DK`_ and `nRF54LM20 DK`_ include on-board external flash and are therefore the supported targets for this sample. + +The following board targets match :file:`sample.yaml`: + +.. tabs:: + + .. group-tab:: Simple board variants + + The following board variants do **not** have DFU capabilities: + + .. include:: /includes/supported_boards_with_external_flash_memory_non-mcuboot_variants_s115.txt + + .. include:: /includes/supported_boards_with_external_flash_memory_non-mcuboot_variants_s145.txt + + .. group-tab:: MCUboot board variants + + The following board variants have DFU capabilities: + + .. include:: /includes/supported_boards_with_external_flash_memory_mcuboot_variants_s115.txt + + .. include:: /includes/supported_boards_with_external_flash_memory_mcuboot_variants_s145.txt + +.. note:: + The `nRF54LV10 DK`_ and `nRF54LS05 DK`_ do **not** have on-board external flash memory and are therefore out of scope for this sample. + However, the :ref:`lib_bm_spi_mngr` library itself is fully supported on these devkits. + You can still use the SPI manager feature on these boards by connecting your own external SPI slave device (for example, an external NOR flash) to the SPI pins of the SoC and adapting the sample configuration accordingly (pin assignments in :file:`board-config.h`, command set, and timing parameters to match your device). + +.. important:: + Before flashing the sample, you must enable **External memory** using the `Board Configurator`_ app in `nRF Connect for Desktop`_, and then write the configuration to the board. + Without this step, the on-board flash is not powered or routed to the SoC, and the sample will not work. + +Overview +******** + +The sample uses the SPI manager library to schedule read, program, and erase operations on the external flash without blocking the CPU. +All operations target the same flash address (``FLASH_ADDR`` in :file:`main.c`) and share a single SPI manager instance. + +The flash is the on-board MX25-class NOR device on the development kit. +Its SPI and strap pins are defined by the ``BOARD_EXTERNAL_MEMORY_*`` macros in :file:`board-config.h`. + +User interface +************** + +LED 0: + Lit when the device is initialized. + +Button 1: + Erase external memory. + +Button 2: + Read external memory. + +Button 3: + Program external memory. + +Building and running +******************** + +This sample can be found under :file:`samples/peripherals/spi_mngr/` in the |BMshort| folder structure. + +For details on how to create, configure, and program a sample, see :ref:`getting_started_with_the_samples`. + +Testing +======= + +You can test this sample by performing the following steps: + +1. Compile and program the application. +#. Observe that the ``SPI manager sample initialized`` message is printed, followed by the list of available buttons and their operations. +#. Press **Button 2** to read the on-board flash. + The hex dump shows either all ``0xFF`` (erased) or the data last written to the address. +#. Press **Button 3** to program the configured message (set with :kconfig:option:`CONFIG_SAMPLE_SPI_MNGR_MSG`, default ``Hello World!``). + The log prints ``Programming "Hello World!" @ 0x00100000``. +#. Press **Button 2** again. + The hex dump now starts with the message bytes, confirming the write succeeded. + For the default configuration, this is ``48 65 6c 6c 6f 20 57 6f 72 6c 64 21`` (ASCII for ``Hello World!``), followed by ``0xFF`` padding. +#. Power-cycle the board and press **Button 2**. + The same message reappears, confirming the data persists across power cycles. +#. Press **Button 1** to erase the sector and wait for the self-timed erase to finish. +#. Power-cycle the board again and press **Button 2**. + The hex dump shows all ``0xFF``, confirming the erase succeeded. diff --git a/samples/peripherals/spi_mngr/prj.conf b/samples/peripherals/spi_mngr/prj.conf new file mode 100644 index 0000000000..aae8a4863d --- /dev/null +++ b/samples/peripherals/spi_mngr/prj.conf @@ -0,0 +1,19 @@ +# Logging +CONFIG_LOG=y +CONFIG_LOG_BACKEND_BM_UARTE=y + +# Enabling SoftDevice is not strictly needed, though we are building with SoftDevice boards. +CONFIG_SOFTDEVICE=y + +# Buttons, LEDs ans timer +CONFIG_BM_BUTTONS=y +CONFIG_BM_GPIOTE=y +CONFIG_BM_TIMER=y + +# HFCLK and related clock services for SoC peripherals. +CONFIG_CLOCK_CONTROL=y + +# SPIM hardware driver, required by bm_spi_mngr for SPI master transfers. +CONFIG_NRFX_SPIM=y +# Queued SPI transactions on top of nrfx SPIM. +CONFIG_BM_SPI_MNGR=y diff --git a/samples/peripherals/spi_mngr/sample.yaml b/samples/peripherals/spi_mngr/sample.yaml new file mode 100644 index 0000000000..978b5b3285 --- /dev/null +++ b/samples/peripherals/spi_mngr/sample.yaml @@ -0,0 +1,26 @@ +sample: + name: SPI Manager Sample +tests: + sample.spi_mngr: + sysbuild: true + build_only: true + integration_platforms: + - bm_nrf54l15dk/nrf54l15/cpuapp/s115_softdevice + platform_allow: + - bm_nrf54l15dk/nrf54l05/cpuapp/s115_softdevice + - bm_nrf54l15dk/nrf54l05/cpuapp/s115_softdevice/mcuboot + - bm_nrf54l15dk/nrf54l05/cpuapp/s145_softdevice + - bm_nrf54l15dk/nrf54l05/cpuapp/s145_softdevice/mcuboot + - bm_nrf54l15dk/nrf54l10/cpuapp/s115_softdevice + - bm_nrf54l15dk/nrf54l10/cpuapp/s115_softdevice/mcuboot + - bm_nrf54l15dk/nrf54l10/cpuapp/s145_softdevice + - bm_nrf54l15dk/nrf54l10/cpuapp/s145_softdevice/mcuboot + - bm_nrf54l15dk/nrf54l15/cpuapp/s115_softdevice + - bm_nrf54l15dk/nrf54l15/cpuapp/s115_softdevice/mcuboot + - bm_nrf54l15dk/nrf54l15/cpuapp/s145_softdevice + - bm_nrf54l15dk/nrf54l15/cpuapp/s145_softdevice/mcuboot + - bm_nrf54lm20dk/nrf54lm20a/cpuapp/s115_softdevice + - bm_nrf54lm20dk/nrf54lm20a/cpuapp/s115_softdevice/mcuboot + - bm_nrf54lm20dk/nrf54lm20a/cpuapp/s145_softdevice + - bm_nrf54lm20dk/nrf54lm20a/cpuapp/s145_softdevice/mcuboot + tags: ci_build diff --git a/samples/peripherals/spi_mngr/src/main.c b/samples/peripherals/spi_mngr/src/main.c new file mode 100644 index 0000000000..bae300003e --- /dev/null +++ b/samples/peripherals/spi_mngr/src/main.c @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2026 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(sample, CONFIG_SAMPLE_SPI_MNGR_LOG_LEVEL); + +BM_SPI_MNGR_DEF(spi_mgr, 4, BOARD_EXTERNAL_MEMORY_SPIM_INST); + +/* SPIM configuration for the external memory: + * You can define several configurations and point each transaction at one, + * so the same SPIM can talk to different devices (pins) as you queue up work. + */ +static nrfx_spim_config_t spim_cfg_ext_mem = NRFX_SPIM_DEFAULT_CONFIG( + BOARD_EXTERNAL_MEMORY_SPIM_PIN_SCK, BOARD_EXTERNAL_MEMORY_SPIM_PIN_MOSI, + BOARD_EXTERNAL_MEMORY_SPIM_PIN_MISO, BOARD_EXTERNAL_MEMORY_SPIM_PIN_CSN); + +/* MX25R6435F opcodes: + * READ, PAGE PROGRAM and SECTOR ERASE all use the same header: 1 opcode byte + 3 address bytes. + * WREN must precede any program or erase. + */ +#define MX25_CMD_WREN 0x06U +#define MX25_CMD_READ 0x03U +#define MX25_CMD_PAGE_PROGRAM 0x02U +#define MX25_CMD_SECTOR_ERASE 0x20U + +#define MX25_CMD_HEADER_LEN 4U +#define FLASH_ADDR 0x00100000UL +#define BLOCK_LEN 64U + +/* Expands to the 4 bytes of an MX25 command header: opcode + 24-bit big-endian address. */ +#define MX25_CMD_HEADER(opcode, addr) \ + (opcode), \ + (uint8_t)((addr) >> 16), \ + (uint8_t)((addr) >> 8), \ + (uint8_t)(addr) + +/* Read buffer must outlive the queued transaction and is accessed by the read callback. */ +static uint8_t read_rx[MX25_CMD_HEADER_LEN + BLOCK_LEN]; + +/* Single-byte WREN command. Required before any program or erase; shared by both. */ +static uint8_t wren_cmd[1] = { MX25_CMD_WREN }; + +/* Drive WP# and RST# high so the chip is writable and not in reset. */ +static void mx25_straps_high(void) +{ + nrf_gpio_cfg_output(BOARD_EXTERNAL_MEMORY_PIN_WP); + nrf_gpio_cfg_output(BOARD_EXTERNAL_MEMORY_PIN_RST); + nrf_gpio_pin_write(BOARD_EXTERNAL_MEMORY_PIN_WP, 1); + nrf_gpio_pin_write(BOARD_EXTERNAL_MEMORY_PIN_RST, 1); +} + +ISR_DIRECT_DECLARE(spim_isr) +{ + nrfx_spim_irq_handler(spi_mgr.spim); + return 0; +} + +static int external_memory_init(void) +{ + mx25_straps_high(); + + BM_IRQ_DIRECT_CONNECT(NRFX_IRQ_NUMBER_GET(BOARD_EXTERNAL_MEMORY_SPIM_INST), IRQ_PRIO_LOWEST, + spim_isr, 0); + irq_enable(NRFX_IRQ_NUMBER_GET(BOARD_EXTERNAL_MEMORY_SPIM_INST)); + + return bm_spi_mngr_init(&spi_mgr, &spim_cfg_ext_mem); +} + +static int flash_erase(void) +{ + static uint8_t erase_cmd[] = { + MX25_CMD_HEADER(MX25_CMD_SECTOR_ERASE, FLASH_ADDR), + }; + /* Two CS frames are required: the first sends WREN (MX25_CMD_WREN) which + * sets the chips Write Enable Latch (WEL) bit, and the second sends SE + * (MX25_CMD_SECTOR_ERASE) which consumes the WEL bit to authorize the erase. + * The SPI manager deasserts CS between transfers, which is what latches WEL. + */ + static const struct bm_spi_mngr_transfer erase_xfers[] = { + BM_SPI_MNGR_TRANSFER(wren_cmd, sizeof(wren_cmd), NULL, 0), + BM_SPI_MNGR_TRANSFER(erase_cmd, sizeof(erase_cmd), NULL, 0), + }; + static struct bm_spi_mngr_transaction erase_txn = { + .transfers = erase_xfers, + .number_of_transfers = ARRAY_SIZE(erase_xfers), + .required_spim_cfg = &spim_cfg_ext_mem, + }; + + LOG_INF("Erasing 4 KiB sector @ 0x%08lx", (unsigned long)FLASH_ADDR); + + return bm_spi_mngr_schedule(&spi_mgr, &erase_txn); +} + +static void flash_read_done(int result, void *user_data) +{ + if (result != 0) { + LOG_ERR("Flash read failed: %d", result); + return; + } + + LOG_INF("Read (0x03) @ 0x%08lx, %u bytes:", (unsigned long)FLASH_ADDR, + (unsigned int)BLOCK_LEN); + LOG_HEXDUMP_INF(&read_rx[MX25_CMD_HEADER_LEN], BLOCK_LEN, "flash"); +} + +static int flash_read(void) +{ + static uint8_t read_tx[] = { + MX25_CMD_HEADER(MX25_CMD_READ, FLASH_ADDR), + }; + static const struct bm_spi_mngr_transfer read_xfers[] = { + BM_SPI_MNGR_TRANSFER(read_tx, sizeof(read_tx), read_rx, sizeof(read_rx)), + }; + static struct bm_spi_mngr_transaction read_txn = { + .transfers = read_xfers, + .number_of_transfers = ARRAY_SIZE(read_xfers), + .end_callback = flash_read_done, + .required_spim_cfg = &spim_cfg_ext_mem, + }; + + return bm_spi_mngr_schedule(&spi_mgr, &read_txn); +} + +static int flash_program(void) +{ + static uint8_t program_buf[MX25_CMD_HEADER_LEN + BLOCK_LEN] = { + MX25_CMD_HEADER(MX25_CMD_PAGE_PROGRAM, FLASH_ADDR), + }; + /* Two CS frames are required: the first sends WREN (MX25_CMD_WREN) which + * sets the chip's Write Enable Latch (WEL) bit, and the second sends PP + * (MX25_CMD_PAGE_PROGRAM) which consumes the WEL bit to authorize the write. + * The SPI manager deasserts CS between transfers, which is what latches WEL. + */ + static const struct bm_spi_mngr_transfer program_xfers[] = { + BM_SPI_MNGR_TRANSFER(wren_cmd, sizeof(wren_cmd), NULL, 0), + BM_SPI_MNGR_TRANSFER(program_buf, sizeof(program_buf), NULL, 0), + }; + static struct bm_spi_mngr_transaction program_txn = { + .transfers = program_xfers, + .number_of_transfers = ARRAY_SIZE(program_xfers), + .required_spim_cfg = &spim_cfg_ext_mem, + }; + + size_t len = strlen(CONFIG_SAMPLE_SPI_MNGR_MSG); + + if (len > BLOCK_LEN) { + len = BLOCK_LEN; + } + + /* Pad unused payload bytes with 0xFF so NOR flash leaves them unchanged. + * This means that only message bytes get programmed. + */ + memset(&program_buf[MX25_CMD_HEADER_LEN], 0xFF, BLOCK_LEN); + memcpy(&program_buf[MX25_CMD_HEADER_LEN], CONFIG_SAMPLE_SPI_MNGR_MSG, len); + + LOG_INF("Programming \"%s\" @ 0x%08lx", CONFIG_SAMPLE_SPI_MNGR_MSG, + (unsigned long)FLASH_ADDR); + + return bm_spi_mngr_schedule(&spi_mgr, &program_txn); +} + +static void button_handler(uint8_t pin, enum bm_buttons_evt_type action) +{ + if (action != BM_BUTTONS_PRESS) { + return; + } + + switch (pin) { + case BOARD_PIN_BTN_1: + (void)flash_erase(); + break; + case BOARD_PIN_BTN_2: + (void)flash_read(); + break; + case BOARD_PIN_BTN_3: + (void)flash_program(); + break; + default: + break; + } +} + +static int buttons_init(void) +{ + int err; + + static const struct bm_buttons_config cfg[] = { + { + .pin_number = BOARD_PIN_BTN_1, + .active_state = BM_BUTTONS_ACTIVE_LOW, + .pull_config = BM_BUTTONS_PIN_PULLUP, + .handler = button_handler, + }, + { + .pin_number = BOARD_PIN_BTN_2, + .active_state = BM_BUTTONS_ACTIVE_LOW, + .pull_config = BM_BUTTONS_PIN_PULLUP, + .handler = button_handler, + }, + { + .pin_number = BOARD_PIN_BTN_3, + .active_state = BM_BUTTONS_ACTIVE_LOW, + .pull_config = BM_BUTTONS_PIN_PULLUP, + .handler = button_handler, + }, + }; + + err = bm_buttons_init(cfg, ARRAY_SIZE(cfg), BM_BUTTONS_DETECTION_DELAY_MIN_US); + if (err != NRF_SUCCESS) { + return err; + } + + return bm_buttons_enable(); +} + +int main(void) +{ + int err; + + err = external_memory_init(); + if (err != 0) { + LOG_ERR("SPI init failed: %d", err); + goto idle; + } + + err = buttons_init(); + if (err != NRF_SUCCESS) { + LOG_ERR("Buttons init failed: %d", err); + goto idle; + } + + nrf_gpio_cfg_output(BOARD_PIN_LED_0); + nrf_gpio_pin_write(BOARD_PIN_LED_0, !BOARD_LED_ACTIVE_STATE); + + LOG_INF("SPI manager sample initialized"); + LOG_INF("Use following buttons to interact with external memory through SPI"); + LOG_INF("Button 1: Erase"); + LOG_INF("Button 2: Read"); + LOG_INF("Button 3: Program"); + + /* LED on once init succeeds. */ + nrf_gpio_pin_write(BOARD_PIN_LED_0, BOARD_LED_ACTIVE_STATE); + +idle: + while (true) { + log_flush(); + k_cpu_idle(); + } +}