AVR ATmega, pomiar częstotliwości przebiegu,

Feb 09, 2011 34 Replies

Witam,



Jakiś czas temu pytałem o sprawy związane z pomiarem częstotliwości sinusoidy o częstotliwości od 2 do 150Hz oraz zmiennej amplitudzie (od kilku V do 400V). Mam już układ elektroniczny przekształcający sinusoidę na prostokąt (brana jest pod uwagę tylko dodatnia połówka, a ujemna jest usuwana). Jest tu kilka rezystorów, mostek, tranzystor, dioda oraz transformatorek. (Może działanie tego układu da się jeszcze ulepszyć, ale to w sumie nie jest teraz tematem). Uzyskany z dodatniej połówki sinusoidy prostokąt jest podawany na nóżkę PE7(INT7) mikrokontrolera ATmega128 (kwarc



16MHz). Dokładność pomiaru, którą chciałbym osiągnąć to 0,2Hz przy częstotliwości 50Hz. Chciałem Was prosić o sprawdzenie mojego kodu, którego zadaniem jest nieustanne mierzenie czasu trwania dodatniej połówki, a następnie wypisywanie tej wartości na wyświetlacz. Kod, który napisałem wygląda na działający, ale może ma jakiś ukryty błąd.

volatile unsigned long int timer = 100000; volatile short actualTime;



./* procedura wywoływana 100000 razy na sekundę */ SIGNAL (SIG_OUTPUT_COMPARE2) { cli();



if (timer < 100000) timer++;



sei(); }



/* wyzwalane zboczem narastającym i opadającym */ SIGNAL (SIG_INTERRUPT7) { cli();



if (PINE & PINE7) { timer = 0; TCCR2 = 0; // stop timera TCNT2 = 0; // zerowanie licznika TCCR2 = _BV(WGM21) | // start timera _BV(C21); // (prescaler 8) } else { actualTime = timer; }



sei(); }



int main(void) { char s[16];



initLCD();



cli();



EICRB |= _BV(ISC70); EIMSK |= _BV(INT7);



TCCR2 = 0; OCR2 = 19; TIFR |= _BV(OCF2); TIMSK |= _BV(OCIE2); while (ASSR & _BV(OCR2UB)) ;



sei();



while (1) {



if (timer == 100000) LCDshowString("---"); else { sprintf(s, "%d ", actualTime); LCDshowString(s); } }



return 0; }


*** Jak to się sprawuje: Jako źródło sygnału podłączyłem po prostu zasilanie sieciowe 230V/50Hz. Wczoraj wieczorem przed godziną 22 liczba na wyświetlaczu wynosiła 996 (ostatnia cyfra niekiedy migała na "5"). Potem zaczęło to rosnąć... 997/8. O godzinie 22 było jakieś 999. Potem zaczęło spadać do 996. Teraz (godzina
13:40) mam 996/7.
***

W szczególności chciałem zapytać o to, czy poprawnie zastosowałem cli() i sei() w procedurach obsługi przerwań. Ponadto chciałem zapytać, czy kod wewnątrz while(1) jest poprawny i czy nie lepiej byłoby zapisać to tak:



while (1) { unsigned long int timerLatch; short actualTimeLatch;



cli(); timerLatch = timer; actualTimeLatch = actualTime; sei();



if (timerLatch == 100000) LCDshowString("---"); else { sprintf(s, "%d ", actualTimeLatch); LCDshowString(s); } }



Chodzi o sytuację, w której w pętli głównej jest przetwarzana wartość zmiennej timer albo actualTime (np. został odczytany pierwszy bajt) i w trakcie nastąpi przerwanie, zmieni wartość zmiennej, obsługa przerwania się skończy, zostanie dokończone przetwarzanie zmiennej timer albo actualTime -- wartość chyba będzie błędna (jeden bajt z poprzedniej wartości, reszta bajtów po aktualizacji w przerwaniu).



Z góry dziękuję za rady.



Robbo


Ja bym liczył czas pomiedzy dwoma narastającymi zboczami. Ale ja herbatę cukrem...

[...]

W dniu 09.02.2011 13:54, Robbo pisze:

Nie.

Jeżeli nie deklarujesz przerwań explicite jako nieblokujących to one dbają o wyłączność za Ciebie. Pisząc sei na końcu przerwania umożliwiasz odpalenie przerwania po 2 cyklach a prawdopodobnie wyjście z przerwania trwa chwilę dłużej - przy dużym nagromadzeniu przerwań do obsługi skończy się to przepełnieniem stosu.

Użytkownik "Robbo" snipped-for-privacy@mmm.com napisał w wiadomości news:4d528e6b$0$2436$ snipped-for-privacy@news.neostrada.pl...

Widzisz, tak to jest jak się nigdy żadnego kodu w assemblerze na AVR-a nie napisało.

Otóż procesor AVR wchodząc w obsługę przerwania sam wyłącza przerwania. Więc stan jest taki, że do czasu zakończenia obsługi aktualnego przerwania, obsługa kolejnych przerwań jest wyłączona.

Więc cli() po wejściu w obsługę przerwania nie ma sensu (bo przerwania już są wyłączone), natomiast sei() dajesz wtedy jak chcesz włączyć ręcznie obsługę przerwań, co oczywiście jest możliwe, tylko na stos trzeba uważać (jeśli tych przerwań jest dużo w jednostce czasu).

sei() na końcu też nie ma sensu, ponieważ procedury obsługi przerwania nie opuszcza się poprzez ret, tylko poprzez reti, będące niczym innym jak połączeniem: ret + sei.

truskawki :P

... no chyba ze znamy inne dowcipy

c.

[...]

Truskawki to ja ze śmietaną, najlepiej bitą.

To nie dowcip.

Nie bawiłem się jeszcze przerwaniami w atmedze i muszę zaraz sprawdzić w nocie jak jest ale zapytam wybiegiem a czy aby nie ma przypadkiem zatrzasku przerwań przychodzących podczas obsługi przerwań i priorytetów przerwań?

Marek

Użytkownik "Robbo" snipped-for-privacy@mmm.com napisał w wiadomości news:4d528e6b$0$2436$ snipped-for-privacy@news.neostrada.pl...

A może wykorzystac dwa liczniki. Jeden zliczający uformowane impulsy a drugi z autoresetem wyzwalający przerwanie co określony czas. W przerwaniu sprawdzałbym stan licznika zliczającego, następnie kasował go i opuszczał przerwanie.

Marek

Użytkownik "RoMan Mandziejewicz" snipped-for-privacy@pik-net.pl napisał w wiadomości news: snipped-for-privacy@pik-net.pl...

Pan policjant widzę chyba po służbie bo też pozwala sobie na NTG. Wiem wiem. Jedzie pan na sygnale jako pojazd uprzywilejowany.

Marek

W dniu 09.02.2011 13:54, Robbo pisze:

Witam Na wstępie to się zapytam, na czym Koledze tak naprawdę zależy ? Na pomiarze czasu (okresu) czy częstotliwości ? Bo pisze Kolega o "dokładności 0,2Hz" i o "wyświetlaniu czasu trwania dodatniej połówki". Ponieważ zakres częstotliwości jest stosunkowo niewielki, można to zrobić na dwa sposoby i kilka wariacji ;-).

  1. Pomiar ilości impulsów w stałym przedziale czasu. Przy wymaganej rozdzielczości 0,2Hz pomiar musiałby trwać co najmniej 5 sekund. Metod realizacji jest wiele. Przy tak małej częstotliwości zbocze sygnału mierzonego można sobie wykrywać programowo w pętli (pod warunkiem, że w systemie nie istnieją przerwania, których obsługa zajmuje ponad 6ms). Jako bramkę czasu najprościej wykorzystać któryś z timerów. Bardziej elegancko liczyć zbocza w obsłudze zewnętrznego przerwania. Można zliczać impulsy sprzętowo za pomocą licznika 16-bitowego, a 8-bitowym realizować bramkę czasu. Można również całkowicie sprzętowo z wykorzystaniem rejestru ICR licznika 16-bitowego i licznika 8-bitowego generującego na wyjściu OCx impulsy o czasie bramkowania.
  2. Pomiar czasu między impulsami (okresu). Tu aż się prosi wykorzystać Input Capture Register.

Pozdrawiam Grzegorz

Dziękuję. A chciałem jeszcze prosić o odpowiedź na pytanie dotyczące użycia sei i cli w pętli głównej.

R.

Ja miodem...

Sprawdziłem. W tej chwili:

- mierzenie czasu między narastającym a opadającym zboczem dodatniej połówki daje czas 998/7.

- mierzenie czasu między narastającymi zboczami kolejnych dodatnich połówek daje czas 2000/1.

R

Za mocny smak...

Jak Ci się wydaje - który wynik jest bardziej zbliżony do rzeczywistości?

Wydaje mi się, że oczywiście ten drugi wynik :)

Ale, jeszcze trzeba będzie popracować nad elektroniką i może uda się wykrzesać coś więcej z metody nr 1 Może też ujemną połówkę uda się użyć i wtedy miałbym szybszy odczyt częstotliwości z niewielkim błędem.

R.

Owszem, nieprecyzyjnie napisałem. Mierzyć będę czas i na tej podstawie będzie działać układ. Użytkownikowi będę jednak prezentował częstotliwość wyznaczoną na podstawie zmierzonego czasu.

I to chyba będzie eleganckie rozwiązanie. Właśnie się tym zająłem i mam już efekty. Jutro pozwolę sobie wkleić kod do oceny.

R.

Użytkownik "Robbo" snipped-for-privacy@mmm.com napisał w wiadomości news:4d531cb8$0$2457$ snipped-for-privacy@news.neostrada.pl...

W wielkim skrócie - cli/sei używasz wtedy gdy dokonujesz zapisu/odczytu zmiennych, które: wykorzystujesz też w przerwaniach i cała operacja zajmuje więcej niż 1 operację procesora (zmienne dłuższe niż 1 bajt i operacje bitowe). No i oczywiście wtedy gdy z pewnych przyczyn (głównie czasowych) dana operacja nie powinna zostać przerwana obsługą przerwania.

Oto kod:

volatile unsigned short icr, prevIcr;

SIGNAL (SIG_INPUT_CAPTURE3) { icr = ICR3; acutalDuration = icr - prevIcr; prevIcr = icr; }

int main(void) { char buf[16]; unsigned short actualDurationLatch;

TCCR3A = 0; TCCR3B |= _BV(ICES3) | _BV(ICNC3) | _BV(CS32) | _BV(30); // prescaler 64 ETIMSK |= _BV(TICIE3);

sei();

while (1) { cli(); actualDurationLatch = actualDuration; sei();

sprintf(s, "%u ", actualDurationLatch); LCDwriteString(s); } }

Na kwarcu 16MHz i prescalerze 64, ICR3 zwiększa się z częstotliwością

1/250000. Dla 50Hz na wyświetlaczu mam liczbę 5000. Mogę zejść do ok. 2Hz -- poniżej już się nie da, bo licznik 16-bitowy ICR3 się przepełnia. Jednak 2Hz jest dla mnie akceptowalnym wynikiem i nie potrzebuję już zwiększać prescalera, kosztem obniżenia dokładności pomiaru.

Chciałem zapytać

  1. Czy kod jest poprawny, a jeśli nie, to co poprawić?
  2. Jak poradzić sobie teraz z "odcinaniem" dla częstotliwości mniejszej niż
2Hz? Poniżej 2Hz licznik się przekręci i wskazany czas nie będzie poprawny; może być też sytuacja, gdy sygnał zaniknie (częstotliwość 0Hz). Muszę mieć o tym informację. W moim programie z timerem nie było problemu (pokazywały się "---" na wyświetlaczu dla zbyt niskich częstotliwości) -- tu nie wiem, jak to dobrze rozwiązać.

R.

W dniu 2011-02-10 16:09, Robbo pisze:

Nie używaj cli i sei do robienia sekcji atomowych, te makra nie są zabezpieczone przed optymalizacją i kompilator może zmienić kolejność instrukcji (choć oczywiście nie musi). Zamiast tego posłuż się ATOMIC_BLOCK

formatting link

Albo zmniejsz taktowanie timera, albo wykorzystaj przerwanie od przepełnienia do "dorobienia" trzeciego bajtu do licznika.

Czy chodzi o coś takiego (pisane z pamięci)?

volatile unsigned long icr, prevIcr; volatile char overflow;

SIGNAL (SIG_INPUT_CAPTURE3) { icr = ICR3; acutalDuration = icr - prevIcr + overflow * 0xffffUL; overflow = 0; prevIcr = icr; }

SIGNAL (SIG_INPUT_OVERFLOW3) { overflow++; }

int main(void) { char buf[16]; unsigned long actualDurationLatch;

TCCR3A = 0; TCCR3B |= _BV(ICES3) | _BV(ICNC3) | _BV(CS32) | _BV(30); // prescaler 64 ETIMSK |= _BV(TICIE3) | _BV(TOIE3);

sei();

while (1) { cli(); actualDurationLatch = actualDuration; sei();

sprintf(s, "%ld ", actualDurationLatch); LCDwriteString(s); } }

Jeśli chodzi o coś takiego, to mi nie działa. "overflow" impulsuje czasem na wartość "1", bo ICR3 nieustannie się przepełnia. Czy ktoś może wskazać, jak poprawnie zastosować rozwiązanie z przepełnieniem? To mogłoby być dobre rozwiązanie "odcięcia" niskich częstotliwości, tylko nie wiem, jak poprawnie to zrealizować.

R.

W dniu 2011-02-10 16:27, Robbo pisze:

Po pierwsze jak obydwa przerwania (przechwytywanie i przepełnienie) zostaną zgłoszone jednocześnie, to będziesz miał nieprawidłowy wynik. Trzeba w przerwaniu od przechwytywania sprawdzać obecność nieobsłużonego przepełnienia. Zmienna prev powinno pamiętać nie tylko stan licznika sprzętowego, ale i software'owego. Zamiast mnożyć 65535 powinieneś mnożyć przez 65536, a zdecydowanie lepiej przesunąć liczbę o 16-bitów (albo jeszcze lepiej użyć unii liczby 32-bitowej z tablicą 4 bajtów, bo gcc ma słabą optymalizację przy obsłudze długich liczb na avr). Do tego trzeba pamiętać o ręcznym rzutowaniu na 32-bity przed przesunięciem, bo kompilator tego za ciebie nie zrobi (co najwyżej zrobi promocję do

16-bitów, bo tyle ma int).

Join the Discussion

Have something to add? Share your thoughts — no account required.

Didn't find your answer?

Ask the community — no account required