sobota, 11 października 2014

Obsługa czujnika ultradźwiękowego HC-SR04 - przykładowy kod dla mikrokontrolera AVR ATmega32

Prezentuję jeden ze sposobów obsługi popularnego modułu czujnika ultradżwiękowego za pomocą mikrokontrolerów AVR ATmega. 

Moduł sprawdzi się w prostych konstrukcjach niewymagających dużej dokładności pomiarowej, gdzie zakres mierzonych odległości zawiera się w przedziale 3-250 cm. Producent określa rozdzielczość przetwornika na poziomie +/-3 mm, jednakże po przeprowadzonych próbach osobiście zalecam dokonywanie pomiarów z dokładnością do 1 cm - w większości amatorskich zastosowań powinna ona wystarczyć.

Powszechność modułu wynika przede wszystkim z jego atrakcyjnej ceny (kilka złotych) co pozwala na stosowanie go w amatorskich konstrukcjach pojazdów i robotów. Innym obszarem zastosowań mogą być proste systemy alarmowe, pomiary poziomu lustra wody w zbiornikach (choć problemem w tym przypadku na pewno będzie brak odborności modułu na wilgoć) itp.

Moduł posiada jednak kilka istotnych wad o których będzie mowa w dalszej części wpisu.

W sieci znajdziemy kilka wersji dokumentacji modułu:
lecz ilość informacji tam zawartych jest delikatnie mówiąc skromna ;)

Zdjęcia omawianego modułu czujnika ultradźwiękowego:





Zasada działania modułu polega na zainicjowaniu pomiaru odlełości wysokim stanem napięcia na wejściu Trig przez okres około 10 us. Spowoduje to wysłanie przez nadajnik 8 impulsów o częstotliwości 40 kHz. Następnie odbiornik nasłuchuje fal dźwiękowych odbitych od odległego obiektu. Im obiekt dalej, tym później odbiornik otrzyma echo wysłanych impulsów (lub nie otrzyma ich wcale, bądź po zbyt długim czasie). Czas powrotu fali dźwiękowej jest proporcjonalny do odległości obiektu od modułu. Na wyjściu Echo moduł emituje sygnał o czasie trwania proporcjonalnym do mierzonej odległości. Zależność czasu i odległości przedstawia się następująco:

odległość_[cm]=czas_sygnału_Echo_[us]/58

lub bezpośrednio z zależności:

odległość_[m]=czas_sygnału_Echo_[s]*prędkość_dźwięku_w_powietrzu[m/s]/2

W powyższym równaniu dzielimy przez 2, ponieważ fala dźwiękowa przebywa dwukrotnie dłuższą drogę zanim powróci do odbiornika. Zatem interesująca nas wielkość jest połową zmierzonej.

Sygnał Echo emitowany jest około 478,5 us po zainicjowaniu konwersji na wejściu Trig. Zależności czasowe tych dwóch sygnałów przedstawia poniższy rysunek:



Proponowana idea dokonywania pomiarów odległości za pomocą modułu HC-SR04 sprowadza się do pomiaru czasu trwania odpowiedzi modułu na wyjściu Echo. Jedną z możliwości jest zliczanie impulsów odpowiednio skonfigurowanego licznika w mikrokontrolerze podczas trwania sygnału Echo. Otrzymywana wartość będzie proporcjonalna do czasu. Zobrazowano to na wcześniejszym rysunku.

Praktyczna rezalizacja polega na skonfigurowaniu Timera1 w mikrokontrolerze ATmega32 taktowanym generatorem kwarcowym o częstotliwości F_CPU=16 MHz, tak aby kolejna inkrementacja rejestru TCNT1 dokonywana była z częstotliwością 2 MHz (możliwe dzięki ustawieniu preskalera=8) czyli co 0,5 us. Zbocze narastające na wyjściu Echo dołączonego do wejścia ICP1 (PD6) mikrokontrolera powoduje wywołanie przerwania w którym następuje zresetowanie wartości Timera1 oraz przestawienie trybu przechwytywania na wyprowadzeniu ICP1. Zbocze opadające na tym wyprowadzeniu spowoduje wywołanie kolejnego przerwania w którym odczytujemy zawartość rejestru ICR1. Im dłuższy czas trwania sygnału Echo tym większa wartość odczytana w rejestrze. Ze wzlędu na inkrementację Timera1 co 0,5 us, ilość zliczonych impulsów dzielimy przez 2. W ten sposób w zmiennej length przechowywana jest wartość czasu trwania sygnału Echo wyrażona w us. Aby zinterpretować ją jako odległość w cm wystarczy podzielić przez 58.


Kod żródłowy programu dla AVR ATmega32:

#include <avr/io.h>
#include <stdlib.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <util/atomic.h>
#include "uart.h"

#define HARDWARE_TRIG /* If using software trigger comment this line */

#ifndef HARDWARE_TRIG
#define SOFTWARE_TRIG
#endif 

#define BAUD 250000  /* configure baud rate for UART */

#define TRIGGER_PIN_SET_HIGH PORTB|=(1<<PB0)
#define TRIGGER_PIN_SET_LOW  PORTB&=~(1<<PB0)

volatile enum {DATA_OLD, DATA_FRESH, DATA_BAD} FreshData;
volatile enum {STATE_FREE, STATE_BUSY} ConversionState;
volatile uint16_t length;
char buffer[4];

/* Function declaration */
void Init(void);

#ifdef SOFTWARE_TRIG
void Trig(void);
#endif /* SOFTWARE_TRIG */


int main(void){
 
 Init(); 
 sei(); //enable interrupts
 
 while(1)
 { 
  if(FreshData == DATA_FRESH){ //send fresh (and correct) data only
   ATOMIC_BLOCK(ATOMIC_RESTORESTATE){
    uart_puts(itoa(length/58,buffer,10));
   }
   uart_putc('\r');
   uart_putc('\n');
   FreshData=DATA_OLD;
  }
 } 
}

void Init(void){
 /* Trigger port configuration */
 DDRB |= 1<<PB0;  //trigger out
 TRIGGER_PIN_SET_LOW;
 /* Timer0 configuration */
 TCCR0 |= 1<<WGM01 | 1<<CS02 | 1<<CS00; //CTC mode, prescaler=1024 
 TIMSK |= 1<<OCIE0; //Output compare match interrupt enable
 OCR0 = 249; //(FCPU=16000000)/(prescaler=1024)/(OCR0+1=250)=62,5 ticks per second (every 16ms)
 /* Timer1 configuration */
 TCCR1B |= 1<<ICES1; //set rising edge
 TCCR1B |= 1<<CS11; //prescaler=8
 TIMSK |=  1<<TICIE1; //Timer1 Capture Interrupt Enable
 
#ifdef HARDWARE_TRIG
 TCCR1B |= 1<<WGM12; //CTC mode
 OCR1A = 19; //19+1 cycles=10us (1 tick=0,5us) 
#endif
 /* UART configuration */
 uart_init((UART_BAUD_SELECT((BAUD),F_CPU)));
}

ISR(TIMER0_COMP_vect){ //62,5 times per second (every 16ms) ---> see Init() fucntion
 static uint8_t cnt=0;
 cnt++;
 if (cnt==4 && ConversionState==STATE_FREE){

#ifdef HARDWARE_TRIG //trigger measurement by hardware - Timer1 CTC Mode
  TCCR1B |= 1<<WGM12; //CTC mode
  TRIGGER_PIN_SET_HIGH;
  TCNT1 = 0; //reset Timer1
  TIFR |= 1<<OCF1A; //clear any previous interrupt flag
  TIMSK |= 1<<OCIE1A; //Output Compare A Match Interrupt Enable  
#endif

#ifdef SOFTWARE_TRIG
  Trig(); //trigger measurement
#endif

  cnt=0;
 } else if(cnt==4){ //after 4 ticks (64ms) still ConversionState=BUSY_STATE ---> it means timeout condition
   cnt=0;
   FreshData=DATA_BAD;
  } 
}

ISR(TIMER1_CAPT_vect){ //interrupt frequency = 2MHZ (F_CPU=16MHZ / prescaler=8), 1 tick every 0,5us
 if( (TCCR1B & (1<<ICES1)) ) //if rising edge
 {
  TCNT1 = 0; //reset counter
  TCCR1B &= ~(1<<ICES1); //set falling edge trigger
  ConversionState = STATE_BUSY;
 }else //falling edge
 {
 if (FreshData != DATA_BAD){ //if timeout condition has not occurred
  length = ICR1/2; //length reading (1 tick=0,5us -> 200=100us  therefore divided by 2)  
  FreshData = DATA_FRESH; //set marker, reseted after sending data in main function
 } else FreshData = DATA_OLD; //next cycle after timeout will be handled correctly
  
  TCCR1B |= (1<<ICES1); //set rising edge trigger
  ConversionState = STATE_FREE;
 }
}

#ifdef HARDWARE_TRIG

ISR(TIMER1_COMPA_vect){ /* interrupt occurs after 10us */
 TRIGGER_PIN_SET_LOW;  //end trigger signal
 TIMSK &= ~(1<<OCIE1A); //Output Compare A Match Interrupt Disable
 TCCR1B &= ~(1<<WGM12); //CTC mode disabled 
}
#endif /* HARDWARE_TRIG */

#ifdef SOFTWARE_TRIG

void Trig(void){
 TRIGGER_PIN_SET_HIGH;
 _delay_us(10);
 TRIGGER_PIN_SET_LOW;
}
#endif /* SOFTWARE_TRIG */



W celu dostosowania kodu konieczne  będzie tylko kilka prostych zmian wprowadzanych na początku kodu:
  • zakomentowanie lub pozostawienie poniższej definicji:

#define HARDWARE_TRIG /* If using software trigger comment this line */

odpowiada ona za metodę generowania sygnału Trig za pomocą funkcji opóźniających delay bądź całkowicie sprzętowo z wykorzystaniem timera.
  • konfiguracja prędkości pracy interfejsu UART do odbierania przesyłanych danych pomiarowych:

#define BAUD 250000  /* configure baud rate for UART */
  • dostosowanie definicji portu dla sygnału Trig, pierwotnie podłączany do wyprowadzenia PB0 portu PORTB:

#define TRIGGER_PIN_SET_HIGH PORTB|=(1<<PB0)
#define TRIGGER_PIN_SET_LOW  PORTB&=~(1<<PB0)


Po kompilacji i zaprogramowaniu mikrokontrolera działanie przykładu wygląda następująco:


Na filmiku celowo prezentuję pewne nieporządane efekty podczas pomiaru odległości tym modułem. Podczas przeprowadzania własnych testów wyglądało to na tyle dziwnie, iż pokusiłem się na zbadanie tego prostym analizatorem stanów logicznych. Efekty prezentuję poniżej.

W dokumentacji można doszukać się informacji, iź inicjowanie pomiarów jest zalecane nie częściej niż 60 ms. W przygotowanym przykładzie konwersja inicjowana jest co 64 ms.


Od inicjalizacji pomiaru do pojawienia się sygnału na wyjściu Echo zawsze upływało 478,5 us.


Wg. dokumentacji, jeśli czujnik nie otrzyma odbitego sygnału fali dźwiękowej długość sygnału Echo powinna wynosić 38 ms. Wartość ta nawet po przeliczeniu na odległość wyniosła by 655 cm czyli o wiele ze dużo, niż deklarowany przez producenta zakres pomiarowy. Zatem praktycznie rzecz biorąc wartość ta jest zasadna i dosyć logiczna. Jednak w tym przypadku praktyka pokazuje zupełnie co innego. Podczas pomiarów odnotowałem impulsy (nazwijmy je do naszych rozważań jako timeout) o długościach ponad 210 ms ! To przeszło pięciokrotnie dłuższy czas niż deklarowany przez producenta. Powoduje to znaczne opóźnienia w możliwości zainicjowania kolejnego pomiaru. Na poniższym zrzucie widoczne jest, iż program został odpowiednio przygotowany, aby nie inicjować nowej konwersji podczas trwającego stanu timeout. Realizacja polegała na obsłudze prostej maszyny stanów złożonej z dwóch zmiennych statusowych typu i kilku instrukcji warunkowych w programie.


Poniżej zrzut przedstawiający poprawną pracę, kiedy moduł mierzy odległość bez opóźnień i przestojów.


Jako kontrast zrzut przedstawiający pracę modułu w trudnych warunkach, tzn. często nie udaje się mu zmierzyć odległości od obiektu (zielone strzałki wskazują poprawny, natomiast czerwone nieporawny pomiar):


Kiedy takie trudne warunki występują? Ano bardzo często. Wymienić można dwa główne przypadki kiedy dochodzi do niekorzystnego zjawiska timeoutu modułu:

  • próbujemy zmierzyć odległość większą od maksymalnej tj. ok. 250 cm,
  • czujnik jest ustawiony względem powierzchni pod zbyt dużym kątem.

W najlepszym przypadku moduł powinien znajdować się w osi prostopadłej do mierzonej powierzchni. Każde odchylenie nawet o kilka stopni powoduje duże błędy pomiarowe. Na filmiku starałem się to pokazać za pomocą zmiany nachylenia kartonika względem modułu. W najgorszym przypadku dochodzi do odbicia fali dźwiękowej w innym kierunku niż nasz odbiornik co objawia się timeoutem i przerwami w dostawie danych pomiarowych.

Specyficzne stany modułu ultradźwiękowego HC-SR04 należy uwzględniać i odpowiednio konstruować swoje programy. Absolutnym błędem byłoby oczekiwanie w pętli na prawidłowy pomiar z czujnika - powodowałoby to wręcz zawieszanie się programu zbudowanego urządzenia na długi czas. 



Dla najwytrwalszych czytelników, którzy dotarli do końca wpisu kod źródłowy całego programu dostępny na GitHubie

Powodzenia
Michał