piątek, 1 kwietnia 2011

Pułapki AVR: Zerowanie bitu przez wpisanie jedynki


Autor: Dondu

Artykuł jest fragmentem cyklu: Pułapki mikrokontrolerów AVR

Co to jest zerowanie bitu? To wpisanie zera na wybranym bicie jakiegoś rejestru. Zapewne wiesz już, że należy użyć takiego kodu:

nazwa_rejestru &= ~(1<<nazwa_bitu);

//na przykład zerowanie bitu TWEN w rejestrze TWCR
TWCR &= ~(1<<TWEN);
Więcej na temat operacji na bitach: Ustawianie i zerowanie bitów

Niestety w niektórych rejestrach mikrokontrolerów AVR są wyjątki od tej reguły, a wyzerowanie bitu za pomocą programu, następuje poprzez wpisanie jedynki, a nie zera


y0yster
... czyli musi być wyczyszczona przez zapisanie tam 1. Nie czaję tego.
Normalnie już sobie włosy wyrywam.


Na potrzeby tego artykułu, nazwę te bity, "bitami szalonymi", podobnie jak manewr Szalonego Iwana.

Przykład:

Rys. Rejestr TWCR mikrokontrolera ATmega8

Czyli, aby wyzerować bit TWINT musimy zapisać do niego jedynkę:

TWCR |= (1<<TWINT);  //zerowanie przez wpisanie jedynki

Jak widzisz, zarówno TWEN jak i TWINT znajdują się w tym samym rejestrze, ale ich zerowanie jest realizowane różnie:
  • TWEN - przez wpisanie zera,
  • TWINT - przez wpisanie jedynki (bit "szalony")

Ale to nie jedyny problem!

Prześledźmy przykład, w którym bit TWINT jest ustawiony (czyli występuje żądanie obsługi przerwania), ale przerwanie jeszcze nie zostało obsłużone, a właśnie dokonujemy zmiany bitów w rejestrze TWCR:

Stan początkowy rejestru TWCR był na przykład taki (binarnie): 10000100 co oznacza, że ustawione były bity  TWINT oraz TWEN.

Załóżmy, że teraz programista chce ustawić tylko i wyłącznie bit TWEA. Robi to w najprostszy sposób

TWCR |= (1<<TWEA); //chcemy ustawić tylko i wyłącznie bit TWEA

Poniżej kod wygenerowany przez kompilator:

IN  R24,  0x36 //wczytaj do rej R24 zawartość rejestru TWCR (adres 0x36)
ORI R24,  0x40 //suma logiczna R24 oraz liczby reprezentującej (1<<TWEA)
OUT 0x36, R24  //zapisz nową wartość do rejestru TWCR

Czy widzisz coś groźnego w tym kodzie, co dotyczy bitu TWINT?

No to zobaczmy, co będzie w rejestrze R24 po wykonaniu dwóch pierwszych rozkazów assemblera pokazanych powyżej:


Trzeci rozkaz assemblera z kodu powyżej załaduje więc do rejestru TWCR nową wartość (binarnie) 11000100.

W przypadku bitu TWEA jedynka zostanie poprawnie ustawiona.

W przypadku bitu TWINT zgodnie z zasadą zerowania przez wipsanie do niego jedynki (bit "szalony"), bit ten zostanie wyzerowany (!) pomimo, że w kodzie C w ogóle go nie wskazywaliśmy.

Podobnie będzie, gdy będziemy chcieli wyzerować jakiś bit za pomocą &= na przykład tak:
TWCR &= ~(1<<TWEN); //chcemy wyzerować wyłącznie bit TWEN

Niestety także wtedy nastąpi podobny ciąg instrukcji assemblera, które także wyślą do rejestru TWCR bajt zawierający jedynkę na bicie TWINT, co spowoduje jego wyzerowanie.

Jak sobie poradzić?
Niestety operując na rejestrach zawierających bity kasowane przez wpisanie jedynki, należy samemu zadbać o to, by wpisując nową wartość nie mieć jedynki na takim bicie. Są dwie metody, które pokażę ponownie na przykładzie bitu TWINT:
  1. zapis konkretnej wartości wszystkich bitów,
  2. wykorzystanie rejestru pośredniczącego.

Pierwszy sposób, zmusza Ciebie do perfekcyjnego pilnowania co w rejestrze ma być w danej chwili, co nie jest takie proste przy bardziej skomplikowanych programach. Wykorzystujesz wtedy po prostu wpisanie nowej wartości całego rejestru, z wyzerowanym bitem TWINT:
TWCR = (1<<TWEA) | (1<<TWEN); 

Drugi sposób to wykorzystanie rejestru pośredniego:
//możemy zrobić tak:
register unsigned char rej_posr; //definiuj rejestr pomocniczy
rej_posr = TWCR & ~(1<<TWINT);   //odczytujemy TWCR do zmiennej pomocniczej 
                                 //i zerujemy w niej bit TWINT
rej_posr |= (1<<TWEA);           //dodajemy bity które chcemy ustawić 
                                 //lub zerujemy za pomocą &=
TWCR = rej_posr;                 //zapisujemy całość do rejestru TWCR


//lub w uproszczeniu tak:
TWCR =  (TWCR  | (1<<TWEA)) & ~(1<<TWINT);

Wygenerowany przez kompilator kod będzie wyglądał tak:
IN  R24,0x36  //wczytaj do rej R24 zawartość rejestru TWCR (adres 0x36)
ANDI R24,0x3F //wyzeruj bit TWINT
ORI  R24,0x40 //suma logiczna R24 oraz liczby reprezentującej (1<<TWEA)
OUT  0x36,R24 //zapisz nową wartość do rejestru TWCR

Zobaczmy więc jak zachowa się taki kod:


Zauważ, że w wyniku w rejestrze R24 dostajemy prawidłowo ustawione bity TWEA i TWEN oraz wyzerowany bit TWINT. Zapisanie takiej wartości z rejestru R24 do rejestru TWCR (zgodnie z zasadą omawianą w tym punkcie) nie wyzeruje "szalonego" bitu TWINT, który jest w tym rejestrze ustawiony, a którego nie chcieliśmy zmieniać.

Czyli taki kod pozwala nam na spokojne operowanie na bitach rejestrów zawierających "szalone" bity, a różni się defacto tylko jednym rozkazem assemblera: ANDI.


Czy to dotyczy tylko bitu TWINT? 
Nie! Bitów "szalonych" jest więcej, dlatego trzeba uważnie sprawdzać datasheet.
Na przykład dla ATmega8:
  • ACI
  • ICF1
  • INTF0, INTF1
  • OCF1A, OCF1B
  • OCF2
  • TOV0, TOV1, TOV2
  • TWINT
W innych mikrokontrolerach mogą być także inne bity - zawsze sprawdzaj w datasheet!

Ta pułapka jest jednym z najczęściej występujących problemów stających na drodze początkujących.



Zobacz pozostałe pułapki AVR


9 komentarzy:

  1. Hmm, niedawno rozpoczełem przygodę z i2c więc oczywiście rzecz będzie o TWINT. W datasheecie stoi:

    "This bit is SET by hardware when the TWI has finished its current job and expects application
    software response. If the I-bit in SREG and TWIE in TWCR are set, the MCU will
    jump to the TWI Interrupt Vector. While the TWINT Flag is set, the SCL low period is
    stretched.
    The TWINT Flag must be cleared by software by writing a logic one to it."

    Bit ten (TWINT) jest USTAWIANY/modyfikowany PRZEZ interfejs wiedy TWi skończy robotę. Oznacza to że MCu skoczy do obsługi przerwania. KIEdy TWINT jest ustawiony przez hardware, scl nie tyka (?). Flaga TWINT musi być wyzerowana poprzez wpisanie za TWINT zera.

    Czyli to nie jest poprostu tak, że interfejs ustawia TWINT na ZERO jeśli skończy robotę? TWINT=0 (skończył poprzednią robotę -> ustawiamy dane do wysłania -> ustawiamy JEDEN na TWINT -> "oho jest jeden, jest robota, myśli TWI, skończe robotę i ustawie na zero, czyli nie ma nic do roboty". Nie tak to czasem idzie?

    A w przykładzie Dondu (świetny blog! ) że tam programista ustawia TWEA psując TWINT, przecież początkowy stan, przed ustawieniem TWEA, a po ustawieniu... różni się tylko TWEA'em.


    POzdrawiam :)

    PS. Widzę pewną analogię do Mirka Krenca... a on w końcu przyznał rację... ;)

    OdpowiedzUsuń
  2. Hej,

    Przeniosłem tutaj Twój post ze spisu treści tego cyklu artykułów, ponieważ chyba o ten artykuł Ci chodziło. Czy tak?

    Czytałem dwa razy, nie bardzo rozumie co właściwie pytasz. Może dlatego, że już dość późno :D

    W tym artykule omawiamy jak to ująłem "szalone bity" w zakresie ich zerowania przez wpisanie jedynki. Nie omawiam tutaj innych aspektów.

    BTW:
    O co chodzi z tą osobą?


    OdpowiedzUsuń
  3. Napisałem ten komentarz, gdyż zauważyłęm że inni komentowali ów TWINT właśnie w spisie treście, wiec przepraszam.

    PO pierwsze w mojej wypowiedzi popełniłem jeden błąd:

    "Flaga TWINT musi być wyzerowana poprzez wpisanie za TWINT zera." (ostatnie zdanie mojego tłumaczenia.)

    Miałem napisać:

    "Flaga TWINT musi być wyzerowana poprzez wpisanie za TWINT jedynki."

    Ale wracając do meritum sprawy - chodziło mi o to że, czy to na pewno jest "bit szalony" który może zostać zmodyfikowany za pomocą: "TWCR |= (1<<TWINT);" ?

    Przecież zgodnie z tym co pisze w datasheet, kiedy TWI kończy pracę, wystawia flagę TWINT ustawiając ZERO na tym bicie. A my jak będziemy gotowi, (ustawimy nowy bajt do wysłania itp.) wyzerujemy flagę, czyli ustawimy JEDEN na bicie TWINT rejestru TWCR.

    I teraz mamy sytuację która jest przedstawiona w artykule - chcemy zmienić tylko bit TWEA. Początkowo rejestr TWCR miał wartośc 10000100. na pierwszym miejscu jest 1 - czyli flaga TWINT jest opuszczona czyli TWI coś robi. Potem instrukcją

    TWCR |= (1<<TWINT);

    zmieniamy wartość rejestru TWCR czyli 11000100. Czyli nie zmieniliśmy wartości TWINT. TWI dalej sobei pracuje, a jak skończy ustawi TWCR na: 01000100.

    Moje pytanie brzmi: (czy/gdzie) w moim rozumowaniu jest błąd?

    PS: w spisie treści jest Twój (Dondu) komentarz daty "23 stycznia 2013 22:36".
    Tam piszesz że "Gdy jest ustawiony (ma wartość 1) oznacza, że wystąpiło przerwanie z TWI. Gdy jest zerem oznacza, że przerwanie nie wystąpiło." A nie jest czasem właśnie na odwrót? Wcześniej nie zauważyłem tego komentarza - ao to mi zasadniczo chodzi - czy czasem nie jest na odwrót - jeśli zerowanie flagi TWINT oznacza wpisanie JEDEN do TWINT, to że flaga (czyli wystąpienie przerwania) jest podniesiona gdy TWINT = 0?

    00:41 Campi się po nocach, co? :D

    OdpowiedzUsuń
  4. Nie ma za co przepraszać. Wszedłem od razu na Twój post i czytając go nie patrzyłem, co jest powyżej. Tamten temat kiedyś zawierał także problem opisany w tym artykule, a później stał się spisem treści, bo artykuł był za długi (podział na poszczególne problemy). Stąd znajdują się w nim komentarze dot. tego artykułu. :-)

    ---------------------------

    Odpowiadając na Twoje pytanie:

    TWINT jest ustawiany na 1 po zakończeniu pracy:

    "This bit is set by hardware when the TWI has finished its current job and expects application software response."

    i oczekuje reakcji programu. Następnie program (np. poprzez przerwanie lub tzw. pulling) robi co ma robić i musi także zgasić TWINT:

    "The TWINT Flag must be cleared by software by writing a logic one to it. Note that this flag is not automatically cleared by hardware when executing the interrupt routine."

    Użyłem słowa "zgasić" przez co rozumiemy zgodnie z datasheet wyzerowanie bitu TWINT, co zgodnie z powyższym cytatem wymaga wpisania jedynki, czyli tak jak opisuje ten artykuł.

    Czy teraz wszystko jasne?

    OdpowiedzUsuń
  5. Zgadzam się z Anonimowym. TWINT już było ustawione na 1 przed zamianą, więc się nie zmieniło (trochę jak obracanie czegokolwiek o 360°).

    OdpowiedzUsuń
  6. Czy jest znane jakieś techniczne uzasadnienie takiego, jak się nasuwa wprost: głupiego, nielogicznego zupełnie rozwiązania, które (nie ma rady) "trzeba znać"? Czy też po prostu jest to zwyczajna wada projektowa, której usunięcie producent uznał za "zbyt kosztowne", no i teraz sobie klepie tak "upiększone" kostki?

    OdpowiedzUsuń
    Odpowiedzi
    1. Owszem, jest techniczne uzasadnienie dla takiego działania - wynika ono ze sprzętowej implementacji tego typu flag z użyciem przerzutników. Przejrzyj tabele prawdy dla tego typu układów i odpowiedź stanie się oczywista.

      Usuń
    2. Skoro tak - to już jest oczywista: "taniej wyszło" zostawić rozwiązanie badziewne, niż je dopracować.

      Usuń
    3. Dlaczego uważasz, że to badziewne rozwiązanie? To taka sama umowa, jak, że np. zerujemy wpisjując zero. Przy czym zerowanie poprzez wpisanie jeden ma kilka zalet, m.in. taką, że możesz coś wpisać do rejestru, tak, aby nie zmieniać bitów stanu. Czasami w rejestrze stanu jest kilka flag i wtedy możliwość zerowania tylko określonych ma sens. Takie rozwiązanie jest stosowane powszechnie w mikrokontrolerach, nie tylko AVR.

      Usuń