Come scrivere un grande progetto.
Un nuovo programma in Arduino molto spesso sembra una tela illeggibile in cui tutto è accatastato. Più grande diventa il progetto, più difficile e spiacevole diventa il suo ulteriore sviluppo. In questo argomento, esamineremo diversi approcci per semplificare e migliorare la scrittura di progetti di grandi dimensioni. Per comprendere questo argomento, è necessario ricordare alcuni degli argomenti trattati in precedenza:
Miglioramento del codice.
Suddividi in funzioni.
Quasi ogni parte del codice può essere racchiusa in una funzione e rimossa dal ciclo principale. Per esempio:
void loop()
{
// costruzione del timer.
// interrogazione del sensore.
// filtraggio del valore.
// altro codice.
}
Creiamo una funzione:
void loop()
{
sensorRead();
// altro codice.
}
void sensorRead()
{
// costruzione del timer.
// interrogazione del sensore.
// filtraggio del valore.
}
Queste funzioni possono essere inserite in schede e raggruppate per significato.
Variabili globali.
Dobbiamo sforzarci di ridurre il numero di variabili globali nello schizzo. In primo luogo, è solo "brutto" e in secondo luogo, a un certo punto, i problemi possono iniziare con l'invenzione di nomi univoci e il loro riconoscimento nell'area del codice.
Il primo passaggio consiste nell'utilizzare variabili statiche ove possibile. L'esempio più semplice è la costruzione di un timer software:
uint32_t tmr1; // variabile per timer.
void setup()
{
}
void loop()
{
if (millis() - tmr1 >= 1000)
{
tmr1 = millis();
// eseguire un'azione.
}
}
Se hai bisogno di più timer, dovrai creare più variabili e trovare i nomi per loro. Puoi spostare ciascun timer in una funzione separata e rendere statica la variabile al suo interno. Esempio con due timer:
void setup()
{
}
void loop()
{
sensorTimer();
timeoutTimer();
}
void sensorTimer()
{
static uint32_t tmr;
if (millis() - tmr >= 1000)
{
tmr = millis();
// eseguire un'azione.
}
}
void timeoutTimer()
{
static uint32_t tmr;
if (millis() - tmr >= 1000)
{
tmr = millis();
// eseguire un'azione.
}
}
Puoi anche utilizzare le strutture per raggruppare diverse variabili che hanno un significato simile sotto un'unica etichetta:
float tempRaw;
float tempFilter;
int filterPeriod;
bool btnState;
bool btnFlag;
int btnPeriod;
void setup()
{
}
void loop()
{
}
Combiniamo in strutture e semplifichiamo i nomi delle variabili:
struct
{
float raw;
float filter;
int period;
} temp;
struct
{
bool state;
bool flag;
int period;
} btn;
void setup()
{
}
void loop()
{
// accedere agli elementi.
temp.raw = sensorRead();
btn.state = pinRead();
}
Separare dati e codice.
Uno dei fondamenti della programmazione C++ è la separazione dei dati (variabili) dal codice eseguibile. Nella maggior parte dei casi, puoi usare variabili locali e passarle alle funzioni invece di prenderle dall'ambito globale.
Considera un esempio astratto in cui due funzioni indipendenti accedono alla stessa variabile globale:
float temp; // temperatura dal sensore.
void setup()
{
}
void loop()
{
sensorRead();
regulator();
}
void sensorRead()
{
temp = leggere il sensore;
// filtraggio/elaborazione temp.
}
void regulator()
{
// ad esempio controller PID.
output = temp * k;
}
Eliminiamo la variabile globale in questo modo:
void setup()
{
}
void loop()
{
float temp = sensorRead();
regulator(temp);
}
float sensorRead()
{
// leggere il sensore.
// filtraggio/elaborazione
return risultato;
}
void regulator(float temp)
{
// ad esempio controller PID.
output = temp * k;
}
Puoi andare oltre e persino sbarazzarti della variabile locale:
void setup()
{
}
void loop()
{
regulator(sensorRead());
}
float sensorRead()
{
// leggere il sensore.
// filtraggio/elaborazione temp.
return risultato;
}
void regulator(float temp)
{
// ad esempio controller PID.
output = temp * k;
}
Usa le classi.
Sarebbe ancora più corretto creare una classe in cui sia implementata una parte del programma completamente indipendente, con proprie variabili e funzioni. La comodità della classe risiede anche nel fatto che gli sviluppi progettati come classi possono essere utilizzati in altri progetti senza modificare il codice della classe. Ad esempio, puoi considerare qualsiasi libreria: per un sensore, un display o un qualche tipo di algoritmo.
Dividi in file.
Affinché un progetto possa essere suddiviso in più file, deve inizialmente essere costituito da parti che possono funzionare indipendentemente l'una dall'altra e non sovrapporsi l'una all'altra. Tale parte può essere chiamata "sottoprogramma", che contiene il proprio codice e un insieme di variabili e può anche essere composta da più file allo stesso modo. Storia familiare? Dopotutto, è così che vengono implementate le librerie! La libreria contiene una serie di strumenti che non dipendono dal programma principale e possono essere utilizzati anche in un altro progetto. Utilizzando la libreria, stiamo già suddividendo il nostro progetto in più file.
Non ci sono librerie per tutte le occasioni, quindi dovrai scrivere tu stesso una parte decente del codice: tutti i tipi di algoritmi, effetti e così via. È proprio così che possono essere inseriti in file separati, ci sono due modi: avvolgerlo in una classe o semplicemente inserire il codice in un file separato.
Esempio.
Diamo un'occhiata a un esempio di trasformare un codice terribile con un mucchio di variabili globali e un pasticcio nel ciclo principale in un programma chiaro con subroutine indipendenti separate. In questo esempio abbiamo due pulsanti collegati (ai pin D2 e D3) e due led (usiamo quello di bordo sul pin D13 e quello esterno su D12).
Scriviamo un programma che:
- Pulsanti di polling asincrono con smorzamento software del rimbalzo dei contatti.
- Lampeggia il LED 1 con periodo impostabile tramite i pulsanti.
- Lampeggia il LED 2 con un periodo che:
- Impostato in modo casuale utilizzando l'algoritmo di randomizzazione hardware.
- Lo fa ogni 2 secondi su un timer.
Schizzo per il miglioramento.
// pin
const byte btn1 = 2;
const byte btn2 = 3;
const byte led1 = 13;
const byte led2 = 12;
const byte analogPin = 0;
// timer anti rimbalzo dei pulsanti.
int32_t btn1Tmr;
uint32_t btn2Tmr;
// pulsanti di polling flag
bool btn1Flag;
bool btn2Flag;
// variabili per il LED 1
uint32_t ledTmr1;
uint16_t ledPeriod1 = 1000; // periodo iniziale 1 s.
bool ledState1 = false;
const int step = 50; // cambia passo.
// variabili per il LED
2
uint32_t ledTmr2;
uint16_t ledPeriod2 = 1000;
bool ledState2 = false;
// timer per un periodo casuale(random).
uint32_t rndTmr;
uint16_t rndPeriod = 2000;
// variabile per un periodo casuale(random).
uint32_t seed = 0;
void setup ()
{
// impostare i pin
pinMode(btn1, INPUT_PULLUP);
pinMode(btn2, INPUT_PULLUP);
pinMode(led1, OUTPUT);
pinMode(led2, OUTPUT);
}
void loop()
{
// Timer LED 1.
if (millis() - ledTmr1 >= ledPeriod1)
{
ledTmr1 = millis();
ledState1 = !ledState1;
digitalWrite(led1, ledState1);
}
// Timer LED
2
if (millis() - ledTmr2 >= ledPeriod2)
{
ledTmr2 = millis();
ledState2 = !ledState2;
digitalWrite(led2, ledState2);
}
// Timer per un periodo casuale(random).
if (millis() - rndTmr >= rndPeriod)
{
rndTmr = millis();
for (int i = 0; i < 16; i++)
{
seed *= 4;
seed += analogRead(analogPin) & 3;
}
// limite 1000 ms (1 secondo)
ledPeriod2 = seed % 1000;
}
// polling del primo pulsante con un antirimbalzo di 100 ms
bool btn1State = digitalRead(btn1);
if (!btn1State && !btn1Flag && millis() - btn1Tmr >= 100)
{
btn1Flag = true;
btn1Tmr = millis();
ledPeriod1 += step; // увеличить период
}
if (btn1State && btn1Flag && millis() - btn1Tmr >= 100)
{
btn1Flag = false;
btn1Tmr = millis();
}
// polling del secondo pulsante con un antirimbalzo di 100 ms.
bool btn2State = digitalRead(btn2);
if (!btn2State && !btn2Flag && millis() - btn2Tmr >= 100)
{
btn2Flag = true;
btn2Tmr = millis();
ledPeriod1 -= step; // ridurre il periodo.
}
if (btn2State && btn2Flag && millis() - btn2Tmr >= 100)
{
btn2Flag = false;
btn2Tmr = millis();
}
}
L'aggiunta di pulsanti aggiuntivi o funzionalità LED aggiuntive al programma porterà a molta confusione e ad un aumento della quantità di codice, sarà molto più difficile capirlo.
Avvolgiamo l'elaborazione dei pulsanti in una classe, perché abbiamo già due pulsanti identici e in futuro possiamo aggiungerne altri al programma. Prenderemo immediatamente la classe in un file separato e lo organizzeremo come una libreria. Lo faccio in un file in modo da non duplicare lo stesso codice.
button.h
// Classe di pulsanti.
#pragma once
#include <Arduino.h>
#define _BTN_DEB_TIME 100 // Timeout di rimbalzo
class Button
{
public:
Button (byte pin) : _pin(pin)
{
pinMode(_pin, INPUT_PULLUP);
}
bool click()
{
bool btnState = digitalRead(_pin);
if (!btnState && !_flag && millis() - _tmr >= _BTN_DEB_TIME)
{
_flag = true;
_tmr = millis();
return true;
}
if (btnState && _flag && millis() - _tmr >= _BTN_DEB_TIME)
{
_flag = false;
_tmr = millis();
}
return false;
}
private:
const byte _pin;
uint32_t _tmr;
bool _flag;
};
Il gestore dei pulsanti ora funziona in questo modo: restituisce true se è stato fatto clic sul pulsante corrispondente. Nel programma principale, metteremo il metodo click() in una condizione e cambieremo il periodo del LED in base ad esso.
Successivamente, utilizziamo più volte la stessa costruzione del timer per millis(). Avvolgiamolo in una classe.
timer.h
#pragma once
#include <Arduino.h>
class Timer
{
public:
Timer(uint16_t nprd = 0)
{
setPeriod(nprd);
}
void setPeriod(uint16_t nprd)
{
_prd = nprd;
}
uint16_t getPeriod()
{
return _prd;
}
bool ready()
{
if (millis() - _tmr >= _prd)
{
_tmr = millis();
return true;
}
return false;
}
private:
uint32_t _tmr = 0;
uint16_t _prd = 0;
};
Ora è sufficiente dichiarare e configurare il timer e per verificarlo è necessario eseguire il polling del metodo ready(), che restituirà true quando attivato. Puoi impostare il periodo con setPeriod() e ottenerlo con getPeriod().
Abbiamo due LED, quindi sarebbe anche logico racchiudere il codice lampeggiante in una classe. Il LED lampeggia su un timer, quindi possiamo utilizzare la libreria timer già scritta all'interno della classe LED, questo ridurrà la quantità di codice e ridurrà il peso totale del programma:
led.h
#pragma once
#include <Arduino.h>
#include "timer.h"
class LED
{
public:
LED (byte pin, int period) : _pin(pin)
{
pinMode(_pin, OUTPUT);
tmr.setPeriod(period);
}
void setPeriod(uint16_t prd)
{
tmr.setPeriod(prd);
}
uint16_t getPeriod()
{
return (tmr.getPeriod());
}
void blink()
{
if (tmr.ready())
{
digitalWrite(_pin, flag);
flag = !flag;
}
}
private:
const byte _pin;
bool flag;
Timer tmr;
};
Ora è sufficiente dichiarare un LED con un pin, periodo di lampeggio e chiamare semplicemente il metodo blink() nel ciclo.
Abbiamo ancora un algoritmo per ottenere numeri casuali da un pin analogico. Non è necessaria una classe per questo, basterà semplicemente mettere la funzione in un file separato e farle accettare il numero pin. Qui dobbiamo creare due file: un file di intestazione e un file di implementazione. "Nascondiamo" la variabile del generatore dal programma principale dichiarandola statica all'interno del file di implementazione.
rnd.h
#pragma once
#include <Arduino.h>
uint32_t getRandom(byte pin);
rnd.cpp
#include "rnd.h"
static uint32_t seed = 0;
uint32_t getRandom(byte pin)
{
for (int i = 0; i < 16; i++)
{
seed *= 5;
seed += analogRead(pin) & 3;
}
return seed;
}
Mettiamo le nostre librerie accanto al file di schizzo, includiamole nel codice e vediamo come appare ora il nostro progetto. Ho anche contrassegnato tutte le costanti di pin con #define per non duplicarle nella RAM:
Schizzo
#include "timer.h"
#include "button.h"
#include "led.h"
#include "rnd.h"
#define BTN1_PIN 2
#define BTN2_PIN 3
#define LED1_PIN 13
#define LED2_PIN 12
#define LED1_STEP 50
LED led1(LED1_PIN, 1000);
LED led2(LED2_PIN, 1000);
Button btn1(BTN1_PIN);
Button btn2(BTN2_PIN);
Timer rndTimer(2000);
void setup()
{
}
void loop()
{
led1.blink();
led2.blink();
if (rndTimer.ready())
led2.setPeriod(getRandom(0) % 1000);
if (btn1.click())
led1.setPeriod(led1.getPeriod() + LED1_STEP);
if (btn2.click())
led1.setPeriod(led1.getPeriod() - LED1_STEP);
}
Il programma è diventato molto più compatto e occupa anche meno memoria: 1618 byte contro 1796. Quasi 200 byte più leggeri.
Aggiungiamo funzionalità: lasciamo che ci siano altri due pulsanti (D4 e D5), con dei clic sarà possibile accendere e spegnere rispettivamente il primo e il secondo LED. Per fare ciò, aggiungi un flag di stato alla classe LED, tramite il quale spegneremo forzatamente il LED. Avrai anche bisogno di un paio di metodi per impostare o cancellare questo flag. Puoi crearne uno che cambi lo stato del LED, tale implementazione sarà più compatta:
led.h
#pragma once
#include <Arduino.h>
#include "timer.h"
class LED
{
public:
LED (byte pin, int period) : _pin(pin)
{
pinMode(_pin, OUTPUT);
tmr.setPeriod(period);
}
void setPeriod(uint16_t prd)
{
tmr.setPeriod(prd);
}
uint16_t getPeriod()
{
return (tmr.getPeriod());
}
void blink()
{
if (state)
{
if (tmr.ready())
{
flag = !flag;
digitalWrite(_pin, flag);
}
}
else
{
if (flag)
digitalWrite(_pin, 0);
}
}
void toggle()
{
state = !state;
}
private:
const byte _pin;
bool flag, state = true;
Timer tmr;
};
E il schizzo finale:
Schizzo
#include "timer.h"
#include "button.h"
#include "led.h"
#include "rnd.h"
#define BTN1_PIN 2
#define BTN2_PIN 3
#define BTN3_PIN 4
#define BTN4_PIN 5
#define LED1_PIN 13
#define LED2_PIN 12
#define LED1_STEP 50
LED led1(LED1_PIN, 1000);
LED led2(LED2_PIN, 1000);
Button btn1(BTN1_PIN);
Button btn2(BTN2_PIN);
Button btn3(BTN3_PIN);
Button btn4(BTN4_PIN);
Timer rndTimer(2000);
void setup()
{
}
void loop()
{
led1.blink();
led2.blink();
if (rndTimer.ready())
led2.setPeriod(getRandom(0) % 1000);
if (btn1.click())
led1.setPeriod(led1.getPeriod() + LED1_STEP);
if (btn2.click())
led1.setPeriod(led1.getPeriod() - LED1_STEP);
if (btn3.click())
led1.toggle();
if (btn4.click())
led2.toggle();
}
Vediamo come funziona in pratica. Creiamo un circuito dove ce un Arduino UNO, 4 pulsanti e due led. Deve funzionare come descritto sopra:
- Lampeggia il led1 con periodo impostabile tramite i pulsanti btn1 e btn2.
- Lampeggia il led2 con un periodo che e Impostato in modo casuale utilizzando l'algoritmo di randomizzazione hardware, lo fa ogni 2 secondi su timer .
- Con i pulsanti btn3 e btn4 sarà possibile accendere e spegnere rispettivamente led1 e led2.

Download file Miglioramento_codice.