[164] Karta SD cz. 4
Nasze zabawy w tworzenie plików i ich edycję mają ciemną stronę: nieumiejętnie wdrożone mogą szybko doprowadzić do zużycia i w rezultacie uszkodzenia karty. Jak się przed tym ustrzec? Poznajmy dwa przykłady: zły i dobry. Złym będzie modyfikacja pewnego szkicu z biblioteki, który – owszem, działa, ale nie nadaje się do użycia praktycznego, w każdym razie w tej postaci. Ale po kolei.
#include <SD.h> // Biblioteka obsługi karty SD
File file;
const byte cardCS = 4; // Deklaracja adresu linii CS11
const byte Loudness = A0; // Port mikrofonu.
const byte Potentiometer = A1; // Port potencjometru.
const byte Temperature = A2; // Port termometru.
const byte Brightness = A3; // Port fotorezystora.
void setup() {
Serial.begin(115200); // Inicjuj port monitora.
Serial.println(F("\
Czekam na sygnał z karty SD...")); // Wyślij komunikat.
if (!SD.begin(cardCS)) { // Inicjuj kartę określając port CS (jeśli brak - pin 10)
Serial.println(F("Niepowodzenie, nie wykryto karty.")); // Karta nie została wykryta.
while (1) // Koniec programu.
;
}
Serial.println(F("Powodzenie, wykryto kartę."));
Serial.println(F("\
Usunę plik DANE.TXT..."));
SD.remove("DANE.TXT"); // Usuwam plik DANE.TXT
}
void loop() {
String sensorsString = F(""); // Powołujemy do życia zmienną tekstową.
sensorsString += F("Czas: "); // Sygnatura czasowa w sekundach.
sensorsString += String(millis() / 1000);
sensorsString += F(", Głośność: "); // Port mikrofonu.
sensorsString += String(analogRead(Loudness));
sensorsString += F("/1023, Potencjometr: "); // Port potencjometru.
sensorsString += String(analogRead(Potentiometer));
sensorsString += F("/1023, Temperatura: "); // Port termometru.
sensorsString += String(analogRead(Temperature) * 0.125 - 22.0);
sensorsString += F(", Jasność: "); // Port fotorezystora.
sensorsString += String(analogRead(Brightness));
sensorsString += F("/1023\
");
file = SD.open("DANE.TXT", FILE_WRITE); // Otwieram plik DANE.TXT, który będzie zapisywany.
if (file) { // Jeśli udało się otworzyć plik...
file.println(sensorsString); // Zapisuję w nim tekst ze stanami sensorów.
file.close(); // Zapisuję otwarty plik na karcie.
Serial.println(sensorsString); // Wysyłam ten tekst także na monitor.
} else {
Serial.println(F("Niepowodzenie, nie dałem rady zapisać pliku DANE.TXT"));
}
delay(1000); // Zapisy wykonuj nie częściej niż to konieczne!
}Oto program, już bardzo praktyczny, który co jakiś czas zapisuje stan czterech czujników wraz ze znacznikiem czasu. To bardzo popularna aplikacja różnego rodzaju loggerów – choćby termometrów monitorujących warunki transportu. Na płytce edukacyjnej TME zamontowano termometr, fototranzystor, mikrofon i potencjometr i wszystko to teraz wykorzystam. W związku z tym na początku deklaruję adresy portów sensorów. Potem mamy znany nam już blok inicjacji karty. Następnie usuniemy sobie ewentualnie istniejący już plik z tak zwanymi logami, czyli cyklicznymi wpisami z odczytanymi parametrami. Oczywiście można to zrobić jeśli chcemy zawsze mieć nowy, czysty plik po resecie albo pominąć usuwanie, jeśli chcemy się dopisać do zrzutów powstałych wcześniej.
String sensorsString = F(""); // Powołujemy do życia zmienną tekstową.
sensorsString += F("Czas: "); // Sygnatura czasowa w sekundach.
sensorsString += String(millis() / 1000);
sensorsString += F(", Głośność: "); // Port mikrofonu.
sensorsString += String(analogRead(Loudness));
sensorsString += F("/1023, Potencjometr: "); // Port potencjometru.
sensorsString += String(analogRead(Potentiometer));
sensorsString += F("/1023, Temperatura: "); // Port termometru.
sensorsString += String(analogRead(Temperature) * 0.125 - 22.0);
sensorsString += F(", Jasność: "); // Port fotorezystora.
sensorsString += String(analogRead(Brightness));
sensorsString += F("/1023\
");W pętli głównej będziemy budować zmienną typu string, która będzie się składać z opisów oraz mierzonych wartości przekształconych w znaki. Więc po kolei: najpierw powołujemy zmienną do życia. Następnie wypełniamy ją napisem „Czas: „ i dodajemy zrzut zmiennej millis, która jest pokładowym zegarem, zwiększającym wartość co 1/1000 sekundy. Aż takiej dokładności nam nie potrzeba, wystarczy sekundowy znacznik – więc podzielimy ją przez tysiąc.
Tu dygresja: w warunkach praktycznych znacznik czasowy najlepiej określić bardziej zwyczajnie, tj. za pomocą kalendarza i zegara. Obie te rzeczy można napisać albo skorzystać z zegara RTC, który istnieje w niektórych wersjach płytek Arduino. My tutaj skupiamy się na czymś innym, więc pominąłem ten etap. Zainteresowanych zapraszam do artykułów o zegarze.
Teraz do łańcucha tekstu będziemy doklejać kolejne składniki: opisy oraz odczytane z przetworników dane. Nie zawracam sobie głowy konwersją na jakieś konkrety z wyjątkiem temperatury, która wyrazi się Celsjuszami po przeprowadzeniu takiej prostej konwersji. Zbieranie danych kończymy tym wpisem, oznaczającym złamanie wiersza – a to w celu zachowania porządku w pliku.
file = SD.open("DANE.TXT", FILE_WRITE); // Otwieram plik DANE.TXT, który będzie zapisywany.
if (file) { // Jeśli udało się otworzyć plik...
file.println(sensorsString); // Zapisuję w nim tekst ze stanami sensorów.
file.close(); // Zapisuję otwarty plik na karcie.
Serial.println(sensorsString); // Wysyłam ten tekst także na monitor.
} else {
Serial.println(F("Niepowodzenie, nie dałem rady zapisać pliku DANE.TXT"));
}
delay(1000); // Zapisy wykonuj nie częściej niż to konieczne!Teraz nastąpi zapis na karcie. W tym celu otworzymy ją, dopiszemy do pliku string, zamkniemy, wyślemy go dodatkowo na monitor i zaczekamy tak długo, jak często będą nam potrzebne zapisy, czyli tutaj – co sekundę.
Cóż, po kompilacji możemy chwilkę zaczekać, poświecić w fototranzystor, pokrzyczeć w mikrofon, pokręcić gałką, w końcu wyłączyć zasilanie i przenieść kartę do czytnika. Znajdziemy tam plik z zapisami, więc wszystko działa. Tylko że prawdopodobnie… no i właśnie nie wiadomo jak długo – czy po miesiącu, czy później – karta zacznie wykazywać błędy, które będą skutkiem cosekundowego dużego zapisu. Dlaczego dużego, skoro łańcuch ma tylko kilkadziesiąt bajtów? Otóż każde otwarcie i zamknięcie karty, choćby w celach zapisu jednego tylko bajtu, powoduje, że dochodzi do przeorganizowania struktury obejmującej znacznie większe obszary – rzędu kilobajtów. Dodatkowo jeśli będziemy kasować choćby najmniejszy plik, zakres działań jest jeszcze większy. Co prawda każda karta ma wbudowane mechanizmy rozkładania zapisów w celu wyrównania zużycia, więc cykliczne zapisywanie małego pliku nie jest tożsame z deklarowaną ilością zapisów kart przez producenta, jednak na zimne należy dmuchać i podejść do problemu inaczej.
#include // Biblioteka obsługi karty SD
File file;
const byte cardCS = 4; // Deklaracja adresu linii CS11
const byte Loudness = A0; // Port mikrofonu.
const byte Potentiometer = A1; // Port potencjometru.
const byte Temperature = A2; // Port termometru.
const byte Brightness = A3; // Port fotorezystora.
const byte saveButton = 8; // Port przycisku zamknięcia pliku.
bool fileReady = false; // Flaga gotowości pliku do zapisu.
char sensorsString[128]; // Tablica dla bufora ramki, tj. pojedynczego odczytu sensorów.
unsigned long lastWrite = 0; // Czas ostatniego zapisu.
const unsigned long interval = 1000; // Przerwa pomiędzy odczytami sensorów w milisekundach.
void setup() {
pinMode(saveButton, INPUT_PULLUP); // Deklaruj port przycisku jako wejście.
Serial.begin(115200); // Inicjuj port monitora.
Serial.println(F("\
Czekam na sygnał z karty SD...")); // Wyślij komunikat.
if (!SD.begin(cardCS)) { // Inicjuj kartę określając port CS (jeśli brak - pin 10)
Serial.println(F("Niepowodzenie, nie wykryto karty.")); // Karta nie została wykryta.
while (1) // Koniec programu.
;
}
Serial.println(F("Powodzenie, wykryto kartę."));
Serial.println(F("\
Usunę plik DANE.TXT..."));
SD.remove("DANE.TXT"); // Usuwam plik DANE.TXT
if (SD.exists("DANE.TXT")) { // Sprawdzam czy na karcie istnieje plik DANE.TXT
Serial.println(F("Niepowodzenie, nie dałem rady usunąć pliku DANE.TXT"));
} else {
Serial.println(F("Powodzenie, usunąłem plik DANE.TXT"));
}
Serial.println(F("\
Stworzę plik DANE.TXT..."));
file = SD.open("DANE.TXT", FILE_WRITE); // Otwieram plik DANE.TXT, który będzie zapisywany.
if (file) { // Sprawdzam czy na karcie istnieje plik DANE.TXT
Serial.println(F("Powodzenie, stworzyłem plik DANE.TXT\
"));
fileReady = true; // Plik jest gotowy do zapisu.
} else {
Serial.println(F("Niepowodzenie, nie dałem rady stworzyć pliku DANE.TXT"));
}
}
void loop() {
if (millis() - lastWrite >= interval && fileReady) { // Czy minął czas interwału między odczytami sensorów?
snprintf(sensorsString, sizeof(sensorsString), // Buduję w tablicy łańcuch stringów oraz wartości.
"Czas: %lu, Głośność: %i/1023, Potencjometr: %i/1023, Temperatura: %i/1023, Jasność: %i/1023\
", // Szablon.
millis() / 1000, // Argumenty.
analogRead(Loudness),
analogRead(Potentiometer),
analogRead(Temperature),
analogRead(Brightness));
file.print(sensorsString); // Dopisuję zawartość tablicy do karty.
lastWrite = millis(); // Aktualizuję czas dla obliczania interwału.
Serial.print(sensorsString); // Wysyłam zawartość tablicy na monitor.
}
if (digitalRead(saveButton) == HIGH) { // Jeśli przycisk zamknięcia pliku został wciśnięty...
file.flush(); // Zapisuję bufor do pliku.
file.close(); // Zamkykam plik
Serial.println(F("\
Zamykam plik DANE.TXT i kończę pracę programu."));
while (1) // Koniec programu.
;
}
}Przekształcimy nasz szkic w taki, który o wiele łagodniej postępuje z kartą. Początek jest identyczny, poza deklaracją dodatkowych zmiennych, które przydadzą się nam później. Natomiast otwarcie karty nastąpi tylko raz, jeszcze przed główną pętlą.
W tejże będziemy nieco inaczej postępować z budową wyrażenia zapisywanego na karcie. Otóż używanie funkcji string może prowadzić do różnych problemów związanych z defragmentacją pamięci, a zauważmy, że praca z kartą już na wstępie sporo jej rezerwuje. Stąd wszędzie w napisach wymusiłem trzymanie ich w pamięci flash. Lepiej tworzyć łańcuch w zadeklarowanej wstępnie tablicy – o długości wziętej z zapasem na najdłuższy wpis. 128 bajtów wystarczy aż za dość.
if (millis() - lastWrite >= interval && fileReady) { // Czy minął czas interwału między odczytami sensorów?
snprintf(sensorsString, sizeof(sensorsString), // Buduję w tablicy łańcuch stringów oraz wartości.
"Czas: %lu, Głośność: %i/1023, Potencjometr: %i/1023, Temperatura: %i/1023, Jasność: %i/1023\
", // Szablon.
millis() / 1000, // Argumenty.
analogRead(Loudness),
analogRead(Potentiometer),
analogRead(Temperature),
analogRead(Brightness));
file.print(sensorsString); // Dopisuję zawartość tablicy do karty.
lastWrite = millis(); // Aktualizuję czas dla obliczania interwału.
Serial.print(sensorsString); // Wysyłam zawartość tablicy na monitor.
}Najpierw jednak określimy interwał – już nie za pomocą funkcji delay, a obliczenia różnicy pokładowego zegara. Gdy nadejdzie czas, utworzymy łańcuch w tablicy. Idea jest podobna, ale wykonanie zupełnie inne. Najpierw tworzymy szablon składający się ze znaków oraz zmiennych, które rozpoczyna znak procentu. I tak: lu oznacza typ long bez znaku, i – int i tak dalej. Niestety ośmiobitowe Arduino zostało tu ogryzione ze zmiennej typu float, więc temperaturę będę zapisywał za pomocą int. Jeśli byłby to problem, należy wcześniej zamienić ten format na zmiennoprzecinkowy, reprezentowany znakami i osadzić go tutaj – ale nie to jest podmiotem rozważań, więc odpuściłem.
Mając już szablon, należy wymienić argumenty – w tej samej kolejności, oddzielane przecinkami. Tablica powstała, możemy ją wysłać do karty. Ale bez otwierania, bo karta została już otwarta i także bez zamykania – dzięki czemu pozbędziemy się problemu szybkiego zużywania karty.
No dobrze, działa to fajnie, tylko jak zakończyć proces? Otwarta karta wyjęta z czytnika to pokusa, by wszystko się rozleciało. I tu mamy problem. Rozwiązałem to prościutko: wykorzystałem jeden z przycisków na płytce.
if (digitalRead(saveButton) == HIGH) { // Jeśli przycisk zamknięcia pliku został wciśnięty...
file.flush(); // Zapisuję bufor do pliku.
file.close(); // Zamkykam plik
Serial.println(F("\
Zamykam plik DANE.TXT i kończę pracę programu."));
while (1) // Koniec programu.
;
}Jego wciśnięcie natychmiast zapisze wszystko, co siedzi w pamięci RAM karty w jej komórki i zamknie ją, a następnie zakończy program. Innymi słowy, to taki odpowiednik „Można teraz bezpiecznie wyłączyć komputer” z Windows 95. W praktyce używa się watchdoga albo innych metod wykrywających spadek napięcia zasilającego i na ostatnim tchnieniu zapisują się dane na karcie, po czym następuje jej zamknięcie. Dobrze też cyklicznie, np. raz na kilkaset zapisów, użyć funkcji flush, która zrzuca bufor danych do komórek karty. No i w zastosowaniach przemysłowych raczej nie stosuje się takiego formatu, a same, surowe dane rozdzielone np. przecinkami, który to format łatwo importować choćby do Excela. Opisy zamieściłem tylko na nasze potrzeby, by wszystko było jasne i zrozumiałe.

Inne artykuły z tej kategorii
Jak wyświetlić cyfry o rozmiarze 10x8 i 10x16 pikseli?














































































































