Mikrokontrolery - Jak zacząć?

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

środa, 23 marca 2011

Język C - Zawijanie indeksu tablicy

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


Autor: BlueDraco
Redakcja: Dondu

Kurs języka C: Tips & Tricks - Spis treści

Bardzo często mamy w programie potrzebę „zawinięcia” indeksu tablicy, czyli spowodowania, by po dojściu do końca tablicy indeks „przeskoczył" na jej początek. Korzystamy z tego m.in. przy obsłudze mulipleksowanych wyświetlaczy LED oraz przy realizacji wszelkich buforów cyklicznych.

Najbardziej ogólne i uniwersalne rozwiązanie programowe polega na zdefiniowaniu rozmiaru wektora jako stałej preprocesora i zadeklarowaniu bufora oraz indeksu, na przykład tak:

#define BUF_SIZE 8

unsigned int buf[BUF_SIZE];
unsigned int idx;

Musimy przy tym oczywiście zwrócić uwagę, by typ użyty dla indeksu umożliwiał zapisanie wartości z zakresu od 0 do BUF_SIZE – 1, czyli przy BUF_SIZE nie przekraczającym 256 wystarczy unsigned char, a dla większych rozmiarów powinniśmy użyć typu unsigned int.

Dostęp do kolejnego elementu wektora i modyfikacja indeksu mogą wyglądać np. tak:

x = buf[idx ++];

Po czym zaraz powinno nastąpić zawinięcie indeksu, co najprościej zapisać tak:

idx %= BUF_SIZE;

Zapis ten jest bardzo prosty i czytelny - idx zyskuje wartość będącą resztą z dzielenia bieżącej wartości przez BUF_SIZE, czyli wartość BUF_SIZE zostaje zamieniona w wartość 0.

Warto jednak  przyjrzeć się bliżej tej prościutkiej instrukcji i zdać sobie sprawę z konsekwencji jej użycia, które silnie zależą od wartości stałej BUF_SIZE.

Zacznijmy od zauważenia, że jeśli BUF_SIZE ma wartość 256, a idx jest typu unsigned char, to instrukcja „zawijająca” indeks staje się całkowicie zbędna, gdyż w wyniku samej inkrementacji 8-bitowej danej o wartości 255 uzyskujemy wartość 0.

Jeżeli BUF_SIZE jest potęgą dwójki (np. 4, 8, 16, 32 itd.), to dobry kompilator zamieni naszą instrukcję

idx %= BUF_SIZE;

na równoważny jej zapis

idx &= BUF_SIZE – 1;

Czyli np. zamiast

idx %= 4;

kompilator wykona operację

idx &= 3;

Jeśli nasze zaufanie do zdolności optymalizacyjnych kompilatora jest ograniczone, możemy wyręczyć kompilator zapisując instrukcję jawnie w postaci iloczynu logicznego.

Co się stanie, gdy rozmiar bufora nie będzie potęgą dwójki?
Po prostu kompilator nie zoptymalizuje operacji zawinięcia według pokazanego powyżej schematu i wykona dzielenie zgodnie z zapisem programu, przypisując do zmiennej idx wartość reszty z tego dzielenia.

Programista powinien zdawać sobie sprawę z kosztu tej operacji. Jeśli procesor nie ma instrukcji dzielenia, dzielenie jest realizowane przez procedurę programową, a jego czas jest porównywalny z czasem wykonania od kilkudziesięciu do nawet kilkuset dodawań. Z taką sytuacją mamy do czynienia w niemal wszystkich mikrokontrolerach 8-bitowych (w tym AVR i PIC), ale również w 32-bitowym rdzeniu Cortex-M0. Nawet w procesorach używanych w komputerach PC czas operacji dzielenia liczb całkowitych jest porównywalny z czasem kilkunastu dodawań.

Jeśli więc nie chcemy przeciążyć naszego mikrokontrolera zbędnym w tym przypadku dzieleniem, powinniśmy napisać:

if (idx == BUF_SIZE) idx = 0;

która to sekwencja na typowym mikrokontrolerze 8-bitowym, ale również na 32-bitowych Cortexach-M0 wykona się szybciej, niż np.

idx %= 23;

Co prawda nasza strona jest poświęcona mikrokontrolerom, ale wypada wspomnieć, że w przypadku procesorów x86 stosowanych w PC wcale nie jest oczywiste, które z tych rozwiązań jest szybsze – odgrywa tutaj rolę wiele złożonych czynników.





Przykłady

Poniżej kilka przykładów dostępnych w kompilatorze CManiak, który powinien być w tej chwili widoczny w lewym górnym rogu przeglądarki. Możesz uruchamiać oraz modyfikować przykłady we własnym zakresie, aby poćwiczyć zawijanie indeksów.

Przykład 1:

Obliczenie kwadratów liczb na bazie indeksu tablicy i zapisanie wyniku ich do tablicy pod odpowiadającym mu indeksem.
#include <stdio.h>
int main(void){
  
  #define BUF_SIZE 8 
  
  unsigned int buf[BUF_SIZE];  
  unsigned int idx; 
  
  //oblicz kwadraty kolejnych liczb i zapisz w tablicy
  idx = 0;
  while(idx < BUF_SIZE) buf[idx++] = idx * idx;  //idx do kwadratu

  //pokaż wyniki zapisane w tablicy  
  idx = 0;
  while(idx < BUF_SIZE) {
    printf("idx[%d] = %d \n", idx, buf[idx]);
    idx++;
  }
  
  return 0;
}


Przykład 2:

Przykład podobnie jak powyższy liczący kwadraty liczb, lecz wyświetlający wielokrotnie zawartość tablicy począwszy od wybranej komórki.
#include <stdio.h>
#include <stdio.h>

int main(void){
    
  #define BUF_SIZE   8 
  unsigned int buf[BUF_SIZE];  
  unsigned int idx; 

  //wypełniamy tablicę wartościami kwadratu idx
  //oraz pokaż wyniki zapisane w tablicy  
  printf("Tablica wypełniona - stan początkowy: \n");
  for(idx=0; idx<BUF_SIZE; idx++){
    buf[idx] = idx * idx;
    printf("tab[%d] = %d \n", idx, buf[idx]);
  }

  //Wyświetalmy tablicę począwszy od wybranej komórki 
  //wykonując X zadanych kroków
  //po natrafieniu na koniec tablicy zawijamy indeks, by kolejne
  //komórki tablicy pokazywać od jej poczatku
  printf("\nElementy pokazane począwszy od wybranego: \n");
  
  #define ILOSC_KROKOW    18
  #define KOMORKA_POCZATKOWA   3

  idx = KOMORKA_POCZATKOWA;
  unsigned int krok =  0;
  while(krok++ < ILOSC_KROKOW) {
    printf("krok=%-3d tab[%d]=%d \n", krok, idx, buf[idx]);
    idx++;
    idx %= BUF_SIZE;    //zawiń indeks
  }

  return 0;
}


Kurs języka C: Tips & Tricks - Spis treści

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

sobota, 19 marca 2011

Techniki programowania: switch() oraz if()


Autor: BlueDraco
Redakcja: Dondu


Punkt II.9 Smoczych zasad brzmi:

Nie używaj instrukcji switch() ani sekwencji if()...else if(), gdy poszczególne sekcje case różnią się tylko wartościami danych, a nie wykonywanymi operacjami.

Rozejście warunkowe jest zawsze czasochłonne i warto go unikać, gdy nie jest niezbędne. Jeżeli w zależności od wartości zmiennej używamy tylko różnych danych, a nie wykonujemy różnych akcji, należy zastąpić rozejście kodu wyborem odpowiedniej wartości danych – zwykle przy użyciu tablicy zmiennych lub stałych.

Ilustrują to dwa poniższe przykłady, zaczerpnięte z polskiego Internetu.





Przykład 1

Pierwszy przykład pokazuje zamianę wartości cyfry na kod wyświetlacza siedmiosegmentowego. Oryginalny kod wygląda tak:

unsigned char transl(uint32_t cyfra){
    switch (cyfra) {
        case 0 :
            return 0x3f;
        case 1 :
            return 0x06;
        case 2 :
            return 0x5b;
        case 3 :
            return 0x4f;
        case 4 :
            return 0x66;
        case 5 :
            return 0x6d;
        case 6 :
            return 0x7d;
        case 7 :
            return 0x07;
        case 8 :
            return 0x7f;
        case 9 :
            return 0x6f;
        case 10 :
            return 0x77;
        case 11 :
            return 0x7c;
        case 12 :
            return 0x39;
        case 13 :
            return 0x5e;
        case 14 :
            return 0x79;
        case 15 :
            return 0x71;
        default :
            return 0x40;
        }
}

Po wprowadzeniu tablicy zastępującej zbędne rozejście, kod przyjmie następującą postać:

uint8_t transl(uint32_t cyfra)
{
    static const uint8_t cyfry[] = {
        0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07,
        0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71,
        0x40
        };
    return cyfry[cyfra <= 16 ? cyfra : 16];
}





Przykład 2


28 marca 2013r. godz. 11:38
W związku ze zgłoszeniem możliwego naruszenia praw autorskich w zakresie niniejszego przykładu, wstrzymujemy jego publikację do momentu wyjaśnienia wszelkich wątpliwości.
Dondu


Jako pracę domową proponuję Czytelnikom przyjrzenie się kodowi asemblerowemu powstałemu przez kompilację obu wersji programów oraz porównanie zajętości pamięci.

Więcej: Techniki programowania mikrokontrolerów - Smocze zasady

Autor: BlueDraco
Redakcja: Dondu

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

poniedziałek, 14 marca 2011

Techniki programowania uC: Smocze zasady


Autor: BlueDraco
Redakcja: Dondu


Przedstawiam zbiór zasad poprawnego projektowania oprogramowania, który powstał na bazie moich doświadczeń z praktyki zawodowej.

Nie ma reguł bez wyjątków – dla niemal każdego z poniższych „przykazań” można wskazać przypadek, gdy niestosowanie go będzie uzasadnione – sam w wielu projektach nie stosowałem tej, czy innej zasady.

Jeśli jednak napisałeś mniej niż 20 programów i uważasz, że wiele z tych reguł nie ma zastosowania do Twojego projektu, to prawdopodobnie jesteś w błędzie, a działanie Twojego oprogramowania może być dalekie od Twoich oczekiwań.





I. Struktura oprogramowania


I.1. Opóźnienia programowe

Nie używaj opóźnień programowych w postaci funkcji z pętlami („delay”) nigdzie poza sekwencją inicjującą działanie mikrokontrolera, wykonywaną jednorazowo przy starcie urządzenia.


I.2. Timer

W każdym projekcie, nawet najprostszym, użyj timera zgłaszającego przerwania ze stałą częstotliwością. Nawet nie wiesz, jak bardzo jest potrzebny.


I.3. Jeden timer wystarczy

Nie używaj dwóch timerów zgłaszających przerwania ze stałą częstotliwością jeśli programujesz mikrokontroler z jednopoziomowym systemem przerwań (np. taki jak AVR Mega lub Tiny).


I.4. Żadnego oczekiwania w obsłudze przerwania

Aktywne oczekiwanie stosuj wyłącznie w kodzie o najniższym priorytecie (pętli zdarzeń, a w przypadku wielopoziomowego systemu przerwań i programu bez pętli zdarzeń – w obsłudze przerwania o najniższym priorytecie).

Nie czekaj w obsłudze przerwania jeżeli poza procedurą obsługi przerwania w systemie jest jeszcze inny kod o takim samym lub niższym priorytecie wykonania (np. pętla główna lub inne przerwanie o takim samym priorytecie).


I.5. Nie obniżaj priorytetu

Nie obniżaj programowo priorytetu procesora. Na przykład w funkcjach przerwania w mikrokontrolerach AVR, nie używaj ISR_NO_BLOCK, bo narażasz się na kłopoty.


I.6. Zbędne przerwania

Nie używaj procedur obsługi przerwań, w których jedyną akcją jest ustawienie znacznika wystąpienia zdarzenia lub odczyt danej z rejestru do pojedynczej zmiennej skalarnej. Jeśli tak napisałeś program – to znaczy, że w ogóle nie potrzebujesz tego przerwania.


I.7. Zmiany stanu portów

Jeżeli dwa różne fragmenty kodu działające na różnych priorytetach (np. program główny i procedura obsługi przerwania) wykonują operacje logiczne lub arytmetyczne na tej samej danej lub tym samym porcie – zawsze blokuj przerwania na czas wykonania takiej operacji przez kod o niższym priorytecie (program główny).


I.8. ADC i przerwania

Jeśli korzystasz z przetwornika analogowo-cyfrowego mikrokontrolera i nie jest on wyzwalany sprzętowo przez timer – nie korzystaj z przerwania ADC. Jeśli jest wyzwalany sprzętowo przez timer – używaj tylko jednego z przerwań – timera albo ADC.


I.9. Unikaj pętli zdarzeń ...

Unikaj niepustych pętli głównych, o ile jest to możliwe. Nie stosuj pętli zdarzeń przy programowaniu procesorów z wielopoziomowym systemem przerwań. Nie jest dobrze używać pętli głównej do czegokolwiek poza usypianiem procesora.


I.10. ... a przynajmniej zbyt długich

Jeśli Twoja pętla główna urosła do pięciuset linii - pomyśl o użyciu systemu operacyjnego.





II. Zapis programu w języku C


II.1. Deklaracje funkcji 

Używaj pełnej listy argumentów, a w funkcjach bezargumentowych – słowa kluczowego void.


II.2. Zapis stałych

Stałe używane w programie zapisuj w takiej postaci, w jakiej pierwszy raz o nich pomyślałeś.
Zapisuj:
  • znaki jako znaki, a nie ich numery w tabeli ASCII,
  • maski bitowe – jako liczby binarne, szesnastkowe lub ósemkowe,
  • liczniki i podzielniki – jako liczby dziesiętne.

II.3. Typy o jawnych rozmiarach

Jeśli dana ma mieć określoną długość, używaj typów o jawnie określonych długościach, zdefiniowanych w standardowym pliku nagłówkowym stdint.h, np. uint8_t, int8_t, uint32_t, itp..


II.4. Rozmiary danych

Dla stałych i zmiennych statycznych (czyli takich, których czas życia jest równy czasowi życia programu) używaj najkrótszych typów gwarantujących poprawne działanie programu.

Dla lokalnych zmiennych roboczych procedur używaj w miarę możliwości typów o rozmiarze odpowiadającym szerokości rejestrów procesora, np. programując procesory ARM używaj typów 32-bitowych, a dla AVR8-bitowych.


II.5. Atrybut static w deklaracjach zewnętrznych 

Stosuj atrybut static dla wszystkich danych i funkcji na poziomie zewnętrznym, za wyjątkiem publicznych, czyli tych, które są używane w innych modułach Umożliwia to kompilatorowi skuteczniejszą optymalizację kodu oraz lepszą detekcję błędów w programie.


II.6. Dane lokalne funkcji

Zmienne używane wyłącznie wewnątrz jednej funkcji deklaruj wewnątrz tej funkcji. Jeżeli zmienna ma zachowywać wartość pomiędzy wywołaniami, należy ją zadeklarować z atrybutem static.


II.7. Stałe statyczne

Używaj atrybutu const dla wszystkich stałych, a stałe definiowane wewnątrz procedur deklaruj jako static const.


II.8. Volatile

Używaj atrybutu volatile dla wszystkich zmiennych statycznych, które są używane na więcej niż jednym poziomie priorytetowym oraz dla takich, które są współużywane przez procedury mogące podlegać wywłaszczaniu (czyli np. przez dwa procesy w systemie operacyjnym). Nie używaj atrybutu volatile dla zmiennych deklarowanych wewnątrz funkcji.


II.9. switch() oraz if()

Nie używaj instrukcji switch(), gdy poszczególne sekcje case różnią się tylko wartościami danych, a nie wykonywanymi operacjami.



II.10. Ostrzeżenia kompilatora

Pisz program tak, by kompilator nie generował ostrzeżeń. Zawsze czytaj ostrzeżenia i usuwaj ich przyczyny, a nawet jeśli uważasz, że zapis jest poprawny, a kompilator nadgorliwy (a tak naprawdę jest w przypadku nowych wersji kompilatorów).


To moje zasady, a Wasze?


Autor: BlueDraco
Redakcja: Dondu

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

piątek, 11 lutego 2011

O drganiach styków bez bajek (przykład dla STM32)

Autor: BlueDraco
Redakcja: Dondu


Jednym z powszechnych problemów, z jakimi styka się projektant urządzeń cyfrowych, są drgania styków zawartych w przyciskach, klawiszach, impulsatorach, przełącznikach i przekaźnikach. Samo zjawisko jest bardzo proste, jednak narosło wokół niego wiele mitów, z których najdziwniejszy głosi, że zjawisko to w ogóle nie występuje lub występuje bardzo rzadko.

Niestety, drgania styków występują w praktycznie każdym łączniku mechanicznym. Przy każdym zwieraniu i rozwieraniu styku mechanicznego następują sprężyste odbicia, które skutkują tam, że podczas jednej logicznej zmiany stanu styku w rzeczywistości następuje takich zmian kilka.

Jeżeli styk jest rozwarty i następuje jego zwarcie, w rzeczywistości w bardzo krótkim czasie nastąpi seria zwarć i rozwarć, po której nastąpi trwałe zwarcie. Jeżeli przez taki przycisk zasilimy np. żarówkę lub diodę świecącą, to nie zauważymy żadnych niespodziewanych efektów, gdyż czas drgań styku jest znacznie krótszy zarówno od czasu zaświecania włókna żarówki jak i od czasu bezwładności ludzkiego oka, które nie jest w stanie zauważyć tak krótkich rozbłysków pozbawionej praktycznie bezwładności diody świecącej.


Drgania styków ujawniają się wtedy, gdy stan styku oddziałuje na układ elektroniczny, którego czas reakcji wyraża się w nanosekundach lub mikrosekundach, a nie w dziesiątkach milisekund, jak ma to miejsce w przypadku zmysłu wzroku.

Pojedyncze naciśnięcie przycisku jest w takim przypadku rejestrowane jako seria kolejnych naciśnięć i zwolnień.

Jeżeli więc urządzenie zlicza pochodzące od przycisku impulsy – pojedyncze naciśnięcie zostanie odebrane jako seria impulsów.

Najprostszym przykładem uwidaczniającym ten efekt jest wyłącznik światła reagujący na naciśnięcia pojedynczego przycisku chwilowego. Jedno naciśnięcie ma powodować zaświecenie światła, następne – jego zgaszenie.

Jeżeli nie uwzględnimy zjawiska drgań zestyków – działanie przycisku będzie dość przypadkowe, gdyż podczas naciskania i zwalniania przycisk będzie generował kilka impulsów, a wpływ naciskania przycisku na końcowy efekt w postaci zaświecenia lub zgaszenia światła będzie zależał od tego, czy liczba odbić styku podczas łączenia i rozłączania była nieparzysta, czy parzysta.


Bajki zaczynają się na "be", i takie są ...


Spróbujmy przyjrzeć się praktycznie temu problemowi, korzystając z niedrogiej i wygodnej w użyciu płytki demonstracyjnej STM32F0DISCOVERY, wyposażonej w mikrokontroler serii STM32F051, dwie diody świecące i jeden przycisk podłączony do portu wejściowego mikrokontrolera.

Skorzystamy z dostępnego na jednej z polskich witryn internetowych poświęconych mikrokontrolerom programu, który ma demonstrować brak lub nieistotność efektu drgań styków. Program, stworzony oryginalnie dla mikrokontrolerów rodziny AVR, zaadaptujemy dla STM32F0. Zamiast zaświecać i gasić jedną diodę, program będzie sterował obiema diodami na płytce, naprzemiennie zaświecając je i gasząc.

Poniżej zamieszczam treść programu, zawartego w projekcie o nazwie Drgania1. Oba projekty demonstracyjne zostały przygotowane w darmowej wersji środowiska Keil i są zawarte w dostępnym poniżej pliku drgania.zip.


/*
 STM32F0DISCOVERY
 Drgania stykow - wersja 1 - bledna
 BlueDraco 02'2013
*/

#include "stm32f0xx.h"
//========================================================================
// STM32F05x
#define GPIO_MODER_OUT 1
//========================================================================
// STM32F0DISCOVERY
#define LED_PORT  GPIOC
#define BLUE_LED_BIT 8
#define GREEN_LED_BIT 9

#define BUTTON_PORT  GPIOA
#define BUTTON_BIT  0
//========================================================================
void SystemInit(void)
{
}
//========================================================================
int main(void)
{
    uint8_t key_lock = 0;

    // konfiguracja portow
    RCC->AHBENR = RCC_AHBENR_GPIOCEN |  RCC_AHBENR_GPIOAEN; // GPIOC, GPIOA
    LED_PORT->MODER = GPIO_MODER_OUT << (GREEN_LED_BIT << 1)
                      | GPIO_MODER_OUT << (BLUE_LED_BIT << 1); // LED
    LED_PORT->ODR = 1 << GREEN_LED_BIT; // green LED on

    while (1)
    {
        if ( !key_lock && (BUTTON_PORT->IDR >> BUTTON_BIT & 1))
        {
            key_lock=1;
            // reakcja na wcisniecie przycisku
            LED_PORT->ODR ^= 1 << GREEN_LED_BIT | 1 << BLUE_LED_BIT;
        }
        else if( key_lock && !(BUTTON_PORT->IDR >> BUTTON_BIT & 1))
            key_lock = 0;
    }
}


Do pobrania oba programy: drgania.zip


Po skompilowaniu programu i zaprogramowaniu mikrokontrolera możemy rozpocząć próby.

Program według intencji jego Autora miał zmieniać stan diody na przeciwny po każdym naciśnięciu przycisku. Możemy jednak zaobserwować, że niekiedy program podczas naciskania przycisku nie zmienia stanu diod; zdarza się również, że stan diod zmienia się podczas zwalniania przycisku.

Dlaczego?

To proste – zarówno podczas naciskania jak i zwalniania przycisku następuje kilka zwarć i rozwarć, które są wykrywane przez program. Program nie jest w stanie rozróżnić, czy połączenie jest wynikiem naciśnięcia przycisku, czy odbicia styku.

Okres drgań jest wielokrotnie większy od czasu obiegu pętli, stąd bierze się losowe zachowanie programu, całkowicie zgodne z tym, czego można się spodziewać po błędnie napisanym kodzie – zarówno podczas naciskania jak i zwalniania przycisku program kilkukrotnie wykonuje obie instrukcje warunkowe, po jednym razie dla każdej zarejestrowanej zmiany stanu przycisku.

Jak więc pozbyć się tego efektu?

Każde poprawne rozwiązanie problemu ignorowania drgań musi być oparte o odmierzanie czasu (opóźnienia).


Z kolei poprawne odliczanie czasu wymaga w praktyce zawsze użycia sprzętowego timera, gdyż działanie opóźnień programowych zależy od tak wielu czynników, że nie można polegać na odliczonym w ten sposób czasie.

Wymieńmy tylko niektóre z tych czynników:
  • częstotliwość procesora, 
  • czas oczekiwania na dostęp do pamięci, 
  • atrybuty użyte w deklaracjach danych, 
  • wersja kompilatora, 
  • wybrany stopień optymalizacji, 
  • liczba zmiennych używanych w danym fragmencie programu, 
  • bieżący stan warunków dla instrukcji warunkowych użytych w programie.


Delay() ... brr!

Jedynym sposobem na wygenerowanie deterministycznego opóźnienia przez samo oprogramowanie jest użycie procedury opóźniającej (takiej jak np. delay) o znanym rozwinięciu asemblerowym, wykonywanej przy zablokowanych przerwaniach mikrokontrolera.

W praktyce trudno jest wskazać zastosowanie mikrokontrolera, które nie wymagałoby użycia przerwań, więc rozwiązanie z opóźnieniem programowym może być używane wyłącznie w krótkich programach o charakterze dydaktycznym i demonstracyjnym, a nie w realnych zastosowaniach.


Pętle zdarzeń

Rzadko również mamy do czynienia z sytuacją, w której możemy sobie pozwolić na tracenie czasu w procedurze opóźnienia – nawet w programach demonstracyjnych bez przerwań, których budowa opiera się na tzw. pętli zdarzeń.

Na ogół nie możemy zatrzymać pętli zdarzeń, gdyż mogłoby nastąpić „zgubienie” zdarzenia.

Nie możemy również odliczać czasu obiegami pętli zdarzeń, gdyż z definicji musi ona zawierać instrukcje warunkowe, a więc czas jej obiegu zależy od wartości logicznych warunków tych instrukcji.

Jeżeli spróbujemy odmierzać czas obiegami pętli zdarzeń, to dla skutecznego ignorowania drgań musimy przyjąć do obliczenia niezbędnej liczby obiegów najkrótszy możliwy czas obiegu pętli. Problem w tym, że w realnym programie, który robi coś użytecznego, pętla zdarzeń zawiera wiele złożonych bloków warunkowych w postaci instrukcji if, zapewniających reakcje na zdarzenia.

Czas wykonania tych bloków jest wielokrotnie dłuższy od czasu obiegu pętli głównej w przypadku niewystąpienia żadnego ze zdarzeń. Może się więc zdarzyć, że „pusty” obieg pętli głównej będzie trwał np. jedną setną czasu potrzebnego na obieg z obsługą zdarzeń. Jeśli okres ignorowania drgań zostanie poprawnie wyznaczony dla obiegu pustego, to przy obiegach niepustych czas opóźnienia reakcji na naciśnięcie przycisku może sięgnąć nawet kilku sekund.

Typowe przykłady takich czasochłonnych akcji programu, to:
  • odczyt temperatury z czujników, 
  • współpraca z wyświetlaczem LCD
  • transmisja szeregowa ciągu znaków z aktywnym oczekiwaniem,
a więc coś, co występuje w większości programów z pętlą zdarzeń.

Opóźnienie uzyskane poprzez zliczanie obiegów pętli zależy od tylu czynników, że trudno jest podać prawdziwą jego wartość, a każda modyfikacja programu powoduje zmianę czasu opóźnienia.

Proces określania liczby obiegów pętli gwarantującej poprawną reakcję na stan przycisku musiałby być prowadzony metodą prób i błędów, co zdecydowanie nie jest właściwym podejściem inżynierskim.


Sprzętowe opóźnienie, to jest to!

Jak widać, w prawdziwym projekcie oprogramowania, które miałoby robić cokolwiek poza zapalaniem i gaszeniem światła zachowując przy tym determinizm czasowy, nie da się wyeliminować drgań przycisków bez sprzętowego odmierzania czasu.

Tymczasem sprzętowe odmierzanie czasu, którego unika się w wielu poradnikach dla konstruktorów, nie kosztuje nic, gdyż praktycznie w każdym projekcie systemu mikroprocesorowego używamy (a przynajmniej powinniśmy używać) timera, zaprogramowanego na stałą częstotliwość, najczęściej mieszczącą się w zakresie od kilkudziesięciu Hz do kilku kHz. Mamy więc do dyspozycji wiarygodną i deterministyczną bazę czasu - musimy tylko jej użyć.

Do programowego ignorowania drgań styków potrzebne jest więc przerwanie timera. Podstawowy odcinek czasu używany przy eliminacji drgań zależy od okresu drgań styku. Dla większości współczesnych niewielkich elementów stykowych okres ten nie przekracza 20..30 ms.

Najprostsza technika ignorowania drgań polega na testowaniu stanu styku z okresem przekraczającym czas drgań. Uogólniając, oprogramowanie powinno reagować na dwa lub więcej kolejno próbkowanych stanów zestyku, przy czym długość przedziału czasowego analizowanych stanów musi być większa od maksymalnego czasu drgań.

Przyjmując maksymalny czas drgań nie przekraczający 30 ms, powinniśmy monitorować stan styku przez min. 30 ms, próbkując w tym czasie stan styku nie mniej niż dwukrotnie.

Przykładowe rozwiązanie (jedno z wielu możliwych), to analiza czterech stanów w odstępach 10 ms, czyli próbkowanych z częstotliwością 100 Hz. Odstęp czasowy pomiędzy pierwszą i ostatnią z czterech próbek wynosi w tym przypadku 30 ms.

Jeżeli przerwanie timera zgłaszane jest z częstotliwością 100 Hz – notujemy stan zestyku w każdym przerwaniu. Jeśli częstotliwość przerwań jest większa – w procedurze obsługi przerwania odliczamy przerwania, wywołując fragment kodu sprawdzający stan zestyku co pewną liczbę przerwań.

Program demonstrujący tę technikę przedstawiamy poniżej. Należy zwrócić uwagę, że tak napisany program nie jest w stanie ignorować zakłóceń, powodujących błędny odczyt stanu zestyku, może on więc być stosowany tylko wtedy, gdy sam odczyt stanu przez mikrokontroler nie jest zakłócany przez czynniki zewnętrzne. Technika ignorowania zakłóceń wpływających na błędny odczyt stanu jest nieco (ale tylko nieco) bardziej złożona.

Jak działa nasz program?

Timer systemowy SysTick został zaprogramowany na częstotliwość 100 Hz. W każdym przerwaniu timera notowany jest stan przycisku, a zmienna bstate przechowuje cztery ostatnie stany. Program reaguje na naciśnięcie przycisku, gdy po trzykrotnym stwierdzeniu zwolnienia przycisku nastąpi wykrycie jego naciśnięcia.

Dzięki temu program podczas naciskania przycisku zareaguje tylko na pierwsze zetknięcie styków, ignorując ich ewentualne odbicia, o ile skończą się one w czasie trzech kolejnych okresów próbkowania (nasze opóźnienie).

Program nie zareaguje również na odbicia podczas zwalniania przycisku, bo ewentualne zwarcie styków po ich rozwarciu podczas zwalniania przycisku nie będzie poprzedzone trzema okresami rozwarcia.


Przykład 2 (w kompilatorze)
/*
 STM32F0DISCOVERY
 Eliminacja drgan stykow - wersja 1
 BlueDraco 02'2013
*/

#include "stm32f0xx.h"
//========================================================================
#define GPIO_MODER_OUT 1
//========================================================================
// STM32F0DISCOVERY
#define LED_PORT  GPIOC
#define BLUE_LED_BIT 8
#define GREEN_LED_BIT 9

#define BUTTON_PORT  GPIOA
#define BUTTON_BIT  0
//========================================================================
// Parametry czasowe
#define SYSCLK_FREQ  HSI_VALUE
#define SYSTICK_FREQ 100 // 100 Hz -> 10 ms
//========================================================================
void SystemInit(void)
{
}
//========================================================================
int main(void)
{
    // konfiguracja portow
    RCC->AHBENR = RCC_AHBENR_GPIOCEN |  RCC_AHBENR_GPIOAEN; // GPIOC, GPIOA
    LED_PORT->MODER = GPIO_MODER_OUT << (GREEN_LED_BIT << 1)
                      | GPIO_MODER_OUT << (BLUE_LED_BIT << 1); // LED
    LED_PORT->ODR = 1 << GREEN_LED_BIT; // green LED on

    // kofiguracja timera SysTick
    SysTick->LOAD = SYSCLK_FREQ / SYSTICK_FREQ - 1;
    SysTick->VAL = 0;
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk
                    | SysTick_CTRL_ENABLE_Msk;
    SCB->SCR = SCB_SCR_SLEEPONEXIT_Msk;
    __WFI();
}
//========================================================================
void SysTick_Handler(void)
{
    static uint8_t bstate = 0; //ostatnie stany przycisku

    if ((bstate = (bstate << 1 & 0xf) | 
        (BUTTON_PORT->IDR >> BUTTON_BIT & 1)) == 1)
        
         // byl zwolniony, teraz jest wcisniety - zmiana stanu LED
        LED_PORT->ODR ^= 1 << GREEN_LED_BIT | 1 << BLUE_LED_BIT;
}


Jak widać, sam kod testujący przycisk jest krótszy, niż w poprzednim, błędnym przykładzie. Oczywiście musimy zaprogramować timer i oprogramować jego przerwanie, ale i tak robimy to w każdym projekcie.

Stawiając pytanie o koszt takiej techniki obsługi warto wziąć pod uwagę nie liczbę linii kodu programu, a liczbę instrukcji związanych z obsługą przycisku wykonywanych w jednostce czasu, np. w ciągu sekundy.

Jak widać rozwiązanie z przerwaniem timera  wykonuje kilka linii kodu 100 razy na sekundę; przez pozostały czas procesor może być uśpiony lub wykonywać inne zadania. Błędne rozwiązanie z testowaniem stanu przycisku w pętli zużywa cały czas procesora na testowanie stanu przycisku, drenując przy tym energię ze źródła zasilania, co często jest bardzo istotnym elementem projektu w dobie miniaturyzacji.

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.