Od zera do bohatera, czyli callbacki dla (t)opornych.

Tu możesz pisać o swoich problemach z pisaniem programów w języku C dla AVR.
Awatar użytkownika
Antystatyczny
Geek
Geek
Posty: 1080
Rejestracja: czwartek 03 wrz 2015, 22:02

Od zera do bohatera, czyli callbacki dla (t)opornych.

Postautor: Antystatyczny » sobota 27 sie 2016, 18:48

Witam serdecznie.

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:


Poradnik_callbacki.png



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.
AVR8_callbacks.7z
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.
"The true sign of intelligence is not knowledge but imagination" Albert Einstein.

Awatar użytkownika
matty24
User
User
Posty: 298
Rejestracja: sobota 31 paź 2015, 20:11
Lokalizacja: Małopolska

Re: Od zera do bohatera, czyli callbacki dla (t)opornych.

Postautor: matty24 » niedziela 28 sie 2016, 00:17

Bardzo dobry poradnik. Na garści drobnicy można się nauczyć "lepszej" techniki programowania. To jest dobra baza do dalszej nauki i próby wykorzystania callbacków do czegoś bardziej zaawansowanego. Dla mnie plusem jest dodanie do workspace wszystkich etapów pisania programu i wykorzystanie popularnego uC. Do tego dobrze opisane kolejne kroki wykonywania programu. Polecam każdemu przetestować bo to nie jest takie trudne na jakie wygląda.

Awatar użytkownika
Antystatyczny
Geek
Geek
Posty: 1080
Rejestracja: czwartek 03 wrz 2015, 22:02

Re: Od zera do bohatera, czyli callbacki dla (t)opornych.

Postautor: Antystatyczny » niedziela 28 sie 2016, 00:19

Cieszę się, że Ci się przydaje. Gdybyś jednak znalazł jakieś niedociągnięcia, nieścisłości, pytaj i wskazuj śmiało. Jeśli potrzebujesz innych przykładów, proś, a będzie Ci dane. :)
"The true sign of intelligence is not knowledge but imagination" Albert Einstein.

Awatar użytkownika
j23
User
User
Posty: 345
Rejestracja: czwartek 08 paź 2015, 18:40

Re: Od zera do bohatera, czyli callbacki dla (t)opornych.

Postautor: j23 » niedziela 28 sie 2016, 01:07

Świetny poradnik o funkcjach typu callback dla języka C ! :) Wielkie dzięki. :)
Pozdrawiam! j23

Awatar użytkownika
Antystatyczny
Geek
Geek
Posty: 1080
Rejestracja: czwartek 03 wrz 2015, 22:02

Re: Od zera do bohatera, czyli callbacki dla (t)opornych.

Postautor: Antystatyczny » niedziela 28 sie 2016, 01:09

Dzięki za dobre słowo. W przygotowaniu są dwa kolejne artykuły. Mam cichą nadzieję, że komuś się przydadzą :)

Pozdrawiam!
"The true sign of intelligence is not knowledge but imagination" Albert Einstein.

Awatar użytkownika
inż.wielki
User
User
Posty: 216
Rejestracja: niedziela 20 gru 2015, 23:11

Re: Od zera do bohatera, czyli callbacki dla (t)opornych.

Postautor: inż.wielki » poniedziałek 29 sie 2016, 08:09

A mnie zastanawia, czy pamiętasz już dokładnie jak robi się callbacki czy posiłkujesz się jakimiś materiałami. Nie ukrywam że ja często wykorzystuje właśnie callbacki, ale nie zawsze pamiętam jak je zapisać w kodzie :D
Ale artykuł na fajf plus.

Awatar użytkownika
Antystatyczny
Geek
Geek
Posty: 1080
Rejestracja: czwartek 03 wrz 2015, 22:02

Re: Od zera do bohatera, czyli callbacki dla (t)opornych.

Postautor: Antystatyczny » poniedziałek 29 sie 2016, 10:17

wielki pisze:A mnie zastanawia, czy pamiętasz już dokładnie jak robi się callbacki czy posiłkujesz się jakimiś materiałami


Przy odrobinie wprawy nie trzeba zaglądać do żadnych materiałów. Spoglądasz na problem programistyczny, wymyślasz sobie sposób z callbackami, a następnie zamykasz oczy i widzisz cały cykl pracy programu. Tak, pisałem z głowy...

rezasurmar pisze: Pewnie wyszła moja niewiedza w temacie, ale program się dziwnie zachowywał, jak zbyt dużo zdarzeń przylatywało, albo nakładało się 2-3 różne zdarzenia (od różnych callbacków). Mówię tu oczywiście o programie na AVR


Wszystko zależy od wymagań stawianych programowi oraz konstrukcji programu. Jeśli callbacki wywołujemy bezpośrednio z przerwania, możemy sobie strzelić w kolano, jeśli nie zadbamy o zwarty i szybki kod w callbacku. Z drugiej strony trudno oczekiwać od każdego obcego użytkownika, że napisze super callback. Oczywiście można się częściowo przed takim scenariuszem zabezpieczyć i zdarzenia obsługiwać już poza przerwaniem, ale i tak należy zadbać o to, by zostało ono obsłużone zanim wystąpi kolejne zdarzenie.
"The true sign of intelligence is not knowledge but imagination" Albert Einstein.

Awatar użytkownika
dambo
Expert
Expert
Posty: 592
Rejestracja: czwartek 17 mar 2016, 17:12

Re: Od zera do bohatera, czyli callbacki dla (t)opornych.

Postautor: dambo » niedziela 05 lut 2017, 14:22

Do callbacków podchodziłem już kilka razy, tym razem zostanę na dłużej :)

Jedna uwaga co do tego poradnika - można odnieść wrażenie, że dąży się tu do całkowitego wyczyszczenia pętli głównej "Program wygląda coraz lepiej, ale razi mnie ten paskudny switch w pętli głównej.", tu jest ładnie wyczyszczona, bo obsługę funkcji od przycisków można bez problemu dać w przerwaniu. W przypadku jakiś większych funkcji - lepiej zapalać flagę eventu i sprawdzać go w pętli głównej (oczywiście odpowiednią funkcją bez podciągania tej zmiennej do maina - sam kiedyś tak robiłem :/ ).
Zapraszam na mojego pseudobloga z projektami itp: http://projektydmb.blogspot.com/

Awatar użytkownika
Antystatyczny
Geek
Geek
Posty: 1080
Rejestracja: czwartek 03 wrz 2015, 22:02

Re: Od zera do bohatera, czyli callbacki dla (t)opornych.

Postautor: Antystatyczny » niedziela 05 lut 2017, 14:30

Tak, to prawda, można odnieść takie wrażenie. Jest to jedna z metod pisania programów. Oczywiście nie ma co nadmiernie zagracać przerwań i callbacków, bo nagle się okaże, że w którymś miejscu program zaczyna się przytykać. Na ogół z modułu obsługującego jakiś układ czy grupę funkcji wystawiam jeszcze jedną funkcję, która sprawdza flagę i w razie czego woła odpowiedni (zazwyczaj uprzednio zarejestrowany) callback. No i tutaj wywołanie następuje już z pętli głównej niejako (z wnętrza funkcji testującej flagę, a sama funkcja jest w pętli głównej). A co do zmiennych "podciąganych do maina", czyli tych o widoczności zewnętrznej... No cóż, też tak robiłem. Warto jednak hermetyzować moduły, by w przyszłości nie mieć problemów np. z powtórnie użytą nazwą dla nowej zmiennej.
"The true sign of intelligence is not knowledge but imagination" Albert Einstein.

Awatar użytkownika
dambo
Expert
Expert
Posty: 592
Rejestracja: czwartek 17 mar 2016, 17:12

Re: Od zera do bohatera, czyli callbacki dla (t)opornych.

Postautor: dambo » piątek 17 mar 2017, 14:18

Nie wiem, czy dać to jako nowy wątek, bardzo pasuje tutaj - fajny opis zmiany podejścia switch-case na callbacki: http://codeandlife.com/2013/10/06/tutor ... callbacks/
Zapraszam na mojego pseudobloga z projektami itp: http://projektydmb.blogspot.com/

Awatar użytkownika
mokrowski
User
User
Posty: 144
Rejestracja: czwartek 08 paź 2015, 20:50
Lokalizacja: Tam gdzie Centymetro

Re: Od zera do bohatera, czyli callbacki dla (t)opornych.

Postautor: mokrowski » piątek 17 mar 2017, 14:52

A jeszcze lepsze jest przygotowanie tablicy/tablic przejść dla danych stanów bo masz jednocześnie dokumentację poprawności implementacji pracy całej maszyny. Instrukcje switch/case były i zawsze będą słabym sposobem na zapisanie wyborów ;-/
,,Myślenie nie jest łatwe, ale można się do niego przyzwyczaić" - Alan Alexander Milne: Kubuś Puchatek

Awatar użytkownika
dambo
Expert
Expert
Posty: 592
Rejestracja: czwartek 17 mar 2016, 17:12

Re: Od zera do bohatera, czyli callbacki dla (t)opornych.

Postautor: dambo » piątek 17 mar 2017, 16:37

Tablic przejść - o masz przez to na myśli? Taki diagram jak się robiło na matematyce dyskretnej/VHDLu dla automatów?
Zapraszam na mojego pseudobloga z projektami itp: http://projektydmb.blogspot.com/

Awatar użytkownika
mokrowski
User
User
Posty: 144
Rejestracja: czwartek 08 paź 2015, 20:50
Lokalizacja: Tam gdzie Centymetro

Re: Od zera do bohatera, czyli callbacki dla (t)opornych.

Postautor: mokrowski » sobota 18 mar 2017, 18:01

Bardzo podobny diagram. Pierwszy z brzegu przykład https://www.michalwolski.pl/diagramy-um ... -stanowej/
Implementacja jest stosunkowo prosta a umożliwia łatwą weryfikację poprawności i zapewnienie poprawności działania.
,,Myślenie nie jest łatwe, ale można się do niego przyzwyczaić" - Alan Alexander Milne: Kubuś Puchatek


Wróć do „Programowanie AVR w C”

Kto jest online

Użytkownicy przeglądający to forum: Obecnie na forum nie ma żadnego zarejestrowanego użytkownika i 1 gość