Autor: legier
Redakcja: Dondu
Dalmierz ultradźwiękowy jest urządzeniem o stosunkowo prostej konstrukcji i zasadzie działania. W wielkim skrócie jego działanie opiera się na wysłaniu paczki impulsów ultradźwiękowych, a następnie nasłuchiwaniu echa (powrót impulsów po odbiciu od przeszkody).
Czas między nadaniem, a odbiorem pomnożony przez prędkość dźwięku da nam całkowitą odległość przebytą przez impulsy. Trzeba tylko zwrócić uwagę, że jest ona 2 razy większa niż odległość od przeszkody, gdyż fala musiała najpierw do niej dotrzeć, a potem pokonać drogę powrotną.
Z uzyskanych pomiarów wynika, że błąd pokazywanego wyniku wynosi około +/-1cm, a zasięg wynosi od 0,5 metra do kilku metrów.
Dalmierz ultradźwiękowy |
Układ nadajnika
Układ nadajnika składa się jedynie z nadajnika oraz kanałów PWM timera1. Jeden pin nadajnika podłączony jest do OC1A, a drugi do OC1B. W przerwaniu od zrównania tego licznika generuję najpierw stan wysoki na OC1A i stan niski na OC1B, a następnie stan niski na OC1A i wysoki na OC1B. Dzięki temu napięcie między szczytowe na nadajniku wynosi 10V. Wszystko jest przeliczone tak, aby uzyskać częstotliwość 40kHz (częstotliwość podana przez producenta nadajnika).
Układ odbiornika
Z odbiornikiem nie jest już tak prosto jak z nadajnikiem. Dlaczego? Oto kilka powodów:
- Niska sprawność nadajnika
- Bardzo szybki wzrost pola powierzchni fali, przez co energia przypadająca na m^2 bardzo szybko maleje
- Niska sprawność odbiornika
- Zakłócenia
Na 1. WO (Wzmacniaczu Operacyjnym) ustawiłem teoretyczne wzmocnienie około 25, a następnie jego wyjście podłączyłem do wejścia kolejnego wzmacniacza, na którym jest wzmocnienie około 47, więc całkowite wzmocnienie teoretycznie wyniosło 1170.
Czemu teoretyczne? W nocie katalogowej wzmacniacza możemy wyczytać, że w zależności od częstotliwości jego wzmocnienie może się zmieniać. Dla częstotliwości 40kHz i napięcia zasilania 5V ten wzmacniacz charakteryzuje się wzmocnieniem rzędu 20dB, co daje nam praktyczne wzmocnienie około: 400 razy.
Dlaczego minimalny zasięg wynosi zaledwie 0,5m? Wiąże się to z tym, że po wysłaniu paczki impulsów z nadajnika odbiornik od razu je wykrywa co spowodowałoby pokazanie odległości rzędu 2cm. Dlatego trzeba wprowadzić opóźnienie włączenia nasłuchiwania echa, ze względu na drgania, w które na początku został wprowadzony odbiornik.
Wykres zależności wzmocnienia wzmacniacza od częstotliwości |
Wyjście tego układu wzmacniaczy doprowadzam do pinu AIN0 (wejście nieodwracające komparatora), a na wejście AIN1 (wejście odwracające komparatora) podałem napięcie odniesienia wzmacniaczy, tj. Vcc/2 czyli około 2,5V.
Schemat układu odbiornika |
Czujnik temperatury
Do odczytu temperatury użyłem popularnego czujnika ds18b20. Więcej o konwersji temperatury możecie poczytać tutaj: Temperatura - pomiar.
Po co ten czujnik? Otóż prędkość dźwięku w powietrzu najbardziej zależy od temperatury. Jeśli różnica temperatury w 2 miejscach gdzie wykonywaliśmy pomiary wyniesie np. około 10 stopni celsjusza, to dalmierz, bez wprowadzonej korekty mógłby pokazać wyniki różniące się między sobą nawet o kilka-kilkanaście centymetrów. Włożenie czujnika nie jest obowiązkowe - jeśli mikrokontroler go nie wykryje, przyjmie za prędkość dźwięku standardową wartość 343,5m/s.
Przybliżony wzór, według którego jest obliczana prędkość dźwięku:
Buzzer
Dodałem buzzer do projektu, aby móc użyć dalmierza jako tzw. "czujnik parkowania". W programie w zależności od uzyskanej odległości generuję z niego dźwięki o rożnym wypełnieniu - im mniejsza odległość tym szybciej pika.
Schemat układu buzzera |
Wyświetlacz LCD
Do wyświetlenia odległości użyłem standardowego wyświetlacza LCD 8x2. Podłączenie tego wyświetlacza jest chyba oczywiste i wynika ze schematu. Jedyne co, to skupię się na opisie jego zasilania. Wyświetlacz włączy się po ustawieniu pinu PD4 w rejestrze PORTD na 1.
Regulacja kontrastu odbywa się za pomocą jednego z potencjometrów. Drugi wraz z fotorezystorem służy za analogową zmianę jasności wyświetlacza. Potencjometrem możemy oczywiście regulować tę jasność.
Schemat układu LCD |
Schemat i płytka
Schemat dalmierza ultradźwiękowego |
Płytka drukowana dalmierza ultradźwiękowego |
Do pobrania pliki Eagle: Dalmierz.rar (kopia)
Część programowa
Wyświetlacz LCD
Do obsługi wyświetlacza użyłem darmowej biblioteki udostępnionej przez autora na stronie: radzio.dxp.pl/hd44780
Działa on w trybie 4 bitowym z obsługą flagi zajętości.
Czujnik temperatury DS18B20
Swego czasu pisałem sobie kiedyś bibliotekę obsługi tego czujnika (interfejs 1-wire). Do tego wystarczy tylko kilka funkcji, które opisałem w komentarzach kodu.
//********************************************************* //////////////////FUNKCJE OBSLUGI DS18B20////////////////// //********************************************************* void send_bit(char bit) //wysłanie jednego bitu na magistralę { clear_1wire; _delay_us(10); if(bit == 1) set_1wire; _delay_us(60); set_1wire; _delay_us(3); } void send_byte(unsigned char value) //wysłanie jednego bajtu na magsitralę { char i; unsigned char sub_value; for(i = 0; i < 8; i++) { sub_value = value >> i; sub_value = 0x01 & sub_value; send_bit(sub_value); } } unsigned char read_bit(void) //odebranie jednego bitu z magistrali { clear_1wire; _delay_us(3); set_1wire; _delay_us(11); if(state) { _delay_us(60); return 1; } else { _delay_us(60); return 0; } } unsigned char read_byte(void) //odebranie jednego bajtu z magistrali { unsigned char value = 0; char i; for(i = 0; i < 8; i++) { if(read_bit()) value |= 0x01 << i; } return value; } char reset(void) //zresetowanie magistrali { char presence = 0; clear_1wire; _delay_us(500); set_1wire; _delay_us(60); if(state == 0) presence = 1; _delay_us(500); return presence; } inline void skip_rom(void) { send_byte(0xCC); //skip rom } void write_scratchpad(void) { send_byte(0x4E); //write scratchpad send_byte(Th); send_byte(Tl); send_byte(31 + ((precision - 9) << 5)); } void read_scratchpad(char *bytes) // { char i; send_byte(0xBE); //read scratchpad for(i = 0; i < 2; i++) { bytes[i] = read_byte(); } } void convert_t(void) { char n; send_byte(0x44); //convert T n = 0x01 << (precision - 9); while(n > 0) { _delay_ms(96); n--; } }
Pomiar odległości
W przerwaniu od zbocza narastającego na wyjściu komparatora zatrzymuję zegar liczący czas oraz wyłączam to przerwanie aby mikrokontroler nie reagował na zakłócenia mogące pojawić się na odbiorniku.
W przerwaniach od przepełnienia timera0 (preskaler 1) liczę czas między wysłaniem impulsów, a odbiorem echa. Timer ten jest włączany w momencie wysyłania impulsów.
ISR(ANA_COMP_vect) { TCCR0 ^= (1<<CS00); //zatrzymanie zegara odmierzającego czas między nadaniem impulsu a jego odbiorem ACSR &= (~(1<<ACIE)); //zatrzymanie przerwań od komparatora TCNT2 = 0; //wyzerowanie licznika timera0 } ISR(TIMER0_OVF_vect) { time++; //każde +1 to 16us } ISR(TIMER2_OVF_vect) { delay++; //zmienna odmierzająca upływ czasu licznik++; //j.w używana tylko dla buzzera if((PIND & (1<<PD3)) == 0) //sprawdzenie czy przycisk jest wcisnięty button++; else button = 0; if(button == 25) //jeśli będzie wciśnięty przez ponad 0.1s to ustawiam flagę button_flag = 1; if(button_flag == 1 && button == 0) //jesli flaga będzie ustawiona, a przycisk zwolniony { //to jest to krótkie przyciśnięcie start ^= 0x01; //krótkim przyciśnięciem włączam/wyłączam pomiar odległości button_flag = 0; } if(button == 255) //jesli przycisk będzie wciśnięty ponad 1s to { //jest to włączenie/wyłączenie buzzera buzz ^= 0x01; button_flag = 0; PORTB |= (1<<PB3); } if(delay == 255) PORTB &= ~(1<<PB3); if(buzz) //zmiana wypełnienia na wyjściu buzzera { // w zależności od odelgłości if(licznik <= 50) PORTB |= (1<<PB3); else PORTB &= ~(1<<PB3); if(licznik == 250 - 10000/dist) licznik = 0; } } ISR(TIMER1_COMPA_vect) { count++; //licznik wysłanych impulsów if(count == no_impulse) //po wysłaniu liczby impulsów równej no_impulse -> wyłaczenie zegara { TCCR1B ^= (1<<CS10); //zatrzymanie timera1 -> zatrzymanie nadajnika TCNT1 = 0; //wyzerowanie stanu licznika timera1 count = 0; //wyzerowanie liczby wysłanych impulsów } }
Pętla główna i funkcje dokonujące pomiarów
#define no_impulse 5 volatile long int time = 0; volatile char volatile count = 0; volatile unsigned char delay= 0, licznik = 0; volatile char flag, button_flag = 0, start = 0, buzz = 0; volatile unsigned int button = 0; volatile int dist = 0; void sprint(int dist, char *distance, char size); long int get_dist(void); int speed(void); void init_ds18b20(void); //****************************************************** int main(void) { DDRB = (1 <<PB1) | (1<<PB2) | (1<< PB3); //ustawienie pinów OC1A i OC1B jako wyjścia, PB3 => tranzystor buzzera //timera 1 w tryb szybkiego PWM ze zliczaniem do wartości w rejestrze ICR1, preskaler 1, // stan wysoki na OCR1A oraz stan niski na OCR1B przy zrównaniu TCCR1A = (1<<WGM11) | (1<<COM1A1) | (1<<COM1B1) | (1<<COM1B0);// TCCR1B = (1<<WGM13) | (1<<WGM12); // preskaler = 1 TIMSK = (1<<TOIE0) | (1<<TOIE2) | (1<<OCIE1A); //przerwania od przepełnienia licznika timera0, timera 2 oraz zrównania T1A TCCR2 = (1<<CS21) | (1<<CS22); //włączenie timera2 z preskalerem 256; ACSR |= (1<<ACIS1) | (1<<ACIS0); //przerwanie od komparatora - zbocze narastające ICR1 = 399; //ustawienie wartości końcowej zliczania (400 tactów procesora) OCR1A = 199; //200 to czas trwania połowy okresu 200 * 1/16 000 000 s = 12.5us -> OCR1B = 199; //-> okres 25us -> f = 40kHz sei(); //włączenie gloablnej obsługi przerwań DDRD |= (1<<PD4); //ustawienie odpowiednich wejść/wyjść dla tranzystorów oraz przycisku PORTD |= (1<<PD4) | (1<<PD3); char distance[5]; init_ds18b20(); LCD_Initalize(); LCD_Clear(); while(1) { if(start && delay == 255) { dist = get_dist(); LCD_GoTo(0,0); sprint(dist, distance, 5); LCD_WriteText(distance); LCD_WriteText(" cm"); } } cli(); return 0; } //****************************************************** void sprint(int dist, char *distance, char size) //funkcja zamieniająca liczbę na łańcuch znakowy { char i = 0; while(i < size - 1) //wypełnienie tablicy spacjami { distance[i] = 32; i++; } distance[size - 1] = 0; //wyzerowanie elementu na którym ma się zakońzyć wyświetlanie for(i = 0; dist > 0 && i < size - 1; i++) { distance[size - 2 - i] = (dist % 10) + 48; //przepisanie do tablicy ostatniej cyfry liczby w kodzie ASCII dist /= 10; //podzielenie liczby przez 10 => odcięcie ostatniej cyfry } } long int get_dist(void) { time = 0; TCCR0 |= (1<<CS00); //włączenie timera0 odmierzającego czas między nadaniem a odbiorem impulsów TCCR1B |= (1<<CS10); //włączenie timera1 nadającego impulsy ultradźwiękowe while(time < 160){} //oczekiwanie w pętli na włączenie nasłuchiwania echa ACSR |= (1<<ACIE); //włączenie przerwań od komparatora while(TCCR0 != 0) //oczekiwanie na nadejście sygnału zwrotnego - jesli nie nadejdzie po sekundzie zwraca -1 { if(time > 32000) { TCCR0 = 0; return 0; } } return time * speed() / (6250.0 * 2); //obliczenie dystansu //return time * 2 * 34 / 125; //wynik w cm } void init_ds18b20(void) { DDRC |= (1<<PC1); //ustawienie pinu PC1 jako wyjście // do komunikacji 1-wire z czujnikiem temperatury if(reset()) //zresetowanie magistrali i sprawdzenie czy jest na niej czujnik { skip_rom(); //jesli czujnik jest, pomijamy sprawdzenie adresu ROM write_scratchpad(); //czujnik jest tylko jeden, więc sprawdzenie byłoby niepotrzebne flag = 1; //zapisanie w scratchapdzie czujnika temepratur alarmowych } //i precyzji wykonywania konwersji temperatury else flag = 0; } int speed(void) { if(flag) //jeśli czujnik był obecny flaga jest ustawiona na 1 { char temp[2]; //deklaracja tablicy przechowującej temepraturę int temperature; //zmienna pod którą będzie temperatura reset(); //**************************************** skip_rom(); //procedura konwersji temperatury convert_t(); //**************************************** reset(); //**************************************** skip_rom(); //procedura odebrania bajtów temperatury read_scratchpad(temp); //**************************************** LCD_Clear(); temperature = (temp[0] >> 4) | ((temp[1] & 0xF) << 4); //przeliczenie temperatury z pominięciem części ułamkowej LCD_GoTo(3,1); char t[3]; sprint(temperature, t, 3); //dtostrf(temperature, 2, 1, t); LCD_WriteText(t); LCD_WriteText("C"); return (3315 + temperature * 6); //obliczenie dziesięciokrotności prędkości dźwięku w danej temperaturze } else return 3435; //jeżeli czujnik nie został wykryty zwracana jest prędkość dźwięku } //w temperaturze 20st C
Do taktowania ATmega8 użyłem zewnętrznego kwarcu 16MHz. Dlaczego? W artykule Czas - odmierzanie można przeczytać, że wewnętrzne oscylatory mikrokontrolera są dość słabej jakości (+- 2%) co skutkowałoby w najgorszym wypadku (przy odległości 3m) błędnym wskazaniem aż o 10cm! Przy zewnętrznym kwarcu uzyskujemy zdecydowanie lepszy pomiar czasu co skutkuje mniejszym błędem zmierzonej odległości.
Pliki źródłowe
Do pobrania komplet plików: Dalmierz-program-pliki-zrodlowe.zip (kopia)
Witam, chciał bym się zapytać czy układ na wykonanym pcb działa stabilnie ? Zbudowałem taki dalmierz ale na zestawie uruchomieniowym firmy ATNEL (lekkie zmiany wyprowadzeń pod atmega32, możliwych różnic timerów oraz komparatorów jeszcze nie zbadałem), układ pomiarowy jest na płytce uniwersalnej z kondensatorem kompensującym przy zasilaniu wzmacniacza, wszystko na krótkich kabelkach. Ciągle mierzy 44cm niezależnie od przeszkody, dotknięcie płytki od spodu powoduje zmianę wartości na losową (więc pewnie tu mój problem). Można zapytać skąd czujniki ultradźwiękowe (moje to wymontowane z chińskiego czujnika do arduino) ?
OdpowiedzUsuńPrawidłowo zmontowany układ dalmierza ultradźwiękowego działa bardzo stabilnie. Albo masz gdzieś błąd, albo ci się sprzęga odbiornik z nadajnikiem. Czasami dochodzi do sprzężenia mechanicznego.
Usuń