nRF54L15 SPIS21 slave receives correct RX amount but RX buffer is all zeros when capturing camera (BF3901/BF30A2) data

Hi,

I'm porting a working SPI camera demo from nRF52840 to nRF54L15 and I'm stuck with an issue where the SPIS RX amount is correct, but the received buffer is all zeros.

Hardware / setup:
- New platform: nRF54L15 DK, CPUAPP
- Old reference project: nRF52840 (custom board) BLE image transfer demo using BF30A2 (GC032A) camera module (Nordic nRF5 SDK)
- Camera module: BF3901 / BF30A2 (GC032A-compatible)
- SPI slave instance:
- nRF52840: SPIS1
- nRF54L15: SPIS21
- Pins on nRF54L15:
- SCK = P2.6
- MOSI = P2.0
- MISO = P2.8
- CSN = P2.10
- A GPIO P2.3 is shorted to P2.10 on the board, and I drive P2.3 in software as the camera CS. So effectively camera CSN and SPIS CSN are the same line (this is the same topology we use on the original nRF52840 board, where P0.6 and P0.27 are shorted).
- SCCB / I2C init sequence for the camera is identical to the working nRF52840 project.

Devicetree for SPIS21 (nRF54L15):

&pinctrl {
    spis21_default: spis21_default {
        group1 {
            psels = <NRF_PSEL(SPIS_SCK,  2, 6)>,
                    <NRF_PSEL(SPIS_MOSI, 2, 0)>,
                    <NRF_PSEL(SPIS_MISO, 2, 8)>,
                    <NRF_PSEL(SPIS_CSN,  2, 10)>;
        };
    };

    spis21_sleep: spis21_sleep {
        group1 {
            psels = <NRF_PSEL(SPIS_SCK,  2, 6)>,
                    <NRF_PSEL(SPIS_MOSI, 2, 0)>,
                    <NRF_PSEL(SPIS_MISO, 2, 8)>,
                    <NRF_PSEL(SPIS_CSN,  2, 10)>;
            low-power-enable;
        };
    };
};

&spi21 {
    compatible = "nordic,nrf-spis";
    status = "okay";
    pinctrl-0 = <&spis21_default>;
    pinctrl-1 = <&spis21_sleep>;
    pinctrl-names = "default", "sleep";
    def-char = <0x00>;
    /delete-property/rx-delay-supported;
    /delete-property/rx-delay;
};

Software (nRF Connect SDK v3.0.0 / Zephyr 4.0.99):

1) I first tried using nrfx_spis directly (nrfx_spis_init + nrfx_spis_buffers_set). The event handler reported RX amount = 42413 bytes (expected frame size), but my RX buffer (static uint8_t m_rx_buf[65536]) was all zeros.

2) To avoid any DMA / memory-region / cache issues on nRF54, I switched to using the Zephyr SPI slave driver spi_nrfx_spis:

- Device:
static const struct device *spis_dev = DEVICE_DT_GET(DT_NODELABEL(spi21));

- Config (I tried all four modes; original nRF52840 code uses MODE_2):
static const struct spi_config spis_cfg = {
.operation = SPI_OP_MODE_SLAVE | SPI_MODE_CPOL | SPI_MODE_CPHA | SPI_WORD_SET(8),
.frequency = 0,
.slave = 0,
.cs = NULL,
};

- Transfer code (simplified):

spis_xfer_done = false;
last_rx_amount = 0;

struct spi_buf tx_buf = { .buf = m_tx_buf, .len = totol_size };
struct spi_buf rx_buf = { .buf = m_rx_buf, .len = totol_size };
struct spi_buf_set tx_set = { .buffers = &tx_buf, .count = 1 };
struct spi_buf_set rx_set = { .buffers = &rx_buf, .count = 1 };

int ret = spi_transceive_cb(spis_dev, &spis_cfg, &tx_set, &rx_set,
spis_transfer_cb, NULL);

// in callback:
// if (result >= 0) last_rx_amount = (size_t)result;
// spis_xfer_done = true;

// software-driven CS window (P2.3 shorted to CSN P2.10):
gpio_pin_set_dt(&csn_pin, 0); // CS low
NRFX_DELAY_US(180 * 1000); // 180 ms window for camera to output frame
gpio_pin_set_dt(&csn_pin, 1); // CS high

// wait for callback
while (!spis_xfer_done && timeout--) {
k_busy_wait(10);
}

if (spis_xfer_done && last_rx_amount > 0) {
sys_cache_data_invd_range(m_rx_buf, last_rx_amount);
// print first bytes
}

Observations:

- spi_transceive_cb ret = 0
- Callback is called, xfer_done = 1
- last_rx_amount = 42413 (which matches totol_size)
- But m_rx_buf[0..last_rx_amount-1] are all 0x00, sum = 0.

Logic analyzer:

- Probes on the nRF54L15 DK pins directly:
- SCLK (P2.6) shows valid clock only while CS (P2.3/P2.10) is low.
- MOSI (P2.0) shows valid camera data in that CS-low window (not all zeros), matching what works on the nRF52840 design.
- CS line (P2.3 shorted to P2.10) is low for ~180ms then goes high, as expected.

Other info:

- I tried SPI modes 0/1/2/3 on nRF54L15; RX amount is always correct, but RX buffer content is always 0x00.
- On the original nRF52840 project, with essentially identical timing and camera register setup, the same BF30A2/BF3901 module streams valid image data over SPI slave.
- SCCB (I2C) ID read of the sensor returns the expected ID on both boards.
- NCS version: nRF Connect SDK v3.0.0 (Zephyr v4.0.99).
- prj.conf has CONFIG_SPI=y, CONFIG_SPI_SLAVE=y, CONFIG_SPI_ASYNC=y and CONFIG_NRFX_SPIS21=y.

Questions:

1. Is there anything special about SPIS21 on nRF54 (SERIAL subsystem, SPU, memory-regions, etc.) that could cause "RX amount correct but RX buffer all zeros" in SPI slave mode, even when using the Zephyr spi_nrfx_spis driver?
2. Are there known restrictions on which memory regions can be used for SPI slave RX buffers on nRF54L15 when using spi_nrfx_spis? (I assumed the driver + DMM would handle this.)
3. Is it valid to use a software-driven CS window (P2.3) shorted to SPIS CSN (P2.10) in slave mode, similar to how we did on nRF52840, or does SPIS21 require a different CS timing or configuration?
4. Could SPU or some default security configuration be forcing the SPIS21 MISO/MOSI inputs low or blocking the data path, while still allowing the transfer to complete and report a non-zero RX amount?

Any guidance on how to debug this further on nRF54L15 (e.g., registers to inspect for SPIS21, SPU settings, or recommended way to capture continuous SPI camera data in slave mode) would be greatly appreciated.

Thanks!

Parents Reply Children
  • Hello,

    it's okay. Thank you very much for your help! I have attempted to read several bytes of data using SPIS21, but the situation remains unchanged. However, the small static buffer has not been tested yet, and I will give it a try.

    This is my main.c file:

    #include <stdio.h>
    #include "sccb.h"
    #include "bf3901.h"
    
    volatile uint8_t width= 200;
    volatile uint8_t height= 100;
    volatile int timers_gain = 1;;
    volatile uint16_t totol_size;
    
    volatile bool capture_flag = false;
    
    uint16_t m_length_rx_done;
    static uint8_t m_rx_buf[65536] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, 0x44};    /**< RX buffer. */
    
    // static const struct gpio_dt_spec my_pin = GPIO_DT_SPEC_GET_OR(DT_NODELABEL(user_pin), gpios, {0});
    
    int main(void)
    {
    	printf("Hello World! %s\n", CONFIG_BOARD_TARGET);
    
    	sccb_init();
        bf3901_camera_on(); 
        bf3901_read_id();
    
    	while(1) {
    		uint32_t sum = 0;
    
    		m_length_rx_done = bf3901_camera_single_capture(m_rx_buf);
    		printf("Capture complete: size %u bytes\n", (uint32_t)m_length_rx_done);
    
    		for (uint16_t i = 0; i < m_length_rx_done; i++) {
    			sum += m_rx_buf[i];
    		}
    		printf("sum = %u\n", sum);
    		k_msleep(1000);
    	}
    
    	return 0;
    }
    

    my prj.conf file:

    CONFIG_GPIO=y
    CONFIG_SPI=y
    CONFIG_SPI_SLAVE=y
    CONFIG_SPI_ASYNC=y
    CONFIG_LOG=y
    CONFIG_NRFX_GRTC=y
    CONFIG_NRFX_SPIS21=y
    
    CONFIG_TRUSTED_EXECUTION_NONSECURE=n
    # STEP 1 - Enable debugging
    CONFIG_DEBUG_THREAD_INFO=y
    CONFIG_DEBUG_OPTIMIZATIONS=y

    Since my main function will call other functions, I am also including my bf3901.c and sccb.c here.

    #include "bf3901.h"
    
    LOG_MODULE_DECLARE(Bt_Transmission);
    
    static const struct device *spis_dev = DEVICE_DT_GET(DT_NODELABEL(spi21));
    
    static const struct spi_config spis_cfg = {
        .operation = SPI_OP_MODE_SLAVE | SPI_MODE_CPOL | SPI_TRANSFER_LSB | SPI_WORD_SET(8),
        .frequency = 0,
        .slave = 0,
        .cs = NULL,
    };
    
    extern uint8_t width;
    extern uint8_t height;
    extern uint16_t totol_size;
    extern int timers_gain;
    
    #define SPI_INSTANCE_ID    21
    
    static const struct gpio_dt_spec csn_pin = GPIO_DT_SPEC_GET_BY_IDX(DT_NODELABEL(user_pin), gpios, 0);
    // static const struct gpio_dt_spec pdn_pin = GPIO_DT_SPEC_GET_BY_IDX(DT_NODELABEL(my_i2c_pins), gpios, 1);
    static const nrfx_spis_t spis = NRFX_SPIS_INSTANCE(SPI_INSTANCE_ID);
    
    static uint8_t m_tx_buf[42414];
    
    
    static size_t last_rx_amount;
    
    static void spis_transfer_cb(const struct device *dev, int result, void *data)
    {
        (void)dev;
        (void)data;
        if (result >= 0) {
            last_rx_amount = (size_t)result;
        } else {
            last_rx_amount = 0;
        }
        spis_xfer_done = true;
    }
    
    
    
    int bf3901_read_id(void)
    {
        uint8_t h_id = read_reg(0xfc);
        uint8_t l_id = read_reg(0xfd);
        printf("%02X %02X\n", h_id, l_id);
        // if(((uint16_t)h_id << 8) + l_id != 0x3901) {
        //     printf("BF3901 read id faild!");
        //     return -1;
        // }
        return 0;
    }
    
    int bf3901_init_reg(void)
    {
        int len = sizeof(gc032a_init_reg_tb) / sizeof(gc032a_init_reg_tb[0]);
        for(int i = 0; i < len; i++) {
            write_reg(gc032a_init_reg_tb[i][0], gc032a_init_reg_tb[i][1]);
        }
        write_reg(0x17, 0x00);
        write_reg(0x18, width);
        write_reg(0x19, 0x00);
        write_reg(0x1a, height);
        return 0;
    }
    
    void bf3901_camera_on(void)
    {
        gpio_pin_configure_dt(&csn_pin, GPIO_OUTPUT | GPIO_PULL_UP);
    	gpio_pin_set_dt(&csn_pin, 1);
        // nrf_gpio_cfg_output(PDN);   
        // nrf_gpio_pin_clear(PDN);
        // camera_clk_out();
    
        nrf_gpio_pin_control_select(GRTC_PIN, NRF_GPIO_PIN_SEL_GRTC);
    	nrfy_grtc_clkout_divider_set(NRF_GRTC, 1);
    	nrfy_grtc_clkout_set(NRF_GRTC, NRF_GRTC_CLKOUT_FAST, true);
    
        // nrf_gpio_cfg(AVDD,NRF_GPIO_PIN_DIR_OUTPUT,NRF_GPIO_PIN_INPUT_DISCONNECT,NRF_GPIO_PIN_PULLUP,NRF_GPIO_PIN_S0S1,NRF_GPIO_PIN_NOSENSE);
        // nrf_gpio_pin_set(AVDD);
        bf3901_init_reg();
    }
    
    
    
    uint16_t bf3901_camera_single_capture(uint8_t *m_rx_buf)
    {
        memset(m_tx_buf, 0xFF, sizeof(m_tx_buf));
    
        totol_size = (12 + width * 1) * height * 2 + 13;
        spis_xfer_done = false;
        last_rx_amount = 0;
    
        if (!device_is_ready(spis_dev)) {
            printf("spis_dev not ready\n");
            return 0;
        }
    
        struct spi_buf tx_buf = {
            .buf = m_tx_buf,
            .len = totol_size,
        };
        struct spi_buf rx_buf = {
            .buf = m_rx_buf,
            .len = totol_size,
        };
        struct spi_buf_set tx_set = {
            .buffers = &tx_buf,
            .count = 1,
        };
        struct spi_buf_set rx_set = {
            .buffers = &rx_buf,
            .count = 1,
        };
    
        int ret = spi_transceive_cb(spis_dev, &spis_cfg, &tx_set, &rx_set, spis_transfer_cb, NULL);
        printf("spi_transceive_cb ret = %d\n", ret);
        if (ret < 0) {
            return 0;
        }
    
        gpio_pin_set_dt(&csn_pin, 0);
        // debug_capture_spi_edges();
        NRFX_DELAY_US(180 * 1000);
    
        gpio_pin_set_dt(&csn_pin, 1);
    
        printf("wait XFER Done\n");
    
        uint32_t timeout = 1000000;
        while (!spis_xfer_done && timeout--) {
            k_busy_wait(10);
        }
    
        printf("xfer_done: %d, timeout: %s, len: %u\n",
               spis_xfer_done,
               (timeout == 0) ? "yes" : "no",
               (unsigned int)last_rx_amount);
    
        if (spis_xfer_done && last_rx_amount > 0) {
            sys_cache_data_invd_range(m_rx_buf, last_rx_amount);
    
            printf("first 10 bytes:\n");
            for (int i = 0; i < 64 && i < last_rx_amount; i++) {
                printf("0x%02X ", m_rx_buf[i]);
            }
            printf("\n");
        }
    
        return (uint16_t)last_rx_amount;
    }
    

    sccb.c:

    #include "sccb.h"
    
    static const struct gpio_dt_spec scl_pin = GPIO_DT_SPEC_GET_BY_IDX(DT_NODELABEL(my_i2c_pins), gpios, 0);
    static const struct gpio_dt_spec sda_pin = GPIO_DT_SPEC_GET_BY_IDX(DT_NODELABEL(my_i2c_pins), gpios, 1);
    
    static void sccb_set_sda_out(void)
    {
        if (sda_pin.port) {
            gpio_pin_configure_dt(&sda_pin, GPIO_OUTPUT | GPIO_PULL_UP);
        }
    }
    
    static void sccb_set_scl_out(void)
    {
        if (scl_pin.port) {
            gpio_pin_configure_dt(&scl_pin, GPIO_OUTPUT | GPIO_PULL_UP);
        }
    }
    
    void sccb_init(void)
    {
        sccb_set_sda_out();
        sccb_set_scl_out();
    }
    
    static inline void sccb_delay(void)
    {
        NRFX_DELAY_US(2);
    }
    
    
    static void sccb_start(void)
    {
        gpio_pin_set_dt(&sda_pin, 1);
        gpio_pin_set_dt(&scl_pin, 1);
        sccb_delay();
        gpio_pin_set_dt(&sda_pin, 0);
        sccb_delay();
        gpio_pin_set_dt(&scl_pin, 0);
    }
    
    static void sccb_stop(void)
    {
        gpio_pin_set_dt(&sda_pin, 0);
        sccb_delay();
        gpio_pin_set_dt(&scl_pin, 1);
        sccb_delay();
        gpio_pin_set_dt(&sda_pin, 1);
        sccb_delay();
    }
    
    static void sccb_write_byte(uint8_t dat)
    {
        int8_t dat_index;
        uint8_t dat_bit;
    
        for (dat_index=7; dat_index>=0; dat_index--)
        {
            dat_bit = (dat >> dat_index) & 0x01;
            if (dat_bit == 1)
                gpio_pin_set_dt(&sda_pin, 1);
            else
                gpio_pin_set_dt(&sda_pin, 0);
            sccb_delay();
            gpio_pin_set_dt(&scl_pin, 1);
            sccb_delay();
            gpio_pin_set_dt(&scl_pin, 0);
        }
    
        gpio_pin_set_dt(&sda_pin, 1);
        sccb_delay();
        gpio_pin_set_dt(&scl_pin, 1);
        sccb_delay();
        gpio_pin_set_dt(&scl_pin, 0);
    }
    
    static uint8_t sccb_read_byte(void)
    {
        int8_t dat_index;
        uint8_t dat_bit;
        uint8_t dat=0;
        if (sda_pin.port) {
            gpio_pin_configure_dt(&sda_pin, GPIO_INPUT | GPIO_PULL_UP);
        }
        for (dat_index=7; dat_index>=0; dat_index--)
        {
            sccb_delay();
            gpio_pin_set_dt(&scl_pin, 1);
            dat_bit = gpio_pin_get_dt(&sda_pin);
            dat |= (dat_bit << dat_index);
            sccb_delay();
            gpio_pin_set_dt(&scl_pin, 0);
        }
    
        sccb_delay();
        gpio_pin_set_dt(&scl_pin, 1);
        sccb_delay();
        gpio_pin_set_dt(&scl_pin, 0);
        sccb_delay();
        gpio_pin_set_dt(&sda_pin, 0);
        sccb_delay();
        sccb_set_sda_out();
        return dat;
    }
    
    static void sccb_3_phase_write(uint8_t id_addr, uint8_t sub_addr, uint8_t dat)
    {
        sccb_start();
        sccb_write_byte((id_addr << 1) | SCCB_WRITE);
        sccb_write_byte(sub_addr);
        sccb_write_byte(dat);
        sccb_stop();
    }
    
    
    static void sccb_2_phase_write(uint8_t id_addr, uint8_t sub_addr)
    {
        sccb_start();
        sccb_write_byte((id_addr << 1) | SCCB_WRITE);
        sccb_write_byte(sub_addr);
        sccb_stop();
    }
    
    
    static uint8_t sccb_2_phase_read(uint8_t id_addr)
    {
        uint8_t dat=0;
        sccb_start();
        sccb_write_byte((id_addr << 1) | SCCB_READ);
        dat=sccb_read_byte();
        sccb_stop();
        return dat;
    }
    
    void write_reg(uint8_t reg, uint8_t dat)
    {
        sccb_3_phase_write(SCCB_ADDR, reg, dat);
    }
    
    uint8_t read_reg(uint8_t reg)
    {
        uint8_t dat = 0;
        sccb_2_phase_write(SCCB_ADDR, reg);
        dat = sccb_2_phase_read(SCCB_ADDR);
    
        return dat;
    }
    
    
    

    Thanks.

Related