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.