Od zera do bohatera, czyli callbacki dla (t)opornych w C++

Tu poruszamy tematy związane z pisaniem programów w języku C++ dla AVR.
Awatar użytkownika
mokrowski
User
User
Posty: 190
Rejestracja: czwartek 08 paź 2015, 20:50
Lokalizacja: Tam gdzie Centymetro

Od zera do bohatera, czyli callbacki dla (t)opornych w C++

Postautor: mokrowski » sobota 11 lut 2017, 23:59

Dzień dobry…

Po krótkiej dyskusji z Antystatycznym, wpadliśmy na pomysł że warto zaprezentować to samo zagadnienie implementowane w C jak i w C++. Pozwoli to Ci wyrobić sobie zdanie o przydatności C++ w zastosowaniach do budowania oprogramowania na platformę AVR.
Znajdziesz tu trochę wiedzy podstawowej jak i tej bardziej zaawansowanej i mam nadzieję zapoznasz się ze smaczkami implementacji wywołań zwrotnych (ang. callback) w języku C++. Niejako awansem, zaprezentuję także podział na klasy i budowanie pełnego projektu. Jeśli o chodzi o konfigurację sprzętu oraz przykłady kolejnych etapów w C, to odsyłam do artykułu Antystatycznego: Od zera do bohatera, czyli callbacki dla (t)opornych w C. Ten artykuł należy czytać w odniesieniu także do projektów które Anty umieścił.

Na początek, banalnie prosty przykład. Tu zobaczysz jedynie podział kodu na klasy. Są to klasy z metodami statycznymi bo nie będzie potrzebna instancja wielu przycisków czy diod. W całości artykułu pozostawię także makra choć nie ukrywam że te które będą utrudniały czytanie usunę.
No to do kodu:

Kod: Zaznacz cały

#include <avr/io.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

struct LED {
   static void init() {
      LED_DDR = 0xFF;
      LED::led_off();
   }
   static void led_on() {
      LED_PORT = LED_ALL_ON;
   }
   static void led_off() {
      LED_PORT = LED_ALL_OFF;
   }
};

struct BTN {
   static void init() {
      BTN_PORT = 0x0F;
   }
   static bool any_pressed() {
      return (BTN_PIN & 0x0F) != 0x0F;
   }
};

/* Punkt wejścia. */
int main() {
   LED::init();
   BTN::init();

   for (;;) {
      /* Sprawdzam stan przycisków. Jeśli którykolwiek został wciśnięty, zaświeć
       * wszystkie diody. W przeciwnym przypadku zgaś diody. */

      if (BTN::any_pressed()) {
         LED::led_on();
      } else {
         LED::led_off();
      }
   }
}

Kod kompilujemy w trybie C++14 i uruchamiamy w sposób tradycyjny :-) Jeśli działa (tak jak opisał to Anty) , to przechodzimy dalej.

Dalsze prace to napisanie programu obsługującego kilka przycisków. Zwróć uwagę że w porównaniu z rozwiązaniem Antystatycznego, przeniosłem logikę włączania i wyłączania led’ów do metody LED::toggle_mask(…). Poprawiłem także nieco logikę aby nie było tylu makr z maskami. Można je po prostu policzyć i zastosować bezpośrednio do portu. W porównaniu do rozwiązania w C, powoduje to większą hermetyzację kodu i myślę że polepsza logikę. Metoda toggle_mask(..) to przecież zmiana stanu led.
Podobnie postąpiłem z przyciskami. metoda BTN::check(..) zwraca indeks wciśniętego klawisza lub 0 jeśli nie został wciśnięty żaden.
Zwróć uwagę jak zmieniła się zawartość main(). Pozostała jedynie inicjalizacja oraz pętla ze zmianą stanu diod w zależności od stanu przycisków.
Jeszcze wiele zmian przed nami. Oto stan prac:

Kod: Zaznacz cały

#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

struct LED {
   static void init() {
      LED_DDR = 0xFF;
      LED::led_off();
   }
   static void led_on() {
      LED_PORT = LED_ALL_ON;
   }
   static void led_off() {
      LED_PORT = LED_ALL_OFF;
   }
   static void toggle_mask(uint8_t mask_index) {
      /*
       * Diody podłączone i obsługiwane z maską: 0b00000011, 0b00001100, 0b00110000, 0b11000000
       * Jeśli mask_index == 0, nie należy nic zmieniać na porcie.
       * Jeśli mask_index w zakresie [1, 4], należy przesujnąć 3'kę bo 3 == 0b00000011 :-)
       * mask_index to wciśnięty przycisk (0 -> żaden)
      */
      uint8_t mask = 3;
      if((mask_index > 0) and (mask_index <= 4)) {
         mask <<= ((mask_index - 1) << 1);
         LED_PORT ^= mask;
      }
   }
};

struct BTN {
   static void init() {
      BTN_PORT = 0x0F;
   }
   static bool any_pressed() {
      return (BTN_PIN & 0x0F) != 0x0F;
   }
   static uint8_t check() {
      uint8_t button{};
      /* Sprawdzenie przycisków które są pod odpowiednimi bitami maski.
       * button == 1 -> 0x00000001;
       * button == 2 -> 0x00000010;
       * ...
       * Iterujemy po maskach i sprawdzamy stan niski, stąd not w warunku if
       * Jeśli nie wciśnieto niczego, zwracamy 0'ro.
       */
      for (uint8_t mask = 0x01; mask < 0x10; mask <<= 1) {
         if (not (BTN_PIN & mask)) {
            return ++button;
         }
         ++button;
      }
      return 0;
   }
};

int main(void) {
   LED::init();
   BTN::init();

   for(;;) {
      LED::toggle_mask(BTN::check());
      _delay_ms(100);
   }
}

W następnej części dodałem jak w pierwotnym artykule obsługę timer’a. Doszła więc klasa TIMER oraz obsługa przerwań która przejęła główne wywołanie z pętli poprzedniego przykładu. Teraz pętla programu nie zawiera już nic. Cała logika jest wywoływana w przerwaniu.
Oto stan projektu:

Kod: Zaznacz cały

#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 LED_ALL_ON      0x00
#define LED_ALL_OFF      0xFF

struct LED {
   static void init() {
      LED_DDR = 0xFF;
      LED::led_off();
   }
   static void led_on() {
      LED_PORT = LED_ALL_ON;
   }
   static void led_off() {
      LED_PORT = LED_ALL_OFF;
   }
   static void toggle_mask(uint8_t mask_index) {
      /*
       * Diody podłączone i obsługiwane z maską: 0b00000011, 0b00001100, 0b00110000, 0b11000000
       * Jeśli mask_index == 0, nie należy nic zmieniać na porcie.
       * Jeśli mask_index w zakresie [1, 4], należy przesujnąć 3'kę bo 3 == 0b00000011 :-)
       * mask_index to wciśnięty przycisk (0 -> żaden)
       */
      uint8_t mask = 3;
      if ((mask_index > 0) and (mask_index <= 4)) {
         mask <<= ((mask_index - 1) << 1);
         LED_PORT ^= mask;
      }
   }
};

struct BTN {
   static void init() {
      BTN_PORT = 0x0F;
   }
   static bool any_pressed() {
      return (BTN_PIN & 0x0F) != 0x0F;
   }
   static uint8_t check() {
      uint8_t button { };
      /* Sprawdzenie przycisków które są pod odpowiednimi bitami maski.
       * button == 1 -> 0x00000001;
       * button == 2 -> 0x00000010;
       * ...
       * Iterujemy po maskach i sprawdzamy stan niski, stąd not w warunku if
       * Jeśli nie wciśnieto niczego, zwracamy 0'ro.
       */
      for (uint8_t mask = 0x01; mask < 0x10; mask <<= 1) {
         if (not (BTN_PIN & mask)) {
            return ++button;
         }
         ++button;
      }
      return 0;
   }
};

struct TIMER {
   static void init() {
      /* 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);
   }
};

/* Punkt wejścia. */
int main(void) {
   LED::init();
   BTN::init();
   TIMER::init(); /* Timer będzie generował 100 przerwań na sekundę. */
   sei();

   for(;;) {
      ;
   }
}

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 { };

   // Dość śmieszna sztuczka oszczędzająca 2-4 bajty na AVR :-)
   if ((SlowDownTimer++ == 10) and (SlowDownTimer = 0, true)) {
      LED::toggle_mask(BTN::check());
   }
}

Następny projekt, to podział na pliki i modularyzacja. Zawartość main.cpp skurczyła się bardzo. Zdecydowałem się także zmienić nazwy klas na zgodne z konwencją Camel Notation a nie nazwami przypominającymi makra. Z istotnych zabiegów, wyprowadziłem implementację obsługi przerwań do osobnego pliku interrupts.cpp oraz dodałem metodę Timer::start(). Wykonuje ona operację sei() czyli włącznie przerwań.
Zwróć uwagę na wadę tego projektu. Wymaga w pliku interrupts.cpp włączenia nagłówków led.hpp i button.hpp. Przecież przerwanie jako takie nie powinno wiedzieć nic o takich elementach programu.
Drugim znanym problemem jest dość nieładne pozostawienie makr w plikach nagłówkowych. Makra rezydują w 1 przestrzeni nazewniczej i w dużych projektach może to być istotny problem. Po ustaleniach z Antystatycznym, zdecydowaliśmy jednak że pozostawimy je w projekcie. Ot później je ukryję w plikach implementacji.

Oto projekt. Tym razem o wiele więcej kodu i o wiele więcej plików.

Na początek nagłówki aby zapoznać się z architekturą rozwiązania:

button.hpp

Kod: Zaznacz cały

#ifndef BUTTON_HPP_
#define BUTTON_HPP_

#include <stdint.h>

#define BTN_DDR         DDRB
#define BTN_PORT         PORTB
#define BTN_PIN         PINB

struct Button {
   static void init();
   static bool any_pressed();
   static uint8_t check();
};

#endif /* BUTTON_HPP_ */

Tu zwróć uwagę na konieczność wczytania <stdint.h> ze względu na obecność typu uint8_t w interfejsie klasy.

led.hpp

Kod: Zaznacz cały

#ifndef LED_HPP_
#define LED_HPP_

#include <stdint.h>

#define LED_DDR         DDRA
#define LED_PORT        PORTA

#define LED_ALL_ON      0x00
#define LED_ALL_OFF     0xFF

struct Led {
   static void init();
   static void led_on();
   static void led_off();
   static void toggle_mask(uint8_t mask_index);
};

#endif /* LED_HPP_ */

Podobnie i w led.hpp. Być może uda Ci się skompilować taki interfejs bez nagłówka… Jest to jednak błąd który będzie się mścił jeśli nastąpi ponowne użycie klasy w innym projekcie.

timer.hpp

Kod: Zaznacz cały

#ifndef TIMER_HPP_
#define TIMER_HPP_

#define CPU_FREQ      F_CPU
#define TIMER0_PSC    1024
#define IRQ_FREQ      100

struct Timer {
   static void init();
   static void start();
};

#endif /* TIMER_HPP_ */

Tu nie mam nic do dodania. Nagłówek jaki jest każdy widzi :-)

Pliki implementacji…

button.cpp

Kod: Zaznacz cały

#include <avr/io.h>
#include "button.hpp"

void Button::init() {
   BTN_PORT = 0x0F;
}
bool Button::any_pressed() {
   return (BTN_PIN & 0x0F) != 0x0F;
}
uint8_t Button::check() {
   uint8_t button { };
   /* Sprawdzenie przycisków które są pod odpowiednimi bitami maski.
    * button == 1 -> 0x00000001;
    * button == 2 -> 0x00000010;
    * ...
    * Iterujemy po maskach i sprawdzamy stan niski, stąd not w warunku if
    * Jeśli nie wciśnieto niczego, zwracamy 0'ro.
    */
   for (uint8_t mask = 0x01; mask < 0x10; mask <<= 1) {
      if (not (BTN_PIN & mask)) {
         return ++button;
      }
      ++button;
   }
   return 0;
}

Jak widać niezbędne wczytanie informacji o portach (<avr/io.h>). Nie jest ładne zwracanie zera w tym miejscu. Taka technika nazywa się magic value i w dużym projekcie będzie miała niedobre konsekwencje. Definiowanie jednak wartości enum dla tak prostego projektu, wydawało mi się przerostem formy nad treścią. Jeśli będzie to was interesowało, dajcie znać a pokażę jak to zrobić zgodnie z wykładnią C++11 lub nowszymi.

interrupts.cpp

Kod: Zaznacz cały

#include <avr/interrupt.h>
#include "led.hpp"
#include "button.hpp"

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 { };

   // Dość śmieszna sztuczka oszczędzająca 2-4 bajty na AVR :-)
   if ((SlowDownTimer++ == 10) and (SlowDownTimer = 0, true)) {
      Led::toggle_mask(Button::check());
   }
}

Samodzielna implementacja przerwań z „niefajnym” sprzężeniem do led i button. Zwracam uwagę na operator przecinka który pozwolił tu na skrócenie kodu wynikowego o kilka bajtów. Warto było użyć rejestru zawierającego zmienną SlowDownTImer i go wyzerować jeśli to konieczne. Przypominam że operator przecinka przetwarza dane w kolejności od lewej do prawej a wartość to argument po prawej. W tym przypadku true. Operator and działa z kolei w sposób leniwy i jeśli SlowDownTImer będzie równy 10, wykona drugą część (tę z zerowaniem).

led.cpp

Kod: Zaznacz cały

#include <avr/io.h>
#include "led.hpp"

void Led::init() {
   LED_DDR = 0xFF;
   Led::led_off();
}
void Led::led_on() {
   LED_PORT = LED_ALL_ON;
}
void Led::led_off() {
   LED_PORT = LED_ALL_OFF;
}
void Led::toggle_mask(uint8_t mask_index) {
   /*
    * Diody podłączone i obsługiwane z maską: 0b00000011, 0b00001100, 0b00110000, 0b11000000
    * Jeśli mask_index == 0, nie należy nic zmieniać na porcie.
    * Jeśli mask_index w zakresie [1, 4], należy przesujnąć 3'kę bo 3 == 0b00000011 :-)
    * mask_index to wciśnięty przycisk (0 -> żaden)
    */
   uint8_t mask = 3;
   if ((mask_index > 0) and (mask_index <= 4)) {
      mask <<= ((mask_index - 1) << 1);
      LED_PORT ^= mask;
   }
}

Jakiś specjalnych sztuczek tu nie ma. Zwykłe wydzielenie do pliku implementacji.

timer.hpp

Kod: Zaznacz cały

#include <avr/io.h>
#include <avr/interrupt.h>
#include "timer.hpp"

void Timer::init() {
   /* 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 Timer::start() {
   sei();
}

Widoczna metoda Timer::start() która wykonuje sei(). Dlatego właśnie należy włączyć <avr/interrupt.h>. Jak widać wszystko sugeruje że obsługa przerwań powinna być tu a nie w wydzielonym pliku (czyli teraz z interrupts.cpp).

main.cpp

Kod: Zaznacz cały

#include "button.hpp"
#include "led.hpp"
#include "timer.hpp"

/* Punkt wejścia. */
int main(void) {
   Led::init();
   Button::init();
   Timer::init(); /* Timer będzie generował 100 przerwań na sekundę. */
   Timer::start(); // Wywołanie sei()

   for(;;) {
      ;
   }
}

Oj jak on „schudł” :-) Nic tylko inicjalizacja i martwa pętla.

Nadszedł czas na implementację wywołań zwrotnych. Postanowiłem je umieścić w klasie Timer i wprowadzić możliwość rejestracji, wyrejestrowania oraz w samej klasie dodać metodę wywoływaną przez przerwanie. Dodatkowo przeniosłem „paskudne makra” z pliku nagłówka do *.cpp. Oczywiście niczego to nie rozwiązuje ale… jest nieco czytelniejsze. To jak się ich pozbyć, to zupełnie inna historia… :-)
Nie widzę dużego sensu umieszczanie ponownie całości kodu. Poprzestanę jedynie na omówieniu zmian.

Plik nagłówkowy timer.hpp

Kod: Zaznacz cały

#ifndef TIMER_HPP_
#define TIMER_HPP_

struct Timer {
   using callback_t = void (*)();
   static void init();
   static void start();
   static void register_callback(callback_t callback);
   static void unregister_callback();
   static void callback();
private:
   static callback_t callback_function;
};

#endif /* TIMER_HPP_ */

Dość istotne zmiany. Dodano metody do obsługi wywołań zwrotnych. Zwraca także uwagę definicja typu wywołania zwrotnego (using … ). Jest to wskaźnik na funkcję void funkcja() (czyli bez argumentów). Przypominam że w C w przeciwieństwie do C++, należy podać w argumentach void aby funkcja … nie przyjmowała argumentów. W C++ należy nawiasy z argumentami po prostu pozostawić puste.
Jeśli pierwszy raz widzisz using w takim kontekście, być może pomocne będzie stwierdzenie że:

Kod w nagłówku:

Kod: Zaznacz cały

using callback_t = void (*)();

Równoważny jest …

Kod: Zaznacz cały

typedef void (*callback_t)();

Ten drugi jednak kiepsko poddaje się szablonowaniu w C++ więc namawiam do konstrukcji z C++11 (ta z using).

W klasie Timer, pojawiło się także statyczne pole callback_function. Zawierać ono będzie wskaźnik na funkcję wołaną przez przerwanie.

Prześledźmy teraz kod implementacji Timer.

timer.cpp

Kod: Zaznacz cały

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/atomic.h>
#include "timer.hpp"

#define CPU_FREQ      F_CPU
#define TIMER0_PSC    1024
#define IRQ_FREQ      100

Timer::callback_t Timer::callback_function = nullptr;

void Timer::init() {
   /* 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 Timer::start() {
   sei();
}

void Timer::register_callback(Timer::callback_t callback) {
   ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
      Timer::callback_function = callback;
   }
}

void Timer::unregister_callback() {
   ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
      Timer::callback_function = nullptr;
   }
}

void Timer::callback() {
   if(Timer::callback_function != nullptr) {
      Timer::callback_function();
   }
}

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 { };

   // Dość śmieszna sztuczka oszczędzająca 2-4 bajty na AVR :-)
   if ((SlowDownTimer++ == 10) and (SlowDownTimer = 0, true)) {
      Timer::callback();
   }
}


Popatrz na linię z Timer::callback_t Timer::callback_function = nullptr; C++ wymaga aby pola statyczne inicjalizować jedynie 1 raz i to najlepiej w pliku implementacji. Tu ustawiam je na nullptr. Sygnalizuje to brak rejestracji. W metodzie callback() jak widzisz sprawdzam czy nie jest to przypadkiem tenże nullptr. Wywołanie funkcji z takim adresem (czyli zero), nie zakończy się dobrze :-)
W register_*/unregister_*, kod zmiany wskaźnika umieszczam w bloku ATOMIC… Należy tak uczynić bo w tle realizowane są przerwania a operacja powinna być niepodzielna.

Do pliku implementacji trafiła także obsługa przerwania. Dzięki logice w callback(), procedura obsługi przerwania jest stosunkowo prosta.

Ostatnie zmiany zaszyły w main.cpp…

Kod: Zaznacz cały

#include <util/delay.h>
#include "button.hpp"
#include "led.hpp"
#include "timer.hpp"

/* Punkt wejścia. */
int main(void) {
   Led::init();
   Button::init();
   Timer::init(); /* Timer będzie generował 100 przerwań na sekundę. */
   Timer::start(); // Wywołanie sei()

   for(;;) {
      // Obsługa klawiszy działa co 3 sec.
      _delay_ms(3000);
      // Callback zarejestrowany z użyciem lambdy
      Timer::register_callback(
         []() {
            Led::toggle_mask(Button::check());
      });
      _delay_ms(3000);
      Timer::unregister_callback();
   }
}

Aby zaprezentować możliwości rejestracji i wyrejestrowania funkcji wywołania zwrotnego, dodałem naiwne _delay_ms(…). Możesz sprawdzić że rzeczywiście co 3 sekundy system przestaje reagować na przyciski.
Dodałem także wywołanie przez lambdę. Jej ciało jest wpisane bezpośrednio w wywołanie Timer::register_callback(…). Pisanie tak naiwnej funkcji która zawiera wyłącznie 2 wywołania, bardzo często ceduje się na lambdę. Tu ma ona typ przewidziany w Timer::callback_t czyli void (*)().

Myślę że takie zaprezentowanie wywołań zwrotnych w C++ w porównaniu do implementacji w
C, pozwoli wyrobić sobie zdanie czy jest sens programowania w C++ na platformie AVR czy nie. Dość że … każdy z tych przykładów był mniejszy niż analogiczna implementacja w C. Zachowywał równocześnie wszystkie funkcjonalności.

Jeśli masz pytania znalazłeś nieścisłości w tym artykule, pytaj. To możliwe że coś pominąłem lub uczyniłem „zgniły kompromis” :-)
Mogę zamieścić także komplet przykładów projektów z IDE Eclipse dla łatwiejszej analizy.
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.
,,Myślenie nie jest łatwe, ale można się do niego przyzwyczaić" - Alan Alexander Milne: Kubuś Puchatek

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

Re: Od zera do bohatera, czyli callbacki dla (t)opornych w C++

Postautor: j23 » niedziela 12 lut 2017, 00:43

Dziękuję za konkretną "porcyjkę" wiedzy.

Mam takie 3 pytania/prośby:
1. Jakiego kompilatora (albo IDE) użyto do skomplikowania projektu do użytku z uC AVR?
2. Czy mógłbyś Kolego Mokrowski podać zastosowanie enum zamiast return 0.
3. Dlaczego w "nazwie" lambdy stosuje się nawiasy kwadratowe? -czy to jest standard składni C++, czy jest to po prostu taki przykład i można inaczej.

Pozdrawiam! j23 Jarek
Ostatnio zmieniony niedziela 12 lut 2017, 01:23 przez j23, łącznie zmieniany 1 raz.
Internet łączy ludzi, którzy dzielą się swoimi zainteresowaniami, pomysłami i potrzebami, bez względu na geograficzne (przeciwności).
BOB TAYLOR, PARC

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

Re: Od zera do bohatera, czyli callbacki dla (t)opornych w C++

Postautor: Antystatyczny » niedziela 12 lut 2017, 00:48

Przychylam się do prośby o enum. Miałem pytanie odnośnie nowej wersji wskaźnika na funkcję, ale już to zrozumiałem. Artykuł jest mega dobry do nauki. Serdeczne dzięki. Pewnie jeszcze pojawią się pytania, ale póki co trawię to, co jest. :)
"The true sign of intelligence is not knowledge but imagination" Albert Einstein.

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

Re: Od zera do bohatera, czyli callbacki dla (t)opornych w C++

Postautor: mokrowski » niedziela 12 lut 2017, 00:53

1. Eclipse z wtyczką do obsługi AVR. Standardowy zestaw. Kompilator z obsługą C++11 czyli gcc-g++ 4.9 wystarczy do skompilowania przykładów.
2. Ok.. poprawię i dodam...
3. W nawiasach kwadratowych lambdy może być podana informacja o "przechwytywaniu" zmiennych poza lambdą. Domyślnie lambda jest "hermetyczna" czyli nie możesz użyć nic czego w niej nie zdefiniujesz.

Kod: Zaznacz cały

#include <iostream>

using namespace std;

int main() {
    // Tym razem chwycę lambdę do nazwy dla ułatwienia przykładu.
    int z = 123;
    auto moja_lambda = [z](int a) {
        cout << "To ja, lambda i dostałam a = " << a << '\n'
            << "Dodatkowo 'chwyciłam' z poprzez kopię o wartości z = " << z << '\n';
    };
    auto moja_lambda2 = [&z](int a) {
        cout << "To ja, lambda2. Teraz chwytam przez referencję więc mogę zmienić z"
            << " poza moim ciałem a a wynosi: " << a << '\n';
        z = 4433;
    };

    moja_lambda(12);
    moja_lambda2(43);
    cout << "Wartość w main z = " << z << endl;
}

wynik:

Kod: Zaznacz cały

To ja, lambda i dostałam a = 12
Dodatkowo 'chwyciłam' z poprzez kopię o wartości z = 123
To ja, lambda2. Teraz chwytam przez referencję więc mogę zmienić z poza moim ciałem a a wynosi: 43
Wartość w main z = 4433
,,Myślenie nie jest łatwe, ale można się do niego przyzwyczaić" - Alan Alexander Milne: Kubuś Puchatek

kijas1
Posty: 10
Rejestracja: sobota 02 sty 2016, 18:50

Re: Od zera do bohatera, czyli callbacki dla (t)opornych w C++

Postautor: kijas1 » niedziela 12 lut 2017, 10:09

Jak zawsze solidna porcja wiedzy. Jestem jeszcze ciekaw jak sprawa ma się z metodami niestatycznymi. Doszukałem, że używać można tutaj szablonów z std::function i std::bind, ale nie za bardzo wiem co i jak. Chodzi mi o to, czy mając klasę timer mogę stworzyć co na wzór callbacka, który będzie mógł wołać dowolną metodę z dowolnego obiektu posiadającego tą metodę, czy raczej jedyną drogą jest implementacja interfejsu i jego dziedziczenie.

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

Re: Od zera do bohatera, czyli callbacki dla (t)opornych w C++

Postautor: mokrowski » niedziela 12 lut 2017, 14:25

std::function oraz std::bind to funkcje biblioteki standardowej C++. Takowej nie ma dla platformy AVR. Ściślej, jest szereg dostępnych rozwiązań ale żadne nie jest dostarczane z avr-gcc. Oczywiście nic nie stoi na przeszkodzie by takie funkcje sobie dodać do projektu :-)

Możesz spokojnie używać instancji obiektów i wywołań dla tych instancji. Ja tu tego nie pokazywałem z kilku powodów:
1. Nie miało to sensu dla kilku diod i kilku przycisków
2. Chciałem zachować korelację z przykładem w C Antystatycznego aby można było porównać oba rozwiązania.
3. Komplikowało by to przykład (np. wywołania wskaźników do metod w klasie z podanym elementem w postaci obiektu). Nie na tym etapie i to już temat na inny tutorial.
4. Nie chciałem ponosić kosztów trzymania wskaźników do this których i tak nie użyję.

Ogólnie stosuje się technikę przekazywania elementów niezmiennych w całej aplikacji embedded jako argumentów szablonów. W ten sposób przekazuję porty i maski (one się przecież nie zmieniają w danej klasie). Niestety aby to zrobić, należy "rozebrać definicję'" portu z otoczki typu bo domyślny typ nie jest dopuszczalny jako argument szablonowy.

Reasumując. Masz dostępne domyślnie dla avr-gcc to co jest w _rdzeniu_ języka C++ bo biblioteki standardowej nie ma (zapomnij o klasach vector, string, iteratorach czy algorytmach.... ).
,,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 2 gości