Zephyr OS Asynchronous SPI: Keep Transfer State Alive Until Completion
Returning from an asynchronous SPI submission does not mean the transfer has finished using its data. A request built from temporary local buffers can look correct at the call site and still become invalid before the callback arrives. Review the lifetime of the whole transfer state, not only the pointer passed as callback user data.
This discussion uses Zephyr 4.4.0's callback-based SPI interface. It proposes ownership rules for an application and does not report a tested driver migration. Specific controller and DMA restrictions still require inspection of the selected driver and hardware documentation.

Separate submission from completion
The tagged SPI API describes asynchronous transfers as returning after the operation starts and reporting completion through a callback or signal. It also notes that an asynchronous call can block while waiting for a busy bus. “Asynchronous” therefore does not promise an instantaneous, nonblocking submission in every circumstance.
Check the submission result before assuming a completion will arrive. For an accepted request, consume the completion result before treating received data as valid. The application needs distinct paths for a request that never started and one that started but completed with an error.
A null callback removes the normal completion notification from the callback interface. That may be intentional in a specialized design, but it is not a safe default for code that needs to know when to reuse receive storage.
Put the request's storage under one lifetime
Identify transmit bytes, receive bytes, buffer descriptors, configuration, and callback context. A conservative application design keeps all transfer-referenced state alive and unchanged through the driver completion path, rather than depending on undocumented copying inside a particular driver.
One practical pattern is a request object containing the descriptors and context, with buffers whose ownership is tied to that object. Allocate it before submission and release it only after the driver completion epilogue and application readers no longer need its members. A fixed request pool can make capacity explicit; static storage can suit a single outstanding transfer. Neither pattern automatically supports overlapping requests.
The common SPI context helper stores pointers for current transmit and receive descriptors, data, configuration, and callback state. Not every driver must use this helper, but its design is a concrete reason to avoid assuming that a submission deep-copies everything supplied by the caller.
Do not reuse one request object concurrently
Consider a hypothetical status reader that submits a transfer, returns to its caller, and immediately fills the same transmit buffer for a second command. If the first transfer still references those bytes, the device can receive data different from what the first request intended. Increasing thread priority does not establish a correct ownership boundary.
Use an explicit in-flight state and define which component is permitted to change it. If multiple producers can request transfers, serialize ownership or allocate independent request objects. Keep request identity in the completion context so a late notification cannot be mistaken for the newest operation.
The same rule applies to receive buffers. A second request must not overwrite a result while a worker is still consuming it. The callback reports transfer outcome, but callback entry is not a universal release boundary for every request member. Application readers can extend the data lifetime further.
Treat configuration as stable state
The SPI API documents that configuration pointer comparison may be used to detect changes. Mutating a configuration object in place and expecting every driver to recognize a new setup is therefore a poor assumption. Use stable, deliberate configuration objects and verify the selected driver's reconfiguration behavior when settings must change.
This concern is distinct from payload lifetime. A perfectly retained byte buffer does not correct an unintended clock, word size, or chip-select configuration. Keep those settings associated with the request being reviewed, rather than relying on whatever values another caller last wrote into shared storage.
Keep callbacks small and context-aware
Do not assume every driver's completion callback runs in an ordinary application thread. Inspect its execution context before using operations that can block. A callback can record status and signal an appropriate worker, while retaining request storage through both driver completion and worker consumption.
Do not destroy or rewrite the configuration, or a request object containing it, inside the callback. In this tagged helper, spi_context_complete() calls the callback and then reads ctx->config->operation. Even a signalled higher-priority worker could reclaim memory too early if it runs before that completion epilogue finishes. Prefer a separate, long-lived stable configuration object; reclaim other request storage only at a boundary established for the selected driver.
Also inspect submission from inside completion callbacks before adopting that pattern. The common context helper invokes the callback before releasing some bus-lock state. Immediate resubmission from that callback can therefore interact with locking; it should not be presented as a universally safe chaining technique.
Verify lifetime with adverse scheduling
A useful test deliberately delays completion and application consumption while another producer attempts a transfer. Track request identity, submission result, completion result, and the point at which each buffer becomes reusable. Do not overwrite live data merely to demonstrate the failure on production hardware.
The design is ready when storage ownership is explainable across submission, completion, and consumption, including error paths. That is a stronger guarantee than “the transfer worked once,” and it addresses an asynchronous SPI integration problem without changing the peripheral's on-wire protocol.