Znamy już podstawy programowania naszego Arduino, pora przygotowanie nieco większego programu, a może nawet własnej biblioteki.
Wiele układów peryferyjnych korzysta z interfejsu szeregowego podobnego do omówionego w poprzednim odcinku rozbudowując do o możliwość adresowania poszczególnych urządzeń, a więc podłączania do tych samych przewodów wielu układów peryferyjnych. Potrzebna jest nam także komunikacja dwukierunkowa – najlepiej po tym samym połączeniu tak by można było pytać o stan urządzenia i odbierać od niego wynik.
To że Arduino potrafi używać pojedynczej go wyprowadzenia zarówno jako wejścia jak i wyjścia – to wiemy, ale jak zorganizować połączenie które pozwoli nie tylko zmieniać role, ale także rozwiązywać problem kolizji – czyli sytuacji w której więcej niż jedno urządzenie będzie próbowało nadawać? Najprostszym rozwiązaniem jest kontrolowanie stanu linii komunikacyjnej przez procesor, jednak potrzebny będzie jeszcze jeden dodatkowy trik.
Jeśli do jednego przewodu podłączymy kilka urządzeń z których każde będzie nadawać sygnał cyfrowy – to w przypadku gdy jeden układ ustawi logiczną wartość 1 a inny 0 – to w przypadku normalnych wyjść które w zależności od stanu są zwarte z zasilaniem lub z masą – będziemy mieli coś w rodzaju zwarcia1. Do tego nie można dopuścić. Dlatego wyjścia układów przystosowanych do takiej komunikacji pracują inaczej – wymuszają na połączeniu wyłącznie stan niski zwierając go z masą, pozostawiając nieodłączone jeśli nadają stan wysoki.

Aby utrzymać stan wysoki na linii komunikacyjnej – niezbędny jest opornik który zwiera ją na stałe z zasilaniem. Jego wartość jest z dołu ograniczona przez prąd który może płynąć przez tranzystory, z drugiej – przez wymaganą prędkość przesyłania informacji. Może to wydać się dziwne, ale im większa będzie oporność, tym wolniej wspólna linia będzie wracała do stanu wysokiego. Dzieje się tak ponieważ każdy przewodnik ma pewna niezerową pojemność elektryczną zależną od jego kształtu, rozmiarów i odległości od innych przewodów. Im dłuższy będziemy mieli przewód łączący z układem peryferyjnym i im bliżej będzie od przewodu masy lub zasilania – tym większą będzie miała pojemność dochodząca nawet do kilkuset pikofaradów. Na szczęście indukcyjność przewodu przy krótkich połączeniach (do kilkudziesięciu centymetrów) możemy pominąć.
W ramach kursu, przygotujemy trzy połączenia szeregowe z układami peryferyjnymi które mamy do dyspozycji i które użyjemy do przygotowania prostej stacji pogodowej dla niewielkiego kąpieliska lub żaglówki. Stacja ta będzie mierzyć temperaturę i wilgotność powietrza, temperaturę wody oraz ciśnienie. Czyjemu detektorów:
- Cyfrowego termometru DS18B20 w szczelnej, odpornej na wodę obudowie,
- Czujnika temperatury i wilgotności AM2302
- Czujnika ciśnienia BMP180.
Wszystkie te układy pracują w oparciu o interfejs szeregowy, jednak każdy działa na innym protokole – to znaczy w inny sposób interpretuje zmianę sygnałów na wspólnej linii, dlatego musimy je podłączyć do oddzielnych wyprowadzeń Arduino:
Zacznijmy od czujnika temperatury i wilgotności. Czujnik AM2302 pracuje na pojedynczym przewodzie i czeka na polecenia rozpoczęcia pomiaru. Polecenie to wydaje procesor ustawiając stan niski na określony czas. Czujnik potwierdza ustawiając stan niski na następnie zaczyna nadawać stosując do kodowania różne długości impulsów.
Wygląda to prosto i nie stanowi specjalnego wyzwania programistycznego, jednak przy okazji możemy przygotować sobie kilka przydatnych komponentów. Na początek napiszmy sterownik linii. Będziemy z niego korzystali przy oprogramowaniu bardziej zaawansowanych układów. Sterownik ten jest bardzo prosty – potrzebujemy w nim operacji ustawiania stanu 1 oraz 0 na linii oraz sprawdzenia jaki jest obecnie stan. Przyda się nam także odbieranie impulsów o niskim lub wysokim stanie, pozwalające na sprawdzenie jak długo trwał taki impuls. Całość zamkniemy w klasie wraz ze zmienną w której zapamiętamy na jakim wyprowadzeniu Arduino realizujemy te wszystkie operacje.
class single_wire_connection_driver {
public:
single_wire_connection_driver(byte pin)
: communication_pin(pin) {
high();
}
public:
void high() {
// pull up resistor
digitalWrite(communication_pin, HIGH);
pinMode(communication_pin, INPUT);
}
void low(){
pinMode(communication_pin, OUTPUT);
digitalWrite(communication_pin, LOW);
}
bool in() {
return digitalRead(communication_pin);
}
unsigned in_low_pulse() {
return pulseIn(communication_pin, LOW);
}
unsigned in_high_pulse() {
return pulseIn(communication_pin, HIGH);
}
protected:
byte communication_pin;
} ;
Metody nie są specjalnie skomplikowane, i wiele z nich odwołuje się do funkcji bibliotecznych, jednak warto je zebrać razem i ponazywać w sposób który coś nam będzie mówił.
Sam pomiar możemy zrealizować dość prosto. Wysyłamy krótki sygnał na linię komunikacyjną, a następnie czekamy na impulsy o odpowiednich długościach, a w zasadzie czekamy na odpowiedź termometru a następnie na impulsy wysokie, mierząc ich czas i dodając do odbieranych wartości – odpowiednie bity. AM2302 nadaje zawsze pięć bajtów – dwa dla temperatury, dwa dla wilgotności oraz sumę kontrolna która pozwala sprawdzić czy nie pojawiły się błędy w czasie komunikacji.:
driver.low();
delay(2);
driver.high();
unsigned pulse = driver.in_low_pulse();
if (pulse == 0) {
return false;
}
byte received[5];
for (byte i=0; i<5; ++i) {
received[i] = 0;
for (byte bit_mask = 0x80; bit_mask; bit_mask >>= 1) {
pulse = driver.in_high_pulse();
if (!pulse) {
return false;
}
if (pulse > 45) {
received[i] |= bit_mask;
}
}
}
byte checksum = received[0]+received[1]+received[2]+received[3];
if (checksum !=received[4]) {
return false;
humidity = (received[0]*256.0 + received[1])/10;
temperature = ((received[2]&0x7f)*256.0 + received[3])/10;
if (received[2] & 0x80) {
temperature = -temperature;
}
return true;
W przedstawionym fragmencie kodu założyliśmy że driver jest obiektem typu single_wire_connection_driver a zmienne humidity oraz temperature są typu float. Wartości te będą zdefiniowane w klasie w której umieścimy powyższy kod.
Wartości temperatury i wilgotności – musimy zbudować z dwóch bajtów, dodatkowo pamiętając że temperatury ujemne są kodowane dość dziwnie – najstarszy bit oznacza znak, a reszta bitów – wartość bezwzględna temperatury.
Może się też nam wydać dziwny sposób budowania pętli, ale zamiast liczyć od 0 do 7, prościej jest przesuwać pojedynczy bit, aż całkiem wypadnie z bajtu. Wartość zmiennej kontrolnej pętli przy okazji służy nam za wartość którą dodajemy operatorem |= (który liczy sumę logiczną i umieszcza jej wynik w pierwszym argumencie).
Program jest w zasadzie napisany, jednak warto byłoby jeszcze chwilę się zastanowić, nad organizacją kodu. Pamiętajmy, że w programie często mamy do czynienia z wieloma urządzeniami pomiarowymi, i nie zawsze chcemy czy każdym razem odpytywać urządzenie. W naszym przypadku układ pomiarowy zwraca od razu obie wartości, dlatego wystarczy go raz zapytać, by potem odczytać wartości które zostały przesłane.
Oddzielenie pomiaru od odczytania wartości, warto przygotować sobie jako oddzielne klasy, które będziemy mogli dołączyć do obsługi naszego przyrządu, i użyć ich ponownie w przy okazji oprogramowywanie kolejnych przyrządów. Warto więc przyjrzeć się innym układom pomiarowym i zastanowić jak to zorganizować.
Niektóre przyrządy – jak na przykład ciśnieniomierz BMP180 czy termometr DS18B20 wymagają zainicjowania pomiaru. Do takich urządzeń musimy na początku wysłać sygnał inicjujący pomiar, a następnie po odczekaniu określonego czasu – możemy odczytać zmierzone wartości.
Dlatego proponuję przygotować dodatkowe klasy:
- dla urządzenia pomiarowego – klasa która pozwoli na
- zainicjowanie pomiaru
- poinformowaniu jaki czas należy odczekać do momentu odczytu danych z urządzenia
- odebranie danych z przyrządu pomiarowego.
- termometr klasa pozwalająca na zapamiętanie temperatury i zwrócenie jej w postaci liczby
- higrometr – podobnie jak termometr, ale dla wilgotności.
class measuring_device_interface {
public:
// initialize device and start
virtual void start_measuring()
{ }
// time [ms] need to wait for result
virtual int measurning_time() {
return 1;
}
// read the data into buffer
virtual bool read_data() = 0;
public:
void measure() {
start_measuring();
delay(measurning_time());
read_data();
}
} ;
class thermometer_interface {
public:
float get_temperature() { // [C]
return temperature;
}
protected:
float temperature;
} ;
class higrometer_interface {
public:
float get_humidity() { // [%]
return humidity;
}
protected:
float humidity;
} ;
Klasy te tworzą interfejsy które są bardzo przydatne w budowie bardziej skomplikowanych aplikacji. Jeśli nasz program wymaga użycia gdzieś termometru – wystarczy, że użyjemy dowolnej klasy która jest wyprowadzona (dziedziczy) z klasy thermometer_interface, i wszystko będzie poprawnie działać. Określenie po czym nasza klasa dziedziczy podajemy po nazwie klasy:
class am2302_device
: public thermometer_interface
, public higrometer_interface
, public measuring_device_interface {
public:
am2302_device(byte pin) : driver(pin) { }
public:
bool read_data();
private:
single_wire_connection_driver driver;
} ;
bool am2302_device::read_data() {
driver.low();
delay(2);
driver.high();
unsigned pulse = driver.in_low_pulse();
if (pulse == 0) {
return false;
}
byte received[5];
for (byte i=0; i<5; ++i) {
received[i] = 0;
for (byte bit_mask = 0x80; bit_mask; bit_mask >>= 1) {
pulse = driver.in_high_pulse();
if (!pulse) {
return false;
}
if (pulse > 45) {
received[i] |= bit_mask;
}
}
}
byte checksum = received[0]+received[1]+received[2]+received[3];
humidity = (received[0]*256.0 + received[1])/10;
temperature = ((received[2]&0x7f)*256.0 + received[3])/10;
if (received[2] & 0x80) {
temperature = -temperature;
}
return (checksum == received[4]);
}
Zdefiniowaliśmy tu klasę implementującą połęczenie i odczyt danych z urządzenia, która dziedziczy z klasy interfejsu przyrządu pomiarowego oraz termometru i barometru. Dziedziczenie oznacza, że obiekty naszej klasy mogą być użyte w miejscu gdzie wymagane są obiekty klasy z których dziedziczymy.
Na uwagę zasługują tu jeszcze dwa elementy języka: W klasach w niektórych miejscach piszemy private, public lub protected. Oznaczają one, jak dostępne będą elementy klas które po nich następują (aż do kolejnego określenia dostępu).
- public – oznacza, że lementu klasy są publiczne – to znaczy każdy może ich używać.
- private – że tylko obiekty tej właśnie klasy maja dostęp do tych elementów
- protected – że tylko obiekty tej klasy oraz klas które z niej dziedziczą – mają do nich dostę..
Kolejnym elementem jest słowo kluczowe virtual oznaczające, że implementacja oznaczonej tak metody może zostać nadpisania w klasie pochodnej, i będzie ważna także dla wywołań poprzez klasę bazową. Jeśli po nazwie metody napiszemy =0; – oznacza to, że klasa pochodna musi dostarczyć implementacji, bo w klasie bazowej nie da się jej określić.
Użycie interfejsów pozwala na standaryzację dostępu do określonych funkcjonalności mimo dużych różnic w implementacjach.
Zadanie:
Napisz program który skorzysta z drivera i będzie na bieżąco podawał temperaturę i wilgotność na porcie szeregowym.
Literatura
- http://web.sensor-ic.com:8000/ZLXIAZAI/AOSONG/2010105155657352.pdf