Skip to content

lib: bm_spi_mngr: sample: port SPI transaction manager to NCS bare metal, add sample and board configs#795

Open
PizzaAllTheWay wants to merge 2 commits into
nrfconnect:mainfrom
PizzaAllTheWay:spi-mngr-port
Open

lib: bm_spi_mngr: sample: port SPI transaction manager to NCS bare metal, add sample and board configs#795
PizzaAllTheWay wants to merge 2 commits into
nrfconnect:mainfrom
PizzaAllTheWay:spi-mngr-port

Conversation

@PizzaAllTheWay
Copy link
Copy Markdown
Contributor

@PizzaAllTheWay PizzaAllTheWay commented May 13, 2026

Port the legacy nRF5 SDK nrf_spi_mngr library to NCS bare metal as bm_spi_mngr. The library serializes SPIM work on a single hardware instance by queuing transactions in a FIFO and running them back-to-back.

Main changes from the original:

  • nrfx_spim instead of nrf_drv_spi.
  • Zephyr ring buffer instead of nrf_queue, with irq_lock around shared queue access.
  • All public APIs return int instead of ret_code_t.
  • Error codes follow the <errno.h> (negative errno) scheme. nrfx error codes are propagated as-is since they already use the same convention.
  • bm_spi_mngr_perform asserts its idle hook is not called from ISR context, as a precaution against silent deadlocks.

Both the non-blocking (schedule) and blocking (perform) APIs are preserved, as is per-transaction SPIM reconfiguration.

Also adds:

  • A sample under samples/peripherals/spi_mngr that demonstrates non-blocking read, page program, and sector erase against the on-board external NOR flash. Only supported on the nRF54L15 DK and nRF54LM20 DK, as the other supported development kits do not have on-board external memory, so it would be hard to showcase SPI Manager there without external hardware.
  • BOARD_EXTERNAL_MEMORY_* macros in the affected board-config.h files so the sample picks up the on-board flash automatically.

@github-actions github-actions Bot added the doc-required PR must not be merged without tech writer approval. label May 13, 2026
@PizzaAllTheWay PizzaAllTheWay force-pushed the spi-mngr-port branch 2 times, most recently from 32371c8 to e7097a5 Compare May 13, 2026 09:00
@github-actions
Copy link
Copy Markdown

You can find the documentation preview for this PR here.

@PizzaAllTheWay PizzaAllTheWay force-pushed the spi-mngr-port branch 2 times, most recently from fcd778b to 60908f4 Compare May 13, 2026 10:23
@PizzaAllTheWay PizzaAllTheWay self-assigned this May 13, 2026
@PizzaAllTheWay PizzaAllTheWay changed the title lib: bm_spi_mngr: port SPI transaction manager to NCS bare metal lib: bm_spi_mngr: port SPI transaction manager to NCS bare metal, add sample and board configs May 13, 2026
@PizzaAllTheWay PizzaAllTheWay force-pushed the spi-mngr-port branch 3 times, most recently from 13d6fed to 27108a0 Compare May 13, 2026 16:16
@PizzaAllTheWay PizzaAllTheWay changed the title lib: bm_spi_mngr: port SPI transaction manager to NCS bare metal, add sample and board configs lib: bm_spi_mngr: sample: port SPI transaction manager to NCS bare metal, add sample and board configs May 13, 2026
@PizzaAllTheWay PizzaAllTheWay force-pushed the spi-mngr-port branch 3 times, most recently from b7f31c2 to 5e0dff8 Compare May 13, 2026 17:19
@PizzaAllTheWay PizzaAllTheWay marked this pull request as ready for review May 13, 2026 17:28
@PizzaAllTheWay PizzaAllTheWay requested review from a team as code owners May 13, 2026 17:28
@PizzaAllTheWay PizzaAllTheWay force-pushed the spi-mngr-port branch 5 times, most recently from ce4b003 to af04708 Compare May 18, 2026 11:38
Copy link
Copy Markdown
Contributor

@eivindj-nordic eivindj-nordic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please have a look through the documentation, header and source code to align the format with the rest of the NCS BM repo, avoid typedefs, hungarian notation etc.

Comment thread doc/nrf-bm/libraries/bm_spi_mngr.rst Outdated
Comment on lines +19 to +24
The library exposes a non-blocking API and a blocking API on top of the same scheduling engine.
Return values follow the negative errno convention:

* :c:func:`bm_spi_mngr_init` and :c:func:`bm_spi_mngr_perform` return ``0`` on success, or a negative errno-style code from this module or from nrfx SPIM on failure.
* :c:func:`bm_spi_mngr_schedule` returns ``0`` on success or ``-ENOMEM`` if the queue is full.
* The end callback receives ``0`` on success, or a negative errno-style code (for example ``-EIO`` from this module, or a negative errno from nrfx SPIM) on failure.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This belongs in the header.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oki. I rewrote API documentation to align with other API docs

unsigned int key = irq_lock();

if (switch_transaction || bm_spi_mngr_is_idle(p_bm_spi_mngr)) {
if (queue_pop(p_bm_spi_mngr,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice that irq_lock() is called in both this function and the queue_pop() function, is it necessary to do it both functions? queue_pop() is not called any other places.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I removed the irq_lock() from queue_pop(). Added a comment saying the caller must hold the lock.

Comment thread samples/peripherals/spi_mngr/README.rst Outdated
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 storage** using the `Board Configurator`_ app in `nRF Connect for Desktop`_, and then write the configuration to the board.
Copy link
Copy Markdown
Contributor

@sondrep sondrep Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Board Configurator v1.2.0 the external storage is referred to as External memory.

Suggested change
Before flashing the sample, you must enable **external storage** using the `Board Configurator`_ app in `nRF Connect for Desktop`_, and then write the configuration to the board.
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread samples/peripherals/spi_mngr/src/main.c Outdated
#define MX25_CMD_PAGE_PROGRAM 0x02U
#define MX25_CMD_SECTOR_ERASE 0x20U

#define MX25_CMD_ADDR_LEN 4U
Copy link
Copy Markdown
Contributor

@asilz asilz Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Header represents the length better since address is 3 bytes and opcode is 1 byte

Suggested change
#define MX25_CMD_ADDR_LEN 4U
#define MX25_CMD_HEADER_LEN 4U

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread samples/peripherals/spi_mngr/src/main.c Outdated

static int flash_read(void)
{
static uint8_t read_tx[MX25_CMD_ADDR_LEN + BLOCK_LEN] = {
Copy link
Copy Markdown
Contributor

@asilz asilz Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently sending extra 0x00 bytes after the command

Suggested change
static uint8_t read_tx[MX25_CMD_ADDR_LEN + BLOCK_LEN] = {
static uint8_t read_tx[] = {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread lib/bm_spi_mngr/bm_spi_mngr.c Outdated
/* 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.
*/
typedef volatile struct {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it strictly necessary for this struct to be volatile? It is only touched by spi_internal_transaction_cb() and bm_spi_mngr_perform(), so I don't see why it needs to be volatile, unless there is a problem with the memory consistency without it.

Copy link
Copy Markdown
Contributor Author

@PizzaAllTheWay PizzaAllTheWay Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it needs to be there, and you are correct about memory consistency. My initial thinking was honestly that the compiler would just optimize it out either way, so it wouldn't matter much. The case I had in mind is the blocking bm_spi_mngr_perform(), which spins here:

while (cb_data.transaction_in_progress) {
    if (user_function) {
        user_function();
    }
}

Nothing inside bm_spi_mngr_perform() ever writes false to that flag, it's cleared from the SPIM interrupt callback. So in theory the compiler could read it once, cache it, and turn this into an infinite loop, ie:

while (true) {
    if (user_function) {
        user_function();
    }
}

I actually tested it with a blocking call and it did seem to work the same with and without volatile. So one would assume that volatile is not needed here. However, that's just because of the circumstances: the loop calls an opaque user_function() and the address of cb_data gets passed off into another function, so the compiler can't prove nothing touches the flag and ends up re-reading it each iteration anyway.

The problem is that user_function is optional, passing NULL is perfectly valid (and the __ASSERT_NO_MSG(user_function == NULL || !k_is_in_isr()) explicitly allows it). For example, a blocking erase with no idle hook:

return bm_spi_mngr_perform(&spi_mgr, NULL, erase_xfers, ARRAY_SIZE(erase_xfers), NULL);

With user_function == NULL, that accidental unintended safety net is gone. The loop body collapses to nothing the compiler can see touching cb_data:

while (cb_data.transaction_in_progress) {
    /* if (user_function) -> NULL, dead code, removed */
}

Now the compiler is free to cache the flag and produce a genuine infinite loop => deadlock, even though the ISR clears it in memory. So this isn't a theoretical edge case, it's a fully supported way of calling the function.

volatile forces a fresh read of the flag every iteration regardless of whether user_function is set, so the ISR write is always observed and the loop exits. It also keeps us aligned with the old nRF5 SDK and makes it clear to the reader that this variable is touched from an interrupt and needs special attention and care.

Comment thread lib/bm_spi_mngr/bm_spi_mngr.c Outdated
__ASSERT_NO_MSG(p_bm_spi_mngr->p_queue != NULL);
__ASSERT_NO_MSG(p_bm_spi_mngr->p_queue->size > 0);
__ASSERT_NO_MSG(p_bm_spi_mngr->p_queue->p_byte_storage != NULL);
__ASSERT_NO_MSG(p_default_spim_config != NULL);
Copy link
Copy Markdown
Contributor

@sondrep sondrep Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the asserts be handled like other files in lib, e.g.

Suggested change
__ASSERT_NO_MSG(p_default_spim_config != NULL);
if (p_default_spim_config == NULL) {
return -EFAULT;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, replaced the user input asserts in the public API (bm_spi_mngr_init, bm_spi_mngr_schedule, bm_spi_mngr_perform) with guard clauses returning -EFAULT/-EINVAL, and updated the header @retval docs to match. Kept __ASSERT_NO_MSG for the internal checks since those guard library invariants/misuse, not user input.

@PizzaAllTheWay PizzaAllTheWay force-pushed the spi-mngr-port branch 11 times, most recently from a45033e to 02214d5 Compare June 3, 2026 10:47
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 <martynas.smilingis@nordicsemi.no>
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 <martynas.smilingis@nordicsemi.no>
@PizzaAllTheWay
Copy link
Copy Markdown
Contributor Author

Please have a look through the documentation, header and source code to align the format with the rest of the NCS BM repo, avoid typedefs, hungarian notation etc.

Done. I converted all struct typedefs to named struct tags (keeping the function-pointer typedefs with _t, the same way other headers do it), removed the p_ Hungarian prefixes throughout, and rewrote the API docs to match other API doc style. Let me know if I have missed anything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc-required PR must not be merged without tech writer approval.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants