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ń