Mikrokontrolery - Jak zacząć?

... czyli zbiór praktycznej wiedzy dot. mikrokontrolerów.
Pokazywanie postów oznaczonych etykietą Problemy C. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą Problemy C. Pokaż wszystkie posty

czwartek, 14 kwietnia 2011

Problemy C - VOLATILE


Autor: Dondu


ARTYKUŁ W TRAKCIE MODYFIKACJI
CZYTASZ NA WŁASNE RYZYKO
:-)



Początkujący programiści C zapominają często, że w niektórych przypadkach zmienne globalne muszą być poprzedzone magicznym: VOLATILE

Zaletą kompilatora języka C wykorzystującego różne poziomy optymalizacji kodu jest znaczne zmniejszenie zajmowanej przez program pamięci.

Ale ponieważ nie ma nic za darmo, stąd w niektórych przypadkach program pokazany poniżej nie będzie działał prawidłowo. Dlaczego?

nonor
Nie mam już bladego pojęcia co się dzieje, bo spodziewałbym się, że program albo działa, albo nie działa...

folkien
Ktoś wie w czym jest haczyk?

Przykład:
#include <avr/io.h>      //podstawowa biblioteka AVRów
#include <avr/interrupt.h>


//--- Definicje zmiennych globalnych -----------------------------------

unsigned char flaga;     //przykładowa zmienna



//--- Obsługa przerwania z INT0 -----------------------------------

ISR(INT0_vect){       //wystąpiło przerwanie z INT0

 //... jakiś kod

 flaga = 1;       //ustaw flagę

 //... jakiś kod
}



//--- Program główny ---------------------------------------------

int main(void){

 // tutaj ustaw przerwanie INT0 dla swojego procesora na którym działasz
 // ...


 DDRA = 0x01;     //ustaw pin z podłączoną diodą jako wyjście

 sei();        //włącz przerwania

 while(1){       //pętla główna
  
  if(flaga){      //gdy flaga zostanie ustawiona
 
   PORTA = 0x01;   //zapal diodę LED
 
  }
 }
}

Ponieważ kompilator optymalizując kod nie wie, czy i kiedy przerwanie z INT0 zostanie wykonane, stąd zmienna flaga może nie zostać prawidłowo obsłużona.





Przykład

Przyglądnijmy się kodowi wynikowemu kompilatora, czyli instrukcjom assemblera wygenerowanych przez prosty program dla dwóch przypadków zmiennej globalnej flaga:

  1. bez modyfikatora volatile
  2. z moidyfikatorem volatile

Naszym programem bazowym będzie bardzo prosty program:

#include <avr/io.h>

char flaga = 0;  //definicja zmiennej globalnej

int main(void){  //główna funkcja programu

   PORTB = flaga; //zapisz zmienną do portu B
   PORTC = flaga; //zapisz zmienną do portu C
   PORTD = flaga; //zapisz zmienną do portu D
 
}


Programy zostały skompilowane kompilatorem GCC ver. 4.3.3 dla mikrokontrolera ATmega8.


Przypadek bez modyfikatora volatile

Dla wersji powyższej, czyli bez modyfikatora volatile kompilator przygotował następujący wynikowy kod assemblera:

char flaga = 0;  //definicja zmiennej globalnej

int main(void){  //główna funkcja programu

   PORTB = flaga; //zapisz zmienną do portu B
  48: 80 91 60 00  lds r24, 0x0060
  4c: 88 bb        out 0x18, r24 ; 24
   PORTC = flaga; //zapisz zmienną do portu C
  4e: 85 bb        out 0x15, r24 ; 21
   PORTD = flaga; //zapisz zmienną do portu D
  50: 82 bb        out 0x12, r24 ; 18
 
}

Ponieważ możesz nie znać assemblera stąd możesz posłużyć się ..... AVR INSTRUCTIO SET

ale byś tego nie musiał robić wytłumaczę po kolei instrukcje assemblera.

Zacznijmy od tego, że kompilator dla przechowywania naszej zmiennej globalnej flaga:

char flaga = 0;  //definicja zmiennej globalnej

wyznaczył miejsce w pamięci SRAM pod adresem: 0x0060 (pierwszy bajt pamięci SRAM) ponieważ jest to pierwsza zmienna w naszym programie:




Nie musiał jej przypisywać wartości zero ponieważ domyślnie po resecie wartości komórek w pamięci SRAM są równe zero.


W pierwszej instrukcji assemblera mikrokontroler odczyta za pomocą rozkazu LDS (ang. Load Direct from Data Space) zawartość pamięci spod adresu 0x0060, w której jest przechowywana nasza zmienna flaga:

  48: 80 91 60 00  lds r24, 0x0060


Daną tę zapisał sobie do rejestru R24 (rejestry na których bezpośrednio operuje rdzeń mikrokontrolera, czyli CPU). Następnie mikrokontroler wykonuje trzy kolejne rozkazy OUT

  4c: 88 bb        out 0x18, r24 ; 24
  4e: 85 bb        out 0x15, r24 ; 21
  50: 82 bb        out 0x12, r24 ; 18 


zapisując kolejno do portów:




Wniosek 1
Kompilator dla zmiennej bez modyfikatora volatile dokonał odczytu zmiennej do rejestru R24 tylko raz, i następnie korzystał już z rejestru do wykonania rozkazów zapisu do portów PORTB, PORTC oraz PORTD.



Zastanówmy się co by się stało, gdyby program zawierał funkcje obsługi przerwania, które modyfikowałoby zmienną flaga tak jak okazałem na początku tego artykułu. Mogłoby to wyglądać na przykład tak:


char flaga = 0;  //definicja zmiennej globalnej

int main(void){  //główna funkcja programu

  48: 80 91 60 00  lds r24, 0x0060
  4c: 88 bb        out 0x18, r24 ; 24 
  4e: 85 bb        out 0x15, r24 ; 21
    //tutaj nastąpiło przerwanie i wykonanie funkcji przerwania w którym zmienna 
    //flaga zostałaby zmodyfikowana np. ustawiona na 1
  50: 82 bb        out 0x12, r24 ; 18
 
}


Jak widzisz program nie uwzględni nowej wartości zmiennej flaga, ponieważ jest ona zapisywana w pamięci SRAM pod adresem 0x0060. Powyższy program przed ostatnim rozkazem zapisu do PORTD nie odczytuje ponownie pamięci SRAM, tylko kontynuuje zapis wartości z rejestru R24.

Wniosek 2
Program nie zauważył, że zmienna flaga została zmieniona poprzez przerwanie i nadal posługuje się starą wartością zmiennej flaga!





Dlatego w takich przypadkach można wymusić, aby za każdym użyciem zmiennej flaga była ona odczytywana i/lub zapisywana do pamięci. Służy do tego właśnie volatile dodawana na początku definicji zmiennej globalnej.

Czyli linia nr 07 powinna wyglądać tak:
volatile unsigned char flaga;

nonor
Faktycznie - zupełnie zapomniałem o volatile ...

Krzysiu6699
Problem rozwiązany - brakowało volatile przy zmiennej ...

folkien
Szok! Taka mała rzecz, a ja straciłem nad tym 1h.


Rady TYLKO dla początkujących:
  • stosuj volatile do wszystkich zmiennych globalnych,
  • stosuj volatile zawsze dla zmiennych używanych w przerwaniach i poza nimi.
Takie podejście zmniejszy ilość Twoich problemów w początkowej fazie nauki C. Później gdy już poznasz dokładniej język C, będziesz wiedział kiedy i jakie odstępstwa od tych zasad można lub należy robić.

Należy jednak pamiętać, że w częściach programu, w których zależy nam na szybkości zmienne globalne z użyciem volatile powodują wydłużenie kody wynikowego, przez co wolniejsze jego działanie.

Zobacz: Kurs języka C z kompilatorem CManiak online.

Więcej dowiesz się z tych książek: Książki dla Ciebie
Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

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:

Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

Problemy C - Przerwania


Autor: Dondu

Na początek zapamiętaj najważniejszą zasadę:

PRZERWANIA POWINNY WYKONYWAĆ SIĘ
JAK NAJSZYBCIEJ

I wcale nie mam na myśli szybkiego zegara procesora np. 64MHz, tylko sposób pisania programu.

Do częstych problemów z przerwaniami należą:
  1. brak VOLATILE w deklaracji zmiennych
  2. używanie niewłaściwych lub starych wektorów przerwań
  3. oczekiwanie w przerwaniu
  4. włączanie przerwań w funkcji obsługującej przerwanie
  5. wykonywanie skomplikowanych obliczeń w przerwaniu




1. BŁĄD: Brak VOLATILE w deklaracji zmiennych

To najbardziej popularny problem opisałem tutaj: Problemy C - VOLATILE




2. BŁĄD:  Używanie niewłaściwych lub starych wektorów przerwań

To także istotny choć nie zawsze powodujący błędy problem. Aktualnie standardem jest używanie wektorów przerwań według standardu ISR(), a nie SIGNAL().

Dokładny opis tego problemu z listą wektorów i mikrokontrolerów znajdziesz tutaj: AVR Interrupts




3. BŁĄD: Oczekiwanie w przerwaniu

To bardzo często spotykany błąd polegający na wstawianiu w procedurę obsługi przerwania:

  • funkcji opóźnienia (delay)
  • pętli oczekujących na pojawienie się jakiegoś zdarzenia

Funkcje opóźnienia:
ISR(tutaj wektor przerwania){  //wystąpiło przerwanie

   ....    //jakiś kod
 
   _delay_ms(100);   //opóźnienie

   ...     //jakiś kod
}

Pętla oczekująca:
ISR(tutaj wektor przerwania){  //wystąpiło przerwanie

   ....    //jakiś kod
 
   while (ADCSRA & (1<<ADSC));     //oczekiwanie w pętli na zakończenie pomiaru

   ....    //jakiś kod

}

Wprowadzenie opóźnienia za pomocą DELAY czy dowolnej PĘTLI może doprowadzić do sytuacji, w której przerwania będą tracone z powodu dużych opóźnień, ponieważ nowe przerwania przychodzą zanim poprzednie się zakończą.

Co robią wtedy początkujący? Włączają przerwania w funkcji obsługi przerwania, co tylko powiększa katastrofalne działanie programu, co opisuję poniżej.


Są też tacy, którzy wiedzą, że nie powinni stosować DELAY w przerwaniu, ale mimo tego robią to, i w dodatku o czasie 1 sekundy (!!!), jak na przykład kol:

nsmarcin
Za kod z góry przepraszam
Wiem, że nie powinienem stosować delay w przerwaniach :-)

... a później dziwią się, że program nie działa prawidłowo.




4. BŁĄD: Włączanie przerwań w funkcji obsługującej przerwanie

To jeden z największych błędów jakie może popełnić początkujący programista robiąc tak:

ISR(tutaj wektor przerwania){  //wystąpiło przerwanie

   ....   //jakiś kod
 
   sei();   //włącz przerwania

   ...    //jakiś kod
}

Dlaczego? Ponieważ:
  • odblokowanie przerwań, podczas wykonywania procedury obsługi przerwania, bardzo często prowadzi do nieprzewidywalnego działania programu, przez co ciężko jest ustalić przyczynę błędów.
  • wiele kolejnych nie zakończonych przerwań może doprowadzić do przepełnienia stosu
  • i pewnie jeszcze wiele innych problemów
Dla baaaaardzo doświadczonego programisty, włączenie przerwań w funkcji przerwania (w specyficznych przypadkach) może być dobrym rozwiązaniem, ale Ty tego nie rób.

Czy można na wszelki wypadek dodać cli() i sei(), na przykład tak?:
ISR(tutaj wektor przerwania) 
{ 
   cli(); 
   ...     //jakiś kod
   sei(); 
}
Zobaczmy, co na to datasheet mikrokontrolera


Tłumaczenie:

Datasheet ATmega8 - str. 11
Bit I (flaga zezwolenia na przerwania globalne) jest zerowana sprzętowo w momencie wystąpienia przerwania, a ustawiany przez instrukcję RETI w celu umożliwienia następnych przerwań.

Jak widzisz nie musisz wstawiać rozkazów cli() oraz sei() w funkcji obsługi przerwania.

Jeżeli jednak to zrobisz, to wygenerujesz sobie problem. Zobaczmy co zrobi kompilator GCC dla mikrokontrolerów AVR (asemblerowy kod wynikowy funkcji obsługi przerwania):
//prolog
PUSH      R1             Push register on stack
PUSH      R0             Push register on stack
IN        R0,0x3F        In from I/O location
PUSH      R0             Push register on stack
CLR       R1             Clear Register
PUSH      R24            Push register on stack

CLI                      Global Interrupt Disable //Twój rozkaz cli()
                                                  //nic nie zmienia bo już 
                                                  //przerwania są wyłączone

 ... //jakiś program


SEI                      Global Interrupt Enable  //Twój rozkaz sei() 
                                                  //od tego miejsca przerwania 
                                                  //włączasz - widzisz problem?
//epilog
POP       R24            Pop register from stack
POP       R0             Pop register from stack
OUT       0x3F,R0        Out to I/O location
POP       R0             Pop register from stack
POP       R1             Pop register from stack
RETI                     Interrupt return

Problem który sobie wygenerujesz na własne życzenie polega na tym, że począwszy od linii 17 kodu powyżej przerwania będą włączone, choć powinny być włączone dopiero ostatnim rozkazem RETI. Oznacza to, że jeżeli od linii 17-tej nastąpi przerwanie, zostanie ono obsłużone pomimo, że poprzednie jeszcze nie zostało zakończone. Skutki opisałem na początku tego punktu.




5. BŁĄD: Przerwanie z funkcjami z ukrytym oczekiwaniem

Ten problem jest związany z punktem nr 3 (opisany wyżej), choć bezpośrednio programista tego nie widzi. Chodzi o to, że w przerwaniu nie powinno się wykorzystywać funkcji, które zawierają w sobie pętle oczekujące lub opóźnienia.

Niby oczywiste, ale często początkujący nie są świadomi, że wykorzystywane przez nich funkcje bibliotek kompilatora lub innych (np. do obsługi wyświetlacza LCD, komunikacji, itd.), zawierają w sobie pętle oczekujące lub opóźnienia.

Przykład:

Częstym błędem jest takie stosowanie przerwań (wyświetlanie w przerwaniu):
ISR(tutaj wektor przerwania){  //wystąpiło przerwanie

     ....    //jakiś kod
 
     LCD_GoTo(1,1);
     LCD_WriteText('Test LCD');

     ...     //jakiś kod
}

Wykorzystamy (nota bene bardzo dobrą) bibliotekę obsługi wyświetlaczy LCD, której autorem jest Radosław Kwiecień: źródło

Oto definicje kilku najczęściej wykorzystywanych funkcji:
void LCD_Clear(void){
     LCD_WriteCommand(HD44780_CLEAR);
     _delay_ms(2);
}

void LCD_GoTo(unsigned char x, unsigned char y){
     LCD_WriteCommand(HD44780_DDRAM_SET | (x + (0x40 * y)));
}

void LCD_Home(void){
     LCD_WriteCommand(HD44780_HOME);
     _delay_ms(2);
}
Zauważyłeś na pewno, że dwie z powyższych funkcji zawierają opóźnienia _delay_ms() generujące opóźnienie, aż 2 ms(!).

Ale nie tylko to jest problemem, ponieważ wszystkie wyżej pokazane funkcje używają funkcji LCD_WriteCommand(). Zobaczmy więc, co w niej jest:
void LCD_WriteCommand(unsigned char commandToWrite){
     LCD_RS_PORT &= ~LCD_RS;
     _LCD_Write(commandToWrite);
     while(LCD_ReadStatus()&0x80);
}
I tutaj następna niespodzianka, czyli pętla while() oczekująca na potwierdzenie przyjęcia komendy przez wyświetlacz.

Podobnie ma się sprawa z funkcją LCD_WriteText(), której definicja wygląda tak:
void LCD_WriteText(char * text){
   while(*text) LCD_WriteData(*text++);
}
Tutaj nie dość, że jest pętla wysyłająca po kolei znaki napisu, który chcesz wyświetlić (inaczej się nie da), to jeszcze wykorzystuje dla przesłania każdego znaku funkcję LCD_WriteData(), wyglądającą tak:
void LCD_WriteData(unsigned char dataToWrite){
     LCD_RS_PORT |= LCD_RS;
     _LCD_Write(dataToWrite);
     while(LCD_ReadStatus()&0x80);
}
A to oznacza dla każdego wysyłanego do LCD znaku napisu, oczekiwanie w pętli na zakończenie jego odbioru przez LCD, czyli łącznie kupa czekania :-)


Stosowanie bibliotek w funkcjach przerwania, powinno zostać poprzedzone analizą ich zawartości, by nie narazić się na problem zbyt długiej realizacji przerwania. 




6. BŁĄD: Wykonywanie skomplikowanych obliczeń w przerwaniu

To jeden z częściej pojawiających się problemów. W skutkach jest on podobny do punktu nr 5, czyli zbyt długiego czasu obsługi przerwania. Mikrokontroler nie nadąża wykonywać funkcji przerwania przed nadejściem następnych przerwań, przez co gubi przerwania.

W funkcjach obsługi przerwań staraj się nie wykonywać bardzo skomplikowanych obliczeń.

W szczególności problem ten dotyczy wykonywania w funkcji obsługi przerwania operacji na danych zmiennoprzecinkowych.





Flagi - rozwiązaniem problemu

Rozwiązaniem problemu zbyt długiego czasu wykonywania się funkcji obsługi przerwania może być zastosowanie metody ustawiania w przerwaniu jedynie jakiejś flagi (zmiennej), a wykonywanie właściwego kodu przerwania, dopiero w pętli głównej programu po stwierdzeniu, że flaga została ustawiona przez przerwanie. 


W takim przypadku pamiętaj, że flaga musi być jako volatile, co opisałem w punkcie nr 1 niniejszego artykułu.

Przykład obsługi przerwań z wykorzystaniem flagi:
//flaga musi być zmienną globalną z volatile
volatile char flaga_1;

//funkcja obsługi przerwania
ISR(tutaj wektor przerwania){

  // ... jakiś kod

  //ustaw flagę
  flaga_1 = 1;   

}


//program główny
int main(void){

  // ... jakiś kod

  //pętla główna
  while(1){

     // ... jakiś kod

     //sprawdzamy, czy wystąpiło przerwanie
     if(flaga_1){
       
       //tak, wystąpiło przerwanie

       flaga_1 = 0;  //zgaś flagę
  
       // ... kod, który ma się wykonać w odpowiedzi na przerwanie

     } //koniec if()

     // ... jakiś kod

  } //koniec pętli głównej
}

Takie rozwiązanie ma oczywiście także pewną wadę.
Mianowicie, kod który zostanie wykonany w odpowiedzi na przerwanie nie jest wykonywany dokładnie w momencie rozpoczęcia wykonywania funkcji przerwania. Dzieje się tak dlatego, że przerwanie ustawia jedynie flagę, a pętla główna wykonuje się w bliżej nieokreślonym miejscu. Musi więc dojść do momentu sprawdzania flagi i dopiero wtedy wykona się właściwy kod przerwania.

Dlatego sam musisz zadecydować, czy takie opóźnienie ma dla Ciebie istotne znaczenie. Nie ma to np. znaczenia dla projektu typu zegarek (opóźnienie nie wpłynie na dokładność odmierzanego czasu), ale dla projektu typu bardzo szybki sterownik generujący impulsy z dużą dokładnością, może mieć to znaczenie.

Jeżeli przerwanie ma jedynie ustawiać flagę, to takie rozwiązanie jest nieekonomiczne. W takiej sytuacji należy w pętli głównej sprawdzać flagę przerwania i gasić ją programowo po jej wykryciu (w AVR uważaj na ten wyjątek).

Taki kod wykonywać się będzie szybciej ponieważ nie jest generowany skok do funkcji obsługi przerwania i nie są odkładane na stos niezbędne rejestry.



Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

sobota, 9 kwietnia 2011

Compound literals, czyli jak wygodnie przekazywać parametry złożone

Autor: tmf
Redakcja: Dondu

Kurs języka C: Spis treści

Jakiś czas temu na Elektrodzie pojawiło się pytanie o to, w jaki sposób wygodnie przekazywać do funkcji złożony argument.

O co chodzi?
Spójrzmy na prototyp poniższej funkcji:
void foo(int *ptr);
Nasza funkcja foo przyjmuje jako argument wskaźnik na int. Skoro musimy jej przekazać wskaźnik, to znaczy, że jej wywołanie musi wyglądać tak jak poniżej:
int var=10;
foo(&var);
Wszystko pięknie, ale w powyższym trywialnym przykładzie, aby funkcji przekazać wartość 10, musimy utworzyć tymczasową zmienną var, nadać jej wartość, a następnie przekazać adres zmiennej var do funkcji foo. Z oczywistych powodów konstrukcje typu:
foo(&10);
lub też:
foo(10);
nie zadziałają wcale (błędy podczas kompilacji) lub nie zrobią tego, czego oczekujemy. Jak ten problem rozwiązać? Standard C99 wprowadza nam eleganckie rozwiązanie powyższego problemu pod postacią tzw. literałów złożonych (compound literals). Na czym one polegają? Ogólnie zamiast stworzyć tymczasowe zmienne poza wywołaniem funkcji, tak jak to pokazałem w powyższym przykładzie, możemy utworzyć tymczasowe „obiekty” w samym wywołaniu funkcji:
foo(&(int){10});

Jak widzimy zastosowanie takiego literału polega na podaniu w nawiasie okrągłym jego typu, a następnie w nawiasie klamrowym inicjalizatora typu – czyli po prostu wartości tworzonego obiektu.

Pamiętaj, że o ile zapis z wykorzystaniem literału złożonego jest krótszy i bardziej elegancki, to współczesne kompilatory są na tyle sprawne, że zarówno zapis z literałem złożonym, jak i zapis wykorzystujący zmienne tymczasowe powinien wygenerować identyczny kod asemblerowy. Stąd też stosowanie literałów złożonych w dużej mierze związane jest tylko z większą czytelnością zapisu kodu.

Obiekt będący literałem złożonym istnieje tylko w czasie wywołania funkcji foo – po jej zakończeniu jest automatycznie niszczony, nie możemy się już do niego odwoływać. Korzyści ze stosowania tego typu literałów jeszcze wyraźniej widać w przypadku struktur danych. Rzućmy okiem na kolejny przykład:

typedef union
{
   struct
   {
      uint8_t IB02      : 3;
      uint8_t AM        : 1;
      uint8_t ID        : 2;
      uint8_t TY        : 2;
      uint8_t DMode     : 1;
      uint8_t NoSync    : 1;
      uint8_t WMode     : 1;
      uint8_t DenMode   : 1;
      uint8_t IB12      : 1;
      uint8_t DFM       : 2;
      uint8_t VSMode    : 1;
   };
   uint16_t word;
} ssd2119_EntryMode_Reg;

Zdefiniowaliśmy sobie unię struktury anonimowej i typu uint16_t, który będzie reprezentował 16-bitową wartość jej pól. Dzięki temu mamy wygodny dostęp do poszczególnych pól (jak łatwo zauważyć pola te definiują bity sterujące jednego z rejestrów kontrolera SSD2119), a pole word będzie zawierać bieżącą reprezentację binarną całej struktury. Zadeklarujmy sobie teraz prototyp funkcji wykorzystującej powyższą unię:
void ssd2119_SendCmdWithData(uint8_t cmd, uint16_t data);
Jak widzimy, nasza unia przekazywana jest do funkcji jako argument data o typie uint16_t, a nie ssd2119_EntryMode_Reg. Dlaczego tak? Nasza funkcja ssd2119_SendCmdWithData wysyła do kontrolera LCD po prostu polecenie przekazywane jako argument cmd, a pole data zawiera tylko dane, które razem z poleceniem wysyłamy. Dane te są zależne od wybranego polecenia, w związku z tym ich typ może być inny niż ssd2119_EntryMode_Reg. Mamy już strukturę danych, a także funkcję wysyłającą polecenie do kontrolera, połączmy to teraz w całość. Klasycznie, polecenie zapisu do rejestru Entry Mode kontrolera wysłalibyśmy tworząc zmienną tymczasową:
ssd2119_EntryMode_Reg tmp={.DFM=0b11, .DenMode=1, .TY=0b01, .ID=0b11, .AM=0};
ssd2119_SendCmdWithData(0x11, tmp.word);
Ale możemy też to uczynić z wykorzystaniem literałów złożonych:
ssd2119_SendCmdWithData(0x11, (ssd2119_EntryMode_Reg){.DFM=0b11, .DenMode=1, .TY=0b01, .ID=0b11, .AM=0}.word);
W tym przypadku utworzony został obiekt tymczasowy o typie ssd2119_EntryMode_Reg, który inicjalizujemy analogicznie jak to robimy w przypadku inicjalizacji struktur/unii, a do funkcji, która oczekuje przecież typu uint16_t, przekazujemy wartość pola word reprezentującego naszą unię. Proste, prawda? W ten sposób uzyskujemy o wiele krótszy zapis i moim zdaniem bardziej przejrzysty.

Wykorzystanie takich literałów daje także inne korzyści, m.in. możliwość łatwej inicjalizacji typów złożonych, np. list – co wykorzystujemy np. podczas tworzenia menu i obiektów związanych z grafiką. Liczne przykłady wykorzystujące literały złożone wykorzystuję w swoich książkach „AVR. Praktyczne projekty” i „AVR. Układy peryferyjne”. Można się z nimi zapoznać pobierając darmowe przykłady (ze strony wydawnictwa Helion). Warto to zrobić, bo jak widać, literały złożone to prosty sposób na tworzenie przejrzystych programów. Chodzi tu nie tylko o uzyskaną zwartość kodu, ale także o niezaśmiecanie przestrzeni nazw zmiennymi tymczasowymi, co pozwala nam uniknąć wielu problemów.

Kurs języka C: Spis treści

Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

wtorek, 22 marca 2011

Problem znaku zapytania podczas konwersji float do znaków ASCII w celu wyświetlenia na np. LCD itp.


Autor: Dondu

Ponieważ co jakiś czas otrzymuję pytania dot. problemu znaku zapytania przy konwersji za pomocą funkcji z rodziny printf, sprintf, itp.:


Anonimowy
... kiedy chcę wysłać liczbę to na terminalu wyświetla się tylko ?

Anonimowy
... kiedy importuje projekt do ATMEL Studio 6.1, linkuje 2 biblioteki libprint_flt.a i libm.a i wgrywam to już zamiast rps i ms/obr pokazują się pytajniki. Mógłbyś mi powiedzieć co robię źle?

Anonimowy
... potem podczas proby wysylania cyfry w wyniku otrzymuje wyswietlony na ekranie znak zapytania.

Anonimowy
wysłanie liczby daje efekt w postaci "?"

Anonimowy
Chciałbym na ekranie telefonu (chodzi o system Android - dop. red.) wyświetlać wartości odczytywane przez mikrokontroler z czujnika temperatury, niestety sposób przedstawiony powyżej generuje na ekranie pytajniki.


postanowiłem skrobnąć ten krótki artykuł.

Chodzi o moment, w którym obliczoną liczbę typu float (zmiennoprzecinkową) konwertujemy za pomocą funkcji  printf, sprintf, itp. do postaci ciągu znaków ASCII, w celu np. jej wyświetlenia na LCD, czy też wysłania za pomocą jakiegoś interfejsu np. RS-232C do odbiornika np. komputera.


Anonimowy
Na LCD wyświetla mi się"U=? [V]". Natomiast wartość nieprzeliczona wyświetla się normalnie.



Objawy

Zamiast otrzymania za pomocą w/w funkcji prawidłowo sformatowanego wyniku w postaci ciągu znaków otrzymujemy znak zapytania.


Przykład - Spodziewasz się ładnie sformatowanych liczb:


Przykład prawidłowej konwersji liczby typu float.
Przykład prawidłowej konwersji liczb float na ciągi znaków.
Pochodzi z artykułu: RS-232: Komunikacja ATmega8 z komputerem

a otrzymujesz jedynie znaki zapytania (przykład dostarczony przez Anonimowego):


Przykład nieprawidłowej konwersji liczby typu float.
Przykład nieprawidłowej konwersji liczb float na ciągi znaków.

Powyższy przykład pokazuje wysyłanie danych z mikrokontrolera w formie znaków ASCII przez RS-232 do komputera. Ale jak już wspomniałem wcześniej te same objawy dot. LCD, itp.



Rozwiązanie

Należy wykonać dwie czynności dodając w opcjach projektu:


1. dodanie bibliotek libm.a oraz libprintf_flt.a

Wersja dla AVR Studio:

AVR Studio - Dodawanie bibliotek.
Przykład dla AVR Studio 4.xx

Wersja dla Atmel Studio:


Atmel Studio - ustawienia projektu.
Atmel Studio - ustawienia projektu.


Atmel Studio - dodanie bibliotek.
Atmel Studio - dodanie bibliotek.


2. dodanie opcji linkera:

Wersja dla AVR Studio:

AVR Studio - Dodawanie opcji linkera.
Przykład dla AVR Studio 4.xx


Na niebiesko zaznaczyłem pole, w którym po kolei powinieneś wpisać wszystkie parametry i klikając przycisk Add, dla każdego parametru osobno. Efekt końcowy ma być taki jak na powyższym screen-ie - kolejność parametrów nie ma znaczenia.

Aby nie było wątpliwości co do literek:
-WI
-lprintf_flt
-lm
-u
vfprintf

Zauważ, że vfprintf jako jedyny nie ma poprzedzającego znaku minus (myślnika).

Szczegóły w zakresie opcji linkera znajdziesz pod niniejszym linkiem: <stdio.h>: Standard IO facilities


Wersja dla Atmel Studio:


Atmel Studio - opcje linkera.
Atmel Studio - opcje linkera. 


Nie zapomnij zapisać ustawień :-)



Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

środa, 16 marca 2011

PROGMEM i problemy z kompatybilnością

Autor: tmf
Redakcja: Dondu

W związku z wprowadzeniem do toolchaina nowej wersji avr-gcc (4.6 i wyższe) użytkownicy często napotykają drobny problem wynikający ze zmiany nastawienia twórców kompilatora do sposobu deklarowania zmiennych w pamięci FLASH. Zmiany te są daleko bardziej idące, niż tu pokażę, ale to temat na zupełnie oddzielny artykuł.

Na razie zajmijmy się kompatybilnością starego kodu z nowszymi wersjami avr-gcc.


W avr-gcc do wersji 4.4 zmienne i dane mogliśmy umieszczać w pamięci FLASH korzystając z makrodefinicji PROGMEM (plik nagłówkowy <avr/pgmspace.h>):
uint8_t LED PROGMEM = {1, 2, 3};
Lecz równie dobrze mogliśmy stosować nieco inną notację:
const uint8_t LED PROGMEM = {1, 2, 3};
Co nam zmienia modyfikator const? Jak wiemy, powoduje on, że tak zdefiniowana zmienna jest stałą, której w sposób legalny w programie nie możemy zmieniać (możemy ją zmienić przy pomocy wskaźników, lecz takie działanie jest zgodnie ze standardem C niezdefiniowane i np. na architekturze ARM zwykle prowadzi do błędu ochrony pamięci).

Dzięki umieszczeniu const kompilator lepiej zna nasze intencje – powyżej zdefiniowaliśmy 3-elementową tablicę, której zawartość w programie nie może być zmieniona, a sama tablica znajdzie się w pamięci FLASH.

Stąd też umieszczenie słowa const przed definicją nie ma znaczenia wyłącznie kosmetycznego – pozwala kompilatorowi wykrywać błędy polegające na próbie modyfikacji tablicy (czy ogólnie danych w pamięci FLASH, która jak wiemy jest pamięcią niezapisywalną w „normalny” sposób). Twórcy avr-gcc wyszli właśnie z tego założenia – skoro dane w pamięci FLASH są niemodyfikowalne, to nie tylko powinniśmy, ale od wersji avr-gcc 4.6 musimy je poprzedzić modyfikatorem const.

Co jeśli tego nie zrobimy? Oczywiście kompilator zgłosi błąd:
Error      1             variable 'LED' must be const in order to be put into read-only section by means of '__attribute__((progmem))'

Ok, czyli pamiętamy, że jeśli używamy PROGMEM to zmienna do której się ono odnosi musi być zdefiniowana/zadeklarowana z atrybutem const.

Nie jest to wielka dokuczliwość, po prostu zamiast:
char xx[] PROGMEM="AAA";
piszemy:
const char xx[] PROGMEM="AAA";

Proste, prawda?

Proste, ale w sumie uciążliwe, stąd też dla czytelników mojej książki “Język C dla mikrokontrolerów AVR. Od podstaw do zaawansowanych aplikacji” przygotowałem nowe wersje przykładów, w których owo const zostało dodane i dzięki temu poprawnie się one kompilują z nowymi kompilatorami (przy okazji poprawiłem też drobne inne błędy i dodałem pliki projektów dla Atmel Studio 6.1).

Zachęcam do pobrania nowych przykładów (załączników do książki) ze strony: Przykłady na FTP

Czasami czytelnicy pytają dlaczego po prostu nie wymusić, aby stałe były automatycznie umieszczane w pamięci FLASH? Standardowa odpowiedź twórców gcc jest prosta – ponieważ const nie służy do określenia lokalizacji zmiennej. I mają w tym sporo racji. Do tego w gcc 4.7 wprowadzono nowe mechanizmy, ale to zupełnie inna historia ...

Osoby przechodzące na nową wersję gcc powinny pamiętać jeszcze o jednej zmianie. Otóż dla wygody często programiści definiowali sobie nowe typy:
typedef void PROGMEM prog_char;
Powyższe definiuje typ prog_char, w efekcie dane przyporządkowane takiej zmiennej powinny zostać umieszczone w pamięci FLASH. Unikamy dzięki temu pisania za każdym razem char PROGMEM ...

Tu niestety pojawia się problem. Zgodnie ze standardem C, atrybuty [a PROGMEM jest po prostu makrodefinicją rozwijaną do postaci __attribute__((__progmem__)) ] nie mogą być przypisywane do typów.

Taką funkcjonalność niejako przez przypadek posiada kompilator avr-gcc, lecz nie jest to funkcja udokumentowana i wsparcie dla niej może zostać w każdej chwili usunięte, co oczywiście spowoduje katastrofę jeśli z tego mechanizmu korzystamy.

Stąd obecnie nie zaleca się używania typów zdefiniowanycvh w <avr/progmem.h> rozpoczynających się prefiksem prog_.

Jeśli z jakiegoś powodu nie chcemy się dostosować do zmian, możemy włączyć (na własne ryzyko) kompatybilność ze starą wersją gcc definując symbol __PROG_TYPES_COMPAT__:
#define __PROG_TYPES_COMPAT__

#include <avr/pgmspace.h>

const prog_char aa[]="aaa";
Jak widzimy słowa const i tak nie unikniemy. Niemniej ciągle będziemy dostawać ostrzeżenie:
Warning 1 'prog_char' is deprecated: prog_char type is deprecated. [-Wdeprecated-declarations] 
przypominające nam, że to co robimy nie świadczy o naszym rozsądku :-)

Przy okazji mamy podane jak się tego typu ostrzeżeń pozbyć (co jest jeszcze bardziej niemądrym postępowaniem).
Pamiętajmy, żeby atrybuty używać wyłącznie w połączeniu z definicją/deklaracją zmiennej, nigdy z definicją typu.
A może tego całego PROGMEM da się w ogóle jakoś pozbyć? A i owszem, jak to zrobić czytelnicy mojej nowej książki AVR. Praktyczne projekty" dowiedzą się już teraz, a pozostali – jak tylko znajdę chwilkę na nowy artykuł.

Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

Działy
Działy dodatkowe
Inne
O blogu




Dzisiaj
--> za darmo!!! <--
1. USBasp
2. microBOARD M8


Napisz artykuł
--> i wygraj nagrodę. <--


Co nowego na blogu?
Śledź naszego Facebook-a



Co nowego na blogu?
Śledź nas na Google+

/* 20140911 Wyłączona prawa kolumna */
  • 00

    dni

  • 00

    godzin

  • :
  • 00

    minut

  • :
  • 00

    sekund

Nie czekaj do ostatniego dnia!
Jakość opisu projektu także jest istotna (pkt 9.2 regulaminu).

Sponsorzy:

Zapamiętaj ten artykuł w moim prywatnym spisie treści.