Reading SPI data very fast slows down Serial2 communication

yairsh

New member
Hello,
I'm working on a complex project with a Teensy 4.0 and have encountered an interesting issue. Due to business confidentiality, I can't share the full code, but I'll describe the relevant parts and the problem I'm facing.

My setup includes:
  • Reading data from Serial1 at 200Hz
  • Sending data via SerialUSB2 at 100Hz
  • Sending data via Serial2 at 100Hz
  • Performing numerous complex calculations

Recently, I added an SPI device and was reading from it in the main loop. After this addition, I noticed the data transmission rate to Serial2 dropped to around 50Hz, while all other communication rates and calculations remained unaffected.
Interestingly, when I reduced the SPI reading frequency to 5kHz (or even lower), the Serial2 transmission rate returned to the desired 100Hz.

While 5kHz is sufficient for my needs, I'm curious about what caused this issue. Any insights into why adding SPI communication at a higher rate affected only the Serial2 transmission, despite the presence of other intensive operations, would be greatly appreciated.
 
Without seeing the code, best anyone can do is blind guess. So here's some guesswork...

Maybe you need larger receive buffer in Serial1 because your program is spending more CPU time on SPI, so it takes longer before it checks for recently arrived bytes? Maybe addMemoryForRead() can help? See the serial docs for details.

You can also add memory for writing. Not sure if that will matter, but the capability is there. Perhaps if you increase the buffer and transmit 2 frames of data, you can keep the same rate even when your SPI code hogs too much CPU time?

You might also simply have SPI code written in a way that hogs the CPU. Maybe try doing the work in smaller pieces, so your code can share the work with the serial tasks.

And for 1 more blind guess, if you're seeing the serial speed go in a sudden step down from 100 to 50, rather than a gradual decline, this may be a sign of an issue or not-so-robust way your code does its timing to achieve 100 Hz? Remember, this is completely blind guessing since you didn't show any code. But without seeing anything and just general experience with programming, usually when one part of a program hogs the CPU too much the rest suffers proportionally. So if you see a sudden drop to half the rate, that ought to be a cue to investigate why it responds that way.

Not sure if any of this will help. If not, and if you're completely stuck, the best way to move forward would be crafting a small program you can share here on the forum which demonstrates the problem. Best if it's just 1 file so anyone can easily copy into Arduino IDE and upload to a Teensy to reproduce the problem. We're much better at helping with tough programming questions on this forum when we're able to reproduce the problem.
 
Sorry really not much to go on.
Things like how you are doing all of the things like sending and reading data? In the main loop? On some form of interrupts, like
IntervalTimer? ...

Could be as simple as your code is looping doing the SPI and as such not getting to where you are sending the data?
Could be something on an interrupt, that has a higher priority than the one that Serial2

Again can only throw darts without any real information.
 
Thank you all for your responses. You're right that I need to provide more details. Here's a more comprehensive explanation of my setup:
Timing and Measurements:
  • All timing calculations (for sending data and measuring rates) use differences between time points via millis() or micros().
  • Time measurements occur in each main loop cycle.
  • No interrupts or delay() functions are used.
SPI Communication:
  • Reading 2 bytes from a single register.
  • Initially done every main loop cycle, now reduced to a 5kHz rate which works great for me.
SerialUSB2 (100Hz):
  • Used for publishing data with micro-ros.
  • Timing for new data publication is calculated as mentioned above.
  • This part works consistently, even when the Serial2 issue occurs.

Serial1 (200Hz input):
  • Checked each main loop cycle using Serial1.available().
  • Data is read byte-by-byte until a full packet is received (using sync word, message ID, and length).
  • Valid packets are saved in a struct with a millis()-based timestamp.
  • This consistently works at 200Hz, even when the Serial2 issue occurs.

Serial2 (100Hz output):
  • Checked every main loop cycle if 10ms has passed since the last time point.
  • If so, it checks for new data from the Serial1 struct by comparing timestamps.
  • If both conditions are met, a JSON string is built using snprintf.
  • Each byte of the packet is sent in a different main loop cycle using Serial2.print() until the entire packet is transmitted.
  • This works at 100Hz when SPI reading is at a controlled rate, but drops to 50Hz when SPI was read every main loop cycle.

While I've found a workable solution, I'm curious about why this problem occurred in the first place, given that other intensive operations were unaffected.

I plan to create a diagram later to better illustrate how my code works, which should help clarify the flow and timing of operations.

I want to thank you again for trying to help me.
 
I plan to create a diagram later

Might be a better use of your time to instead create a small but complete program which demonstrates the problem.

Many times on this forum we've found problems which often turn out to be subtle programming issues. A diagram or description, or even small code fragments, usually doesn't make the problem apparent. A complete (and hopefully small & simple) demo program usually leads to finding the answer.
 
If it were me I
Thank you all for your responses. You're right that I need to provide more details. Here's a more comprehensive explanation of my setup:
Timing and Measurements:
  • All timing calculations (for sending data and measuring rates) use differences between time points via millis() or micros().
  • Time measurements occur in each main loop cycle.
  • No interrupts or delay() functions are used.
SPI Communication:
  • Reading 2 bytes from a single register.
  • Initially done every main loop cycle, now reduced to a 5kHz rate which works great for me.
SerialUSB2 (100Hz):
  • Used for publishing data with micro-ros.
  • Timing for new data publication is calculated as mentioned above.
  • This part works consistently, even when the Serial2 issue occurs.

Serial1 (200Hz input):
  • Checked each main loop cycle using Serial1.available().
  • Data is read byte-by-byte until a full packet is received (using sync word, message ID, and length).
  • Valid packets are saved in a struct with a millis()-based timestamp.
  • This consistently works at 200Hz, even when the Serial2 issue occurs.

Serial2 (100Hz output):
  • Checked every main loop cycle if 10ms has passed since the last time point.
  • If so, it checks for new data from the Serial1 struct by comparing timestamps.
  • If both conditions are met, a JSON string is built using snprintf.
  • Each byte of the packet is sent in a different main loop cycle using Serial2.print() until the entire packet is transmitted.
  • This works at 100Hz when SPI reading is at a controlled rate, but drops to 50Hz when SPI was read every main loop cycle.

While I've found a workable solution, I'm curious about why this problem occurred in the first place, given that other intensive operations were unaffected.

I plan to create a diagram later to better illustrate how my code works, which should help clarify the flow and timing of operations.

I want to thank you again for trying to help me.
Serial1:
If it were me I would not read serial1 byte by byte but wait until the buffer contains the same (or more) data as the size of the struct.
Then read the data into the struct.
Set a boolean to true to say new data available.
Serial2:
Don't check timestamp but respond to boolean value set by serial1, when true deal with Serial2 stuff and set boolean value to false.

Something like this:
Code:
bool newData = false;

void loop()
{
    DoSpiStuff();
    DoSerialUsbStuff();

// Serial1 Stuff
    if (Serial1.available() >= sizeof(serial1Struct)) {
        Serial1.readBytes(&serial1Struct, sizeof(serial1Struct));
        newData = true;
    }
//Serial2 Stuff
    if (newData) {
        DoSerial2Stuff();
        newData = false;
    }
}
 
All timing calculations (for sending data and measuring rates) use differences between time points via millis() or micros().
For timing I suggest using the macro ARM_DWT_CYCCNT, especially if your doing short timing and a lot of it.

This fetches clock cycles as a uint32_t. It will overflow every 17? or so seconds so you will have to handle that. You will also have to factor in what your cpu speed is set at (standard 600mhz) which you can get calling F_CPU_ACTUAL

The call is way quicker than micros as it's just reading a clock register. It's also a lot more accurate.
 
I think the default SPI T4 libraries are blocking. What that means is that there's stuff in there that says things such as

"while (SPI_STATUS_REGISTER_TX_DONE_BIT != 1) {}; "

the CPU will patiently repeat the check - and refuse to spend time on any other useful tasks.

the lower the SPI clock, the more this will bite.

My workaround was to write a SPI master and SPI slave library that uses DMA and interrupts and thus will not block.
Same issue (and workaround) for the HardwareSerial T4 libraries.

This I think is the SPI.h library code where the blocking occurs:
uint8_t transfer(uint8_t data) {
// TODO: check for space in fifo?
port().TDR = data;
while (1) {
uint32_t fifo = (port().FSR >> 16) & 0x1F;
if (fifo > 0) return port().RDR;
}
//port().SR = SPI_SR_TCF;
//port().PUSHR = data;
//while (!(port().SR & SPI_SR_TCF)) ; // wait
//return port().POPR;
}
uint16_t transfer16(uint16_t data) {
uint32_t tcr = port().TCR;
port().TCR = (tcr & 0xfffff000) | LPSPI_TCR_FRAMESZ(15); // turn on 16 bit mode
port().TDR = data; // output 16 bit data.
while ((port().RSR & LPSPI_RSR_RXEMPTY)) ; // wait while the RSR fifo is empty...
port().TCR = tcr; // restore back
return port().RDR;
}
uint32_t transfer32(uint32_t data) {
uint32_t tcr = port().TCR;
port().TCR = (tcr & 0xfffff000) | LPSPI_TCR_FRAMESZ(31); // turn on 32 bit mode
port().TDR = data; // output 32 bit data.
while ((port().RSR & LPSPI_RSR_RXEMPTY)) ; // wait while the RSR fifo is empty...
port().TCR = tcr; // restore back
return port().RDR;
}
 
Back
Top