Fast SPI communication with external ADC

opcode_x64
Posts: 47
Joined: Sun Jan 13, 2019 5:39 pm

Fast SPI communication with external ADC

Postby opcode_x64 » Thu Jun 11, 2020 9:10 am

Hello everbody,

Firstly, I hope you all are good within the Covid-19 time...

I want to read out an external analog digital converter (adc) with an esp32. The communication between esp32 and the adc is done by SPI. The adc uses a digital output signal called DRDY to signalize when a sample is ready to be read out. At the moment, I am reading the adc out by bit banging and buffering the read outs in a buffer array inside an gpio interrupt (DRDY signal) within the esp32 which runs on core1, where core0 is sending the data by WiFi. The problem is, that the SCLK generated in bit banging is limited to < 10MHz... Now, another, maybe faster, method would be reading out the external adc with hardware SPI and here I have some questions.

1.) it is possible to trigger the SPI Read Function by an external gpio trigger event (DRDY signal) ? I think with software this should be possible, but can we use here a hardware solution ?

2.) Is the SPI hardware suitable to this ? I am not sure because of the overhead the SPI hardware/driver of esp32 is using... The adc is sending 24 Bit data with a frequency (sample rates) about 10kHz, but I would to increase it up to 50kHz if possible.

3.) Can I read out the data directly to the buffer array with DMA but still triggering the SPI Read by the DRDY signal of the adc ??

Thank you !

Best regards,
opcode_x64

PeterR
Posts: 621
Joined: Mon Jun 04, 2018 2:47 pm

Re: Fast SPI communication with external ADC

Postby PeterR » Thu Jun 11, 2020 10:17 am

Hi,
I have used this style of ADC before. The device makes many samples and signals DRDY when it has a 256/512 average.

As I recall you (the ESP) have to supply the clock and so are SPI master.

As you point out the issues will be latency. If I construct the SPI transaction then I have about 30uS from the start of my ISR to the first SPI clock. Longer if I request DMA. I am constructing the transaction object though and could have created before & reuse.
Next you have the problem of other ISRs blocking and/or cutting across you. Using IDF 4.1 I sometimes see 2.5mS gaps. Check out then I2S posts and you will see that there are plenty of latency & blocking concerns/heart ache.
The stock answer is to place your ADC interrupt on core 1 and run all other services from core 0. You then only need to worry about your ADC code then.
10KHz (100uS) should be OK. 50KHz (20uS minus SPI trasaction = 18uS) will need some thinking and possible custom driver.

I don't think that you can trigger the SPI master module from DRDY. You should be able to create a clock signal when DRDY though (say RMT) and then maybe feed the ADC output into ESP SPI slave or similar.
& I also believe that IDF CAN should be fixed.

opcode_x64
Posts: 47
Joined: Sun Jan 13, 2019 5:39 pm

Re: Fast SPI communication with external ADC

Postby opcode_x64 » Thu Jun 11, 2020 10:37 am

Hello PeterR,

First, thank you for your quick reply !

I also believe, that hardware only solution is not possible, but I wanted to ask about that before burying the idea :-)

Like you mentioned, on core1 only the sampling process is executed, and on core0 all other things are executed (sending data over WiFi...).

Your idea to create a "pulse pattern" to generate the SCLK via RMT sounds very good ! But there is one question: How can I read then the data from the adc ?

For example, big banging is working like this:

SCLK --> HIGH
READ PIN STATE ON DOUT
SCLK --> LOW

this means I have to read the pin state of the DOUT of adc when changing the SCLK, but the RMT solution will send a pattern with 24 items doing 50% High and 50% Low (lets keep duty cycle 50% for quality =))... so I need a buffer which reads pin state of DOUT after sending one High Low Item of pulse pattern ?!

Can you give me a detailed hint about this ?

Thank you very much

bienvenu
Posts: 15
Joined: Fri Nov 27, 2015 11:06 am

Re: Fast SPI communication with external ADC

Postby bienvenu » Sun Jul 05, 2020 12:18 pm

I'm doing something similar, and have hit similar issues. If you really want to use the RMT to output signals, the only thing I could think of would be feeding the SCLK you output back into the ESP32 on another pin, and using a SPI peripheral in slave mode to read in the data. (I don't think you can "double connect" in the ESP32's IO mux, happy to be wrong.)

I'm presently trying to use a level 5 interrupt triggered from an external clock to do the GPIO pulse creation, delay, then use a pre-configured SPI peripheral in master mode, wait until the SPI completes and then read the data out of the SPI register. Hitting some issues with the interrupt watchdog, needs more work.

edit: I see your old thread over at https://esp32.com/viewtopic.php?f=13&t=15213
Will ingest that ;-)

bienvenu
Posts: 15
Joined: Fri Nov 27, 2015 11:06 am

Re: Fast SPI communication with external ADC

Postby bienvenu » Mon Jul 06, 2020 11:08 am

Hi opcode_x64,

Two days of head bashing, and I got that working. The esp-idf High Level Interrupt documentation needs to be updated, as we now need to add something to CMakeLists.txt to get it to link in the assembly (replace the last word with whatever you put at the bottom of the .S file:

Code: Select all

target_link_libraries(${COMPONENT_LIB} INTERFACE "-u ld_include_highint_hdl")
Beyond that, I've gone a much simpler route than you were thinking. I'll document it here for my future sanity.

I'm talking with a TI ADS8887 SAR ADC, which needs a >500ns conversion start pulse in, >12ns delay, then an SPI transaction to read in the data. I need to synchronise between multiple ADCs, so using SAR's seemed the easiest.

I use an external GPIO as reference clock input, using the rising edge to start an outgoing pulse with assembly. The assembly code also starts an SPI transaction on the the SPI peripheral. When the SPI peripheral is done, it then fires an interrupt to my own handler, which does all of the housekeeping, and passes the data in chunks to another task to be transmitted over TCP.

1) Set up the SPI peripheral as normal for interrupt handling. Run one SPI transaction just to confirm everything works. I run this from CPU1 using "xTaskCreatePinnedToCore()"

Code: Select all

static void my_spi_init(void) {
    spi_bus_config_t buscfg = {
        ...
    };
    spi_device_interface_config_t devcfg = {
        ...
    };

    ESP_ERROR_CHECK( spi_bus_initialize(SPI2_HOST, &buscfg, 1) );
    ESP_ERROR_CHECK( spi_bus_add_device(SPI2_HOST, &devcfg, &spi_dev) );
    ESP_ERROR_CHECK( spi_device_acquire_bus(spi_dev, portMAX_DELAY) );

    // do one transmit first to ensure that the settings are correct
    ESP_ERROR_CHECK( spi_device_queue_trans(spi_dev, &spi_trans, portMAX_DELAY ) );
    ESP_ERROR_CHECK( spi_device_get_trans_result(spi_dev, &spi_trans, portMAX_DELAY ) );
    
    // from here down is unusual
    vTaskDelay(1);

    intr_handle_t *spi_int = spi_bus_get_intr(SPI2_HOST);
    ESP_ERROR_CHECK( esp_intr_free(spi_int) );
    ESP_ERROR_CHECK( esp_intr_alloc(ETS_SPI2_INTR_SOURCE, ESP_INTR_FLAG_IRAM|ESP_INTR_FLAG_LEVEL3, spi2_handler, NULL, &spi2_intr_handle) );
    ESP_ERROR_CHECK( esp_intr_enable(spi2_intr_handle) );

    vTaskDelete(NULL);
}
2) You'll note the above reaches into the ESP SPI stack, disables the interrupt to their SPI handler "spi_intr()", and then connects up with my own SPI handler.
I added a helper function to esp-idf's spi-master.c in order to get access to the relevant intr_handle_t:

Code: Select all

intr_handle_t spi_bus_get_intr(spi_host_device_t host) {
    return spihost[host]->intr;
}
I'm not sure if there's a better way to do that.

I then manually enable the SPI interrupt to my handler:

Code: Select all

static void IRAM_ATTR spi2_handler(void *arg) {
    SPI2.slave.trans_done = 0; // reset the register
    uint32_t data = SPI_SWAP_DATA_RX(*(SPI2.data_buf), 18);
    ... do something with the data ...
}
3) Input sampling clock to ESP32 as a GPIO input. Set up a level 5 interrupt, and use assembly code to generate output pulse, and start SPI transaction:

Code: Select all

#include <xtensa/coreasm.h>
#include <xtensa/corebits.h>
#include <xtensa/config/system.h>
#include "freertos/xtensa_context.h"
#include "esp_debug_helpers.h"
#include "esp_private/panic_reason.h"
#include "sdkconfig.h"
#include "soc/soc.h"
#include "soc/dport_reg.h"

#define L5_INTR_STACK_SIZE  12
#define L5_INTR_A2_OFFSET   0
#define L5_INTR_A3_OFFSET   4
#define L5_INTR_A4_OFFSET   8

.data
_l5_intr_stack:
 .space      L5_INTR_STACK_SIZE

.section .iram1,"ax"
 .global     xt_highint5
 .type       xt_highint5,@function
 .align      4
 .literal .GPIO_STATUS1_W1TC_REG, 0x3FF44058
 .literal .GPIO_STATUS1_REG, 0x3FF44050
 .literal .GPIO_OUT_W1TS_REG, 0x3FF44008
 .literal .GPIO_OUT_W1TC_REG, 0x3FF4400C
 .literal .GPIO__NUM_33, (1<<1)
 .literal .GPIO__NUM_2,  (1<<2)
 .literal .SPI_CMD_REG, 0x3FF64000
 .literal .SPI_USR, (1<<18)
xt_highint5:
    /* save contents of registers A2-A4 */
    movi    a0, _l5_intr_stack
    s32i    a2, a0, L5_INTR_A2_OFFSET
    s32i    a3, a0, L5_INTR_A3_OFFSET
    s32i    a4, a0, L5_INTR_A4_OFFSET

    /* clearing the interrupt status of GPIO_NUM_33 */
    l32r a2, .GPIO_STATUS1_W1TC_REG
    l32r a3, .GPIO__NUM_33
    s32i a3, a2, 0

    /* setting GPIO2 to high */
    l32r a2, .GPIO_OUT_W1TS_REG
    l32r a3, .GPIO__NUM_2
    s32i a3, a2, 0

    /* busy wait */
    movi a4, 50
1:
    addi a4, a4, -1
    bnez a4, 1b

    /* setting GPIO2 to low */
    /* l32r a2, .GPIO_OUT_W1TC_REG */
    /* l32r a3, .GPIO__NUM_2*/
    s32i a3, a2, 4 /* GPIO_OUT_W1TC_REG = GPIO_OUT_W1TS_REG + 4 */

    /* SPI2.cmd.usr = 1; */
    /* The SPI peripheral takes a few hundred nanoseconds 
       to start, no need for extra delay */
    l32r a2, .SPI_CMD_REG
    l32r a3, .SPI_USR
    s32i a3, a2, 0

    /* This hack doesn't seem needed. Add if double interrupting occuring.
    l32r a2, .GPIO_STATUS1_REG
    l32i a2, a2, 0
    memw*/

    /* restore contents of registers A2-A4 */
    movi    a0, _l5_intr_stack
    l32i    a2, a0, L5_INTR_A2_OFFSET
    l32i    a3, a0, L5_INTR_A3_OFFSET
    l32i    a4, a0, L5_INTR_A4_OFFSET
    rsync                                   /* ensure register restored */

    /* hand back from interrupt */
    rsr     a0, EXCSAVE_5
    rfi     5

    .global ld_include_highint_hdl
ld_include_highint_hdl:
With all that running, it works:
Image
Yellow is input clock (30kHz), blue is output sample pulse, pink is SCLK (1MHz), blue is MISO.

There is a little jitter in the width of the assembly-code generated GPIO output pulse, but this isn't doesn't impact my application. I'm supposing this is caused by CPU0 halting CPU1 temporarily whilst accessing something. Not sure if this is something I can fix.

This is an excellent outcome, and I didn't need to resort to bit-banging SPI in assembly or anything too hideous :D
I've tested this to 200kHz with a higher frequency SCLK, and the limiting factor was the data transmission (TCP/IP and Ethernet code).

usulrasolas
Posts: 26
Joined: Tue Jun 09, 2020 5:27 pm

Re: Fast SPI communication with external ADC

Postby usulrasolas » Mon Jul 06, 2020 11:16 pm

thank you, this is very informative, it almost gets me to solving my own IRQ issues! Thanks for sharing OP and others! I am unaware of the SPI documentation issue and wonder if there's more information on the fix? Any tips help as I am working on very similar task

bienvenu
Posts: 15
Joined: Fri Nov 27, 2015 11:06 am

Re: Fast SPI communication with external ADC

Postby bienvenu » Thu Jul 09, 2020 9:11 am

Small update on my previous post:
Whilst ADC data flow "works fine" at 100kSPS, the THD+N of the captured waveform shows there is significant sampling jitter (or data misalignment). 50kSPS seems to work better. I think that it's a timing issue on the SPI handler - it may not get to run in between each sample at 100kSPS (10us) but at 50kSPS (20us) it works fine. Going from 100kSPS to 80kSPS shows a 20dB improvement in THD+N, and going from 80kSPS to 50kSPS shows another 30dB improvement.

A Helmut Weber style hard loop on the second CPU would probably improve this, but I don't want to spend the time on implementing that to check.

opcode_x64
Posts: 47
Joined: Sun Jan 13, 2019 5:39 pm

Re: Fast SPI communication with external ADC

Postby opcode_x64 » Tue Aug 25, 2020 6:01 pm

Hello bienvenu,

at first I want to apologize for replying so late... due to other things at work I had no time to continue on that topic. Secondly I want to thank you very much for providing this information ! It is possible to share a minimal example code which I can study in more detail ? Maybe a single *.c File is enough ?!

Thank you !

Best regards,
opcode_x64

pcp_696
Posts: 3
Joined: Thu Oct 07, 2021 4:39 am

Re: Fast SPI communication with external ADC

Postby pcp_696 » Thu Oct 07, 2021 9:04 am

I have to read the external ADC i.e ADS131a02 from TI in asynchronous slave mode, 32-bit device word, Hamming code word validation off. I should read when i receive an interrupt on DRDY pin, I am using the ESP_INTR_FLAG_EDGE as interrupt flag, i will call a vTaskNotifyGiveFromISR() from the interrupt handler. In the task i'll wait for notification using ulTaskNotifyTake(), once the notification is received i'll call "spi_device_transmit()" function with txBuffer of 0s and of length 96bits (32 bits for status+ 32 bits for channel1+ 32 bits for channel2). the RXbuffer with rxlength 96bits received after transmission shows the status as 0x2220 to NULL command, which indicates that "SPI fault" has occured. To further check on SPI fault i read the STAT_S register it gives the status as 0x2501, which indicates "Frame fault".
I am using the SPI clock speed at 26MHZ, clock speed above it doesn't work for the ADC.
I am using ESP32-WROOMD with cpu clock speed of 240MHz.

Who is online

Users browsing this forum: arnoldb1778 and 298 guests