lib: bm_spi_mngr: sample: port SPI transaction manager to NCS bare metal, add sample and board configs#795
Conversation
32371c8 to
e7097a5
Compare
|
You can find the documentation preview for this PR here. |
fcd778b to
60908f4
Compare
60908f4 to
77cc9cc
Compare
13d6fed to
27108a0
Compare
b7f31c2 to
5e0dff8
Compare
ce4b003 to
af04708
Compare
eivindj-nordic
left a comment
There was a problem hiding this comment.
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.
| 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. |
There was a problem hiding this comment.
This belongs in the header.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Good point, I removed the irq_lock() from queue_pop(). Added a comment saying the caller must hold the lock.
| 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. |
There was a problem hiding this comment.
In Board Configurator v1.2.0 the external storage is referred to as External memory.
| 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. |
| #define MX25_CMD_PAGE_PROGRAM 0x02U | ||
| #define MX25_CMD_SECTOR_ERASE 0x20U | ||
|
|
||
| #define MX25_CMD_ADDR_LEN 4U |
There was a problem hiding this comment.
Header represents the length better since address is 3 bytes and opcode is 1 byte
| #define MX25_CMD_ADDR_LEN 4U | |
| #define MX25_CMD_HEADER_LEN 4U |
|
|
||
| static int flash_read(void) | ||
| { | ||
| static uint8_t read_tx[MX25_CMD_ADDR_LEN + BLOCK_LEN] = { |
There was a problem hiding this comment.
Currently sending extra 0x00 bytes after the command
| static uint8_t read_tx[MX25_CMD_ADDR_LEN + BLOCK_LEN] = { | |
| static uint8_t read_tx[] = { |
| /* 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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| __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); |
There was a problem hiding this comment.
Should the asserts be handled like other files in lib, e.g.
| __ASSERT_NO_MSG(p_default_spim_config != NULL); | |
| if (p_default_spim_config == NULL) { | |
| return -EFAULT; | |
| } |
There was a problem hiding this comment.
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.
a45033e to
02214d5
Compare
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>
02214d5 to
25fd73e
Compare
Done. I converted all struct typedefs to named |
Port the legacy nRF5 SDK
nrf_spi_mngrlibrary to NCS bare metal asbm_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_spiminstead ofnrf_drv_spi.nrf_queue, withirq_lockaround shared queue access.intinstead ofret_code_t.<errno.h>(negative errno) scheme.nrfxerror codes are propagated as-is since they already use the same convention.bm_spi_mngr_performasserts 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:
samples/peripherals/spi_mngrthat 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 affectedboard-config.hfiles so the sample picks up the on-board flash automatically.