Hello Mikael
I have a complete solution for this in the uTasker project, which allows port triggering to write/read SPI sequences without any interrupt overhead. Below are some details and I have attached binary that you can load to a FRDM-K64F to check the behavior.
1. If you load the binary to the board you will find a 48kHz PWM output on pin J2-4 (PTC4). By connecting this to J2-20 (PTE-24) it will be used as the port edge DMA trigger (or else you can apply faster clocks to the pin too if you prefer).
SPI0 is used (this has a 4 deep FIFO on Tx and Rx - SPI1 and SPI2 have only 1 deep FIFOs but this is in fact not relevant since the DMA operation is the same for all). This means that SPI0_CS0 can be measured on J2-6 (PTD0), SPI0_SCLK on J2-12 (PTD1), SPI0_SOUT on J2-8 (PTD2). By connecting J2-8 to J2-10 the output can be looped back to the input.
This is what the signals look like (SPI sends 0x01, 0x02, 0x03, etc.)

As seen, the SPI sequence is started by the falling edge of the input.
The received data is stored to a buffer which can be of any size. There is an interrupt when it is half full and when it is completely full and this can be used to retrieve the data from this half while the next is still filling (ping-pong buffer). It is in fact optional since it is also possible to use no interrupts and still successfully receive the data.
2. This is the code used, which makes use of the uTasker DMA driver API which make it very simple to configure and modify to achieve flexible and power ful solutions like this with little effort (it also allows simulation of the operation in the uTasker simulation so that the complete operation can be checked and verified before running it on the HW):
#define DMA_CHANNEL_FOR_CS_END 6 // use DMA channel 6 for CS end trigger
#define DMA_CHANNEL_FOR_PORT_EDGE 7 // use DMA channel 7 for SPI sequence start trigger
#define DMA_CHANNEL_FOR_SPI_TX 8 // use DMA channel 8 for SPI Tx
#define DMA_CHANNEL_FOR_SPI_RX 9 // use DMA channel 9 for SPI Rx
// The SPI Tx data to be sent at each trigger
//
static const unsigned long ulSPI_TX[8] = { // fixed SPI transmission (0x01, 0x02,.. 0x08) with CS asserted throughout the frame
(0x01 | SPI_PUSHR_CONT | SPI_PUSHR_PCS0 | SPI_PUSHR_CTAS_CTAR0),
(0x02 | SPI_PUSHR_CONT | SPI_PUSHR_PCS0 | SPI_PUSHR_CTAS_CTAR0),
(0x03 | SPI_PUSHR_CONT | SPI_PUSHR_PCS0 | SPI_PUSHR_CTAS_CTAR0),
(0x04 | SPI_PUSHR_CONT | SPI_PUSHR_PCS0 | SPI_PUSHR_CTAS_CTAR0),
(0x05 | SPI_PUSHR_CONT | SPI_PUSHR_PCS0 | SPI_PUSHR_CTAS_CTAR0),
(0x06 | SPI_PUSHR_CONT | SPI_PUSHR_PCS0 | SPI_PUSHR_CTAS_CTAR0),
(0x07 | SPI_PUSHR_CONT | SPI_PUSHR_PCS0 | SPI_PUSHR_CTAS_CTAR0),
(0x08 | SPI_PUSHR_EOQ | SPI_PUSHR_PCS0 | SPI_PUSHR_CTAS_CTAR0), // final byte negates CS after transmission
};
static volatile unsigned char ucRxData[128] = {0}; // circular reception buffer (SPi rx data is stored here)
static const unsigned char ucDMA_start = ((DMA_ERQ_ERQ0 << (DMA_CHANNEL_FOR_SPI_TX - 8)) | (DMA_ERQ_ERQ0 << (DMA_CHANNEL_FOR_SPI_RX - 8))); // the value to be written to start the SPI DMA transfer
static const unsigned long ulSPI_clear = (SPI_SR_RFDF | SPI_SR_RFOF | SPI_SR_TFUF | SPI_SR_EOQF | SPI_SR_TCF); // this is written to the SPI status register after the CS negates in order to clear flags and allow subsequent transfers
// Configure the DMA trigger from an input pin edge to start an SPI transfer
//
INTERRUPT_SETUP interrupt_setup; // interrupt configuration parameters
interrupt_setup.int_port = PORTE; // the port that the interrupt input is on
interrupt_setup.int_port_bits = PORTE_BIT24; // PTE24
interrupt_setup.int_port_sense = (IRQ_FALLING_EDGE | PULLUP_ON | PORT_DMA_MODE); // DMA on falling edge
interrupt_setup.int_handler = 0; // no interrupt handler when using DMA
fnConfigureInterrupt((void *)&interrupt_setup); // configure interrupt/DMA
// Configure the DMA trigger from an input pin edge to clear the SPI status register
//
interrupt_setup.int_port = PORTD; // the port that the interrupt input is on
interrupt_setup.int_port_bits = PORTD_BIT0; // PTD0, which is the SPI CS output
interrupt_setup.int_port_sense = (IRQ_RISING_EDGE | PULLUP_ON | PORT_DMA_MODE | PORT_KEEP_PERIPHERAL); // DMA on rising edge (keep CS peripheral to trigger on the end of a transfer9
fnConfigureInterrupt((void *)&interrupt_setup); // configure interrupt/DMA
// Initialise SPI interface
//
POWER_UP_ATOMIC(6, SPI0);
_CONFIG_PERIPHERAL(D, 0, (PD_0_SPI0_PCS0 | PORT_SRE_FAST | PORT_DSE_HIGH));
_CONFIG_PERIPHERAL(D, 1, (PD_1_SPI0_SCK | PORT_SRE_FAST | PORT_DSE_HIGH));
_CONFIG_PERIPHERAL(D, 2, (PD_2_SPI0_SOUT | PORT_SRE_FAST | PORT_DSE_HIGH));
_CONFIG_PERIPHERAL(D, 3, (PD_3_SPI0_SIN));
SPI0_MCR = (SPI_MCR_MSTR | SPI_MCR_DCONF_SPI | SPI_MCR_CLR_RXF | SPI_MCR_CLR_TXF | SPI_MCR_PCSIS_CS0 | SPI_MCR_PCSIS_CS1 | SPI_MCR_PCSIS_CS2 | SPI_MCR_PCSIS_CS3 | SPI_MCR_PCSIS_CS4 | SPI_MCR_PCSIS_CS5);
SPI0_RSER = (SPI_SRER_TFFF_DIRS | SPI_SRER_TFFF_RE | SPI_SRER_RFDF_DIRS | SPI_SRER_RFDF_RE); // enable rx and tx DMA requests
SPI0_CTAR0 = (SPI_CTAR_DBR | SPI_CTAR_FMSZ_8 | SPI_CTAR_PDT_7 | SPI_CTAR_BR_4 | SPI_CTAR_CPHA | SPI_CTAR_CPOL); // for 60MHz bus, 15MHz speed and 120ns min de-select time
// Configure SPI TX DMA to transfer the content of ulSPI_TX[] on each DMA trigger
//
fnConfigDMA_buffer(DMA_CHANNEL_FOR_SPI_TX, DMAMUX_CHCFG_SOURCE_SPI0_TX, sizeof(ulSPI_TX), (void *)ulSPI_TX, (void *)SPI0_PUSHR_ADDR, (DMA_DIRECTION_OUTPUT | DMA_LONG_WORDS | DMA_SINGLE_CYCLE), 0, 0); // source is the tx buffer and destination is the SPI transmit register without interrupts (free-running)
// Configure SPI RX DMA to save reception continuously to ucRxData[] (with optional interrupt call back at haf and complete buffer)
//
fnConfigDMA_buffer(DMA_CHANNEL_FOR_SPI_RX, DMAMUX_CHCFG_SOURCE_SPI0_RX, sizeof(ucRxData), (void *)SPI0_POPR_ADDR, (void *)ucRxData, (DMA_DIRECTION_INPUT | DMA_BYTES | DMA_HALF_BUFFER_INTERRUPT), spi_half_buffer, PRIORITY_DMA9); // source is the SPI reception register and destination is the input buffer with interrupt at half- and full buffer
// Configure the falling edge port input to a DMA cycle
//
fnConfigDMA_buffer(DMA_CHANNEL_FOR_PORT_EDGE, DMAMUX0_CHCFG_SOURCE_PORTE, sizeof(ucDMA_start), (void *)&ucDMA_start, (void *)(((unsigned char *)DMA_ERQ_ADDR) + 1), (DMA_FIXED_ADDRESSES | DMA_BYTES), 0, 0); // use DMA channel without any interrupts (free-runnning)
fnDMA_BufferReset(DMA_CHANNEL_FOR_PORT_EDGE, DMA_BUFFER_START); // enable the DMA operation - a falling edge on the port will now trigger SPI Tx and Rx DMA operation
// Configure a rising edge port input (CS line negation) to reset the SPI status register so that the following cycle can operate
//
fnConfigDMA_buffer(DMA_CHANNEL_FOR_CS_END, DMAMUX0_CHCFG_SOURCE_PORTD, sizeof(ulSPI_clear), (void *)&ulSPI_clear, (void *)SPI0_SR_ADDR, (DMA_FIXED_ADDRESSES | DMA_LONG_WORDS), 0, 0); // use DMA channel without any interrupts (free-runnning)
fnDMA_BufferReset(DMA_CHANNEL_FOR_CS_END, DMA_BUFFER_START); // enable the DMA operation - a rising edge on the port will now a clear of the SPI status register
// From this point the SPI operation runs driven exclusively by DMA and no CPU intervention (the operation continues even when the debugger pauses the CPU operation)
//
// Optional interrupt call back handler
//
static void spi_half_buffer(void)
{
// This is called each time the SPI Rx buffer is half-full or full so that the previous half buffer content can be retrieved while the next half is still being filled
//
}
3. This diagram shows how 4 DMA channels are required, whereby there is a slight complication since it is also necessary to clear the SPI status register flags after each transfer so that it can continue (otherwise only 3 would have been necessary)

Conclusion:
- Is is very simple to achieve extremely efficient SPI operation of this type, allowing very high speed sampling rates without CPU intervention. By setting a large Rx buffer the CPU can read and process the data at leisure without risk of overruns.
Final notes: the attached binary also has USB-CDC on the USB and a TCP/IP stack operation on the Ethernet (IP 192.168.0.5) to show that the high speed SPI sampling doesn't interfere with the other operations.
On the debug interface (OpenSDA VCOM at 115200 Baud or the USB-CDC interface) there is a command line menu. In the I/O menu you can view memory.
The command "md 1fff02cc b 128" displays the content of the Rx buffer (the address was found in the map file) so that you can check received data if you loop back the SPI Tx to see that it fills up as the triggers arrive.
This is available as turn-key solution in the uTasker project and runs on any Kinetis part with DSPI and eDMA.
Please contact me in case you would like the code or advice on how to adapt yours to achieve your requirement.
Regards
Mark
Kinetis: http://www.utasker.com/kinetis.html
k64:
- http://www.utasker.com/kinetis/FRDM-K64F.html
- http://www.utasker.com/kinetis/TWR-K64F120M.html
- http://www.utasker.com/kinetis/TEENSY_3.5.html
- http://www.utasker.com/kinetis/Hexiwear-K64F.html
Faster and cheaper project development....