Hardware vs bit banged SPI in Teensy 4.x

Sudo

Member
Forgive me if this is an obvious question, but given the speed of a Teensy 4, I wonder if there is a significant performance difference between using hardware SPI vs big-banged SPI. I get that in an 8-bit AVR microcontroller using hardware accelerated SPI could make a huge difference, but in a 600 MHz beast I am not so sure.

Can you tell me what is the maximum transfer rate of SPI in a Teensy 4:
  • Using the hardware-accelerated SPI pins and the the <SPI.h> library?
  • Manually bit-banging generic digital output pins?
The bit-banging code I am thinking of for simple Master-to-Slave communication would be something like this:

C:
uint8_t spi_delay = 10;      // SPI clock delay in microseconds

void tick() {
    delayMicroseconds (spi_delay);
}

void spi_bitBangByte(uint8_t data) {   
    // Iterate the 8 bits
    for(i = 0; i < 8; i++) {
        // Lower clock signal and set the data bit (most significant)
        digitalWrite (SCK_PIN, 0);
        if(data&0x80)
            digitalWrite (MOSI_PIN, 1);
        else
            digitalWrite (MOSI_PIN, 0); 
        // Wait a clock cycle
        tick();   
        // Raise the clock signal to trigger sampling (SPI Mode 0)
        DEV_Digital_Write(SCK_PIN, 1);           
        // Wait a clock cycle
        tick();
        // Bitshift to the left
        data = data << 1;
    }
}
 
Why bit bang if you have hardware support with DMA?

Im sure someone can answer it, but what is the reason behind the question?
 
I want to port some Waveshare ePaper libraries to Teensy 4. Waveshare has example drivers for ESP32 and Arduino. I see they bit-bang the SPI communication instead of using the SPI library. But their implementation looks strange to me, as they use some delays that the protocol apparently does not specify. For example, they pause after resetting SCLK and before setting the MOSI pin. Also, they use a double delay after setting SCLK. I wonder if their hardware uses some non-standard version of SPI and that's why their code looks strange. Or maybe it is just sloppy coding. Or maybe I am not understanding SPI correctly.

My initial idea was to modify their code as little as possible, but actually I'll start by using <SPI.h> and if that does not work I will switch back to their code.

I am still curious if using hardware SPI makes sense performance-wise in modern fast microcontrollers.

For reference, this is the SPI bit-bang code included in the Waveshare GitHub Library I mentioned earlier. I commented the two extra clock delays that I find out of place. If someone can make sense of this code, I would be glad to know:

C:
void DEV_SPI_WriteByte(uint8_t data) {

    DEV_Digital_Write(EPD_SCK_PIN, 0);
    for(i = 0; i < 8; i++) {
        DEV_Delay_us(10);
        DEV_Digital_Write(EPD_SCK_PIN, 0);
        DEV_Delay_us(10);    // <-- Is this delay necessary?
        if(data&0x80)
            DEV_Digital_Write(EPD_MOSI_PIN, 1);
        else
            DEV_Digital_Write(EPD_MOSI_PIN, 0);
        data = data << 1;
        DEV_Delay_us(10);
        DEV_Digital_Write(EPD_SCK_PIN, 1);
        DEV_Delay_us(10);   // <-- Is this delay necessary? There is another at the begining of the  loop
    }
}
 
Last edited:
Using hardware SPI is generally better to match the timing requirements, with the Teensy being so fast it's very easy to miss inserting a manual delay somewhere (for example the code you posted doesn't show the timing of CS which typically has a minimum inactive period between transfers, setup time before the first clock edge, etc).
 
For any IO, you really want to keep the CPU out of the loop as much as possible so it can do other stuff (regardless of the 600Mhz clock speed). If your project is simple enough, go ahead and use bit-banging, but it still wastes CPU time. From worst to best, bit-banging is least preferred, but it can help with diagnosis. Polling comes next, followed by interrupt-driven I/O. DMA is the lightest touch, but it requires a deeper understanding of how DMA is implemented on the iMXRT1062. I have used bit-banging to help diagnose clock and data timing. Those delays might be necessary, but they're probably related to the clock and data setup times on the peripheral you're talking to. Ultimately, the peripheral you're communicating with will have a maximum bit rate/bit time and the clock and data polarity it expects (mode). Start well below that maximum speed on the SPI peripheral you're using, and aim to use the built-in SPI peripheral interface, even in polling mode, and you will be better off.
Here is some Arduino code I wrote several years ago that writes to three 7 segment displays.
Code:
// 7-Segment display driver using SPI
// XC3714 driver.ino
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <math.h>
#include "XC3714_driver.h"
T_seven_seg_errors seven_seg_errors;  // trapping and recording any seven segment display errors
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
// When we drive the 7 segment display, our first job is to format a buffer of digits from the floating point or integer argument "num".
// When our lowset digit measures whole microns, there can't be more than 3 digits to the right of the decimal point (thousandths of a millimeter, single microns).
// There are up to four significant digits to the left of the decimal point. 9999.999 meters at a resolution of one micron. (or approx ten thousand millimeters)
// There will be a blank digit to the left of the significant digits that will optionally display a minus sign, for negative numbers.
// Using this method effectively gives 'leading-zero' suppression except where there is less than 1 mm displayed, as the value will be for example "-0.999" or "-0.001".
// The printf() formatter behaviour for %1.3f, cannot be displayed in 8 digits for all sizes beyond 4 LSD's.
// When the maximum number of significant digits has been exceeded, while counting from 9999 to 10000, we lose the least signifcant digit.
// then everything moves one step to the right including the decimal point.
// However, when the most significant digits reduce to four digits, we again see the 3 'least significant digits' (thousandths of a mm or 10E-6 metres).
// The maximum full number is +9999.999 or -9999.999 with a single lsb resolution. [one part in 10 million]
// In other words, we can dislay approx "plus or minus 10 million" as an integer.
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
// Testing the SPI clock rate: This has been run at 5.25 Mhz with a 16 bit frame width.
// SPI runs as 'Transmit Master Only'.
// Low level chip select (/CS) is connected to Port 10 via a 74HC14 inverter. Use macros "DISABLE_7SEG" and "ENABLE_7SEG"
// For a 3 axis set of measuremements, Teensy P2, P3, P4 will be used to select the correct 7-segment display.
// Level shifting from 3.3 V SPI outputs to 5.0V is done using 74HC14 schmitt trigger devices.
// We only use the absolute magnitude of the float "num", so we don't see the minus sign in the digit buffer out of snprintf() (the sign is added later).
// The null teminator is not counted in the snprintf() character count, but we need to count it when we allow storage space;
// Our 7 segment format has an implied plus + sign and leading zero suppression. examples: -0.898, -2.756, 4.675, 1000.098, 9066.456
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
void format_float_7segment(unsigned char display, float num)
{
char digit_buff[10];
int i, k;                                                  // i indexes length of the digit buffer, k is used to clear left over digits from a longer string
char *bf;                                                  // gets the current digit from the digit_buff[] char buffer.
char v;                                                    // holds the value of the next digit to send to the display so we don't have to derefernce the pointer each time we act on the digit value.
                                                           //
snprintf(digit_buff,MAX_7SEG_DIGITS+1,"%1.3f",fabs(num));  // snprintf prints a maximum of 8 displayable digits plus a null terminator. Numbers less than one, have a decimal point with one leading zero.
if (strlen(digit_buff) > MAX_7SEG_DIGITS+1)
    seven_seg_errors.too_many_digits = 1;                  // note, this count will include the decimal point a a character
strrev(digit_buff);                                        // we reverse the digit order to make it easier to send to the display, right to left.(LSD to MSD)
bf =(char*)&digit_buff;                                    // Assign a pointer to access the formatted digit_buff[] - the output of snprintf().
i = 1;                                                     // In the XC3714, digit 0 is indexed by the number 1
while(*bf)                                                 // Iterate the digit buffer until the null terminator is found.
  {                                                        // note: our call to snprintf() formats the absolute value of "num" --- fabs(num)
   v = *bf;                                                // get a digit.....
   if (v == '.') {++bf; v = *bf++ | 0x80; }                // if the character represents a decimal point, skip to the next digit and set its bit 7 to include the dp.
   else v = *bf++;                                         // else, just read the next digit from the digit buffer
   write_7segment(i,v & 0x8F, display);                    // write the current digit (as BCD) to the display via SPI with its optional b7 flag (decimal point)
   i++;                                                    // increment i to the next digit in the sequence (keep count).
  }                                                        // once we are here, the formatted string has been output.
if (num < 0.0) write_7segment(i,0x0A, display);            // next, optionally light the minus sign, (g segment)
         else  write_7segment(i,0x0F, display );           // then blank the next-but-one leading digit
for (k = i+1; k < 9; k++) write_7segment(k,0x0F,display);  // now blank the remaining digits i.e. no 'leftover' or leading digits will appear on the display.
}
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
void format_int_7segment(unsigned char display, short int num)
{
char digit_buff[11];
int i, k;                                                  // i indexes length of the digit buffer, k is used to clear left over digits from a longer string
char *bf;                                                  // gets the current digit from the digit_buff[] char buffer.
char v;                                                    // holds the value of the next digit to send to the display so we don't have to derefernce the pointer each time we act on the digit value.
                                                           //
snprintf(digit_buff,MAX_7SEG_DIGITS+2,"%d",abs(num));  // snprintf prints a maximum of 8 displayable digits plus a null terminator. Numbers less than one, have a decimal point with one leading zero.
if (strlen(digit_buff) > MAX_7SEG_DIGITS+1)
seven_seg_errors.too_many_digits = 1;                  // note, this count will include the decimal point a a character
strrev(digit_buff);                                        // we reverse the digit order to make it easier to send to the display, right to left.(LSD to MSD)
bf =(char*)&digit_buff;                                    // Assign a pointer to access the formatted digit_buff[] - the output of snprintf().
i = 1;                                                     // In the XC3714, digit 0 is indexed by the number 1
while(*bf)                                                 // Iterate the digit buffer until the null terminator is found.
  {                                                        // note: our call to snprintf() formats the absolute value of "num" --- fabs(num)
   v = *bf;                                                // get a digit.....
//..   if (v == '.') {++bf; v = *bf++ | 0x80; }                // if the character represents a decimal point, skip to the next digit and set its bit 7 to include the dp.
   v = *bf++;                                         // else, just read the next digit from the digit buffer
   write_7segment(i,v & 0x8F, display);                    // write the current digit (as BCD) to the display via SPI with its optional b7 flag (decimal point)
   i++;                                                    // increment i to the next digit in the sequence (keep count).
  }                                                        // once we are here, the formatted string has been output.
if (num < 0) write_7segment(i,0x0A, display);            // next, optionally light the minus sign, (g segment)
         else  write_7segment(i,0x0F, display );           // then blank the next-but-one leading digit
for (k = i+1; k < 9; k++) write_7segment(k,0x0F,display);  // now blank the remaining digits i.e. no 'leftover' or leading digits will appear on the display.
}
//--------------------------------------------------------------------------------------------
int write_7segment(unsigned char address, unsigned char data, unsigned char display)
{
T_XC3714_frame frame;
int response = 0;
uint16_t *p;
frame.bits = 0;
frame.address = address & 0x0F;
frame.data = data;
p = (uint16_t *) &frame;
switch(display)
 {
  case 0:ENABLE_7SEG_0 ; break;
  case 1:ENABLE_7SEG_1 ; break;
  case 2:ENABLE_7SEG_2 ; break;
 }
 SPI.beginTransaction(SPISettings(SPI_SPEED_7SEG, MSBFIRST, SPI_MODE0));
 response = SPI.transfer16(*p);     //
 SPI.endTransaction();
 switch(display)
 {
  case 0: DISABLE_7SEG_0;  break;
  case 1: DISABLE_7SEG_1;  break;
  case 2: DISABLE_7SEG_2;  break;
 }
 return(response);
}
//----------------------------------------------------------------------------------------------------------
// does an inplace reverse of the characters in the argument string.
void strrev(char* str)
{
  if (!str) { return; }                // return immediately if the string has zero length (str is a NULL pointer)
                                       // set up indexes to the start and end of the string
    int i = 0;                         // start
    int j = strlen(str) - 1;           // end
                                       // step through each character and reverse the string
    while (i < j) {
        char c = str[i];  // get the first char
        str[i] = str[j];  // get the end char
        str[j] = c;       // insert the first char into the end char position
        i++;              // increment  index of the first char.
        j--;              // decrement the index of the last char.
    }
}
//--------------------------------------------------------------------------------------------
void init_XC3714 ()     // default initialisation for the XC3714 display driver chip
{
pinMode(CS_PIN0, OUTPUT);
pinMode(CS_PIN1, OUTPUT);
pinMode(CS_PIN2, OUTPUT);
 SPI.begin();
 write_7segment(SHUT_DOWN, 0x01, 0);   // Shutdown - set display-enable bit
 write_7segment(DECODE_MODE, 0xFF,0);  // Decode mode - all digits are BCD formatted
 write_7segment(SCAN_LIMIT, 0x07,0);   // Scan limit - include all 8 digits to scan and display
 write_7segment(INTENSITY, 0x01,0);    // Display intensity control - set it to minimum
 clear_7segment(0);
 write_7segment(SHUT_DOWN, 0x01, 1);   // Shutdown - set display-enable bit
 write_7segment(DECODE_MODE, 0xFF,1);  // Decode mode - all digits are BCD formatted
 write_7segment(SCAN_LIMIT, 0x07,1);   // Scan limit - include all 8 digits to scan and display
 write_7segment(INTENSITY, 0x01,1);    // Display intensity control - set it to minimum
clear_7segment(1);
 write_7segment(SHUT_DOWN, 0x01, 2);   // Shutdown - set display-enable bit
 write_7segment(DECODE_MODE, 0xFF,2);  // Decode mode - all digits are BCD formatted
 write_7segment(SCAN_LIMIT, 0x07,2);   // Scan limit - include all 8 digits to scan and display
 write_7segment(INTENSITY, 0x01,2);    // Display intensity control - set it to minimum
clear_7segment(2);
}
//---------------------------------------------------------------------------------------------
void clear_7segment(unsigned char display)
{
 write_7segment(0x01,0x0F,display);
 write_7segment(0x02,0x0F,display);
 write_7segment(0x03,0x0F,display);
 write_7segment(0x04,0x0F,display);
 write_7segment(0x05,0x0F,display);
 write_7segment(0x06,0x0F,display);
 write_7segment(0x07,0x0F,display);
 write_7segment(0x08,0x0F,display);
}
//---------------------------------------------------------------------------------------------
void pulse_7segment(uint8_t count, unsigned char display)
{
uint8_t i;
for (i = 0; i <count; i++)
 {
  write_7segment(0x01,0x0A,display); // minus sign
  write_7segment(0x02,0x0A,display);
  write_7segment(0x03,0x0A,display);
  write_7segment(0x04,0x0A,display);
  write_7segment(0x05,0x0A,display);
  write_7segment(0x06,0x0A,display);
  write_7segment(0x07,0x0A,display);
  write_7segment(0x08,0x0A,display);
  delay(100);
  write_7segment(0x01,0x0F,display); // blank display
  write_7segment(0x02,0x0F,display);
  write_7segment(0x03,0x0F,display);
  write_7segment(0x04,0x0F,display);
  write_7segment(0x05,0x0F,display);
  write_7segment(0x06,0x0F,display);
  write_7segment(0x07,0x0F,display);
  write_7segment(0x08,0x0F,display);
  delay(100);
 }
}
//--------------------------------------------------------------------------------------------
void set_dp_7segment(uint8_t pos)
{
  //write_7segment(pos,0x8F);
}
//--------------------------------------------------------------------------------------------
 
Last edited:
Thanks to all for the insightful answers. I will dig deeper into all the information you have provided me. Might do a followup on the results in a few days.
 
I see you're a Jaycar shopper. There's actually a couple of libraries in Teensyduino that can interface with the MAX7219 to do the same job (it runs fine on 3.3V logic).
Hehe yep. Guilty as charged; I am a Jaycar shopper and an AliExpress shopper, and a reformed eBay shopper and a PJRC shopper, before it went to SparkFun. :) My desire in this piece was to work out the end-to-end details and discover the bits+pieces for myself. I'm a sucker for the dreaded "rabbit hole". Probably way too much detail for the question being asked, but there we are. Also - agree - the MAX7219 will be a much better option for 3.3V. If I ever do a PCB for this, it will use that part for sure.
 
If you decide to explore the FlexIO route, look for the FlexIO_t4 library. It among the many libs that get automatically installed. In Arduino IDE, click File > Examples > FlexIO_t4 > SPI > Simple to get started.
 
Also - agree - the MAX7219 will be a much better option for 3.3V. If I ever do a PCB for this, it will use that part for sure.
What I was getting at was "XC3714" is the internal Jaycar catalogue number for a MAX7219 with 8 segmented LED displays. At least it used to be...

Back in the day they were surprisingly good value, selling for around $8 but now the same "product" has switched to using shift registers (I think they're HC595s) instead of the MAX7219 and the price has doubled to $16. You can actually find different stores selling the different models depending on how old their inventory is. It's a very crappy thing to do because people buy them thinking they're get a board that works with existing code...
 
What I was getting at was "XC3714" is the internal Jaycar catalogue number for a MAX7219 with 8 segmented LED displays. At least it used to be...

Back in the day they were surprisingly good value, selling for around $8 but now the same "product" has switched to using shift registers (I think they're HC595s) instead of the MAX7219 and the price has doubled to $16. You can actually find different stores selling the different models depending on how old their inventory is. It's a very crappy thing to do because people buy them thinking they're get a board that works with existing code...
Ah yes, you're right - I forgot about the real device name. (getting old) Actually, I tried it on 3.3v and couldn't get the same bit rates as 5V, but they did work. And you're right too, about the shift register version (eccchh!!). Jaycar was selling that as the "equivalent", and I bought a few until I realised. After that, I got onto some Chinese source on AliExpress for a couple of dollars each and bought 20 or more that used the MAX7219. They are waiting patiently in my parts stash along with 35 Teensy 4.1's I bought in a fit of enthusiasm a while back. I'll probably die with them :D .....
 
I noticed Teensyduino does not come with Waveshare ePaper libraries. Does Teensyduino welcome contributions? If I wanted to contribute some ePaper libraries when they are finished, how should that be done?
 
I noticed Teensyduino does not come with Waveshare ePaper libraries. Does Teensyduino welcome contributions? If I wanted to contribute some ePaper libraries when they are finished, how should that be done?
I'd start by asking Paul Stoffregen. There are quite a number of regular contributors to the Arduino codebase and heaps of links to GitHub for similar additions along these lines in C++ and such. I'd start with Paul.
 
Waveshare ePaper libraries ... how should that be done?

If your library uses the SPI or Wire library for all communication, so there's no substantial code unique to Teensy hardware and it would work on all Arduino compatible boards with SPI / Wire libs, normally you would send a pull request to Arduino's library index. Their process for adding new libraries is fully automated, so you just send the pull request and within about 1 day it automatically appears in the Arduino IDE library manager.

Along the way, please share photos! Everyone loves seeing hardware. Especially if you're testing with multiple boards, sharing some benchmarks like speed, memory usage, etc would be really interesting. When you release the library, or if you're ready for beta testers, we might be able to show it on the PJRC website blog. At least 1 good photo showing Teensy connected would be the only real requirement.

But if your library does use special Teensy hardware features, definitely discuss here. Along the way, we'll talk of whether it should be included with the libraries that ship with the Teensy software. Maybe that will make sense, but just to be realistic, usually not. Most of those libraries are from the early days of Arduino, before the Arduino library manager existed. Several pre-date sites like Github! Some were abandoned by their original authors, after they had become in widespead. Those are usually the compelling reason to include with the Teensy software.

If you're continuing to update your library on Gihub, even if it has special code for Teensy, usually Arduino's library manager is the preferred way to distribute it to the Teensy community, and also to the larger Arduino world (assuming it uses SPI / Wire as a fallback to support all other boards).
 
normally you would send a pull request to Arduino's library index
Yes, that makes a lot of sense.

But if your library does use special Teensy hardware features, definitely discuss here.
Actually it uses minimal Teensy-specific code, like for example putting the framebuffer in DMAMEM and using Teensy <SPI.h> functions like beginTransaction and SPISettings. But as you mentioned, this is not a substantial difference and does not justify a separate library from all other boards.

Regarding Teensy-specific libraries, I wonder if someone could clarify to me how the Teensyduino libraries work in the Arduino IDE:
  • Does the Arduino IDE always prefer the hardware/teensy/ libraries instead of the hardware/arduino/ libraries whenever a Teensy board is selected in Tools > Board?
  • As soon as one switches to an Arduino board, all Teensy libraries would not be available for compilation?
  • Are Teensyduino libraries (in hardware/teensy/) common to all versions of Teensy (2.x, 3.x, 4.x)?
 
Back
Top