Zapisywanie danych z czujnika na karcie SD w systemie Arduino
Prezentujemy przykład zapisywania danych na karcie SD w systemie Arduino. Rejestrowane będą dane o temperaturze otoczenia, pobrane z popularnego sensora DS18B20. Cały projekt działa na płytce Cytron Maker Uno, w pełni kompatybilnej z Arduino UNO.
W roli modułu czytnika kard SD wykorzystano płytkę Fermion: SD Card Module z oferty DFRobot. Płytka zawiera slot na karty typu microSD i komunikuje się poprzez interfejs SPI. Zawiera wlutowane złącze szpilkowe, co pozwala na dołączenie modułu do płytki stykowej.
Rys. 1. Moduł czytnika kart SD Fermion: SD Card Module
W roli źródła danych zapisywanych na karcie SD zastosowano czujnik temperatury DS18B20. Pozwala na pomiar temperatury w zakresie -55~+125°C z dokładnością ±0,5°C (w zakresie -10~+85°C). Do komunikacji wykorzystywany jest interfejs 1-Wire. W celu łatwiejszego podłączenia do Arduino, skorzystamy z płytki KAmodDS18B20. Ta płytka jest wyposażona w złącza w standardzie Pmod oraz KAmod, ale można też skorzystać z tradycyjnych, pojedynczych przewodów.
Rys. 2. Moduł KAmodDS18B20 z czujnikiem temperatury DS18B20
Połączenie modułów do płytki Arduino
Sensor temperatury korzysta z interfejsu 1-Wire, natomiast czytnik kart z SPI. Arduino Uno ma wystarczającą liczbę wyprowadzeń, aby bez problemu podłączyć oba moduły. Oba korzystają z zasilania 5 V, co oznacza, że prawdopodobnie będzie trzeba je rozgałęzić, na przykład poprzez szynę zasilania na płytce stykowej.
Oto tabele prezentujące połączenia modułów z płytką Arduino:
Tab. 1. Połączenie płytki KAmodDS18B20 z Arduino
KAmodDS18B20 | Maker Uno |
VDD | 5V |
GND | GND |
1-W | 2 |
Tab. 2. Połączenie czytnika kart SD z Arduino
Fermion: SD Card Module | Maker Uno |
MISO | 12 |
SCK | 13 |
SS | 4 |
MOSI | 11 |
GND | GND |
+5V | 5V |
Rys. 3. Połączenie wszystkich elementów układu
Kod przykładu
Tak jak wspomniano wcześniej, kod ma za zadanie odczytać dane z sensora temperatury, a następnie zapisać je w pliku na karcie SD. Dane będą zapisywane w pliku typu CSV z danymi oddzielonymi średnikiem, co pozwoli od razu otworzyć plik w programie Excel i LibreOffice.
Pierwszą kolumną danych będzie oznaczenie czasu pomiaru. W tym przypadku jest to aktualny wynik funkcji millis(), ale można łatwo zmodyfikować projekt, aby zapisywał w tym miejscu aktualną datę i godzinę. Drugą kolumną jest oczywiście aktualna temperatura.
Na początku importujemy niezbędne biblioteki. Do obsługi czytnika kart użyjemy dedykowanej biblioteki dla kart SD, a także pakietu do obsługi SPI. Podobna sytuacja jest w przypadku termometru, gdzie korzystamy z biblioteki DallasTemperature.h do obsługi czujnika oraz OneWire.h do obsługi interfejsu.
#include <SPI.h> #include <SD.h> #include <OneWire.h> #include <DallasTemperature.h>
W kolejnym kroku definiujemy wyprowadzenia używane przez poszczególne moduły. Przede wszystkim chodzi tu o port Chip Select interfejsu SPI oraz linię danych magistrali 1-Wire.
#define ONE_WIRE_BUS 2 const int chipSelect = 4;
Następnie definiujemy zmienne współpracujące z funkcją millis. W previousMillis będziemy przechowywać dane o poprzednio zapisanej wartości funkcji, którą potem będziemy porównywać z wartością aktualną. Stała interval natomiast przechowuje częstotliwość powtarzania pętli operacyjnej – będzie to 1000 ms, czyli 1 sekunda.
unsigned long previousMillis = 0; const long interval = 1000;
Inicjalizujemy jeszcze obiekty klas OneWire i DallasTemperature, co umożliwi obsługę sensora temperatury.
OneWire oneWire(ONE_WIRE_BUS); DallasTemperature sensors(&oneWire);
Instrukcje w funkcji setup
W funkcji setup uruchamiam port szeregowy, inicjalizuję czujnik oraz kartę SD. Na porcie szeregowym wyświetlona zostanie informacja, czy karta jest poprawnie widoczna w systemie czy też nie (np. na skutek braku karty w slocie).
void setup() { Serial.begin(9600); sensors.begin(); if (!SD.begin(chipSelect)) { Serial.println("Card failed, or not present"); while (1); } Serial.println("card initialized."); }
Pętla główna
W pętli głównej programu inicjalizujemy zmienną currentMillis i zapisujemy do niej czas pracy programu w milisekundach. Następnie w warunku if sprawdzamy czy od ostatniego wykonania programu minęło zdefiniowane wcześniej 1000 ms. Dalsza część pętli nie wykona się o ile warunek ten nie zostanie spełniony.
void loop() { unsigned long currentMillis = millis(); if (currentMillis - previousMillis >= interval){
Po spełnieniu warunku if, zapisujemy aktualną wartość czasową w zmiennej previousMillis do kolejnych porównań. Tworzymy teraz łańcuch znaków dataString, w którym będziemy zapisywać dane w postaci tekstowej. Do pustego łańcucha dodajemy timestamp ze zmiennej previousMillis, a następnie średnik i spację.
previousMillis = currentMillis; String dataString = ""; dataString += previousMillis; dataString +="\; ";
W kolejnym kroku uruchamiamy pomiar temperatury i wpisujemy go do zmiennej tempC. Zmienna ta bez problemu daje się dodać do łańcucha znaków za pomocą operatora +=.
sensors.requestTemperatures(); float tempC = sensors.getTempCByIndex(0); dataString += tempC;
Następnie inicjujemy kotwicę do otwarcia pliku jednocześnie otwierając sam plik. Jeśli plik zostanie prawidłowo otwarty, to w następnym kroku program zapisze do niego utworzoną linijkę danych i zamknie plik. Następnie ta sama linijka zostanie wypisana na port szeregowy.
File dataFile = SD.open("datalog.csv", FILE_WRITE); if (dataFile) { dataFile.println(dataString); dataFile.close(); Serial.println(dataString); } else { Serial.println("error opening datalog.csv"); }
Taki sposób obsługi pliku oznacza, że plik otwierany jest tylko przez krótką chwilę na czas zapisania danych. Można więc odłączyć urządzenie od zasilania prawie bez obaw o uszkodzenie pliku. Po odłączeniu zasilania program zaczyna bez problemu z powrotem zapisywać dane w utworzonym pliku (aczkolwiek funkcja millis liczy od początku). Ciekawostką jest też fakt, że po włączeniu portu szeregowego, funkcja millis również rozpoczyna zliczanie od początku.
Pełen kod programu
#include <SPI.h> #include <SD.h> #include <OneWire.h> #include <DallasTemperature.h> #define ONE_WIRE_BUS 2 const int chipSelect = 4; unsigned long previousMillis = 0; const long interval = 1000; OneWire oneWire(ONE_WIRE_BUS); DallasTemperature sensors(&oneWire); void setup() { Serial.begin(9600); sensors.begin(); if (!SD.begin(chipSelect)) { Serial.println("Card failed, or not present"); while (1); } Serial.println("card initialized."); } void loop() { unsigned long currentMillis = millis(); if (currentMillis - previousMillis >= interval){ previousMillis = currentMillis; String dataString = ""; dataString += previousMillis; dataString +="\; "; sensors.requestTemperatures(); float tempC = sensors.getTempCByIndex(0); dataString += tempC; File dataFile = SD.open("datalog.csv", FILE_WRITE); if (dataFile) { dataFile.println(dataString); dataFile.close(); Serial.println(dataString); } else { Serial.println("error opening datalog.csv"); } } }
Pliki projektowe są dostępne w sekcji Do pobrania.
Wynik pracy programu
Jak wspominałem, program zapisuje dane w pliku CSV w dwóch kolumnach oddzielonych średnikiem. Pozwala to łatwo zaimportować plik do Excela oraz wielu innych programów, ze względu na prostotę i powszechność tego formatu. Oczywiście po implementacji można dokonać analizy danych w bardzo szerokim zakresie.
Rys. 4. Dane z pliku zaimportowane w Excelu
Wszystkie komponenty użyte w projekcie są dostępne w ofercie sklepu Kamami.pl