How perform fast SPI ADC with Timer on NRF52840 and Zephyr

Hi, i'm using NRF52840 DK connected with SPI on a 12 bits ADC reader  : MAX11665 

My objective is to perform a 256 values continuous reading at 44.1 khz on SPI and perform an FFT on the result set.

I'm using nrf connect with zephyr 2.4.2.

My concern is to precisely get 128 (or even 256) values every 1/44100 seconds, meaning every 22 microseconds.

Goal Algoritm : 

While (not finish reading 256 values)

Get current time

Read ADC

Wait until time elapsed with previous current time = 22 microseconds

End While

Do FFT transform

I didn't find a good way to get elapsed time in microseconds, as zephyr function k_uptime_delta i only in miliseconds precision

So i change with a timer and callback solution as it seem to take microseconds in the parameter with this call :

k_timer_start(&adc_timer, K_USEC(INTERVAL), K_USEC(INTERVAL));

But it seam that the overal process is very time consuming and it didn't perform the whole adc reading in the given time.

What is the good practice to do it ? Do i have to use k_uptime_ticks ? 
Is it a stable constant as it relay on the system speed ?

Here's my code :

prj.conf :

CONFIG_SPI=y
CONFIG_NRFX_SPI1=y
CONFIG_MAIN_STACK_SIZE=4048

CONFIG_FPU=y
CONFIG_NEWLIB_LIBC=y
CONFIG_NEWLIB_LIBC_FLOAT_PRINTF=y
CONFIG_CMSIS_DSP_TRANSFORM=y 
CONFIG_CMSIS_DSP=y
CONFIG_CMSIS_DSP_COMPLEXMATH=y
CONFIG_CMSIS_DSP_STATISTICS=y

nrf52840.overlay : 

&pinctrl {
	spi1_default: spi1_default {
                group1 {
					psels = <NRF_PSEL(SPIM_SCK, 0, 31)>,
					<NRF_PSEL(SPIM_MOSI, 0, 30)>,
					<NRF_PSEL(SPIM_MISO, 1, 15)>;//dout
                };
	};

	spi1_sleep: spi1_sleep {
                group1 {
                        psels = <NRF_PSEL(SPIM_SCK, 0, 31)>,
                                <NRF_PSEL(SPIM_MOSI, 0, 30)>,
                                <NRF_PSEL(SPIM_MISO, 1, 15)>;
                };
	};
};

my_spi_device: &spi1 {
	compatible = "nordic,nrf-spi";
	status = "okay";
	pinctrl-0 = <&spi1_default>;
	pinctrl-1 = <&spi1_sleep>;
	cs-gpios = <&gpio1 7 GPIO_ACTIVE_LOW>;
	pinctrl-names = "default", "sleep";
	clock-frequency = <8000000>;
	reg_my_spi_master: spi-dev-a@0 {
		reg = <0>;
		compatible = "spi-device";
        spi-max-frequency = < 800000 >;
	};
};
main.c with timer : (but too long to process)

#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/devicetree.h>
#include <arm_math.h>
#include <arm_const_structs.h>

#define SPI1_NODE DT_NODELABEL(spi1)
#define MY_SPI_DEVICE DT_NODELABEL(my_spi_device)
#define FFT_SIZE 128
#define FS 44100                   // ADC Fequency
#define INTERVAL (1000000 / FS) // 


#define MY_GPIO1 DT_NODELABEL(gpio1)
#define GPIO_1_CS 7
static const struct device *gpio1_dev = DEVICE_DT_GET(MY_GPIO1);

volatile int current_index = 0;

const struct device *spi_dev;
arm_rfft_fast_instance_f32 fft_instance ;

struct spi_cs_control spim_cs = {
    .gpio = SPI_CS_GPIOS_DT_SPEC_GET(DT_NODELABEL(reg_my_spi_master)),
    .delay = 0,
};
struct k_work adc_work;

uint8_t tx_buffer[2] = {0};
uint8_t rx_buffer[2] = {0};

struct k_timer adc_timer;


static struct spi_config spi_cfg = {
    .frequency = 8000000,
    .operation = SPI_OP_MODE_MASTER | SPI_TRANSFER_MSB | SPI_WORD_SET(8) | SPI_LINES_SINGLE | SPI_MODE_CPOL | SPI_MODE_CPHA,
    .slave = 0,
    .cs = &spim_cs};


// input data

float32_t fftInput[FFT_SIZE];
float32_t fftOutput[FFT_SIZE];
float32_t fftMagnitude[FFT_SIZE/2];


static void spi_init(void)
{
    spi_dev = DEVICE_DT_GET(MY_SPI_DEVICE);
    if (!device_is_ready(spi_dev))
    {
        printk("SPI master device not ready!\n");
    }
    if (!device_is_ready(spim_cs.gpio.port))
    {
        printk("SPI master chip select device not ready!\n");
    }
}


void adc_timer_handler(struct k_timer *dummy)
{
    k_work_submit(&adc_work);
}

void adc_work_handler(struct k_work *work)
{
    uint16_t adc_value;
    read_adc_value(&adc_value);
    fftInput[current_index] = (float32_t)adc_value;
    current_index = (current_index + 1) % FFT_SIZE;
}

void read_adc_value(uint16_t *value)
{

    const struct spi_buf tx_buf = {
        .buf = tx_buffer,
        .len = sizeof(tx_buffer)};

    const struct spi_buf_set tx = {
        .buffers = &tx_buf,
        .count = 1};

    struct spi_buf rx_buf = {
        .buf = rx_buffer,
        .len = sizeof(rx_buffer)};

    const struct spi_buf_set rx = {
        .buffers = &rx_buf,
        .count = 1};

    tx_buffer[0] = 0;
    tx_buffer[1] = 0;
    rx_buffer[0] = 0;
    rx_buffer[1] = 0;

    
    int error = spi_transceive(spi_dev, &spi_cfg, &tx, &rx);
    
    if (error != 0)
    {
        printk("SPI transceive error: %i  cfg=%d\n", error, spi_cfg.frequency);
        return;
    }

    *value = ((rx_buffer[0] << 8) | rx_buffer[1]) >> 4;
}
int main(void)
{
      arm_status status;

    status = ARM_MATH_SUCCESS;
    if (!device_is_ready(gpio1_dev))
    {
        printk("GPIO device not found!\n");
        return 1;
    }

    printk("%s started\n\n", CONFIG_BOARD);
    spi_init();

    if (!device_is_ready(gpio1_dev))
    {
        printk("GPIO device not found!\n");
        return 1;
    }
    gpio_pin_configure(gpio1_dev, GPIO_1_CS, GPIO_OUTPUT_ACTIVE | GPIO_ACTIVE_LOW);

    if (!device_is_ready(spi_dev))
    {
        printk("SPI device not found!\n");
        return 1;
    }

    status = arm_rfft_fast_init_f32(&fft_instance, FFT_SIZE);
    if (status != ARM_MATH_SUCCESS)
    {
        printf(" RFFT init error : %i\n",status);
        return -1;
    }
    k_work_init(&adc_work, adc_work_handler);

    k_timer_init(&adc_timer, adc_timer_handler, NULL);
    k_timer_start(&adc_timer, K_USEC(INTERVAL), K_USEC(INTERVAL));

    while (1)
    {
        while(current_index != 0);
        
    // RFFT execution 
    arm_rfft_fast_f32(&fft_instance, fftInput, fftOutput, 0);

    // magnitude calculation
    arm_cmplx_mag_f32(fftOutput, fftMagnitude, FFT_SIZE/2);


    // find max index.
    uint32_t maxIndex;
    float32_t maxValue;
    fftMagnitude[0]=0;//exlude low frequency
    arm_max_f32(fftMagnitude, FFT_SIZE/2, &maxValue, &maxIndex);
    
    // main frequency
    float32_t dominantFrequency = maxIndex * (FS / FFT_SIZE);
    
    printf("%f %f\n", dominantFrequency, maxValue);
    
    }
    return 0;
}

main.c in simple continuous mode (but i need to wait 22 microseconds every adc reads) 

#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/devicetree.h>
#include <arm_math.h>
#include <arm_const_structs.h>

#define SPI1_NODE DT_NODELABEL(spi1)
#define MY_SPI_DEVICE DT_NODELABEL(my_spi_device)
#define FFT_SIZE 256
#define FS 44100                   // Freaq reading
#define INTERVAL (1000000 / FS) // 

#define MY_GPIO1 DT_NODELABEL(gpio1)
#define GPIO_1_CS 7
static const struct device *gpio1_dev = DEVICE_DT_GET(MY_GPIO1);

const struct device *spi_dev;
arm_rfft_fast_instance_f32 fft_instance ;//= &arm_cfft_sR_f32_len16;

//arm_rfft_fast_instance_f32
struct spi_cs_control spim_cs = {
    .gpio = SPI_CS_GPIOS_DT_SPEC_GET(DT_NODELABEL(reg_my_spi_master)),
    .delay = 0,
};

uint8_t tx_buffer[2] = {0};
uint8_t rx_buffer[2] = {0};

static void spi_init(void)
{
    spi_dev = DEVICE_DT_GET(MY_SPI_DEVICE);
    if (!device_is_ready(spi_dev))
    {
        printk("SPI master device not ready!\n");
    }
    if (!device_is_ready(spim_cs.gpio.port))
    {
        printk("SPI master chip select device not ready!\n");
    }
}



static struct spi_config spi_cfg = {
    .frequency = 8000000,
    .operation = SPI_OP_MODE_MASTER | SPI_TRANSFER_MSB | SPI_WORD_SET(8) | SPI_LINES_SINGLE | SPI_MODE_CPOL | SPI_MODE_CPHA,
    .slave = 0,
    .cs = &spim_cs};


// reading arrays

float32_t fftInput[FFT_SIZE];
float32_t fftOutput[FFT_SIZE];
float32_t fftMagnitude[FFT_SIZE/2];

void read_adc_value(uint16_t *value)
{

    const struct spi_buf tx_buf = {
        .buf = tx_buffer,
        .len = sizeof(tx_buffer)};

    const struct spi_buf_set tx = {
        .buffers = &tx_buf,
        .count = 1};

    struct spi_buf rx_buf = {
        .buf = rx_buffer,
        .len = sizeof(rx_buffer)};

    const struct spi_buf_set rx = {
        .buffers = &rx_buf,
        .count = 1};

    tx_buffer[0] = 0;
    tx_buffer[1] = 0;
    rx_buffer[0] = 0;
    rx_buffer[1] = 0;

    int error = spi_transceive(spi_dev, &spi_cfg, &tx, &rx);

    if (error != 0)
    {
        printk("SPI transceive error: %i  cfg=%d\n", error, spi_cfg.frequency);
        return;
    }

    *value = ((rx_buffer[0] << 8) | rx_buffer[1]) >> 4;
    // printk("value final => %d\n", *value);
}
int main(void)
{
      arm_status status;

    status = ARM_MATH_SUCCESS;
    if (!device_is_ready(gpio1_dev))
    {
        printk("GPIO device not found!\n");
        return 1;
    }

    printk("%s started\n\n", CONFIG_BOARD);
    spi_init();

    if (!device_is_ready(gpio1_dev))
    {
        printk("GPIO device not found!\n");
        return 1;
    }
    gpio_pin_configure(gpio1_dev, GPIO_1_CS, GPIO_OUTPUT_ACTIVE | GPIO_ACTIVE_LOW);

    if (!device_is_ready(spi_dev))
    {
        printk("SPI device not found!\n");
        return 1;
    }

    status = arm_rfft_fast_init_f32(&fft_instance, FFT_SIZE);
    if (status != ARM_MATH_SUCCESS)
    {
        printf("RFFT init error : %i\n",status);
        return -1;
    }

    while (1)
    {

        for (int i = 0; i < FFT_SIZE; i++)
        {
            uint16_t adc_value;
            read_adc_value(&adc_value);
            fftInput[i] = (float32_t)adc_value;

            //k_usleep(INTERVAL); // NEED TO SLEEP HERE !
        }
        
    arm_rfft_fast_f32(&fft_instance, fftInput, fftOutput, 0);
    arm_cmplx_mag_f32(fftOutput, fftMagnitude, FFT_SIZE/2);
    uint32_t maxIndex;
    float32_t maxValue;
    fftMagnitude[0]=0;
    arm_max_f32(fftMagnitude, FFT_SIZE/2, &maxValue, &maxIndex);

    float32_t dominantFrequency = maxIndex * (FS / FFT_SIZE);

    printf("%f %f\n", dominantFrequency, maxValue);

    }
    return 0;
}


Regards

  • Hi,

    In the main.c with timer code, why didn't you just set a flag in the timer callback handler and then check this flag in the main loop, and if set start the SPI transaction. I see that you instead, add a task to the work queue etc. Maybe that would decrease the time it takes for the SPI transaction to start,

    regards

    Jared 

  • Yes, you're right i firstly did this because i was doing all SPI in the callback which is a terrible idea.

    So i put a simple flag and it's faster but not enough, i'm stuck at around 20khz (40 microseconds), when i put something faster in the timer then it slows down.
    When i perform the reading without any wait in the loop the reading speed is about 40khz which is my goal but i can't let it go like that as it's not meant to be stable.

    updated code : 

    #include <zephyr/kernel.h>
    #include <zephyr/drivers/gpio.h>
    #include <zephyr/drivers/spi.h>
    #include <zephyr/devicetree.h>
    #include <arm_math.h>
    #include <arm_const_structs.h>
    
    #define SPI1_NODE DT_NODELABEL(spi1)
    #define MY_SPI_DEVICE DT_NODELABEL(my_spi_device)
    #define FFT_SIZE 256
    #define FS 20000                // Fréquence d'échantillonnage
    #define INTERVAL (1000000 / FS) // Intervalle en microsecondes basé sur FS
    #ifndef M_PI
    #define M_PI 3.14159265358979323846
    #endif
    const struct device *spi_dev;
    arm_rfft_fast_instance_f32 fft_instance; //= &arm_cfft_sR_f32_len16;
    struct k_timer sampling_timer;
    
    volatile bool adc_read_flag = false;
    void sampling_handler(struct k_timer *dummy)
    {
        adc_read_flag = true;
    }
    
    // arm_rfft_fast_instance_f32
    struct spi_cs_control spim_cs = {
        .gpio = SPI_CS_GPIOS_DT_SPEC_GET(DT_NODELABEL(reg_my_spi_master)),
        .delay = 0,
    };
    
    uint8_t tx_buffer[2] = {0};
    uint8_t rx_buffer[2] = {0};
    
    static void spi_init(void)
    {
        spi_dev = DEVICE_DT_GET(MY_SPI_DEVICE);
        if (!device_is_ready(spi_dev))
        {
            printk("SPI master device not ready!\n");
        }
        if (!device_is_ready(spim_cs.gpio.port))
        {
            printk("SPI master chip select device not ready!\n");
        }
    }
    
    #define MY_GPIO1 DT_NODELABEL(gpio1)
    #define GPIO_1_CS 7
    static const struct device *gpio1_dev = DEVICE_DT_GET(MY_GPIO1);
    
    static struct spi_config spi_cfg = {
        .frequency = 8000000,
        .operation = SPI_OP_MODE_MASTER | SPI_TRANSFER_MSB | SPI_WORD_SET(8) | SPI_LINES_SINGLE | SPI_MODE_CPOL | SPI_MODE_CPHA,
        .slave = 0,
        .cs = &spim_cs};
    
    // Tableau des données
    
    float32_t fftInput[FFT_SIZE];
    float32_t fftOutput[FFT_SIZE];
    float32_t fftMagnitude[FFT_SIZE / 2];
    
    void read_adc_value(uint16_t *value)
    {
    
        const struct spi_buf tx_buf = {
            .buf = tx_buffer,
            .len = sizeof(tx_buffer)};
    
        const struct spi_buf_set tx = {
            .buffers = &tx_buf,
            .count = 1};
    
        struct spi_buf rx_buf = {
            .buf = rx_buffer,
            .len = sizeof(rx_buffer)};
    
        const struct spi_buf_set rx = {
            .buffers = &rx_buf,
            .count = 1};
    
        tx_buffer[0] = 0;
        tx_buffer[1] = 0;
        rx_buffer[0] = 0;
        rx_buffer[1] = 0;
    
        // printk("before => %d %d %d %d\n",tx_buffer[0],tx_buffer[1],rx_buffer[0],rx_buffer[1]);
        int error = spi_transceive(spi_dev, &spi_cfg, &tx, &rx);
        // printk("after => %d %d %d %d\n",tx_buffer[0],tx_buffer[1],rx_buffer[0],rx_buffer[1]);
        // k_usleep(10);
    
        if (error != 0)
        {
            printk("SPI transceive error: %i  cfg=%d\n", error, spi_cfg.frequency);
            return;
        }
    
        *value = ((rx_buffer[0] << 8) | rx_buffer[1]) >> 4;
        // printk("value final => %d\n", *value);
    }
    float32_t hammingWindow[FFT_SIZE];
    float32_t blackmanWindow[FFT_SIZE];
    float32_t hannWindow[FFT_SIZE];
    
    int main(void)
    {
        arm_status status;
    
        status = ARM_MATH_SUCCESS;
        if (!device_is_ready(gpio1_dev))
        {
            printk("GPIO device not found!\n");
            return 1;
        }
    
        printk("%s started\n\n", CONFIG_BOARD);
        spi_init();
    
        if (!device_is_ready(gpio1_dev))
        {
            printk("GPIO device not found!\n");
            return 1;
        }
        gpio_pin_configure(gpio1_dev, GPIO_1_CS, GPIO_OUTPUT_ACTIVE | GPIO_ACTIVE_LOW);
    
        if (!device_is_ready(spi_dev))
        {
            printk("SPI device not found!\n");
            return 1;
        }
    
        // arm_cfft_init_f32(&fft_instance,FFT_SIZE);
        status = arm_rfft_fast_init_f32(&fft_instance, FFT_SIZE);
    
    
        k_timer_init(&sampling_timer, sampling_handler, NULL);
        k_timer_start(&sampling_timer, K_USEC(INTERVAL), K_USEC(INTERVAL));
        
    
        while (1)
        {
    
            for (int i = 0; i < FFT_SIZE; i++)
            { 
                while(!adc_read_flag){}
    
                    adc_read_flag=false;
                    uint16_t adc_value;
                    read_adc_value(&adc_value);
                    fftInput[i] = (float32_t)adc_value;
            }
    
            uint32_t maxIndex;
            float32_t maxValue;
            float32_t dominantFrequency;
            // Copie du tableau d'entrée
            float32_t fftInputCopy[FFT_SIZE];
            memcpy(fftInputCopy, fftInput, sizeof(float32_t) * FFT_SIZE);
    
            // Exécution de la RFFT.
            arm_rfft_fast_f32(&fft_instance, fftInputCopy, fftOutput, 0);
            arm_cmplx_mag_f32(fftOutput, fftMagnitude, FFT_SIZE / 2);
            fftMagnitude[0] = 0;
            arm_max_f32(fftMagnitude, FFT_SIZE / 2, &maxValue, &maxIndex);
            dominantFrequency = maxIndex * (FS / FFT_SIZE);
        }
        return 0;
    }
    

  • Hmm,

    Another alternative but a bit different is to use Timer + PPI + SPI. You configure the SPI to start a transaction every time the timer generates a COMPARE event. You do this by connecting the SPI START task with the COMPARE event by using PPI. The PPI enables the peripherals to start and end without the involvement of the CPU, the only thing the CPU have to do between transaction is to process the data and set the new buffer before a new transaction.

    Relevant samples that can be used for reference for implementing this are:

    PPI: https://github.com/too1/ncs-nrfx-pulse-count-example

    Timer: https://github.com/too1/ncs-nrfx-pulse-count-example

    SPIM: https://github.com/zephyrproject-rtos/hal_nordic/tree/master/nrfx/samples/src/nrfx_spim/non_blocking

    regards

    Jared 

  • This is finnaly what i did and it works perfectky, thanks !

  • Great job!,

    I'll mark this case as resolved then,

    regards

    Jared 

Related