Pytanie:
Procedura obsługi przerwań AVR nie wykonuje się tak szybko, jak oczekiwano (narzut instrukcji?)
arminb
2013-04-01 23:23:36 UTC
view on stackexchange narkive permalink

Tworzę mały analizator logiczny z 7 wejściami. Moje urządzenie docelowe to ATmega168 z częstotliwością taktowania 20 MHz. Aby wykryć zmiany logiczne, używam przerwań zmiany pinów. Teraz próbuję znaleźć najniższą częstotliwość próbkowania, jaką mogę wykryć te zmiany pinów. Ustaliłem wartość minimum 5,6 µs (178,5 kHz). Nie mogę poprawnie wychwycić każdego sygnału poniżej tego współczynnika.

Mój kod jest napisany w C (avr-gcc). Moja procedura wygląda następująco:

  ISR () {pinc = PINC; // char timestamp_ll = TCNT1L; // char timestamp_lh = TCNT1H; // char timestamp_h = timerh; // 2-bajtowa liczba całkowita stack_counter ++;}  

Moja przechwycona zmiana sygnału znajduje się w pinc . Aby go zlokalizować, mam 4-bajtową wartość znacznika czasu.

W arkuszu danych przeczytałem, że procedura obsługi przerwań wymaga 5 zegarów, aby wskoczyć i 5 zegarów, aby powrócić do głównej procedury. Zakładam, że każde polecenie w moim ISR () wymaga 1 zegara do wykonania; Podsumowując, powinno to być narzut 5 + 5 + 5 = 15 . Czas trwania jednego zegara powinien być zgodny z częstotliwością zegara 20 MHz 1/20000000 = 0,00000005 = 50 ns . Całkowity narzut w sekundach powinien zatem wynosić: 15 * 50 ns = 750 ns = 0,75 µs . Teraz nie rozumiem, dlaczego nie mogę uchwycić niczego poniżej 5,6 µs. Czy ktoś może wyjaśnić, co się dzieje?

może 5 zegarów do wysłania kodu ISR, który obejmuje zapisywanie kontekstu i przywracanie epilogu / prologu, którego nie widzisz w źródle C. Ponadto, co robi sprzęt, gdy przerwanie zostaje przerwane? Czy jest w jakimś stanie snu. (Nie znam AVR, ale generalnie przerywanie przetwarzania niektórych stanów może potrwać dłużej).
@arminb Zobacz także [to pytanie] (http://electronics.stackexchange.com/questions/64360/circuit-that-can-convert-current-pulses-to-voltage), aby uzyskać więcej pomysłów na to, jak rejestrować zdarzenia zewnętrzne z większą precyzją . Interesująca może być również [ta notatka] (www.atmel.com/Images/doc2505.pdf).
Dwa odpowiedzi:
angelatlarge
2013-04-01 23:30:36 UTC
view on stackexchange narkive permalink

Jest kilka problemów:

  • Nie wszystkie polecenia AVR wymagają 1 zegara do wykonania: jeśli spojrzysz na tył arkusza danych, zobaczysz liczbę zegarów potrzebnych do wykonania każda instrukcja do wykonania. Na przykład AND jest instrukcją jednokierunkową, MUL (multiply) pobiera dwa zegary, a LPM (ładowanie pamięci programu) to trzy , a CALL to 4. Tak więc, jeśli chodzi o wykonanie instrukcji, to naprawdę zależy od instrukcji.
  • 5 zegarów do włączenia i 5 do powrotu może wprowadzać w błąd. Jeśli spojrzysz na zdezasemblowany kod, zauważysz, że oprócz instrukcji skoku i RETI kompilator dodaje różnego rodzaju inny kod, co również wymaga czasu. Na przykład możesz potrzebować zmiennych lokalnych, które są tworzone na stosie i muszą zostać usunięte, itp. Najlepszą rzeczą do zrobienia, aby zobaczyć, co się naprawdę dzieje, jest przyjrzenie się demontażu.
  • Na koniec pamiętaj gdy jesteś w swojej procedurze ISR, twoje przerwania nie są wyzwalane. Oznacza to, że nie będziesz w stanie uzyskać takiego rodzaju wydajności, jakiego oczekujesz od analizatora logicznego, chyba że wiesz, że poziomy sygnału zmieniają się w odstępach czasu dłuższych niż jest to potrzebne do obsługi przerwania. Dla jasności, po obliczeniu czasu potrzebnego na wykonanie ISR, daje to górną granicę tego, jak szybko możesz przechwycić jeden sygnał . Jeśli chcesz uchwycić dwa sygnały, zaczynasz mieć kłopoty. Aby być zbyt szczegółowym na ten temat, rozważ następujący scenariusz:

enter image description here

Jeśli x to czas potrzebny na obsługę twojego przerwania , wtedy sygnał B nigdy nie zostanie przechwycony.


Jeśli weźmiemy twój kod ISR, umieść go w procedurze ISR (użyłem procedury ISR (PCINT0_vect) ), zadeklaruj wszystkie zmienne volatile i skompiluj dla ATmega168P , zdemontowany kod wygląda następująco (zobacz odpowiedź @ jipple, aby uzyskać więcej informacji) zanim przejdziemy do kodu, który „coś robi” ; innymi słowy, prolog do twojego ISR jest następujący:

  37 .loc 1 71 0 38 .cfi_startproc 39 0000 1F92 push r1 40 .LCFI0: 41 .cfi_def_cfa_offset 3 42 .cfi_offset 1, - 2 43 0002 0F92 push r0 44 .LCFI1: 45 .cfi_def_cfa_offset 4 46 .cfi_offset 0, -3 47 0004 0FB6 in r0, __ SREG__ 48 0006 0F92 push r0 49 0008 1124 clr __zero_reg__ 50 000a 8F93 push r2_ 52_FCIoffset: 51. 5 53 .cfi_offset 24, -4 54 000c 9F93 push r25 55 .LCFI3: 56 .cfi_def_cfa_offset 6 57 .cfi_offset 25, -5 58 / * prologue: Signal * / 59 / * rozmiar ramki = 0 * / 60 / * rozmiar stosu = 5 * / 61 .L__stack_usage = 5  

więc PUSH x 5, in x 1, clr x 1. Nie tak źle, jak 32-bitowe vars jipple, ale nadal nie jest niczym.

Część tego jest konieczna (rozwiń dyskusję w komentarzach). Co oczywiste, ponieważ procedura ISR może wystąpić w dowolnym momencie, musi wstępnie ustawić rejestry, których używa, chyba że wiesz, że żaden kod, w którym może wystąpić przerwanie, nie używa tego samego rejestru, co procedura przerwania. Na przykład następujący wiersz w zdemontowanym ISR:

  push r24  

Jest tak, ponieważ wszystko przechodzi przez r24 : twój pinc jest tam ładowany, zanim trafi do pamięci, itd. Więc musisz mieć to najpierw. __SREG__ jest ładowany do r0 , a następnie wypychany: jeśli to mogłoby przejść przez r24 , możesz zapisać sobie PUSH


Kilka możliwych rozwiązań:

  • Użyj ścisłej pętli odpytywania, zgodnie z sugestią Kaza w komentarzach. Prawdopodobnie będzie to najszybsze rozwiązanie, bez względu na to, czy piszesz pętlę w C czy asemblerze.
  • Zapisz swój ISR w asemblerze: w ten sposób możesz zoptymalizować użycie rejestru w taki sposób, aby jak najmniej muszą zostać zapisane podczas ISR.
  • Zadeklaruj swoje procedury ISR ISR_NAKED, chociaż okazuje się, że jest to bardziej rozwiązanie typu „czerwony śledź”. Kiedy deklarujesz procedury ISR ISR_NAKED , gcc nie generuje kodu prologu / epilogu, a ty jesteś odpowiedzialny za zapisywanie wszystkich rejestrów modyfikowanych przez twój kod, a także za wywołanie reti (powrót z przerwanie). Niestety, nie ma sposobu na bezpośrednie użycie rejestrów w avr-gcc C (oczywiście można to zrobić w asemblerze), jednak jedyne, co możesz zrobić, to powiązać zmienne z określonymi rejestrami za pomocą rejestru + asm słowa kluczowe, takie jak: register uint8_t counter asm ("r3"); . Jeśli to zrobisz, dla ISR będziesz wiedział, jakich rejestrów używasz w ISR. Problem polega na tym, że nie ma możliwości wygenerowania push i pop w celu zapisania używanych rejestrów bez wbudowanego asemblacji (por. Punkt 1). Aby zapewnić konieczność zapisywania mniejszej liczby rejestrów, możesz również powiązać wszystkie zmienne inne niż ISR z określonymi rejestrami, jednak nie, napotkasz problem, że gcc używa rejestrów do tasowania danych do iz pamięci. Oznacza to, że jeśli nie spojrzysz na demontaż, nie będziesz wiedział, jakich rejestrów używa twój główny kod. Więc jeśli rozważasz ISR_NAKED , równie dobrze możesz napisać ISR w asemblerze.
Dzięki, więc mój kod C powoduje ogromne obciążenie? Czy byłoby szybciej, gdybym napisał to w asemblerze? Jeśli chodzi o drugą rzecz, zdawałem sobie z tego sprawę.
@arminb: Nie wiem wystarczająco, aby odpowiedzieć na to pytanie. Zakładam, że kompilator jest dość inteligentny i nie bez powodu robi to, co robi. Powiedziawszy to, jestem pewien, że gdybyś spędził trochę czasu z asemblacją, mógłbyś wycisnąć jeszcze kilka cykli zegara z rutyny ISR.
Myślę, że jeśli chcesz najszybszej odpowiedzi, generalnie unikasz przerwań i odpytujesz piny w ciasnej pętli.
@Kaz: to może być prawda: wiem, że pisanie SPI tak szybko, jak to możliwe, jest szybsze niż używanie odpytywania niż przerwań. Nie wiem na pewno w tej sprawie, ale nie wydaje się to nieprawdopodobne.
Mając na uwadze konkretne cele, można zoptymalizować kod za pomocą assemblera. Na przykład kompilator zaczyna od umieszczenia wszystkich używanych rejestrów na stosie, a następnie rozpoczyna wykonywanie właściwej procedury. Jeśli masz rzeczy krytyczne czasowo, możesz przesunąć część pchnięcia do tyłu i przesunąć rzeczy krytyczne czasowo do przodu. Więc tak, możesz optymalizować za pomocą asemblera, ale sam kompilator jest całkiem sprytny. Lubię używać skompilowanego kodu jako punktu startowego i modyfikować go ręcznie zgodnie z moimi specyficznymi wymaganiami.
Czego używasz do generowania listy demontażu?
AilivblvuyCMT `avr-gcc ... -ahlms`
Naprawdę fajna odpowiedź. Dodam, że kompilator dodaje różnego rodzaju przechowywanie i przywracanie rejestrów, aby zaspokoić potrzeby większości użytkowników. Możliwe jest napisanie własnego modułu obsługi przerwań typu bare-bone - jeśli nie potrzebujesz całego tego narzutu. Niektóre kompilatory mogą nawet oferować opcję tworzenia „szybkiego” przerwania, pozostawiając dużą część „księgowości” programiście. Niekoniecznie musiałbym iść od razu do ciasnej pętli bez ISR, gdybym nie mógł dotrzymać harmonogramu. Najpierw rozważyłbym szybsze uC, a potem wymyśliłbym, czy mógłbym użyć jakiegoś sprzętu do klejenia, takiego jak zatrzask i RTC.
jippie
2013-04-01 23:47:40 UTC
view on stackexchange narkive permalink

Jest wiele rejestrów PUSH'ing i POP'ing, które mają miejsce przed uruchomieniem twojego ISR, to jest na szczycie 5 cykli zegara, o których wspomniałeś. Przyjrzyj się deasemblacji wygenerowanego kodu.

W zależności od używanego łańcucha narzędzi, zrzucanie zestawu zawierającego listę nas wykonanych na różne sposoby. Pracuję na linii poleceń Linuksa i to jest polecenie, którego używam (wymaga pliku .elf jako danych wejściowych):

  avr-objdump -C -d $ (src) .elf  

Spójrz na fragment kodu, którego ostatnio użyłem dla ATtiny. Oto jak wygląda kod w C:

  ISR (INT0_vect) {uint8_t myTIFR = TIFR; uint8_t myTCNT1 = TCNT1;  

A to jest wygenerowany dla niego kod asemblera:

  00000056 <INT0_vect>: 56: 1f 92 push r1 58: 0f 92 push r0 5a: 0f b6 w r0, SREG; 0x3f 5c: 0f 92 pchanie r0 5e: 11 24 eor r1, r1 60: 2f 93 pchanie r18 62: 3f 93 pchanie r19 64: 4f 93 pchanie r20 66: 8f 93 pchanie r24 68: 9f 93 pchanie r25 6a: af 93 pchanie r26 6c: bf 93 push r27 6e: 48 b7 w r20, TIFR; uint8_t myTIFR = TIFR; 70: 2f b5 w r18, TCNT1; uint8_t myTCNT1 = TCNT1;  

Szczerze mówiąc, moja procedura C używa kilku dodatkowych zmiennych, które powodują te wszystkie push'e i pop, ale masz o co chodzi.

Ładowanie 32-bitowej zmiennej wygląda następująco:

  ec: 80 91 78 00 lds r24, 0x0078 f0: 90 91 79 00 lds r25, 0x0079 f4: a0 91 7a 00 lds r26, 0x007A f8 : b0 91 7b 00 lds r27, 0x007B  

Zwiększenie 32-bitowej zmiennej o 1 wygląda następująco:

  5e: 11 24 eor r1, r1 d6 : 01 96 adiw r24, 0x01; 1 d8: a1 1d adc r26, r1 da: b1 1d adc r27, r1  

Przechowywanie 32-bitowej zmiennej wygląda następująco:

  dc: 80 93 78 00 o. 0x0078, r24 e0: 90 93 79 00 o. 0x0079, r25 e4: a0 93 7a 00 o. 0x007A, r26 e8: b0 93 7b 00 sts 0x007B, r27  

Oczywiście, po wyjściu z ISR, musisz oczywiście zdjąć stare wartości:

  126: bf 91 pop r27 128: af 91 pop r26 12a: 9f 91 pop r25 12c: 8f 91 pop r24 12e: 4f 91 pop r20 130: 3f 91 pop r19 132: 2f 91 pop r18 134: 0f 90 pop r0 136: 0f poza SREG, r0; 0x3f 138: 0f 90 pop r0 13a: 1f 90 pop r1 13c: 18 95 reti  

Zgodnie z podsumowaniem instrukcji w arkuszu danych, większość instrukcji to pojedynczy cykl, ale PUSH i POP są podwójne cykl. Wiesz, skąd pochodzi opóźnienie?

Dzięki za odpowiedź! Teraz jestem świadomy tego, co się dzieje. Szczególnie dziękuję za polecenie `avr-objdump -C -d $ (src) .elf`!
Poświęć chwilę, aby zrozumieć instrukcje asemblacji, które wypluwa `avr-objdump`, są one pokrótce wyjaśnione w arkuszu danych w Podsumowaniu instrukcji. Moim zdaniem dobrą praktyką jest zapoznanie się z mnemonikami, ponieważ może to bardzo pomóc podczas debugowania kodu C.
Faktycznie, deasemblacja jest przydatna jako część domyślnego pliku `Makefile`: więc za każdym razem, gdy budujesz swój projekt, jest on również automatycznie deasemblowany, więc nie musisz o tym myśleć ani pamiętać, jak zrobić to ręcznie.


To pytanie i odpowiedź zostało automatycznie przetłumaczone z języka angielskiego.Oryginalna treść jest dostępna na stackexchange, za co dziękujemy za licencję cc by-sa 3.0, w ramach której jest rozpowszechniana.
Loading...