Skocz do zawartości

[Programowanie] Konfiguracja i algorytm linefollowera w C - dla początkujących i nie tylko


baton

Pomocna odpowiedź

Wielkie dzięki za artykuł! Dzięki niemu pierwszy raz zaprogramowałem swojego lf'a w c i do tego dostał jeszcze algorytm zamiast męczących case'ów.

EDIT:

Co do mojego problemu z odczytywaniem tablic to jak to zazwyczaj bywa sprawa była banalna, ale jej znalezienie zajęło mi jakieś 2 dni. Po pierwsze był błąd w obsłudze LCD, po drugie zapomniałem w jednej funkcji o nawiasach klamrowych. Teraz moim największym zmartwieniem jest odpowiednie dobranie wag i współczynników.

Link do komentarza
Share on other sites

Pozwolicie, że odkopię 😉. Ostatnio miałem znów chwilkę czasu (albo i konieczność), żeby pogrzebać w programie mojego lF'a.

Moje pytania dotyczą tego fragmentu:

➡️ Timer - przerwanie czasowe

Funkcja pełniąca rolę głónej pętli algorytmu LFa powinna wykonywać się w równych odstępach czasu, co pozwoli na dokładną kontrolę toru jazdy robota. Wykonywanie jej w pętli głównej programu i opóźnianie za pomocą funkcji _delay_ms() nie jest dobrym rozwiązaniem - wraz z rozwojem algorytmu, będzie on potrzebował coraz więcej czasu na wykonanie, przez co w rzeczywistości częstotliwość wywoływania funkcji będzie spadać. Aby temu zapobiec i mieć pełną kontrolę nad okresem czasu między kolejnymi przejsciami pętli algorytmu, wykorzystamy Timer 0 do generowania przerwań z określoną częstotliwością.

Zapoznając się z zasadą oraz trybami działania Timera 0 opisanymi w DSie od strony 96. znajdujemy interesujące nas rozwiązanie - CTC (punkt 15.7.2 na s. 102). W trybie tym timer zlicza w górę aż do osiągnięcia zadanej wartości, po czym wywołuje przerwanie i automatycznie się zeruje. Efektem jest cyklicznie powtarzające się przerwanie w równych odstępach czasu.

Opis użycia timera jak zwykle podsumowują tabelki informujące o działaniu poszczególnych rejestrów konfiguracyjnych. Z tabeli 15-8 uzyskujemy informację, jak ustawić tryb CTC:

TCCR0A |= _BV(WGM01);

Do poprawnego działania timera jest jeszcze wymagane ustawienie preskalera. Zakładając, że chcemy uzyskać częstotliwość wywoływania przerwania równą 100Hz, musimy wybrać jego maksymalną dostępną wartość - 1024. Pozwoli to na ustawienie częstotliwości przepełnienia licznika w zakresie 30,5 - 7812,5 Hz (taktowanie timera - 8Mhz/1024 = 7812,5Hz; może zliczać do 256 - 7812,5/256 = 30,5Hz).

TCCR0B |= _BV(CS02) | _BV(CS00);

Wartość maksymalną licznika, po osiągnięciu której ten będzie się zerował i generował przerwanie, wpisujemy do rejestru OCR0A. Wymaganą do uzyskania częstotliwości bliskiej 100Hz wartością jest 78 (8Mhz/1024/78 = 100,16Hz):

OCR0A = 78;

Ostatnią konieczną czynnością jest włączenie przerwania wywoływanego po osiągnieciu przez timer zadanej przez nas wartości. Zgodnie z punktem 15.9.6 DSa, wymaga to ustawienia jednego bitu:

TIMSK0 |= _BV(OCIE0A);

Zbierając wszystko w całość, konfiguracja Timera 0 przedstawia się następująco:

TCCR0A |= _BV(WGM01);
TCCR0B |= _BV(CS02) | _BV(CS00);
OCR0A = 78;
TIMSK0 |= _BV(OCIE0A);

W tym momencie w odstępach 10ms uruchamiany będzie ISR TIMER0_COMPA_vect. Aby w prosty sposób sprawdzić, czy wszystko działa zgodnie z założeniami, możemy napisać testową obsługę przerwania o treści:

ISR(TIMER0_COMPA_vect)
{
LED ^= 1;
}

W efekcie dioda powinna migać z częstotliwością ~50Hz (zmiana stanu 100 razy na sekundę). Jeśli mamy oscyloskop lub multimetr pozwalający na pomiar częstotliwości - jest możliwość skonfrontowania teorii z praktyką.

Mam pewne trudność z ustaleniem współczynników, wydaje mi się że robot czasem po prostu nie widzi zakrętu. Pierwsza myśl to zbyt wolne realizowanie pętli głównej. Faktycznie, przyjmijmy prędkość dla ułatwienia 120cm/s. (teoretycznie około połowa maksymalnych możliwości). Wywoływanie pętli głównej z częstotliwością 100Hz to odczytywanie czujników co 1,2cm, niby nie tak dużo ale jednak, zwłaszcza jeśli jedzie nieco szybciej. Ale ja popędzam uc z 20Mhz, więc w teorii mogę 2,5 raza więcej wycisnąć.

Wziąłem się więc za pomiary czasu wykonywani pętli, metoda dość prymitywna, w pętli głównej zmieniałem stan jednego z wyjść i częstotliwość mierzyłem dobrej klasy multimetrem. Właściwa częstotliwość wykonywania pętli bez żadnych timerów odmierzających czas i przy 8 czujnikach to jakieś 260Hz, jednak po wyjechaniu za linię spada do ok 100Hz (w sumie poza kilkoma ifami nie wiem co może mu zajmować tyle czasu). Co gorsza po podniesieniu czasu wywoływania przez timer do 1/200s taką częstotliwość otrzymuję też tylko przy jeździe po linii. Przy ustawieniu flagi przestrzelony częstotliwość spada do owych 100Hz. Co w tym programie może powodować, że pętla po ustawieniu przestrzelania nie zdąży się wykonać przed upływem zamierzonego przeze mnie czasu?

Podniesienie szybkości wykonywania pętli przyniosło u mnie pewne poprawienie, jednak raz na parę okrążeń nadal zdarza mu się wylecieć na zakręcie. Jak sobie radzą z tym inne, znacznie szybsze roboty? W ich przypadku tylko cyfrowe odczytywanie czujników wchodzi w grę?

Link do komentarza
Share on other sites

Zarejestruj się lub zaloguj, aby ukryć tę reklamę.
Zarejestruj się lub zaloguj, aby ukryć tę reklamę.

jlcpcb.jpg

jlcpcb.jpg

Produkcja i montaż PCB - wybierz sprawdzone PCBWay!
   • Darmowe płytki dla studentów i projektów non-profit
   • Tylko 5$ za 10 prototypów PCB w 24 godziny
   • Usługa projektowania PCB na zlecenie
   • Montaż PCB od 30$ + bezpłatna dostawa i szablony
   • Darmowe narzędzie do podglądu plików Gerber
Zobacz również » Film z fabryki PCBWay

Natanoj, u mnie w Tsubame przy prędkości 1m/s pętla programu wykonuje się kilka ms, mam odczyt z czujników chyba co 0,5mm i to przy odczycie z ADC. Przy prędkości powyżej 1m/s możesz po prostu nie łapać linii na zakręcie jeśli odczyt będzie chwilkę przed linią i chwilkę po linii.

  • Lubię! 1
Link do komentarza
Share on other sites

Dzięki za odpowiedź, czyli przy czytaniu przez ADC taka częstotliwość pomiaru jest normalna. Dobrze wiedzieć, bo świadczy to o tym, że gęsto ustawione czujniki są bez sensu. U mnie odległość między środkami czujników to 0,5cm. Przy połowie maksymalnej szybkości mam odczyt co ok. 0,6cm. Jeżeli chciałbym jechać szybciej to lepiej szerzej rozstawić czujniki. Wtedy dokładne podążanie za linią jest coraz mniej dokładne. Wysuwam więc daleko idący wniosek, że szybkie roboty używające ADC nie są w stanie jeździć dokładnie po linii.

Nadal jednak nie mogę zrozumieć dlaczego ten fragment:

if(przestrzelony)					// zmniejszenie wag czujników w przypadku przestrzelenia zakrętu
{
	wagi[0] = -40;
	wagi[1] = -20;
	wagi[2] = -12;
	wagi[3] = -5;
	wagi[4] = 5;
	wagi[5] = 12;
	wagi[6] = 20;
	wagi[7] = 40;
}

(... NIEWAŻNY FRAGMENT KODU ...)

else								// czujniki nie wykryły linii
{
	if(prev_err < -50)				// linia ostatanio widziana po lewej stronie
	{
		err = -75;					// ustalamy ujemny błąd większy od błędu skrajnego lewego czujnika
		przestrzelony = 1;			// ustawienie flagi - przestrzelony, linia po lewej


	}
	else if(prev_err > 50)			// linia ostatanio widziana po prawej stronie
	{
		err = 75;					// ustalamy błąd większy od błędu skrajnego prawego czujnika
		przestrzelony = 2;			// ustawienie flagi - przestrzelony, linia po prawej


	}
	else
		err = 0;
}

if(przestrzelony == 1 && err >= 0)	// zerowanie flagi przestrzelenia zakrętu po powrocie na środek linii
przestrzelony = 0;
else if(przestrzelony == 2 && err <= 0)
przestrzelony = 0;

}

tak bardzo obciąża procesor. 200Hz to przecież nie tak dużo jak taktowanie mam 20Mhz. Czemu wykonując powyższy fragment częstotliwość pętli spada do 50Hz?

EDIT 30.05.2013

Cóż, miałem nadzieję na jakąś choć małą dyskusję na temat dokładności śledzenia linii czy rozstawu czujników, trudno. W każdym razie Sabre zasłużył na jakieś wyróżnienie, jako jedyny się odezwał 🙂.

Link do komentarza
Share on other sites

Rzadko bywałem ostatnio na forbocie, koniec semestru boli, a odezwałbym się wcześniej.

Pewnie problem został dawno rozwiązany, ale w każdym razie... nie wiem dlaczego to by miało tyle czasu trwać 😃

Dawno temu robiłem testy czasu trwania pomiarów ADC w atmedze, żeby udowodnić komuś, że trzeba mieć naprawdę rakietę, żeby nie wyrobić się z odczytami. Nie pamiętam teraz dokładnych wyników, ale w trakcie 1ms na pewno dało się przeprowadzić kilkukrotnie pomiar na wszystkich kanałach ADC, więc to raczej nie jest kwestia czasu konwersji.

Będę strzelał, to co mi przyszło do głowy, co warto sprawdzić: optymalizacja kompilacji, przejście na try 'release', preskaler ADC, sprawdzenie, czy to faktycznie jest 20MHz (pin CLKO bodajże).

Tak czy inaczej, 1ms na taką banalną pętlę to naprawdę sporo. Przy 20MHz daje to 20k taktów. To musi działać 😃

Link do komentarza
Share on other sites

co ile wywołujecie członD w swoich robotach?

U mnie przy 10ms różnica między uchybem aktualnym a poprzednim wygląda tak:

(wysyłąne po UARCIE co 10ms wartość uchyb_akt - uchyb_pop)

-15
0
0
0
5
0
10
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-10
0
-5
0
0
0
0
0
0
0
0
0
0
0
0
0
5
10
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-6
-9
0
0
0
0
0
0
-5
-5
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
5
0
5
0
0
0
0
0
9
0
6
0
0
0
0
0
6
0
0
9
0
0
0
0
0
0
0
0
0
0
0
0
-15
0
0
0
-6
-9
0
0
-10
0
-5
0
22
9
4
0
0
-7
-33
0
-5
0
0
0
0
0
0
0
0
0
0
0
0
5
0
0
10
0
10
0
15
0
15
0
10
0
10
0
5
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-5
0
0
-10

W tym przypadku powinienem zwiększyć czas pomiędzy wyzwalaniem członu D?

Link do komentarza
Share on other sites

Nadal odczyty są podobne, ale chyba ma tak być.

Pololki HP 10:1, kołą pololu standardowe 32x7

Nap. silników 6V, 10ms, kp=34, kd=110, pwm=106.

Ale z tego co widzę, teraz moim największym problemem są koła, muszę zdobyć jakieś szersze i bardziej przyczepne.

No i chyba warto by było zwiększyć czas wyzwalania członu D z 10 na jakieś 15ms lub 20ms?

Trasą jest odwrócone linoleum (po drugiej stronie jest białe), są dwa kąty proste, no i na koncie prostym często wypadam, ale to wina kół. Jak wyczyszczę koła IPA to przejeżdża.

-12
-6
-14
-5
-5
-5
0
0
0
0
0
0
0
0
0
10
0
0
10
0
15
0
15
0
10
0
0
10
0
0
0
0
0
0
-10
0
0
0
-10
0
-5
-10
0
-10
-5
0
0
0
0
0
0
4
6
0
5
-15
0
4
6
5
17
-17
0
0
0
10
0
0
0
0
0
0
-10
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-10
0
0
10
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
10
0
0
-10
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
10
0
0
-10
0
0
0
0
0
0
0
0
0
0
10
5
0
0
0
0
-5
0
-10
0
0
0
0
0
0
0
0
0
0
0
0
0
-10
0
-5
0
0
5
0
0
0
0

[ Dodano: 07-04-2015, 22:59 ]

PS. W piątek może uda mi się sprawdzić go na trasie z płyty.

Link do komentarza
Share on other sites

Witam,
Chcialbym odkopac temat, mianowicie u mnie przy uzyciu tego algorytmu powstawal problem z blednymi odczytami z transoptorow CNY70, czasami okazywalo sie ze jeden wykrywal linie a nie powinien itd. Podejrzewam ze wynika to z niejednolitego wykonania transoptorow i innych elementow jak rezystory, straty w stykach itp ktore zaklocaja pomiary, w zwiazku z czym na sztywno ustalany prog pomiedzy czarnym kolorem a bialym moze nie pasowac do wszystkich odczytow z roznych transoptorow cny70.

Pomyslalem ze fajnym rozwiazaniem byloby wykorzystanie komparatora,tak aby za kazdym razem robot wybieral najwyzszy odczyt, bez wzgledu na prog itp.

Zeby sie nie meczyc postanowilem zaimplementowac komparator programowo.

zalozenie bylo takie aby czujniki szukaly najwiekszego odczytu z ADC i wybieraly go jako ten z linia.

W efekcie przeksztalcilem funkcje czytad_adc() autora artykulu(ktoremu gratuluje i dziekuje za ciezka prace, wiele sie z niej nauczylem) i usunalem kalibracje.

void czytaj_adc()
{	int i;
max=tab_czujnikow[0];
min=tab_czujnikow[0];
for(i=0; i<5; i++)
{
	ADMUX &= 0b11100000;			// zerujemy bity MUX odpowiedzialne za wybór kanału (s. 255 w DS)
	ADMUX |= tab_czujnikow[i];		// wybieramy kanał przetwornika
	ADCSRA |= _BV(ADSC);			// uruchamiamy pomiar
	while(ADCSRA & _BV(ADSC)) {};	// czekamy aż skończy się pomiar

	czujniki[i]=ADCH;
	 if(czujniki[i]>max) max=czujniki[i]; //szukamy czujnika z najwyzszym odczytem
	 if(czujniki[i]<min) min=czujniki[i]; //szukamy czujnika z najnizszym odczytem (czysto dydaktycznie)

	}

	for(i=0;i<5;i++){
		if(czujniki[i]>=max)  czujniki[i] =  1; //jezeli odczyt jest rowny badz wiekszy od max to dostaje 1
		 else                         czujniki[i] =  0;
	}

Czekam na wasze komentarze odnosnie kodu.

BTW czy tylko mi nie dzialal ten algorytm do momentu kiedy zmienilem wartosci

int tab_czujnikow[7] = {5,4,3,2,1,0,7};

na

int tab_czujnikow[7] = {7,6,5,1,2,3,4};

jak dla mnie powodowalo to bledy przy wybieraniu kanalu ADC (algorytm probowal wybrac kanal 0)

Mial ktos podobny problem?

[ Dodano: 05-09-2015, 00:44 ]

mam ostatnio chyba pecha, wyglada na to ze trafilem jakies trefne CNY70, kazdy ma inne odczyty na wzglednie ta sama powierzchnie.

Nie mam innych pod reka wiec pomyslalem ze dobrym sposobem byloby ustawianie progu miedzy czarnym a bialym dla kazdego transoptora z osobna, kalibracja jest 2-etapowa, najpierw wszystkie czujki sa nad czarna linia, a potem wszystkie nad biala.

Sprawdza sie to bardzo dobrze, nawet z pewna tolerancja co do wysokosci i faktury podloza.

teraz kod wyglada tak:


void kalibracja()
{ int i;
	for(i=0;i<5;i++){
	    czarne[i] = 0;
		ADMUX &= 0b11100000;				// zerujemy bity MUX odpowiedzialne za wybór kanału (s. 255 w DS)
		ADMUX |= tab_czujnikow[i];							// wybieramy kanał przetwornika
		ADCSRA |= _BV(ADSC);				// uruchamiamy pomiar
		while(ADCSRA & _BV(ADSC)) {};		// czekamy aż skończy się pomiar
		czarne[i] = ADCH;
		}						// odczyt z czujnika nad białym polem
		{
		PORTD |=(1<<LED0)|(1<<LED1)|(1<<LED2)|(1<<LED3)|(1<<LED4);
		_delay_ms(2000);
		PORTD &=(1<<LED0)&(1<<LED1)&(1<<LED2)&(1<<LED3)&(1<<LED4);
		_delay_ms(100);
		}

			 for(i=0;i<5;i++){
				 biale[i] = 0;
				 ADMUX &= 0b11100000;				// zerujemy bity MUX odpowiedzialne za wybór kanału (s. 255 w DS)
				 ADMUX |= tab_czujnikow[i];							// wybieramy kanał przetwornika
				 ADCSRA |= _BV(ADSC);				// uruchamiamy pomiar
				 while(ADCSRA & _BV(ADSC)) {};		// czekamy aż skończy się pomiar
				 biale[i] = ADCH;
		 }

			 for(i=0;i<5;i++){
				 progi[i]=0;
				 progi[i]=biale[i]+(czarne[i]-biale[i])/2;
		 }
		 PORTD |=(1<<LED0)|(1<<LED1)|(1<<LED2)|(1<<LED3)|(1<<LED4);
		 _delay_ms(200);
		 PORTD &=(1<<LED0)&(1<<LED1)&(1<<LED2)&(1<<LED3)&(1<<LED4);
		 _delay_ms(100);
}
void czytaj_adc()

{


	int i;

min=czujniki[0];
for(i=0; i<5; i++)
{

	ADMUX &= 0b11100000;			// zerujemy bity MUX odpowiedzialne za wybór kanału (s. 255 w DS)
	ADMUX |= tab_czujnikow[i];		// wybieramy kanał przetwornika
	ADCSRA |= _BV(ADSC);			// uruchamiamy pomiar
	while(ADCSRA & _BV(ADSC)) {};	// czekamy aż skończy się pomiar

	czujniki[i]=ADCH;

	if(czujniki[i]>=progi[i])  czujniki[i] =  1;
	else                 czujniki[i] =  0;
	}

	for(i=0; i<5; i++){
if(czujniki[i]==1)
	PORTD |=LEDY[i];
	_delay_ms(50);
	PORTD &=~LEDY[i];
	_delay_ms(50);
}
}

zrezygnowalem z programowego komparatora ze wzgledu na problem pojawiajacy sie gdy linie wykrywal wiecej niz 1 czujnik.

Kod w dalszym ciagu jest jedynie zmodyfikowana wersja autora tematu!

moze komus sie przyda moja koncepcja

Link do komentarza
Share on other sites

Jeżeli masz faktycznie czujniki o dużej rozbieżności odczytów, to faktycznie najlepszym wyjściem jest tablica progi[].

BTW czy tylko mi nie dzialal ten algorytm do momentu kiedy zmienilem wartości

Jak masz podłączone kanały ADC uC do listwy czujników? W sensie czy podłączyłeś je w tej samej kolejności co na schemacie autora tematu.

A tak po za tym pamiętaj, że skuteczny algorytm do LF to prosty algorytm. Sam też bawiłem się coraz bardziej z modyfikacjami swojego kodu, aż kod zaczynał się wykonywać coraz dłużej i coraz trudniej było przewidzieć jak zachowa się robot 🙂 . W końcu napisałem jeszcze raz wszystko od nowa, i wyszło o wiele lepiej, prościej, szybciej i czytelniej 😉 .

Link do komentarza
Share on other sites

ok, dzieki, juz widze czemu nie dzialalo, CNY70 byly podlaczone kolejno do wejsc ADC1...ADC5. ADC0 byl nieuzywany, w sumie to nie patrzylem na schemat, tylko po swojemu kombinowalem, stad ta roznica.

Tez sie martwilem nad szybkoscia wykonywania kodu, wlaczylem sobie nawet jednego

LEDa zmieniajacego stan jak XOR przy kazdym wykonaniu petli i nie widac nawet momentu gaszenia i zapalania, nawet przy 8 Mhz wiec chyba jest ok, co innego jak zmienilem typ zmiennej progi z int na float - wtedy wyraznie widac bylo zapalanie i gaszenie leda

Link do komentarza
Share on other sites

Wprowadzenie float w avr prawie zawsze powoduje ogromne spowolnienie programu. Nie widać migającego leda, bo zrobiłeś coś na kształt programowego PWM. spróbuj zrobić tego xora co 100 lub 1000 obiegów pętli a wtedy będziesz widział wyraźne zmiany czasu mrugania w zależności od zmian w kodzie.

Link do komentarza
Share on other sites

wiem wiem, robi sie wtedy takie PWM z wspolczynikiem wypelnienia kolo 40%(akurat w tym przypadku), ale w tym przypadku to nie jest problemem, chcialem miec poprostu jakiekolwiek informacje o tym czy petla sie wykonuje 😋

Link do komentarza
Share on other sites

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Anonim
Dołącz do dyskusji! Kliknij i zacznij pisać...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.