SPI cannot deliver 800 ksps to external DAC

janlksrckts
Posts: 6
Joined: Thu Feb 02, 2023 8:53 am

SPI cannot deliver 800 ksps to external DAC

Postby janlksrckts » Wed Feb 22, 2023 4:53 pm

I need the ESP32 to provide data over SPI to a 12-bit DAC (AD5452) at a rate of at least 800 ksps, this equals an update every 1.25 us. The thing with DACs however (at least the AD5452 and the MCP48XX family) is that they require the CS to briefly (30 ns) go high after every 16 bit sent. This acts as a frame synchronization, and makes the DAC convert the digital value to an analogue voltage.

But the ESP32’s hardware SPI doesn’t seem to provide a means to do this. Therefore I split up the data in a sequence of 2-byte SPI transactions, after each one I set and cleared the DAC’s CS-line. This however resulted in a whopping 10 us delay in between transactions! The documentation indeed mentions this on https://docs.espressif.com/projects/esp ... iderations but I cannot understand why this was implemented with such an absurd overhead.

I finally resorted to implementing SPI in software, see tx_16bit_to_DAC(..) in code below; now I got 12 us per DAC update which is 9.6 times too slow.
I then reworked this to the lowest level code I could, see tx_16bit_to_DAC_low_level(..); now 2.6 us per update, still 2.1 times too slow.

Here is the entire runnable program:

Code: Select all

#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <inttypes.h>
#include <math.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_attr.h"
#include "FreeRTOSConfig.h"
#include "esp_pm.h"
#include "soc/gpio_struct.h"
#include "esp_timer.h"

// GPIO output pins for SPI
#define SCLK_PIN 18
#define MOSI_PIN 23
#define CS_PIN 5
#define OUTPUT_PINS  (1ULL << SCLK_PIN) | (1ULL << MOSI_PIN) | (1ULL << CS_PIN)

#define SPI_DATA_SIZE 256

DRAM_ATTR uint16_t _spi_tx_data[SPI_DATA_SIZE];

static uint32_t SCLK_VALUE = 1 << SCLK_PIN;
static uint32_t MOSI_VALUE = 1 << MOSI_PIN;
static uint32_t CS_VALUE = 1 << CS_PIN;

static void init_gpio() {
    gpio_config_t io_conf = {};
    io_conf.intr_type = GPIO_INTR_DISABLE;
    io_conf.mode = GPIO_MODE_OUTPUT;
    io_conf.pin_bit_mask = OUTPUT_PINS;	// bit mask of the pins that you want to set,e.g.GPIO18/19
    io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;	// == 0
    io_conf.pull_up_en = GPIO_PULLUP_DISABLE;	// == 0
    ESP_ERROR_CHECK(gpio_config(&io_conf));

    ESP_ERROR_CHECK(gpio_set_level(CS_PIN, 1));
}

static void init_sine_data() {
	float PI = 3.14159265;
	uint32_t max_value = 4095;	// 12 bit
	uint32_t step_size = max_value / SPI_DATA_SIZE;

	for (int i = 0; i < SPI_DATA_SIZE; i++) {
		uint16_t value = 2047 + round(2047 * sin(i * step_size * PI / 180));
		_spi_tx_data[i] = (value & 0x0FFF) | 0x3000;	// value is 12 bit OR 0x3000 for Gain = 1
	}
}

static void IRAM_ATTR tx_16bit_to_DAC(uint16_t value) {
	gpio_set_level(CS_PIN, 0);	// activate CS DAC

	for (uint16_t mask = 0x8000; mask > 0; mask >>= 1) {
		gpio_set_level(SCLK_PIN, 0);
		gpio_set_level(MOSI_PIN, (value & mask) != 0);
		gpio_set_level(SCLK_PIN, 1);
	}

	gpio_set_level(SCLK_PIN, 0);
	gpio_set_level(CS_PIN, 1);	// deactivate CS DAC
}

static void IRAM_ATTR tx_16bit_to_DAC_low_level(uint16_t value) {
	// Setting and clearing GPIO pins using out_w1ts & out_w1tc only for GPIO pin numbers < 32 !!
	// See gpio_ll.h line 430: 'gpio_ll_set_level(..)'

	GPIO.out_w1tc = CS_VALUE;		// gpio_set_level(CS_PIN, 0);	// activate CS DAC

	for (uint16_t mask = 0x8000; mask > 0; mask >>= 1) {
		GPIO.out_w1tc = SCLK_VALUE;	// gpio_set_level(SCLK_PIN, 0);
		if ((value & mask) != 0) {	// gpio_set_level(MOSI_PIN, (value & mask) != 0);
			GPIO.out_w1ts = MOSI_VALUE;
		}
		else {
			GPIO.out_w1tc = MOSI_VALUE;
		}
		GPIO.out_w1ts = SCLK_VALUE;	// gpio_set_level(SCLK_PIN, 1);
	}

	GPIO.out_w1tc = SCLK_VALUE;		// gpio_set_level(SCLK_PIN, 0);
	GPIO.out_w1ts = CS_VALUE;		// gpio_set_level(CS_PIN, 1);	// deactivate CS DAC
}

static void IRAM_ATTR tx_signal_to_DAC() {
	for (int i = 0; i < SPI_DATA_SIZE; i++) {
		// tx_16bit_to_DAC(_spi_tx_data[i]);	// 1 update takes 12 us, 9.6 times too slow
		tx_16bit_to_DAC_low_level(_spi_tx_data[i]);	// 1 update takes 2.6 us, 2.1 times too slow
	}
}

void spi_test(void * param) {
	init_gpio();
	init_sine_data();	// init table with sine values

	while (true) {
		tx_signal_to_DAC();
		vTaskDelay(200 / portTICK_PERIOD_MS);
	}
}

void app_main(void) {
	static uint8_t ucParams = 23;
	TaskHandle_t pCreatedTask = NULL;

	// Run on APP CORE
	xTaskCreatePinnedToCore(spi_test, "SPI_test", 2048, &ucParams, 19 | portPRIVILEGE_BIT, &pCreatedTask, 1);
	configASSERT(pCreatedTask);
}
I’m using an ESP32-WROOM DevKitC V4, 240 MHz clock speed, critical code in IRAM, compiled optimized for speed, and started this code as a separate Task on APP CORE with priority 19.

What else can I do to speed things up?
Convert this into assembly?
In viewtopic.php?t=17549 ESP_Sprite writes ‘fast GPIO’ options are available in the -S2 and -C3, would this help?

I was surprised to find only 1 person on the internet with the same question; AutogolazzoJr in viewtopic.php?p=109215#p109215: are we the only 2 people who are trying to do this (unlikely), or are we missing something trivially? I posted my question to him but so far no reply.

I chose the ESP32 for my project because I was impressed with its 240 MHz clock speed, powerful SPI at 80 MHz, hardware timers, etc., but I must say this SPI-to-DAC experience has been a pretty disappointing one :shock: . Hopefully someone can help me out.

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

Re: SPI cannot deliver 800 ksps to external DAC

Postby MicroController » Sat Feb 25, 2023 9:10 pm

Have a look into the I2S peripheral (https://docs.espressif.com/projects/esp ... ation-mode).
I2S has a data line, a clock line, and a "word select" (WS) line. You may be able to repurpose the WS line as the CS for your DAC, and you can configure what kind of signal the hardware should generate for WS. The "PCM" mode of the I2S already seems to be pretty close to what you need (see link above); with a little tweaking of the configuration (specifically i2s_std_slot_config_t), maybe you'll get somewhere.

Who is online

Users browsing this forum: No registered users and 51 guests