# SPI 从机半双工模式

[English]

## 简介

半双工 (HD) 模式是 ESP SPI 从机外设提供的一种特殊模式。比起全双工 (FD) 模式（用于 GPSPI 事务，详情请参阅 SPI 从机驱动程序），在半双工模式下，硬件会提供更多服务。这些服务减轻了 CPU 负载，提高了 SPI 从机的响应速度。然而，通信格式由硬件确定，始终为半双工模式，即在同一时刻只能进行单向数据传输，半双工模式因此得名。

当进行 SPI 事务时，根据事务的 命令 阶段，可以将事务分为不同的类型。每个事务可能包含以下阶段：命令、地址、dummy、数据。命令阶段是必需的，其他阶段存在与否则取决于命令的需求。在命令、地址、dummy 阶段，总线的控制权始终由主设备（通常是主机）控制，数据阶段的方向则取决于命令。数据阶段可以是输入阶段，即主设备将数据写入从设备（例如，主机向从机发送数据）；也可以是输出阶段，即主设备从从设备读取数据（例如，主机从从机接收数据）。

### 协议

有关主设备与 SPI 从机通信的详细信息，请参阅 ESP SPI 从机 HD（半双工）模式协议。

通过不同类型的事务，从设备为主设备提供以下服务：

- 一个 DMA 通道，支持主设备向从设备写入大量数据。

- 一个 DMA 通道，支持主设备从从设备读取大量数据。

- 一些通用寄存器，由主设备和从设备共享。

- 一些通用中断，用于主设备打断从设备的软件执行。

## 术语

- 事务 (transaction)

- 通道 (channel)

- 发送 (sending)

- 接收 (receiving)

- 数据描述符 (data descriptor)

## 驱动程序特性

- 支持主设备以分段方式读写事务

- 支持发送数据队列和接收数据队列

## 驱动程序使用

### 从机初始化

调用 `spi_slave_hd_init()` 初始化 SPI 总线、外设和驱动程序。在初始化之后，SPI 从机将独占使用 SPI 外设和总线上的管脚。这意味着在反初始化前，其他设备无法使用这些资源。因此，为了确保其他设备可以正确利用 SPI 资源并进行正常通信，从机的大部分配置应在其初始化过程中完成。

结构体 `spi_bus_config_t` 指定了总线的初始化方式，结构体 `spi_slave_hd_slot_config_t` 指定了 SPI 从机驱动程序的运行方式。

### 从机反初始化（可选）

调用 `spi_slave_hd_deinit()` 卸载驱动程序。该反初始化函数会释放相关资源，包括管脚、SPI 外设、驱动程序所用内存、中断源等。

### 通过 DMA 通道发送/接收数据

要通过 DMA 通道向主设备发送数据，应用程序需要先将数据正确地封装在 `spi_slave_hd_data_t` 描述符结构体中，然后再将数据描述符和通道参数 `SPI_SLAVE_CHAN_TX` 传递给 `spi_slave_hd_queue_trans()`。数据描述符的指针存储在队列中，一旦接收到主设备的 Rd_DMA 命令，就会按照调用 `spi_slave_hd_queue_trans()` 时数据进入队列的顺序，依次将数据发送给主设备。

应用程序需要检查数据发送的结果。为此，应用程序可以调用 `spi_slave_hd_get_trans_res()`，并将通道参数设置为 `SPI_SLAVE_CHAN_TX`。该函数将阻塞程序，直到主设备发起的 Rd_DMA 命令事务成功完成或超时。函数中的参数 `out_trans` 将输出刚刚完成的数据描述符的指针，从而提供有关已完成的发送操作的信息。

通过 DMA 通道从主设备接收数据的操作与发送数据类似。应用程序需要使用正确的数据描述符调用 `spi_slave_hd_queue_trans()`，并将通道参数设置为 `SPI_SLAVE_CHAN_RX`。随后，应用程序调用 `spi_slave_hd_get_trans_res()` 获取接收 buffer 的描述符，然后处理接收 buffer 中的数据。

备注

驱动程序本身并没有用于发送或接收数据的内部 buffer。应用程序需要通过数据描述符为驱动程序提供 buffer，从而向主设备发送数据，或接收来自主设备的数据。

在使用 `spi_slave_hd_queue_trans()` 将数据描述符成功发送到驱动程序的内部队列后、并由 `spi_slave_hd_get_trans_res()` 返回前，应用程序需要正确地维护数据描述符以及它所指向的 buffer。在此期间，根据需要，硬件和驱动程序可以随时读取或写入 buffer 和描述符。

注意，在使用该驱动程序进行数据传输时，可以根据实际需要提前终止数据传输，而不需要等待整个 buffer 填满或者完全发送完毕。例如，在分段事务模式下，无论发送/接收 buffer 是否已使用完（已满），主设备都需要发送 `CMD7` 终止 `Wr_DMA` 事务，或发送 `CMD8` 以分段方式终止 `Rd_DMA` 事务。

### 以自定义用户参数使用数据描述符

在某些情况下，发送包函数和回收包函数可能会分散在不同位置。发送包函数用于发送数据描述符，回收包函数用于处理返回的数据描述符。在回收包函数中获取返回的数据描述符时，可能需要一些额外信息，帮助处理数据传输完成后返回给应用程序的描述符。例如，多次发送相同数据时，你可能想知道返回的描述符来自哪一轮发送。

为此，可以通过强制类型转换，将数据描述符中的 `arg` 设置为变量，提供事务信息；或者将其指向一个包含处理发送/接收数据所需的所有信息的结构体。在回收包函数处理返回的描述符时，即可使用这个额外信息。

### 使用回调函数

备注

这些回调函数在 ISR 中调用，因此需要迅速处理所需操作，并尽快返回，确保系统正常运行。因此，在编写 ISR 的代码时，需要十分谨慎。

由于中断处理过程是与主程序并发执行的，长时间的延迟或阻塞操作可能会导致系统响应变慢，或导致不可预测的行为。因此，在编写回调函数时，应避免使用可能引起延迟或阻塞的操作，例如等待、睡眠、资源锁等。

在初始化 SPI 从机半双工驱动程序时，会传递结构体 `spi_slave_hd_slot_config_t` 中的 `spi_slave_hd_callback_config_t`，为任意事件设置回调函数。

每个不为 NULL 的回调函数都将使能对应的中断，所以回调函数会在对应的中断事件触发时立即调用。对于不感兴趣的事件，则无需为其提供回调函数。

配置结构体中的 `arg` 可以给回调函数传递部分上下文信息，或在使用相同的回调函数处理多个 SPI 从机外设时，指明特定的 SPI 从机实例。通过强制类型转换，可以将 `arg` 设置为表示 SPI 从机实例的变量，或者将其指向某个上下文结构体变量。所有回调函数都会使用在初始化回调函数时设置的 `arg` 参数。

配置结构体中的 `event` 和 `awoken` 参数也可以给回调函数传递上下文信息。

> - 参数 `event` 向回调函数传递当前事件信息。`spi_slave_hd_event_t` 包含事件类型和刚刚处理完的数据描述符等信息，此时，通常会使用 data argument。

> - 参数 `awoken` 是一个输出参数，用于告知 ISR，在回调函数后已有其他操作唤醒任务，ISR 应调用 portYIELD_FROM_ISR() 调度这些任务。只需将 `awoken` 参数传递给可能解除任务阻塞的 FreeRTOS API，ISR 即可接收 `awoken` 的返回值。

### 写入/读取共享寄存器

调用 `spi_slave_hd_write_buffer()` 写入共享 buffer，调用 `spi_slave_hd_read_buffer()` 读取共享 buffer。

备注

在 ESP32-C2 上，应用程序以字为单位读取/写入共享寄存器，但主机以字节为单位读取/写入共享寄存器。这样一来，就无法确保从主机读取的四个连续字节是来自从机应用程序写入的同一个字。同时，如果从机在主机写入字节时读取了一个字，可能会得到这样的字：主机刚刚写入它的一半，另一半尚未写入。

通过两次读取同个字，并对两次读取的值做比较，主机可以确保读取的字处于非过渡态。

对从机而言，要确保读取的字处于非过渡态则更为困难，因为主机写入四个字节的过程可能会非常长，达到 32 个 SPI 时钟周期。为此，可以在写入的字的最后一个（最大地址）字节中添加一些冗余校验码 (CRC)，确保在写入含有 CRC 的字节时，即代表整个字完全写入。

从软件读取/写入和从主机读取/写入可能存在冲突，在多核心系统中尤为如此。因此，建议在数据传输过程中，一个字只在一个方向上使用，即要么只由主机写入，要么只由从机写入。

### 接收来自主机的通用中断

当主机发送 `CMD8`、`CMD9` 或 `CMDA` 时，从机会触发相应的动作。目前，`CMD8` 固定用于指示 `Rd_DMA` 段的终止。要接收通用中断，可以在从机初始化时为 `CMD9` 和 `CMDA` 注册回调函数，详情请参阅 使用回调函数。

## 应用示例

查看从机设备/主机通信的示例代码，请前往 ESP-IDF 示例的 peripherals/spi_slave_hd 目录。

## API 参考

### Header File

- components/esp_driver_spi/include/driver/spi_slave_hd.h

- This header file can be included with:

  >     #include "driver/spi_slave_hd.h"

- This header file is a part of the API provided by the `esp_driver_spi` component. To declare that your component depends on `esp_driver_spi`, add the following to your CMakeLists.txt:

  >     REQUIRES esp_driver_spi

  > or

  >     PRIV_REQUIRES esp_driver_spi

### Functions

esp_err_t spi_slave_hd_init(spi_host_device_t host_id, const spi_bus_config_t *bus_config, const spi_slave_hd_slot_config_t *config)

  Initialize the SPI Slave HD driver.

  参数:

    - host_id -- The host to use

    - bus_config -- Bus configuration for the bus used

    - config -- Configuration for the SPI Slave HD driver

  返回:

    - ESP_OK: on success

    - ESP_ERR_INVALID_ARG: invalid argument given

    - ESP_ERR_INVALID_STATE: function called in invalid state, may be some resources are already in use

    - ESP_ERR_NOT_FOUND if there is no available DMA channel

    - ESP_ERR_NO_MEM: memory allocation failed

    - or other return value from `esp_intr_alloc`

esp_err_t spi_slave_hd_deinit(spi_host_device_t host_id)

  Deinitialize the SPI Slave HD driver.

  参数:

    host_id -- The host to deinitialize the driver

  返回:

    - ESP_OK: on success

    - ESP_ERR_INVALID_ARG: if the host_id is not correct

esp_err_t spi_slave_hd_queue_trans(spi_host_device_t host_id, spi_slave_chan_t chan, spi_slave_hd_data_t *trans, TickType_t timeout)

  Queue transactions (segment mode)

  参数:

    - host_id -- Host to queue the transaction

    - chan -- SPI_SLAVE_CHAN_TX or SPI_SLAVE_CHAN_RX

    - trans -- Transaction descriptors

    - timeout -- Timeout before the data is queued

  返回:

    - ESP_OK: on success

    - ESP_ERR_INVALID_ARG: The input argument is invalid. Can be the following reason:

      - The buffer given is not DMA capable

      - The length of data is invalid (not larger than 0, or exceed the max transfer length)

      - The transaction direction is invalid

    - ESP_ERR_TIMEOUT: Cannot queue the data before timeout. Master is still processing previous transaction.

    - ESP_ERR_INVALID_STATE: Function called in invalid state. This API should be called under segment mode.

esp_err_t spi_slave_hd_get_trans_res(spi_host_device_t host_id, spi_slave_chan_t chan, spi_slave_hd_data_t **out_trans, TickType_t timeout)

  Get the result of a data transaction (segment mode)

  备注

  This API should be called successfully the same times as the `spi_slave_hd_queue_trans`.

  参数:

    - host_id -- Host to queue the transaction

    - chan -- Channel to get the result, SPI_SLAVE_CHAN_TX or SPI_SLAVE_CHAN_RX

    - out_trans -- [out] Pointer to the transaction descriptor (`spi_slave_hd_data_t`) passed to the driver before. Hardware has finished this transaction. Member `trans_len` indicates the actual number of bytes of received data, it's meaningless for TX.

    - timeout -- Timeout before the result is got

  返回:

    - ESP_OK: on success

    - ESP_ERR_INVALID_ARG: Function is not valid

    - ESP_ERR_TIMEOUT: There's no transaction done before timeout

    - ESP_ERR_INVALID_STATE: Function called in invalid state. This API should be called under segment mode.

void spi_slave_hd_read_buffer(spi_host_device_t host_id, int addr, uint8_t *out_data, size_t len)

  Read the shared registers.

  参数:

    - host_id -- Host to read the shared registers

    - addr -- Address of register to read, 0 to `SOC_SPI_MAXIMUM_BUFFER_SIZE-1`

    - out_data -- [out] Output buffer to store the read data

    - len -- Length to read, not larger than `SOC_SPI_MAXIMUM_BUFFER_SIZE-addr`

void spi_slave_hd_write_buffer(spi_host_device_t host_id, int addr, uint8_t *data, size_t len)

  Write the shared registers.

  参数:

    - host_id -- Host to write the shared registers

    - addr -- Address of register to write, 0 to `SOC_SPI_MAXIMUM_BUFFER_SIZE-1`

    - data -- Buffer holding the data to write

    - len -- Length to write, `SOC_SPI_MAXIMUM_BUFFER_SIZE-addr`

esp_err_t spi_slave_hd_append_trans(spi_host_device_t host_id, spi_slave_chan_t chan, spi_slave_hd_data_t *trans, TickType_t timeout)

  Load transactions (append mode)

  备注

  In this mode, user transaction descriptors will be appended to the DMA and the DMA will keep processing the data without stopping

  参数:

    - host_id -- Host to load transactions

    - chan -- SPI_SLAVE_CHAN_TX or SPI_SLAVE_CHAN_RX

    - trans -- Transaction descriptor

    - timeout -- Timeout before the transaction is loaded

  返回:

    - ESP_OK: on success

    - ESP_ERR_INVALID_ARG: The input argument is invalid. Can be the following reason:

      - The buffer given is not DMA capable

      - The length of data is invalid (not larger than 0, or exceed the max transfer length)

      - The transaction direction is invalid

    - ESP_ERR_TIMEOUT: Master is still processing previous transaction. There is no available transaction for slave to load

    - ESP_ERR_INVALID_STATE: Function called in invalid state. This API should be called under append mode.

esp_err_t spi_slave_hd_get_append_trans_res(spi_host_device_t host_id, spi_slave_chan_t chan, spi_slave_hd_data_t **out_trans, TickType_t timeout)

  Get the result of a data transaction (append mode)

  备注

  This API should be called the same times as the `spi_slave_hd_append_trans`

  参数:

    - host_id -- Host to load the transaction

    - chan -- SPI_SLAVE_CHAN_TX or SPI_SLAVE_CHAN_RX

    - out_trans -- [out] Pointer to the transaction descriptor (`spi_slave_hd_data_t`) passed to the driver before. Hardware has finished this transaction. Member `trans_len` indicates the actual number of bytes of received data, it's meaningless for TX.

    - timeout -- Timeout before the result is got

  返回:

    - ESP_OK: on success

    - ESP_ERR_INVALID_ARG: Function is not valid

    - ESP_ERR_TIMEOUT: There's no transaction done before timeout

    - ESP_ERR_INVALID_STATE: Function called in invalid state. This API should be called under append mode.

### Structures

struct spi_slave_hd_data_t

  Descriptor of data to send/receive.

  Public Members

  uint8_t *data

    Buffer to send, must be DMA capable.

  size_t len

    Len of data to send/receive. For receiving the buffer length should be multiples of 4 bytes, otherwise the extra part will be truncated.

  size_t trans_len

    For RX direction, it indicates the data actually received. For TX direction, it is meaningless.

  uint32_t flags

    Bitwise OR of SPI_SLAVE_HD_TRANS_* flags.

  void *arg

    Extra argument indicating this data.

struct spi_slave_hd_event_t

  Information of SPI Slave HD event.

  Public Members

  spi_event_t event

    Event type.

  spi_slave_hd_data_t *trans

    Corresponding transaction for SPI_EV_SEND and SPI_EV_RECV events.

struct spi_slave_hd_callback_config_t

  Callback configuration structure for SPI Slave HD.

  Public Members

  slave_cb_t cb_buffer_tx

    Callback when master reads from shared buffer.

  slave_cb_t cb_buffer_rx

    Callback when master writes to shared buffer.

  slave_cb_t cb_send_dma_ready

    Callback when TX data buffer is loaded to the hardware (DMA)

  slave_cb_t cb_sent

    Callback when data are sent.

  slave_cb_t cb_recv_dma_ready

    Callback when RX data buffer is loaded to the hardware (DMA)

  slave_cb_t cb_recv

    Callback when data are received.

  slave_cb_t cb_cmd9

    Callback when CMD9 received.

  slave_cb_t cb_cmdA

    Callback when CMDA received.

  void *arg

    Argument indicating this SPI Slave HD peripheral instance.

struct spi_slave_hd_slot_config_t

  Configuration structure for the SPI Slave HD driver.

  Public Members

  uint8_t mode

    SPI mode, representing a pair of (CPOL, CPHA) configuration:

    - 0: (0, 0)

    - 1: (0, 1)

    - 2: (1, 0)

    - 3: (1, 1)

  uint32_t spics_io_num

    CS GPIO pin for this device.

  uint32_t flags

    Bitwise OR of SPI_SLAVE_HD_* flags.

  uint32_t command_bits

    command field bits, multiples of 8 and at least 8.

  uint32_t address_bits

    address field bits, multiples of 8 and at least 8.

  uint32_t dummy_bits

    dummy field bits, multiples of 8 and at least 8.

  uint32_t queue_size

    Transaction queue size. This sets how many transactions can be 'in the air' (queued using spi_slave_hd_queue_trans but not yet finished using spi_slave_hd_get_trans_result) at the same time.

  spi_dma_chan_t dma_chan

    DMA channel to used.

  spi_slave_hd_callback_config_t cb_config

    Callback configuration.

### Macros

SPI_SLAVE_HD_TRANS_DMA_BUFFER_ALIGN_AUTO

  Automatically re-malloc dma buffer if user buffer doesn't meet hardware alignment or dma_capable, this process may lose some memory and performance.

SPI_SLAVE_HD_TXBIT_LSBFIRST

  Transmit command/address/data LSB first instead of the default MSB first.

SPI_SLAVE_HD_RXBIT_LSBFIRST

  Receive data LSB first instead of the default MSB first.

SPI_SLAVE_HD_BIT_LSBFIRST

  Transmit and receive LSB first.

SPI_SLAVE_HD_APPEND_MODE

  Adopt DMA append mode for transactions. In this mode, users can load(append) DMA descriptors without stopping the DMA.

### Type Definitions

typedef bool (*slave_cb_t)(void *arg, spi_slave_hd_event_t *event, BaseType_t *awoken)

  Callback for SPI Slave HD.

### Enumerations

enum spi_slave_chan_t

  Channel of SPI Slave HD to do data transaction.

  Values:

  enumerator SPI_SLAVE_CHAN_TX

    The output channel (RDDMA)

  enumerator SPI_SLAVE_CHAN_RX

    The input channel (WRDMA)

提供有关此文档的反馈