How can I receive continuously with SPI into a circular buffer?
How can I receive continuously with SPI into a circular buffer?
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?
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?
-
- Posts: 9719
- Joined: Thu Nov 26, 2015 4:08 am
Re: How can I receive continuously with SPI into a circular buffer?
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.
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.
Re: How can I receive continuously with SPI into a circular buffer?
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.
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.
Re: How can I receive continuously with SPI into a circular buffer?
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.
After getting the descriptors setup I then start the capture:
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:
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?
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;
}
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;
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 ................
Re: How can I receive continuously with SPI into a circular buffer?
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:
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?
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 ................
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?
Re: How can I receive continuously with SPI into a circular buffer?
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:
spi_continuous.c:
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
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;
}
-
- Posts: 9719
- Joined: Thu Nov 26, 2015 4:08 am
Re: How can I receive continuously with SPI into a circular buffer?
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.
-
- Posts: 1699
- Joined: Mon Oct 17, 2022 7:38 pm
- Location: Europe, Germany
Re: How can I receive continuously with SPI into a circular buffer?
That "sending" signal sounds a lot like the usual /CS signal for SPI.
Have you tried setting the CS pin to the "sending" signal pin when configuring the SPI slave?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
Who is online
Users browsing this forum: Baidu [Spider] and 78 guests