Teensy3.5 SPI RX using DMA

Status
Not open for further replies.

badsector

Well-known member
So I want to use a Teensy3.5 as I/O server to the main CPU. I have already found libraries to implement PS2 mouse and keyboard, albeit not without several problems (such as the mouse not initialising). I want to use the SDcard to emulate a 720k floppy disk, by putting a floppy image file on the normal SD file structure. My intention is to transfer the data using SPI, but in order to simplify things I will have 2 sets of SPI connections, one for CPU to Teensy and another for Teensy to CPU. Yes I know this is very wasteful because SPI can already do bi-directional transfer, but I really want to make implementation as easy as possible, even if it costs a few pins. The Teensy has several sets of SPI ports so this is of no real consequence.

Transfers from the CPU to the Teensy are in sets of 16 bit SPI packets, with a single SS which does not toggle between packets. Overall number of SPI packets will vary, could be 8 words for a command and 256 words for a disk sector transfer.

I was hoping to find a SPI RX library, but the standard Teensy one is only TX. There do appear to be user written libraries, but I couldn't get my head around them.

So in order to use the power of the Teensy3.5 it appeared that it might be possible to use interrupts (on each SPI packet) or DMA. I found code on the forum and played around with it, and it took quite some effort to get packets to be received, even though the transmitted data was very clean. I think mainly it was figuring out which registers to initialise.

I couldn't figure out the syntax for interrupts after each SPI packet so I investigated using DMA. Again I used code found in this forum. I noted the use of DMA transfer (DMAchannel) but found the documentation lacking. After quite a few hours of looking at the library code it started to make sense.

I am posting my code here, which is HEAVILY derived from others code (sorry I can't recall who - so my apologies in advance!). I hope it might be of use to anyone else considering SPI receive using DMA.

This is an extract of the critical parts from the full sketch. I used an OLED for debug. It probably won't compile as is.

Code:
// migry_tech
//
// 12-Dec-2019 : Update to accept packet of data.
//

// ******** WARNING - STRIPPED DOWN FOR POSTING - NOT TESTED AS IS **********

#include <Arduino.h>
#include "mk20dx128.h"
#include "core_pins.h"

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <SPI.h>

#define OLED_RESET -1
Adafruit_SSD1306 oled(OLED_RESET);

#define LINE1  0
#define LINE2 12
#define LINE3 24
#define LINE4 36
#define LINE5 48

#define SPI_SSINT  32 // used by SPI hardware
#define SPI_DEBUG  31

#define LED 13
int ledstate=0;

DMAChannel *dmaSPI0rx;

//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
//GLOBAL VARIABLES

char sbuf[128];

volatile byte SSint=0;
volatile byte SSintrise=0;
volatile byte rise=0;
volatile byte fall=0;
volatile uint16_t spibuffer[2000];
volatile uint16_t junk;
volatile int      spicount=0;

//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
void dumpDMA_TCD( DMABaseClass *dmabc )
{
  Serial.printf("SPI buffer = %08p\n",spibuffer);
	Serial.printf("BC:   %08x\nTCD:  %08x\n", (uint32_t)dmabc, (uint32_t)dmabc->TCD);
	Serial.printf("SADDR:%08x\n", (uint32_t)dmabc->TCD->SADDR);
  Serial.printf("SLAST:%08x\n"  , dmabc->TCD->SLAST);
  Serial.printf("SOFF: %d\n"  , dmabc->TCD->SOFF);
  Serial.printf("SATTR:%02x\n", (dmabc->TCD->ATTR)>>8);
  Serial.print("\n");
  Serial.printf("DADDR:%08x\n", (uint32_t)dmabc->TCD->DADDR);
  Serial.printf("DLAST:%08x\n", dmabc->TCD->DLASTSGA);
  Serial.printf("DOFF: %d\n"  , dmabc->TCD->DOFF);
  Serial.printf("DATTR:%02x\n", (dmabc->TCD->ATTR)&0xff);
  Serial.print("\n");
  Serial.printf("CSR:  %08x\n", dmabc->TCD->CSR);
  Serial.printf("NBYTE:%08x\n", dmabc->TCD->NBYTES);
  Serial.printf("CITER:%08x\n", dmabc->TCD->CITER);
  Serial.printf("BITER:%08x\n", dmabc->TCD->BITER);               
  Serial.print("\n");
  Serial.print("\n");
}

//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
void TSPI_SS_Change(void)
{
  digitalWrite(SPI_DEBUG,HIGH); //connect to scope
  if (digitalRead(SPI_SSINT)==HIGH) {

    // RISING EDGE OF SS 
    SSint = 0x81;
    rise++;
    SPI0_MCR |= SPI_MCR_HALT | SPI_MCR_MDIS; // Stop SPI
    dmaSPI0rx->disable(); 
    SSintrise=1; 
  } else {

    // FALLING EDGE OF SS
    SSint = 0x01;
    fall++;
    // must always reset DAADR to start of SPI buffer since we are receiving a variable number of SPI words
    dmaSPI0rx->destinationBuffer(spibuffer,600); // sets BITER and CITER to half this value (for 16 bit transfer)
    dmaSPI0rx->transferCount(512);               // sets BITER and CITER to this value (no of 16 bit transfers)
    dmaSPI0rx->TCD->DLASTSGA=0;
    dmaSPI0rx->triggerAtHardwareEvent(DMAMUX_SOURCE_SPI0_RX);
    SPI0_MCR &= ~SPI_MCR_HALT & ~SPI_MCR_MDIS; //Re-start SPI
    dmaSPI0rx->enable(); 

  }
  digitalWrite(SPI_DEBUG,LOW); //connect to scope
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//

void TSPIPrintConfig(void) 
{
  uint32_t CTAR=0;
  
  Serial.println("SPIO STATUS");
  Serial.println();
  Serial.println("                 33222222222211111111110000000000");
  Serial.println("                 10987654321098765432109876543210");
  Serial.println("                 --------------------------------");
  Serial.print("SPIO_MCR:        ");
 
  for (unsigned int mask = 0x80000000; mask; mask >>= 1) 
  {
    Serial.print(mask&SPI0_MCR?'1':'0');
  }

  Serial.println();
  Serial.print("SPIO_CTAR_SLAVE: ");
  CTAR = SPI0_CTAR0_SLAVE;

  for (unsigned int mask = 0x80000000; mask; mask >>= 1) { Serial.print(mask&CTAR?'1':'0'); }
      
  Serial.println();
  Serial.print("SPI0_SR:         ");
  for (unsigned int mask = 0x80000000; mask; mask >>= 1) { Serial.print(mask&SPI0_SR?'1':'0'); }
  Serial.println(" "); 
  Serial.print("TCF  ="); Serial.println((SPI0_SR&0x80000000)?"transfer complete":"transfer not complete"); 
  Serial.print("RFOF ="); Serial.println((SPI0_SR&0x00080000)?"RX FIFO overflow ":"RX FIFO no overflow  "); 
  Serial.print("RFDF ="); Serial.println((SPI0_SR&0x00020000)?"RX FIFO not empty":"RX FIFO empty        "); 
  Serial.print("RXCTR="); Serial.println((SPI0_SR&0x00000040)>>4,DEC); 
    
  Serial.println();
  Serial.print("SPI0_RSER:       ");
  for (unsigned int mask = 0x80000000; mask; mask >>= 1) { Serial.print(mask&SPI0_RSER?'1':'0'); }
  Serial.println(); 
  Serial.println(); 
  Serial.println();   
}

void TSPIconfig(void)
{
  SIM_SCGC6 |= SIM_SCGC6_SPI0;  // enable clock to SPI.

  // SPI stop
  SPI0_MCR |= SPI_MCR_HALT | SPI_MCR_MDIS;

  CORE_PIN13_CONFIG = PORT_PCR_MUX(0); // de-assign SCK
  CORE_PIN11_CONFIG = PORT_PCR_MUX(0);
  CORE_PIN12_CONFIG = PORT_PCR_MUX(0);
  //CORE_PIN10_CONFIG = PORT_PCR_MUX(2);

  CORE_PIN2_CONFIG  =                PORT_PCR_MUX(2); // SS   = pin 2
  CORE_PIN8_CONFIG  =                PORT_PCR_MUX(2); // SIN  = pin 8
  CORE_PIN7_CONFIG  = PORT_PCR_DSE | PORT_PCR_MUX(2); // SOUT = pin 7
  CORE_PIN14_CONFIG = PORT_PCR_DSE | PORT_PCR_MUX(2); // SCK  = pin 14
 
   // Slave mode
  SPI0_MCR &= ~SPI_MCR_MSTR;
  
  SPI0_MCR |= SPI_MCR_PCSIS(0x1F);

  // SPI0_CTAR0_SLAVE config
  SPI0_CTAR0_SLAVE = 0x78000000; // 16 bit transfer
  //  same as SPI0_CTAR0_SLAVE = SPI_CTAR_FMSZ(15); where  #define SPI_CTAR_FMSZ(n)             (((n) & 15) << 27)   
  // Clock Polarity
  SPI0_CTAR0_SLAVE &= ~SPI_CTAR_CPOL; // inactive state of SCK is '0'
  // Clock Phase
  SPI0_CTAR0_SLAVE &= ~SPI_CTAR_CPHA; // data is captured on the leading edge of SCK

  // DMA/Interrupt Request Select and Enable Register
  //          RFDF_RE   (Receive FIFO Drain Request Enable)                  1=>RFDF Int or DMA are enabled
  //          RFDF_DIRS (Receive FIFO Drain DMA or Interrupt Request Select) 1=>DMA Request
  SPI0_RSER = SPI_RSER_RFDF_RE | SPI_RSER_RFDF_DIRS;

  while ((SPI0_SR&0xf0)!=0) { volatile uint16_t pop = SPI0_POPR; }
  //SPI start
  SPI0_MCR |= SPI_MCR_DIS_TXF; // Disable TX FIFO
  SPI0_MCR |= SPI_MCR_DIS_RXF; // Disable RX FIFO 
  SPI0_MCR |= SPI_MCR_CLR_RXF; // Clear RX FIFO
  SPI0_MCR |= SPI_MCR_CLR_TXF; // Clear TX FIFO

  while(!Serial) { }
  TSPIPrintConfig();
}

//INITIALIZAITON
void setup() {
  pinMode(LED,OUTPUT);
  digitalWrite(LED,HIGH);
  Serial.begin(9600); //debug
  
  oled.begin ();
  oled.setTextSize(1);
  oled.setTextColor(1);

  pinMode(SPI_DEBUG,OUTPUT); // used to detect SPI SS changes
  digitalWrite(SPI_DEBUG,LOW);
  pinMode(SPI_SSINT,INPUT_PULLUP); // used to detect SPI SS changes
  attachInterrupt(SPI_SSINT,TSPI_SS_Change,CHANGE);

  // Enable FIFO Drain Request DMA
  dmaSPI0rx = new DMAChannel();
  dmaSPI0rx->source((volatile uint16_t&) SPI0_POPR);
  dmaSPI0rx->destinationBuffer(spibuffer,600); // sets BITER and CITER to half this value (for 16 bit transfer)
  dmaSPI0rx->transferCount(512);               // sets BITER and CITER to this value (no of 16 bit transfers)
  dmaSPI0rx->TCD->DLASTSGA=0;                  // I want to see where the last DMA transfer went to
  dmaSPI0rx->triggerAtHardwareEvent(DMAMUX_SOURCE_SPI0_RX);
  //dmaSPI0rx->interruptAtCompletion(); //not sure how to use this
  //dmaSPI0rx->attachInterrupt(func()); //think this might be needed?
}


void loop() {
    oled.clearDisplay();
    if (SSint!=0) {
      SSint=0;
      if (SSintrise==1) { 
        dumpDMA_TCD( dmaSPI0rx ); 
        SSintrise=0; 
      }
    }

    // both calculations give the number of bytes received.
    uint16_t bt=2*(0x200-(dmaSPI0rx->TCD->CITER));
    uint32_t of=(uint32_t)(dmaSPI0rx->TCD->DADDR)-(uint32_t)spibuffer;

    oled.setCursor(0,LINE1);
    sprintf(sbuf,"%08x %4d bytes",of, bt);
    oled.print(sbuf);

    if (rise>0) {
      oled.setCursor(0,LINE2);
      sprintf(sbuf,"SPI rising %3d",rise);
      oled.print(sbuf);
    }
    if (fall>0) {
      oled.setCursor(0,LINE3);
      sprintf(sbuf,"SPI falling %3d",fall);
      oled.print(sbuf);
    }
    int i;
    oled.setCursor(0,LINE4);
    for (i=0;i<8;i++) { 
      sprintf(sbuf,"%04x ",spibuffer[i]);
      oled.print(sbuf);
    }
    oled.display();
    delay(1000);
}

I found out that I could get an interrupt on the rising and falling edge of the SS (SPI Select) which is inactive high.
On the falling edge I set up the DMA ready for receiving the SPI packets. This entails resetting the DADDR DMA register to point to the start of the SPI buffer.
I program BITER too big so that I can figure out the actual number of words received.
On the rising edge of SS, I disable the DMA (so I don't overrun the buffer!), and set some flags dealt with in the main loop.
I can both look at CITER (which counts down from the BITER reload value) to determine the number of DMA word transfers done or I can use the value of the DMA write address (DADDR) to calculate the same thing.

Code:
    // both calculations give the number of bytes received.
    uint16_t bt=2*(0x200-(dmaSPI0rx->TCD->CITER));
    uint32_t of=(uint32_t)(dmaSPI0rx->TCD->DADDR)-(uint32_t)spibuffer;

How to change which SPI pins are used. This took me some time to figure out. PORT_PCR_MUX(0) sort of means this pin is not assigned to any internal hardware.

Code:
  CORE_PIN13_CONFIG = PORT_PCR_MUX(0); // de-assign SCK
  CORE_PIN11_CONFIG = PORT_PCR_MUX(0); //de-assign MOSI0
  CORE_PIN12_CONFIG = PORT_PCR_MUX(0); //de-assisn MISO0

  CORE_PIN2_CONFIG  =                PORT_PCR_MUX(2); // SS   = pin 2
  CORE_PIN8_CONFIG  =                PORT_PCR_MUX(2); // SIN  = pin 8
  CORE_PIN7_CONFIG  = PORT_PCR_DSE | PORT_PCR_MUX(2); // SOUT = pin 7
  CORE_PIN14_CONFIG = PORT_PCR_DSE | PORT_PCR_MUX(2); // SCK  = pin 14

Here is a scope trace. The yellow trace is SS. The little blips on D7 on the falling and rising edges indicate when the interrupt code is running. It runs for only less than 2us! In the captured example I am transmitting 3 SPI packets. I needed to confirm that I had set up DMA well in advance of the first packet arriving.

DS2_QuickPrint2.png

I hope this helps. Thanks to those who posted their own code, without which this would not be possible.

regards...
--migry/badsector
 
Status
Not open for further replies.
Back
Top