SPI support for bare metal (version 2.0.99?)

Hello,

I would like to use spi to connect to a ADC on a nrf54l15dk at high speed using DMA.

I want to use nrfconnect bare metal, but cannot find a starting example.

In  https://github.com/nrfconnect/sdk-nrf-bm there are a few spi-examples.

How can I use them? It seems that this is part of a version v2.0.99.

So, how can I use these examples starting from bm version v2.0.1? That is the only option I have in vscode.

    Thanks in advance, Sietse

Parents
  • Hi,

    It's great that you found the examples. Please feel free to drop the link so others can also learn from them.

    To answer your second question, I would suggest a hardware-based approach combining Hardware Timers + Programmable Peripheral Interconnect (PPI) to drive individual samples, along with EasyDMA double buffering (and optional CPU block handling) to maintain uninterrupted streaming.

    TIMER + PPI for jitter-free triggering

    Instead of starting each SPI transfer in software, use a Hardware TIMER mapped to the SPIM TASKS_START via PPI. The TIMER runs on the high-frequency clock (HFCLK) and its frequency is derived as fTIMER = 16 MHz / (2^PRESCALER); it can be configured to auto-clear on a compare match using the SHORTS register

    1. TIMER: Set to clear automatically on compare at a period of ~3.33 µs (300 kHz).
    2. PPI Channel: Connects TIMER→EVENTS_COMPARE[0] → SPIM→TASKS_START. The PPI enables peripherals to interact autonomously using tasks and events independent of the CPU, synchronized to the 16 MHz clock, and supports fixed and configurable channels (20 configurable + 12 fixed channels) 
    3. CS Handling: If the SPI driver doesn't handle Chip Select automatically in hardware mode, you can use a second PPI channel (or PPI FORK, since each TEP implements a fork mechanism allowing a second task to be triggered simultaneously) to toggle a GPIOTE pin for CS
    void timer_init(void) {
        NRF_TIMER1->MODE = TIMER_MODE_MODE_Timer;
        NRF_TIMER1->BITMODE = TIMER_BITMODE_BITMODE_16Bit;
        NRF_TIMER1->PRESCALER = 0; // 16 MHz base clock
    
        NRF_TIMER1->CC[0] = 53; // ~3.31 us period
        NRF_TIMER1->SHORTS = TIMER_SHORTS_COMPARE0_CLEAR_Msk;
    }
    
    void ppi_init(void) {
        // Route TIMER1 Compare[0] event directly to SPIM TASKS_START
        NRF_PPI->CH[0].EEP = (uint32_t)&NRF_TIMER1->EVENTS_COMPARE[0];
        NRF_PPI->CH[0].TEP = (uint32_t)&NRF_SPIM0->TASKS_START;
    
        NRF_PPI->CHENSET = PPI_CHENSET_CH0_Msk;
    }


    This guarantees zero-jitter spacing between sample acquisitions down to the cycle level, independent of CPU activity.

    EasyDMA double buffering

    The SPIM supports EasyDMA for reading/writing data packets to/from RAM without CPU involvement, using RXD.PTR/TXD.PTR and RXD.MAXCNT/TXD.MAXCNT registers. These pointer and count registers are double-buffered, meaning they can be updated and prepared for the next transmission immediately after the STARTED event is received.

    1. Buffer Setup: Allocate two RAM buffers, Buffer A and Buffer B (e.g., 2000 bytes each).
    2. Double Queuing: Load Buffer A address into RXD.PTR and start the transfer, then immediately queue Buffer B into RXD.PTR while Buffer A is filling. When Buffer A finishes, the hardware automatically switches to Buffer B.
    #define BLOCK_SIZE 2000
    
    static uint8_t buffer_a[BLOCK_SIZE];
    static uint8_t buffer_b[BLOCK_SIZE];
    
    static volatile uint8_t active_buf = 0; // 0 = writing to A, 1 = writing to B
    static volatile bool block_ready = false;
    
    void spim_init(void) {
        // 1. Set pins and SPI clock speed (e.g., 8 MHz for fast 2-byte transfers)
        NRF_SPIM0->FREQUENCY = SPIM_FREQUENCY_FREQUENCY_M8;
    
        // 2. Load Buffer A into the active DMA register
        NRF_SPIM0->RXD.PTR = (uint32_t)buffer_a;
        NRF_SPIM0->RXD.MAXCNT = BLOCK_SIZE;
    
        // 3. Queue Buffer B into the DMA pipeline register for auto-switch
        NRF_SPIM0->RXD.PTR = (uint32_t)buffer_b;
    
        // 4. Enable END interrupt (fires only when a full BLOCK_SIZE is filled)
        NRF_SPIM0->INTENSET = SPIM_INTENSET_END_Msk;
        NVIC_EnableIRQ(SPIM0_SPIS0_TWIM0_TWIS0_SPI0_TWI0_IRQn);
    
        NRF_SPIM0->ENABLE = SPIM_ENABLE_ENABLE_Enabled;
    }

    Servicing the ISR: An interrupt fires on EVENTS_END (or EVENTS_STARTED when switching buffers). The CPU then has the duration of a full 2000-byte block (~6.6 ms) to update RXD.PTR for the next buffer.

    void SPIM0_SPIS0_TWIM0_TWIS0_SPI0_TWI0_IRQHandler(void) {
        if (NRF_SPIM0->EVENTS_END) {
            NRF_SPIM0->EVENTS_END = 0; // Clear event
    
            if (active_buf == 0) {
                // Buffer A finished; hardware is currently filling Buffer B.
                // Re-queue Buffer A as the next target.
                NRF_SPIM0->RXD.PTR = (uint32_t)buffer_a;
                active_buf = 1;
            } else {
                // Buffer B finished; hardware is currently filling Buffer A.
                // Re-queue Buffer B as the next target.
                NRF_SPIM0->RXD.PTR = (uint32_t)buffer_b;
                active_buf = 0;
            }
    
            block_ready = true; // Signal main loop
        }
    }

    Start the timer once during startup and handle completed data blocks asynchronously in main().

    Please let me know if this helps you implement your code.

    Regards

    Pallavi
  • Thanks again, this helps!

    I'll report when I have a working example.

Reply Children
No Data
Related