# ESP-IDF SPI_Link
`esp_spi_link` is an ESP-IDF component for short-range SPI communication between
two ESP boards. It wraps the ESP-IDF SPI Master and SPI Slave drivers with a
small framed protocol, CRC validation, a READY notification GPIO, pending
responses, and a PING/PONG handshake.
The component is designed for two-board systems such as an ESP32-S3 main
controller talking to a coprocessor, sensor board, or algorithm board over SPI.
## ESP Component Registry usage
After this component is published, add it to an ESP-IDF project with:
```bash
idf.py add-dependency "miraitowa-la/esp_spi_link^0.1.0"
```
Or add it manually in the consuming project's `main/idf_component.yml`:
```yaml
dependencies:
miraitowa-la/esp_spi_link: "^0.1.0"
```
## Files
```text
esp_spi_link/
|-- CMakeLists.txt
|-- idf_component.yml
|-- LICENSE
|-- README.md
|-- docs/
| `-- SPI_Link说明文档.html
|-- include/
| `-- spi_link.h
`-- src/
`-- spi_link.c
```
## Features
- SPI Master and SPI Slave role initialization.
- Fixed wire frame with header, version, command, payload length, and CRC16.
- READY GPIO notification from Slave to Master.
- One pending Slave response that Master reads with `SPI_LINK_CMD_READ`.
- PING/PONG handshake with reset-tolerant fallback paths.
- ACK, NACK, and ERROR response commands.
- Single-instance runtime context with mutex protection and DMA-capable buffers.
## Hardware connection
Both boards must share GND. SPI is intended for short-distance links, preferably
short wires or the same PCB.
| Signal | Master | Slave | Description |
| --- | --- | --- | --- |
| SCLK | GPIO12 | GPIO12 | SPI clock generated by Master |
| MOSI | GPIO11 | GPIO11 | Master output to Slave |
| MISO | GPIO13 | GPIO13 | Slave output to Master |
| CS | GPIO10 | GPIO10 | SPI chip select |
| READY | GPIO9 input | GPIO9 output | Slave has pending response |
| GND | GND | GND | Common ground |
The GPIO numbers above are examples. Configure the actual pins through
`spi_link_config_t`.
## Wire frame
```text
| 0xAA | 0x55 | Version | CMD | LEN_H | LEN_L | Payload... | CRC_H | CRC_L |
```
CRC uses CRC-16/CCITT-FALSE and covers:
```text
Version + CMD + LEN + Payload
```
The two header bytes are synchronization markers and are not included in the CRC
scope.
## Command ranges
| Range | Usage |
| --- | --- |
| `0x01` to `0x1F` | Reserved protocol commands such as PING, PONG, and READ |
| `0x20` to `0xEF` | User-defined business commands |
| `0xF0` to `0xFF` | System responses such as ACK, NACK, and ERROR |
## Master example
```c
#include "spi_link.h"
#include "esp_log.h"
static const char *TAG = "app_master";
void app_main(void)
{
spi_link_config_t config = {
.mode = SPI_LINK_MODE_MASTER,
.host = SPI2_HOST,
.pin_mosi = 11,
.pin_miso = 13,
.pin_sclk = 12,
.pin_cs = 10,
.pin_ready = 9,
.clock_speed_hz = SPI_LINK_DEFAULT_CLOCK_HZ,
.queue_size = SPI_LINK_DEFAULT_QUEUE_SIZE,
.max_transfer_size = SPI_LINK_DEFAULT_MAX_TRANS_SIZE,
};
ESP_ERROR_CHECK(spi_link_init(&config));
while (spi_link_handshake(1000) != ESP_OK) {
ESP_LOGW(TAG, "Waiting for Slave handshake...");
vTaskDelay(pdMS_TO_TICKS(1000));
}
uint8_t rx_buf[128] = {0};
size_t rx_len = sizeof(rx_buf);
esp_err_t err = spi_link_master_request(SPI_LINK_CMD_DATA,
"hello",
5,
rx_buf,
&rx_len,
1000);
if (err == ESP_OK) {
ESP_LOGI(TAG, "Response length: %u", (unsigned)rx_len);
}
}
```
## Slave example
```c
#include "spi_link.h"
#include "esp_log.h"
static const char *TAG = "app_slave";
void app_main(void)
{
spi_link_config_t config = {
.mode = SPI_LINK_MODE_SLAVE,
.host = SPI2_HOST,
.pin_mosi = 11,
.pin_miso = 13,
.pin_sclk = 12,
.pin_cs = 10,
.pin_ready = 9,
.queue_size = SPI_LINK_DEFAULT_QUEUE_SIZE,
.max_transfer_size = SPI_LINK_DEFAULT_MAX_TRANS_SIZE,
};
ESP_ERROR_CHECK(spi_link_init(&config));
while (1) {
spi_link_packet_t packet = {0};
esp_err_t err = spi_link_slave_receive(&packet, portMAX_DELAY);
if (err != ESP_OK) {
continue;
}
if (packet.cmd == SPI_LINK_CMD_DATA) {
const char reply[] = "ok";
spi_link_slave_reply(SPI_LINK_CMD_ACK, reply, sizeof(reply) - 1);
} else if (packet.cmd >= SPI_LINK_CMD_USER_BASE &&
packet.cmd <= SPI_LINK_CMD_USER_END) {
spi_link_slave_reply(SPI_LINK_CMD_ACK, NULL, 0);
} else {
spi_link_slave_reply(SPI_LINK_CMD_NACK, NULL, 0);
}
}
}
```
## API overview
Common APIs:
- `spi_link_init()`
- `spi_link_deinit()`
- `spi_link_is_connected()`
- `spi_link_is_slave_ready()`
Master APIs:
- `spi_link_handshake()`
- `spi_link_master_send()`
- `spi_link_master_request()`
- `spi_link_master_read()`
Slave APIs:
- `spi_link_slave_receive()`
- `spi_link_slave_reply()`
## Current limitations
- The component currently keeps one global runtime context and supports a single
SPI_Link instance.
- Automatic retry is not implemented.
- Large payload fragmentation is not implemented. A single payload is limited to
`SPI_LINK_MAX_PAYLOAD_SIZE`.
See [docs/SPI_Link说明文档.html](docs/SPI_Link说明文档.html) for the full Chinese
design and API document.
80b13cef2424762fe5c223adae645ef5d96236da
idf.py add-dependency "miraitowa-la/esp_spi_link^0.1.0"