SPI 从机半双工模式
=====================

:link_to_translation:`en:[English]`

简介
------------

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

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

协议
^^^^^^^^

有关主设备与 SPI 从机通信的详细信息，请参阅 `ESP SPI 从机半双工协议 <https://espressif.github.io/idf-extra-components/latest/esp_serial_slave_link/spi_slave_hd_protocol.html#spi-slave-hd-half-duplex-protocol>`_ 。

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

- 一个 DMA 通道，支持主设备向从设备写入大量数据。
- 一个 DMA 通道，支持主设备从从设备读取大量数据。
- 一些通用寄存器，由主设备和从设备共享。
- 一些通用中断，用于主设备打断从设备的软件执行。

术语
-----------

- 事务 (transaction)
- 通道 (channel)
- 发送 (sending)
- 接收 (receiving)
- 数据描述符 (data descriptor)

驱动程序特性
--------------

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

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

驱动程序使用
------------

从机初始化
^^^^^^^^^^^^^^^^^^^^

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

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

如需使用 3-wire 模式，也称 single I/O (SIO) 模式，请在 :cpp:member:`spi_slave_hd_slot_config_t::flags` 中设置 :c:macro:`SPI_SLAVE_HD_3WIRE_MODE`。该模式下，MOSI 同时用于输入和输出数据，因此 :cpp:member:`spi_bus_config_t::mosi_io_num` 必须设置为支持输出的 GPIO。MISO 信号线不会使用，:cpp:member:`spi_bus_config_t::miso_io_num` 可以设置为 ``-1``。在该模式下，主设备应使用 1-bit SPI Slave HD 命令。带有 DIO/QIO 等 mask 的命令会使硬件选择 2 线或 4 线数据阶段，与 3-wire 模式不兼容，导致数据出错。

启用/禁用从机驱动（可选）
^^^^^^^^^^^^^^^^^^^^^^^^^

从机驱动程序支持在程序初始化后通过调用 :cpp:func:`spi_slave_hd_disable` / :cpp:func:`spi_slave_hd_enable` 来禁用/启用驱动程序，以便能够更改时钟或电源配置或休眠以节省电量。默认情况下，驱动程序在初始化后为“启用”状态。

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

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

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

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

.. only:: SOC_PSRAM_DMA_CAPABLE

    驱动程序支持使用 PSRAM 进行传输。直接传入 PSRAM 地址作为 :cpp:member:`spi_slave_hd_data_t::data` 即可。对于 DMA 接收通道，其内存地址和传输长度有对齐要求，使用 :cpp:func:`heap_caps_malloc` 分配内存可以自动处理对齐要求。对于不能控制的内存，也可以使用 :c:macro:`SPI_SLAVE_HD_TRANS_DMA_BUFFER_ALIGN_AUTO` 标志位，驱动会自动从 PSRAM 重新分配满足要求的内存。

    请注意该功能共享 MSPI 总线带宽（总线频率 * 总线位宽），因此主机对该设备的传输带宽应小于 PSRAM 带宽，否则 **可能会丢失传输数据**，此时获取传输结果会返回 :c:macro:`ESP_ERR_INVALID_STATE` 错误。

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

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

.. note::

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

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

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

.. _spi_slave_hd_data_arguments:

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

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

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

.. _spi_slave_hd_callbacks:

使用回调函数
^^^^^^^^^^^^^^^

.. note::

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

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

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

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

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

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

    - 参数 ``event`` 向回调函数传递当前事件信息。:cpp:type:`spi_slave_hd_event_t` 包含事件类型和刚刚处理完的数据描述符等信息，此时，通常会使用 :ref:`data argument <spi_slave_hd_data_arguments>`。
    - 参数 ``awoken`` 是一个输出参数，用于告知 ISR，在回调函数后已有其他操作唤醒任务，ISR 应调用 `portYIELD_FROM_ISR()` 调度这些任务。只需将 ``awoken`` 参数传递给可能解除任务阻塞的 FreeRTOS API，ISR 即可接收 ``awoken`` 的返回值。

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

调用 :cpp:func:`spi_slave_hd_write_buffer` 写入共享 buffer，调用 :cpp:func:`spi_slave_hd_read_buffer` 读取共享 buffer。

.. note::

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

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

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

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

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

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

.. only:: SOC_SPI_SUPPORT_SLEEP_RETENTION

    睡眠保留
    ^^^^^^^^

    {IDF_TARGET_NAME} 支持在进入 **Light Sleep** 之前保留 SPI 寄存器中的内容，并在唤醒后恢复。即程序不需要在 **Light Sleep** 唤醒后重新配置 SPI。

    该特性可以通过置位配置中的 :c:macro:`SPICOMMON_BUSFLAG_SLP_ALLOW_PD` 标志位启用。启用后驱动允许系统在 Light Sleep 时对 SPI 掉电，同时保存寄存器配置。它可以帮助降低轻度睡眠时的功耗，但需要花费一些额外的存储来保存寄存器的配置。

    注意在 Slave 角色下，不支持在所有传输（发送和接收）未完成时进入睡眠，否则将会出错。

.. only:: not esp32

  应用示例
  -------------------

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

  :example:`peripherals/spi_slave_hd/append_mode` 演示了如何使用 SPI Slave HD 驱动程序和 ESSL 驱动程序进行通信（ESSL 驱动程序是基于 SPI 主机驱动程序的封装层，用于与半双工模式的 SPI 从设备通信）。

  :example:`peripherals/spi_slave_hd/segment_mode` 演示了两种使用 SPI 从机半双工分段模式的方法：一种是使用 SPI 从机半双工驱动程序，通过两个任务与 SPI 主机进行多次通信；另一种是使用 ESP 串行从机连接 API 与从机进行多次数据交换。


API 参考
-------------

.. include-build-file:: inc/spi_slave_hd.inc
