Abstract

Una singola lettura analogica può essere alterata da rumore elettrico, variazioni dell’alimentazione, disturbi impulsivi o instabilità del sensore. Ripetere la misura e calcolare una semplice media riduce le oscillazioni casuali, ma un solo valore anomalo può modificare sensibilmente il risultato.

Il metodo descritto acquisisce dieci campioni, li ordina, elimina i due valori più bassi e i due più alti e calcola la media dei sei campioni centrali. Il procedimento riduce l’influenza delle letture estreme senza richiedere librerie esterne.

Sono presentate tre implementazioni: C, C++ e framework Wiring. La versione Wiring utilizza una temporizzazione non bloccante basata su millis() e acquisisce i campioni a intervalli regolari.

Perché una singola lettura può essere insufficiente

Il valore restituito dall’ADC rappresenta la tensione presente sull’ingresso durante una specifica conversione.

Anche quando la grandezza fisica misurata rimane costante, letture successive possono risultare differenti:

511
513
512
509
514
512

Le variazioni possono essere causate da:

  • rumore presente sull’alimentazione;
  • disturbi raccolti dai collegamenti;
  • commutazione di uscite digitali;
  • instabilità della tensione di riferimento;
  • impedenza elevata della sorgente;
  • variazioni interne del sensore;
  • interferenze impulsive;
  • contatti elettrici non stabili.

Una singola conversione non permette di distinguere una variazione reale da un disturbo momentaneo.

Ripetere la misura consente di utilizzare l’insieme dei campioni per ottenere un valore più stabile.

Media aritmetica semplice

La soluzione più immediata consiste nel acquisire più letture e calcolarne la media:

media = somma dei campioni / numero dei campioni

Per esempio:

510
512
511
513
512
514

La somma vale:

510 + 512 + 511 + 513 + 512 + 514 = 3072

La media è:

3072 / 6 = 512

La media riduce le piccole oscillazioni casuali, ma rimane sensibile ai valori estremi.

Considerando la serie:

510
512
511
900
512
514

si ottiene:

510 + 512 + 511 + 900 + 512 + 514 = 3459

e quindi:

3459 / 6 = 576,5

Un solo valore anomalo ha spostato il risultato da circa 512 a circa 577.

Valori anomali e outlier

Un outlier è un valore molto distante dagli altri campioni della stessa serie.

Può essere prodotto da:

  • un disturbo impulsivo;
  • un collegamento momentaneamente instabile;
  • una commutazione elettrica;
  • un errore temporaneo del sensore;
  • una conversione eseguita durante una fase transitoria.

Non ogni valore minimo o massimo è necessariamente errato. La rimozione degli estremi costituisce una scelta di filtraggio e deve essere adeguata alla velocità e alla natura del segnale.

Se il segnale cambia realmente e rapidamente, eliminare i valori estremi può attenuare una variazione effettiva.

Metodo utilizzato

Il procedimento adottato utilizza dieci campioni:

numero totale di campioni = 10

I valori vengono ordinati in modo crescente:

valore minore → valore maggiore

Successivamente vengono eliminati:

2 valori più bassi
2 valori più alti

Rimangono sei valori centrali:

10 − 2 − 2 = 6

La media filtrata viene calcolata soltanto sui sei campioni rimanenti:

media filtrata =
somma dei 6 campioni centrali / 6

Il metodo può essere definito come media troncata o media con esclusione degli estremi.

Esempio completo

Si consideri la seguente serie:

511
510
513
900
512
509
514
100
511
512

Dopo l’ordinamento:

100
509
510
511
511
512
512
513
514
900

Vengono eliminati i due valori più bassi:

100
509

e i due valori più alti:

514
900

Rimangono:

510
511
511
512
512
513

La somma vale:

510 + 511 + 511 + 512 + 512 + 513 = 3069

La media filtrata è:

3069 / 6 = 511,5

Utilizzando una divisione intera, il risultato restituito dal programma è:

511

La media semplice dei dieci campioni sarebbe invece:

4592 / 10 = 459,2

I valori estremi avrebbero quindi modificato sensibilmente il risultato.

Ordinamento dei campioni

Prima di eliminare gli estremi, i campioni devono essere ordinati.

Con dieci elementi non è necessario utilizzare un algoritmo complesso. È sufficiente un ordinamento per inserimento.

Il procedimento considera un elemento alla volta e lo sposta nella posizione corretta rispetto agli elementi già ordinati.

Esempio iniziale:

512
509
514

Dopo l’ordinamento:

509
512
514

La funzione utilizzata nei programmi è:

void ordina_campioni(uint16_t campioni[], uint8_t numero)
{
    for (uint8_t i = 1; i < numero; i++)
    {
        uint16_t valore = campioni[i];
        uint8_t posizione = i;

        while (
            posizione > 0 &&
            campioni[posizione - 1] > valore
        )
        {
            campioni[posizione] =
                campioni[posizione - 1];

            posizione--;
        }

        campioni[posizione] = valore;
    }
}

Con un gruppo di soli dieci elementi, il tempo richiesto dall’ordinamento è molto ridotto rispetto ai normali intervalli di acquisizione di un sensore ambientale.

Numero di campioni e valori eliminati

Il numero di campioni influenza stabilità, rapidità e memoria utilizzata.

Un numero maggiore di letture:

  • riduce l’effetto delle variazioni casuali;
  • richiede più tempo;
  • rallenta la risposta ai cambiamenti reali;
  • occupa più memoria;
  • aumenta il numero di conversioni.

Con dieci campioni e quattro valori eliminati, il risultato utilizza il 60% delle letture acquisite:

6 / 10 = 0,6

Eliminare troppi valori riduce eccessivamente la quantità di informazione utilizzata.

Eliminare un solo valore per lato rende invece il filtro meno resistente agli impulsi multipli.

La configurazione:

10 campioni
2 valori bassi eliminati
2 valori alti eliminati
6 valori mediati

costituisce un compromesso semplice per segnali che variano lentamente.

Intervallo tra i campioni

Acquisire dieci campioni consecutivi nel minor tempo possibile non garantisce sempre dieci misure realmente indipendenti.

Se il disturbo dura più a lungo della serie di conversioni, tutti i campioni possono essere influenzati nello stesso modo.

Separare le letture nel tempo permette di osservare il segnale in istanti differenti.

Nella versione Wiring viene utilizzato un intervallo di:

20 ms

Tra il primo e l’ultimo campione trascorrono quindi circa:

9 × 20 ms = 180 ms

Il valore filtrato viene aggiornato approssimativamente ogni 200 ms.

L’intervallo deve essere scelto in base alla velocità del segnale.

Per una temperatura, un livello, un’umidità del terreno o una posizione meccanica lenta, 20 ms tra i campioni è normalmente un intervallo breve.

Per segnali rapidi può risultare eccessivo.

Temporizzazione non bloccante

La versione Wiring non utilizza delay().

Il programma confronta il valore corrente di millis() con il tempo dell’ultima acquisizione:

if (
    tempo_attuale - tempo_ultimo_campione >=
    intervallo_campioni_ms
)

Quando l’intervallo è trascorso, viene acquisito un nuovo campione.

Durante il tempo compreso tra due letture, il ciclo principale rimane disponibile per altre operazioni:

  • gestione di ingressi digitali;
  • controllo di uscite;
  • comunicazione seriale;
  • aggiornamento di display;
  • gestione della rete;
  • controllo di altre temporizzazioni.

La sottrazione tra valori unsigned long mantiene corretto il confronto anche quando il contatore di millis() supera il proprio valore massimo.

Valori validi e valori estremi

Lo scarto dei due valori più bassi e dei due più alti avviene dopo l’acquisizione della serie.

È possibile aggiungere un controllo precedente per rifiutare letture che non appartengono all’intervallo previsto dal sensore.

Per l’ADC a 10 bit l’intervallo numerico generale è:

0–1023

Nei programmi vengono quindi definite due costanti:

const int adc_minimo_valido = 0;
const int adc_massimo_valido = 1023;

Con questi valori nessuna conversione ADC normale viene esclusa.

Quando il sensore è stato caratterizzato, i limiti possono essere ristretti. Per esempio, se il circuito non può produrre valori inferiori a 150 o superiori a 900:

const int adc_minimo_valido = 150;
const int adc_massimo_valido = 900;

Questi numeri devono derivare dal circuito reale, dalla documentazione del sensore o da una calibrazione. Non devono essere scelti arbitrariamente.

Il controllo dell’intervallo e lo scarto statistico svolgono funzioni differenti:

controllo dell’intervallo
→ rifiuta valori fisicamente o tecnicamente non ammessi

scarto degli estremi
→ riduce l’influenza dei valori più lontani all’interno della serie

Numero massimo di tentativi

Se vengono accettati soltanto i campioni compresi in un intervallo ristretto, il programma potrebbe non riuscire a raccogliere dieci valori validi.

Per evitare che l’acquisizione rimanga indefinitamente incompleta viene definito un numero massimo di tentativi:

30 tentativi

Se dopo trenta letture non sono stati ottenuti dieci campioni validi, la serie viene annullata e l’acquisizione ricomincia.

Questa condizione può indicare:

  • sensore scollegato;
  • limite configurato in modo errato;
  • tensione fuori intervallo;
  • disturbo persistente;
  • guasto del circuito.

Media filtrata e mediana

La media filtrata non è l’unico metodo disponibile.

La mediana seleziona il valore centrale della serie ordinata.

Con un numero pari di campioni si possono utilizzare i due valori centrali:

mediana = media dei due campioni centrali

La mediana è molto resistente ai valori estremi, ma non utilizza direttamente tutte le informazioni contenute nei campioni.

La media filtrata conserva invece più valori e produce un risultato graduale.

Con dieci letture e quattro estremi eliminati:

media filtrata → utilizza 6 campioni
mediana        → utilizza principalmente i 2 centrali

La scelta dipende dal segnale e dal tipo di disturbo.

Effetti e limiti del filtro

Il metodo riduce l’influenza dei valori estremi, ma non elimina ogni possibile errore.

Non corregge:

  • una tensione di riferimento errata;
  • un errore costante del sensore;
  • una calibrazione sbagliata;
  • un collegamento elettrico difettoso permanente;
  • un disturbo presente su tutti i campioni;
  • una saturazione dell’ingresso;
  • un errore nella conversione verso l’unità fisica.

Se tutti i campioni sono spostati dello stesso valore, la media filtrata rimane spostata.

Il filtro non sostituisce:

  • una corretta alimentazione;
  • il disaccoppiamento;
  • il condizionamento analogico;
  • la schermatura;
  • la calibrazione;
  • il rispetto dei limiti elettrici.

Programma completo in C

Il programma acquisisce dieci letture da A0, le ordina, elimina i due valori minori e i due maggiori e calcola la media dei sei centrali.

Il LED collegato al pin digitale 8 viene acceso quando il valore filtrato raggiunge o supera 512.

#define F_CPU 16000000UL

#include <avr/io.h>
#include <stdint.h>

#define NUMERO_CAMPIONI 10
#define SCARTO_PER_LATO 2
#define NUMERO_UTILIZZATI 6

#define ADC_MINIMO_VALIDO 0
#define ADC_MASSIMO_VALIDO 1023

#define MASSIMO_TENTATIVI 30
#define SOGLIA_LED 512

void configura_adc(void)
{
    /*
     * Riferimento AVCC.
     * Ingresso utilizzato: A0.
     */
    ADMUX = (1 << REFS0);

    /*
     * ADC abilitato.
     * Prescaler pari a 128.
     * Clock ADC pari a 125 kHz.
     */
    ADCSRA =
        (1 << ADEN)  |
        (1 << ADPS2) |
        (1 << ADPS1) |
        (1 << ADPS0);

    /*
     * Buffer digitale disattivato su A0.
     */
    DIDR0 |= (1 << ADC0D);

    /*
     * Prima conversione eseguita e scartata.
     */
    ADCSRA |= (1 << ADSC);

    while (ADCSRA & (1 << ADSC))
    {
    }

    (void)ADC;
}

uint16_t leggi_adc(void)
{
    ADCSRA |= (1 << ADSC);

    while (ADCSRA & (1 << ADSC))
    {
    }

    return ADC;
}

uint8_t lettura_valida(uint16_t valore)
{
    if (
        valore >= ADC_MINIMO_VALIDO &&
        valore <= ADC_MASSIMO_VALIDO
    )
    {
        return 1;
    }

    return 0;
}

void ordina_campioni(
    uint16_t campioni[],
    uint8_t numero
)
{
    for (uint8_t i = 1; i < numero; i++)
    {
        uint16_t valore = campioni[i];
        uint8_t posizione = i;

        while (
            posizione > 0 &&
            campioni[posizione - 1] > valore
        )
        {
            campioni[posizione] =
                campioni[posizione - 1];

            posizione--;
        }

        campioni[posizione] = valore;
    }
}

uint16_t calcola_media_filtrata(
    uint16_t campioni[]
)
{
    uint32_t somma = 0;

    ordina_campioni(
        campioni,
        NUMERO_CAMPIONI
    );

    for (
        uint8_t i = SCARTO_PER_LATO;
        i < NUMERO_CAMPIONI - SCARTO_PER_LATO;
        i++
    )
    {
        somma += campioni[i];
    }

    return (uint16_t)(
        somma / NUMERO_UTILIZZATI
    );
}

uint8_t acquisisci_media_filtrata(
    uint16_t *risultato
)
{
    uint16_t campioni[NUMERO_CAMPIONI];

    uint8_t campioni_validi = 0;
    uint8_t tentativi = 0;

    while (
        campioni_validi < NUMERO_CAMPIONI &&
        tentativi < MASSIMO_TENTATIVI
    )
    {
        uint16_t valore = leggi_adc();

        tentativi++;

        if (lettura_valida(valore))
        {
            campioni[campioni_validi] = valore;
            campioni_validi++;
        }
    }

    if (campioni_validi < NUMERO_CAMPIONI)
    {
        return 0;
    }

    *risultato =
        calcola_media_filtrata(campioni);

    return 1;
}

int main(void)
{
    uint16_t valore_filtrato = 0;

    /*
     * Pin digitale 8 configurato
     * come uscita per il LED.
     */
    DDRB |= (1 << DDB0);
    PORTB &= ~(1 << PORTB0);

    configura_adc();

    while (1)
    {
        if (
            acquisisci_media_filtrata(
                &valore_filtrato
            )
        )
        {
            if (valore_filtrato >= SOGLIA_LED)
            {
                PORTB |= (1 << PORTB0);
            }
            else
            {
                PORTB &= ~(1 << PORTB0);
            }
        }
        else
        {
            /*
             * Serie non valida:
             * LED spento.
             */
            PORTB &= ~(1 << PORTB0);
        }
    }
}

Spiegazione del programma in C

Le costanti:

#define NUMERO_CAMPIONI 10
#define SCARTO_PER_LATO 2
#define NUMERO_UTILIZZATI 6

definiscono la struttura del filtro.

La relazione deve rimanere coerente:

NUMERO_UTILIZZATI =
NUMERO_CAMPIONI − 2 × SCARTO_PER_LATO

Con i valori scelti:

10 − 2 × 2 = 6

La funzione:

uint8_t lettura_valida(uint16_t valore)

controlla che il campione appartenga all’intervallo ammesso.

La funzione:

void ordina_campioni(...)

ordina l’array in modo crescente.

La funzione:

uint16_t calcola_media_filtrata(...)

somma gli elementi con indice compreso tra 2 e 7:

campioni[2]
campioni[3]
campioni[4]
campioni[5]
campioni[6]
campioni[7]

Gli elementi:

campioni[0]
campioni[1]

sono i due più bassi.

Gli elementi:

campioni[8]
campioni[9]

sono i due più alti.

La somma viene memorizzata in una variabile a 32 bit:

uint32_t somma = 0;

La dimensione evita problemi quando il numero dei campioni o la risoluzione dell’ADC vengono aumentati.

La funzione:

uint8_t acquisisci_media_filtrata(
    uint16_t *risultato
)

restituisce:

1 → serie completata correttamente
0 → numero insufficiente di campioni validi

Il valore filtrato viene scritto nella variabile indicata dal puntatore risultato.

Programma completo in C++

La versione C++ raccoglie l’acquisizione e il filtraggio nella classe FiltroAnalogico.

#define F_CPU 16000000UL

#include <avr/io.h>
#include <stdint.h>

class FiltroAnalogico
{
public:
    static constexpr uint8_t numeroCampioni = 10;
    static constexpr uint8_t scartoPerLato = 2;
    static constexpr uint8_t numeroUtilizzati = 6;

    static constexpr uint16_t adcMinimoValido = 0;
    static constexpr uint16_t adcMassimoValido = 1023;

    static constexpr uint8_t massimoTentativi = 30;

    FiltroAnalogico()
    {
        configuraAdc();
    }

    bool acquisisci(uint16_t &risultato)
    {
        uint16_t campioni[numeroCampioni];

        uint8_t campioniValidi = 0;
        uint8_t tentativi = 0;

        while (
            campioniValidi < numeroCampioni &&
            tentativi < massimoTentativi
        )
        {
            uint16_t valore = leggiAdc();

            tentativi++;

            if (letturaValida(valore))
            {
                campioni[campioniValidi] = valore;
                campioniValidi++;
            }
        }

        if (campioniValidi < numeroCampioni)
        {
            return false;
        }

        ordina(campioni);

        uint32_t somma = 0;

        for (
            uint8_t i = scartoPerLato;
            i < numeroCampioni - scartoPerLato;
            i++
        )
        {
            somma += campioni[i];
        }

        risultato = static_cast<uint16_t>(
            somma / numeroUtilizzati
        );

        return true;
    }

private:
    void configuraAdc()
    {
        /*
         * Riferimento AVCC.
         * Ingresso utilizzato: A0.
         */
        ADMUX = (1 << REFS0);

        /*
         * ADC abilitato.
         * Prescaler pari a 128.
         */
        ADCSRA =
            (1 << ADEN)  |
            (1 << ADPS2) |
            (1 << ADPS1) |
            (1 << ADPS0);

        DIDR0 |= (1 << ADC0D);

        /*
         * Prima conversione eseguita
         * e scartata.
         */
        ADCSRA |= (1 << ADSC);

        while (ADCSRA & (1 << ADSC))
        {
        }

        (void)ADC;
    }

    uint16_t leggiAdc()
    {
        ADCSRA |= (1 << ADSC);

        while (ADCSRA & (1 << ADSC))
        {
        }

        return ADC;
    }

    bool letturaValida(uint16_t valore) const
    {
        return
            valore >= adcMinimoValido &&
            valore <= adcMassimoValido;
    }

    void ordina(uint16_t campioni[]) const
    {
        for (
            uint8_t i = 1;
            i < numeroCampioni;
            i++
        )
        {
            uint16_t valore = campioni[i];
            uint8_t posizione = i;

            while (
                posizione > 0 &&
                campioni[posizione - 1] > valore
            )
            {
                campioni[posizione] =
                    campioni[posizione - 1];

                posizione--;
            }

            campioni[posizione] = valore;
        }
    }
};

constexpr uint16_t sogliaLed = 512;

int main()
{
    /*
     * Pin digitale 8 configurato
     * come uscita per il LED.
     */
    DDRB |= (1 << DDB0);
    PORTB &= ~(1 << PORTB0);

    FiltroAnalogico filtro;

    while (true)
    {
        uint16_t valoreFiltrato = 0;

        if (filtro.acquisisci(valoreFiltrato))
        {
            if (valoreFiltrato >= sogliaLed)
            {
                PORTB |= (1 << PORTB0);
            }
            else
            {
                PORTB &= ~(1 << PORTB0);
            }
        }
        else
        {
            PORTB &= ~(1 << PORTB0);
        }
    }
}

Spiegazione del programma in C++

La classe:

class FiltroAnalogico

contiene:

  • configurazione dell’ADC;
  • lettura dell’ingresso;
  • controllo dei valori;
  • ordinamento;
  • calcolo della media filtrata.

La funzione pubblica:

bool acquisisci(uint16_t &risultato)

restituisce true quando sono stati acquisiti dieci campioni validi.

Il parametro:

uint16_t &risultato

è un riferimento alla variabile nella quale deve essere scritto il valore filtrato.

La funzione può quindi essere utilizzata mediante:

uint16_t valoreFiltrato = 0;

if (filtro.acquisisci(valoreFiltrato))
{
    /*
     * valoreFiltrato disponibile
     */
}

Le funzioni di configurazione, lettura e ordinamento sono private perché vengono utilizzate soltanto internamente dalla classe.

Implementazione con framework Wiring

La versione Wiring utilizza le funzioni:

analogRead()
pinMode()
digitalWrite()
millis()
Serial.begin()
Serial.print()

L’acquisizione non viene completata all’interno di una sola funzione bloccante.

Il programma mantiene invece lo stato della serie mediante alcune variabili globali:

int campioni[NUMERO_CAMPIONI];

byte campioni_validi = 0;
byte tentativi = 0;

Ogni volta che trascorrono 20 ms viene eseguita una lettura:

int valore = analogRead(pin_sensore);

Se il valore è valido, viene inserito nell’array:

campioni[campioni_validi] = valore;
campioni_validi++;

Quando sono presenti dieci campioni:

if (campioni_validi == NUMERO_CAMPIONI)

il programma:

  1. copia i valori in un secondo array;
  2. ordina la copia;
  3. elimina logicamente i primi due e gli ultimi due;
  4. calcola la media dei sei centrali;
  5. stampa i campioni acquisiti;
  6. stampa i campioni ordinati;
  7. stampa il risultato filtrato;
  8. controlla il LED;
  9. prepara una nuova serie.

L’array originale viene conservato per mostrare l’ordine reale di acquisizione.

La copia ordinata viene utilizzata per il calcolo.

Costanti della versione Wiring

Il pin analogico è definito mediante:

const int pin_sensore = A0;

Il LED utilizza:

const int pin_led = 8;

La dimensione della serie è:

const byte NUMERO_CAMPIONI = 10;

Il numero di elementi eliminati per ciascun lato è:

const byte SCARTO_PER_LATO = 2;

I valori utilizzati per la media sono:

const byte NUMERO_UTILIZZATI = 6;

L’intervallo tra due campioni è:

const unsigned long intervallo_campioni_ms = 20UL;

La soglia utilizzata per il LED è:

const int soglia_led = 512;

La soglia non modifica il filtraggio. Serve soltanto a rendere utilizzabile il risultato nell’applicazione.

Funzione di validazione

La funzione:

bool lettura_valida(int valore)

restituisce true quando il valore si trova tra il minimo e il massimo configurati.

return
    valore >= adc_minimo_valido &&
    valore <= adc_massimo_valido;

Con i limiti 0 e 1023, tutte le normali letture dell’ADC vengono accettate.

Funzione di ordinamento

La funzione:

void ordina_campioni(int valori[])

modifica direttamente l’array ricevuto.

Il ciclo esterno seleziona ogni elemento a partire dal secondo.

Il ciclo interno sposta verso destra gli elementi maggiori del valore da inserire.

Alla fine della funzione, l’array è disposto dal valore minimo al valore massimo.

Funzione di calcolo

La funzione:

int calcola_media_filtrata(int valori[])

somma i valori con indice da 2 a 7.

for (
    byte i = SCARTO_PER_LATO;
    i < NUMERO_CAMPIONI - SCARTO_PER_LATO;
    i++
)

La divisione finale utilizza:

somma / NUMERO_UTILIZZATI

La divisione è intera. L’eventuale parte decimale viene eliminata.

Gestione non bloccante nel loop

Il ciclo principale legge il tempo corrente:

unsigned long tempo_attuale = millis();

La nuova conversione viene eseguita soltanto quando è trascorso l’intervallo previsto:

if (
    tempo_attuale - tempo_ultimo_campione >=
    intervallo_campioni_ms
)

Non viene utilizzato delay().

Se il campione è valido, viene memorizzato.

Se non è valido, il numero dei campioni accettati rimane invariato, ma aumenta il numero dei tentativi.

Quando viene raggiunto il numero massimo di tentativi senza completare la serie, il programma stampa un messaggio e ricomincia da zero.

Programma Wiring completo

const int pin_sensore = A0;
const int pin_led = 8;

const byte NUMERO_CAMPIONI = 10;
const byte SCARTO_PER_LATO = 2;
const byte NUMERO_UTILIZZATI = 6;

const int adc_minimo_valido = 0;
const int adc_massimo_valido = 1023;

const byte massimo_tentativi = 30;

const int soglia_led = 512;

const unsigned long intervallo_campioni_ms = 20UL;

int campioni[NUMERO_CAMPIONI];

byte campioni_validi = 0;
byte tentativi = 0;

unsigned long tempo_ultimo_campione = 0;

bool lettura_valida(int valore)
{
    return
        valore >= adc_minimo_valido &&
        valore <= adc_massimo_valido;
}

void ordina_campioni(int valori[])
{
    for (
        byte i = 1;
        i < NUMERO_CAMPIONI;
        i++
    )
    {
        int valore = valori[i];
        byte posizione = i;

        while (
            posizione > 0 &&
            valori[posizione - 1] > valore
        )
        {
            valori[posizione] =
                valori[posizione - 1];

            posizione--;
        }

        valori[posizione] = valore;
    }
}

int calcola_media_filtrata(int valori[])
{
    unsigned long somma = 0;

    for (
        byte i = SCARTO_PER_LATO;
        i < NUMERO_CAMPIONI - SCARTO_PER_LATO;
        i++
    )
    {
        somma += valori[i];
    }

    return (int)(
        somma / NUMERO_UTILIZZATI
    );
}

void stampa_campioni(
    const char testo[],
    int valori[]
)
{
    Serial.print(testo);

    for (
        byte i = 0;
        i < NUMERO_CAMPIONI;
        i++
    )
    {
        Serial.print(valori[i]);

        if (i < NUMERO_CAMPIONI - 1)
        {
            Serial.print(' ');
        }
    }

    Serial.println();
}

void elabora_serie()
{
    int campioni_ordinati[NUMERO_CAMPIONI];

    /*
     * Copia dei campioni originali.
     */
    for (
        byte i = 0;
        i < NUMERO_CAMPIONI;
        i++
    )
    {
        campioni_ordinati[i] = campioni[i];
    }

    /*
     * Ordinamento della copia.
     */
    ordina_campioni(campioni_ordinati);

    /*
     * Calcolo della media dei sei
     * valori centrali.
     */
    int valore_filtrato =
        calcola_media_filtrata(
            campioni_ordinati
        );

    /*
     * Visualizzazione dei dati.
     */
    stampa_campioni(
        "Campioni acquisiti: ",
        campioni
    );

    stampa_campioni(
        "Campioni ordinati:  ",
        campioni_ordinati
    );

    Serial.print(
        "Valore filtrato:    "
    );

    Serial.println(valore_filtrato);
    Serial.println();

    /*
     * Utilizzo del valore filtrato.
     */
    if (valore_filtrato >= soglia_led)
    {
        digitalWrite(pin_led, HIGH);
    }
    else
    {
        digitalWrite(pin_led, LOW);
    }
}

void azzera_serie()
{
    campioni_validi = 0;
    tentativi = 0;
}

void setup()
{
    /*
     * Pin digitale 8 configurato
     * come uscita per il LED.
     */
    pinMode(pin_led, OUTPUT);
    digitalWrite(pin_led, LOW);

    /*
     * Comunicazione seriale.
     */
    Serial.begin(115200);

    /*
     * Riferimento analogico predefinito.
     */
    analogReference(DEFAULT);

    /*
     * Prima conversione eseguita
     * e scartata.
     */
    analogRead(pin_sensore);

    tempo_ultimo_campione = millis();
}

void loop()
{
    unsigned long tempo_attuale = millis();

    /*
     * Acquisizione periodica
     * senza delay().
     */
    if (
        tempo_attuale - tempo_ultimo_campione >=
        intervallo_campioni_ms
    )
    {
        tempo_ultimo_campione = tempo_attuale;

        int valore =
            analogRead(pin_sensore);

        tentativi++;

        if (lettura_valida(valore))
        {
            campioni[campioni_validi] =
                valore;

            campioni_validi++;
        }

        /*
         * Serie completa.
         */
        if (
            campioni_validi ==
            NUMERO_CAMPIONI
        )
        {
            elabora_serie();
            azzera_serie();
        }

        /*
         * Numero massimo di tentativi
         * raggiunto senza completare
         * la serie.
         */
        else if (
            tentativi >= massimo_tentativi
        )
        {
            Serial.println(
                "Serie annullata: "
                "campioni validi insufficienti."
            );

            Serial.println();

            digitalWrite(pin_led, LOW);

            azzera_serie();
        }
    }

    /*
     * In questo punto possono essere
     * gestite altre funzioni.
     */
}

Collegamenti completi

Il programma può essere utilizzato con un sensore analogico compatibile con la tensione di alimentazione della scheda.

Collegamento del sensore:

VCC del sensore → +5 V
GND del sensore → GND
uscita analogica del sensore → A0

Il collegamento a 5 V è valido soltanto per sensori progettati per questa alimentazione.

Un sensore alimentato a 3,3 V deve essere collegato secondo la propria documentazione.

La tensione presente sull’uscita analogica non deve superare il limite ammesso da A0.

Il LED viene collegato mediante una resistenza in serie:

pin digitale 8
→ resistenza da 330 Ω
→ anodo del LED

catodo del LED
→ GND

Tutti i componenti devono condividere la stessa massa:

GND del sensore
GND del LED
GND della scheda

Per verificare il programma senza un sensore è possibile utilizzare un potenziometro lineare da 10 kΩ:

primo terminale esterno → +5 V
terminale centrale → A0
secondo terminale esterno → GND

Il potenziometro produce una tensione regolabile tra circa 0 V e 5 V.

Lo schema complessivo è:

+5 V
 |
sensore analogico
 |
uscita analogica ───────── A0
 |
GND ────────────────────── GND comune


pin digitale 8
 |
resistenza 330 Ω
 |
anodo LED
 |
catodo LED
 |
GND comune

Fonti principali

Microchip Technology, ATmega328P Data Sheet — convertitore analogico-digitale, multiplexer, tensione di riferimento, clock ADC e caratteristiche elettriche.
Microchip Technology, documentazione AVR ADC — campionamento, impedenza della sorgente e conversione analogico-digitale.
Wiring Language Reference — analogRead(), analogReference(), millis(), pinMode(), digitalWrite() e comunicazione seriale.
Documentazione tecnica del sensore analogico utilizzato — alimentazione, intervallo della tensione di uscita, tempi di risposta e condizioni operative.