For this tutorial, we are going to connect a BMP280 barometric pressure sensor to an Arduino and write the sensor data to a CSV file on a MicroSD card. But before that, let’s recall some related basic concepts.

How SD Cards Work

SD cards have non-volatile flash memory capable of writing at 16 MB/s and having capacities up to hundreds of GB.

BONUS: I made a quick start guide for this tutorial that you can download and go back to later if you can’t set this up right now. It covers all of the steps, diagrams, and code you need to get started.

Flash memory is written and erased in large blocks, not a byte at a time. Flash memory can only go through about 10,000 write/erase cycles before it wears out. Although this is a known limitation, it is not usually a problem since this is equivalent to writing and erasing the entire card’s contents once a day, every day for 27 years.

Flash memory is made from millions of floating gate MOSFET transistors and has no moving parts. Its main advantage over EEPROM is the amount of memory per given size.

SD cards use a computer file system architecture called FAT, which stands for File Allocation Table. FAT16 is for cards up to 2GB and FAT32 is for greater than 2GB. To know what yours is, insert the SD card into the card slot on your computer and in Explorer, right-click and choose ‘Properties’. This will tell you what file system is your card using. Another older option was NTFS but this was for older drives that can be split into partitions.

To reformat the card, right-click on it and select ‘Format’.

SD cards come in many types. Shown here is an SD card reader from Adafruit and another SD card reader from DFRobot. You can also get card readers that use standard-sized SD cards, but they are larger and take up more space. All SD card readers work on the same standard SD.h library.

Two different SD card modules

Logging Data to an SD Card

Using an SD card, we will create a data logger for the BMP280 connected to an Arduino. Generally, a data logger is an electronic device used to record data from sensors over time and stores it for further use or analysis.

The BMP280 will be connected as I2C and the SD Card as SPI. We will read the elapsed time since the Arduino was started and include this as data in the file as a make-shift time-stamp. This is the bare-bones for a data logging application. Here, we will use a real-time clock module as the timekeeper.

For this tutorial, we will use the I2C 3.3V version. This one, although 3.3V, is 5V compliant on the data pins. For more information on how the BMP280 works, check out our tutorial on Wireless Communication Between Two Arduinos.

BMP280 Pins

How to Connect an SD Card Reader to the Arduino

These are the parts you will need to build this project:

Writing Data to Files on an SD Card in Arduino - Wiring of SD Card to Arduino

If you want to learn more about the Arduino, check out our Ultimate Guide to the Arduino video course. You’ll learn basic to advanced Arduino programming and circuit building techniques that will prepare you to build any project.

Installing the Libraries

There are a few libraries that need to be installed first:

  • #include <SPI.h>
  • #include <SD.h>
  • #include <Adafruit_BMP280.h>
  • SPI.h and SD.h are standard libraries that come with the Arduino
  • The BMP280 library can be found here.

Notes on the Sketch

For both sketches, I have avoided the easy but ill-advised Arduino String object. The string object is not your friend, it is the proverbial wolf in grandma’s pajamas! Serious programmers avoid this entirely and rather use C-type character strings. Read more on reasons to avoid the string object here.

The sketch starts with a check that the SD card is present and that the wiring is correct. Otherwise, it will just halt and wait forever. while(1) simply means ‘wait here’ so long as 1 is true – which always is. You could also write while(1==1). Then the main loop waits for you to enter a key from the serial monitor and automatically converts to lower case in case you typed in capitals. The switch(charRead) will call a function depending on what was typed.

Good programming dictates that whenever there is a place where user error is likely to occur, you ‘wrap’ that in some sort of error checking. In this sketch, all file operations do that as it is quite likely the SD card could be missing or the file name is incorrect or the file is missing.

In the readfile() function, char inputChar = myFile.read() gets one char at a time and adds it to the next cell in the input string char array until it encounters the newline char (10), then adds a 0 onto the end to terminate the string correctly, then deals with the next line.

myString = myFile.readStringUntil('\n'); will read an entire line at a time (it reads until it finds the invisible newline character). The rest of the sketch requires no explanation.

Code for Writing, Reading, and Deleting Text Strings to the SD Card

Let’s take a look at a simple sketch for writing sample strings to the SD card, reading them back, and deleting them. This sketch will not use a BMP280 yet. At the beginning of the sketch are 3 pangrams – an interesting sentence that uses every one of the 26 letters of the alphabet at least once. Pangrams are often used by programmers to test displays and printers.

#include <SD.h>
#include <SPI.h>
File myFile;
char fileName[] = "simple.txt";
const int chipSelect = 10;
char charRead;
char pangram_1[] = "The five boxing wizards jump quickly";
char pangram_2[] = "Pack my box with five dozen liquor jugs";
char pangram_3[] = "The quick brown fox jumps over the lazy dog";

void setup()
{
  Serial.begin(9600);
  Serial.println("Simple SD Card Demo");

   if (SD.begin(chipSelect))
    {
    Serial.println("SD card is present & ready");
    } 
    else
    {
    Serial.println("SD card missing or failure");
    while(1);  //wait here forever
    }
    Serial.println("Enter w for write, r for read or d for delete");
}

void loop() 
{
  //Create a loop to read a command character from the keyboard
  //This will be 'r' for read, 'w' for write and 'd' for delete.

  if (Serial.available()) 
     {
      charRead = tolower(Serial.read());  //force ucase
      Serial.write(charRead); //write it back to Serial window
      Serial.println();
     }
     
  //get command from keyboard:
   switch(charRead)
   {
    case 'r':
        readFromFile();   //read
        break;
    case 'w':
        writeToFile(); //write to file
        break;
    case 'd':
        deleteFile();  //delete
        break;
   }
}

void readFromFile()
{
  byte i=0; //counter
  char inputString[100]; //string to hold read string
  
  //now read it back and show on Serial monitor 
  // Check to see if the file exists:
  if (!SD.exists(fileName)) 
      Serial.println("simple.txt doesn't exist."); 
  Serial.println("Reading from simple.txt:");
  myFile = SD.open(fileName);

  while (myFile.available()) 
  {   
   char inputChar = myFile.read(); // Gets one byte from serial buffer
    if (inputChar == '\n') //end of line (or 10)
    {
      inputString[i] = 0;  //terminate the string correctly
      Serial.println(inputString);
      i=0;
    }
    else
    {
      inputString[i] = inputChar; // Store it
      i++; // Increment where to write next
      if(i> sizeof(inputString))
        {
        Serial.println("Incoming string longer than array allows");
        Serial.println(sizeof(inputString));
        while(1);
        }
    }
  }
 }

void writeToFile()
{
  myFile = SD.open(fileName, FILE_WRITE);
  if (myFile) // it opened OK
    {
    Serial.println("Writing to simple.txt");
    myFile.println(pangram_1);
    myFile.println(pangram_2);
    myFile.println(pangram_3);
    myFile.close(); 
    Serial.println("Done");
    }
  else 
    Serial.println("Error opening simple.txt");
}

void deleteFile()
{
 //delete a file:
  if (SD.exists(fileName)) 
    {
    Serial.println("Removing simple.txt");
    SD.remove(fileName);
    Serial.println("Done");
   } 
}

Notes on the Code

The sketch starts just like the previous one, with the card check, then pre-deletes the CSV file in preparation, and before you can select an option to run in the Serial window. The header (column names) is pre-written to the file.

float QNH = 1022.67; QNH is an aviation term and used as a correction factor that when applied to an altimeter, will allow to accurately read the elevation above sea level at the current location. To learn more about this, please refer to our tutorial on Wireless Communication Between Two Arduinos.

Note that you don’t have to get this correction factor. However, without a corrected value, the altitude reading at your location will be incorrect and might even display negative.

The loop() function kicks off with resetting the dataStr to 0 to refresh it, then gets a sort of timestamp with the millis() function (the number of milliseconds elapsed since we started). It now needs to add this on to the end (appends) of the dataStr, and also add the comma-separated variable. strcat() is a little confusing. You would expect something like this: buffer = strcat(dataStr, temperature). But that’s not how it actually works. A buffer is a temporary storage to hold the result.

Code to Save the BMP280 Data to a CSV File on an SD Card

This sketch will read the BMP280 and save the values to the SD card in CSV format. CSV stands for Comma Separated Values and is often used where data with similar structures are saved to file or transmitted over a link. It is also format-compatible with MS Excel so that when you open the file or drag into Excel, it will automatically set itself up in rows and columns with the first row of the file creating headings for the columns. Once in Excel, you can easily display the data as graphs of many types.

#include <SD.h>
#include <SPI.h>
#include <Adafruit_BMP280.h>
Adafruit_BMP280 bmp; 
File myFile;

// change this to match your SD shield or module:
// Adafruit SD shields and modules: pin 10
// Sparkfun SD shield: pin 8
//mega pin = 53;
const int chipSelect = 10;
float QNH = 1022.67; //Change the "1022.67" to your current sea level barrometric pressure (https://www.wunderground.com)
const int BMP_address = 0x76;

float pressure;   
float temperature;  
float altimeter; 
char charRead;
char runMode;
byte i=0; //counter
char dataStr[100] = "";
 char buffer[7];

void setup()
{
  Serial.begin(9600);
  Serial.println("BMP280/SD Card Demo");
  bmp.begin(BMP_address); 
  if (SD.begin(chipSelect))
  {
    Serial.println("SD card is present & ready");
  } 
  else
  {
    Serial.println("SD card missing or failure");
    while(1); //halt program
  }
  //clear out old data file
  if (SD.exists("csv.txt")) 
  {
    Serial.println("Removing simple.txt");
    SD.remove("csv.txt");
    Serial.println("Done");
  } 

  //write csv headers to file:
   myFile = SD.open("csv.txt", FILE_WRITE);  
   if (myFile) // it opened OK
    {
    Serial.println("Writing headers to csv.txt");
    myFile.println("Time,Pressure,Temperature,Altitude");
    myFile.close(); 
    Serial.println("Headers written");
    }
  else 
    Serial.println("Error opening csv.txt");  
  Serial.println("Enter w for write, r for read or s for split csv");  
}

void loop(void) 
{
 dataStr[0] = 0;
 pressure = bmp.readPressure()/100;  //and conv Pa to hPa
 temperature = bmp.readTemperature();
 altimeter = bmp.readAltitude (QNH); //QNH is local sea lev pressure
//----------------------- using c-type ---------------------------
 //convert floats to string and assemble c-type char string for writing:
 ltoa( millis(),buffer,10); //conver long to charStr
 strcat(dataStr, buffer);//add it onto the end
 strcat( dataStr, ", "); //append the delimeter
 
 //dtostrf(floatVal, minimum width, precision, character array);
 dtostrf(pressure, 5, 1, buffer);  //5 is mininum width, 1 is precision; float value is copied onto buff
 strcat( dataStr, buffer); //append the coverted float
 strcat( dataStr, ", "); //append the delimeter

 dtostrf(temperature, 5, 1, buffer);  //5 is mininum width, 1 is precision; float value is copied onto buff
 strcat( dataStr, buffer); //append the coverted float
 strcat( dataStr, ", "); //append the delimeter

 dtostrf(altimeter, 5, 1, buffer);  //5 is mininum width, 1 is precision; float value is copied onto buff
 strcat( dataStr, buffer); //append the coverted float
 strcat( dataStr, 0); //terminate correctly 
 //Serial.println(dataStr);
 //create a loop to read from the keyboard a command character
 //this will be 'r' for read, 'w' for write and 'd' for delete.

  if (Serial.available()) //get command from keyboard:
     {
      charRead = tolower(Serial.read());  //force ucase
      Serial.write(charRead); //write it back to Serial window
      Serial.println();
     }
 
  if(charRead == 'w')  //we are logging
      runMode = 'W';
  if(charRead == 'r')  //we are reading
      runMode = 'R';
  if(charRead == 'd')  //we are deleting
      runMode = 'D';

  if(runMode == 'W') //write to file
  {     
   //----- display on local Serial monitor: ------------
   Serial.print(pressure); Serial.print("hPa  ");
   Serial.print(temperature); 
   Serial.write(0xC2);  //send degree symbol
   Serial.write(0xB0);  //send degree symbol
   Serial.print("C   ");  
   Serial.print(altimeter); Serial.println("m");

   // open the file. note that only one file can be open at a time,
    myFile = SD.open("csv.txt", FILE_WRITE);     
    // if the file opened okay, write to it:
    if (myFile) 
    {
      Serial.println("Writing to csv.txt");
      myFile.println(dataStr); 
      myFile.println(dataStr); 
      myFile.close();
    } 
    else 
    {
      Serial.println("error opening csv.txt");
    }
    delay(1000);  
  }

    if(runMode == 'R')  //we are reading
   {
    if (!SD.exists("csv.txt")) 
        Serial.println("csv.txt doesn't exist."); 
   Serial.println("Reading from csv.txt");
   myFile = SD.open("csv.txt");
    
   while (myFile.available()) 
  {   
   char inputChar = myFile.read(); // Gets one byte from serial buffer
    if (inputChar == '\n') //end of line (or 10)
    {
      dataStr[i] = 0;  //terminate the string correctly
      Serial.println(dataStr);
      i=0; //reset the counter
    }
    else
    {
      dataStr[i] = inputChar; // Store it
      i++; // Increment where to put next char
      if(i> sizeof(dataStr))  //error checking for overflow
        {
        Serial.println("Incoming string longer than array allows");
        Serial.println(sizeof(dataStr));
        while(1);
        }
    }
  }
}

  if(runMode=='D')
  {
   //delete a file:
   if (SD.exists("csv.txt")) 
      {
      Serial.println("Removing csv.txt");
      SD.remove("csv.txt");
      Serial.println("Done");
     } 
  }
}

If everything is wired correctly, open the serial window on the receiving Arduino and you should see the following:

Now, remove the card, insert it into your PC and open the CSV file in MS Excel (or drag and drop it) and you should see the column headings and the data nicely formatted into columns. Lastly, highlight the time and pressure columns and choose to insert a chart. You should get something like the one below (I did some heavy breathing on the BMP sensor to cause some pressure fluctuations!):

Thanks for reading! Leave a comment below if you have questions about anything.