Arduino: întreruperi în clase și funcții callback

 Autor:   Publicat pe:   Actualizat pe:  2019-05-04T17:17:34Z

Atașarea întreruperilor în funcții membru ale unei clase și utilizarea de funcții callback pentru trimiterea datelor în schiță

Arduino este o platformă de dezvoltare cu sursă deschisă, ușor de folosit, datorită limbajului de programare mult simplificat și a hardware-ului flexibil. Mediul de dezvoltare Arduino IDE vine cu biblioteci predefinite cu funcții ușor de înțeles pentru setarea și citirea stării pinilor, dar și pentru comunicarea cu diverse module folosind protocoale standard. Limbajul de programare nu este altceva decât C/C++.

Dacă ai în plan un proiect mai mare sau dacă îți creezi o bibliotecă pentru Arduino, vei ajunge să definești noi clase C/C++. O clasă este o extindere a conceptului de structură, care unește nu numai date și proprietăți, dar și funcții și metode care prelucrează aceste date. Spre deosebire de structuri, nu toți membrii claselor sunt accesibili din afara clasei. Controlul accesului se face folosind specificatorii de acces. Mai multe informații despre clase în programarea C/C++ poți găsi aici.

Arduino: întreruperi în clase și funcții callback

Pentru a demonstra conceptele prezentate în acest articol, voi crea o bibliotecă C/C++ pentru Arduino, în care voi atașa o întrerupere unui pin și voi „notifica” schița la modificarea stării pinului. Deschide dosarul „sketchbook” (în care Arduino IDE salvează schițele) și, în dosarul libraries vom crea unul nou. Să-l numim „testlib”. În acesta vom crea un fișier text gol „testlib.h”, în care vom defini noua clasă. Cam așa arată definiția:

#ifndef TESTLIB_H
#define TESTLIB_H

class testLib {

public:
  void begin(int interruptPin) { pinMode(interruptPin, INPUT); }

} ;

#endif

Primele două și ultima linie sunt de fapt directive pentru preprocesor. La ce folosesc? Dacă incluzi propria bibliotecă în mai multe fișiere de cod folosind #include "testlib.h" vei genera o eroare la compilare, deoarece ai definiții multiple ale clasei testLib. Dar, preprocesorul verifică fișierele de cod înainte de a fi compilate. La prima includere a bibliotecii într-un alt fișier, TESTLIB_H va fi definit. La următoarele includeri, condiția #ifndef TESTLIB_H nu mai este îndeplinită, și tot ce urmează de la acest #ifndef până la primul #endif nu este luat în considerare.

Definiția clasei este cât se poate de simplă, cu o singură funcție publică, begin() care acceptă ca argument pinul ce va genera întreruperea.

Asociere întrerupere

Tot în funcția begin() ar trebui să asociem întreruperea. Unul din argumentele funcției attachInterrupt() este chiar funcția callback care gestionează întreruperea. Această funcție, conform documentației Arduino, nu acceptă parametri și nu returnează nimic. Cam așa are trebui să arate:

class testLib {

public:
  void begin(int interruptPin) { 
                pinMode(interruptPin, INPUT); 
                attachInterrupt(digitalPinToInterrupt(interruptPin), libInterruptHandler, CHANGE);
                }

private:
  void libInterruptHandler(void);

} ;

Am declarat o funcție care să gestioneze întreruperea. Această funcție este un membru al clasei și este privată. Acum, dacă încercăm să compilăm acest cod, nu vom reuși. Tocmai pentru că funcția libInterruptHandler() este membru al unei clase. Nu putem asocia întreruperea decât unei funcții globale, neatașată unei clase. Simplu, nu? Dar totuși, întreruperea trebuie gestionată în interiorul clasei, deci de funcții membru.

Se poate face în felul următor. Se declară un pointer de tipul clasei. Se definește și funcția globală ce va fi folosită ca argument pentru attachInterrupt(). Apoi, într-o funcție membru, de preferat în constructorul clasei (acest exemplu nu are) sau oriunde înainte de folosire, pointerului îi este asociată instanța curentă a clasei. În funcția globală de gestionare a întreruperii, nu facem nimic legat de întrerupere, doar chemăm funcția membru, folosind instanța clasei. Funcția de gestionare a întreruperii din clasă trebuie să fie membru public. Vom împărți codul în două fișiere, header și sursă C++.

testlib.h

#ifndef TESTLIB_H
#define TESTLIB_H

#include "Arduino.h" // trebuie inclus

class testLib {

public:
  void begin(int interruptPin);
  void classInterruptHandler(void); // membru care gestioneaza intreruperea

} ;

#endif

testlib.cpp

#include "testlib.h"

// In afara clasei
testLib *pointerToClass; // pointer de tipul clasei

static void outsideInterruptHandler(void) { // functia globala
  pointerToClass->classInterruptHandler(); // apeleaza functia membru
}

// Membrii clasei
void testLib::begin(int interruptPin) {
  pinMode(interruptPin, INPUT);
  
  pointerToClass = this; // asociaza instanta curenta a clasei pointerului (IMPORTANT!!!)
  attachInterrupt(digitalPinToInterrupt(interruptPin), outsideInterruptHandler, CHANGE);
}

void testLib::classInterruptHandler(void) {
  // urmeaza: apeleaza o functie callback
}

Funcții callback

Până acum, avem o funcție, classInterruptHandler(), care este apelată automat de program la modificarea stării pinului. Dar nu o putem folosi în schiță. O putem apela din schiță dacă o facem publică, dar nu asta vrem. În schimb, ne interesează când este apelată. Am putea să o facem să modifice o variabilă și să verificăm periodic acea variabilă. Se poate și așa, dar există altă metodă, mai bună, folosind o funcție callback. Inclusiv aceste funcții pe care le-am definit pentru gestionarea întreruperii sunt funcții callback, fiind apelate automat de program. Acestea sunt funcții predeclarate, deci cunoscute.

Dar funcția callback din schița principală nu este încă declarată. Și nu putem ști cum va fi numită. De aceea, în clasă vom adăuga o funcție care asociază un pointer funcției ce va fi definită de utilizator (programator) în schiță. Vom stoca acest pointer într-o variabilă privată a clasei. Vom folosi acest pointer pentru a apela funcția din schiță. Ce se întâmplă în funcția din schiță este la îndemâna programatorului. Voi complica puțin lucrurile, utilizând o funcție callback cu un parametru (starea pinului).

testlib.h

#ifndef TESTLIB_H
#define TESTLIB_H

#include "Arduino.h"

class testLib {

public:
  void begin(int interruptPin);
  void classInterruptHandler(void);
  void setCallback(void (*userDefinedCallback)(const int)) {
                      localPointerToCallback = userDefinedCallback; }

private:
  int localInterruptPin; // stocheaza pinul pentru ca va fi folosit intr-o functie
  void (*localPointerToCallback)(const int);

} ;

#endif

testlib.cpp

#include "testlib.h"

// In afara clasei
testLib *pointerToClass; // declare a pointer to testLib class

static void outsideInterruptHandler(void) { // functia globala
  pointerToClass->classInterruptHandler(); // apeleaza functia membru
}

// Class members
void testLib::begin(int interruptPin) {
  pinMode(interruptPin, INPUT);
  
  pointerToClass = this; // asociaza instanta curenta a clasei pointerului (IMPORTANT!!!)
  attachInterrupt(digitalPinToInterrupt(interruptPin), outsideInterruptHandler, CHANGE);

  localInterruptPin = interruptPin;
}

void testLib::classInterruptHandler(void) {
  localPointerToCallback(digitalRead(localInterruptPin));
}

Biblioteca este gata de utilizare. Să vedem cum.

Utilizare

În primul rând vom include biblioteca în schiță și vom declara o instanță a clasei oferită de aceasta. Va trebui să definim și funcția callback. Schița va fi compilată și fără aceasta, dar nu va funcționa (nu putem afla când are loc întreruperea). Funcția callback nu returnează nimic (void) și primește un singur parametru (const int), după cum am definit-o în clasă: void (*userDefinedCallback)(const int). Poți denumi funcția și parametrul ei după cum vrei, cât timp respecți tipul de date. În funcție, faci ce vrei cu datele primite (în acest caz, starea pinului). Iată cum arată schița care trimite pe portul serial starea pinului.

#include "testlib.h"

const int pin = 2;
testLib myLib; // obiect tip clasa

void writePinStateToSerial(const int state) {
  Serial.print("Pin "); Serial.print(pin, DEC);
  Serial.print(" state is: ");
  Serial.println(state ? "HIGH" : "LOW");
}

void setup() {
  Serial.begin(115200);
  
  myLib.begin(pin);
  myLib.setCallback(writePinStateToSerial);
}

void loop() {
  // nimic aici
}

Testare

Să nu uităm de partea hardware. Dacă folosești o placă de dezvoltare cu microcontroller ATmega328p, poți genera întreruperi doar pe pinii digitali 2 și 3. Folosește o placă de test breadboard și conectează un rezistor între pinul de întrerupere și alimentare (5V - rezistor - D2). Conectează și un buton între acest pin și masă (D2 - buton - GND). Încarcă schița și pornește monitorul serial. Vei vedea că apăsarea butonului generează întreruperi. O singură apăsare generează chiar mai multe. Acest lucru este normal, deoarece la apăsare contactul nu se face perfect și stabil dintr-o dată.

Testare bibliotecă cu placă Nano (ATmega328p)

Testare bibliotecă cu placă Nano (ATmega328p)

Citirea apăsării unui buton este doar în scop demonstrativ. Metoda poate fi folosită pentru citirea datelor de la diverse module sau senzori care generează întreruperi când trebuie să trimită date către procesor și așteaptă ca acesta să inițieze secvența de citire.

Niciun comentariu :

Trimiteți un comentariu

Vă recomandăm să citiți regulamentul comentariilor înainte de a scrie un comentariu.