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

  • SPI pins are configured too slow, you need high drive H0H1 drive mode:

    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)>;  

                nordic,drive-mode = <NRF_DRIVE_H0H1>;
            };
        };

    Those enable the stronger output drivers and should work up to 16 MHz.

  • Thank you so much for your help. You've resolved my second question.

    Thanks to the code you added, the CLK signal is now being output at 16MHz. Although the quality has slightly decreased, it is a significant improvement compared to before the code was added.

    (16MHz signal is coming in successfully) 

    However, even with the improved hardware output, there is still a significant delay in the spi_transceive() function's operation.

    I was wondering if I could ask for your help with this issue as well.


    I think I found the source code of spi_transceive() function at C:/ncs/v3.0.0/zephyr/drivers/spi/spi_nrfx_spi.c

    static int transceive(const struct device *dev,
    		      const struct spi_config *spi_cfg,
    		      const struct spi_buf_set *tx_bufs,
    		      const struct spi_buf_set *rx_bufs,
    		      bool asynchronous,
    		      spi_callback_t cb,
    		      void *userdata)
    {
    	struct spi_nrfx_data *dev_data = dev->data;
    	const struct spi_nrfx_config *dev_config = dev->config;
    	int error;
    
    	spi_context_lock(&dev_data->ctx, asynchronous, cb, userdata, spi_cfg);
    
    	error = configure(dev, spi_cfg);
    	if (error == 0) {
    		dev_data->busy = true;
    
    		if (dev_config->wake_pin != WAKE_PIN_NOT_USED) {
    			error = spi_nrfx_wake_request(&dev_config->wake_gpiote,
    						      dev_config->wake_pin);
    			if (error == -ETIMEDOUT) {
    				LOG_WRN("Waiting for WAKE acknowledgment timed out");
    				/* If timeout occurs, try to perform the transfer
    				 * anyway, just in case the slave device was unable
    				 * to signal that it was already awaken and prepared
    				 * for the transfer.
    				 */
    			}
    		}
    
    		spi_context_buffers_setup(&dev_data->ctx, tx_bufs, rx_bufs, 1);
    		spi_context_cs_control(&dev_data->ctx, true);
    
    		transfer_next_chunk(dev);
    
    		error = spi_context_wait_for_completion(&dev_data->ctx);
    		if (error == -ETIMEDOUT) {
    			/* Set the chunk length to 0 so that event_handler()
    			 * knows that the transaction timed out and is to be
    			 * aborted.
    			 */
    			dev_data->chunk_len = 0;
    			/* Abort the current transfer by deinitializing
    			 * the nrfx driver.
    			 */
    			nrfx_spi_uninit(&dev_config->spi);
    			dev_data->initialized = false;
    
    			/* Make sure the transaction is finished (it may be
    			 * already finished if it actually did complete before
    			 * the nrfx driver was deinitialized).
    			 */
    			finish_transaction(dev, -ETIMEDOUT);
    
    			/* Clean up the driver state. */
    			k_sem_reset(&dev_data->ctx.sync);
    		}
    
    		spi_context_cs_control(&dev_data->ctx, false);
    	}
    
    	spi_context_release(&dev_data->ctx, error);
    
    	return error;
    }

    Is there any problem in this function?

    Best regards,

    gwan0624

  • Hi gwan0624, 

    Just letting you know we are looking into this and will get back to you. 

    Best regards, 
    Håkon

  • Its not completely obvious from the driver implementation, but SPIM driver uses an interrupt to signal transfer complete. That means you get a delay for the required interrupt handling every time you do a request. 

    Should be obvious when enabling the 128MHz main core clock as the delay should be reduced quite a bit.

    Workaround is sensor dependent, I'd try to avoid very small individual spi_transceive transactions if at all possible.

    I did my own SPIM driver for a project, but that was not at all trivial to implement.

  • Have you tried letting Zephyr automatically toggle the CS pin automatically, using spi_tranceive_dt? Does that make any difference? 

    I am trying to exchange data with an external slave sensor via SPIM communication using the nRF5340DK.
    gwan0624 said:
    Although the quality has slightly decreased, it is a significant improvement compared to before the code was added.

    How is the sensor connected? Is it possible to shorten the wires or solder the sensor directly onto the DK? Would you be able to share a photo of your setup? 

Related