Allenamento per l’esame di maturità
Percorso di laboratorio con Arduino per studenti di quinta ITIS

Obiettivo didattico
Scansionare una tastiera 4×4 senza usare una libreria esterna, acquisire una sequenza di tasti, confrontarla con un codice corretto e fornire un feedback di esito positivo o errore. L’attività allena gestione di matrici di pin, scansione righe/colonne, buffer di input e logica non bloccante.
Materiali suggeriti
- Arduino UNO R3 o UNO R4;
- tastiera 4×4;
- LED verde;
- LED rosso,
- buzzer opzionale;
- 2 resitori da 220 Ohm (per i LED);
- breadboard;
- cavetti jumper.
Schema di collegamento
Richiamo teorico
Una tastiera a matrice riduce il numero di fili usando righe e colonne. Il microcontrollore attiva una riga alla volta e legge le colonne. Se una colonna va a livello attivo, il tasto corrispondente a quella riga e colonna è premuto. Anche qui bisogna evitare il rimbalzo e gestire l’ingresso come sequenza di eventi.
Schema logico dell’attività
Il programma scansiona una riga alla volta. Quando rileva un tasto stabile, lo aggiunge al buffer. Se viene premuto # confronta il buffer con il codice atteso. Se il codice è corretto accende il LED verde. Se è errato accende il LED rosso, svuota il buffer e ricomincia.
Diagramma di flusso
Diagramma di flusso Mermaid
flowchart TD
A[Inizio] --> B[Configura righe, colonne e LED]
B --> C[Scansione tastiera]
C --> D{Tasto valido trovato?}
D -- No --> C
D -- Sì --> E{Tasto uguale a # ?}
E -- No --> F[Aggiungi carattere al buffer]
F --> C
E -- Sì --> G[Confronta buffer con codice]
G --> H{Codice corretto?}
H -- Sì --> I[Feedback verde e reset buffer]
H -- No --> J[Feedback rosso e reset buffer]
I --> C
J --> C
Programma
/*
Prof. Maffucci Michele
Esercizio 2: Tastiera 4x4 non bloccante con codice di accesso e feedback di errore
*/
// ---------------------------
// Pin delle 4 righe
// ---------------------------
const int righe[4] = { 2, 3, 4, 5 };
// ---------------------------
// Pin delle 4 colonne
// ---------------------------
const int colonne[4] = { 6, 7, 8, 9 };
// ---------------------------
// Mappa dei tasti
// ---------------------------
char mappaTasti[4][4] = {
{ '1', '2', '3', 'A' },
{ '4', '5', '6', 'B' },
{ '7', '8', '9', 'C' },
{ '*', '0', '#', 'D' }
};
// ---------------------------
// LED di feedback
// ---------------------------
const int PIN_LED_VERDE = 10;
const int PIN_LED_ROSSO = 11;
// ---------------------------
// Codice corretto da inserire
// ---------------------------
char codiceCorretto[] = "2580";
// ---------------------------
// Buffer di ingresso utente
// ---------------------------
char bufferInput[8];
int indiceInput = 0;
// ---------------------------
// Variabili per antirimbalzo
// ---------------------------
char ultimoTastoLetto = '\0';
char ultimoTastoConfermato = '\0';
unsigned long istanteCambio = 0;
const unsigned long TEMPO_DEBOUNCE = 40;
void setup() {
// Le righe vengono pilotate in uscita.
for (int i = 0; i < 4; i = i + 1) {
pinMode(righe[i], OUTPUT);
digitalWrite(righe[i], HIGH);
}
// Le colonne sono ingressi con pull-up.
for (int i = 0; i < 4; i = i + 1) {
pinMode(colonne[i], INPUT_PULLUP);
}
pinMode(PIN_LED_VERDE, OUTPUT);
pinMode(PIN_LED_ROSSO, OUTPUT);
Serial.begin(9600);
svuotaBuffer();
}
void loop() {
// Leggo il tasto corrente tramite scansione.
char tastoCorrente = leggiTastiera();
// Se è cambiato il valore grezzo, aggiorno il tempo.
if (tastoCorrente != ultimoTastoLetto) {
ultimoTastoLetto = tastoCorrente;
istanteCambio = millis();
}
// Se il valore è stabile da abbastanza tempo, lo confermo.
if ((millis() - istanteCambio) >= TEMPO_DEBOUNCE) {
if (tastoCorrente != ultimoTastoConfermato) {
ultimoTastoConfermato = tastoCorrente;
// Elaboro il tasto solo quando è reale e non nullo.
if (ultimoTastoConfermato != '\0') {
gestisciTasto(ultimoTastoConfermato);
}
}
}
}
// ----------------------------------------------------------
// Scansione manuale della tastiera.
// Attivo una riga alla volta e leggo tutte le colonne.
// ----------------------------------------------------------
char leggiTastiera() {
for (int r = 0; r < 4; r = r + 1) {
// Prima porto tutte le righe alte.
for (int i = 0; i < 4; i = i + 1) {
digitalWrite(righe[i], HIGH);
}
// Poi attivo solo la riga corrente.
digitalWrite(righe[r], LOW);
// Leggo tutte le colonne.
for (int c = 0; c < 4; c = c + 1) {
if (digitalRead(colonne[c]) == LOW) {
return mappaTasti[r][c];
}
}
}
// Se non trovo alcun tasto, restituisco nullo.
return '\0';
}
// ----------------------------------------------------------
// Gestione del carattere ricevuto.
// * cancella, # conferma, altri tasti vengono memorizzati.
// ----------------------------------------------------------
void gestisciTasto(char tasto) {
Serial.print("Tasto ricevuto: ");
Serial.println(tasto);
if (tasto == '*') {
svuotaBuffer();
Serial.println("Buffer cancellato");
} else if (tasto == '#') {
verificaCodice();
} else {
if (indiceInput < 7) {
bufferInput[indiceInput] = tasto;
indiceInput = indiceInput + 1;
bufferInput[indiceInput] = '\0';
}
}
}
// ----------------------------------------------------------
// Confronto del buffer con il codice corretto.
// ----------------------------------------------------------
void verificaCodice() {
bool corretto = true;
for (int i = 0; codiceCorretto[i] != '\0'; i = i + 1) {
if (bufferInput[i] != codiceCorretto[i]) {
corretto = false;
}
}
// Verifico anche la lunghezza.
if (indiceInput != 4) {
corretto = false;
}
if (corretto == true) {
Serial.println("CODICE CORRETTO");
digitalWrite(PIN_LED_VERDE, HIGH);
delay(500);
digitalWrite(PIN_LED_VERDE, LOW);
} else {
Serial.println("CODICE ERRATO");
digitalWrite(PIN_LED_ROSSO, HIGH);
delay(500);
digitalWrite(PIN_LED_ROSSO, LOW);
}
svuotaBuffer();
}
// ----------------------------------------------------------
// Ripulisce il buffer e rimette l'indice a zero.
// ----------------------------------------------------------
void svuotaBuffer() {
for (int i = 0; i < 8; i = i + 1) {
bufferInput[i] = '\0';
}
indiceInput = 0;
}
01. Spiegazione generale
Questo sketch permette di gestire una tastiera 4×4 gestita senza librerie esterne, acquisisce i tasti premuti, costruisce una sequenza in memoria e la confronta con un codice corretto. Se il codice inserito è giusto accende il LED verde, se è sbagliato accende il LED rosso.
L’aspetto interessante è che la tastiera viene letta con una scansione manuale riga per riga, senza usare funzioni bloccanti per la lettura dei tasti.
È presente anche una gestione dell’antirimbalzo software.
Struttura generale del programma
Il programma è composto da queste parti:
- definizione dei pin di righe e colonne;
- definizione della mappa dei tasti;
- definizione del codice corretto e del buffer di input;
- inizializzazione dei pin nel setup();
- scansione continua della tastiera nel loop();
- gestione del tasto ricevuto;
- verifica del codice inserito;
- svuotamento del buffer.
02. Definizione delle righe e delle colonne
const int righe[4] = { 2, 3, 4, 5 };
const int colonne[4] = { 6, 7, 8, 9 };
Qui vengono creati due array:
righe[4]contiene i pin collegati alle 4 righe della tastiera;colonne[4]contiene i pin collegati alle 4 colonne.
La tastiera 4×4 ha infatti 16 tasti, ma non richiede 16 fili separati.
Ogni tasto si trova all’incrocio tra una riga e una colonna.
Quando si preme un tasto, viene messa in contatto una determinata riga con una determinata colonna.
03. Mappa dei tasti
char mappaTasti[4][4] = {
{ '1', '2', '3', 'A' },
{ '4', '5', '6', 'B' },
{ '7', '8', '9', 'C' },
{ '*', '0', '#', 'D' }
};
Questa matrice serve per associare a ogni posizione fisica il carattere corretto.
Per esempio:
- riga 0, colonna 0 >
'1' - riga 0, colonna 1 >
'2' - riga 3, colonna 2 >
'#'
Quando durante la scansione il programma rileva che è stato premuto il tasto alla posizione [r], restituisce il carattere:
mappaTasti[r]
Quindi la matrice non legge i pin: traduce la posizione elettrica nel simbolo del tasto.
04. LED di feedback
const int PIN_LED_VERDE = 10; const int PIN_LED_ROSSO = 11;
Sono i due LED usati per segnalare l’esito:
- verde = codice corretto;
- rosso = codice errato.
05. Codice corretto da confrontare
char codiceCorretto[] = "2580";
Il codice corretto è memorizzato come una stringa di caratteri.
In C/C++ una stringa termina sempre con un carattere speciale invisibile chiamato terminatore nullo:
'\0'
Quindi in memoria "2580" è in realtà:
'2''5''8''0''\0'
Questo dettaglio è importante perché il programma usa proprio '\0' per capire dove finisce la stringa.
06. Buffer di ingresso utente
char bufferInput[8]; int indiceInput = 0;
bufferInput è il vettore che contiene i tasti digitati dall’utente.
Per esempio, se l’utente preme:
2, 5, 8
allora il buffer conterrà:
{'2', '5', '8', '\0', ...}
La variabile indiceInput indica quanti caratteri sono già stati inseriti e in quale posizione scrivere il prossimo.
La dimensione è 8, quindi si possono memorizzare fino a 7 caratteri più il terminatore '\0'.
07. Variabili per l’antirimbalzo
char ultimoTastoLetto = '\0'; char ultimoTastoConfermato = '\0'; unsigned long istanteCambio = 0; const unsigned long TEMPO_DEBOUNCE = 40;
Queste variabili servono per stabilizzare la lettura del tasto.
Significato delle variabili
- ultimoTastoLetto: ultimo valore grezzo letto dalla tastiera;
- ultimoTastoConfermato: ultimo tasto considerato valido;
- istanteCambio: istante in cui il valore è cambiato;
- TEMPO_DEBOUNCE: tempo minimo di stabilità prima di considerare valido un tasto.
Quando un pulsante o un tasto viene premuto, il contatto meccanico non è perfetto: può oscillare molto rapidamente tra ON e OFF per alcuni millisecondi. Questo fenomeno, come già abbiamo visto in molte occasioni, si chiama rimbalzo.
Senza debounce, una singola pressione potrebbe essere interpretata come più pressioni.
08. Inizializzazione nel setup()
void setup() {
for (int i = 0; i < 4; i = i + 1) {
pinMode(righe[i], OUTPUT);
digitalWrite(righe[i], HIGH);
}
for (int i = 0; i < 4; i = i + 1) {
pinMode(colonne[i], INPUT_PULLUP);
}
pinMode(PIN_LED_VERDE, OUTPUT);
pinMode(PIN_LED_ROSSO, OUTPUT);
Serial.begin(9600);
svuotaBuffer();
}
Cosa fa il setup()
a. Configura le righe come uscite
pinMode(righe[i], OUTPUT); digitalWrite(righe[i], HIGH);
Ogni riga viene impostata come uscita e inizialmente portata a livello alto.
Questo significa che, quando non la stiamo scansionando, ogni riga resta inattiva.
b. Configura le colonne come ingressi con pull-up
pinMode(colonne[i], INPUT_PULLUP);
Le colonne vengono lette come ingressi.
Con INPUT_PULLUP, ogni colonna è normalmente a livello logico HIGH.
Se però una colonna viene collegata a una riga portata a LOW tramite la pressione di un tasto, allora quella colonna viene letta LOW.
Quindi:
- tasto non premuto > colonna HIGH
- tasto premuto sulla riga attiva > colonna LOW
c. Configura i LED
I due LED diventano uscite.
d. Avvia la seriale
Serve per monitorare il comportamento del programma.
e. Svuota il buffer iniziale
Il buffer viene pulito prima di cominciare.
09. Il cuore del programma: il loop()
void loop() {
char tastoCorrente = leggiTastiera();
if (tastoCorrente != ultimoTastoLetto) {
ultimoTastoLetto = tastoCorrente;
istanteCambio = millis();
}
if ((millis() - istanteCambio) >= TEMPO_DEBOUNCE) {
if (tastoCorrente != ultimoTastoConfermato) {
ultimoTastoConfermato = tastoCorrente;
if (ultimoTastoConfermato != '\0') {
gestisciTasto(ultimoTastoConfermato);
}
}
}
}
Il loop() viene eseguito continuamente e svolge tre operazioni.
Operazione 1: legge il valore grezzo della tastiera
char tastoCorrente = leggiTastiera();
Questa funzione restituisce:
- il carattere del tasto premuto, ad esempio
'5'; '\0'se nessun tasto è premuto.
Operazione 2: controlla se il valore è cambiato
if (tastoCorrente != ultimoTastoLetto) {
ultimoTastoLetto = tastoCorrente;
istanteCambio = millis();
}
Se cambia il valore letto, il programma:
- aggiorna
ultimoTastoLetto; - memorizza il tempo del cambiamento.
In pratica dice:
"Ho visto una variazione, adesso aspetto un po’ per vedere se è stabile".
Operazione 3: conferma il tasto solo se resta stabile abbastanza a lungo
if ((millis() - istanteCambio) >= TEMPO_DEBOUNCE) {
Se il valore resta uguale per almeno 40 ms, allora può essere considerato stabile.
Poi verifica anche che non sia già stato confermato prima:
if (tastoCorrente != ultimoTastoConfermato) {
Questo evita di registrare più volte lo stesso tasto mentre viene tenuto premuto.
Infine:
if (ultimoTastoConfermato != '\0') {
gestisciTasto(ultimoTastoConfermato);
}
La funzione gestisciTasto() viene chiamata solo per tasti reali, non quando nessun tasto è premuto.
Importante
Questo meccanismo fa sì che:
- una pressione venga accettata una sola volta;
- il rilascio del tasto venga rilevato, ma non elaborato come comando;
- il sistema sia pronto per la pressione successiva.
10. Scansione manuale della tastiera
char leggiTastiera() {
for (int r = 0; r < 4; r = r + 1) {
for (int i = 0; i < 4; i = i + 1) {
digitalWrite(righe[i], HIGH);
}
digitalWrite(righe[r], LOW);
for (int c = 0; c < 4; c = c + 1) {
if (digitalRead(colonne) == LOW) {
return mappaTasti[r];
}
}
}
return '\0';
}
Questa è la parte più importante del programma.
Come funziona la scansione
Il programma prova una riga alla volta.
Passo 1: porta tutte le righe a HIGH
for (int i = 0; i < 4; i = i + 1) {
digitalWrite(righe[i], HIGH);
}
Serve a "resettare" la matrice prima di attivare una nuova riga.
Passo 2: attiva una sola riga mettendola a LOW
digitalWrite(righe[r], LOW);
Solo la riga corrente viene attivata.
Passo 3: legge tutte le colonne
for (int c = 0; c < 4; c = c + 1) {
if (digitalRead(colonne) == LOW) {
return mappaTasti[r];
}
}
Se una colonna risulta LOW, significa che il tasto all’incrocio tra quella riga e quella colonna è premuto.
A quel punto il programma restituisce il carattere corrispondente.
Esempio
Supponiamo di premere il tasto 5.
Dalla matrice:
{ '4', '5', '6', 'B' }
5 si trova in:
- riga 1
- colonna 1
Quando il programma porta a LOW la riga 1 e legge la colonna 1, troverà LOW e restituirà:
mappaTasti[1][1]
cioè '5'.
Se nessun tasto è premuto
La funzione restituisce:
'\0'
che significa "nessun carattere".
11. Gestione del tasto premuto
void gestisciTasto(char tasto) {
Serial.print("Tasto ricevuto: ");
Serial.println(tasto);
if (tasto == '*') {
svuotaBuffer();
Serial.println("Buffer cancellato");
} else if (tasto == '#') {
verificaCodice();
} else {
if (indiceInput < 7) {
bufferInput[indiceInput] = tasto;
indiceInput = indiceInput + 1;
bufferInput[indiceInput] = '\0';
}
}
}
Questa funzione decide cosa fare con il tasto confermato.
Caso 1: tasto *
if (tasto == '*') {
svuotaBuffer();
}
Il tasto * funziona come cancella.
Tutto il contenuto del buffer viene azzerato.
Caso 2: tasto #
else if (tasto == '#') {
verificaCodice();
}
Il tasto # funziona come invio/conferma, quando l’utente lo preme, il programma confronta il buffer con il codice corretto.
Caso 3: qualunque altro tasto
else {
if (indiceInput < 7) {
bufferInput[indiceInput] = tasto;
indiceInput = indiceInput + 1;
bufferInput[indiceInput] = '\0';
}
}
Se il tasto non è né * né #, allora viene aggiunto al buffer.
Perché viene scritto anche '\0'?
bufferInput[indiceInput] = '\0';
Per mantenere il buffer sempre come stringa valida.
Esempio:
dopo aver premuto 2, 5, 8, il contenuto diventa:
bufferInput[0] = '2'bufferInput[1] = '5'bufferInput[2] = '8'bufferInput[3] = '\0'
Così il buffer è già leggibile come stringa "258".
12. Verifica del codice
void verificaCodice() {
bool corretto = true;
for (int i = 0; codiceCorretto[i] != '\0'; i = i + 1) {
if (bufferInput[i] != codiceCorretto[i]) {
corretto = false;
}
}
if (indiceInput != 4) {
corretto = false;
}
if (corretto == true) {
Serial.println("CODICE CORRETTO");
digitalWrite(PIN_LED_VERDE, HIGH);
delay(500);
digitalWrite(PIN_LED_VERDE, LOW);
} else {
Serial.println("CODICE ERRATO");
digitalWrite(PIN_LED_ROSSO, HIGH);
delay(500);
digitalWrite(PIN_LED_ROSSO, LOW);
}
svuotaBuffer();
}
Questa funzione confronta il contenuto inserito con "2580".
Passo 1: parte assumendo che sia corretto
bool corretto = true;
Passo 2: confronta carattere per carattere
for (int i = 0; codiceCorretto[i] != '\0'; i = i + 1) {
if (bufferInput[i] != codiceCorretto[i]) {
corretto = false;
}
}
Finché non incontra il terminatore '\0' del codice corretto, confronta ogni posizione.
Se trova anche una sola differenza, imposta:
corretto = false;
Passo 3: controlla anche la lunghezza
if (indiceInput != 4) {
corretto = false;
}
Questo è importante.
Infatti, anche se i primi caratteri coincidessero, un codice con lunghezza sbagliata non deve essere accettato.
Esempio:
- buffer =
"258"> sbagliato - buffer =
"25801"> sbagliato
Passo 4: segnala il risultato
Se il codice è corretto:
digitalWrite(PIN_LED_VERDE, HIGH); delay(500); digitalWrite(PIN_LED_VERDE, LOW);
Se è errato:
digitalWrite(PIN_LED_ROSSO, HIGH); delay(500); digitalWrite(PIN_LED_ROSSO, LOW);
Infine il buffer viene svuotato.
13. Svuotamento del buffer
void svuotaBuffer() {
for (int i = 0; i < 8; i = i + 1) {
bufferInput[i] = '\0';
}
indiceInput = 0;
}
Questa funzione cancella completamente il contenuto del buffer, quello che è stato appena inserito.
Cosa fa in pratica?
- mette
'\0'in tutte le celle; - riporta l’indice a zero.
In questo modo il sistema è pronto per una nuova digitazione.
Importante
E' necessario svuotare il buffer perché una nuova pressione del tasto # senza aver digitato altri caratteri non ripete il confronto sul vecchio codice, ma viene interpretata come tentativo vuoto e quindi produce errore. Lo svuotamento serve dunque a rendere indipendenti i tentativi successivi.
Esempio completo di esecuzione
Supponiamo che l’utente prema:
2 > 5 > 8 > 0 > #
Fase 1: lettura dei tasti
La funzione leggiTastiera() rileva ogni tasto premuto.
Fase 2: debounce
Ogni tasto viene accettato solo se stabile per almeno 40 ms.
Fase 3: memorizzazione
gestisciTasto() inserisce i caratteri nel buffer:
- dopo
2>"2" - dopo
5>"25" - dopo
8>"258" - dopo
0>"2580"
Fase 4: conferma
Quando arriva #, viene chiamata verificaCodice().
Fase 5: confronto
Il buffer "2580" viene confrontato con codiceCorretto.
Fase 6: risposta
Il LED verde si accende per 500 ms.
Fase 7: reset
Il buffer viene svuotato.
Importante
Anche se l’attività viene presentata come non bloccante, questo sketch contiene ancora un punto bloccante:
delay(500);
presente nella funzione verificaCodice().
Durante quei 500 ms Arduino resta fermo e non legge la tastiera.
Quindi si precisa che:
- la lettura della tastiera è organizzata in modo non bloccante;
- il feedback finale invece è ancora bloccante.
Se si volesse realizzare una versione completamente non bloccante, bisognerebbe sostituire quei delay(500) con una gestione temporale basata su millis(), lascio a voi l'esercizio 🙂 .
Possibile miglioramento sul confronto del codice
Nel codice compare questo controllo:
if (indiceInput != 4) {
corretto = false;
}
Funziona bene perché il codice corretto è lungo 4 caratteri.
Per rendere il programma più generale, si potrebbe evitare di scrivere direttamente il numero 4 e ricavare la lunghezza dal codice corretto, lascio a voi come esercizio questa modifica.
Esercizio aggiuntivo da svolgere in autonomia

Questo esercizio è basato sull’attività proposta nella lezione, ma richiede l’aggiunta di nuove funzionalità da sviluppare in autonomia. La soluzione verrà pubblicata in un post successivo, così da favorire il lavoro personale di analisi, progettazione e sperimentazione.
Consegna per lo studente
Partendo dall'esercizio principale, estendete il sistema a tastiera 4×4 aggiungendo due funzionalità:
- dopo tre codici errati consecutivi il sistema deve rimanere bloccato per 30 secondi;
- premendo il tasto A si avvia la procedura di cambio PIN: inserimento del PIN attuale,
nuovo PIN a quattro cifre e conferma del nuovo PIN.
Di seguito la sequenza logica utile per la realizzazione del diagramma di flusso:
- scansione continua della tastiera a matrice senza usare delay().
- se il sistema è in lockout, viene visualizzato il tempo residuo e i tasti sono ignorati.
- se arriva un tasto valido, il programma aggiorna il buffer o cambia stato logico.
- con il tasto # il buffer viene verificato; con il tasto A si entra nella procedura di cambio PIN.
- dopo tre errori consecutivi si attiva il blocco di sicurezza.
Buon Coding a tutti


