Primjer pisanja vlastite mbed biblioteke
Kreiranje HC-SR04 klase¶
Iako već postoje klase za ultrazvučno mjerenje udaljenosti, u ovoj vježbi izgradit ćemo vlastitu klasu za senzor HC-SR04. To je prikladan način za učenje i vježbanje razvoja vlastitih klasa i biblioteka.
Najprije ćemo pogledati reviziju 0 programa napisanog u prethodnoj vježbi Tajmeri i prekidi. Program možete uvesti s donje poveznice:
Import programHC-SR04
A program that demonstrates the development of library, using as an example an ultrasonic distance sensor HC-SR04.
Napomena
Revizije programa i način korištenja u svrhu suradnje s drugim korisnicima opisane su ovdje.
Datoteka main.cpp
sadrži sljedeće dijelove, koji zajedno obavljaju zadatak mjerenja udaljenosti:
- 2 mikroupravljačka pina (
p5
ip7
), - 3 glavna objekta (
echo
,trigger
itimer
), - 2 funkcije (
startTimer()
istopTimer()
), - konfiguraciju za rastući i padajući brid
echo
objekta na početkumain.cpp
datoteke, - dio kôda za pokretanje mjerenja i izračun udaljenosti.
Ova zapažanja ćemo sada pokušati integrirati u klasu. Klasa sadrži deklaracijski dio (prototip) i implementacijski dio. Prvo ćemo napisati deklaracijski dio klase.
Deklaracijski dio klase¶
Prvo deklariramo klasu imena HC-SR04
. U javnom dijelu klase (public
) najprije deklariramo (napišemo prototip) konstruktor. Iz gornjih opažanja znamo da ćemo instancirati naše HC-SR04
objekte s dva pina, tako da naš konstruktor mora primiti dva argumenta tipa PinName
, npr. echoPin
i triggerPin
. Ako prilikom deklaracije javne funkcije imamo potrebu za argumentima funkcije, kao u ovom slučaju, poželjno je odmah te argumente i dokumentirati pomoću markup opcije @param
(linije 6 i 7 u kôdu ispod).
Nadalje, integriramo naša 3 objekta u privatni dio deklaracije klase (private
), kako bi ih zaštitili od neželjenih promjena izvan klase. U ovom trenutku tim objektima nije moguće pridijeliti i pinove (npr. objektima echo
i trigger
), koji se inače traže pri instanciraju. Razlog tome je što se deklaracijom klase još uvijek ne instanciraju objekti. Budući da se objekti instanciraju pozivom konstruktora, ovo pridruživanje pinova objektima ćemo riješiti kasnije u implementacijskom dijelu konstruktora.
U idućem koraku dodajemo naše dvije funkcije u deklaraciju klase. Pri tome moramo odlučiti da li ćemo ih smjestiti u public
ili private
dio klase. Budući da korisnici klase nemaju potrebu za eksplicitnim korištenjem tih funkcija, one se smještaju pod private
dijelom klase. Prilikom kopiranja zaglavlja funkcija poželjno je obuhvatiti i komentare iznad zaglavlja, koji će kasnije poslužiti za automatsko dokumentiranje klase.
Nadalje, konfiguracije rastućih i padajućih bridova echo
objekta, kao i sve ostale konfiguracije, bit će implementirane pozivom init()
funkcije u konstruktoru. Zbog toga ćemo zasad samo dodati deklaraciju init()
funkcije u private
dio deklaracije klase.
Konačno, dodajmo deklaracije funkcija za pokretanje mjerenja te izračun i vraćanje vrijednosti izračunate udaljenosti. Koja od tih funkcija bi trebala biti private
, a koja public
? Kod funkcija koje vraćaju neku vrijednost možemo dokumentirati i povratni tip, tj. varijablu koju funkcija vraća, korištenjem markup opcije @returns
. Dodajmo i varijablu za smještaj podatka o izračunatoj udaljenosti u private
dio deklaracijskog dijela klase.
Deklaracijski dio klase bi sada trebao izgledati nekako ovako:
class HCSR04 { public: /** Receives two PinName variables. * @param echoPin mbed pin to which the echo signal is connected to * @param triggerPin mbed pin to which the trigger signal is connected to */ HCSR04(PinName echoPin, PinName triggerPin); /** Calculates the distance in cm, with the calculation time of 25 ms. * @returns distance of the measuring object in cm. */ float getDistance_cm(); private: InterruptIn echo; // echo pin DigitalOut trigger; // trigger pin Timer timer; // echo pulsewidth measurement float distance; // store the distance in cm /** Start the timer. */ void startTimer(); /** Stop the timer. */ void stopTimer(); /** Initialization. */ void init(); /** Start the measurement. */ void startMeasurement(); };
Sada možemo prijeći na implementacijski dio klase.
Implementacijski dio klase¶
Prvo ćemo implementirati konstruktor. Kao što je spomenuto u deklaracijskom dijelu klase, sve inicijalizacije moraju se izvesti u konstruktoru. Pridruživanje pinova je obvezno i izvodi se pomoću :
operatora, neposredno nakon zaglavlja konstruktora (linija 1 kôda ispod). Sve ostale inicijalizacije se izvode pozivom init()
funkcije (linija 2):
HCSR04::HCSR04(PinName echoPin, PinName triggerPin) : echo(echoPin), trigger(triggerPin) { init(); }
Zašto je dobra ideja pozivati posebnu funkciju, poput init()
, umjesto pisanja inicijalizacijskog kôda izravno u konstruktoru? Kasnije se možemo odlučiti implementirati drugi konstruktor (ili više njih), u kojima se mogu realizirate i neke dodatne ili drukčije opcije. Kad bi pisali inicijalizacijski kôd u konstruktoru, onda bi morali kopirati taj dio u svaki konstruktor, što nije baš praktično. Nakon eventualnih promjena prilikom razvoja kôda, te promjene bi trebalo raditi na svim mjestima gdje je kôd kopiran. Pogledajmo implementaciju init()
funkcije:
void HCSR04::init() { /** configure the rising edge to start the timer */ echo.rise(this, &HCSR04::startTimer); /** configure the falling edge to stop the timer */ echo.fall(this, &HCSR04::stopTimer); distance = -1; // initial distance }
U konfiguracijama rastućeg i padajućeg brida echo
pina sada imamo i pokazivač this
. Razlog tome je što su funkcije pripadnice klase smještene u zasebnom dijelu memorije, odvojeno od instanciranih objekata. Kad objekt poziva funkciju, on mora proslijediti svoju adresu toj funkciji, kako bi funkcija znala nad kojim objektom mora izvršiti svoje zadatke. Adresa objekta je spremljena upravo u pokazivaču this
.
Još jedna sitnica koju možemo primijetiti je inicijalizacija varijable za spremanje udaljenosti (distance
) na nerealnu vrijednost -1
. Ta vrijednost nam može poslužiti kao indikator da mjerenje još nije bilo pokrenuto. Dobra programerska praksa je inicijalizirati sve varijable u konstruktoru.
Ostatak implementacija je relativno jednostavno prenijeti iz main.cpp
datoteke revizije 0 programa. Revizija 1 programa sadrži dosad napravljeni deklaracijski i implementacijski dio klase, kao i main()
funkciju s instanciranim objektom i primjerom mjerenja.
Razdvajanje u datoteke¶
Slijedeći logičan korak je razdvajanje deklaracijskog i implementacijskog dijela klase, kao i main()
funkcije u zasebne datoteke. Stvorimo dvije nove datoteke unutar našeg programa (programsko stablo s lijeve strane). Nazovimo te datoteke HCSR04.h
(za deklaracijski dio klase) i HCSR04.cpp
(za implementacijski dio klase). Zatim kreiramo novu biblioteku (New Library) i nazovemo je istim imenom kao i klasu HCSR04
. Zatim naše dvije datoteke odvučemo pod novostvorenu biblioteku (drag&drop ili cut-paste metodom).
Ako nakon ovog postupka pokušamo prevesti program, dobit ćemo pogrešku. Očito će biti potrebno još neke informacije dati samom prevoditelju, kako bi znao gdje potražiti naš kôd.
Prvo pogledajmo našu trenutnu main.cpp
datoteku.
main.cpp
/** Revision 2: Separating the class declaration, implementation and main() function into files. */ #include "mbed.h" Serial pc(USBTX, USBRX); // communication with terminal int main() { HCSR04 sensor(p5, p7); // instantiate the sensor object while(1) { /** Print the result in cm to the terminal with 1 decimal place * (number 5 after % means that total of 5 digits will be reserved * for printing the number, including the dot and one decimal place). */ pc.printf("Distance: %5.1f cm\r", sensor.getDistance_cm()); wait_ms(1000-25); } }
Pogreška pri prevođenju (compile time error) uzrokovana je u liniji 8, zbog toga što prevoditelj ne zna za postojanje naše klase (biblioteke) HCSR04
. Mi mu moramo eksplicitno reći da uključi našu biblioteku u program. Dakle, u liniji 4 jednostavno dodamo:
Add to line 4 of main.cpp
#include "HCSR04.h"
Deklaracija HCSR04.h
je dobra i u trenutnom obliku, ali u ovom trenutku poželjno je dodati opis klase zbog kasnijeg automatskog generiranja dokumentacije. Obično uz opis klase dodajemo i primjer načina na koji se klasa koristi (kao u našoj main.cpp
datoteci) markup opcijama @code
i @endcode
, kako bi korisnici koji će eventualno koristiti tu klasu potrošili čim manje vremena na odgonetavanje kako se klasa koristi. Datoteka HCSR04.h
bi sad trebala izgledati nekako ovako:
HCSR04.h
/** A distance measurement class using ultrasonic sensor HC-SR04. * * Example of use: * @code * #include "mbed.h" * #include "HCSR04.h" * * Serial pc(USBTX, USBRX); // communication with terminal * * int main() { * HCSR04 sensor(p5, p7); // instantiate the sensor object * while(1) { * pc.printf("Distance: %5.1f cm\r", sensor.getDistance_cm()); * wait_ms(975); // print the result every 1 second * } * } * @endcode */ class HCSR04 { public: /** Receives two PinName variables. * @param echoPin mbed pin to which the echo signal is connected to * @param triggerPin mbed pin to which the trigger signal is connected to */ HCSR04(PinName echoPin, PinName triggerPin); /** Calculates the distance in cm, with the calculation time of 25 ms. * @returns distance of the measuring object in cm. */ float getDistance_cm(); private: InterruptIn echo; // echo pin DigitalOut trigger; // trigger pin Timer timer; // echo pulsewidth measurement float distance; // store the distance in cm /** Start the timer. */ void startTimer(); /** Stop the timer. */ void stopTimer(); /** Initialization. */ void init(); /** Start the measurement. */ void startMeasurement(); };
Program još uvijek nije moguće prevesti, jer u implementacijskom dijelu klase nedostaju prototipovi (deklaracije) funkcija pripadnica klase koje su implementirane, kao i prototipovi funkcija iz mbed.h
biblioteke. Ovo se jednostavno rješava uključivanjem tih deklaracija (#include
naredbe) na početku HCSR04.cpp
datoteke (linije 1 i 2):
HCSR04.cpp
#include "mbed.h" #include "HCSR04.h" HCSR04::HCSR04(PinName echoPin, PinName triggerPin) : echo(echoPin), trigger(triggerPin) { init(); } void HCSR04::init() { /** configure the rising edge to start the timer */ echo.rise(this, &HCSR04::startTimer); /** configure the falling edge to stop the timer */ echo.fall(this, &HCSR04::stopTimer); distance = -1; // initial distance } void HCSR04::startTimer() { timer.start(); // start the timer } void HCSR04::stopTimer() { timer.stop(); // stop the timer } void HCSR04::startMeasurement() { /** Start the measurement by sending the 10us trigger pulse. */ trigger = 1; wait_us(10); trigger = 0; /** Wait for the sensor to finish measurement (generate rise and fall interrupts). * Minimum wait time is determined by maximum measurement distance of 400 cm. * t_min = 400 * 58 = 23200 us = 23.2 ms */ wait_ms(25); /** calculate the distance in cm */ distance = timer.read() * 1e6 / 58; timer.reset(); // reset the timer to 0 after storing the distance } float HCSR04::getDistance_cm() { startMeasurement(); return distance; }
Naš kôd sad se može prevesti bez pogrešaka. Posljednji zadatak nam je dodati tzv. include čuvare u deklaracijsku datoteku, kako bi se spriječilo višestruko uključivanje naše biblioteke u neki drugi program. Kompletan program i dosad razvijena biblioteka je objavljena u reviziji 2 našeg programa. Potrebno je napomenuti kako je sad naša biblioteka razdvojena od glavnog programa i može se modificirati bez promjene glavnog programa. Također možemo pokrenuti i novi program i uključiti u njega biblioteku HCSR04
.
Import libraryHCSR04
A distance measurement class using ultrasonic sensor HC-SR04.
Do ove točke razvoja biblioteke imamo početnu reviziju i dvije manje revizije vezane za dokumentaciju. Prebacimo se na reviziju 2 biblioteke i pogledajmo automatski generiranu dokumentaciju. Dokumentacija se može pogledati unutar prevoditelja ili na wiki stranicama biblioteke. Pri čitanju dokumentacije sve bi trebalo biti dovoljno jasno. Ako smo zadovoljni trenutnim stanjem biblioteke i njenom dokumentacijom, možemo početi s testiranjem senzora i njegovim korištenjem unutar neke realne aplikacije.
Poboljšanje biblioteke¶
Prilikom testiranja biblioteke s HC-SR04 ultrazvučnim senzorom, možemo primijetiti čudna očitanja kad je mjerena udaljenost ispod 4 cm i iznad 400 cm. Ove vrijednosti bi se u klasi trebale postaviti kao granične, te ih interpretirati kao <4 cm i >400 cm. Pokušajte implementirati ova ograničenja u klasi. Jedno od mogućih rješenja dano je u reviziji 3 biblioteke.
Nadalje, dodajte funkciju pripadnicu klase pomoću koje korisnik može podesiti navedene granice u tvornički deklariranom rasponu od 2 do 400 cm. Neka pretpostavljene (default) vrijednosti budu upravo u tvornički deklariranom rasponu. Dodatno, omogućite korisniku dohvaćanje udaljenosti u milimetrima, te dohvaćanje minimalne i maksimalne udaljenosti koje je korisnik namjestio. Moguće rješenje dano je u reviziji 5 biblioteke, a primjer korištenja dan je u reviziji 3 programa.
Pokušajte primijetiti tipični primjer logički pogrešno implementirane funkcije pripadnice klase setRanges()
, koji se u programima često naziva bug, te pokušajte ispraviti taj bug.
Dodatno poboljšanje biblioteke možemo napraviti ukoliko uklonimo sve pozive blokirajućih wait()
funkcija. Primjer neispravnog rada biblioteke u kombinaciji s Ticker
objektom u main()
funkciji možete pogledati u reviziji 4 programa s filtriranjem. Zbog tog problema napisana je verzija 5 programa, u kojoj je moguće vršiti filtriranje, međutim, puno je bolje rješenje modificirati klasu tako da u njoj nema blokirajućih funkcija. Moguće poboljšanje dano je u reviziji 6 biblioteke.
Korištenje biblioteke uz filtriranje¶
Revizija 6 programa demonstrira korištenje biblioteke s filtriranjem pomoću PT1 filtra s vremenskom konstantom od 2 sekunde. Mjerni rezultati za veće udaljenosti (>100 cm) su sada stabilniji, ali pri naglim promjenama udaljenosti, filtrirani rezultat treba otprilike 10 sekundi za stabilizaciju mjerenja (otprilike 5 vremenskih konstanti). Ovo kašnjenje se mora uzeti u obzir pri projektiranju sustava upravljanja s dotičnim senzorom i filtrom.
Čestitke!
Završili ste sve vježbe iz teme Primjer pisanja vlastite mbed biblioteke.
Povratak na naslovnu stranicu TVZ Mechatronics Team-a.