/*
* LCD 16x2 / HD44780U: bus parallelo a 8 bit, senza librerie LCD.
* Scheda: Arduino Uno, 5 V, 16 MHz.
* Datasheet locale: HD44780U.pdf, pp. 24-25, 45, 52, 58.
* Sono usate soltanto le primitive del core Arduino.
* RW e' collegato a GND: attese fisse, nessuna lettura del busy flag.
*
* (c) Filippo Bilardo
* versione 1.0, 19/09/2026
*/
// Fornisce pinMode(), digitalWrite(), delay(), delayMicroseconds() e millis().
// E' il core della scheda Arduino, non una libreria di gestione del display.
#include <Arduino.h>
// COLLEGAMENTI: i numeri indicano i pin digitali di Arduino Uno.
// uint8_t e' un intero senza segno di 8 bit (valori da 0 a 255).
// const impedisce al programma di modificare queste associazioni.
// RS (Register Select): LOW = comando, HIGH = dato/carattere.
const uint8_t LCD_RS = 10;
// E (Enable): l'impulso autorizza l'acquisizione del byte sul bus.
const uint8_t LCD_E = 11;
// Indice = numero del bit LCD: DB0 -> D2, ... DB7 -> D9.
// DB0 e' il bit meno significativo; DB7 quello piu' significativo.
const uint8_t LCD_DB[8] = {2, 3, 4, 5, 6, 7, 8, 9};
// TEMPI DEL BUS: espressi in microsecondi (1 us = 1000 ns).
// Setup: stabilita' prima dell'impulso; hold: stabilita' dopo l'impulso.
// Le chiamate digitalWrite() aggiungono tempo alle attese esplicite.
const unsigned int BUS_SETUP_US = 2;
const unsigned int ENABLE_HIGH_US = 2;
const unsigned int BUS_HOLD_US = 2;
// TEMPI DI ESECUZIONE: dopo l'impulso il controller deve ancora lavorare.
// Non leggendo il busy flag, attendiamo un tempo sufficiente per terminare.
// Margine anche per fOSC = 190 kHz: 37 * 270/190 = 52.6 us.
const unsigned int EXEC_US = 100;
// Clear/home: 1520 * 270/190 = 2160 us circa.
const unsigned int CLEAR_HOME_US = 3000;
// Un trasferimento elettrico. Non attende l'esecuzione del comando.
// value contiene il byte; isData distingue un dato (true) da un comando (false).
/*
* TEMPORIZZAZIONE DELLA SCRITTURA A 8 BIT
* un solo impulso E trasferisce DB7..DB0; RW e' sempre collegato a massa.
* Il tempo scorre verso destra; le larghezze non sono in scala.
* Le doppie tracce RS/DB indicano un valore stabile, non un livello alto.
*
* t0 t1 t2 t3
* | | | |
* ______ _________________________________________________
* RS ______>---<____________ RS valido: 0 oppure 1 _______________
*
* RW ___________________________________________________________
* _____________
* E _________________________/ \___________________
* __________ ________________________________________________
* DB __________X_______________ BYTE VALIDO _____________________
* | | | |
* |<-- setup -->|<-- E alto-->|<--- hold -->|
* | 2 us | 2 us | 2 us |
* ^
* acquisizione sul fronte di discesa
*
* t0: tutti gli otto dati sono pronti; RS e' stato impostato prima.
* t1: E sale dopo BUS_SETUP_US; t2: E scende dopo ENABLE_HIGH_US.
* t3: termina BUS_HOLD_US; lcdWriteBus() puo' ritornare al chiamante.
* Anche dopo t3 RS e dati restano invariati finche' non vengono riscritti.
*
* Confronto con il datasheet a 4.5..5.5 V (p. 52, figura 25 p. 58):
* tAS >= 40 ns: RS stabile prima di t1 (qui almeno 2 us).
* tDSW >= 80 ns: DB stabili prima di t2 (qui almeno 4 us).
* PW_EH >= 230 ns: E alto da t1 a t2 (qui almeno 2 us).
* tAH >= 10 ns: RS stabile dopo t2 (qui almeno 2 us).
* tH >= 10 ns: DB stabili dopo t2 (qui almeno 2 us).
* tcycE >= 500 ns: fra due salite E (gia' almeno 6 us di sole attese
* del bus, cui si aggiunge l'attesa di esecuzione).
* I valori "qui" contano le attese esplicite: digitalWrite() e le altre
* istruzioni allungano gli intervalli. Non sono misure della simulazione.
* L'attesa di esecuzione interna NON e' svolta da questa funzione.
*/
void lcdWriteBus(uint8_t value, bool isData) {
// Durante la preparazione dei segnali E deve rimanere basso.
digitalWrite(LCD_E, LOW);
// Operatore ternario: se isData e' vero seleziona HIGH, altrimenti LOW.
digitalWrite(LCD_RS, isData ? HIGH : LOW);
for (uint8_t bit = 0; bit < 8; ++bit) {
// >> porta il bit desiderato in posizione 0; & 0x01 isola quel bit.
// Esempio: per value=0x41 ('A') risultano alti DB6 e DB0.
// I fili cambiano uno alla volta, ma saranno tutti stabili all'acquisizione:
// il trasferimento al display e' quindi parallelo, un byte per impulso.
digitalWrite(LCD_DB[bit], (value >> bit) & 0x01);
}
// tAS si misura prima della salita di E, tDSW prima della discesa.
// Stabilizzare entrambi prima della salita rispetta tutti e due (pp. 52, 58).
delayMicroseconds(BUS_SETUP_US);
digitalWrite(LCD_E, HIGH);
delayMicroseconds(ENABLE_HIGH_US); // PW_EH >= 230 ns a 5 V.
digitalWrite(LCD_E, LOW); // Il display acquisisce il byte.
delayMicroseconds(BUS_HOLD_US); // tH, tAH >= 10 ns.
}
// Invia un'istruzione al registro comandi (RS=0), poi ne attende la fine.
/*
* TEMPORIZZAZIONE FRA COMANDO E DATO
* Esempio: lcdCommand(0x80), poi lcdData('A') per scrivere nella prima cella.
* RS distingue i due trasferimenti; RW resta basso in entrambi.
*
* ______________________
* RS _______________________________________/ |
* RW ______________________________________________________________
* ______ ______
* E _______/ \_________________________________/ \______
* ______ ________________________________ ______________________
* DB ______X_________ 0x80 __________________X_______ 0x41 __________
* | | |
* |<- hold ->|<-- esecuzione-->| | |
* 2 us 100 us acquisizione A
*
* Dopo la discesa di E, lcdWriteBus() attende 2 us di hold.
* Poi lcdCommand() attende 100 us, oppure 3000 us per clear/home.
* Solo al ritorno si puo' preparare il byte successivo e il suo setup.
* Quindi la distanza tra gli impulsi comprende hold, esecuzione,
* preparazione delle GPIO e setup: non coincide con il solo EXEC_US.
* Durante l'esecuzione il controller lavora anche se E e' basso.
*/
void lcdCommand(uint8_t command) {
lcdWriteBus(command, false);
// Return home ammette sia 0x02 sia 0x03: DB0 e' indifferente.
// La maschera 0xFE (11111110) azzera il bit 0 per riconoscere entrambi.
// Clear (0x01) cancella il testo; home riporta il cursore all'origine.
// Queste operazioni richiedono piu' tempo degli altri comandi.
if (command == 0x01 || (command & 0xFE) == 0x02) {
delayMicroseconds(CLEAR_HOME_US);
} else {
delayMicroseconds(EXEC_US);
}
}
// Invia un codice carattere al registro dati (RS=1).
// Con la configurazione usata, il controller lo memorizza nella DDRAM
// alla posizione corrente e incrementa automaticamente l'indirizzo.
// Sequenza temporale di lcdData():
// RS=1, DB pronti -> setup 2 us -> E alto 2 us -> E basso -> hold 2 us
// -> esecuzione 100 us -> ritorno al chiamante.
// Il diagramma di lcdWriteBus() vale anche qui, con RS mantenuto alto.
void lcdData(uint8_t character) {
lcdWriteBus(character, true);
delayMicroseconds(EXEC_US);
}
// Prepara le uscite e porta il display in uno stato iniziale conosciuto.
/*
* TEMPORIZZAZIONE DELL'INIZIALIZZAZIONE (figura 23, p. 45)
* Ogni /\ rappresenta un impulso E completo; il byte si acquisisce
* sul lato discendente. RS=0 e RW=0 per tutta la sequenza.
*
* RS ______________________________________________________________
* RW ______________________________________________________________
* E __________/\____________/\____________/\____________/\____ ...
* Byte 0x30 0x30 0x30 0x38
* |--50 ms--| |---5 ms----| |--150 us---| |--150 us---|
*
* Le etichette indicano le chiamate delay()/delayMicroseconds():
* gli intervalli reali includono anche hold, GPIO e setup del nuovo byte.
* Dopo l'ultimo impulso mostrato si completa la configurazione:
*
* byte: 0x38 0x08 0x01 0x06 0x0C
* E: __/\_________/\_________/\__________/\_________/\__
* attesa: 100 us 100 us 3000 us 100 us 100 us
*
* Qui "attesa" e' il ritardo di esecuzione aggiunto da lcdCommand(),
* dopo i 2 us di hold di ogni impulso. I 3000 us servono per il clear.
* Le scale dei due diagrammi sono schematiche e diverse fra loro.
*/
void lcdBegin() {
// Imposta a zero il valore memorizzato per ciascuna uscita prima di abilitarla.
// In questo modo E parte basso e non produce un impulso alto indesiderato.
digitalWrite(LCD_E, LOW);
pinMode(LCD_E, OUTPUT);
digitalWrite(LCD_RS, LOW);
pinMode(LCD_RS, OUTPUT);
for (uint8_t bit = 0; bit < 8; ++bit) {
digitalWrite(LCD_DB[bit], LOW);
pinMode(LCD_DB[bit], OUTPUT);
}
// Inizializzazione software, figura 23, pagina 45.
// Si assume che LCD e Arduino siano alimentati insieme a 5 V.
// delay() usa millisecondi; delayMicroseconds() usa microsecondi.
// I tre Function set 0x30 iniziali stabiliscono l'interfaccia a 8 bit.
// Qui usiamo lcdWriteBus(): le attese speciali sono indicate esplicitamente.
delay(50); // > 15 ms dopo VCC >= 4.5 V.
lcdWriteBus(0x30, false);
delay(5); // > 4.1 ms dopo il primo Function set.
lcdWriteBus(0x30, false);
delayMicroseconds(150); // > 100 us dopo il secondo.
lcdWriteBus(0x30, false);
delayMicroseconds(150); // Attesa esecuzione del terzo.
// Configurazione definitiva: i valori esadecimali codificano i bit di comando.
// 0x38 = 00111000: DL sceglie il bus, N le righe, F la matrice del carattere.
lcdCommand(0x38); // DL=1: 8 bit; N=1: 2 righe; F=0: 5x8.
lcdCommand(0x08); // Display spento durante la preparazione.
lcdCommand(0x01); // Clear: richiede l'attesa lunga!
lcdCommand(0x06); // Incremento indirizzo, nessuno shift.
lcdCommand(0x0C); // Display acceso, cursore e blink spenti.
}
// Coordinate a partire da zero. Fuori dal rettangolo 16x2: nessun invio.
// column: 0..15; row: 0..1. La DDRAM e' la memoria dei caratteri visualizzati.
void lcdSetCursor(uint8_t column, uint8_t row) {
if (column >= 16 || row >= 2) {
return;
}
// La seconda riga non segue la prima all'indirizzo 16: parte da 0x40.
const uint8_t baseAddress = (row == 0) ? 0x00 : 0x40;
// OR con 0x80 imposta il bit 7, che identifica il comando Set DDRAM Address.
// Esempi: (0,0) -> 0x80; (0,1) -> 0xC0; (9,1) -> 0xC9.
lcdCommand(0x80 | (baseAddress + column));
}
// Scrive esattamente 16 celle: tronca il testo lungo e completa con spazi.
// Usare testo ASCII: il display non interpreta le sequenze UTF-8.
// text deve puntare a una stringa valida terminata dal carattere nullo '\0'.
// const char * permette di avanzare il puntatore senza modificare il testo.
void lcdPrintLine(uint8_t row, const char *text) {
if (row >= 2) {
return;
}
lcdSetCursor(0, row); // Ogni scrittura riparte dalla prima colonna.
for (uint8_t column = 0; column < 16; ++column) {
// *text legge il carattere puntato; '\0' segnala la fine della stringa.
if (*text != '\0') {
// *text++ usa il carattere corrente e poi avanza al successivo.
// static_cast converte il carattere nel byte richiesto da lcdData().
lcdData(static_cast<uint8_t>(*text++));
} else {
// Gli spazi cancellano eventuali caratteri di una precedente riga lunga.
// Il puntatore resta sul terminatore: non si legge oltre la stringa.
lcdData(' ');
}
}
}
// Converte il valore numerico in sei cifre ASCII e aggiorna solo quelle celle.
// seconds e' passato per valore: le divisioni non modificano secondsCount.
void showSeconds(unsigned long seconds) {
// E' un array di sei caratteri, non una stringa terminata da '\0':
// lo inviamo con un ciclo di lunghezza nota, senza funzioni per stringhe.
char digits[6];
// int8_t ha segno: dopo l'indice 0, i diventa -1 e il ciclo termina.
// Un indice senza segno ripartirebbe da 255 invece di diventare negativo.
for (int8_t i = 5; i >= 0; --i) {
// % 10 estrae l'ultima cifra; sommare '0' produce il suo codice ASCII.
// / 10 elimina la cifra appena estratta. Si riempie da destra a sinistra.
// Esempio: 42 diventa {'0', '0', '0', '0', '4', '2'}.
digits[i] = '0' + seconds % 10;
seconds /= 10;
}
// "Secondi: " occupa nove colonne (0..8); le cifre occupano le colonne 9..14.
lcdSetCursor(9, 1); // 0x80 | (0x40 + 9) = 0xC9.
for (uint8_t i = 0; i < 6; ++i) {
lcdData(digits[i]);
}
}
// Variabili globali: mantengono il proprio valore tra le chiamate a loop().
// Su Arduino Uno unsigned long e' un intero senza segno di 32 bit.
unsigned long lastTickMs = 0; // Riferimento temporale dell'ultimo secondo.
unsigned long secondsCount = 0; // Valore da mostrare, compreso tra 0 e 999999.
// Il core Arduino esegue setup() una sola volta dopo l'accensione o il reset.
void setup() {
lcdBegin();
lcdPrintLine(0, "Bus parallelo 8b");
lcdPrintLine(1, "Secondi: 000000");
// Il conteggio parte dopo l'inizializzazione e la scrittura dei messaggi.
lastTickMs = millis();
}
// Il core richiama loop() continuamente. Tra gli aggiornamenti non c'e'
// un delay di un secondo: il programma controlla il tempo e puo' fare altro.
// Le brevi attese del driver LCD rimangono invece bloccanti e necessarie.
void loop() {
// millis() restituisce il tempo dall'avvio, in millisecondi.
const unsigned long now = millis();
// Sottrazione unsigned: funziona anche al rollover di millis().
// Su Uno millis() riparte da zero dopo circa 49,7 giorni.
// Il suffisso UL indica costanti di tipo unsigned long.
if (now - lastTickMs >= 1000UL) {
// Avanza di un intervallo esatto, senza accumulare il tempo di scrittura LCD.
// Se il ciclo ritarda, le chiamate successive recuperano un secondo per volta.
lastTickMs += 1000UL;
// Il modulo limita il numero a sei cifre: dopo 999999 si torna a zero.
secondsCount = (secondsCount + 1UL) % 1000000UL;
// Nessun clear: si riscrivono solo le sei cifre, evitando lo sfarfallio.
showSeconds(secondsCount);
}
}