W niniejszym artykule znajdziesz trochę podstawowej wiedzy na temat funkcji wywoływanych zwrotnie, czyli callbacków. Na kilku prostych przykładach postaram się wyjaśnić ten mechanizm oraz wskazać jego zalety i wady.
Każdy, kto już troszkę programuje, na pewno spotkał się z określeniem callback, jednak spora część unika tego typu rozwiązań jak ognia. Przyczyną takiego stanu rzeczy może być pozornie wysoki poziom skomplikowania kodu oraz deficyt opracowań upowszechniających takie konstrukcje programowe. Postaram się odrobinę przybliżyć oraz odczarować ów temat.
Aby wyjaśnić mechanizm działania callbacków na jakimś konkretnym i jednocześnie nieskomplikowanym przykładzie, zdecydowałem się wykorzystać jedną z najprostszych w użyciu rodzin mikrokontrolerów, czyli AVR. Wymaga ona od programisty minimum pracy podczas konfiguracji sprzętu. Oto uproszczony schemat układu, który wykorzystałem:
Jak widać układ jest banalnie prosty. Diody posłużą do prezentacji działania programu, natomiast przyciski staną się moim "obiektem pożądania". Na początek wrzucam do mikrokontrolera program, który wyłącznie sprawdzi poprawność podłączenia diod I przycisków do układu. Oto on:
Kod: Zaznacz cały
/*
* Sprawdzenie sprzętu.
*
*/
#include<stdbool.h> /* Zawiera definicje true i false. */
#include<avr/io.h>
#include<util/delay.h>
#define LED_DDR DDRA
#define LED_PORT PORTA
#define BTN_DDR DDRB
#define BTN_PORT PORTB
#define BTN_PIN PINB
#define LED_ALL_ON 0x00
#define LED_ALL_OFF 0xFF
void LED_Init(void);
void BTN_Init(void);
/* Punkt wejścia. */
int main(void)
{
LED_Init();
BTN_Init();
while(true)
{
/* Sprawdzam stan przycisków. Jeśli którykolwiek został wciśnięty, zaświeć
* wszystkie diody. W przeciwnym przypadku zgaś diody. */
if((BTN_PIN & 0x0F) != 0x0F)
{
LED_PORT = LED_ALL_ON;
}
else
{
LED_PORT = LED_ALL_OFF;
}
}
}
void LED_Init(void)
{
/* Cały port A jako wyjście. */
LED_DDR = 0xFF;
LED_PORT = LED_ALL_OFF;
}
void BTN_Init(void)
{
/* Włączam podciąganie do zasilania dla przycisków. */
BTN_PORT = 0x0F;
}
No dobrze, sprzęt jest sprawdzony i gotowy do dalszego użytku. A zatem do dzieła. Napiszę program, którego zadaniem będzie miganie diodami w sposób zależny od ostatnio nacisniętego przycisku. Na pierwszy ogień pójdzie jakaś prosta konstrukcja, a następnie będę ją stopniowo komplikował. Oto on:
Kod: Zaznacz cały
/*
* Podstawowa wersja programu.
*
*/
#include<stdbool.h> /* Zawiera definicje true i false. */
#include<avr/io.h>
#include<util/delay.h>
#define LED_DDR DDRA
#define LED_PORT PORTA
#define BTN_DDR DDRB
#define BTN_PORT PORTB
#define BTN_PIN PINB
#define BTN_BTN1_MASK 0x01 /* PB0 */
#define BTN_BTN2_MASK 0x02 /* PB1 */
#define BTN_BTN3_MASK 0x04 /* PB2 */
#define BTN_BTN4_MASK 0x08 /* PB3 */
#define LED_ALL_ON 0x00
#define LED_ALL_OFF 0xFF
void LED_Init(void);
void BTN_Init(void);
uint8_t BTN_Check(void);
uint8_t LED_ToggleMask;
/* Punkt wejścia. */
int main(void)
{
LED_Init();
BTN_Init();
while(true)
{
/* W pętli głównej testuję stan przycisków. Jeśli został naciśnięty
* przycisk S1, diody podłączone do PA0 i PA1 mają migać. Jeśli naciśnięto
* przycisk S2, diody podłączone do PA2 i PA3 mają migać. Jeśli naciśnięto
* przycisk S3, diody podłączone do PA4 i PA5 mają migać. Jeśli naciśnięto
* przycisk S4, diody podłączone do PA6 i PA7 mają migać. */
switch(BTN_Check())
{
case 1:
LED_ToggleMask = 0b00000011;
break;
case 2:
LED_ToggleMask = 0b00001100;
break;
case 3:
LED_ToggleMask = 0b00110000;
break;
case 4:
LED_ToggleMask = 0b11000000;
break;
default:
/* Brak reakcji. */
break;
}
/* Migam diodami zgodnie z maską wpisaną do LED_ToggleMask. */
LED_PORT ^= LED_ToggleMask;
_delay_ms(100);
}
}
void LED_Init(void)
{
/* Cały port A jako wyjście. */
LED_DDR = 0xFF;
LED_PORT = LED_ALL_OFF;
}
void BTN_Init(void)
{
/* Włączam podciąganie do zasilania dla przycisków. */
BTN_PORT = 0x0F;
}
uint8_t BTN_Check(void)
{
uint8_t Button = 0;
if( (BTN_PIN & BTN_BTN1_MASK) == 0 ) /* Stan niski na PB0 */
{
Button = 1; /* Wciśnięty przycisk S1. */
}
else if( (BTN_PIN & BTN_BTN2_MASK) == 0 ) /* Stan niski na PB1 */
{
Button = 2; /* Wciśnięty przycisk S2. */
}
else if( (BTN_PIN & BTN_BTN3_MASK) == 0 ) /* Stan niski na PB2 */
{
Button = 3; /* Wciśnięty przycisk S3. */
}
else if( (BTN_PIN & BTN_BTN4_MASK) == 0 ) /* Stan niski na PB3 */
{
Button = 4; /* Wciśnięty przycisk S4. */
}
return Button;
}
Program raczej nie powinien być trudny do analizy, ale w skrócie wyjaśnię, co on robi. Na początek inicjalizowane są porty połączone z diodami i przyciskami. PORTA działa jako wyjście, bo steruje diodami LED, natomiast cztery najmłodsze piny PORTB obsługują przyciski, a więc działają jako wejście z włączonym podciąganiem do zasilania (ang. pull up). Po inicjalizacji program wpada w pętlę główną,w której sprawdza stan przycisków funkcją BTN_Check. Wynik działania tej funkcji użyty jest w instrukcji switch. W switchu zaś modyfikowana jest zmienna LED_ToggleMask, ale pod warunkiem, że funkcja testująca klawisze zwróciła wynik 1, 2, 3 lub 4. Potem następuje mignięcie diodami zgodnie z maską, a na koniec mamy opóźnienie 100 milisekund, by miganie było widoczne gołym okiem. Program prosty, ale i brzydki. Przede wszystkim postaram się usunąć instrukcję _delay_ms(), a w zamian uruchomię jakiś sprzętowy licznik. Ok, program gotowy i wygląda następująco:
Kod: Zaznacz cały
/*
* Zamiana _delay_ms() na sprzętowy licznik.
*
*/
#include<stdbool.h> /* Zawiera definicje true i false. */
#include<avr/io.h>
#include<avr/interrupt.h>
#define CPU_FREQ F_CPU
#define TIMER0_PSC 1024
#define IRQ_FREQ 100
#define LED_DDR DDRA
#define LED_PORT PORTA
#define BTN_DDR DDRB
#define BTN_PORT PORTB
#define BTN_PIN PINB
#define BTN_BTN1_MASK 0x01 /* PB0 */
#define BTN_BTN2_MASK 0x02 /* PB1 */
#define BTN_BTN3_MASK 0x04 /* PB2 */
#define BTN_BTN4_MASK 0x08 /* PB3 */
#define LED_ALL_ON 0x00
#define LED_ALL_OFF 0xFF
void LED_Init(void);
void BTN_Init(void);
void TIMER0_Init(void);
uint8_t BTN_Check(void);
uint8_t LED_ToggleMask;
/* Punkt wejścia. */
int main(void)
{
LED_Init();
BTN_Init();
TIMER0_Init(); /* Timer będzie generował 100 przerwań na sekundę. */
sei();
while(true)
{
/* W pętli głównej testuję stan przycisków. Jeśli został naciśnięty
* przycisk S1, diody podłączone do PA0 i PA1 mają migać. Jeśli naciśnięto
* przycisk S2, diody podłączone do PA2 i PA3 mają migać. Jeśli naciśnięto
* przycisk S3, diody podłączone do PA4 i PA5 mają migać. Jeśli naciśnięto
* przycisk S4, diody podłączone do PA6 i PA7 mają migać. */
switch(BTN_Check())
{
case 1:
LED_ToggleMask = 0b00000011;
break;
case 2:
LED_ToggleMask = 0b00001100;
break;
case 3:
LED_ToggleMask = 0b00110000;
break;
case 4:
LED_ToggleMask = 0b11000000;
break;
default:
/* Brak reakcji. */
break;
}
}
}
void LED_Init(void)
{
/* Cały port A jako wyjście. */
LED_DDR = 0xFF;
LED_PORT = LED_ALL_OFF;
}
void BTN_Init(void)
{
/* Włączam podciąganie do zasilania dla przycisków. */
BTN_PORT = 0x0F;
}
void TIMER0_Init(void)
{
/* Konfiguracja zakresu pracy licznika. W moim przypadku licznik będzie zliczał
* od 0 do 107, a następnie wywoła przerwanie. */
OCR0 = (CPU_FREQ / TIMER0_PSC / IRQ_FREQ) - 1;
/* Tryb pracy CTC, preskaler 1024 */
TCCR0 = ((1 << WGM01) | (1 << CS02) | (1 << CS00));
/* Zezwalam na generowanie przerwań dla trybu CTC. */
TIMSK = (1 << OCIE0);
}
uint8_t BTN_Check(void)
{
uint8_t Button = 0;
if( (BTN_PIN & BTN_BTN1_MASK) == 0 ) /* Stan niski na PB0 */
{
Button = 1; /* Wciśnięty przycisk S1. */
}
else if( (BTN_PIN & BTN_BTN2_MASK) == 0 ) /* Stan niski na PB1 */
{
Button = 2; /* Wciśnięty przycisk S2. */
}
else if( (BTN_PIN & BTN_BTN3_MASK) == 0 ) /* Stan niski na PB2 */
{
Button = 3; /* Wciśnięty przycisk S3. */
}
else if( (BTN_PIN & BTN_BTN4_MASK) == 0 ) /* Stan niski na PB3 */
{
Button = 4; /* Wciśnięty przycisk S4. */
}
return Button;
}
ISR(TIMER0_COMP_vect)
{
/* Przerwanie wywołuje się sto razy na sekundę, a do migania diodami potrzebuję
* najwyżej dziesięć zmian na sekundę. Poniższa zmienna umożliwi mi miganie diodami
* co dziesiąte przerwanie. */
static uint8_t SlowDownTimer = 9;
if(SlowDownTimer == 0)
{
SlowDownTimer = 9;
LED_PORT ^= LED_ToggleMask;
}
SlowDownTimer--;
}
Wielkich zmian nie ma, ale... Wywaliłem _delay_ms(), uruchomiłem timer sprzętowy. Miganie diodą wrzucone jest bezpośrednio do obsługi przerwania. No dobra, ale w pętli głównej wciąż wisi mi testowanie stanu przycisków, które wykonuje się setki tysięcy razy na sekundę. Czy aby na pewno muszę tak często testować stan przycisków? Na pewno nie. Wystarczy testować przyciski sto razy na sekundę, a nawet dziesięć razy na sekundę! Wobec tego nic nie stoi na przeszkodzie, bym testowanie przycisków również przerzucił do obsługi przerwania, prawda? No dobra, to jedziemy z tym koksem... naniosłem poprawki i obecnie mój kod wygląda tak:
Kod: Zaznacz cały
/*
* Obsługa migania diodami oraz testowanie przycisków przeniesione
* do obsługi przerwania.
*
*
*/
#include<stdbool.h> /* Zawiera definicje true i false. */
#include<avr/io.h>
#include<avr/interrupt.h>
#define CPU_FREQ F_CPU
#define TIMER0_PSC 1024
#define IRQ_FREQ 100
#define LED_DDR DDRA
#define LED_PORT PORTA
#define BTN_DDR DDRB
#define BTN_PORT PORTB
#define BTN_PIN PINB
#define BTN_BTN1_MASK 0x01 /* PB0 */
#define BTN_BTN2_MASK 0x02 /* PB1 */
#define BTN_BTN3_MASK 0x04 /* PB2 */
#define BTN_BTN4_MASK 0x08 /* PB3 */
#define LED_ALL_ON 0x00
#define LED_ALL_OFF 0xFF
void LED_Init(void);
void BTN_Init(void);
void TIMER0_Init(void);
uint8_t BTN_Check(void);
volatile uint8_t LED_ToggleMask;
/* Punkt wejścia. */
int main(void)
{
LED_Init();
BTN_Init();
TIMER0_Init(); /* Timer będzie generował 100 przerwań na sekundę. */
sei();
while(true)
{
}
}
void LED_Init(void)
{
/* Cały port A jako wyjście. */
LED_DDR = 0xFF;
LED_PORT = LED_ALL_OFF;
}
void BTN_Init(void)
{
/* Włączam podciąganie do zasilania dla przycisków. */
BTN_PORT = 0x0F;
}
void TIMER0_Init(void)
{
/* Konfiguracja zakresu pracy licznika. W moim przypadku licznik będzie zliczał
* od 0 do 107, a następnie wywoła przerwanie. */
OCR0 = (CPU_FREQ / TIMER0_PSC / IRQ_FREQ) - 1;
/* Tryb pracy CTC, preskaler 1024 */
TCCR0 = ((1 << WGM01) | (1 << CS02) | (1 << CS00));
/* Zezwalam na generowanie przerwań dla trybu CTC. */
TIMSK = (1 << OCIE0);
}
uint8_t BTN_Check(void)
{
uint8_t Button = 0;
if( (BTN_PIN & BTN_BTN1_MASK) == 0 ) /* Stan niski na PB0 */
{
Button = 1; /* Wciśnięty przycisk S1. */
}
else if( (BTN_PIN & BTN_BTN2_MASK) == 0 ) /* Stan niski na PB1 */
{
Button = 2; /* Wciśnięty przycisk S2. */
}
else if( (BTN_PIN & BTN_BTN3_MASK) == 0 ) /* Stan niski na PB2 */
{
Button = 3; /* Wciśnięty przycisk S3. */
}
else if( (BTN_PIN & BTN_BTN4_MASK) == 0 ) /* Stan niski na PB3 */
{
Button = 4; /* Wciśnięty przycisk S4. */
}
return Button;
}
ISR(TIMER0_COMP_vect)
{
/* Przerwanie wywołuje się sto razy na sekundę, a do migania diodami potrzebuję
* najwyżej dziesięć zmian na sekundę. Poniższa zmienna umożliwi mi miganie diodami
* co dziesiąte przerwanie. */
static uint8_t SlowDownTimer = 9;
if(SlowDownTimer == 0)
{
SlowDownTimer = 9;
switch(BTN_Check())
{
case 1:
LED_ToggleMask = 0b00000011;
break;
case 2:
LED_ToggleMask = 0b00001100;
break;
case 3:
LED_ToggleMask = 0b00110000;
break;
case 4:
LED_ToggleMask = 0b11000000;
break;
default:
/* Brak reakcji. */
break;
}
LED_PORT ^= LED_ToggleMask;
}
SlowDownTimer--;
}
Program wciąż działa tak samo mimo pustej pętli głównej. Patrzę na kod i myślę sobie: Ok, ale fajnie by było, żeby mój kawałek kodu, ten testujący przyciski, potrafił wywoływać jakąś funkcję, bo póki co zwraca jedynie numer naciśniętego przycisku. Spróbuję zatem stworzyć jakąś funkcję, która będzie wywoływana za każdym razem, gdy zostanie wciśnięty przycisk. Zmiana jest czysto kosmetyczna i obecnie wygląda to tak:
Kod: Zaznacz cały
/*
* Dodanie funkcji wywoływanej po każdym naciśnięciu przycisku.
*/
#include<stdbool.h> /* Zawiera definicje true i false. */
#include<avr/io.h>
#include<avr/interrupt.h>
#define CPU_FREQ F_CPU
#define TIMER0_PSC 1024
#define IRQ_FREQ 100
#define LED_DDR DDRA
#define LED_PORT PORTA
#define BTN_DDR DDRB
#define BTN_PORT PORTB
#define BTN_PIN PINB
#define BTN_BTN1_MASK 0x01 /* PB0 */
#define BTN_BTN2_MASK 0x02 /* PB1 */
#define BTN_BTN3_MASK 0x04 /* PB2 */
#define BTN_BTN4_MASK 0x08 /* PB3 */
#define LED_ALL_ON 0x00
#define LED_ALL_OFF 0xFF
void LED_Init(void);
void BTN_Init(void);
void TIMER0_Init(void);
uint8_t BTN_Check(void);
void ChangeToggleMask(uint8_t NewMask);
volatile uint8_t LED_ToggleMask;
/* Punkt wejścia. */
int main(void)
{
LED_Init();
BTN_Init();
TIMER0_Init(); /* Timer będzie generował 100 przerwań na sekundę. */
sei();
while(true)
{
}
}
void LED_Init(void)
{
/* Cały port A jako wyjście. */
LED_DDR = 0xFF;
LED_PORT = LED_ALL_OFF;
}
void BTN_Init(void)
{
/* Włączam podciąganie do zasilania dla przycisków. */
BTN_PORT = 0x0F;
}
void TIMER0_Init(void)
{
/* Konfiguracja zakresu pracy licznika. W moim przypadku licznik będzie zliczał
* od 0 do 107, a następnie wywoła przerwanie. */
OCR0 = (CPU_FREQ / TIMER0_PSC / IRQ_FREQ) - 1;
/* Tryb pracy CTC, preskaler 1024 */
TCCR0 = ((1 << WGM01) | (1 << CS02) | (1 << CS00));
/* Zezwalam na generowanie przerwań dla trybu CTC. */
TIMSK = (1 << OCIE0);
}
uint8_t BTN_Check(void)
{
uint8_t Button = 0;
if( (BTN_PIN & BTN_BTN1_MASK) == 0 ) /* Stan niski na PB0 */
{
Button = 1; /* Wciśnięty przycisk S1. */
}
else if( (BTN_PIN & BTN_BTN2_MASK) == 0 ) /* Stan niski na PB1 */
{
Button = 2; /* Wciśnięty przycisk S2. */
}
else if( (BTN_PIN & BTN_BTN3_MASK) == 0 ) /* Stan niski na PB2 */
{
Button = 3; /* Wciśnięty przycisk S3. */
}
else if( (BTN_PIN & BTN_BTN4_MASK) == 0 ) /* Stan niski na PB3 */
{
Button = 4; /* Wciśnięty przycisk S4. */
}
return Button;
}
void ChangeToggleMask(uint8_t NewMask)
{
LED_ToggleMask = NewMask;
}
ISR(TIMER0_COMP_vect)
{
/* Przerwanie wywołuje się sto razy na sekundę, a do migania diodami potrzebuję
* najwyżej dziesięć zmian na sekundę. Poniższa zmienna umożliwi mi miganie diodami
* co dziesiąte przerwanie. */
static uint8_t SlowDownTimer = 9;
if(SlowDownTimer == 0)
{
SlowDownTimer = 9;
switch(BTN_Check())
{
case 1:
ChangeToggleMask(0b00000011);
break;
case 2:
ChangeToggleMask(0b00001100);
break;
case 3:
ChangeToggleMask(0b00110000);
break;
case 4:
ChangeToggleMask(0b11000000);
break;
default:
/* Brak reakcji. */
break;
}
LED_PORT ^= LED_ToggleMask;
}
SlowDownTimer--;
}
Program wciąż działa tak samo, ale zaczynam odnosić wrażenie, że w kodzie robi się bałagan. Nic dziwnego, bo zgodnie z ogólnie przyjętymi zasadami poprawnego programowania powinienem już program podzielić na pliki. Zaczyna pachnieć callbackami, ale o tym za kilka chwil. Najpierw podział na pliki. Na początek stworzę folder o nazwie button, a w nim dwa pliki: button.c oraz button.h. Zrobię z tego moduł obsługujący przyciski. No to do roboty, bo tym razem czeka mnie troszkę więcej pisania i zmian. Uff, nawet sprawnie poszło. W tej chwili plik main.c wygląda tak:
Kod: Zaznacz cały
/*
* Podział programu na pliki celem odzyskania czytelności.
*/
#include<stdbool.h> /* Zawiera definicje true i false. */
#include<avr/io.h>
#include<avr/interrupt.h>
#include"button/button.h"
#define CPU_FREQ F_CPU
#define TIMER0_PSC 1024
#define IRQ_FREQ 100
#define LED_DDR DDRA
#define LED_PORT PORTA
#define LED_ALL_ON 0x00
#define LED_ALL_OFF 0xFF
void LED_Init(void);
void TIMER0_Init(void);
void ChangeToggleMask(uint8_t NewMask);
volatile uint8_t LED_ToggleMask;
/* Punkt wejścia. */
int main(void)
{
LED_Init();
BTN_Init();
TIMER0_Init(); /* Timer będzie generował 100 przerwań na sekundę. */
sei();
while(true)
{
switch(BTN_PressedButtonNumber)
{
case 1:
ChangeToggleMask(0b00000011);
break;
case 2:
ChangeToggleMask(0b00001100);
break;
case 3:
ChangeToggleMask(0b00110000);
break;
case 4:
ChangeToggleMask(0b11000000);
break;
default:
/* Brak reakcji. */
break;
}
}
}
void LED_Init(void)
{
/* Cały port A jako wyjście. */
LED_DDR = 0xFF;
LED_PORT = LED_ALL_OFF;
}
void TIMER0_Init(void)
{
/* Konfiguracja zakresu pracy licznika. W moim przypadku licznik będzie zliczał
* od 0 do 107, a następnie wywoła przerwanie. */
OCR0 = (CPU_FREQ / TIMER0_PSC / IRQ_FREQ) - 1;
/* Tryb pracy CTC, preskaler 1024 */
TCCR0 = ((1 << WGM01) | (1 << CS02) | (1 << CS00));
/* Zezwalam na generowanie przerwań dla trybu CTC. */
TIMSK = (1 << OCIE0);
}
void ChangeToggleMask(uint8_t NewMask)
{
LED_ToggleMask = NewMask;
}
ISR(TIMER0_COMP_vect)
{
/* Przerwanie wywołuje się sto razy na sekundę, a do migania diodami potrzebuję
* najwyżej dziesięć zmian na sekundę. Poniższa zmienna umożliwi mi miganie diodami
* co dziesiąte przerwanie. */
static uint8_t SlowDownTimer = 9;
/* Wywołanie funkcji testującej przyciski. Jej wynik zostanie zapisany w
* zmiennej BTN_PressedButtonNumber. */
BTN_IrqFunc();
/* Obsługa migania diodami. */
if(SlowDownTimer == 0)
{
SlowDownTimer = 9;
LED_PORT ^= LED_ToggleMask;
}
SlowDownTimer--;
}
Wszystko, co się tyczyło przycisków, wyrzuciłem do osobnego pliku, czyli button.c i od razu widać, że jest przejrzyście. Niestety nie obyło się bez małych powrotów do przeszłości, czyli testowanie zmiennej BTN_PressedButtonNumber w pętli głównej oraz fakt wystawienia tej zmiennej poza moduł, co jest pewnym naruszeniem technik poprawnego programowania. Skoro jednak już jestem przy podziale na pliki, zdecyduję się na wyrzucenie elementów sterujących diodami do osobnego modułu (led.c i led.h). Za moment wracam…
(15 minut później…)
No dobra, w kodzie nastał porządek. Fragmenty kodu dotyczące ledów wylądowały w folderze led, a te dotyczące przycisków w folderze button. Nie będę ich tu wszystkich listował, pokażę jedynie main.c:
Kod: Zaznacz cały
#include<stdbool.h> /* Zawiera definicje true i false. */
#include<avr/io.h>
#include<avr/interrupt.h>
#include"button/button.h"
#include"led/led.h"
#define CPU_FREQ F_CPU
#define TIMER0_PSC 1024
#define IRQ_FREQ 100
void TIMER0_Init(void);
/* Punkt wejścia. */
int main(void)
{
LED_Init();
BTN_Init();
TIMER0_Init(); /* Timer będzie generował 100 przerwań na sekundę. */
sei();
while(true)
{
switch(BTN_PressedButtonNumber)
{
case 1:
LED_ChangeToggleMask(0b00000011);
break;
case 2:
LED_ChangeToggleMask(0b00001100);
break;
case 3:
LED_ChangeToggleMask(0b00110000);
break;
case 4:
LED_ChangeToggleMask(0b11000000);
break;
default:
/* Brak reakcji. */
break;
}
}
}
void TIMER0_Init(void)
{
/* Konfiguracja zakresu pracy licznika. W moim przypadku licznik będzie zliczał
* od 0 do 107, a następnie wywoła przerwanie. */
OCR0 = (CPU_FREQ / TIMER0_PSC / IRQ_FREQ) - 1;
/* Tryb pracy CTC, preskaler 1024 */
TCCR0 = ((1 << WGM01) | (1 << CS02) | (1 << CS00));
/* Zezwalam na generowanie przerwań dla trybu CTC. */
TIMSK = (1 << OCIE0);
}
ISR(TIMER0_COMP_vect)
{
/* Wywołanie funkcji testującej przyciski. Jej wynik zostanie zapisany w
* zmiennej BTN_PressedButtonNumber. */
BTN_IrqFunc();
/* Wywołanie funkcji sterującej miganiem diod LED. */
LED_IrqFunc();
}
Program wygląda coraz lepiej, ale razi mnie ten paskudny switch w pętli głównej. Nie można tego zrobić jakoś inaczej? Ależ można! Najpierw jednak objaśnię to i owo. Na początku programu inicjalizuję port ledów oraz przycisków, konfiguruję timer0 oraz włączam przerwania. Tutaj nic się nie zmieniło. W przerwaniu, czyli sto razy na sekundę, wywoływane są dwie nowopowstałe funkcje. Jedna z nich bada stan przycisków i zapisuje wynik w zmiennej, a druga obsługuje miganie diodami zgodnie z maską zawartą w zmiennej LED_ToggleMask. Taka konstrukcja zapewnia większą szczelność kodu, czyli mniej zmiennych o łączności (widoczności) przekraczającej jedną jednostkę translacji (w uproszczeniu chodzi o moduł). Niestety, jedna zmienna nadal posiada łączność zewnętrzną… Ta zmienna to BTN_PressedButtonNumber. Jak zatem sprawdzać numer naciśniętego przycisku bez posiadania dostępu do zmiennej? Po chwili zastanowienia dochodzę do wniosku, że wcale nie muszę mieć do niej dostępu. Niech się numerem przycisku martwi funkcja obsługująca przyciski. Niech to będzie funkcja, do której zostanie bezpośrednio przekazany numer przycisku. Tak, tutaj zaczyna się zabawa. Nazwę tę funkcję „MyCallback” i tak wszystko skonstruuję, by ona mogła przyjąć numer wciśniętego przycisku. Mało tego, będzie ona wywoływana wyłącznie wtedy, gdy ten przycisk faktycznie jest wciśnięty. Uff, nareszcie coś na temat... Ok, do dzieła.
Na początek stworzę nowy typ, który będzie trochę przypominał deklarację funkcji.
Kod: Zaznacz cały
typedef void (BTN_CallbackType)(uint8_t);
Należy to czytać mniej więcej tak: Ten typ nosi nazwę BTN_CallbackType, przyjmuje jeden argument typu uint8_t oraz niczego nie zwraca. Jeśli stworzę wskaźnik na taki typ, będę mógł zapisać w nim adres dowolnej funkcji o takiej konstrukcji: void jakaś_moja_funkcja(uint8_t jakiś_parametr_przekazywany_do_tej_funkcji). No dobra, stworzę więc taki wskaźnik i od razu funkcję, która będzie mi umożliwiała zapisywanie adresu funkcji w tym wskaźniku. Oto wskaźnik:
Kod: Zaznacz cały
BTN_CallbackType *MyCallbackPointer;
A oto funkcja, która potrafi zapisać adres w powyższym wskaźniku:
Kod: Zaznacz cały
void BTN_SetCallback(BTN_CallbackType *callback)
{
MyCallbackPointer = callback;
}
Tę funkcję umieszczam w module button i dbam o to, by była widoczna w innych częściach programu. Teraz wracam do pliku main.c i tam napiszę funkcję, która będzie dysponowała numerem wciśniętego klawisza. Następnie nazwę tej funkcji (czyli, de facto, jej adres) przekażę do funkcji BTN_SetCallback. Będzie to wyglądało tak:
Kod: Zaznacz cały
BTN_SetCallback(MyCallback);
A sama funkcja "MyCallback" wygląda nastepująco:
Kod: Zaznacz cały
void MyCallback(uint8_t Button)
{
switch(Button)
{
case 1:
LED_ChangeToggleMask(0b00000011);
break;
case 2:
LED_ChangeToggleMask(0b00001100);
break;
case 3:
LED_ChangeToggleMask(0b00110000);
break;
case 4:
LED_ChangeToggleMask(0b11000000);
break;
default:
/* Brak reakcji. */
break;
}
}
Jak to działa? Na początku programu mamy inicjalizację sprzętu, czyli diody, przyciski i timer. Oczywiście nie brakuje włączenia przerwań, ale chcę się skupić na nowości, czyli przekazaniu adresu mojego callback'a. Funkcja BTN_SetCallback() ma tylko jedno zadanie: Przekazać do modułu button adres funkcji użytkownika, którą ów moduł będzie musiał wywoływać po każdym stwierdzeniu naciśnięcia przycisku. A co robi funkcja użytkownika? Zmienia maskę, wedle której następuje miganie diodami. Tak więc program nadal robi to samo, czyli miga diodami, a sposób migania wybierany jest jednym z czterech przycisków. Ok, ale wróćmy do kodu. Po inicjalizacji sprzętu, rejestracji callbacka i włączeniu przerwań natrafiamy na pustą pętlę główną. Tak akurat wyszło, ale można ten program inaczej skonstruować. Nie chciałem go jednak nadmiernie komplikować.
W momencie wystąpienia przerwania program porzuca pustą pętlę główną i skacze do podprogramu obsługi przerwania. Tam napotyka na dwie funkcje: Jedna pochodzi z modułu button, a druga z modułu led. Wskakuje do pierwszej i sprawdza przyciski. Wynik testu zapisuje w zmiennej. Jeśli wartość zmiennej wskazuje na to, że jakiś przycisk został wciśnięty, sprawdzany jest stan wskaźnika przetrzymującego adres funkcji typu callback. Jesli wskaźnik nie jest pusty, następuje skok pod zapisany adres oraz przekazanie numeru wciśniętego przycisku. Ten skok to tak naprawdę wywołanie funkcji "MyCallback". Po zakończeniu działania funkcji program wraca do przerwania, kasuje numer klawisza w zmiennej (choć to tak naprawdę nie jest potrzebne w tym programie) i wychodzi z funkcji obsługującej klawisze. Potem wskakuje do funkcji obsługującej diody, a tam radośnie sobie mignie, o ile programowy timerek na to pozwoli. No i tyle...
Co mi daje taka konstrukcja? Przede wszystkim pozbyłem się zmiennej BTN_PressedButtonNumber o łączności zewnętrznej, czyli uszczelniłem moduł button. Kolejna zaleta to możliwość podłączania dowolnej funkcji w dowolnym momencie działania programu, która obsłuży fakt (zdarzenie, ang. Event) wciśnięcia przycisku. Jeden przycisk może mieć wiele znaczeń zaleznie np. od treści wyświetlanej na wyświetlaczu. Czasem może działać jako zwiększanie głośności, a czasem jako zwiększanie nasycenia, jaskrawości lub kontrastu. Ok, czas na jakies wnioski.
Korzyści płynące z wykorzystywania funkcji typu callback:
- Możliwość łatwego uszczelniania kodu,
- Możliwość żonglowania reakcją na naciśnięcie tego samego przycisku, a nawet jego wyłączenia,
- Duża przejrzystość programu przy odpowiednio dobranych nazwach funkcji.
Wady takiego rozwiązania:
- Nieco wyższy stopień skomplikowania kodu,
- Nieco więcej straconego czasu na jego napisanie,
- Zwiększona objętość kodu wynikowego (stopień spuchnięcia programu zależny jest od kompilatora oraz jego ustawień),
- Odrobinę zwiększony czas wykonywania kodu spowodowany skokami do callbacka. W przypadku, gdy czas reakcji na wciśnięcie przycisku jest sprawą krytyczną, warto rozważyć zastosowanie rozwiązania pozbawionego rejestracji funkcji oraz każdorazowego testowania wskaźnika na funkcję tuż przed skorzystaniem z niego. W praktyce oznacza to całkowite zrezygnowanie z callbacka na rzecz bezpośredniego wykonywania szeregu instrukcji.
No dobra, na dziś tyle... Gdyby były jakieś pytania, walcie śmiało. Postaram się dopowiedzieć wszystko to, co udało mi się przeoczyć.
Pozdrawiam!
PS. Wrzucam cały workspace z poszczególnymi fazami tworzenia programu.