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.