Max speed SPI

Hi,

How can we achieve maximum SPI speed without using DMA?

If we change (in SPI.h, line 57-58)

  // Default speed set to 4MHz, SPI mode set to MODE 0 and Bit order set to MSB first.
  SPISettings() { init_AlwaysInline(4000000, MSBFIRST, SPI_MODE0); }

to

  // Default speed set to 12MHz, SPI mode set to MODE 0 and Bit order set to MSB first.
  SPISettings() { init_AlwaysInline(12000000, MSBFIRST, SPI_MODE0); }

and then at the beginning of the code write

SPI.setClockDivider(4);

are we reaching the max speed?

Or does the setClockDivider function do the same as the changing the 4MHz default to 12MHz?

setClockDivider should not be used at all.

Use SPI.beginTransaction() to set the speed. Details here:

You should use the maximum rated speed of whatever chip you're connecting. If the chip is rated for 20 MHz, use this:

SPI.beginTransaction(SPISettings(20000000, MSBFIRST, SPI_MODE0))

The beginTransaction code inside SPI will automatically use Arduino Zero's fastest 12 MHz. The advantage of this new approach is your code will automatically use 16.8 MHz when compiled on Arduino Due, since that's Due's best speed not more than 20 MHz. If compiled on a Teensy 3.2, it'll automatically be 16 MHz. On regular AVR boards, it'll use 8 MHz (or whatever speed it possible... this also proper scales if your AVR board has a different F_CPU speed).

The point is you embed the number 20000000, representing the actual maximum speed your other chip can use. As more types of high speed boards appear in the coming years, your code will automatically adapt to use their fastest speeds which don't exceed the 20 MHz rating.

The other important feature of this approach is you do NOT configure the speed in setup().

Configuring speed just once results in code that can't be combined with other SPI-based code and libraries. Even if you only intend to use this 1 SPI chip in this particular project, when you later build another project with this chip and other SPI parts, or if you ever share this code with other people who try to use it with other SPI chips, the beginTransaction() functions guarantee each SPI chip always gets proper settings when it needs to use the SPI bus.

SPI is meant to be a bus for multiple chips, and Arduino is meant to be a platform for developing and sharing code within a large community. The old configure-only-once approach resulted in tons of code that wasn't portable between different chips and couldn't be shared and combined with other code.

It's time to leave the old, obsolete idea of configuring SPI settings just once in the past. Use the new functions. They're much better.

Hi Paul,

Thanks for you quick answer.

The library I'm working with doesn't use the SPI.beginTransaction(...). It only use SPI.transfer(...).

It's the Adafruit lib for SSD1351 Oled display : GitHub - adafruit/Adafruit-SSD1351-library: Adafruit library for the 1.27" and 1.5" color OLEDs in the shop.

How should I modifiy the code to add this function?

Hi AloyseTech,

I got the Adafruit SSD1306 library working with the Zero, it's probably quite similar to the SSD1351.

In the "Adafruit_SSD1306.cpp" file I changed the following:

In the includes at the top, I added:

#ifndef __arm__
 #include <util/delay.h>
#endif

In the Adafruit_SSD1306::begin() function I changed the set clock divider section to:

#ifdef __SAM3X8E__
      SPI.setClockDivider (9); // 9.3 MHz
#elif __SAMD21G18A__
  SPI.setClockDivider (6); // 8 MHz
#else
      SPI.setClockDivider (SPI_CLOCK_DIV2); // 8 MHz
#endif

Finally I added the following #ifndefs in the Adafruit_SSD1306::ssd1306_data() function to remove the AVR TWBR register definition for the ARM processors:

Midway in the function:

#ifndef __arm__
    uint8_t twbrbackup = TWBR;
    TWBR = 12; // upgrade to 400KHz!
#endif

and at the bottom of the same function:

#ifndef __arm__
    TWBR = twbrbackup;
#endif

Hope this helps?

Adafruit do update their libraries, their latest version may include support for the Arduino Zero? It might be worth checking first.

Hi Martin,

I have been able to make the library work for the zero, what in trying to do is to improve the speed of the display.

At the moment I use the setClockDivider(4) function. I was just wondering if changing the settings as well will increase the speed again or if it is just a different way to change the clock
speed.

I would love to implement DMA but it seems to be a bit difficult...

Anyway, thanks both for your answers :slight_smile:

The SSD1351 datasheet gives the minimum SPI clock period as 50ns, or in other words a maximum frequency of 20MHz.

Dividing the Zero's 48MHz clock by 4 gives 12MHz. I don't know if dividing by 3 to give 16MHz might work?

I'll try that but I think I've read somewhere that 12MHz is the absolute max for the SAMD21.

I will give a try to DMA in the future.

Even at 12MHz, SPI.transfer() runs at less than 3 mbs (megabits/sec), as noted in thread
https://forum.arduino.cc/index.php?topic=344029.0
DMA at 12MHz achieves 11.8mbs

If you are willing to bypass the layers of slow C++ stuff, you can get nearly 12Mb/S without using DMA for write-only operations.

This code writes 128x128x3 bytes to an LCD in ~33mS . . . 11.9Mb/S out of a theoretical 12Mb/S

The trick is to wait for the REG_SERCOM4_SPI_INTFLAG DRE bit, which gives you an entire 600nS while the previous byte clocks out to do your data and loop overhead.

It took some digging to find that the Adafruit M0 Adalogger is using SERCOM4 and that DRE is INTFLAG.0x01, but once you know those two bits, it works quite easily.

// 128x128x3*8 bits / 33mS = 11.9Mbits/S
void Fill_LCD(uint8_t R, uint8_t G, uint8_t B)
  {
  register int
    i;
  Set_LCD_for_write_at_X_Y(0, 0);

  //Fill display with a given RGB value
  SET_RS;
  // Select the LCD controller
  CLR_CS;
  for (i = 0; i < (128 * 128); i++)
    {
    //There is a 1-byte buffer in front of the
    //the SPI transmit shift register. If that
    //register is empty, we can write.
    //Speedup: write directly to the register
    while(0==(REG_SERCOM4_SPI_INTFLAG&0x1))  //DRE
      {
      //wait;
      }
    REG_SERCOM4_SPI_DATA=B;
    while(0==(REG_SERCOM4_SPI_INTFLAG&0x1))  //DRE
      {
      //wait;
      }
 
    REG_SERCOM4_SPI_DATA=G;
    while(0==(REG_SERCOM4_SPI_INTFLAG&0x1))  //DRE
      {
      //wait;
      }
    REG_SERCOM4_SPI_DATA=R;
     }
  SET_CS;
  }