Elektroda.pl
Elektroda.pl
X
Proszę, dodaj wyjątek www.elektroda.pl do Adblock.
Dzięki temu, że oglądasz reklamy, wspierasz portal i użytkowników.

[ATTiny2313/C] Błąd w przerwaniu po dodaniu jednej lini kodu

ADI-mistrzu 03 Maj 2010 20:11 1852 9
  • #1 03 Maj 2010 20:11
    ADI-mistrzu
    Poziom 30  

    Witam!

    Mam taki oto kod:

    Code:
    #include <avr/io.h>
    
    #include <avr/interrupt.h>
    #define F_CPU 10000000UL
    #include <util/delay.h>

    char znaki[10]={48, 49, 50, 51, 52, 53, 54, 55, 56, 57};
    //znaki ASCII:  0    1   2   3   4   5   6   7   8   9

    volatile uint16_t m_sekundy_wtrysk=0, m_sekundy_obr=0, licznik_z, wynik_z; //dokładność od 0.1ms do 200ms
    volatile  uint16_t *licznik; //wskaźnik na zmienną "licznik"
    volatile  uint16_t *wynik; //wskaźnim na zmienną "wynik"


    volatile ISR(INT1_vect){ //przerwanie od złącza PD3 (INT1)
       TCCR1B |= _BV(CS10); //start timera (przerwanie przez zrównanie)
       GIMSK &= ~_BV(INT1); //wyłącz przerwania od opadajacego zbocza (wezwanie tego przerwania)
       sei(); //uruchomienie przerwań w przerwaniu
       while((PIND&0x08)==0); //dopuki jest sygnał... (blokada wyjścia z przerwania)
          m_sekundy_wtrysk = licznik_z;
          _delay_us(5);
       while(PIND&0x08); //obliczanie czasu pomiędzy impulsami
          TCCR1B &= ~_BV(CS10); // zatrzymanie timera (przerwanie przez zrównanie)
          m_sekundy_obr = licznik_z;
             write_command(0xC3); //przejdź do 3 znaku w 2 wierszu
             if(m_sekundy_obr >= 0xFFDC){ //jesli zmienna przepełniona
                write_text("Nadmiar!");
             }else{
                write_to_lcd(znaki[m_sekundy_obr/10000]);
                write_to_lcd(znaki[(m_sekundy_obr/1000)%10]);
                write_to_lcd(znaki[(m_sekundy_obr/100)%10]);//wyświetlanie liczby 5 cyfrowej
                write_to_lcd(znaki[(m_sekundy_obr/10)%10]);
                write_to_lcd(znaki[m_sekundy_obr%10]);
             }
             write_command(0x89); //9 kolumna, 0 wiersz na wyswietlaczu
       licznik_z = 0; //zerowanie zmiennej licznikowej
       TCNT1 = 0; // zerowanie licznika
       while((PIND&0x08)==0);//jesli pojawil sie sygnal, zaczekaj az zaniknie




       GIMSK |= _BV(INT1); //wlacz przerwania od opadajacego zbocza
    }

    volatile ISR(TIMER1_COMPA_vect){ //przerwanie od zrównania licznika
       TCNT1 = 0; // zerowanie licznika
       licznik_z += 0x0001; //zwiększ zmienną o 1
       if(licznik_z==0xFFFF){ //jesli licznik sie przepelnil
          write_command(0xC9);
          write_text("!"); //poinformuj o tym

       }
    }
    int main(void){

    DDRB = 0xFF;
    PORTB = 0x0F;

    DDRD = ~_BV(PD3);//0xFF;
    PORTD = _BV(PD3);//0x00;

    MCUCR |= _BV(ISC11); //Opadajace zborze wyzwala przerwanie
    GIMSK |= _BV(INT1);//Włączenie przerwania od INT1 (PD3)

    TIMSK |= _BV(OCIE1A); //Przerwanie od zrównania z OCR1A
    TCCR1B = 0x00; //Timer wyłączony przy starcie
    OCR1A = 1000; //Przerwanie gdy wartość licznika równa 1000

       lcd_init();
       write_command(0xC0); //0 kolumna, 1 wiersz
       write_text("->");
       sei(); //wlacz przerwania
    while(1)
    {

    }
    }

    Program jest dość prosty, ma wyliczać jak długo trwał impuls. Czyli gdy pojawi się opadające zbocze, wyzwala się przerwanie, które uruchamia przerwanie od zrównania licznika, w którym to zmienna jest zwiększana o 1.
    Jeśli wyłączę linię while(PIND&0x08); która oblicza czas pomiędzy impulsami, liczy wtedy sam czas impulsów i to działa ok, ale gdy ją włączę, to za pierwszym razem działa program ok, raz zewrę z masą pin to nic się nie stanie, ale za drugim wyświetli się dana. Ale gdy drugi raz chce to zrobić, to już wystarczy że dotknę masy i się cyfry wyświetlają, ale po kilku razach już niema cyfr a są jakieś dziwne znaczki i procek się restartuje :|

    Nie wrzucałem już funkcji od wyświetlacza bo dobrze je przetestowałem i chodzą prawidłowo, niema raczej sensu zaśmiecać tematu zbędnym kodem, coś jest w tym przerwaniu czego nie mogę znaleźć.
    Jak dla mnie to powinno działać dobrze, jak raz zewrę pin to program przechodzi w przerwanie gdzie odlicza jak długo jest impuls. Gdy puszczę wpisuje tą daną do zmiennej i liczy dalej sprawdzając jaki jest odstęp między impulsami.
    Po ponownym zwarciu powinien przerwać odliczanie, zatrzymać przerwanie, wyświetlić co tam wyliczył, zaczekać aż puszę pin i włączyć przerwanie od zwarcia pinu i utknąć w pętli while w głównej funkcji i czekać na przerwanie, a tak się nie dzieje nie wiem czemu.

    Pozdrawiam

    0 9
  • Pomocny post
    #2 03 Maj 2010 20:34
    BoskiDialer
    Poziom 34  

    Średnio chce mi się analizować ten kod, ale jeśli załączasz przerwania w przerwaniu od INT1 po wcześniejszym wyłączeniu przerwania od INT1 - jest poprawnym rozwiązaniem, gdyż nie ma możliwości wejścia do funkcji przerwania od INT1 podczas jej wykonywania niezależnie od okoliczności; tak ponowne zezwolenie na przerwanie od INT1 bez wcześniejszego zablokowania przerwań (cli) może doprowadzić do zagnieżdżania się przerwania od INT1, co ostatecznie doprowadzi do przepełnienia stosu i resetu. Będąc w przerwaniu od INT1 nie możesz zezwolić na to, aby były ustawione jednocześnie zezwolenie na INT1 oraz globalne I a to się dzieje pod koniec funkcji. Dodaj cli(); zaraz przed "GIMSK |= _BV(INT1);"

    Dodano po 12 [minuty]:

    W przerwaniu masz pętle oczekujące na zmianę stanu INT1, pomijając to, że nie polecam takiego rozwiązania, to wiedz, że będąc w funkcji obsługi przerwania flaga od INT1 również będzie ustawiana, więc natychmiast po wyjściu z funkcji obsługi przerwania ponownie wejdziesz do przerwania, a więc z wcześniejszym moim opisem to jest bezpośrednia przyczyna przepełnienia stosu i resetu. Jeśli tego nie chcesz, zaraz po dodanym cli() o którym pisałem będziesz musiał skasować flagę od INT1 dodając gdzieś po ponownym załączeniu przerwania od INT1 coś takiego:
    GIFR = (1<<INTF1);

    Swoją drogą skoro w pętli głównej w main jest pusto a przerwanie i tak można przerwać, to czemu nie przeniesiesz kodu właśnie do pętli wewnątrz main? Wystąpienie warunku przerwania można sprawdzać ręcznie testując flagę INTF1 w rejestrze GIFR;

    0
  • #3 03 Maj 2010 20:52
    ADI-mistrzu
    Poziom 30  

    W ATTiny2313 zamiast GIFR jest EIFR i sądzę że o to Ci chodziło.
    W main nie mogę dać, bo tam będzie inny kod wykonywany, a ten jest poboczny, jako dodatkowa funkcja wykonywana przez uC.

    Po dodaniu cli() i zaraz za tym EIFR = (1<<INTF1); już jest wszystko ok, działa jak powinno :wink:

    0
  • #4 03 Maj 2010 21:16
    mirekk36
    Poziom 42  

    ADI-mistrzu --> kolega pokazał idealny przykład "jak nie powinno się pisać programów dla takich procków" a w szczególności "jak nie powinno się pisać procedur obsługi przerwań" - to już jest nawet lekki hardcore. (pętle oczekujące w przerwaniu???? wyświetlanie na LCD w przerwaniu???) No ale to wprost wynika jeszcze z niezrozumienia do końca działania przerwań niestety.

    Masz złudną nadzieję, że już wszystko jest OK i działa jak powinno. Zobaczysz armagedon jak zaczniesz pisać program w pętli głównej.

    Tak jak radził ci kolega wyżej praktycznie cały kod przerwania INT możesz przenieść do pętli głównej nawet jeśli tam będzie wykonywany jeszcze inny i nawet bardzo długi i skomplikowany kod.

    Wykorzystaj tylko jakiś timer do utworzenia timerów-programowych i będziesz mógł w "jednym czasie" wykonywać w pętli głównej wiele różnych procesów w ustalonej kolejności.

    0
  • #5 03 Maj 2010 21:27
    ADI-mistrzu
    Poziom 30  

    Jak to stworzyć timer do utworzenia timerów-programowych ? Nie mogę sobie tego jakoś na razie wyobrazić, albo na razie nie wiem o co chodzi.

    Z tym co BoskiDialer to też tak trochę nie widzi mi się, bo jeśli kod programu jakiś tam będzie wykonywany, i co jakiś czas sprawdzane czy nastąpiło przerwanie czy pin został zwarty i tam wejdzie, to może zajść taka sytuacja że sygnał pojawił się wcześniej i np. w jego połowie dopiero to zauważyliśmy i dopiero wtedy zaczeliśmy odliczać.

    A ja chciałem zrobić tak, że sygnał jest najważniejszy, że jeśli się pojawi to ma zostać zbadany ponad wszystko, potem można robić resztę (prócz niektórych miejsc w kodzie głównym, gdzie przerwanie nie może nastąpić, np. podczas transmisji danych jakąś szyną).
    Samo wyświetlanie zostanie wyciągnięte z przerwania, na razie tam siedzi bo łatwiej mi kodem operować w czasie pisania. W zamyśle chce po prostu co jakiś czas zmienne wyświetlać w których będą czasy sygnałów.

    0
  • Pomocny post
    #6 03 Maj 2010 21:57
    BoskiDialer
    Poziom 34  

    Nie neguję rozwiązania przerwanie-w-przerwaniu, w niektórych sytuacjach jest to rozwiązanie bardzo eleganckie i efektywne, tylko należy pamiętać aby dobrze wyłączać przerwania. Nie popieram natomiast pętli oczekującej w przerwaniu, kod warto przeprojektować względem tych pętli pozbywając się ich - czy to przez automat skończony zmieniając INT1 na przerwanie na obu zboczach i w zależności od aktualnego stanu na pinie i aktualnego stanu wykonywać odpowiedni kod, czy to przesuwając część do pętli głównej. W większości przypadków powinno dać się tak przeprojektować kod, aby nie trzeba było używać zagnieżdżonych przerwań, ale to już kwestia samego projektu, byle by nie było pętli oczekujących.

    Automat skończony można zrealizować w tym przypadku dość prosto:
    przy zmianie stanu pinu:
    - jeśli przy INT1 aktualnie stan niski (zbocze opadające) oraz stan 1, to: załączenie timera, zmiana na stan 2
    - jeśli przy INT1 aktualnie stan wysoki (zbocze narastające) oraz stan 2, to załączenie dodatkowego timera, aby przepełnił się po 5us, zmiana na stan 3
    - jeśli przy INT1 aktualnie stan niski (zbocze opadające) oraz stan 3, to: jeśli dodatkowy timer się przepełnił, to wykonanie tych czynności ze środka kodu, stan4, w przeciwnym przypadku załączyć przerwanie od przepełnienia dodatkowego timera
    - jeśli dodatkowy timer się przepełnił oraz stan 3, to realizacja tego czegoś ze środka kodu, przejście do stanu 4
    - jeśli przy INT1 aktualnie stan wysoki (zbocze narastające), to przejście do stanu 1.

    W ten sposób wszystko jest w 100% na przerwaniach bez żadnych oczekiwać na bardzo prostym automacie skończonym, jednak wszystko zależy od projektu. Ze względu na pętle oczekujące na stan na pinie od przerwania kod da się bardzo łatwo przepisać na automat skończony.

    0
  • #7 04 Maj 2010 17:34
    asembler
    Poziom 32  

    BoskiDialer napisał:
    Nie neguję rozwiązania przerwanie-w-przerwaniu, w niektórych sytuacjach jest to rozwiązanie bardzo eleganckie i efektywne.

    Oj to miód na moje uszy. Program powinien sie skaldac z samych przerwaN o ile to jest uzasadnione a program głowny tylko to spina w całość (a tak w ogóle jest nipotrzebny-chyba nie?)

    0
  • #8 06 Maj 2010 19:23
    ADI-mistrzu
    Poziom 30  

    Zmodyfikowałem przerwanie do takiej postaci:

    Code:
    ISR(INT1_vect) //przerwanie od złącza PD3 (INT1)
    
    {
       if( ((PIND&0x08)==0)&(znacznik==0) ) //jeśli został zwarty pin...
       {
          TCCR1B |= _BV(CS10); //start timera (przerwanie przez zrównanie)
          znacznik=1; //oznacz że rozpoczeło się liczenie
       }
       if( (PIND&0x08)&znacznik ) //jesli zanikną sygnał ale liczenie trwa...
       {
          m_sekundy_wtrysk = m_sekundy_obr; //wpisz licznik do pierwszej zmiennej
       }
       if( ((PIND&0x08)==0)&znacznik ) //jeśli pojawił się sygnał ale liczenie już się odbywa...
       {
          TCCR1B &= ~_BV(CS10); // zatrzymanie timera (przerwanie przez zrównanie)
          write_command(0xC3);
          SET_RS;
          if(m_sekundy_obr >= 0xFFDC){
             write_text("Nadmiar!");
          }else{
             write_to_lcd(znaki[m_sekundy_obr/10000]);
             write_to_lcd(znaki[(m_sekundy_obr/1000)%10]);
             write_to_lcd(znaki[(m_sekundy_obr/100)%10]);
             write_to_lcd(znaki[(m_sekundy_obr/10)%10]);
             write_to_lcd(0x2C);//znak przecinka
             write_to_lcd(znaki[m_sekundy_obr%10]);
          }
          write_command(0x89); //9 kolumna, 0 wiersz
          m_sekundy_obr = znacznik = 0;  //zerowanie zmiennej licznikowej i znacznika
          TCNT1 = 0; // zerowanie licznika
          EIFR = (1<<INTF1);
       }
    }

    ISR(TIMER1_COMPA_vect){ //przerwanie od zrównania licznika
       TCNT1 = 0; // zerowanie licznika
       m_sekundy_obr += 0x0001; //zwiększ zmienną o 1
       if(m_sekundy_obr==0xFFFF){
          write_command(0xC9);
          write_text("!");
       }
    }

    No i oczywiście MCUCR |= _BV(ISC10); które według dokumentacji powoduje, ze wszelka zmiana stanu na INT1 powoduje przerwanie.
    Tylko usunąłem sei() w przerwaniu bo ono raczej będzie występować po za nim więc chyba niema sensu aby tam było.
    Wyrzuciłem także zmienną licznik_z i od razu liczę w drugiej zmiennej, zaoszczędzę trochę na pamięci, bo niema sensu wykorzystywać dodatkowej zmiennej.

    Coś takiego jest już dobre ? I możecie napisać dlaczego nie powinno się stosować pętli oczekujących w przerwaniach?

    Pozdrawiam

    0
  • #9 06 Maj 2010 21:52
    Freddie Chopin
    Specjalista - Mikrokontrolery

    Pewnie z tego samego powodu dla którego nie ma sensu jeżdżenie Bugatti Veyronem do sklepu po bułki - nie-o-to-chodzi...

    4\/3!!

    0
  • #10 06 Maj 2010 22:14
    _Robak_
    Poziom 33  

    Za dużo w tych przerwaniach się dzieje, jeszcze zapis do LCD, no porażka:) Te trzy ify w pierwszym przerwaniu zostaw, tylko ustawiaj w nich flagi, które to potem rozpatrujesz w programie głównym. Jeśli uważasz że się tak nie da, to myśl bardziej intensywnie, musisz tak zrobić żeby się dało:)

    0