Back

Kurs Arduino – wykorzystanie łącza SPI

Wcześniej wykorzystywaliśmy łącze szeregowe zwane Serial (RS-232) oraz łącze I2C, a teraz, w siódmym odcinku kursu zajmiemy się głównie łączem SPI (Serial Peripherial Interface). Omówimy urządzenie nadzorujące (master) oraz urządzenia podrzędne (slave). Poznamy linie oznaczone SCK (Serial Clock) oraz skróty MOSI (Master Output Slave Input) i MISO (Master Input, Slave Output). Wykorzystamy karty SD i microSD oraz moduły MAX31865 i MAX31855

Zapewne wielu Czytelników zaintrygowała przedstawiona w poprzednim odcinku niecodzienna konstrukcja z kilkunastoma różnymi czujnikami wilgotności zamontowanymi na pokrywce od słoika. Posłużyła ona tylko do zapoznania się z podstawowymi możliwościami czujników i łącza I2C (TWI = Two Wire). Do tej dziwnej konstrukcji, czujników wilgotności oraz do łącza I2C będziemy jeszcze wracać, ale w ramach podstawowego kursu Arduino musimy iść dalej i poznawać kolejne ważne zagadnienia. I tak koniecznie trzeba zapoznać się z bardzo pożytecznym i dość popularnym łączem SPI.

Wcześniej mieliśmy do czynienia z łączem szeregowym, które w szkicach Arduino nazywamy Serial. Wykorzystujemy je w wielu szkicach do komunikacji naszego Arduino z konsolą na ekranie komputera (będziemy go używać także w tym odcinku). Korzystając z łącza szeregowego musimy w obu komunikujących się urządzeniach ustawić tę sam prędkość transmisji (najczęściej 9600 bitów na sekundę, a w zasadzie powinniśmy też ustawić liczbę bitów w „paczce”, liczbę bitów stopu i kwestie kontroli błędów, ale pozostawiamy popularne wartości domyślne). Łącze, które nazywamy Serial, opiera się na popularnym od prawie sześćdziesięciu lat (!) standardzie RS-232. Sygnały przesyłane przez łącze Serial występują w płytce Arduino na pinach cyfrowych 0, 1, dlatego tych pinów nie wykorzystujemy. A komunikacja Arduino z komputerem wprawdzie opiera się na standardzie szeregowym RS-232, ale jest realizowana za pomocą o wiele nowcześniejszego, „więcej mogącego” łącza USB. W każdym razie w łączu szeregowym RS-232 mamy dwie linie sygnałowe. Każda z nich przekazuje sygnały cyfrowe w jednym kierunku. W łączu tym nie ma sygnału zegarowego (transmisja jest asynchroniczna), dlatego trzeba ustawiać w komunikujących się urządzeniach jednakowe parametry transmisji. Inaczej jest w łączu I2C, gdzie transmisja jest synchroniczna, to znaczy jedną linią przesyłany jest sygnał zegarowy, wytwarzany przez urządzenie master. Druga linia to linia danych, dzięki sprytnym rozwiązaniom służy do dwukierunkowego przekazywania informacji. W łączu I2C zawsze połączenie nawiązuje master, który wysyła na magistralę najpierw adres urządzenia, z którym chce nawiązać kontakt, a potem albo wysyła do tego zaadresowanego urządzenia dane, albo żąda, by to urządzenie przysłało mu jakieś informacje. Jak wiemy, do jednej, dwuprzewodowej magistrali I2C może być dołączonych wiele urządzeń o różnych adresach. Przesyłaniu adresu i danych towarzyszy zawsze potwierdzanie ich odbioru, co jest możliwe dzięki wykorzystaniu specyficznej budowie magistrali, a konkretnie obecności rezystorów podciągających i wyjść z otwartym kolektorem/drenem. Taka sprytna koncepcja pozwala na synchroniczną, dwukierunkową transmisję danych za pomocą tylko dwóch przewodów.

Jeszcze sprytniejszym rozwinięciem tej koncepcji rezystora podciągającego i wyjść ze wspólnym kolektorem/drenem jest jednoprzewodowy interfejs 1-Wire, gdzie nie ma oddzielnego sygnału zegarowego, a dwukierunkowa transmisja odbywa się za pomocą jednego tylko przewodu. Łącze 1-Wire (OneWire) jest używane w popularnych termometrach cyfrowych DS18B20. Odmiana tego jednoprzewodowego łącza występuje też w czujnikach wilgotności i temperatury DHT11, DHT22, które wykorzystywaliśmy w poprzednim odcinku.

Omawiane łącza mają swoje zalety i wady, a ich wspólną cechą jest to, że do dwukierunkowej komunikacji wykorzystują minimalną liczbę przewodów.

Inaczej jest z łączem SPI (Serial Peripherial Interface). Tu też występuje urządzenie nadzorujące (master), które zarządza magistralą, do której podłączonych może być jedno lub więcej urządzeń podrzędnych (slave). Master po pierwsze wytwarza sygnał zegarowy, zwykle oznaczany SCK (Serial Clock). Master (M) ma też wyjście (O – output), z którego dane podawane są na wejścia (I – inputs) urządzeń podrzędnych (S – slave). Stąd skrót MOSI (Master Output Slave Input), który oznacza linię, którą dane są przekazywane od mastera do urządzeń slave.

Druga linia danych ma skrót MISO (Master Input, Slave Output) i służy do przekazywania danych od urządzeń slave do mastera. Te trzy linie (SCK, MOSI, MISO) są wspólne, natomiast każde urządzenie slave ma oddzielne wejście SS (Slave Select) i oddzielną linię, gdzie w spoczynku panuje stan wysoki. Taki najpopularniejszy sposób realizacji łącza SPI ilustruje rysunek 1.

Rysunek 1

Jeżeli w systemie ma pracować N urządzeń z łączem SPI, to w urządzeniu master, w naszym przypadku w Arduino, musimy wykorzystać N+3 pinów.

Gdy master chce się skomunikować z jednym z urządzeń, tylko w linii SS tego właśnie urządzenia zmienia stan z wysokiego na niski, przez co wybiera, adresuje to urządzenie i komunikuje się z nim według protokołu SPI. Pozostałe urządzenia „są głuche i milczą”.

Nie trzeba się wgłębiać we wszystkie, skądinąd interesujące szczegóły protokołu SPI. Trzeba jednak wiedzieć, iż w Arduino mamy bibliotekę SPI. Warto wiedzieć, iż standard SPI jest w pewnym sensie otwarty i nie wszystkie szczegóły są zdefiniowane i wymagane. Dlatego w bibliotece SPI jest funkcja – metoda SPISettings(), która pozwala określić szczegóły, stosownie do właściwości danego urządzenia slave. Metoda ta ustala maksymalną prędkość transmisji (częstotliwość zegara SCK), kolejność bitów (dataOrder: MSBFIRST, LSBFIRST) oraz szczegóły związane z „biegunowością” i fazą sygnału zegarowego (dataMode: SPI_MODE0… SPI_MODE3).

Na szczęście zwykle nie musimy posługiwać się tą i innymi, poniekąd elementarnymi funkcjami – metodami, niemniej inne biblioteki często z nich korzystają bez naszej wiedzy. Dołączając do Arduino urządzenie/układ ze pomocą łącza SPI, zazwyczaj używamy bibliotek, które wyręczają nas i pozwalają na bardzo łatwą komunikację, gdzie zwykle łącza SPI „w ogóle nie widać”. Przekonamy się o tym w tym odcinku.

Karta SD

Łącze SPI nie jest zbyt popularne i można zaryzykować twierdzenie, że traci na popularności. Utrzymuje się przekonanie, że magistrala SPI jest dużo szybsza niż I2C, co w przypadku wielu urządzeń nie ma dziś znaczenia. Niektóre układy scalone z interfejsem SPI mają też do wyboru wbudowany „oszczędniejszy” interfejs I2C.

W kursie Arduino od biedy interfejs SPI można byłoby pominąć, gdyby nie fakt, że jest on wykorzystany do komunikacji z bardzo popularnymi nośnikami pamięci, jakimi są karty SD (Secure Digital). Obecnie coraz częściej wykorzystujemy najmniejsze wersje, czyli karty microSD.

Fotografia 2

Fotografia 2 pokazuje kartę microSD i popularny moduł pozwalający ją dołączyć m.in. do Arduino, a do tego adapter o wielkości standardowej karty SD, który pozwala dołączyć microSD do licznych czytników, m.in instalowanych w laptopach.

Karty SD (i microSD) wewnątrz zawierają nie tylko pamięć FLASH o dużej pojemności, ale i obwody interfejsu SPI. Koniecznie trzeba wiedzieć, że sama karta nie może być zasilana napięciem 5V, a jedynie 3,3V. Trzeba też wiedzieć, że pobór prądu jest największy podczas zapisu danych i wynosi kilkadziesiąt miliamperów (dla niektórych kart według specyfikacji prąd może sięgnąć 100mA). To może być problemem w niektórych zastosowaniach.

Karty (micro)SD nie mogą być dołączane wprost do Arduino Uno choćby ze względu na napięcie zasilania i napięcie sygnałów na pinach cyfrowych. Stosuje się proste adaptery, jak na fotografii 2, zawierające reduktor napięcia stabilizator 3,3V oraz kilka buforów, które pozwalają na współpracę 5-woltowego Arduino z 3-woltową kartą. Schemat takiego modułu jest pokazany na rysunku 3.

Rysunek 3

Bufory są tu typu 75ABT125 i są zasilane napięciem 3,3V z wyjścia stabilizatora. Wersja buforów 74125 z literkami ABT (BiCMOS) ma „zwykłe” wejścia, więc gdy na wejściach pojawi się napięcie 5V, to popłynie prąd przez wewnętrzne diody zabezpieczające wejścia i napięcie zasilające podniosłoby się, zależnie od wartości prądów. Aby temu zapobiec, na wejściach, na które podawane jest napięcie 5V z pinów Arduino, włączone są szeregowe rezystory (3,3kΩ), przez co prąd płynący przez te rezystory w stanie wysokim wyjść Arduino jest zredukowany do wartości poniżej 0,3mA. Prądy te mogą nieco zwiększyć napięcie zasilania, ale w małym, niegroźnym stopniu. Co ciekawe, w module z fotografii 2 te rezystory są niepotrzebne, ponieważ jest tam zastosowana niskonapięciowa kostka (74)LVC125, która ma wejścia o innej budowie, gdzie napięcia wejściowe do 5,5V nie powodują przepływu prądu do dodatniej szyny zasilania. Rezystory są tu zastosowane tylko ze względów technologicznych (na tych samych płytkach można montować różne wersje buforów ’125). Istnieją jednak także moduły do kart SD, zasilane napięciem 5V, gdzie bufory zasilane są napięciem 3,3V ze stabilizatora, ale rezystorów na wejściach nie ma.

Rysunek 4

Przykład z rysunku 4 pokazuje schemat adaptera z kostką 74HC4050, czyli wersją CMOS 4050, która też ma nietypowe obwody zabezpieczające wejścia i szeregowych rezystorów wejściowych nie potrzebuje. Poświęcamy temu tyle uwagi, ponieważ niestety trafiają się tanie moduły do kart SD, które nie mają, a powinny mieć szeregowe rezystory na wejściach.

Fotografia 5

Przykład na fotografii 5 to zasilany napięciem 5V moduł zawierający czytnik kart microSD i zegar RTC DS1307 (dlatego jest koszyk na baterię CR1220). Układ DS1307 według katalogu ma być zasilany napięciem 4,5…5,5V, natomiast czytnik kart SD i bufor 74HC125 jest zasilany napięciem 3,3V uzyskiwanym ze stabilizatora oznaczonego 662K (LM6206 lub odpowiednik). Bufory 74HC125 mają „zwykłe” obwody zabezpieczające, a więc powinny mieć na wejściach szeregowe rezystory ograniczające prąd. Takich rezystorów nie ma, więc jest ryzyko nadmiernego wzrostu napięcia zasilania.

Warto też wiedzieć, iż dostępne są też moduły czytników kart, gdzie na wejściach są włączone rezystory, ale nie szeregowo, tylko równolegle, jako rezystory podciągające (rysunek 6).

Rysunek 6

Można zastanawiać się nad sensem włączenia takich rezystorów, ale na pewno one nie przeszkadzają. Takie moduły ewidentnie przeznaczone są do zasilania napięciem 3…3,3V i do współpracy ze sterownikami zasilanymi takim samym napięciem.

Ryzyko uszkodzenia karty SD istnieje w czytnikach, które mają stabilizator 3,3V, czyli przewidziane są do zasilania napięciem 5V, ale nie zawierają buforów, a tylko wspomniane rezystory podciągające (jak na fotografii 4). One mogą bezpiecznie współpracować tylko ze sterownikami, gdzie sygnały cyfrowe są 3(,3)-woltowe. Niektóre takie moduły nie zawierają stabilizatora (fotografia 7).

Fotografia 7

Oczywiście takie moduły nie nadają się do bezpośredniej współpracy z Arduino UNO, gdzie wprawdzie dostępne jest napięcie 3,3V (tylko czy wydajność prądowa wystarczy dla prądożernego egzemplarza karty?), ale sygnały na wyjściach cyfrowych są 5-woltowe.

Takie „3-woltowe” moduły można jednak łatwo dołączyć do Arduino za pomocą prościutkiego rezystorowego konwertera poziomów według rysunku 8.

Rysunek 8

Wartości rezystorów nie są krytyczne (najlepiej o wartości pojedynczych kiloomów). Rezystory trzeba tak dobrać, żeby z napięcia 5V od strony Arduino uzyskać napięcie około 3,3V. W obwodzie sygnału zegarowego zastosowano tu dzielnik o większym prądzie (1kΩ + 2,2kΩ), który daje napięcie troszkę wyższe od 3,3V, ale jest to dopuszczalne, bo przy zasilaniu 3,3V, napięcia na wejściach cyfrowych bez problemu mogą być wyższe od napięcia zasilania o co najmniej 0,3V, a nawet o 0,5V.

Przedstawiony przegląd pokazuje, że stosując gotowe moduły, nie tylko czytniki kart SD, trzeba mieć odpowiednią wiedzę, żeby czegoś nie uszkodzić.

Uczulam na tę kwestię!

Z lektury wpisów na forach  związanych z Arduino można wywnioskować, że część uczestników popełnia błędy, zasilając moduły zbyt wysokim napięciem. Niektóre moduły (w rzeczywistości zawarte w nich niskonapięciowe układy scalone) nie ulegają przy tym uszkodzeniu, ale niestety znaczna część po takiej operacji nadaje się do wyrzucenia.

Tyle ostrzeżeń. A teraz kolejny moduł.

MAX31865

Chcemy zapoznać się z interfejsem SPI, a oprócz kart SD niezbyt wiele modułów i układów go zawiera. Ponieważ wcześniej wykorzystywaliśmy czujniki temperatury (i wilgotności), użyjmy modułu z kostką MAX31865. Jest to układ scalony, przeznaczony do współpracy z rezystancyjnym platynowym czujnikiem temperatury. Platyna, jako metal, ma dodatni współczynnik cieplny, wiec jej rezystancja rośnie ze wzrostem temperatury. Najpopularniejsze są czujniki PT100, dostępne są także PT1000. Liczba 100 i 1000 oznacza, jaką rezystancję ma dany czujnik w temperaturze 0°C. Ogólnie biorąc, wysokiej jakości czujniki PT100 pozwalają na realizację najbardziej precyzyjnych termometrów, nawet o dokładności setnych i tysięcznych części stopnia. Oczywiście kostka MAX31865 nie zapewni aż takiej precyzji z kilku powodów (deklarowana dokładność to 0,5°C), ale znakomicie ułatwi pomiary. W sumie jest to układ do pomiaru stosunku dwóch rezystancji z 15-bitowym przetwornikiem ADC. Odczytany za pomocą łącza SPI wynik pomiaru w zakresie 0…32767 odpowiada stosunkowi dołączonych rezystancji RX/RREF w zakresie 0…1, co oznacza, że wartość RREF musi być większa niż największa spodziewana wartość RX. Dla czujnika PT100 proponuje się RREF = 400Ω (czujnik PT100 ma rezystancję 390,48Ω w temperaturze 850°C).

Ponieważ rezystancja przewodów ma znaczny wpływ (0,4Ω to błąd około 1 stopnia), przewidziano precyzyjny pomiar czteropunktowy (Kelvina) i kompromisowy pomiar 3-punktowy (3-przewodowy).

Fotografia 9

Fotografia 9 pokazuje tani chiński moduł (<5USD), a rysunek 10 pokazuje schemat modułu (wg Adafruit).

Rysunek 10

Kostka MAX31865 ma maksymalne napięcie zasilania 4V, więc jest zasilana napięciem 3,3V ze stabilizatora  (MIC5225-3.3). Wartość rezystancji odniesienia RREF wynosi 430 omów. Przewidziane są cztery zaciski do czteroprzewodowego podłączenia czujnika PT100. Moduł może być zasilany napięciem 5V i współpracować z 5-woltowymi wyjściami cyfrowymi dzięki diodom  D1-D3 (1N4148) i rezystorom podciągającym R6-R8 (10kΩ). Na rysunku 10 mamy przykład niestandardowych oznaczeń końcówek kompatybilnych z łączem SPI. Zamiast SCK mamy SCL, zamiast MOSI mamy SDI, Zamiast MISO mamy SDO i zamiast SS mamy CS. Zwróć uwagę, że 3,3-woltowe wyjście danych (SDO) zostaje podłączone bezpośrednio do pinu Arduino, pracującego jako wejście MISO.

MAX31855

Kostka o pokrewnym numerze MAX31855K pozwala mierzyć temperaturę za pomocą najpopularniejszej termopary typu K. Tak się złożyło, że w tym numerze, w rozwiązaniu zadania Co tu nie gra? Szkoły Konstruktorów, podano szereg informacji na temat termopar. Schemat jednej z wersji pokazany jest na rysunku 11.

Rysunek 11

Ten moduł przeznaczony jest do zasilania napięciem 5V i dzięki diodom i rezystorom podciągającym może współpracować z wyjściami cyfrowymi Arduino.

Ja dysponowałem innym modułem, pokazanym na fotografii 12.

Fotografia 12

Jest to wersja „3-woltowa”, która nie ma ani stabilizatora 3,3V, ani obwodów do współpracy z wyjściami 5-woltowymi.  Moduł można zasilić napięciem 3,3V z płytki Arduino, ale dodatkowo trzeba zastosować jakieś obwody konwersji poziomów logicznych. Mogą to być bardzo proste rozwiązania z dzielnikami rezystorowymi, jak na wcześniejszym rysunku 8, przy czym ta kostka nie ma wejścia danych (MOSI), więc wystarczą dwa dzielniki.

W przypadku tych modułów trzeba zwrócić uwagę, że tu też końcówki są oznaczone niestandardowo. Zamiast SCK mamy oznaczenie CLK, zamiast MISO mamy DO, zamiast SS mamy CS. W ogóle nie ma końcówki wejściowej MOSI (DI), ponieważ z kostki MAX31855 można tylko odczytać dane, a nie można (i nie ma potrzeby) jakichkolwiek danych tam wpisywać. Układ ten ma obwody pomiaru napięcia wytwarzanego przez termoparę oraz obwody pomiaru i kompensacji temperatury zimnego końca (cold junction), obwody wykrywania awarii (fault) oraz 14-bitowy przetwornik ADC. Rozdzielczość pomiaru temperatury gorącego końca wynosi 0,25°C, ale całkowita dokładność to kilka stopni, co i tak jest niezłym wynikiem. Z kostki można też odczytać zmierzoną temperaturę otoczenia (zimnego końca termopary).

Układ testowy

Na potrzeby tego ćwiczenia zestawiłem układ według idei z rysunku 1, którego realizację pokazuje fotografia 13.

Fotografia 13

Linie SCK, MISO, MOSI dołączone są do pinów Arduino 13, 12, 11, ponieważ tam procesor ATmega328 ma wbudowany sprzętowy interfejs SPI. Gdybyśmy wykorzystali inne piny, łącze SPI zrealizowane byłoby programowo i objętość kodu byłaby większa. Do pinów 10, 9, 8 kolejno doąlczone są wejścia zezwalajace SS modułów: czytnika kart SD, modułu MAX31965, MAX31855. Dla 3-woltowego modułu MAX31855 dodane są jeszcze dwa dzielniki według rysunku 8 z rezystorami 2,2kΩ + 3,3kΩ.

Test karty microSD

Aby zacząć fascynującą przygodę z kartami pamięci SD, nie potrzebujemy niczego instalować. Wykorzystamy gotowy szkic Cardinfo, dostępny w przykładach (Plik – Przykłady – SD – Cardinfo). W szkicu trzeba zmienić numer pinu SS (Chip Select) z pinu 4 na pin 10, co pokazane jest na rysunku 14.

Rysunek 14

Po skompilowaniu szkicu i otwarciu okna monitora ja zobaczyłem informacje, pokazane na rysunku 15.

Rysunek 15

Sukces! Ale i pewien mocno niepokojący fakt: czerwone strzałki pokazują, że nasz banalny programik zajął 33% pamięci FLASH i co gorsza, aż 69% pamięci RAM! Cieszymy się, że gotowe biblioteki (SPI.h, SD.h) genialnie ułatwiają pracę, ale już widać, że kod nie jest optymalny, co na przyszłość zwiastuje problemy związane z pamięcią, zarówno programu, jak i RAM.

Ale na razie cieszymy się, że za pomocą Arduino odczytaliśmy właściwości i zawartość tej karty (porównaj rysunek 16).

Rysunek 16

Do obsługi karty też wykorzystamy gotowy szkic (Plik-Przykłady-SD-ReadWrite), tylko dostosowany do naszych potrzeb i dostępny także tutaj  szkic A0701.ino:

#include <SPI.h>
#include <SD.h>
File mojPlik;     //tworzymy obiekt pomocniczy klasy File
void setup() {    // czekamy na otwarcie konsoli monitora w komputerze:
  Serial.begin(9600); while (!Serial) { ; }  //czekaj na otwarcie konsoli
  Serial.println(„Inicjalizacja karty SD…”);
  if(!SD.begin(10)) {Serial.println(„Niestety, nie udalo sie!”); while (1);} //problem
  Serial.println(„Inicjalizacja przebiegla prawidłowo.”); //gdy jest kontakt z kartą
  // otwieramy plik (jednocześnie może być otwarty tylko jeden plik)
   mojPlik = SD.open(„test.txt”, FILE_WRITE); //tworzymy i otwieramy plik test.txt
    if (mojPlik) {  //jeżeli OK, zapisujemy w pliku na karcie SD kawałek tekstu:
    Serial.println(„Wpisujemy do pliku test.txt na karcie SD tekst okolicznosciowy…”);
    mojPlik.println(„Kurs Arduino w EdW”); // okolicznościowy tekst
    mojPlik.close(); //zamykamy plik metodą .close i zapisujemy tym samym na karcie
    Serial.println(„Zrobione! Tekst został zapisany na karcie SD.”);
    Serial.println(„Zamykamy plik test.txt.”);   
  } else {   // jeżeli plik na karcie SD nie dał się otworzyć i zapisać:
        Serial.println(„nie mozna otworzyc pliku test.txt”);  }
 
  mojPlik = SD.open(„test.txt”); // ponownie otwieramy plik na karcie SD (do oczytu):
  if (mojPlik) { //jeśli plik dostępny, wypisujemy na ekranie:
    Serial.println(„”); // najpierw będzie pusta linia
    Serial.println(„Teraz ponownie otwieramy plik test.txt.”); //w nowej linii
    Serial.println(„Oto jego zawartosc:”);                   //w nowej linii
//a teraz czytamy  po kolei wszystkie znaki zawarte w pliku test.txt:
    while (mojPlik.available()) { //jeśli jest jeszcze jakiś znak w pliku
//odczytaj ten znak i od razu wyślij go do konsoli
      Serial.write(mojPlik.read());      }
    mojPlik.close();     // zamknij plik, a jeśli się nie da otworzyć pliku…
  } else {  Serial.println(„nie moge otworzyc pliku test.txt”);    }
}
void loop() { }   // pusta pętla – program wykona się tylko raz

Na początku dołączamy dwie biblioteki: stosunkowo prostą SPI.h do obsługi łącza i bardzo obszerną SD.h do obsługi karty. Następnie tworzymy obiekt o nazwie mojPlik. Będziemy to omawiać bardziej szczegółowo, a na razie potraktuj, że obiekt mojPlik to „rozbudowana zmienna”, która jest (nieznanego nam dotąd) typu File. Jest to odpowiednik deklarowania zmiennej. Instrukcja:

int mojaZmienna;

powoduje utworzenie w programie zmiennej o nazwie mojaZmienna typu int, czyli 16-bitowej. Analogicznie instrukcja

File mojPlik;

stworzy w programie „złożoną zmienną” a fachowo mówiąc: strukturę  o nazwie mojPlik, której szczegóły budowy określone są przez typ, czyli „rodzaj zmiennej” nazwany File. W przeciwieństwie do typu int, typ File nie jest określony w samym języku C ani w C++, tylko jest to typ określony w Arduino.

Te kwestie omówimy dokładniej, a na razie zauważ, że obiekt mojPlik (nieznanego nam typu File) NIE JEST plikiem na karcie SD. Obiekt mojPlik to „dziwna zmienna” w programie.

Aby skorzystać z karty i biblioteki SD, należy przeprowadzić inicjalizację za pomocą funkcji-metody SD.begin(numer_pinu_SS), która zwraca 1 (true) w przypadku sukcesu i 0 (false) w przypadku braku kontaktu z kartą. Polecenie:

if(!SD.begin(10))

przeprowadza inicjalizację (SS = pin 10), a następnie sprawdza, co ta funkcja-metoda zwróciła. Jeśli zero, to wypisuje komunikat o błędzie i wchodzi w pętlę oczekiwania, a jeśli jedynkę (true), to „program idzie dalej”.

Instrukcja:

mojPlik = SD.open(„test.txt”,FILE_WRITE);

wykorzystuje metodę .open() z biblioteki – struktury SD, by otworzyć (do zapisu) plik o nazwie test.txt na karcie SD. Jeśli takiego pliku na karcie jeszcze nie ma, instrukcja ta go stworzy.

Dziwna jest następna linia:

if (mojPlik) { zestaw instrukcji }

Otóż funkcja-metoda SD.open() zwraca wartość 1 (true), gdy wszystko jest w porządku i 0 (false), gdy pliku nie da się otworzyć/stworzyć. Sprawdzamy to właśnie za pomocą instrukcji if (mojPlik) i w przypadku sukcesu program zapisuje w pliku test.txt „tekst okolicznościowy”, który nie zostaje wysłany na ekran konsoli, a tylko do pliku na karcie. Potem zamykamy plik.

Następnie ponownie otwieramy plik i sprawdzamy, co zwróci funkcja-metoda SD.open(). Wypisujemy na ekranie konsoli informacje wstępne, a następnie po kolei, pojedynczo odczytujemy kolejne znaki z pliku.

W tym celu w odniesieniu do naszego programowego obiektu mojPlik, powiązanego z plikiem test.txt, wykorzystujemy funkcję-metodę .available(), która sprawdza, czy w pliku jest do odczytania kolejny znak. Jeśli jest, czyli jeżeli funkcja-metoda mojPlik.available() zwróci jedynkę (true), wtedy kolejny dostępny znak zostaje odczytany. Funkcja-metoda .read odczytuje kolejny znak z pliku i od razu wysyła go przez łącze szeregowe na ekran konsoli:

Serial.write(mojPlik.read());

Następnie zamyka plik bez kasowania zawartości. Dlatego każde kolejne uruchomienie programu – szkicu kolejny raz dopisze „tekst okolicznościowy” do istniejącego pliku. Potem da to efekt jak na rysunku 17.

Rysunek 17

Omówiliśmy drobną część funkcji bibliotek SPI i SD, pora przejść dalej.

Wykorzystanie MAX31865

Najpierw musimy zainstalować stosowną bibliotekę, na przykład Adafruit_MAX31865 (rysunek 18).

Rysunek 18

Możemy wykorzystać gotowy szkic, dostarczony wraz z biblioteką. Szkic 2 pokazuje zmodyfikowaną i skróconą wersję, a pełna jest w pliku A0702.ino:

#include <Adafruit_MAX31865.h>
// tworzymy obiekt klasy Adafruit_MAX31865 o nazwie max
Adafruit_MAX31865 max = Adafruit_MAX31865(9); // u nas CS = pin 9
#define RREF      430.0
#define RNOMINAL  100.0
void setup() {   Serial.begin(9600);
 Serial.println(„Test czujnika PT100 i biblioteki Adafruit MAX31865”);
 max.begin(MAX31865_2WIRE); }  //inicjalizacja czujnika (2 przewody)
void loop() {
 uint16_t rtd = max.readRTD(); //odczyt do 16-bitowej zmiennej rtd
 Serial.print(„Odczytana \”surowa\” wartosc RTD:”);Serial.println(rtd);
 float ratio = rtd;    ratio /= 32768;
 Serial.print(„Stosunek = „); Serial.println(ratio,5);
 Serial.print(„Rezystancja czujnika = „); Serial.println(RREF*ratio,3);
// odczytujemy wartość temperatury 
 Serial.print(„Temperatura =”);
 Serial.println(max.temperature(RNOMINAL, RREF));
 // sprawdzamy ewentualne błędy związane z czujnikiem i pomiarem
 uint8_t fault = max.readFault();
/* dalej procedury zgłaszania błędów: if(fault){ procedury błędów } */
 Serial.println();    delay(4000); }  //nowa linia i przerwa 4 sekundy

Na początku programu tworzymy obiekt o nazwie max typu Adafruit_MAX31865, określając od razu, do której nóżki (9) dołączone jest wejście zezwalające SS. Podajemy, jaka jest wartość rezystora odniesienia RREF w module (430 omów) i rezystancję czujnika (PT100). W funkcji setup przeprowadzamy inicjalizację w trybie pomiaru dwuprzewodowego:

max.begin(MAX31865_2WIRE);

Potem w pętli loop() co 4 sekundy do tymczasowej zmiennej rtd odczytujemy zmierzony stosunek rezystancji czujnika i rezystancji odniesienia RREF. Przesyłamy na ekran konsoli odczytaną „surową” liczbę z zakresu 0…32767, potem ten stosunek w postaci dziesiętnej w zakresie 0…1 i rezystancję czujnika PT100.

Następnie biblioteczna funkcja-metoda .temperature() podaje przeliczoną wartość temperatury. Jeżeli wystąpiły jakieś błędy, program je przedstawi na ekranie konsoli.

Rysunek 19 pokazuje konsolę w czasie pomiarów.

Rysunek 19

Wykorzystanie MAX31855

Znów najpierw należy zainstalować bibliotekę (Adafruit_MAX31855) odpowiednią dla kostki współpracującej z termoparą typu K i znów można skorzystać z dostarczonego z biblioteką przykładowego programu, którego skrócona wersja pokazana jest w szkicu 3 (pełna w Elportalu jako A0703.ino):

#include <SPI.h>
#include „Adafruit_MAX31855.h”
#define MAXCS   8  // u nas jest CS = pin 8
Adafruit_MAX31855 termopara(MAXCS);
 
void setup() { while (!Serial);
 Serial.begin(9600);
 Serial.println(„Test kostki MAX31855”);
 delay(500); } // stabilizacja kostki MAX
void loop() {
  Serial.print(„Temperatura zimnego konca = „);
  Serial.println(termopara.readInternal());
  double c = termopara.readCelsius();
  if (isnan(c)) { //jeżeli c nie jest liczbą
   Serial.println(„blad termopary!”);
  } else { // gdy wszystko w porządku:
   Serial.println(„Temperatura termopary „);
   Serial.print(„w stopniach Celsjusza: T = „);
   Serial.println(c); }
 delay(3000);    }

Na pewno w szkicu trzeba utworzyć obiekt reprezentujący moduł pomiarowy i zmienić numer nóżki sterującej wejściem zezwalającym SS (CS) – u nas to pin 8. Nie jest potrzebna dodatkowa inicjalizacja. W głównej pętli programu odczytywana ma być i prezentowana na ekranie konsoli temperatura otoczenia (zimnego końca – cold junction) i temperatura końca pomiarowego termopary.

Niestety podczas prób modelu z fotografii 13 z nieznanych powodów (najprawdopodobniej uszkodzenia modułu) konsola wyglądała jak na rysunku 20.

Rysunek 20

Niezależnie od tego szkic 3 pokazuje, że przy użyciu biblioteki wykorzystanie tego modułu z interfejsem SPI jest bardzo proste.

Oczywiście nie omówiliśmy wielu funkcji-metod, dostępnych we wszystkich wspomnianych bibliotekach. Zachęcam Czytelników, żeby samodzielnie przeprowadzili dalsze eksperymenty, zwłaszcza z czytnikiem kart (micro)SD, który okaże się bardzo pożyteczny w wielu konstrukcjach. A w następnym odcinku UR008 u nadal będziemy zajmować się pomiarami parametrów atmosferycznych i spróbujemy wykorzystać czujnik dwutlenku węgla.

Piotr Górecki