CANBus with SD-card

Hello

Using Longan Labs CANBed - Arduino CAN Bus Dev Kit I am trying to log the data to a SD card using the same SPI interface as the MCP 2515 CAN Controller.

This code works fine

#include <SPI.h>
#include <mcp_canbus.h>
// #include <SD.h>
#include "CanMessages.h"
#include "pinDefinitions.h"

#define WAIT_FOR_SERIAL 3000
#define CAN_CS_PIN 17
#define SD_CS_PIN 11

MCP_CAN CAN(CAN_CS_PIN);  // Set CS pin
CanMessageHandler canHandler;

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
    digitalWrite(LED_BUILTIN, true);

    Serial.begin(115200);
    while ((!Serial) && (millis() < WAIT_FOR_SERIAL));  // wait X milliseconds for Serial

    while (CAN_OK != CAN.begin(CAN_500KBPS)) {  // init can bus : baudrate = 500k
        if (Serial) {
            Serial.print(millis());
            Serial.println(" CAN BUS FAIL!");
        }
        delay(100);
    }
    if (Serial) {
        Serial.print(millis());
        Serial.println(" CAN BUS OK!");
    }

#ifdef __SD_H__
    // Initialize SD card
    while (!SD.begin(SD_CS_PIN)) {
      Serial.println("SD card initialization failed.");
      delay(100);
    }
    Serial.println("SD card initialized successfully.");
#endif

    digitalWrite(LED_BUILTIN, false);
}

void loop() {
  static uint64_t last_can_rx = 0;
  static uint64_t period_can_rx = 2;  // ms
  static uint64_t last_log = 0;
  static uint64_t period_log = 20;  // ms
  static uint64_t last_serial_print = 0;
  static uint64_t period_serial_print = 750;

  if ((millis() - last_can_rx >= period_can_rx) && (CAN_MSGAVAIL == CAN.checkReceive())) {
      last_can_rx = millis();
      CanMessage message;
      CAN.readMsgBuf(&message.length, message.data);
      message.extended_frame = CAN.isExtendedFrame();
      message.COB_ID = CAN.getCanId();
      message.node_id = message.COB_ID & 0x007F;
      message.func_id = message.COB_ID & 0x3F80;
      canHandler.bufferCanMessage(message);
  } else if (millis() - last_log >= period_log) {
      last_log = millis();
#ifdef __SD_H__
      File dataFile = SD.open("canlog.txt", FILE_WRITE);
      for (int j = 0; j < canHandler.getCanIdCount(); j++) {
          const CanMessage& msg = canHandler.getCanMessageAt(j);
          printCanMessage(msg, dataFile);
      }
      dataFile.close();
#endif
  }
  canHandler.checkForTimeouts();  // Check and delete messages that have timedout

  if ((millis() - last_serial_print >= period_serial_print) && Serial && true) {
      last_serial_print = millis();
      for (int j = 0; j < canHandler.getCanIdCount(); j++) {
          const CanMessage& msg = canHandler.getCanMessageAt(j);
          printCanMessage(msg, Serial);
      }
  }
}

Printing CAN messages to Serial as expected:

671 CAN BUS OK!
759	181	385	1	1	180	384	0			80	80	1	0	0	0	0	40
1518	181	385	1	1	180	384	0			80	80	1	0	0	0	0	40
2277	181	385	1	1	180	384	0			80	80	1	0	0	0	0	40
3037	181	385	1	1	180	384	0			80	80	1	0	0	0	0	40
3796	181	385	1	1	180	384	0			80	80	1	0	0	0	0	40
4555	181	385	1	1	180	384	0			80	80	0	0	0	0	0	40
5314	181	385	1	1	180	384	0			80	80	0	0	0	0	0	40
6073	181	385	1	1	180	384	0			80	80	0	0	0	0	0	40

However, when // #include <SD.h> is uncommented and the SD library are used the code compiles and uploads but stops output as this:

742 CAN BUS OK!
SD card initialized successfully.
772	4000	16384	0	0	0	0	0			80	80	1	0	0	0	0	40

The CANLOG.TXT exists on the SD-card as an empty file.

Any advice of what changes should be done to make this work?

pinDefinitions and CanMessages header and source files are attached.

CanMessages.cpp (2.0 KB)
CanMessages.h (1.2 KB)
pinDefinitions.h (248 Bytes)

Please post your code (using code tags) instead of attaching it.

Your board does not seem to have an SD card interface onboard; so I guess you're using an external one.

There are cheap SD modules that don't release the MISO line and can interfere with other SPI devices. If you bought some stuff from Ali/Ebay, that might be the cause; you can hack the SD module or buy a decent one from e.g. Pololu or Adafruit.

@sterretje Could you elaborate about how to hack the SD-module?

It is a eLabBay uSD-BO-V2A MicroSD Transflash Breakout Board uSD-BO-V2A MicroSD Transflash Breakout Board – eLabBay

Anyways, I've not tested to much, the board has been rewired, and the code updated.
It seems checking if file opens and specifiying the chip select has made a difference. If not the wiring, hard to tell when changing a lot of stuff at once.

Code:

      digitalWrite(CAN_CS_PIN, HIGH); // Deactivate MCP2515
      digitalWrite(SD_CS_PIN, LOW);   // Activate SD-card
      Serial.println("Opening file ...");
      File dataFile = SD.open("canlog.txt", FILE_WRITE);
      Serial.println("Writing to file ...");
      if (dataFile) {
        dataFile.println(millis());
        for (int j = 0; j < canHandler.getCanIdCount(); j++) {
            const CanMessage& msg = canHandler.getCanMessageAt(j);
            printCanMessage(msg, dataFile);
        }
        Serial.println("Closing file ...");
        dataFile.close();
      } else {
        Serial.println("!dataFile");
        SD.end();
        SD_begin = SD.begin(SD_CS_PIN);
        Serial.print(millis());
        Serial.println((SD_begin ? " SD card initialization successfully." : " SD card initialization failed."));
      }
      digitalWrite(SD_CS_PIN, HIGH);   // Deactivate SD-card
      digitalWrite(CAN_CS_PIN, LOW); // Activate MCP2515

And for the sake of good order, here are the other files.
pinDefinitions.h

#define D4 4
#define D0_RX 0
#define D5 5
#define D1_TX 1
#define D6	6
#define D2_SDA 2
#define A3	21
#define D3_SCL	3
#define D12	12
#define D8	8
#define A0	18
#define D9	9
#define A1	19
#define D10	10
#define A2	20
#define D11	11

CanMessages.h

#ifndef CAN_MESSAGE_H
#define CAN_MESSAGE_H

#include <stdint.h>
#include <Arduino.h>

#define MAX_CAN_IDS 30  // Number of unique CAN IDs to buffer
#define TIMEOUT_PERIOD 1800  // Timeout period in milliseconds

struct CanMessage {
    uint32_t COB_ID;  // 11-bit CAN ID is referred to as the Communication Object Identifier (COB-ID)
    uint32_t node_id;  // 0x07F
    uint32_t func_id;  // 0x3F80
    bool extended_frame;
    uint8_t length;
    uint8_t data[8];
    uint64_t lastReceivedTime;  // Timestamp of the last received message
};

class CanMessageHandler {
public:
    CanMessageHandler();

    void bufferCanMessage(const CanMessage& newMessage);
    void checkForTimeouts();
    const CanMessage* getMessage(uint32_t COB_ID) const;
    int getCanIdCount() const { return canIdCount; }
    const CanMessage& getCanMessageAt(int index) const { return canMessageBuffer[index]; }

private:
    CanMessage canMessageBuffer[MAX_CAN_IDS];
    int canIdCount;
    uint64_t last_buffer_overflow;
};

// Function declaration to print a CanMessage to Serial
void printCanMessage(const CanMessage& msg, Stream& serial);

#endif // CAN_MESSAGE_H

CanMessages.cpp

#include "CanMessages.h"

CanMessageHandler::CanMessageHandler() : canIdCount(0), last_buffer_overflow(0) {}

void CanMessageHandler::bufferCanMessage(const CanMessage& newMessage) {
    for (int i = 0; i < canIdCount; i++) {
        if (canMessageBuffer[i].COB_ID == newMessage.COB_ID) {
            canMessageBuffer[i] = newMessage;
            canMessageBuffer[i].lastReceivedTime = millis();  // Update the timestamp
            return;
        }
    }

    if (canIdCount < MAX_CAN_IDS) {
        CanMessage message = newMessage;
        message.lastReceivedTime = millis();
        canMessageBuffer[canIdCount++] = message;
    } else {
        last_buffer_overflow = millis();
        Serial.println("Buffer overflow! Consider increasing MAX_CAN_IDS.");
    }
}

void CanMessageHandler::checkForTimeouts() {
    uint64_t currentTime = millis();
    for (int i = 0; i < canIdCount;) {
        if (currentTime - canMessageBuffer[i].lastReceivedTime > TIMEOUT_PERIOD) {
            canMessageBuffer[i] = canMessageBuffer[--canIdCount];
        } else {
            i++;
        }
    }
}

void printCanMessage(const CanMessage& msg, Stream& serial) {
    serial.print(millis());
    serial.print("\t");
    serial.print(msg.COB_ID, HEX);
    serial.print("\t");
    serial.print(msg.COB_ID);
    serial.print("\t");
    serial.print(msg.node_id, HEX);
    serial.print("\t");
    serial.print(msg.node_id);
    serial.print("\t");
    serial.print(msg.func_id, HEX);
    serial.print("\t");
    serial.print(msg.func_id);
    serial.print("\t");
    serial.print(msg.extended_frame);
    serial.print("\t\t");
    for (int i = 0; i < msg.length; i++) {
        serial.print("\t");
        serial.print(msg.data[i], HEX);
    }
    serial.println();
}

const CanMessage* CanMessageHandler::getMessage(uint32_t COB_ID) const {
    for (int i = 0; i < canIdCount; i++) {
        if (canMessageBuffer[i].COB_ID == COB_ID) {
            return &canMessageBuffer[i];
        }
    }
    return nullptr;
}

It applies to modules that look like these; I did try to find the topic back on the forum but a quick search did not reveal it.
micro-sd-card-module

I can not see if your SD reader has active components to cater for the voltage difference between your uC board (5V IO) and the SD card (3.3V).

Be careful, 5V on the SD card itself can destroy the card; they are 3.3V devices.

Glad that you got it sorted.

In case anyone else are facing similar issues and ends up here...
Topic changed to CANBus with SD-card, as I've moved away from the CANBed for now.

The new setup involves:

Here is a schematic (including RTC and battery (untested)):

Below is the first code I tested.
This one opens the dataFile for each main loop.
Which leads to a normal write cycle at about 21ms, since I intend to use this for a RC servo signal (50Hz) sent over CANBus, I need a write cycle far less than 20ms.
Using the SD library.

Please keep in mind that the example uses a custom version of MCP_CAN_lib

#include "mcp_can.h"

to fit the 10MHz crystal at MIKROE CAN SPI Click 3.3V board.
The code is not added in this post as the changes can be found at MCP_CAN_lib / Pull requests #33

// CAN datalogger example

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

#define SD_CD_PIN 20
#define SD_CS_PIN 21
#define CAN0_INT_PIN 9
#define CAN0_CS_PIN 10

long unsigned int rxId;
unsigned char len = 0;
unsigned char rxBuf[8];

MCP_CAN CAN0(CAN0_CS_PIN);

bool CAN0_init_OK = false;
bool SD_init_OK = false;

void setup() {
  Serial.begin(115200);
  while ((!Serial) && (millis() < 3000));  // Wait for Serial

  // Initialize MCP2515
  CAN0_init_OK = (CAN0.begin(MCP_ANY, CAN_500KBPS, MCP_10MHZ) == CAN_OK);
  Serial.println(CAN0_init_OK ? "MCP2515 Initialized Successfully!" : "Error Initializing MCP2515...");

  if (CAN0_init_OK) {
    CAN0.setMode(MCP_NORMAL);  // Set MCP2515 to normal mode
  }

  pinMode(CAN0_INT_PIN, INPUT);

  // Initialize SD card
  pinMode(SD_CD_PIN, INPUT_PULLUP);
  SD_init_OK = (!digitalRead(SD_CD_PIN) && SD.begin(SD_CS_PIN));
  Serial.println(SD_init_OK ? "SD initialization OK." : "SD initialization failed.");
}

void loop() {
  delay(1);

  // Check if a CAN message is available
  if (CAN0_init_OK && (CAN_MSGAVAIL == CAN0.checkReceive())) {
    CAN0.readMsgBuf(&rxId, &len, rxBuf);

    // Open datalog file
    File dataFile;
    if (SD_init_OK && !digitalRead(SD_CD_PIN)) {
      dataFile = SD.open("datalog.txt", O_CREAT | O_WRITE | O_APPEND);
    }

    if (dataFile) {
      // Write timestamp, CAN ID, and data length
      dataFile.print(millis());
      dataFile.write(' ');
      dataFile.print(rxId, HEX);
      dataFile.write(' ');
      dataFile.print(len, HEX);

      // Write data bytes
      for (byte i = 0; i < len; i++) {
        if (rxBuf[i] < 10) {
          dataFile.write('0');  // Add leading zero for single-digit hex values
        }
        dataFile.print(rxBuf[i], HEX);
      }

      dataFile.println();
      dataFile.close();
    }
  }
}

The next code, dataFile are opened at setup and kept open as the main loop runs.
Resulting in a normal write cycle at about 11ms.

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

#define SD_CD_PIN 20
#define SD_CS_PIN 21
#define CAN0_INT_PIN 9
#define CAN0_CS_PIN 10

long unsigned int rxId;
unsigned char len = 0;
unsigned char rxBuf[8];

MCP_CAN CAN0(CAN0_CS_PIN);

bool CAN0_init_OK = false;
bool SD_init_OK = false;
File dataFile;

void setup() {
  Serial.begin(115200);
  while ((!Serial) && (millis() < 3000));  // Wait for Serial

  // Initialize MCP2515
  CAN0_init_OK = (CAN0.begin(MCP_ANY, CAN_500KBPS, MCP_10MHZ) == CAN_OK);
  Serial.println(CAN0_init_OK ? "MCP2515 Initialized Successfully!" : "Error Initializing MCP2515...");

  if (CAN0_init_OK) {
    CAN0.setMode(MCP_NORMAL);  // Set MCP2515 to normal mode
  }

  pinMode(CAN0_INT_PIN, INPUT);

  // Initialize SD card
  pinMode(SD_CD_PIN, INPUT_PULLUP);
  SD_init_OK = (!digitalRead(SD_CD_PIN) && SD.begin(SD_CS_PIN));
  Serial.println(SD_init_OK ? "SD initialization OK." : "SD initialization failed.");

  // Open the datalog file once for faster writes
  if (SD_init_OK && !digitalRead(SD_CD_PIN)) {
    dataFile = SD.open("datalog.txt", O_CREAT | O_WRITE | O_APPEND);
    if (!dataFile) {
      Serial.println("Error: Could not open datalog.txt for writing.");
      SD_init_OK = false;
    }
  }
}

void loop() {
  delay(1);

  // Check if a CAN message is available
  if (CAN0_init_OK && (CAN_MSGAVAIL == CAN0.checkReceive())) {
    CAN0.readMsgBuf(&rxId, &len, rxBuf);

    if (SD_init_OK && dataFile) {
      // Write timestamp, CAN ID, and data length
      dataFile.print(millis());
      dataFile.write(' ');
      dataFile.print(rxId, HEX);
      dataFile.write(' ');
      dataFile.print(len, HEX);

      // Write data bytes
      for (byte i = 0; i < len; i++) {
        if (rxBuf[i] < 10) {
          dataFile.write('0');  // Add leading zero for single-digit hex values
        }
        dataFile.print(rxBuf[i], HEX);
      }

      dataFile.println();

      // Flush data to SD card periodically for safety
      static unsigned long lastFlushTime = 0;
      if (millis() - lastFlushTime > 1000) {
        dataFile.flush();
        lastFlushTime = millis();
      }
    }
  }
}

This might be acceptable, but I could not refrain from tuning it further.
The next code, at setup tries to find the fastest frequency the SD card will accept.
Normal write cycle at about 8.51ms.
Here then CAN messages are put into a struct and written binary to a file.
Even though it is not proven here in this post, this has little practical effect on the write time.

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

#define SD_CD_PIN 20
#define SD_CS_PIN 21
#define CAN0_INT_PIN 9
#define CAN0_CS_PIN 10

#ifndef SD_SCK_MHZ
#define SD_SCK_MHZ(mhz) (1000000 * mhz)
#endif

uint32_t calculateCRC32(const uint8_t *data, size_t length) {
  uint32_t crc = 0xFFFFFFFF;
  for (size_t i = 0; i < length; i++) {
    crc ^= data[i];
    for (int j = 0; j < 8; j++) {
      if (crc & 1) {
        crc = (crc >> 1) ^ 0xEDB88320;  // CRC32 polynomial
      } else {
        crc >>= 1;
      }
    }
  }
  return ~crc;
}

struct CanMessage {
  uint64_t timestamp;  // Time of the message
  uint32_t rxId;       // CAN ID
  uint8_t length = 0;  // Length of CAN data (0-8 bytes)
  uint8_t rxBuffer[8]; // CAN data (payload)
  uint32_t crc;        // CRC value
};
CanMessage canMessage;
MCP_CAN CAN0(CAN0_CS_PIN);

bool CAN0_init_OK = false;
bool SD_init_OK = false;
File dataFile;

void setup() {
  Serial.begin(115200);
  while ((!Serial) && (millis() < 3000));  // Wait for Serial

  pinMode(CAN0_INT_PIN, INPUT);
  pinMode(SD_CD_PIN, INPUT_PULLUP);

  CAN0_init_OK = (CAN0.begin(MCP_ANY, CAN_500KBPS, MCP_10MHZ) == CAN_OK);
  Serial.println(CAN0_init_OK ? "MCP2515 Initialized Successfully!" : "Error Initializing MCP2515...");
  if (CAN0_init_OK) {
    CAN0.setMode(MCP_NORMAL);  // Set MCP2515 to normal mode
  }

  if (!digitalRead(SD_CD_PIN)) {
    uint8_t frequencies[] = {50, 25, 20, 16, 12, 10, 8, 4, 1};
    for (uint8_t i = 0; i < sizeof(frequencies) / sizeof(frequencies[0]); i++) {
      SD_init_OK = SD.begin(SD_SCK_MHZ(frequencies[i]), SD_CS_PIN);
      if (SD_init_OK) {
        Serial.print("SD initialization successful at ");
        Serial.print(frequencies[i]);
        Serial.println(" MHz.");
        break;
      } else {
        Serial.print("SD failed at ");
        Serial.print(frequencies[i]);
        Serial.println(" MHz. Trying lower frequency...");
      }
    }
  } else {
    Serial.println("SD card not detected (CD pin indicates no card). Skipping initialization.");
  }

  // Open the datalog file once for faster writes
  if (SD_init_OK && !digitalRead(SD_CD_PIN)) {
    String logName = "canlog.bin";
    dataFile = SD.open(logName, O_CREAT | O_WRITE | O_APPEND);
    if (!dataFile) {
      Serial.println("Error: Could not open '" + logName + "' for writing.");
      SD_init_OK = false;
    }
  }
}

void loop() {
  delay(1);

  // Check if a CAN message is available
  if (CAN0_init_OK && (CAN_MSGAVAIL == CAN0.checkReceive())) {
    canMessage.timestamp = millis();
    CAN0.readMsgBuf(&canMessage.rxId, &canMessage.length, canMessage.rxBuffer);
    canMessage.crc = calculateCRC32((const uint8_t *)&canMessage, sizeof(CanMessage) - sizeof(canMessage.crc));

    if (SD_init_OK && !digitalRead(SD_CD_PIN)) {
      dataFile.write((const uint8_t *)&canMessage, sizeof(canMessage));
      dataFile.flush();  // Ensure data is saved
    }
  }
}

I have briefly tested with SdFat library without any great immediate success. This might be subject for another reply or post. As I understand from my source list below, this library should be even faster than what I've made so far.

Source list increasing SD writing speed search:

Happy tinkering! :smiley: