SPI Slave mostly receiving wrong commands from Master

I am trying to get the master (Heltec ESP 32 Wifi Kit V3) to send 2 bytes to the slave (Arduino Nano). I am trying to send 2 commands A1 and A2 but they are not being received correctly by the slave, often being in the wrong order or having different commands all together.

I have attempted to adjust the clock speed and add delays but nothing has seemed to work. Using SPI.setClockDivider(SPI_CLOCK_DIV8) has not worked either. I have added my master and slave code below:

Master:

#include <SPI.h>
#include <SD.h>

#define SD_CS_PIN 5
#define SD_SCK_PIN 33
#define SD_MISO_PIN 35
#define SD_MOSI_PIN 34

#define SS_PIN_1 41  // Nano 1
#define SCK_PIN 46
#define MISO_PIN 42
#define MOSI_PIN 45

#define BUTTON_PIN 19
#define BUTTON_PIN2 7

unsigned long recordStartTime = 0;
bool isRecording = false;
char currentFilename[32] = "";

void setup() {
  Serial.begin(115200);
  pinMode(SS_PIN_1, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(BUTTON_PIN2, INPUT_PULLUP);
  digitalWrite(SS_PIN_1, LOW);

  SPI.beginTransaction(SPISettings(400000, MSBFIRST, SPI_MODE0));  //

  SPI.begin(SCK_PIN, MISO_PIN, MOSI_PIN, SS_PIN_1);

  Serial.println("SPI initialized for sensor communication.");
  delay(10);  // Short delay to let SPI initialize
}

String generateFilename() {
  String baseFilename = "/FlowData";
  int fileNumber = 0;
  String filename;
  do {
    filename = baseFilename + (fileNumber > 0 ? "_" + String(fileNumber) : "") + ".txt";
    fileNumber++;
  } while (SD.exists(filename));
  filename.toCharArray(currentFilename, sizeof(currentFilename));
  return filename;
}

void loop() {
  static float flowData1[50] = { 0 };

  if (digitalRead(BUTTON_PIN) == LOW) {
    ESP.restart();
  }

  if (digitalRead(BUTTON_PIN2) == LOW) {
    if (!isRecording) {
      Serial.println("Master: Starting RECORD command to Nano 1...");
      isRecording = true;
      recordStartTime = millis();
      while (digitalRead(BUTTON_PIN2) == LOW) {
        delay(10);  // Small debounce delay
      }
      delay(50);  // Small delay to ensure button state change
    }
  }

  if (isRecording) {
    if (millis() - recordStartTime < 2500) {
      collectData(SS_PIN_1, flowData1, "Nano 1");
      stopRecordCommand(SS_PIN_1);
      delay(200);  // Brief delay for synchronization
    } else {
      Serial.println("Master: Stopping RECORD command to Nano 1...");
      isRecording = false;
      saveFlowData(flowData1, 50);
    }
  }
  delay(50);
}

void collectData(int ssPin, float* dataArray, String nanoName) {
  Serial.println("Collecting data from " + nanoName + "...");

  volatile byte commandData[] = {0xA1,0xA2};  // Command bytes to send
  volatile byte myData[2];                       // Array to store received data

  int validDataCount = 0;
  while (validDataCount < 50) {
    digitalWrite(ssPin, LOW);  // Start SPI communication

    // Send the command and receive data
    for (int i = 0; i < 2; i++) {
      myData[i] = SPI.transfer(commandData[i]);  // Send command and receive response
    }
    digitalWrite(ssPin, HIGH);  // End SPI communication

    // Debugging output
    Serial.print("Received Hex Values: 0x");
    Serial.print(myData[0], HEX);
    Serial.print(" 0x");
    Serial.println(myData[1], HEX);

    // Combine the two bytes into a 16-bit integer
    int scaledFlow = (int(myData[0]) << 8) | int(myData[1]);

    if (scaledFlow == 0xFFFF || scaledFlow == 0x0000) {
      dataArray[validDataCount] = NAN;  // Invalid data
    } else {
      dataArray[validDataCount] = scaledFlow;
      Serial.printf("Data %d: %s: %.2f g/min\n", validDataCount, nanoName.c_str(), dataArray[validDataCount]);
    }

    validDataCount++;
    delay(50);  // Short delay between data points to avoid overloading the bus
  }
}

void saveFlowData(float* data1, int dataPoints) {
  digitalWrite(SS_PIN_1, HIGH);
  SPI.end();
  SPI.begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);

  if (!SD.begin(SD_CS_PIN)) {
    Serial.println("SD Card initialization failed!");
    return;
  }

  String filename = generateFilename();
  File file = SD.open(filename, FILE_WRITE);
  if (!file) {
    Serial.println("Failed to open file for writing");
    return;
  }

  file.println("Flow Data (g/min):");
  file.println("Index     Nano 1");

  for (int i = 1; i < dataPoints; i++) {
    if (isnan(data1[i])) continue;
    file.printf("%-9d %-10.2f\n", i, data1[i]);
  }

  file.println();
  file.close();
  Serial.println("Data saved to " + filename);

  SPI.end();
  SPI.begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);
}

void stopRecordCommand(int ssPin) {
  volatile byte stopCommand[] = { 0x00, 0x00 };  // 2-byte stop recording command

  digitalWrite(ssPin, LOW);
  delay(10);

  // Send the 2-byte stop recording command
  SPI.transfer(stopCommand[0]);
  SPI.transfer(stopCommand[1]);

  digitalWrite(ssPin, HIGH);
}

Slave:

#include <SPI.h>
#include <Wire.h>
#include <SensirionI2CSfm3000.h>

#define SS_PIN 10  // Chip Select (D10 for Nano)
#define BUTTON_PIN 5

SensirionI2CSfm3000 sfm;
volatile byte commandData[2];   // 2-byte command data received from master
volatile byte responseData[2];  // 2-byte response data to send back

int sensorDataIndex = 0;
float sensorValues[50];
float scalingFactor = 140.0;
float offset = 32000;
volatile bool commandReceived = false;

void setup() {
  Serial.begin(115200);
  pinMode(SS_PIN, INPUT_PULLUP); // Configure SS pin as input
  pinMode(MISO, OUTPUT);         // Configure MISO pin as output
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  bitSet(SPCR, SPE);             // Enable SPI
  bitClear(SPCR, MSTR);          // Ensure this device is a Slave
  SPI.attachInterrupt();         // Attach SPI interrupt for handling communication

  Wire.begin();
  sfm.begin(Wire, 0x40);
  uint16_t error = sfm.startContinuousMeasurement();
  if (error) {
    Serial.print("Error starting continuous measurement: ");
    Serial.println(error);
  }
  Serial.println("Slave Ready (Responding to 2-byte command)");
}

ISR(SPI_STC_vect) {
  static byte byteIndex = 0;

  // Read the byte from the SPI Data Register (SPDR) and store it in the commandData array
  commandData[byteIndex] = SPDR;
  byteIndex++;

  if (byteIndex >= 2) {  // Only process when both bytes have been received
    byteIndex = 0;  // Reset index for next command

    // Display received command bytes
    Serial.print("Command Received: 0x");
    Serial.print(commandData[0], HEX);
    Serial.print(" 0x");
    Serial.print(commandData[1], HEX);

    // Combine the two bytes into a 16-bit integer (for debugging)
    int receivedCommand = (int(commandData[0]) << 8) | int(commandData[1]);
    Serial.print(" (Combined: 0x");
    Serial.print(receivedCommand, HEX);
    Serial.print(" / Decimal: ");
    Serial.print(receivedCommand);
    Serial.println(")");

    // Check for a valid command
    if (commandData[0] == 0xA1 && commandData[1] == 0xA2) {  // Example command
      if (sensorDataIndex < 50) {
        float flow = sensorValues[sensorDataIndex];
        float rawFlow = flow; // Store raw flow before modifications
        flow = fabs(flow);    // Ensure positive flow value

        // Round and scale the flow value by 100 (to handle two decimal places)
        flow = round(flow * 100) / 100.0;

        // Scale the flow value
        int scaledFlow = int(flow * 100); // Scaled value for transmission (x100)

        // Split the scaled value into high and low bytes
        responseData[0] = (scaledFlow >> 8) & 0xFF;  // High byte
        responseData[1] = scaledFlow & 0xFF;         // Low byte

        // Load the response into SPI data register (SPDR)
        SPDR = responseData[0]; // First send high byte
        SPDR = responseData[1]; // Then send low byte

        sensorDataIndex++;  // Increment the index to move to the next flow value
      } else {
        sensorDataIndex = 0;  // Reset index when all values are sent
      }
    }
  }
}

void loop() {
  while (sensorDataIndex < 50) {
    float flow;
    uint16_t error = sfm.readMeasurement(flow, scalingFactor, offset);
    if (error) {
      Serial.print("Error reading flow measurement: ");
      Serial.println(error);
      flow = 0;  // If an error occurs, set flow to 0
    }
    flow = fabs(flow);  // Ensure positive flow value

    // Round the flow value to 2 decimal points before scaling
    flow = round(flow * 100) / 100.0;

    // Store the unscaled flow value
    sensorValues[sensorDataIndex] = flow;

    // Display unscaled and scaled flow values
    Serial.print("Unscaled Flow (raw): ");
    Serial.print(flow, 2);
    Serial.print(" g/min, ");

    // Scale the rounded value by 100
    int scaledFlow = int(flow * 100); // Scaled value for SPI (x100)
    Serial.print("Scaled Flow Value (x100): ");
    Serial.println(scaledFlow);

    sensorDataIndex++;  // Move to next index
    delay(500);  // Delay between measurements
  }
}

Examples of what is being received is:

14:12:53.262 -> Command Received: 0x0 0xA1 (Combined: 0xA1 / Decimal: 161)
14:12:53.356 -> Command Received: 0xA1 0xA1 (Combined: 0xFFFFA1A1 / Decimal: -24159)
14:12:53.391 -> Command Received: 0xA1 0xA2 (Combined: 0xFFFFA1A2 / Decimal: -24158)
14:12:53.498 -> Command Received: 0x68 0xA1 (Combined: 0x68A1 / Decimal: 26785)
14:12:53.571 -> Command Received: 0xA2 0xA1 (Combined: 0xFFFFA2A1 / Decimal: -23903)
14:12:53.610 -> Command Received: 0xA1 0xA2 (Combined: 0xFFFFA1A2 / Decimal: -24158)
14:12:53.656 -> Command Received: 0xA1 0xA2 (Combined: 0xFFFFA1A2 / Decimal: -24158)

It appears that you don't understand how SPI communication works. Each time the master sends a bit, it reads a bit from the slave.

So when your slave ISR reads a full byte from the SPDR, the master has already read whatever was in the SPDR before the ISR was called. Loading the SPDR after the byte is received means that value will be transmitted to the master when it sends a second byte. So when the master sends a second byte, it reads the response to the first one.

TL;DR: the slave has to read a byte from the SPDR, then write a byte to the SPDR. Rinse and repeat. Writing to the SPDR twice as your code does above simply overwrites the first value with the second.

Also, doing Serial stuff in an ISR is a seriously bad idea. It will bite you. Have the ISR read the SDPR, fill up your buffer, set a flag when it is full and do your processing in the main loop.

And think about the master sending a second command byte right after the first with no delay. Or sending a second command while your ISR is busy calculating and printing stuff from the last command. What do you think happens to the value in the SPDR? There's no FIFO on it.

1 Like

SPI (Serial Peripheral Interface) operates using two shift registers one for sending and one for receiving data. As the master device clocks data out to the slave, the slave simultaneously shifts data back to the master. This means that in many cases, you send and receive data in a single SPI cycle. The entire process is synchronized by the clock (SCK), which is always generated by the master device.

For more details, you can check out this resource:
Basics of SPI – Electronicshub

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.