How can I receive continuously with SPI into a circular buffer?

FozzTexx
Posts: 5
Joined: Thu Aug 29, 2024 6:15 pm

How can I receive continuously with SPI into a circular buffer?

Postby FozzTexx » Thu Aug 29, 2024 6:23 pm

Trying to figure out how I can do a continuous receive on ESP32 with SPI into a circular buffer and be able to know where the SPI peripheral position is at any time. It looks like there is a `dma_continue` flag that can be set, but I haven't foundt how to get the pointer to where in the buffer the SPI peripheral is currently placing data. I need to get the current position when an interrupt occurs so I can save the position, and then when a second interrupt occurs grab the new position.

I haven't yet tested if `dma_continue` works or not since I don't know how to find out the current position in the buffer.

Is there a way to find out the current position while receive is currently ongoing? Is it even possible to do a SPI receive into a circular buffer?

ESP_Sprite
Posts: 9568
Joined: Thu Nov 26, 2015 4:08 am

Re: How can I receive continuously with SPI into a circular buffer?

Postby ESP_Sprite » Fri Aug 30, 2024 1:01 am

I don't think it's possible to get a pointer like that... I believe the DMA subsystem may or may not have a register that indicates the current position, but there's FIFOs in front of that, so whatever byte is written will be an unknown amount of SPI data behind what's actually being read.

Can you elaborate a bit more on what high-level problem you're trying to solve with a setup like this? There may be other solutions.

FozzTexx
Posts: 5
Joined: Thu Aug 29, 2024 6:15 pm

Re: How can I receive continuously with SPI into a circular buffer?

Postby FozzTexx » Fri Aug 30, 2024 1:35 pm

I have a device which will suddenly set a "sending" signal low on one pin and then immediately go into transmitting a variable length data packet on another pin, with a clock speed of 2Mhz. If I call `spi_device_queue_trans` when the signal goes low, there is too long of a delay before the SPI peripheral actually starts capturing and I lose the sync and preamble bytes which are needed to decode the rest of the packet. At first I thought I could use the RMT peripheral, but the RMT buffer fills too quick and RMT doesn't support a circular buffer (`en_partial_rx = 1` errors out with "partial receive not supported").

My thought was that if I can start capturing long before the device starts sending then I won't lose bytes. If I can get a pointer to approximately where in the buffer the SPI peripheral is saving data when the device sets the "sending" signal then I can grab the pointer and save it, and once the packet is done I can shift the start pointer around as necessary to look for the exact start of the packet.

FozzTexx
Posts: 5
Joined: Thu Aug 29, 2024 6:15 pm

Re: How can I receive continuously with SPI into a circular buffer?

Postby FozzTexx » Sat Aug 31, 2024 1:57 pm

I think I've managed to get SPI to receive to a circular buffer, but for some reason it won't fill the chunk before moving to the next one. No matter what I set the chunk size too, it only writes 68 bytes and then skips to the next chunk.

The first thing I do is allocate the buffer and setup a linked list of lldesc_t to divide the buffer up into chunks since lldesc_t.size is limited to 12 bits or 4095. I'm using 128 right now.

Code: Select all


  d2w_buflen = 53168;
  d2w_buffer = (decltype(d2w_buffer)) heap_caps_malloc(d2w_buflen, MALLOC_CAP_DMA);
  memset(d2w_buffer, 0xff, d2w_buflen);

  // SPI continuous                                                                             
  {
#define CHUNK_SIZE 128

    uint32_t num_chunks, idx;
    lldesc_t *desc_ptr;


    num_chunks = (d2w_buflen + CHUNK_SIZE - 1) / CHUNK_SIZE;
    d2w_desc = (lldesc_t *) heap_caps_malloc(sizeof(lldesc_t) * num_chunks, MALLOC_CAP_DMA);
    if (!d2w_desc) {
      ESP_LOGE("SPI_DMA", "Failed to allocate DMA descriptor");
      return;
    }

    memset(d2w_desc, 0, sizeof(lldesc_t) * num_chunks);
    for (idx = 0; idx < num_chunks; idx++) {
      desc_ptr = &d2w_desc[idx];
      desc_ptr->length = CHUNK_SIZE; // Value of this field doesn't seem to make a difference, can be zero
      desc_ptr->size = CHUNK_SIZE;
      desc_ptr->owner = 1; // Owned by DMA hardware                                             
      desc_ptr->buf = &d2w_buffer[idx * CHUNK_SIZE];
      desc_ptr->qe.stqe_next = &d2w_desc[(idx + 1) % num_chunks];
    }
    d2w_desc[num_chunks - 1].size = d2w_buflen % CHUNK_SIZE;
  }
  
After getting the descriptors setup I then start the capture:

Code: Select all


    SPI3.dma_conf.val = SPI3.dma_conf.val | SPI_OUT_RST|SPI_IN_RST|SPI_AHBM_RST|SPI_AHBM_FIFO_RST;
    SPI3.dma_out_link.start  = 0;
    SPI3.dma_in_link.start   = 0;
    SPI3.dma_conf.val = SPI3.dma_conf.val & ~(SPI_OUT_RST|SPI_IN_RST|SPI_AHBM_RST|SPI_AHBM_FIFO_RST);

    SPI3.dma_in_link.addr = (uint32_t) d2w_desc;
    SPI3.dma_inlink_dscr = SPI3.dma_in_link.addr;
    SPI3.user.usr_miso = 1;
    SPI3.dma_in_link.start = 1;
    //SPI3.dma_conf.val = SPI3.dma_conf.val | SPI_OUT_DATA_BURST_EN | SPI_INDSCR_BURST_EN;      

#endif
    SPI3.dma_conf.dma_continue = 1;
#if 1
    // Start the SPI transaction                                                                
    SPI3.cmd.usr = 1;

The data in d2w_buffer ends up like this though, with big gaps in the received data. You can see the 0xff is still there from when I did the memset:

Code: Select all

12:45:39.112 > 0000  40 e4 80 08 40 e4 04 88 40 e4 84 88 40 e4 84 08  @...@...@...@...
12:45:39.121 > 0010  40 e4 84 00 ff ff ff ff ff ff ff ff ff ff ff ff  @...............
12:45:39.133 > 0020  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:45:39.141 > 0030  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:45:39.151 > 0040  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:45:39.161 > 0050  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:45:39.171 > 0060  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:45:39.181 > 0070  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:45:39.191 > 0080  88 40 e8 84 88 40 e4 84 80 40 e4 88 88 40 e8 84  .@...@...@...@..
12:45:39.201 > 0090  88 40 e4 84 80 40 e4 04 08 40 e4 84 88 40 e4 84  .@...@...@...@..
12:45:39.211 > 00a0  88 00 e4 84 88 40 e0 84 88 40 e8 84 80 40 e4 84  .....@...@...@..
12:45:39.221 > 00b0  08 40 e4 04 88 40 e4 84 88 40 e4 84 08 40 e4 84  .@...@...@...@..
12:45:39.231 > 00c0  88 40 e8 00 ff ff ff ff ff ff ff ff ff ff ff ff  .@..............
12:45:39.241 > 00d0  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:45:39.251 > 00e0  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:45:39.261 > 00f0  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:45:39.271 > 0100  84 88 40 e4 84 80 40 e4 80 88 40 e8 84 88 40 e4  ..@...@...@...@.
12:45:39.281 > 0110  84 80 40 e4 04 08 40 e4 84 88 40 e4 84 88 40 e4  ..@...@...@...@.
12:45:39.291 > 0120  84 88 40 e4 84 88 40 e8 84 80 40 e4 84 88 40 e4  ..@...@...@...@.
12:45:39.301 > 0130  04 88 40 e8 84 88 40 e4 84 08 40 e4 04 88 40 e0  ..@...@...@...@.
12:45:39.311 > 0140  84 88 40 00 ff ff ff ff ff ff ff ff ff ff ff ff  ..@.............
12:45:39.321 > 0150  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:45:39.331 > 0160  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:45:39.341 > 0170  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:45:39.351 > 0180  e4 84 80 40 e4 84 88 40 e8 84 88 40 e4 84 80 40  ...@...@...@...@
12:45:39.361 > 0190  e4 04 08 40 e4 84 88 40 e4 84 88 40 e4 84 88 40  ...@...@...@...@
12:45:39.371 > 01a0  e4 04 88 40 e8 84 88 00 e4 84 80 40 e4 04 88 40  ...@.......@...@
12:45:39.381 > 01b0  e8 84 88 40 e4 84 08 40 e4 84 88 40 e4 84 88 40  ...@...@...@...@
12:45:39.391 > 01c0  e4 84 80 00 ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:45:39.401 > 01d0  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:45:39.411 > 01e0  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:45:39.421 > 01f0  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................

Why is it not writing all the data before going to the next chunk? Why does the first chunk have 32 bytes less than all the rest?

FozzTexx
Posts: 5
Joined: Thu Aug 29, 2024 6:15 pm

Re: How can I receive continuously with SPI into a circular buffer?

Postby FozzTexx » Sun Sep 01, 2024 2:03 pm

After more investigation I've found that no matter what I do, the SPI will write 68 bytes no matter what. If I change the chunk size to 64, it still writes 68 bytes, but the extra 4 bytes wrap around to the beginning of the chunk before it moves to the next chunk!

It also is actually only writing 67 bytes of data to the buffer, followed by a null byte. I changed the chunk size to 68 bytes and temporarily hooked the "sending" signal to the MOSI line so that the buffer was filled with longs runs of ones followed by a long run of zero, and I could see this:

Code: Select all

12:10:24.292 > 9e90  ff ff ff 00 ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:10:24.302 > 9ea0  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:10:24.312 > 9eb0  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:10:24.322 > 9ec0  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:10:24.332 > 9ed0  ff ff ff ff ff ff ff 00 ff ff ff ff ff ff ff ff  ................
12:10:24.342 > 9ee0  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:10:24.352 > 9ef0  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:10:24.362 > 9f00  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:10:24.372 > 9f10  ff ff ff ff ff ff ff ff ff ff ff 00 ff ff ff ff  ................
12:10:24.382 > 9f20  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:10:24.392 > 9f30  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:10:24.402 > 9f40  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:10:24.412 > 9f50  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 00  ................
12:10:24.422 > 9f60  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
12:10:24.432 > 9f70  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
During the part where the "sending" signal is low I could see a long run of zeros as expected, however it was only half as many as it should be based on what I have the SPI clock speed set to and how long the signal was low (checked with logic analyzer). For some reason when using continuous receive the SPI is capturing at half the configured clock rate. I know the clock rate is configured correctly because if I use `spi_device_queue_trans` I can see the individual bit time matches the signal.

I found `SPI3.dma_inlink_dscr->buf` and `SPI3.dma_inlink_dscr_bf1` which give me pointers into where the SPI is currently putting data. When I have the chunk size set to an even power of 2, `SPI3.dma_inlink_dscr_bf1` appears to be exactly where it is currently writing, with `SPI3.dma_inlink_dscr->buf` pointing to the next chunk that will be written to. If chunk size is 68, then `SPI3.dma_inlink_dscr_bf1` just contains garbage and doesn't point to the buffer at all.

Very strange.

How can I fix the 67+null terminator problem? How can I get the SPI to capture at the configured clock rate instead of at half?

FozzTexx
Posts: 5
Joined: Thu Aug 29, 2024 6:15 pm

Re: How can I receive continuously with SPI into a circular buffer?

Postby FozzTexx » Sun Sep 08, 2024 3:01 pm

I figured out how to get it to work! Wasn't easy, spent a lot of time going over the ESP32 tech ref manual, the ESP32 programming guide, and walking through the SPI driver source. I was able to get SPI continuous to work and I'm able to get a pointer to where in the buffer that is currently being written.

I had to copy the structs from spi_master.c so that I could work with spi_device_handle_t the same way that the SPI drivers do, but otherwise it uses the SPI driver API, with only a few things added to turn continuous on & off. I also needed to make a function to setup the linked list in a loop, with the option of setting the chunk size so that I can get a pointer to the position within the buffer. Smaller chunk sizes will allow more precision from cspi_get_position. For my use, a 128 byte chunk size was good enough, since there is a sync header in the protocol I'm capturing.

Hopefully continuous/circular/ring buffer support will get added to the official driver.

spi_continuous.h:

Code: Select all

#include <driver/spi_master.h>
#include <soc/lldesc.h>

#ifdef __cplusplus
extern "C" {
#endif

  size_t cspi_alloc_continuous(size_t length, size_t chunk_size,
			       uint8_t **buffer, lldesc_t **desc);
  void cspi_begin_continuous(spi_device_handle_t handle, lldesc_t *desc);
  void cspi_end_continuous(spi_device_handle_t handle);
  size_t cspi_current_pos(spi_device_handle_t handle);

#ifdef __cplusplus
}
#endif
spi_continuous.c:

Code: Select all

#include "spi_continuous.h"
#include <esp_log.h>
#include <esp_private/spi_common_internal.h>
#include <hal/spi_hal.h>
#include <freertos/queue.h>

// FIXME - structs that had to be copied from ESPIDF components/driver/spi_master.c
//         these structs are needed in order to tear down a
//         spi_device_handle_t to access the selected SPI peripheral
//         registers

// spi_device_t and spi_host_t reference each other
typedef struct spi_device_t spi_device_t;
//spi_device_handle_t is a pointer to spi_device_t

typedef struct {
  spi_transaction_t *trans;
  const uint32_t *buffer_to_send;
  uint32_t *buffer_to_rcv;
} spi_trans_priv_t;

typedef struct {
  int id;
  spi_device_t *device[DEV_NUM_MAX];
  intr_handle_t intr;
  spi_hal_context_t hal;
  spi_trans_priv_t cur_trans_buf;
  int cur_cs;
  const spi_bus_attr_t *bus_attr;
  spi_device_t *device_acquiring_lock;

  //debug information
  bool polling;   //in process of a polling, avoid of queue new transactions into ISR
} spi_host_t;

struct spi_device_t {
  int id;
  QueueHandle_t trans_queue;
  QueueHandle_t ret_queue;
  spi_device_interface_config_t cfg;
  spi_hal_dev_config_t hal_dev;
  spi_host_t *host;
  spi_bus_lock_dev_handle_t dev_lock;
};

// END spi_master.c structs

/* Allocates a DMA buffer that is at least length long but is an even
   multiple of chunk_size. Sets up a circular linked list with each
   element pointing to a successive section of buffer that is only
   chunk_size long. The last element in the list points back to the
   first and no element has eof set.

   Returns length of allocated buffer or 0 on error.
*/
size_t cspi_alloc_continuous(size_t length, size_t chunk_size,
			     uint8_t **buffer, lldesc_t **desc)
{
  uint32_t num_chunks, idx;
  lldesc_t *llfirst, *llcur;
  uint8_t *newbuf;


  *buffer = NULL;
  *desc = NULL;

  /* Make sure it's an even multiple of chunk_size */
  num_chunks = (length + chunk_size - 1) / chunk_size;
  length = num_chunks * chunk_size;
  newbuf = (uint8_t *) heap_caps_malloc(length, MALLOC_CAP_DMA);
  if (!newbuf) {
    ESP_LOGE("SPI_DMA", "Failed to allocate DMA descriptor");
    return 0;
  }
  *buffer = newbuf;

  llfirst = (lldesc_t *) heap_caps_malloc(sizeof(lldesc_t) * num_chunks, MALLOC_CAP_DMA);
  if (!llfirst) {
    ESP_LOGE("SPI_DMA", "Failed to allocate DMA descriptor");
    return 0;
  }
  *desc = llfirst;

  memset(llfirst, 0, sizeof(lldesc_t) * num_chunks);
  for (idx = 0; idx < num_chunks; idx++) {
    llcur = &llfirst[idx];
    llcur->size = chunk_size;
    llcur->owner = 1; // Owned by DMA hardware
    llcur->buf = &newbuf[idx * chunk_size];
    llcur->qe.stqe_next = &llfirst[(idx + 1) % num_chunks];
  }

  return length;
}

/* Put SPI peripheral into continuous capture mode */
void cspi_begin_continuous(spi_device_handle_t handle, lldesc_t *desc)
{
  spi_transaction_t rxtrans;
  spi_host_t *host = handle->host;
  spi_dev_t *hw = host->hal.hw;


  /* Do a quick poll transaction with a single bit to get the SPI
     peripheral configured. Must be less than 8 bits otherwise
     continuous doesn't work. */
  memset(&rxtrans, 0, sizeof(spi_transaction_t));
  rxtrans.rxlength = 1;
  rxtrans.rx_buffer = (uint8_t *) desc->buf;

  ESP_ERROR_CHECK(spi_device_polling_start(handle, &rxtrans, portMAX_DELAY));
  spi_device_polling_end(handle, portMAX_DELAY);

  /* Setup SPI peripheral for continuous using passed linked list */
  hw->dma_in_link.addr = (uint32_t) desc;
  hw->dma_inlink_dscr = hw->dma_in_link.addr;
  hw->dma_conf.dma_continue = 1;
  hw->dma_in_link.start = 1;

  // Start SPI receive
  hw->cmd.usr = 1;

  return;
}

void cspi_end_continuous(spi_device_handle_t handle)
{
  spi_transaction_t rxtrans;
  spi_host_t *host = handle->host;
  spi_dev_t *hw = host->hal.hw;


  hw->dma_conf.dma_rx_stop = 1;
  hw->dma_conf.dma_tx_stop = 1;

  /* Wait for transaction to come to a full and complete stop */
  while (hw->ext2.st)
    ;

  hw->dma_conf.dma_continue = 0;

  /* Do a poll transaction with 8 bits to get the SPI peripheral back
     into a state that the ESPIDF driver expects */
  memset(&rxtrans, 0, sizeof(spi_transaction_t));
  rxtrans.rxlength = 8;
  rxtrans.rx_buffer = (uint8_t *) heap_caps_malloc(8, MALLOC_CAP_DMA);
  ESP_ERROR_CHECK(spi_device_polling_start(handle, &rxtrans, portMAX_DELAY));
  spi_device_polling_end(handle, portMAX_DELAY);
  heap_caps_free(rxtrans.rx_buffer);

  return;
}

// Get current SPI position
size_t cspi_current_pos(spi_device_handle_t handle)
{
  uint8_t *buf, *cur;
  lldesc_t *current_desc;
  spi_host_t *host = handle->host;
  spi_dev_t *hw = host->hal.hw;


  // Access the current descriptor being used by DMA
  current_desc = (typeof(current_desc)) hw->dma_inlink_dscr;
  cur = (typeof(cur)) current_desc->buf;
  buf = (typeof(buf)) host->cur_trans_buf.buffer_to_rcv;

  return cur - buf;
}

ESP_Sprite
Posts: 9568
Joined: Thu Nov 26, 2015 4:08 am

Re: How can I receive continuously with SPI into a circular buffer?

Postby ESP_Sprite » Mon Sep 09, 2024 2:22 am

Great you got it to work! Fyi there's an idea to abstract DMA operations for all peripherals into 'DMA patterns' like scatter/gather buffers, pingpong buffers, circular buffers etc and then allow any peripheral driver to make use of that. Not sure when it'll appear in ESP-IDF, but we're thinking of use cases like yours.

MicroController
Posts: 1541
Joined: Mon Oct 17, 2022 7:38 pm
Location: Europe, Germany

Re: How can I receive continuously with SPI into a circular buffer?

Postby MicroController » Tue Sep 10, 2024 2:37 pm

FozzTexx wrote:
Fri Aug 30, 2024 1:35 pm
I have a device which will suddenly set a "sending" signal low on one pin and then immediately go into transmitting a variable length data packet on another pin, with a clock speed of 2Mhz.
[
That "sending" signal sounds a lot like the usual /CS signal for SPI.
If I call `spi_device_queue_trans` when the signal goes low, there is too long of a delay before the SPI peripheral actually starts capturing
Have you tried setting the CS pin to the "sending" signal pin when configuring the SPI slave?

Who is online

Users browsing this forum: Bing [Bot], Google [Bot] and 73 guests