spi-slave
Claim the bus in target (slave) mode using the portable alp_spi_target_* surface from <alp/peripheral.h> (v0.9, [ABI-EXPERIMENTAL]) and answer an external SPI controller.
Source: examples/peripheral-io/spi-slave/.
What it does
alp_init()— return value checked; future backends can fail bring-up.alp_spi_target_open()onBOARD_SPI_ARDUINO,ALP_SPI_MODE_0, 8 bits. There is nocs_pin_idon our side — the external controller owns SCK and /CS.- Loops on
alp_spi_target_transceive(tgt, tx, rx, 5, &got, timeout_ms): preload a TX reply, wait for the controller to clock a transfer, decode what arrived, preload the next reply. alp_spi_target_close().
:::info Replies always lag one frame SPI is full-duplex. A target can never answer a command within the same transfer — the reply you preload is served on the next one. :::
The bounded-wait pattern
A finite timeout_ms keeps the thread responsive when the controller goes quiet, and it's what makes a clean close possible — close refuses with ALP_ERR_BUSY while a transceive is blocked.
Drivers without an async path report ALP_ERR_NOSUPPORT for finite timeouts (on Zephyr this needs CONFIG_SPI_ASYNC). The example detects that, prints bounded wait unsupported; waiting forever, and degrades to the unbounded UINT32_MAX wait. It also gives up after three consecutive idle timeouts.
The toy protocol
Fixed 5-byte frames:
| Command | Byte 0 | Reply (next frame) |
|---|---|---|
| PING | 0x01 | 0x00 + the 4-byte payload echoed back |
| GET_VERSION | 0x02 | 0x00 + ALP_VERSION_MAJOR / MINOR / PATCH from <alp/version.h> + the tag char 'A' |
| anything else | — | 0x00 then 0xEE fill |
board.yaml
som:
sku: E1M-AEN801
preset: e1m-evk
supported_boards:
- e1m-evk
- e1m-x-evk
pins:
- { e1m: E1M_SPI1, macro: EVK_SPI_BUS_ARDUINO, doc: "Arduino UNO header SPI" }
cores:
m55_hp:
app: ./src
peripherals:
- spi
diagnostics:
log_level: info
prj.conf carries two knobs the board.yaml derivation does not cover:
CONFIG_SPI_SLAVE=y # every in-tree Zephyr SPI driver compiles its target path only when set
CONFIG_SPI_ASYNC=y # bounded-wait transceive rides spi_transceive_signal + k_poll
Availability
Zephyr's SPI slave support is patchy — some SoC controller drivers reject SPI_OP_MODE_SLAVE. Backends or drivers without slave mode fail with ALP_ERR_NOSUPPORT, or ALP_ERR_NOT_READY when the bus alias is unset. The example handles both by printing the diagnostic and exiting.
That is the expected outcome on native_sim, which does not wire BOARD_SPI_ARDUINO (only the alp-spi0 loader alias exists there):
[spi-slave] listening on BOARD_SPI_ARDUINO (mode 0, 8 bits)
[spi-slave] target open failed: alp_last_error=-2
[spi-slave] SPI target (slave) mode is unavailable here
[spi-slave] done
Test setup (real hardware)
- Flash this example onto board A (the target).
- Adapt
spi-masteron board B to send 5-byte frames:[0x01, 0xDE, 0xAD, 0xBE, 0xEF]→ expect[0x00, 0xDE, 0xAD, 0xBE, 0xEF]back on the next frame.[0x02, 0xFF, 0xFF, 0xFF, 0xFF]→ expect[0x00, <major>, <minor>, <patch>, 'A']on the next frame.
- Wire SCK–SCK, MOSI–MOSI, MISO–MISO, /CS–/CS, GND–GND. The master drives SCK + MOSI + /CS; the target drives MISO.
- Board A's console shows one
transfer Nline per frame board B clocks.
A missing GND between boards is a classic inter-board SPI bug — it turns SPI edges into noise. A logic analyser with an SPI decoder makes CPOL/CPHA mismatches obvious.
Status
Two twister rows, one per EVK route table, on native_sim/native/64; the harness latches on [spi-slave] done — via the graceful NOT_READY path, not a working transfer. Target-mode behaviour is a bench signal.
See also
<alp/peripheral.h>reference — SPI — the full target-mode contractspi-master— discrete master companionspi-loopback— single-chip self-testi2c-slave— the I²C target-mode sibling- Examples overview