Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 9 of 9

Thread: Teensy 4 as SPI Slave

  1. #1
    Junior Member
    Join Date
    Oct 2019
    Posts
    3

    Teensy 4 as SPI Slave

    I have the need to create something that emulates a piece of hardware that will be built later. The hardware will act as a SPI slave and I thought "heck, that should be easy....right?"

    I've done a bunch of searching and while there are plenty of examples of SPI master code, there just doesn't seem to be much for implementing a slave, particularly on the Teensy 4.0. I'm just making the rabid assumption that the NXP part has lovely built-in register functionality that would allow me to configure it as a slave, point to the data, and let it handle my life for me, but getting that exposed to the Arduino (Teensyduino?) development environment doesn't seem to be as simple as I would have hoped.

    I'm betting that for my purposes I can actually just hack together some bit-banging, polled-not-even-interrupt-driven code that will do what I want (it's pretty basic), but before I jump off of that particular embarrassing bridge I thought I would first publicly display my ignorance and ask if there are any examples of SPI slave code for the Teensy 4.0 out there.

    Anybody have any sage wisdom here? OK, how about any semi-snarky advice? Or should I consider a different profession in the lucrative lawn-mowing business?

    ..glen

  2. #2
    Junior Member
    Join Date
    Oct 2019
    Posts
    7
    Hi Glen,

    I'm in the same position. You're correct that the NXP has excellent hardware support for running as an I2C slave. The bad news is that the teensyduino implementation for Wire only supports master mode.

    I'm writing a replacement driver at the moment which I'll make public as soon as it's at beta quality. I've already got slave mode sorted and I'm currently working on master mode. (Which turns out to be quite hard!) I expect to make it available in 2 to 3 weeks. (I'm working on it at the weekends)

    You could use one of the Teensy 3 boards. I believe the i2c_t3 library supports slave mode and is shipped as part of Teensyduino. It's probably pretty reliable. See the Teensy docs for a link to the main thread about it.

    If you'd like to try my driver for the Teensy 4 but can't wait a few weeks, I could bundle up what I've got this weekend and you could give it a go. I'd appreciate having someone test it for me.

    cheers,
    Richard

  3. #3
    Senior Member+ KurtE's Avatar
    Join Date
    Jan 2014
    Posts
    5,587
    These are two different things: Slave SPI and Slave I2C

    Both should be possible.

    As mentioned the i2c_t3 library supports slave mode on T3.x... Last I noticed on issues up on this library is he now has some T4's and hopefully when he gets time will add support for T4...

    But I also believe that the Wire library also supports slave mode. Example shown in Arduino documentation: https://www.arduino.cc/en/Tutorial/MasterWriter

    And in fact if you look at the examples under the wire library you will see examples on how to do so.

    Unfortunately it looks like we have not yet implemented the slave mode For T4... from WireIMXRT.cpp
    Code:
    void TwoWire::begin(uint8_t address)
    {
    	// TODO: slave mode
    }
    Again it is probably not hard to do. And if someone gets to it, it would be great to do a Pull Request with updated code. I might take a look soon if no one else gets to it...


    Now as for SPI slave again the chip supports it. Unfortunately SPI library does not. I know there have been a few threads about this for T3.x and I played some with it awhile ago (T3.x). At the time, I found it tricky to get SPI client on T3.x to work the way I wanted, in trying to manage when things were in the queue and the like.
    Who knows might be easier on T4...

    Again at some point might play again with it...

  4. #4
    Junior Member
    Join Date
    Oct 2019
    Posts
    7
    Quote Originally Posted by KurtE View Post
    These are two different things: Slave SPI and Slave I2C...
    Errr.... Apparently I completely failed to read the question properly. Sorry about that.

    I'm not doing anything with SPI but I might go and brush up on my reading skills.

    cheers,
    Richard

  5. #5
    Junior Member
    Join Date
    Oct 2019
    Posts
    3
    Thanks for the responses, but as Kurt noted this particular need is a SPI issue, not an I2C issue. Slaves are always trickier than it seems at the beginning because if you want to do it right you almost have to do it with interrupts, which makes life instantly more interesting.

    For this particular case I can be pretty brutish about things since I have have carnal knowledge of both the master and slave side of the communication, and I can just implement some sort of gross bit-bang solution, I just didn't want to be that inelegant if somebody had already solved the problem!

    I shall now put on my boxing gloves and bang out some code that shall never see the light of day beyond my desk....

    ..gb

  6. #6
    Junior Member
    Join Date
    Oct 2018
    Posts
    5

    Simple SPI Slave

    Here's a simple SPI slave that only receives data and only works on Teensy 4.0. It always receives the same number of bytes (the constant BufSize.) DMA is used to write to the destination buffer, and your sketch needs to define the function spiBuf() to return a pointer to the destination buffer. You can also declare swapSpiBuf() if you want to double-buffer the received data.

    This code is obviously hacked out of the Teensy SPI library.


    Just include SPISlave.h and call beginSlave() in your begin() function. Nothing is required in loop().



    The following is C:\Program Files (x86)\Arduino\hardware\teensy\avr\libraries\SPISla ve\SPISlave.h

    Code:
    /*
     * Copyright (c) 2010 by Cristian Maglie <c.maglie@bug.st>
     * Copyright (c) 2014 by Paul Stoffregen <paul@pjrc.com> (Transaction API)
     * Copyright (c) 2014 by Matthijs Kooijman <matthijs@stdin.nl> (SPISettings AVR)
     * SPI Master library for arduino.
     *
     * This file is free software; you can redistribute it and/or modify
     * it under the terms of either the GNU General Public License version 2
     * or the GNU Lesser General Public License version 2.1, both as
     * published by the Free Software Foundation.
     */
    
    #ifndef _SPI_SLAVE_H_INCLUDED
    #define _SPI_SLAVE_H_INCLUDED
    
    #include <Arduino.h>
    
    #if defined(__arm__) && defined(TEENSYDUINO)
    #if defined(__has_include) && __has_include(<EventResponder.h>)
    // SPI_HAS_TRANSFER_ASYNC - Defined to say that the SPI supports an ASYNC version
    // of the SPI_HAS_TRANSFER_BUF
    #define SPI_HAS_TRANSFER_ASYNC 1
    #include <DMAChannel.h>
    #include <EventResponder.h>
    #endif
    #endif
    
    // SPI_HAS_TRANSACTION means SPI has beginTransaction(), endTransaction(),
    // usingInterrupt(), and SPISetting(clock, bitOrder, dataMode)
    #define SPI_HAS_TRANSACTION 1
    
    // Uncomment this line to add detection of mismatched begin/end transactions.
    // A mismatch occurs if other libraries fail to use SPI.endTransaction() for
    // each SPI.beginTransaction().  Connect a LED to this pin.  The LED will turn
    // on if any mismatch is ever detected.
    //#define SPI_TRANSACTION_MISMATCH_LED 5
    
    // SPI_HAS_TRANSFER_BUF - is defined to signify that this library supports
    // a version of transfer which allows you to pass in both TX and RX buffer
    // pointers, either of which could be NULL
    #define SPI_HAS_TRANSFER_BUF 1
    
    
    #ifndef LSBFIRST
    #define LSBFIRST 0
    #endif
    #ifndef MSBFIRST
    #define MSBFIRST 1
    #endif
    
    #define SPI_MODE0 0x00
    #define SPI_MODE1 0x04
    #define SPI_MODE2 0x08
    #define SPI_MODE3 0x0C
    
    #define SPI_CLOCK_DIV4 0x00
    #define SPI_CLOCK_DIV16 0x01
    #define SPI_CLOCK_DIV64 0x02
    #define SPI_CLOCK_DIV128 0x03
    #define SPI_CLOCK_DIV2 0x04
    #define SPI_CLOCK_DIV8 0x05
    #define SPI_CLOCK_DIV32 0x06
    
    #define SPI_MODE_MASK 0x0C  // CPOL = bit 3, CPHA = bit 2 on SPCR
    #define SPI_CLOCK_MASK 0x03  // SPR1 = bit 1, SPR0 = bit 0 on SPCR
    #define SPI_2XCLOCK_MASK 0x01  // SPI2X = bit 0 on SPSR
    
    
    
    /**********************************************************/
    /*     32 bit Teensy 4.x                                  */
    /**********************************************************/
    
    #if defined(__arm__) && defined(TEENSYDUINO) && (defined(__IMXRT1052__) || defined(__IMXRT1062__))
    #define SPI_ATOMIC_VERSION 1
    
    void synchronousRead_isr(void);
    
    
    class DMABaseClass;
    
    
    void dumpDMA_TCD( DMABaseClass *dmabc );
    
    
    class SPISettings 
    {
    public:
    	SPISettings(uint32_t clockIn, uint8_t bitOrderIn, uint8_t dataModeIn) : _clock
    	(clockIn) 
    	{
    		init_AlwaysInline(bitOrderIn, dataModeIn);
    	}
    
    	SPISettings() : _clock(4000000) 
    	{
    		init_AlwaysInline(MSBFIRST, SPI_MODE0);
    	}
    private:
    	void init_AlwaysInline(uint8_t bitOrder, uint8_t dataMode)
    	  __attribute__((__always_inline__)) 
    	  {
    			tcr = LPSPI_TCR_FRAMESZ(7);    // TCR has polarity and bit order too
    
    			// handle LSB setup 
    			if (bitOrder == LSBFIRST) tcr |= LPSPI_TCR_LSBF;
    
    			// Handle Data Mode
    			if (dataMode & 0x08) tcr |= LPSPI_TCR_CPOL;
    
    			// Note: On T3.2 when we set CPHA it also updated the timing.  It moved the 
    			// PCS to SCK Delay Prescaler into the After SCK Delay Prescaler	
    			if (dataMode & 0x04) tcr |= LPSPI_TCR_CPHA; 
    	}
    
    	inline uint32_t clock() {return _clock;}
    
    	uint32_t _clock;
    	uint32_t tcr; // transmit command, pg 2955
    	friend class SPIClass;
    };
    
    
    
    class SPIClass // Teensy 4
    { 
    public:
    	static const uint8_t CNT_MISO_PINS = 1;
    	static const uint8_t CNT_MOSI_PINS = 1;
    	static const uint8_t CNT_SCK_PINS = 1;
    	static const uint8_t CNT_CS_PINS = 1;
    	
    	struct SPI_Hardware_t
    	{
    		volatile uint32_t &clock_gate_register;
    		const uint32_t clock_gate_mask;
    		uint8_t  tx_dma_channel;
    		uint8_t  rx_dma_channel;
    		void     (*dma_rxisr)();
    		const uint8_t  miso_pin[CNT_MISO_PINS];
    		const uint32_t  miso_mux[CNT_MISO_PINS];
    		const uint8_t  mosi_pin[CNT_MOSI_PINS];
    		const uint32_t  mosi_mux[CNT_MOSI_PINS];
    		const uint8_t  sck_pin[CNT_SCK_PINS];
    		const uint32_t  sck_mux[CNT_SCK_PINS];
    		const uint8_t  cs_pin[CNT_CS_PINS];
    		const uint32_t  cs_mux[CNT_CS_PINS];
    
    		volatile uint32_t &sck_select_input_register;
    		volatile uint32_t &sdi_select_input_register;
    		volatile uint32_t &sdo_select_input_register;
    		volatile uint32_t &pcs0_select_input_register;
    		const uint8_t sck_select_val;
    		const uint8_t sdi_select_val;
    		const uint8_t sdo_select_val;
    		const uint8_t pcs0_select_val;
    	};
    	
    	static const SPI_Hardware_t spiclass_lpspi4_hardware;
    	static const SPI_Hardware_t spiclass_lpspi3_hardware;
    	static const SPI_Hardware_t spiclass_lpspi1_hardware;
    
    
    public:
    	constexpr SPIClass(uintptr_t myport, uintptr_t myhardware)
    		: port_addr(myport), hardware_addr(myhardware) 
    	{
    	}
    
    	void beginSlave();
    
    	bool initDMA();
    	
    	void swapSpiBuf()
    	{ 
    	   _dmaRX->destinationBuffer((uint8_t*)spiBuf(), BufSize ); 
    	}
    	
    
    	friend void _spi_dma_rxISR0();
    	inline void dma_rxisr();
    
    
    public:
    	IMXRT_LPSPI_t & port() { return *(IMXRT_LPSPI_t *)port_addr; }
    
    private:
    	const SPI_Hardware_t & hardware() { return *(const SPI_Hardware_t *)hardware_addr; }
    	uintptr_t port_addr;
    	uintptr_t hardware_addr;
    
    	uint32_t _clock = 0;
    	uint32_t _ccr = 0;
    
    	uint8_t miso_pin_index = 0;
    	uint8_t mosi_pin_index = 0;
    	uint8_t sck_pin_index = 0;
    	
    	uint8_t interruptMasksUsed = 0;
    	uint32_t interruptMask[(NVIC_NUM_INTERRUPTS+31)/32] = {};
    	uint32_t interruptSave[(NVIC_NUM_INTERRUPTS+31)/32] = {};
    	
    
    	// DMA Support
    	bool initDMAChannels();
    	enum DMAState { notAllocated, idle, active, completed};
    	enum { MAX_DMA_COUNT = 32767 };
    	DMAState _dma_state = DMAState::notAllocated;
    	uint32_t _dma_count_remaining = 0;	// How many bytes left to output after current DMA completes
    	
    	DMAChannel *_dmaRX = nullptr;
    	EventResponder *_dma_event_responder = nullptr;
    
    };
    
    
    #endif
    
    
    extern SPIClass SPI;
    extern SPIClass SPI1;
    extern SPIClass SPI2;
    
    #endif

    The following is C:\Program Files (x86)\Arduino\hardware\teensy\avr\libraries\SPISla ve\SPISlave.cpp

    Code:
    /*
     *
     * Copyright (c) 2010 by Cristian Maglie <c.maglie@bug.st>
     * SPI Master library for arduino.
     *
     * This file is free software; you can redistribute it and/or modify
     * it under the terms of either the GNU General Public License version 2
     * or the GNU Lesser General Public License version 2.1, both as
     * published by the Free Software Foundation.
     */
    
    #include "SPISlave.h"
    #include "pins_arduino.h"
    
    #define DEBUG_DMA_TRANSFERS
    
    
    /**********************************************************/
    /*     32 bit Teensy 4.x                                  */
    /**********************************************************/
    
    
    void _spi_dma_rxISR0(void) {SPI.dma_rxisr();}
    
    const SPIClass::SPI_Hardware_t  SPIClass::spiclass_lpspi4_hardware = 
    {
    	CCM_CCGR1, CCM_CCGR1_LPSPI4(CCM_CCGR_ON),
    	DMAMUX_SOURCE_LPSPI4_TX, 
    	DMAMUX_SOURCE_LPSPI4_RX, 
    	_spi_dma_rxISR0,
    	12, 
    	3 | 0x10,
    	11,
    	3 | 0x10,
    	13,
    	3 | 0x10,
    	10,  // from https://forum.pjrc.com/threads/57328-Teensy-4-0-SPI-Chip-Select-pins If I remember correctly the one case, where you need to use the Hardware CS pin is if you implement an SPI Client setup, which I have not tried.
    
    	3 | 0x10,
    	IOMUXC_LPSPI4_SCK_SELECT_INPUT, 
    	IOMUXC_LPSPI4_SDI_SELECT_INPUT, 
    	IOMUXC_LPSPI4_SDO_SELECT_INPUT, 
    	IOMUXC_LPSPI4_PCS0_SELECT_INPUT,
    	0, 0, 0, 0
    };
    
    SPIClass SPI((uintptr_t)&IMXRT_LPSPI4_S, (uintptr_t)&SPIClass::spiclass_lpspi4_hardware);
    
    // sketch defines these functions.
    unsigned char *spiBuf();
    void swapSpiBuf();
    
    
    /*
    LPSPI slave mode uses the same shift register and logic as the master mode, but does not
    use the clock configuration register and the transmit command register must remain static
    during SPI bus transfers.
    */
    
    
    /*
    	clock_gate_register = CCM_CCGR1  // 13.7.22 p 1140  
    	clock_gate_mask = CCM_CCGR1_LPSPI4(CCM_CCGR_ON)   // CCM_CCGR_ON = 3
    	tx_dma_channel = DMAMUX_SOURCE_LPSPI4_TX  // 80
    	rx_dma_channel = DMAMUX_SOURCE_LPSPI4_RX  // 79
    	dma_rxisr = SPI.gdr_isr
    			
    	cs_mux[0] = 3 | 0x10
    
    	sck_select_input_register = IOMUXC_LPSPI4_SCK_SELECT_INPUT
    	sdi_select_input_register = IOMUXC_LPSPI4_SDI_SELECT_INPUT
    	sdo_select_input_register = IOMUXC_LPSPI4_SDO_SELECT_INPUT
    	pcs0_select_input_register = IOMUXC_LPSPI4_PCS0_SELECT_INPUT
    	
    	sck_select_val = 0
    	sdi_select_val = 0
    	sdo_select_val = 0
    	pcs0_select_val = 0
    */
    
    static const int rxWater = 15;
    //-----------------------------------------------------------------------------
    void SPIClass::beginSlave()
    //-----------------------------------------------------------------------------
    {
    //	Serial.printf( "clock_gate_register %08x\n", hardware().clock_gate_register );
    //	Serial.printf( "clock_gate_mask %08x\n", hardware().clock_gate_mask );
    
        // turn off the LPSPI4 clock ( CCGR1 )
    	// this is really CCM_CBCMR &= ~CCM_CCGR1_LPSPI4(CCM_CCGR_ON);
    	hardware().clock_gate_register &= ~hardware().clock_gate_mask;
    
    
    	Serial.printf( "SPI MISO: %d MOSI: %d, SCK: %d CS: %d\n", 
                       hardware().miso_pin[0], 
                       hardware().mosi_pin[0], 
                       hardware().sck_pin[0], 
                       hardware().cs_pin[0] );
    
    
        // CCM = Clock control module
    	//Serial.printf( "before CBCMR = %08lX CCGR1 = %08lX\n", CCM_CBCMR, CCM_CCGR1 ); // CCM Bus Clock Multiplexer Register
    
        // IOMUXC = IOMUX Controller
    	// SRE = slew rate fast
    	// DSE(3) = Drive strength R0/5
    	// SPEED(3) = max(200MHz)	
    
    	uint32_t fastio = IOMUXC_PAD_DSE(6) | IOMUXC_PAD_SPEED(1);
    	//uint32_t fastio = IOMUXC_PAD_SRE | IOMUXC_PAD_DSE(3) | IOMUXC_PAD_SPEED(3);
    
    	*(portControlRegister(hardware().miso_pin[0])) = fastio;
    	*(portControlRegister(hardware().mosi_pin[0])) = fastio;
    	*(portControlRegister(hardware().sck_pin[0])) = fastio;
    
    
    //	*(portControlRegister(16) = fastio;
    //	*(portControlRegister(17) = fastio;
    //	*(portControlRegister(19) = fastio;
    
    
        // turn on the LPSPI4 clock ( CCGR1 )
    	// this is really CCM_CBCMR |= CCM_CCGR1_LPSPI4(CCM_CCGR_ON);
    	hardware().clock_gate_register |= hardware().clock_gate_mask;
    
    	Serial.printf( "after CBCMR = %08lX CCGR1 = %08lX\n", CCM_CBCMR, CCM_CCGR1 ); // CCM Bus Clock Multiplexer Register
    
    
    	*(portConfigRegister(hardware().miso_pin[0])) 
    	   = hardware().miso_mux[0];
    	*(portConfigRegister(hardware().mosi_pin[0])) 
    	   = hardware().mosi_mux[0];
    	*(portConfigRegister(hardware().sck_pin[0])) 
    	   = hardware().sck_mux[0];
    
    	// why do I need this in slave mode when master mode doesn't do it?
    	*(portConfigRegister(hardware().cs_pin[0])) 
    	   = hardware().cs_mux[0];  
    
    
    //	*(portConfigRegister(16) = IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B1_07;
    //	*(portConfigRegister(17) = IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B1_06;
    //	*(portConfigRegister(19) = IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B1_00;
    
    
    	// Set the Mux pins 
       	hardware().sck_select_input_register = hardware().sck_select_val;
    	hardware().sdi_select_input_register = hardware().sdi_select_val;
    	hardware().sdo_select_input_register = hardware().sdo_select_val;
    
        // why do I need this in slave mode when master mode doesn't do it?
    	hardware().pcs0_select_input_register = hardware().pcs0_select_val;  
    	   
    	// CR  = Control register
    	// RST = Software reset
    	port().CR = LPSPI_CR_RST;
    
    	// Initialize the Receive FIFO watermark to FIFO size - 1... 
    	port().FCR = LPSPI_FCR_RXWATER( rxWater );
    	
    	port().CR = LPSPI_CR_MEN;     // Module Enable
    
    	
    	initDMA();
    	
    
    	Serial.printf( "PARAM %x CR %x SR %x IER %x DER %x CFGR0 %x CFGR1 %x FCR %x TCR %x\n", 
    	               port().PARAM,
    	               port().CR, port().SR, 
    				   port().IER, port().DER,
    				   port().CFGR0, port().CFGR1, 
    				   port().FCR, port().TCR );	
    }
    
    
    static int const BufSize = 3000;
    
    
    //-----------------------------------------------------------------------------
    bool SPIClass::initDMAChannels() 
    //-----------------------------------------------------------------------------
    {
    	_dmaRX = new DMAChannel();
    
    	if (_dmaRX == nullptr)
    		return false;
    
    Serial.printf( "rx_dma_channel = %d\n", hardware().rx_dma_channel );
    
    	// Set up the RX chain
    	_dmaRX->disable();
    
    	_dmaRX->source( (volatile uint8_t&)port().RDR );
    /*
    Disable Request
    If this flag is set, the eDMA hardware automatically clears the corresponding ERQ bit when the current
    major iteration count reaches zero.
    0b - The channel's ERQ bit is not affected.
    1b - The channel's ERQ bit is cleared when the major loop is complete.
    */
    	_dmaRX->disableOnCompletion();   // TCD->CSR |= DMA_TCD_CSR_DREQ;
    	_dmaRX->triggerAtHardwareEvent( hardware().rx_dma_channel );
    	_dmaRX->attachInterrupt( hardware().dma_rxisr );
    
    /*
    Enable an interrupt when major iteration count completes.
    If this flag is set, the channel generates an interrupt request by setting the appropriate bit in the INT when
    the current major iteration count reaches zero.
    0b - The end-of-major loop interrupt is disabled.
    1b - The end-of-major loop interrupt is enabled.
    */
    
        // cause the dma_rxisr() to be called at the end of the transfer
    	_dmaRX->interruptAtCompletion();  // TCD->CSR |= DMA_TCD_CSR_INTMAJOR;
    	
    	_dma_state = DMAState::idle;  // Should be first thing set!
    	
    	_dmaRX->enable(); 
    	
    	return true;
    }
    
    
    EventResponder _event_responder;
    
    #define DATA_BUFFER_MAX (16*1024)
    
    //-----------------------------------------------------------------------------
    bool SPIClass::initDMA()
    //-----------------------------------------------------------------------------
    {	
    	initDMAChannels();
    	
    	
        if (_dma_state == DMAState::active)
    		return false; // already active
    
    	_event_responder.clearEvent();	// Make sure it is not set yet
    	
    	_dma_count_remaining = 0;
    	
    	_dmaRX->TCD->ATTR_SRC = 0;		// Make sure set for 8 bit mode...
    	_dmaRX->destinationBuffer((uint8_t*)spiBuf(), BufSize );
    	_dmaRX->TCD->DLASTSGA = -BufSize;
    
    	if( (uint32_t)spiBuf() >= 0x20200000u )  
    		arm_dcache_delete( spiBuf(), BufSize );
    	
    	_dma_event_responder = &_event_responder;
    
    	dumpDMA_TCD( _dmaRX );
    
    	// Make sure port is in 8 bit mode and clear watermark
    	port().TCR = (port().TCR & ~(LPSPI_TCR_FRAMESZ(31))) | LPSPI_TCR_FRAMESZ(7);	
    	port().FCR = 0; 
    
    	// Lets try to output the first byte to make sure that we are in 8 bit mode...
     	port().DER = LPSPI_DER_RDDE;	//enable DMA on RX
    	port().SR = 0x3f00;	// clear out all of the other status...
    
    	_dmaRX->enable();
    
    	_dma_state = DMAState::active;
    	
    	return true;
    }
    
    
    
    //-----------------------------------------------------------------------------
    inline void DMAChanneltransferCount( DMAChannel *dmac, unsigned int len ) 
    //-----------------------------------------------------------------------------
    {
    	// note does no validation of length...
    	DMABaseClass::TCD_t *tcd = dmac->TCD;
    	
    	if (!(tcd->BITER & DMA_TCD_BITER_ELINK)) 
    	{
    		tcd->BITER = len & 0x7fff;
    	} 
    	else 
    	{
    		tcd->BITER = (tcd->BITER & 0xFE00) | (len & 0x1ff);
    	}
    	tcd->CITER = tcd->BITER; 
    }
    
    //-----------------------------------------------------------------------------
    void dumpDMA_TCD( DMABaseClass *dmabc )
    //-----------------------------------------------------------------------------
    {
    	Serial.printf("spiData = %p BC: %x TCD: %x:", spiBuf(), (uint32_t)dmabc, (uint32_t)dmabc->TCD);
    
    	Serial.printf("SA:%x SO:%d AT:%x NB:%x SL:%d DA:%x DO:%d CI:%x DL:%x CS:%x BI:%x\n", (uint32_t)dmabc->TCD->SADDR,
    		dmabc->TCD->SOFF, dmabc->TCD->ATTR, dmabc->TCD->NBYTES, dmabc->TCD->SLAST, (uint32_t)dmabc->TCD->DADDR, 
    		dmabc->TCD->DOFF, dmabc->TCD->CITER, dmabc->TCD->DLASTSGA, dmabc->TCD->CSR, dmabc->TCD->BITER);
    }
    
    
    //-------------------------------------------------------------------------
    void SPIClass::dma_rxisr() 
    //-------------------------------------------------------------------------
    {
    	_dmaRX->clearInterrupt();   // 		DMA_CINT = channel;
    	_dmaRX->clearComplete();    // 		DMA_CDNE = channel;
    	
    	// receive a full SPI buffer
        DMAChanneltransferCount( _dmaRX, BufSize );
    
        // Optionally allow the user to double buffer the data.  Comment 
    	//   the swapSpiBuf() functions out if you don't need them.
        ::swapSpiBuf();   // swap the pointers
        swapSpiBuf();     // tell _dmaRX the new buffer
    	
    	
        _dmaRX->enable();
    
        asm("dsb");
    
    }
    
    	
    //-------------------------------------------------------------------------
    void swapSpiBuf()
    //-------------------------------------------------------------------------
    { 
      _dmaRX->destinationBuffer((uint8_t*)spiBuf(), BufSize ); 
    }

  7. #7
    Junior Member
    Join Date
    Oct 2019
    Posts
    3
    First of all: wow, cool!

    For my work however I actually need to be clocking data OUT of the slave (think of the slave kind of acting like a SPI eeprom). However I'm going to bet that if I peruse the code that Gordon provided I'll figure out how to make that happen. Thanks!

    ..glen

    Quote Originally Posted by GordonRush View Post
    Here's a simple SPI slave that only receives data and only works on Teensy 4.0. It always receives the same number of bytes (the constant BufSize.) DMA is used to write to the destination buffer, and your sketch needs to define the function spiBuf() to return a pointer to the destination buffer. You can also declare swapSpiBuf() if you want to double-buffer the received data.

    This code is obviously hacked out of the Teensy SPI library.


    Just include SPISlave.h and call beginSlave() in your begin() function. Nothing is required in loop().



    The following is C:\Program Files (x86)\Arduino\hardware\teensy\avr\libraries\SPISla ve\SPISlave.h

    Code:
    /*
     * Copyright (c) 2010 by Cristian Maglie <c.maglie@bug.st>
     * Copyright (c) 2014 by Paul Stoffregen <paul@pjrc.com> (Transaction API)
     * Copyright (c) 2014 by Matthijs Kooijman <matthijs@stdin.nl> (SPISettings AVR)
     * SPI Master library for arduino.
     *
     * This file is free software; you can redistribute it and/or modify
     * it under the terms of either the GNU General Public License version 2
     * or the GNU Lesser General Public License version 2.1, both as
     * published by the Free Software Foundation.
     */
    
    #ifndef _SPI_SLAVE_H_INCLUDED
    #define _SPI_SLAVE_H_INCLUDED
    
    #include <Arduino.h>
    
    #if defined(__arm__) && defined(TEENSYDUINO)
    #if defined(__has_include) && __has_include(<EventResponder.h>)
    // SPI_HAS_TRANSFER_ASYNC - Defined to say that the SPI supports an ASYNC version
    // of the SPI_HAS_TRANSFER_BUF
    #define SPI_HAS_TRANSFER_ASYNC 1
    #include <DMAChannel.h>
    #include <EventResponder.h>
    #endif
    #endif
    
    // SPI_HAS_TRANSACTION means SPI has beginTransaction(), endTransaction(),
    // usingInterrupt(), and SPISetting(clock, bitOrder, dataMode)
    #define SPI_HAS_TRANSACTION 1
    
    // Uncomment this line to add detection of mismatched begin/end transactions.
    // A mismatch occurs if other libraries fail to use SPI.endTransaction() for
    // each SPI.beginTransaction().  Connect a LED to this pin.  The LED will turn
    // on if any mismatch is ever detected.
    //#define SPI_TRANSACTION_MISMATCH_LED 5
    
    // SPI_HAS_TRANSFER_BUF - is defined to signify that this library supports
    // a version of transfer which allows you to pass in both TX and RX buffer
    // pointers, either of which could be NULL
    #define SPI_HAS_TRANSFER_BUF 1
    
    
    #ifndef LSBFIRST
    #define LSBFIRST 0
    #endif
    #ifndef MSBFIRST
    #define MSBFIRST 1
    #endif
    
    #define SPI_MODE0 0x00
    #define SPI_MODE1 0x04
    #define SPI_MODE2 0x08
    #define SPI_MODE3 0x0C
    
    #define SPI_CLOCK_DIV4 0x00
    #define SPI_CLOCK_DIV16 0x01
    #define SPI_CLOCK_DIV64 0x02
    #define SPI_CLOCK_DIV128 0x03
    #define SPI_CLOCK_DIV2 0x04
    #define SPI_CLOCK_DIV8 0x05
    #define SPI_CLOCK_DIV32 0x06
    
    #define SPI_MODE_MASK 0x0C  // CPOL = bit 3, CPHA = bit 2 on SPCR
    #define SPI_CLOCK_MASK 0x03  // SPR1 = bit 1, SPR0 = bit 0 on SPCR
    #define SPI_2XCLOCK_MASK 0x01  // SPI2X = bit 0 on SPSR
    
    
    
    /**********************************************************/
    /*     32 bit Teensy 4.x                                  */
    /**********************************************************/
    
    #if defined(__arm__) && defined(TEENSYDUINO) && (defined(__IMXRT1052__) || defined(__IMXRT1062__))
    #define SPI_ATOMIC_VERSION 1
    
    void synchronousRead_isr(void);
    
    
    class DMABaseClass;
    
    
    void dumpDMA_TCD( DMABaseClass *dmabc );
    
    
    class SPISettings 
    {
    public:
    	SPISettings(uint32_t clockIn, uint8_t bitOrderIn, uint8_t dataModeIn) : _clock
    	(clockIn) 
    	{
    		init_AlwaysInline(bitOrderIn, dataModeIn);
    	}
    
    	SPISettings() : _clock(4000000) 
    	{
    		init_AlwaysInline(MSBFIRST, SPI_MODE0);
    	}
    private:
    	void init_AlwaysInline(uint8_t bitOrder, uint8_t dataMode)
    	  __attribute__((__always_inline__)) 
    	  {
    			tcr = LPSPI_TCR_FRAMESZ(7);    // TCR has polarity and bit order too
    
    			// handle LSB setup 
    			if (bitOrder == LSBFIRST) tcr |= LPSPI_TCR_LSBF;
    
    			// Handle Data Mode
    			if (dataMode & 0x08) tcr |= LPSPI_TCR_CPOL;
    
    			// Note: On T3.2 when we set CPHA it also updated the timing.  It moved the 
    			// PCS to SCK Delay Prescaler into the After SCK Delay Prescaler	
    			if (dataMode & 0x04) tcr |= LPSPI_TCR_CPHA; 
    	}
    
    	inline uint32_t clock() {return _clock;}
    
    	uint32_t _clock;
    	uint32_t tcr; // transmit command, pg 2955
    	friend class SPIClass;
    };
    
    
    
    class SPIClass // Teensy 4
    { 
    public:
    	static const uint8_t CNT_MISO_PINS = 1;
    	static const uint8_t CNT_MOSI_PINS = 1;
    	static const uint8_t CNT_SCK_PINS = 1;
    	static const uint8_t CNT_CS_PINS = 1;
    	
    	struct SPI_Hardware_t
    	{
    		volatile uint32_t &clock_gate_register;
    		const uint32_t clock_gate_mask;
    		uint8_t  tx_dma_channel;
    		uint8_t  rx_dma_channel;
    		void     (*dma_rxisr)();
    		const uint8_t  miso_pin[CNT_MISO_PINS];
    		const uint32_t  miso_mux[CNT_MISO_PINS];
    		const uint8_t  mosi_pin[CNT_MOSI_PINS];
    		const uint32_t  mosi_mux[CNT_MOSI_PINS];
    		const uint8_t  sck_pin[CNT_SCK_PINS];
    		const uint32_t  sck_mux[CNT_SCK_PINS];
    		const uint8_t  cs_pin[CNT_CS_PINS];
    		const uint32_t  cs_mux[CNT_CS_PINS];
    
    		volatile uint32_t &sck_select_input_register;
    		volatile uint32_t &sdi_select_input_register;
    		volatile uint32_t &sdo_select_input_register;
    		volatile uint32_t &pcs0_select_input_register;
    		const uint8_t sck_select_val;
    		const uint8_t sdi_select_val;
    		const uint8_t sdo_select_val;
    		const uint8_t pcs0_select_val;
    	};
    	
    	static const SPI_Hardware_t spiclass_lpspi4_hardware;
    	static const SPI_Hardware_t spiclass_lpspi3_hardware;
    	static const SPI_Hardware_t spiclass_lpspi1_hardware;
    
    
    public:
    	constexpr SPIClass(uintptr_t myport, uintptr_t myhardware)
    		: port_addr(myport), hardware_addr(myhardware) 
    	{
    	}
    
    	void beginSlave();
    
    	bool initDMA();
    	
    	void swapSpiBuf()
    	{ 
    	   _dmaRX->destinationBuffer((uint8_t*)spiBuf(), BufSize ); 
    	}
    	
    
    	friend void _spi_dma_rxISR0();
    	inline void dma_rxisr();
    
    
    public:
    	IMXRT_LPSPI_t & port() { return *(IMXRT_LPSPI_t *)port_addr; }
    
    private:
    	const SPI_Hardware_t & hardware() { return *(const SPI_Hardware_t *)hardware_addr; }
    	uintptr_t port_addr;
    	uintptr_t hardware_addr;
    
    	uint32_t _clock = 0;
    	uint32_t _ccr = 0;
    
    	uint8_t miso_pin_index = 0;
    	uint8_t mosi_pin_index = 0;
    	uint8_t sck_pin_index = 0;
    	
    	uint8_t interruptMasksUsed = 0;
    	uint32_t interruptMask[(NVIC_NUM_INTERRUPTS+31)/32] = {};
    	uint32_t interruptSave[(NVIC_NUM_INTERRUPTS+31)/32] = {};
    	
    
    	// DMA Support
    	bool initDMAChannels();
    	enum DMAState { notAllocated, idle, active, completed};
    	enum { MAX_DMA_COUNT = 32767 };
    	DMAState _dma_state = DMAState::notAllocated;
    	uint32_t _dma_count_remaining = 0;	// How many bytes left to output after current DMA completes
    	
    	DMAChannel *_dmaRX = nullptr;
    	EventResponder *_dma_event_responder = nullptr;
    
    };
    
    
    #endif
    
    
    extern SPIClass SPI;
    extern SPIClass SPI1;
    extern SPIClass SPI2;
    
    #endif

    The following is C:\Program Files (x86)\Arduino\hardware\teensy\avr\libraries\SPISla ve\SPISlave.cpp

    Code:
    /*
     *
     * Copyright (c) 2010 by Cristian Maglie <c.maglie@bug.st>
     * SPI Master library for arduino.
     *
     * This file is free software; you can redistribute it and/or modify
     * it under the terms of either the GNU General Public License version 2
     * or the GNU Lesser General Public License version 2.1, both as
     * published by the Free Software Foundation.
     */
    
    #include "SPISlave.h"
    #include "pins_arduino.h"
    
    #define DEBUG_DMA_TRANSFERS
    
    
    /**********************************************************/
    /*     32 bit Teensy 4.x                                  */
    /**********************************************************/
    
    
    void _spi_dma_rxISR0(void) {SPI.dma_rxisr();}
    
    const SPIClass::SPI_Hardware_t  SPIClass::spiclass_lpspi4_hardware = 
    {
    	CCM_CCGR1, CCM_CCGR1_LPSPI4(CCM_CCGR_ON),
    	DMAMUX_SOURCE_LPSPI4_TX, 
    	DMAMUX_SOURCE_LPSPI4_RX, 
    	_spi_dma_rxISR0,
    	12, 
    	3 | 0x10,
    	11,
    	3 | 0x10,
    	13,
    	3 | 0x10,
    	10,  // from https://forum.pjrc.com/threads/57328-Teensy-4-0-SPI-Chip-Select-pins If I remember correctly the one case, where you need to use the Hardware CS pin is if you implement an SPI Client setup, which I have not tried.
    
    	3 | 0x10,
    	IOMUXC_LPSPI4_SCK_SELECT_INPUT, 
    	IOMUXC_LPSPI4_SDI_SELECT_INPUT, 
    	IOMUXC_LPSPI4_SDO_SELECT_INPUT, 
    	IOMUXC_LPSPI4_PCS0_SELECT_INPUT,
    	0, 0, 0, 0
    };
    
    SPIClass SPI((uintptr_t)&IMXRT_LPSPI4_S, (uintptr_t)&SPIClass::spiclass_lpspi4_hardware);
    
    // sketch defines these functions.
    unsigned char *spiBuf();
    void swapSpiBuf();
    
    
    /*
    LPSPI slave mode uses the same shift register and logic as the master mode, but does not
    use the clock configuration register and the transmit command register must remain static
    during SPI bus transfers.
    */
    
    
    /*
    	clock_gate_register = CCM_CCGR1  // 13.7.22 p 1140  
    	clock_gate_mask = CCM_CCGR1_LPSPI4(CCM_CCGR_ON)   // CCM_CCGR_ON = 3
    	tx_dma_channel = DMAMUX_SOURCE_LPSPI4_TX  // 80
    	rx_dma_channel = DMAMUX_SOURCE_LPSPI4_RX  // 79
    	dma_rxisr = SPI.gdr_isr
    			
    	cs_mux[0] = 3 | 0x10
    
    	sck_select_input_register = IOMUXC_LPSPI4_SCK_SELECT_INPUT
    	sdi_select_input_register = IOMUXC_LPSPI4_SDI_SELECT_INPUT
    	sdo_select_input_register = IOMUXC_LPSPI4_SDO_SELECT_INPUT
    	pcs0_select_input_register = IOMUXC_LPSPI4_PCS0_SELECT_INPUT
    	
    	sck_select_val = 0
    	sdi_select_val = 0
    	sdo_select_val = 0
    	pcs0_select_val = 0
    */
    
    static const int rxWater = 15;
    //-----------------------------------------------------------------------------
    void SPIClass::beginSlave()
    //-----------------------------------------------------------------------------
    {
    //	Serial.printf( "clock_gate_register %08x\n", hardware().clock_gate_register );
    //	Serial.printf( "clock_gate_mask %08x\n", hardware().clock_gate_mask );
    
        // turn off the LPSPI4 clock ( CCGR1 )
    	// this is really CCM_CBCMR &= ~CCM_CCGR1_LPSPI4(CCM_CCGR_ON);
    	hardware().clock_gate_register &= ~hardware().clock_gate_mask;
    
    
    	Serial.printf( "SPI MISO: %d MOSI: %d, SCK: %d CS: %d\n", 
                       hardware().miso_pin[0], 
                       hardware().mosi_pin[0], 
                       hardware().sck_pin[0], 
                       hardware().cs_pin[0] );
    
    
        // CCM = Clock control module
    	//Serial.printf( "before CBCMR = %08lX CCGR1 = %08lX\n", CCM_CBCMR, CCM_CCGR1 ); // CCM Bus Clock Multiplexer Register
    
        // IOMUXC = IOMUX Controller
    	// SRE = slew rate fast
    	// DSE(3) = Drive strength R0/5
    	// SPEED(3) = max(200MHz)	
    
    	uint32_t fastio = IOMUXC_PAD_DSE(6) | IOMUXC_PAD_SPEED(1);
    	//uint32_t fastio = IOMUXC_PAD_SRE | IOMUXC_PAD_DSE(3) | IOMUXC_PAD_SPEED(3);
    
    	*(portControlRegister(hardware().miso_pin[0])) = fastio;
    	*(portControlRegister(hardware().mosi_pin[0])) = fastio;
    	*(portControlRegister(hardware().sck_pin[0])) = fastio;
    
    
    //	*(portControlRegister(16) = fastio;
    //	*(portControlRegister(17) = fastio;
    //	*(portControlRegister(19) = fastio;
    
    
        // turn on the LPSPI4 clock ( CCGR1 )
    	// this is really CCM_CBCMR |= CCM_CCGR1_LPSPI4(CCM_CCGR_ON);
    	hardware().clock_gate_register |= hardware().clock_gate_mask;
    
    	Serial.printf( "after CBCMR = %08lX CCGR1 = %08lX\n", CCM_CBCMR, CCM_CCGR1 ); // CCM Bus Clock Multiplexer Register
    
    
    	*(portConfigRegister(hardware().miso_pin[0])) 
    	   = hardware().miso_mux[0];
    	*(portConfigRegister(hardware().mosi_pin[0])) 
    	   = hardware().mosi_mux[0];
    	*(portConfigRegister(hardware().sck_pin[0])) 
    	   = hardware().sck_mux[0];
    
    	// why do I need this in slave mode when master mode doesn't do it?
    	*(portConfigRegister(hardware().cs_pin[0])) 
    	   = hardware().cs_mux[0];  
    
    
    //	*(portConfigRegister(16) = IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B1_07;
    //	*(portConfigRegister(17) = IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B1_06;
    //	*(portConfigRegister(19) = IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B1_00;
    
    
    	// Set the Mux pins 
       	hardware().sck_select_input_register = hardware().sck_select_val;
    	hardware().sdi_select_input_register = hardware().sdi_select_val;
    	hardware().sdo_select_input_register = hardware().sdo_select_val;
    
        // why do I need this in slave mode when master mode doesn't do it?
    	hardware().pcs0_select_input_register = hardware().pcs0_select_val;  
    	   
    	// CR  = Control register
    	// RST = Software reset
    	port().CR = LPSPI_CR_RST;
    
    	// Initialize the Receive FIFO watermark to FIFO size - 1... 
    	port().FCR = LPSPI_FCR_RXWATER( rxWater );
    	
    	port().CR = LPSPI_CR_MEN;     // Module Enable
    
    	
    	initDMA();
    	
    
    	Serial.printf( "PARAM %x CR %x SR %x IER %x DER %x CFGR0 %x CFGR1 %x FCR %x TCR %x\n", 
    	               port().PARAM,
    	               port().CR, port().SR, 
    				   port().IER, port().DER,
    				   port().CFGR0, port().CFGR1, 
    				   port().FCR, port().TCR );	
    }
    
    
    static int const BufSize = 3000;
    
    
    //-----------------------------------------------------------------------------
    bool SPIClass::initDMAChannels() 
    //-----------------------------------------------------------------------------
    {
    	_dmaRX = new DMAChannel();
    
    	if (_dmaRX == nullptr)
    		return false;
    
    Serial.printf( "rx_dma_channel = %d\n", hardware().rx_dma_channel );
    
    	// Set up the RX chain
    	_dmaRX->disable();
    
    	_dmaRX->source( (volatile uint8_t&)port().RDR );
    /*
    Disable Request
    If this flag is set, the eDMA hardware automatically clears the corresponding ERQ bit when the current
    major iteration count reaches zero.
    0b - The channel's ERQ bit is not affected.
    1b - The channel's ERQ bit is cleared when the major loop is complete.
    */
    	_dmaRX->disableOnCompletion();   // TCD->CSR |= DMA_TCD_CSR_DREQ;
    	_dmaRX->triggerAtHardwareEvent( hardware().rx_dma_channel );
    	_dmaRX->attachInterrupt( hardware().dma_rxisr );
    
    /*
    Enable an interrupt when major iteration count completes.
    If this flag is set, the channel generates an interrupt request by setting the appropriate bit in the INT when
    the current major iteration count reaches zero.
    0b - The end-of-major loop interrupt is disabled.
    1b - The end-of-major loop interrupt is enabled.
    */
    
        // cause the dma_rxisr() to be called at the end of the transfer
    	_dmaRX->interruptAtCompletion();  // TCD->CSR |= DMA_TCD_CSR_INTMAJOR;
    	
    	_dma_state = DMAState::idle;  // Should be first thing set!
    	
    	_dmaRX->enable(); 
    	
    	return true;
    }
    
    
    EventResponder _event_responder;
    
    #define DATA_BUFFER_MAX (16*1024)
    
    //-----------------------------------------------------------------------------
    bool SPIClass::initDMA()
    //-----------------------------------------------------------------------------
    {	
    	initDMAChannels();
    	
    	
        if (_dma_state == DMAState::active)
    		return false; // already active
    
    	_event_responder.clearEvent();	// Make sure it is not set yet
    	
    	_dma_count_remaining = 0;
    	
    	_dmaRX->TCD->ATTR_SRC = 0;		// Make sure set for 8 bit mode...
    	_dmaRX->destinationBuffer((uint8_t*)spiBuf(), BufSize );
    	_dmaRX->TCD->DLASTSGA = -BufSize;
    
    	if( (uint32_t)spiBuf() >= 0x20200000u )  
    		arm_dcache_delete( spiBuf(), BufSize );
    	
    	_dma_event_responder = &_event_responder;
    
    	dumpDMA_TCD( _dmaRX );
    
    	// Make sure port is in 8 bit mode and clear watermark
    	port().TCR = (port().TCR & ~(LPSPI_TCR_FRAMESZ(31))) | LPSPI_TCR_FRAMESZ(7);	
    	port().FCR = 0; 
    
    	// Lets try to output the first byte to make sure that we are in 8 bit mode...
     	port().DER = LPSPI_DER_RDDE;	//enable DMA on RX
    	port().SR = 0x3f00;	// clear out all of the other status...
    
    	_dmaRX->enable();
    
    	_dma_state = DMAState::active;
    	
    	return true;
    }
    
    
    
    //-----------------------------------------------------------------------------
    inline void DMAChanneltransferCount( DMAChannel *dmac, unsigned int len ) 
    //-----------------------------------------------------------------------------
    {
    	// note does no validation of length...
    	DMABaseClass::TCD_t *tcd = dmac->TCD;
    	
    	if (!(tcd->BITER & DMA_TCD_BITER_ELINK)) 
    	{
    		tcd->BITER = len & 0x7fff;
    	} 
    	else 
    	{
    		tcd->BITER = (tcd->BITER & 0xFE00) | (len & 0x1ff);
    	}
    	tcd->CITER = tcd->BITER; 
    }
    
    //-----------------------------------------------------------------------------
    void dumpDMA_TCD( DMABaseClass *dmabc )
    //-----------------------------------------------------------------------------
    {
    	Serial.printf("spiData = %p BC: %x TCD: %x:", spiBuf(), (uint32_t)dmabc, (uint32_t)dmabc->TCD);
    
    	Serial.printf("SA:%x SO:%d AT:%x NB:%x SL:%d DA:%x DO:%d CI:%x DL:%x CS:%x BI:%x\n", (uint32_t)dmabc->TCD->SADDR,
    		dmabc->TCD->SOFF, dmabc->TCD->ATTR, dmabc->TCD->NBYTES, dmabc->TCD->SLAST, (uint32_t)dmabc->TCD->DADDR, 
    		dmabc->TCD->DOFF, dmabc->TCD->CITER, dmabc->TCD->DLASTSGA, dmabc->TCD->CSR, dmabc->TCD->BITER);
    }
    
    
    //-------------------------------------------------------------------------
    void SPIClass::dma_rxisr() 
    //-------------------------------------------------------------------------
    {
    	_dmaRX->clearInterrupt();   // 		DMA_CINT = channel;
    	_dmaRX->clearComplete();    // 		DMA_CDNE = channel;
    	
    	// receive a full SPI buffer
        DMAChanneltransferCount( _dmaRX, BufSize );
    
        // Optionally allow the user to double buffer the data.  Comment 
    	//   the swapSpiBuf() functions out if you don't need them.
        ::swapSpiBuf();   // swap the pointers
        swapSpiBuf();     // tell _dmaRX the new buffer
    	
    	
        _dmaRX->enable();
    
        asm("dsb");
    
    }
    
    	
    //-------------------------------------------------------------------------
    void swapSpiBuf()
    //-------------------------------------------------------------------------
    { 
      _dmaRX->destinationBuffer((uint8_t*)spiBuf(), BufSize ); 
    }

  8. #8
    Quote Originally Posted by GordonRush View Post
    Here's a simple SPI slave that only receives data and only works on Teensy 4.0. It always receives the same number of bytes (the constant BufSize.) DMA is used to write to the destination buffer, and your sketch needs to define the function spiBuf() to return a pointer to the destination buffer. You can also declare swapSpiBuf() if you want to double-buffer the received data.

    This code is obviously hacked out of the Teensy SPI library.


    Just include SPISlave.h and call beginSlave() in your begin() function. Nothing is required in loop().



    The following is C:\Program Files (x86)\Arduino\hardware\teensy\avr\libraries\SPISla ve\SPISlave.h

    Code:
    /*
     * Copyright (c) 2010 by Cristian Maglie <c.maglie@bug.st>
     * Copyright (c) 2014 by Paul Stoffregen <paul@pjrc.com> (Transaction API)
     * Copyright (c) 2014 by Matthijs Kooijman <matthijs@stdin.nl> (SPISettings AVR)
     * SPI Master library for arduino.
     *
     * This file is free software; you can redistribute it and/or modify
     * it under the terms of either the GNU General Public License version 2
     * or the GNU Lesser General Public License version 2.1, both as
     * published by the Free Software Foundation.
     */
    
    #ifndef _SPI_SLAVE_H_INCLUDED
    #define _SPI_SLAVE_H_INCLUDED
    
    #include <Arduino.h>
    
    #if defined(__arm__) && defined(TEENSYDUINO)
    #if defined(__has_include) && __has_include(<EventResponder.h>)
    // SPI_HAS_TRANSFER_ASYNC - Defined to say that the SPI supports an ASYNC version
    // of the SPI_HAS_TRANSFER_BUF
    #define SPI_HAS_TRANSFER_ASYNC 1
    #include <DMAChannel.h>
    #include <EventResponder.h>
    #endif
    #endif
    
    // SPI_HAS_TRANSACTION means SPI has beginTransaction(), endTransaction(),
    // usingInterrupt(), and SPISetting(clock, bitOrder, dataMode)
    #define SPI_HAS_TRANSACTION 1
    
    // Uncomment this line to add detection of mismatched begin/end transactions.
    // A mismatch occurs if other libraries fail to use SPI.endTransaction() for
    // each SPI.beginTransaction().  Connect a LED to this pin.  The LED will turn
    // on if any mismatch is ever detected.
    //#define SPI_TRANSACTION_MISMATCH_LED 5
    
    // SPI_HAS_TRANSFER_BUF - is defined to signify that this library supports
    // a version of transfer which allows you to pass in both TX and RX buffer
    // pointers, either of which could be NULL
    #define SPI_HAS_TRANSFER_BUF 1
    
    
    #ifndef LSBFIRST
    #define LSBFIRST 0
    #endif
    #ifndef MSBFIRST
    #define MSBFIRST 1
    #endif
    
    #define SPI_MODE0 0x00
    #define SPI_MODE1 0x04
    #define SPI_MODE2 0x08
    #define SPI_MODE3 0x0C
    
    #define SPI_CLOCK_DIV4 0x00
    #define SPI_CLOCK_DIV16 0x01
    #define SPI_CLOCK_DIV64 0x02
    #define SPI_CLOCK_DIV128 0x03
    #define SPI_CLOCK_DIV2 0x04
    #define SPI_CLOCK_DIV8 0x05
    #define SPI_CLOCK_DIV32 0x06
    
    #define SPI_MODE_MASK 0x0C  // CPOL = bit 3, CPHA = bit 2 on SPCR
    #define SPI_CLOCK_MASK 0x03  // SPR1 = bit 1, SPR0 = bit 0 on SPCR
    #define SPI_2XCLOCK_MASK 0x01  // SPI2X = bit 0 on SPSR
    
    
    
    /**********************************************************/
    /*     32 bit Teensy 4.x                                  */
    /**********************************************************/
    
    #if defined(__arm__) && defined(TEENSYDUINO) && (defined(__IMXRT1052__) || defined(__IMXRT1062__))
    #define SPI_ATOMIC_VERSION 1
    
    void synchronousRead_isr(void);
    
    
    class DMABaseClass;
    
    
    void dumpDMA_TCD( DMABaseClass *dmabc );
    
    
    class SPISettings 
    {
    public:
    	SPISettings(uint32_t clockIn, uint8_t bitOrderIn, uint8_t dataModeIn) : _clock
    	(clockIn) 
    	{
    		init_AlwaysInline(bitOrderIn, dataModeIn);
    	}
    
    	SPISettings() : _clock(4000000) 
    	{
    		init_AlwaysInline(MSBFIRST, SPI_MODE0);
    	}
    private:
    	void init_AlwaysInline(uint8_t bitOrder, uint8_t dataMode)
    	  __attribute__((__always_inline__)) 
    	  {
    			tcr = LPSPI_TCR_FRAMESZ(7);    // TCR has polarity and bit order too
    
    			// handle LSB setup 
    			if (bitOrder == LSBFIRST) tcr |= LPSPI_TCR_LSBF;
    
    			// Handle Data Mode
    			if (dataMode & 0x08) tcr |= LPSPI_TCR_CPOL;
    
    			// Note: On T3.2 when we set CPHA it also updated the timing.  It moved the 
    			// PCS to SCK Delay Prescaler into the After SCK Delay Prescaler	
    			if (dataMode & 0x04) tcr |= LPSPI_TCR_CPHA; 
    	}
    
    	inline uint32_t clock() {return _clock;}
    
    	uint32_t _clock;
    	uint32_t tcr; // transmit command, pg 2955
    	friend class SPIClass;
    };
    
    
    
    class SPIClass // Teensy 4
    { 
    public:
    	static const uint8_t CNT_MISO_PINS = 1;
    	static const uint8_t CNT_MOSI_PINS = 1;
    	static const uint8_t CNT_SCK_PINS = 1;
    	static const uint8_t CNT_CS_PINS = 1;
    	
    	struct SPI_Hardware_t
    	{
    		volatile uint32_t &clock_gate_register;
    		const uint32_t clock_gate_mask;
    		uint8_t  tx_dma_channel;
    		uint8_t  rx_dma_channel;
    		void     (*dma_rxisr)();
    		const uint8_t  miso_pin[CNT_MISO_PINS];
    		const uint32_t  miso_mux[CNT_MISO_PINS];
    		const uint8_t  mosi_pin[CNT_MOSI_PINS];
    		const uint32_t  mosi_mux[CNT_MOSI_PINS];
    		const uint8_t  sck_pin[CNT_SCK_PINS];
    		const uint32_t  sck_mux[CNT_SCK_PINS];
    		const uint8_t  cs_pin[CNT_CS_PINS];
    		const uint32_t  cs_mux[CNT_CS_PINS];
    
    		volatile uint32_t &sck_select_input_register;
    		volatile uint32_t &sdi_select_input_register;
    		volatile uint32_t &sdo_select_input_register;
    		volatile uint32_t &pcs0_select_input_register;
    		const uint8_t sck_select_val;
    		const uint8_t sdi_select_val;
    		const uint8_t sdo_select_val;
    		const uint8_t pcs0_select_val;
    	};
    	
    	static const SPI_Hardware_t spiclass_lpspi4_hardware;
    	static const SPI_Hardware_t spiclass_lpspi3_hardware;
    	static const SPI_Hardware_t spiclass_lpspi1_hardware;
    
    
    public:
    	constexpr SPIClass(uintptr_t myport, uintptr_t myhardware)
    		: port_addr(myport), hardware_addr(myhardware) 
    	{
    	}
    
    	void beginSlave();
    
    	bool initDMA();
    	
    	void swapSpiBuf()
    	{ 
    	   _dmaRX->destinationBuffer((uint8_t*)spiBuf(), BufSize ); 
    	}
    	
    
    	friend void _spi_dma_rxISR0();
    	inline void dma_rxisr();
    
    
    public:
    	IMXRT_LPSPI_t & port() { return *(IMXRT_LPSPI_t *)port_addr; }
    
    private:
    	const SPI_Hardware_t & hardware() { return *(const SPI_Hardware_t *)hardware_addr; }
    	uintptr_t port_addr;
    	uintptr_t hardware_addr;
    
    	uint32_t _clock = 0;
    	uint32_t _ccr = 0;
    
    	uint8_t miso_pin_index = 0;
    	uint8_t mosi_pin_index = 0;
    	uint8_t sck_pin_index = 0;
    	
    	uint8_t interruptMasksUsed = 0;
    	uint32_t interruptMask[(NVIC_NUM_INTERRUPTS+31)/32] = {};
    	uint32_t interruptSave[(NVIC_NUM_INTERRUPTS+31)/32] = {};
    	
    
    	// DMA Support
    	bool initDMAChannels();
    	enum DMAState { notAllocated, idle, active, completed};
    	enum { MAX_DMA_COUNT = 32767 };
    	DMAState _dma_state = DMAState::notAllocated;
    	uint32_t _dma_count_remaining = 0;	// How many bytes left to output after current DMA completes
    	
    	DMAChannel *_dmaRX = nullptr;
    	EventResponder *_dma_event_responder = nullptr;
    
    };
    
    
    #endif
    
    
    extern SPIClass SPI;
    extern SPIClass SPI1;
    extern SPIClass SPI2;
    
    #endif

    The following is C:\Program Files (x86)\Arduino\hardware\teensy\avr\libraries\SPISla ve\SPISlave.cpp

    Code:
    /*
     *
     * Copyright (c) 2010 by Cristian Maglie <c.maglie@bug.st>
     * SPI Master library for arduino.
     *
     * This file is free software; you can redistribute it and/or modify
     * it under the terms of either the GNU General Public License version 2
     * or the GNU Lesser General Public License version 2.1, both as
     * published by the Free Software Foundation.
     */
    
    #include "SPISlave.h"
    #include "pins_arduino.h"
    
    #define DEBUG_DMA_TRANSFERS
    
    
    /**********************************************************/
    /*     32 bit Teensy 4.x                                  */
    /**********************************************************/
    
    
    void _spi_dma_rxISR0(void) {SPI.dma_rxisr();}
    
    const SPIClass::SPI_Hardware_t  SPIClass::spiclass_lpspi4_hardware = 
    {
    	CCM_CCGR1, CCM_CCGR1_LPSPI4(CCM_CCGR_ON),
    	DMAMUX_SOURCE_LPSPI4_TX, 
    	DMAMUX_SOURCE_LPSPI4_RX, 
    	_spi_dma_rxISR0,
    	12, 
    	3 | 0x10,
    	11,
    	3 | 0x10,
    	13,
    	3 | 0x10,
    	10,  // from https://forum.pjrc.com/threads/57328-Teensy-4-0-SPI-Chip-Select-pins If I remember correctly the one case, where you need to use the Hardware CS pin is if you implement an SPI Client setup, which I have not tried.
    
    	3 | 0x10,
    	IOMUXC_LPSPI4_SCK_SELECT_INPUT, 
    	IOMUXC_LPSPI4_SDI_SELECT_INPUT, 
    	IOMUXC_LPSPI4_SDO_SELECT_INPUT, 
    	IOMUXC_LPSPI4_PCS0_SELECT_INPUT,
    	0, 0, 0, 0
    };
    
    SPIClass SPI((uintptr_t)&IMXRT_LPSPI4_S, (uintptr_t)&SPIClass::spiclass_lpspi4_hardware);
    
    // sketch defines these functions.
    unsigned char *spiBuf();
    void swapSpiBuf();
    
    
    /*
    LPSPI slave mode uses the same shift register and logic as the master mode, but does not
    use the clock configuration register and the transmit command register must remain static
    during SPI bus transfers.
    */
    
    
    /*
    	clock_gate_register = CCM_CCGR1  // 13.7.22 p 1140  
    	clock_gate_mask = CCM_CCGR1_LPSPI4(CCM_CCGR_ON)   // CCM_CCGR_ON = 3
    	tx_dma_channel = DMAMUX_SOURCE_LPSPI4_TX  // 80
    	rx_dma_channel = DMAMUX_SOURCE_LPSPI4_RX  // 79
    	dma_rxisr = SPI.gdr_isr
    			
    	cs_mux[0] = 3 | 0x10
    
    	sck_select_input_register = IOMUXC_LPSPI4_SCK_SELECT_INPUT
    	sdi_select_input_register = IOMUXC_LPSPI4_SDI_SELECT_INPUT
    	sdo_select_input_register = IOMUXC_LPSPI4_SDO_SELECT_INPUT
    	pcs0_select_input_register = IOMUXC_LPSPI4_PCS0_SELECT_INPUT
    	
    	sck_select_val = 0
    	sdi_select_val = 0
    	sdo_select_val = 0
    	pcs0_select_val = 0
    */
    
    static const int rxWater = 15;
    //-----------------------------------------------------------------------------
    void SPIClass::beginSlave()
    //-----------------------------------------------------------------------------
    {
    //	Serial.printf( "clock_gate_register %08x\n", hardware().clock_gate_register );
    //	Serial.printf( "clock_gate_mask %08x\n", hardware().clock_gate_mask );
    
        // turn off the LPSPI4 clock ( CCGR1 )
    	// this is really CCM_CBCMR &= ~CCM_CCGR1_LPSPI4(CCM_CCGR_ON);
    	hardware().clock_gate_register &= ~hardware().clock_gate_mask;
    
    
    	Serial.printf( "SPI MISO: %d MOSI: %d, SCK: %d CS: %d\n", 
                       hardware().miso_pin[0], 
                       hardware().mosi_pin[0], 
                       hardware().sck_pin[0], 
                       hardware().cs_pin[0] );
    
    
        // CCM = Clock control module
    	//Serial.printf( "before CBCMR = %08lX CCGR1 = %08lX\n", CCM_CBCMR, CCM_CCGR1 ); // CCM Bus Clock Multiplexer Register
    
        // IOMUXC = IOMUX Controller
    	// SRE = slew rate fast
    	// DSE(3) = Drive strength R0/5
    	// SPEED(3) = max(200MHz)	
    
    	uint32_t fastio = IOMUXC_PAD_DSE(6) | IOMUXC_PAD_SPEED(1);
    	//uint32_t fastio = IOMUXC_PAD_SRE | IOMUXC_PAD_DSE(3) | IOMUXC_PAD_SPEED(3);
    
    	*(portControlRegister(hardware().miso_pin[0])) = fastio;
    	*(portControlRegister(hardware().mosi_pin[0])) = fastio;
    	*(portControlRegister(hardware().sck_pin[0])) = fastio;
    
    
    //	*(portControlRegister(16) = fastio;
    //	*(portControlRegister(17) = fastio;
    //	*(portControlRegister(19) = fastio;
    
    
        // turn on the LPSPI4 clock ( CCGR1 )
    	// this is really CCM_CBCMR |= CCM_CCGR1_LPSPI4(CCM_CCGR_ON);
    	hardware().clock_gate_register |= hardware().clock_gate_mask;
    
    	Serial.printf( "after CBCMR = %08lX CCGR1 = %08lX\n", CCM_CBCMR, CCM_CCGR1 ); // CCM Bus Clock Multiplexer Register
    
    
    	*(portConfigRegister(hardware().miso_pin[0])) 
    	   = hardware().miso_mux[0];
    	*(portConfigRegister(hardware().mosi_pin[0])) 
    	   = hardware().mosi_mux[0];
    	*(portConfigRegister(hardware().sck_pin[0])) 
    	   = hardware().sck_mux[0];
    
    	// why do I need this in slave mode when master mode doesn't do it?
    	*(portConfigRegister(hardware().cs_pin[0])) 
    	   = hardware().cs_mux[0];  
    
    
    //	*(portConfigRegister(16) = IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B1_07;
    //	*(portConfigRegister(17) = IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B1_06;
    //	*(portConfigRegister(19) = IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B1_00;
    
    
    	// Set the Mux pins 
       	hardware().sck_select_input_register = hardware().sck_select_val;
    	hardware().sdi_select_input_register = hardware().sdi_select_val;
    	hardware().sdo_select_input_register = hardware().sdo_select_val;
    
        // why do I need this in slave mode when master mode doesn't do it?
    	hardware().pcs0_select_input_register = hardware().pcs0_select_val;  
    	   
    	// CR  = Control register
    	// RST = Software reset
    	port().CR = LPSPI_CR_RST;
    
    	// Initialize the Receive FIFO watermark to FIFO size - 1... 
    	port().FCR = LPSPI_FCR_RXWATER( rxWater );
    	
    	port().CR = LPSPI_CR_MEN;     // Module Enable
    
    	
    	initDMA();
    	
    
    	Serial.printf( "PARAM %x CR %x SR %x IER %x DER %x CFGR0 %x CFGR1 %x FCR %x TCR %x\n", 
    	               port().PARAM,
    	               port().CR, port().SR, 
    				   port().IER, port().DER,
    				   port().CFGR0, port().CFGR1, 
    				   port().FCR, port().TCR );	
    }
    
    
    static int const BufSize = 3000;
    
    
    //-----------------------------------------------------------------------------
    bool SPIClass::initDMAChannels() 
    //-----------------------------------------------------------------------------
    {
    	_dmaRX = new DMAChannel();
    
    	if (_dmaRX == nullptr)
    		return false;
    
    Serial.printf( "rx_dma_channel = %d\n", hardware().rx_dma_channel );
    
    	// Set up the RX chain
    	_dmaRX->disable();
    
    	_dmaRX->source( (volatile uint8_t&)port().RDR );
    /*
    Disable Request
    If this flag is set, the eDMA hardware automatically clears the corresponding ERQ bit when the current
    major iteration count reaches zero.
    0b - The channel's ERQ bit is not affected.
    1b - The channel's ERQ bit is cleared when the major loop is complete.
    */
    	_dmaRX->disableOnCompletion();   // TCD->CSR |= DMA_TCD_CSR_DREQ;
    	_dmaRX->triggerAtHardwareEvent( hardware().rx_dma_channel );
    	_dmaRX->attachInterrupt( hardware().dma_rxisr );
    
    /*
    Enable an interrupt when major iteration count completes.
    If this flag is set, the channel generates an interrupt request by setting the appropriate bit in the INT when
    the current major iteration count reaches zero.
    0b - The end-of-major loop interrupt is disabled.
    1b - The end-of-major loop interrupt is enabled.
    */
    
        // cause the dma_rxisr() to be called at the end of the transfer
    	_dmaRX->interruptAtCompletion();  // TCD->CSR |= DMA_TCD_CSR_INTMAJOR;
    	
    	_dma_state = DMAState::idle;  // Should be first thing set!
    	
    	_dmaRX->enable(); 
    	
    	return true;
    }
    
    
    EventResponder _event_responder;
    
    #define DATA_BUFFER_MAX (16*1024)
    
    //-----------------------------------------------------------------------------
    bool SPIClass::initDMA()
    //-----------------------------------------------------------------------------
    {	
    	initDMAChannels();
    	
    	
        if (_dma_state == DMAState::active)
    		return false; // already active
    
    	_event_responder.clearEvent();	// Make sure it is not set yet
    	
    	_dma_count_remaining = 0;
    	
    	_dmaRX->TCD->ATTR_SRC = 0;		// Make sure set for 8 bit mode...
    	_dmaRX->destinationBuffer((uint8_t*)spiBuf(), BufSize );
    	_dmaRX->TCD->DLASTSGA = -BufSize;
    
    	if( (uint32_t)spiBuf() >= 0x20200000u )  
    		arm_dcache_delete( spiBuf(), BufSize );
    	
    	_dma_event_responder = &_event_responder;
    
    	dumpDMA_TCD( _dmaRX );
    
    	// Make sure port is in 8 bit mode and clear watermark
    	port().TCR = (port().TCR & ~(LPSPI_TCR_FRAMESZ(31))) | LPSPI_TCR_FRAMESZ(7);	
    	port().FCR = 0; 
    
    	// Lets try to output the first byte to make sure that we are in 8 bit mode...
     	port().DER = LPSPI_DER_RDDE;	//enable DMA on RX
    	port().SR = 0x3f00;	// clear out all of the other status...
    
    	_dmaRX->enable();
    
    	_dma_state = DMAState::active;
    	
    	return true;
    }
    
    
    
    //-----------------------------------------------------------------------------
    inline void DMAChanneltransferCount( DMAChannel *dmac, unsigned int len ) 
    //-----------------------------------------------------------------------------
    {
    	// note does no validation of length...
    	DMABaseClass::TCD_t *tcd = dmac->TCD;
    	
    	if (!(tcd->BITER & DMA_TCD_BITER_ELINK)) 
    	{
    		tcd->BITER = len & 0x7fff;
    	} 
    	else 
    	{
    		tcd->BITER = (tcd->BITER & 0xFE00) | (len & 0x1ff);
    	}
    	tcd->CITER = tcd->BITER; 
    }
    
    //-----------------------------------------------------------------------------
    void dumpDMA_TCD( DMABaseClass *dmabc )
    //-----------------------------------------------------------------------------
    {
    	Serial.printf("spiData = %p BC: %x TCD: %x:", spiBuf(), (uint32_t)dmabc, (uint32_t)dmabc->TCD);
    
    	Serial.printf("SA:%x SO:%d AT:%x NB:%x SL:%d DA:%x DO:%d CI:%x DL:%x CS:%x BI:%x\n", (uint32_t)dmabc->TCD->SADDR,
    		dmabc->TCD->SOFF, dmabc->TCD->ATTR, dmabc->TCD->NBYTES, dmabc->TCD->SLAST, (uint32_t)dmabc->TCD->DADDR, 
    		dmabc->TCD->DOFF, dmabc->TCD->CITER, dmabc->TCD->DLASTSGA, dmabc->TCD->CSR, dmabc->TCD->BITER);
    }
    
    
    //-------------------------------------------------------------------------
    void SPIClass::dma_rxisr() 
    //-------------------------------------------------------------------------
    {
    	_dmaRX->clearInterrupt();   // 		DMA_CINT = channel;
    	_dmaRX->clearComplete();    // 		DMA_CDNE = channel;
    	
    	// receive a full SPI buffer
        DMAChanneltransferCount( _dmaRX, BufSize );
    
        // Optionally allow the user to double buffer the data.  Comment 
    	//   the swapSpiBuf() functions out if you don't need them.
        ::swapSpiBuf();   // swap the pointers
        swapSpiBuf();     // tell _dmaRX the new buffer
    	
    	
        _dmaRX->enable();
    
        asm("dsb");
    
    }
    
    	
    //-------------------------------------------------------------------------
    void swapSpiBuf()
    //-------------------------------------------------------------------------
    { 
      _dmaRX->destinationBuffer((uint8_t*)spiBuf(), BufSize ); 
    }
    Well this might be JUST what I was looking for to get a framebuffer snarf from a sensor, was trying to manually port from the vendor provided SAM3XE code and bashing my head on desk while I tried to grok which registers to use.

  9. #9
    Junior Member
    Join Date
    Oct 2019
    Posts
    5
    Cool stuff thanks! By the way is there any example sketch? I know it would be simple but I'm having a hard time figuring it out.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •