czwartek, 14 kwietnia 2011

Problemy C - Ustawianie i zerowanie bitów


Autor: Dondu

Ustawianie i zerowanie bitów w rejestrach czy portach mikrokontrolerów, często powoduje frustrację wśród początkujących programistów C i nie tylko. Problemem z reguły są:
  1. braki w wiedzy na temat arytmetyki logicznej i składni języka C
  2. nie uwzględnianie początkowych wartości rejestrów i portów
  3. zapominanie o tym, że ustawiło się lub wyzerowało jakiś bit rejestru

Skorzystaj z: Kurs języka C z przykładami i kompilatorem (online)



1. Braki w wiedzy na temat arytmetyki logicznej 
i składni języka C


1.1 Operacje na bitach

Nie opisuję tutaj wszelkich zasad i przypadków języka C, a jedynie te, w których początkujący popełniają najwięcej błędów. Jednak na początku przypomnę, że:
  • mnożenie (AND) reprezentuje znak: &
  • dodawanie (OR) reprezentuje znak: |
  • dodawanie modulo 2 (XOR) znak: ^
  • negacja (NOT) znak: ~

Dodatkowo można używać działań na bitach w połączeniu z operatorem przypisywania: =
czyli odpowiednio &=, |=, ^=, ~= .

Szczegóły znajdziesz tutaj: Kurs C - Operatory bitowe

Także tylko dla przypomnienia zasady wykonywania działań, które określił George Boole - Algebra Boole'a i wyglądają tak:


Więcej na ten temat opisane w przystępny sposób, możesz przeczytać tutaj: Algebra Boole'a


1.2 _BV(x) czy (1<<x) ?

Aktualnie preferowane jest operowanie bitami nie za pomocą makra _BV(), ale z wykorzystaniem rozkazu przesuwania bitów: <<
Dlaczego? Ponieważ:

  • kod jest czytelny dla pomagających Ci osób
  • ułatwia przenoszenie kodu bez konieczności przenoszenia definicja makr

// nie pisz tak (choć to poprawne)
 PORTA |= _BV(PA3);
 ADCSRA |= _BV(ADEN) | _BV(ADSC);


 // pisz tak
 PORTA |= (1<<PA3);
 ADCSRA |= (1<<ADEN) | (1<<ADSC);




1.3 BŁĄD: Używanie ! zamiast ~

Początkujący często mylą znaczenie ! oraz ~. Stosowanie ! (negacji używanej w warunkach) do negowania bitów jest nieprawidłowe i nie powoduje negowania bitów. Do tego służy znak: ~

// źle
 PORTA = !(1<<PA3);
 PORTA = !0x04;

 // dobrze
 PORTA = ~(1<<PA3);
 PORTA = ~0x04;




1.4 BŁĄD: Używanie podwójnych && lub || oraz == zamiast pojedynczych i na odwrót

Częste pomyłki zdarzają się także z powodu stosowania podwójnych && i || zamiast pojedynczych i na odwrót. Pamiętaj, że podwójne służą do operacji logicznych, a pojedyncze bitowych.

// Sprawdź czy na pinie PA0 jest wysoki stan (jedynka logiczna)

 // źle
 if(PINA && (1<<PA0)){
    //tak jest jedynka
 }

 // dobrze
 if(PINA & (1<<PA0)){
    //tak jest jedynka
 }

Przy sprawdzaniu równości najczęstszym błędem jest stosowanie pojedynczego znaku równości:
// Sprawdź czy zmienna jest równa 5

 // źle
 if(zmienna = 5){
    //tak
 }

 // dobrze
 if(zmienna == 5){
    //tak
 }


1.5 BŁĄD: Jednoczesne ustawianie i zerowanie bitów za pomocą
|= oraz (0<<x)

Pomyłki także dotyczą przypadków gdy w jednej linijce kodu nowicjusz chce jednocześnie wyzerować i ustawiać bity używając operatora |=

// źle !!!
 PORTA |= (1<<PA3) | (0<<PA2);   //ustaw bit PA3 i jednocześnie zeruj PA2 (źle!)

 // dobrze
 PORTA |= (1<<PA3);   //ustaw bit PA3
 PORTA &= ~(1<<PA2);   //zeruj bit PA2



1.6 BŁĄD: Brak wiedzy o pułapkach AVR-ów

Problem dotyczy przypadku, gdy chcemy wyzerować bit, który jest bitem nietypowym, zerowanym poprzez wpisanie jedynki logicznej, a nie zera (!). Szczegóły opisałem tutaj: AVR: Czyhające pułapki







2. BŁĄD: Nie uwzględnianie wartościach początkowych rejestrów i portów

W większości przypadków mikrokontroler, któremu włączono zasilanie lub wykonano sprzętowy reset, przyjmuje w rejestrach wartości 0 na większości bitów rejestrów i portów:


Jednakże są wyjątki!!!


Dlatego bardzo istotnym jest sprawdzanie datasheet pod tym kątem i odpowiednie pisanie kodu, by nie powstawały sytuacje, w których spodziewałeś się, że nie musisz kasować danego bitu, a okazało się, że on jest ustawiony domyślnie.

ppawel12
Problem już rozwiązałem. Mój błąd to rutyna :-)
... przypomniałem sobie, że w nocie katalogowej jest umieszczone czasem R/W(1/1) lub R/W(0/0). Po zmianie rejestru ... TMR0 zaczął zliczać impulsy :-)

Podobny przypadek:

figa_miga
No tak, IRCF 2:0 są już ustawione po resecie...ale pomroczność. :-)




3. BŁĄD: Zapominanie o tym, że ustawiło się lub wyzerowało jakiś bit rejestru

To podobny problem jak opisany w pkt. 2. W czasie działania programu czasami trzeba przestawiać bity w wybranym rejestrze. Początkujący zapominają, że wcześniej w tym rejestrze ustawiali jakieś bity, ustawiają lub zerują tylko niektóre, co w konsekwencji prowadzi do nieprawidłowego działania programu.

Rys. Atmega8 - konfigurowanie preskalera Timer0 - rejestr TCCR0

Pokażę to na przykładzie ustawiania preskalera Timer0 (Atmega8)

Przykład: Błędny kod

TCCR0 |= (1<<CS00);   // ustaw brak preskalera
 
 // program coś realizujący
 // ....

 TCCR0 |= (1<<CS01);   // ustaw preskaler na 8
 
 // dalsza część prograu
 // ....
Błąd polega na tym, że po włączeniu zasilania czy resecie linia 01 prawidłowo ustawi brak preskalera, ale linia 06 zadziała źle ponieważ do wcześniej ustawionego bitu CS00 doda bit CS01, co w konsekwencji ustawi preskaler na 64 zamiast na spodziewane 8 (patrz tabelka powyżej).


Przykład: Poprawny kod

// ustaw brak preskalera
 TCCR0 |= (1<<CS00);   // ustaw bit CS00
 TCCR0 &= ~((1<<CS02) | (1<<CS01));   // zeruj bity CS02 i CS01
 
 // program coś realizujący
 //....

 // ustaw  preskaler na 8
 TCCR0 |= (1<<CS01);   // ustaw bit CS01
 TCCR0 &= ~((1<<CS02) | (1<<CS00));   // zeruj bity CS02 i CS00
 
 // dalsza część prograu
 // ....

Teraz kod zadziała poprawnie ponieważ zawsze ustawiane są odpowiednio wszystkie 3 bity preskalera.

Rada TYLKO dla początkujących:
  • zawsze konfiguruj wszystkie niezbędne rejestry i bity do ustawienia wybranego przez Ciebie timera, licznika i innych wewnętrznych peryferii





4. BŁĄD: Wielokrotne ustawianie całego rejestru

Aby kod był bardziej czytelny, programiści często dzielą ustawianie bitów w jednym rejestrze, na kilka linii kodu. Jest to akceptowalny sposób, ale jest w nim bardzo łatwo popełnić tzw. "czeski błąd", który trudno wykryć, jak na przykład w tym temacie:

zumek
70 postów i nikt nie zwrócił na to uwagi ?

Czego nie zauważyliśmy doradzając autorowi tematu?
Autor chciał ustawić w rejestrze TCCR0, bity WGM01 oraz CS02, a pozostałe wyzerować. Niestety zrobił tak:
TCCR0 = (1<<WGM01);      //ustawia bit WGM01 i zeruje pozostałe
TCCR0 = (1<<CS02);       //ustawia bit CS02 i zeruje pozostałe
Błąd polega na tym, że w dwóch kolejnych liniach kodu, następują sprzeczne ustawienia bitów tego samego rejestru. Najpierw ustawiany jest bit WGM01, a pozostałe bity są zerowane. W drugiej linii wszystkie bity są zerowane (w tym także WGM01), a ustawiany tylko bit CS02.

W rezultacie tylko bit CS02 został ustawiony poprawnie.

Co zrobić by było prawidłowo?
Należy użyć operatora |= w drugim i każdej następnej operacji na tym samym rejestrze, czyli tak:
TCCR0 = (1<<WGM01);      //ustawia bit WGM01 i zeruje pozostałe
TCCR0 |= (1<<CS02);       //dodaje bit CS02
W rezultacie w rejestrze TCCR0 ustawione są tylko bity WGM01 i CS02, a tak właśnie chciał autor tematu.

nsmarcin
Hehe już wiem Teraz jest już ok, wielkie dzięki na zwrócenie uwagi na tak banalny błąd, a patrzyłem na to tysiąc razy.

Ja także patrzyłem i nie widziałem - wstyd! :-)


Można także od razu ustawiać wybrane bity.
W ten sposób unikniesz "czeskich błędów" nieznacznie pogarszając czytelność kodu (kwestia przyzwyczajenia).
TCCR0 = (1<<WGM01) | (1<<CS02);    //ustawia bity WGM01 i CS02 i zeruje pozostałe
Sam się parę razy złapałem na tym błędzie, dlatego przeważnie stosuję definiowanie rejestru w jednej linii.
Ale są wyjątki opisane w punkcie 1.5 na tej stronie!




Przeczytaj także:

16 komentarzy:

  1. Witam!
    Czy ustawianie bitów na portach w postaci
    PORTB = (1<<PD2) lub
    PORTD = (7<<5);
    jest czytelniejsze od wpisywania wartości szesnatkowo?
    Łatwiej jest mi ustawiać porty hexadecymalnie, ale w celu
    sprawdzenia poprawności kodu na forum łatwiej innym będzie sprawdzać kod z bitami zapisanymi hex czy 1-szym sposobem?

    Pozdrawiam
    Modecom601

    OdpowiedzUsuń
  2. Używanie zdefiniowanych nazw, daje KOLOSALNĄ zaletę, że gdy po paru dniach, tygodniach, czy miesiącach siądziesz ponownie do tak napisanego programu, od razu będziesz wiedział o co chodzi, bez sięgania do dokumentacji mikrokontrolera, by sprawdzić, co oznacza dla rejestru zawartość na przykład 0xf9.

    Poza tym, przenosząc program na inny mikrokontroler tej samej rodziny, będziesz miał KOLOSALNIE mniej pracy z jego adaptacją.

    Tak, pomagający na forum są przyzwyczajeni do zdefiniowanych nazw i nie muszą sięgać do datasheet by sprawdzić Twój program.

    Zawsze możesz jeszcze zdefioniować własne np.:
    #define LED_zielony PD2
    albo wręcz
    #define LED_on PORTD |= (1<<PD2)
    #define LED_off PORTD &= ~(1<<PD2)

    OdpowiedzUsuń
  3. Witam

    w punkcie 1.4 jest napisane:
    // dobrze
    if(PORTA & (1<<PA0)){
    //tak jest jedynka
    }

    nie powinno być przypadkiem:
    // dobrze
    if(PINA & (1<<PA0)){
    //tak jest jedynka
    }

    OdpowiedzUsuń
  4. Witaj,
    Opis dotyczył pinu więc oczywiście masz rację powinno być PIN, a nie PORT. Poprawione, dziękuję za zwrócenie uwagi!

    OdpowiedzUsuń
  5. A czy można jednocześnie ustawić i wyzerować bity w rejestrze w ten sposób:
    DDRB = DDRB | (1<<PB3) & ~(1<<PB4);
    Na mój wysoce matematyczny umysł (13-latka, których tak deprymowałeś, z powodu nieogarniania logiki), powinno to działać (kod się kompiluje).

    I (standardowo) uwaga do bloga: nie da się wstawić HTML w komentarzu. Nigdy. Żadnego. W ogóle.

    OdpowiedzUsuń
    Odpowiedzi
    1. Zacznę od końca:

      Źle odebrałeś moje słowa w naszej dyskusji tutaj. Naprawdę podziwiam Ciebie i przypominam sobie czasy, gdy w Twoim wieku mój tato przywiózł mi komplet elektronicznego hobbysty z ZSRR, w którym były tranzystory germanowe i trochę krzemowych. Do dzisiaj mam jeszcze ich resztki :-)

      Co do HTML, to nie mam na to wpływu, Google ogranicza tylko do tagów: a, i, b.

      Oto program z Twoim fragmentem, ale dla PORTD, który możesz przetestować i modyfikować w CManiaku:

      unsigned char DDRD; //symulujemy rejestr DDRD

      /* PORTD */
      #define PD7 7
      #define PD6 6
      #define PD5 5
      #define PD4 4
      #define PD3 3
      #define PD2 2
      #define PD1 1
      #define PD0 0

      //funkcja konwersji liczby na ciąg znaków reprezentacji binarnej
      const char *byte_to_binary(int x){
      //wykorzystuje nagłówek string.h
      int z; static char b[9]; b[0] = '\0'; for (z = 128; z > 0; z >>= 1){strcat(b, ((x & z) == z) ? "1" : "0");}; return b;
      }

      int main(void) {

      //ustaw w DDRD bit PD0
      DDRD = DDRD | (1<<PD3) & ~(1<<PD4);;
      printf("%s\n", byte_to_binary(DDRD)); //pokaż DDRD

      return 0;

      Usuń
    2. W programie powyżej jest komentarz, który należy usunąć, bo jest z poprzedniego przykładu: "//ustaw w DDRD bit PD0"

      Usuń
    3. O dzisiaj działa. Dziwne, wczoraj nie działało. A kod (jak widzę) nie wykonuje swojej powinności. Zrobienie tego w osobnych liniach zabiera 2B więcej w porównaniu do jednej linii. To dużo, jeżeli ma się 1kB flash'a.

      Usuń
    4. A teraz:

      unsigned char DDRD; //symulujemy rejestr DDRD

      /* PORTD */
      #define PD7 7
      #define PD6 6
      #define PD5 5
      #define PD4 4
      #define PD3 3
      #define PD2 2
      #define PD1 1
      #define PD0 0

      //funkcja konwersji liczby na ciąg znaków reprezentacji binarnej
      const char *byte_to_binary(int x){
      //wykorzystuje nagłówek string.h
      int z; static char b[9]; b[0] = '\0'; for (z = 128; z > 0; z >>= 1){strcat(b, ((x & z) == z) ? "1" : "0");}; return b;
      }

      int main(void) {

      DDRD = 0x0F;
      DDRD = DDRD | (1<<PD6) & ~(1<<PD2);;
      printf("%s\n", byte_to_binary(DDRD)); //pokaż DDRD

      DDRD = 0x0F;
      DDRD = (DDRD & ~(1<<PD2)) | (1<<PD6);
      printf("%s\n", byte_to_binary(DDRD)); //pokaż DDRD


      DDRD = 0x0F;
      DDRD = DDRD & ~(1<<PD2) | (1<<PD6);
      printf("%s\n", byte_to_binary(DDRD)); //pokaż DDRD

      return 0;
      }

      Powinieneś poćwiczyć priorytety operatorów: http://pl.wikibooks.org/wiki/C/Operatory

      Usuń
  6. "2. BŁĄD: Nie uwzględnianie WARTOŚCIach początkowych rejestrów i portów"
    Ta wiadomość ulegnie samozniszczeniu za 5, 4, 3,... ;)

    OdpowiedzUsuń
    Odpowiedzi
    1. Dzięki za zwrócenie uwagi - poprawiłem,
      Wiadomość nie ulegnie samozniszczeniu - bardzo cenimy sobie wszelkie uwagi :D

      Usuń
  7. "1.2 _BV(x) czy (1<<x) ?"
    Pan Tomasz Francuz w książce"Język C do mikrokontrolerów..." Odpowiada na to pytanie odwrotnie niż jest w artykule, i kogo teraz słuchać?? Oto jest pytanie....

    OdpowiedzUsuń
    Odpowiedzi
    1. Na wiele pytań nie ma jednoznacznej odpowiedzi i każdy preferuje coś innego. Dondu woli << co ma zalety o których pisze, niemniej makro _BV jest bardziej odporne na błedy, szczególnie błąd polegający na zastąpieniu << pojedynczym < co tworzy poprawną semantycznie konstrukcję, lecz dającą wynik daleki od oczekiwanego. Także dostałeś argumenty za i przeciw, musisz sam zdecydować co bardziej ci odpowiada.

      Usuń
  8. Witam, mam takie pytanie. Czy można zanegować tylko jeden bit, bez ruszania pozostałych? Myślałem nad PORTC ^= (1<<PC0);, ale to raczej zaneguje też inne bity, których wartość jest równa 0.

    OdpowiedzUsuń
    Odpowiedzi
    1. Nie, to zaneguje tylko bit PC0, gdyż negację wywołuje tylko argument XOR 1, operacja argument XOR 0=argument. Przy okazji warto zauważyć, że tego typu operacje w mikrokontrolerach XMEGA i ARM wywołuje się ustawiając odpowiedni bit rejestrów toggle (dla XMEGA PORTC.OUTRGL=1<<Pin0_bm. Warto pamiętać, że operacja wykonana przy pomocy operatorów logicznych języka C zazwyczaj nie jest wykonywana atomowo.

      Usuń
    2. Faktycznie, źle to przemyślałem, rozpisanie na kartce pomogło również :)

      Usuń