Unnecessary nrfx_spim_init calls with identical SPI configs but different CS lines

Hello everyone,

I am writing an application with three sensors on the same SPI1 interface on my nRF52840. See the following devicetree section:

&spi1 {
	compatible = "nordic,nrf-spim";
	status = "okay";
	pinctrl-0 = <&spi1_default>;
	pinctrl-1 = <&spi1_sleep>;
	pinctrl-names = "default", "sleep";
	cs-gpios = <&gpio0 22 GPIO_ACTIVE_LOW>,
			   <&gpio0 14 GPIO_ACTIVE_LOW>,
			   <&gpio0 23 GPIO_ACTIVE_LOW>;

	iis2dh: iis2dh@0 {
		compatible = "st,iis2dh-ext";
		reg = <0>;
		spi-max-frequency = <8000000>;
		drdy-gpios = <&gpio1 13 GPIO_ACTIVE_HIGH>;
		zephyr,deferred-init;
	};

	kx134: kx134@1 {
		compatible = "rohm,kx134";
		reg = <1>;
		spi-max-frequency = <8000000>;
		drdy-gpios = <&gpio0 30 GPIO_ACTIVE_HIGH>;
		drdy-int = <2>;
		zephyr,deferred-init;
	};

	h3lis200dl: h3lis200dl@2 {
		compatible = "st,h3lis200dl";
		reg = <2>;
		spi-max-frequency = <8000000>;
		drdy-gpios = <&gpio1 11 GPIO_ACTIVE_HIGH>;
		drdy-int = <1>;
		zephyr,deferred-init;
	};
};

For all sensors, I use a custom driver to fetch accelerometer data at high sample rates. As shown above, the spi-max-frequency is set to the same value (8 MHz) for all sensors. The zephyr,deferred-init; property is used to fully turn off the sensor (its supply voltage) to save power when required. Furthermore, all sensors use the same SPI operation, for example:

#define H3LIS200DL_SPI_OPERATION   (SPI_OP_MODE_MASTER | SPI_MODE_CPOL | SPI_MODE_CPHA | SPI_WORD_SET(8))

While debugging, I noticed that each sensor readout took a lot of processing time - but only when a different sensor had just been read before. For two consecutive reads from the same sensor, the SPI readout was much faster.

After looking deeper into the NCS, I found the issue:

zephyr/drivers/spi/spi_context.h:

static inline bool spi_context_configured(struct spi_context *ctx,
					  const struct spi_config *config)
{
	return !!(ctx->config == config);
}

zephyr/drivers/spi/spi_nrfx_spim.c:

static int configure(const struct device *dev,
		     const struct spi_config *spi_cfg)
{
    ...
	if (dev_data->initialized && spi_context_configured(ctx, spi_cfg)) {
		/* Already configured. No need to do it again. */
		return 0;
	}
	...
	nrfx_spim_init(&dev_config->spim, &config,
				event_handler, (void *)dev);
	...
}

The configure function is called for every new SPI transaction, and spi_context_configured compares the SPI configs. However, the comparison is based on pointer values, which are different for each sensor. This always evaluates to false and causes a time-consuming nrfx_spim_init(...) call.

This re-initialization takes about a millisecond, which is significant in my case, given the high sample rates.

However, the re-initialization is not actually required because the sensors all use the exact same hardware configuration - the only difference is the CS lines.

To solve that issue I came up with the following workaround:

static int configure(const struct device *dev,
		     const struct spi_config *spi_cfg)
{
    ...
	/* Skip nrfx_spim_init if same spi params and only cs is different for spi1 */
	if (dev == DEVICE_DT_GET(DT_NODELABEL(spi1))) {
		if (dev_data->initialized &&
			ctx->config->frequency == spi_cfg->frequency &&
			ctx->config->operation == spi_cfg->operation) {
			ctx->config = spi_cfg;
			return 0;
		}
	} else {
		if (dev_data->initialized && spi_context_configured(ctx, spi_cfg)) {
			/* Already configured. No need to do it again. */
			return 0;
		}
	}
	...
	nrfx_spim_init(&dev_config->spim, &config,
				event_handler, (void *)dev);
	...
}

With this, I was able to reduce processor load from almost 100% down to about 30%.

Now I am wondering: is there a better solution than this? In particular, modifying the NCS directly doesn’t feel like the best approach.

Best regards,
Bernhard

Related