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ą:
- braki w wiedzy na temat arytmetyki logicznej i składni języka C
- nie uwzględnianie początkowych wartości rejestrów i portów
- 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
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)
|= 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:
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.
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 :-)
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ść. :-)
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:
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:
zumek
70 postów i nikt nie zwrócił na to uwagi ?
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łeBłą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 CS02W 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.
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łeSam 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:
Witam!
OdpowiedzUsuń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
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.
OdpowiedzUsuń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)
Witam
OdpowiedzUsuń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
}
Witaj,
OdpowiedzUsuńOpis dotyczył pinu więc oczywiście masz rację powinno być PIN, a nie PORT. Poprawione, dziękuję za zwrócenie uwagi!
A czy można jednocześnie ustawić i wyzerować bity w rejestrze w ten sposób:
OdpowiedzUsuń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.
Zacznę od końca:
UsuńŹ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;
W programie powyżej jest komentarz, który należy usunąć, bo jest z poprzedniego przykładu: "//ustaw w DDRD bit PD0"
Usuń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ńA teraz:
Usuń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
"2. BŁĄD: Nie uwzględnianie WARTOŚCIach początkowych rejestrów i portów"
OdpowiedzUsuńTa wiadomość ulegnie samozniszczeniu za 5, 4, 3,... ;)
Dzięki za zwrócenie uwagi - poprawiłem,
UsuńWiadomość nie ulegnie samozniszczeniu - bardzo cenimy sobie wszelkie uwagi :D
"1.2 _BV(x) czy (1<<x) ?"
OdpowiedzUsuń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....
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ń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ń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ńFaktycznie, źle to przemyślałem, rozpisanie na kartce pomogło również :)
Usuń