NRF5340DK High frequency SPIM communication failure / spi_transceive() delay problem

Hello,

I am trying to exchange data with an external slave sensor via SPIM communication using the nRF5340DK.

I am currently facing two main issues:

1.The spi_transceive() function takes too long to execute.

This is significantly reducing the effective data transfer rate.

I have also reviewed a Q&A from someone who pointed out a similar delay with spi_transceive().

Time gap between each spi_transceive )

However, the solution suggested in that Q&A, which involved modifying spi configuration in the main.c, doesn't seem to be applicable to the nRF5340 as it resulted in an error

To clearly explain my situation, I have attached my main.c, overlay, and configuration files. 

main.c

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/sys/printk.h>
#include <zephyr/drivers/gpio.h>
#include <stdio.h>

/* Get SPI device from devicetree */
#define SPI_DEVICE_NODE DT_ALIAS(spi4_basic)
static const struct device *spi_dev = DEVICE_DT_GET(SPI_DEVICE_NODE);

static const struct device *uart_for_data_tx = DEVICE_DT_GET(DT_CHOSEN(zephyr_console));

#define SW0_NODE     DT_ALIAS(sw0)
static const struct gpio_dt_spec cs = GPIO_DT_SPEC_GET(SW0_NODE, gpios);


static struct spi_config spi_cfg = {
    .frequency = 1000000,           // SPI frequency.
    .operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8) | SPI_TRANSFER_MSB, // SPI Mode 0
    .slave = 0,                     // Master mode
    .cs = NULL,
    //.cs = &spi_cs,                // Manage CS pin with cs-gpios in Devicetree
};


/* RHD2000 command definitions (16-bit) */
#define RHD_CMD_CONVERT(channel) (uint16_t)((((uint16_t)(channel) & 0x3F) << 8)) 
// Input: 00RR RRRR 0000 0000 
// Output: DDDD DDDD DDDD DDDD
#define RHD_CMD_WRITE(reg, data) (uint16_t)(0x8000 | (((uint16_t)(reg) & 0x3F) << 8) | ((uint16_t)(data) & 0xFF)) 
// Input: 10RR RRRR DDDD DDDD
// Output: 1111 1111 DDDD DDDD , D: same with input's D (for confirm)
#define RHD_CMD_READ(reg)        (uint16_t)(0xC000 | (((uint16_t)(reg) & 0x3F) << 8))
// Input: 11RR RRRR 0000 0000
#define RHD_CMD_CALIBRATE        (uint16_t)(0x5500) // 0101 0101 0000 0000 
#define RHD_CMD_DUMMY_READ       RHD_CMD_READ(63) //


static int rhd_spi_transfer(uint16_t tx_command, uint8_t *rx_buffer) {
    uint8_t tx_buffer[2];

    // Convert 16-bit command to a Big-Endian byte array
    tx_buffer[0] = (uint8_t)(tx_command >> 8);  // MSB
    tx_buffer[1] = (uint8_t)(tx_command & 0xFF); // LSB

    const struct spi_buf tx_spi_buf = {
        .buf = tx_buffer,
        .len = sizeof(tx_buffer)
    };
    const struct spi_buf_set tx_spi_bufs = {
        .buffers = &tx_spi_buf,
        .count = 1
    };

    // Directly store received data in the rx_buffer passed from main
    struct spi_buf rx_spi_buf = {
        .buf = rx_buffer,
        .len = 2
    };
    const struct spi_buf_set rx_spi_bufs = {
        .buffers = &rx_spi_buf,
        .count = 1
    };

    while (1) {

        gpio_pin_set_dt(&cs, 1); // CS -> 0

        int err = spi_transceive(spi_dev, &spi_cfg,
            &tx_spi_bufs, &rx_spi_bufs); 

        gpio_pin_set_dt(&cs, 0);

        return 0;
    }
}

int main(void)
{
    int ret;
    int err;
    ret = gpio_pin_configure_dt(&cs, GPIO_OUTPUT_INACTIVE);
    gpio_pin_set_dt(&cs, 0); // I still don't understand why the output is 1 when 0 is passed to the set function.

    if (!device_is_ready(spi_dev)) {
        printk("SPI device %s is not ready!\n", spi_dev->name);
        return 0;
    }

    if (!device_is_ready(uart_for_data_tx)) {
        printk("UART device for data TX (%s) is not ready!\n", uart_for_data_tx->name);
        return 0;
    }

    printk("nRF5340 SPIM example started.\n");
    printk("Reading MISO pin (connected to DC voltage) and sending to PC via UART every second.\n");


    uint8_t rx_main_buffer[2];

    err = rhd_spi_transfer(RHD_CMD_WRITE(0, 0xDE), rx_main_buffer); // ADC configuration, disable fast settle
    if (err) return err;


    while (1) {
        /* SPI communication (transceive) - send dummy bytes via MOSI to read MISO value */
        rhd_spi_transfer(RHD_CMD_READ(0) , rx_main_buffer);
    }
    return 0;
};

app.overlay

/ {

    chosen {
        zephyr,console = &uart0;    
        zephyr,shell-uart = &uart0; 
    };

    aliases {
        spi4-basic = &spi4;
    };

};




&uart0 {
    status = "okay";             
    current-speed = <1000000>;    
};


&pinctrl {
    spi4_custom_pins: spi4_custom_pins { 
        group1 {
            psels = <NRF_PSEL(SPIM_SCK,  1, 15)>,   
                    <NRF_PSEL(SPIM_MOSI, 1, 14)>,  
                    <NRF_PSEL(SPIM_MISO, 1, 13)>;  
        };
    };


    spi4_custom_pins_sleep: spi4_custom_pins_sleep {
         group1 {
            psels = <NRF_PSEL(SPIM_SCK,  1, 15)>,
                    <NRF_PSEL(SPIM_MOSI, 1, 14)>,
                    <NRF_PSEL(SPIM_MISO, 1, 13)>;
            low-power-enable; 
        };
    };
};


&spi4 {
    compatible = "nordic,nrf-spim";
    status = "okay"; 
    pinctrl-0 = <&spi4_custom_pins>; 
    pinctrl-1 = <&spi4_custom_pins_sleep>; 
    pinctrl-names = "default", "sleep";   
    cs-gpios = <&gpio1 12 GPIO_ACTIVE_LOW>;
    //max-frequency = <560000>;
    //easydma-maxcnt-bits = <32>;
};

prj.conf

# enable console
CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y

CONFIG_SERIAL=y
CONFIG_PRINTK=y


CONFIG_SPI=y

CONFIG_GPIO=y 

In main.c, when I configure the SPI structure, I set the frequency to 1MHz.

I have confirmed with an oscilloscope that the actual clock frequency is indeed 1MHz.

Also, I'm controlling the CS pin directly via GPIO

2. Although the nRF5340 specifications state that it supports SPI frequencies up to 8MHz (and even up to 32MHz for spi4),

the quality of the Clock (CLK) signal degrades significantly at frequencies higher than 4MHz.

Thank you.

Best regards, 

gwan0624

  • Hi   ,

    I am currently facing this problem and this thread is great.  Try to get data at high speed from a biopotential monitor (also Intan but the RHS version with 32bit commands) and the Zephyr's SPI calls are woefully slow.  I am running SPIM4 @ 16Mhz on the high speed pins and using a combination of spi_tranceive_dt and spi_write_dt (which indecently take the same amount of time).  Look at the trace below - the reds are the clocking of 32bits (4 bytes in rx/tx) and the blues are the time inside spi_write_dt!  What is the point of having 16/32Mhz on SPIM4 if the driver calls are an order of magnitude longer than the SPI clocks themselves? Or is the Zephyr driver simply not up to the task?

    My question is have you basically abandoned the Zephyr driver and implemented using nrfx direct? Your snippet above seems to suggest that? Earlier in the thread it sounded like a hybrid model was being proposed is all.

    Thanks.

  • Dear geoffF,

    1. When using the Zephyr driver, I also experienced significant delays before and after the actual clocking (as mentioned earlier in the thread). In my personal opinion, this latency seems to stem from Zephyr's abstraction layer, which prioritizes compatibility across various manufacturers' boards rather than being optimized solely for Nordic.

    I believe the fact that SPIM4 supports up to 32MHz while the driver lags is because the Nordic hardware and Zephyr OS are fundamentally separate entities with different goals. Nordic focuses on enabling the chip's maximum performance, whereas Zephyr likely prioritizes portability and compatibility over raw peak performance.

    Therefore, SPI implementation on a Nordic device running Zephyr generally goes into two categories: (a) Polling or Interrupt-based: For irregular, asynchronous communication where performance requirements are lower. This allows for easy development using Zephyr APIs. (b) Optimization via DPPI/PPI: For high-performance requirements. This minimizes Zephyr OS intervention.


    2. Yes, I have abandoned the Zephyr SPI driver. I implemented the solution using nrfx directly. Even when using nrfx_spim_xfer based on nrfx, I explored two approaches:

    - Blocking mode: As seen in standard sample codes.

    - DPPI-based Timer Trigger mode: Using hardware interconnection for precise timing.

    If you require high-speed communication, I highly recommend using the DPPI (or PPI) based approach.



    Best regards,

    gwan0624

  • Thanks so much for the response. Never used (D)PPI on Nordic directly so some learning to do! Thanks again.

  • Sorry I did have one final question  how did you get NRFX_SPIM_EXTENDED_ENABLED set in your projects using nrfx with spim4?  The use of use_hw_ss and ss_duration depend in it being defined but it is not a standard kconfig option? If you leave the device tree entry in place it seems to define it, but I was under the impression you don't do that if using nrfx over Zephyr?

  • This is the ambiguous point.

    It seems that nrfx_spim_extended_enabled has implicit dependencies on configurations such as CONFIG_SPI_NRFX or CONFIG_NRFX_SPIM4.

    For SPI communication using DPPI, I only these configurations are required:

    CONFIG_SPI=y
    CONFIG_SPI_NRFX=y
    CONFIG_NRFX_SPIM ?=y

    CONFIG_NRFX_TIMER ?=y
    CONFIG_NRFX_GPPI=y

    Furthermore, since I explicitly configure the device features in the source code via the nrfx driver, I do not rely on Device Tree settings. The application works after removing the SPI node from the overlay file.

Related