// Autore: Ferrari Kevin
// ========= DEFINIZIONI DI PROGETTO (PIN, COMANDI LCD, ECC.) =========
// Definizioni dei pin del microcontrollore collegati al display LCD.
#define LCD_RS PD7 // Pin per selezionare il registro (RS): comandi (0) o dati (1). Collegato al pin PD7.
#define LCD_E PB0 // Pin di "Enable" (E): un impulso su questo pin dice all'LCD di leggere i dati sui pin D4-D7. Collegato a PB0.
#define LCD_D4 PB1 // Pin per il bit 4 dei dati (modalità a 4 bit). Collegato a PB1.
#define LCD_D5 PB2 // Pin per il bit 5 dei dati. Collegato a PB2.
#define LCD_D6 PB3 // Pin per il bit 6 dei dati. Collegato a PB3.
#define LCD_D7 PB4 // Pin per il bit 7 dei dati. Collegato a PB4.
// Definizioni dei comandi standard per un controller LCD HD44780.
#define LCD_CLEARDISPLAY 0x01 // Comando per pulire lo schermo e riportare il cursore all'inizio.
#define LCD_FUNCTIONSET 0x20 // Comando base per impostare la modalità di funzionamento.
#define LCD_DISPLAYCONTROL 0x08 // Comando base per controllare l'accensione del display, cursore, ecc.
#define LCD_ENTRYMODESET 0x04 // Comando base per definire come il cursore si sposta dopo la scrittura.
#define LCD_SETDDRAMADDR 0x80 // Comando base per impostare la posizione del cursore.
// Definizioni delle opzioni da combinare con i comandi base (usando l'operatore OR |).
#define LCD_2LINE 0x08 // Opzione per usare 2 linee del display.
#define LCD_DISPLAYON 0x04 // Opzione per accendere il display.
#define LCD_CURSOROFF 0x00 // Opzione per nascondere il cursore.
#define LCD_BLINKOFF 0x00 // Opzione per disattivare il lampeggio del cursore.
#define LCD_4BITMODE 0x00 // Opzione per la modalità di comunicazione a 4 bit.
#define LCD_5x8DOTS 0x00 // Opzione per usare caratteri di dimensioni 5x8 pixel.
// Definizioni dei pin per i sensori di temperatura e umidità DHT22.
#define DHT1_INTERNAL_PIN PD3 // Pin per il sensore DHT22 interno. Collegato a PD3.
#define DHT2_EXTERNAL_PIN PD2 // Pin per il sensore DHT22 esterno. Collegato a PD2.
// Definizioni dei pin per controllare i relè.
#define RELAY_VENTILATION_PIN_NUM PD4 // Pin per il relè della ventilazione (scambio diretto). Collegato a PD4.
#define RELAY_HEAT_RECOVERY_PIN_NUM PD5 // Pin per il relè del recuperatore di calore. Collegato a PD5.
// Definisce le maschere di bit per identificare quale bottone è stato premuto.
// (1 << PINCx) crea un numero binario con solo il bit corrispondente a PINCx impostato a 1.
#define BUTTON_PIN_GREEN_MASK (1 << PINC1) // Maschera per il bottone verde su PC1.
#define BUTTON_PIN_BLUE_MASK (1 << PINC2) // Maschera per il bottone blu su PC2.
#define BUTTON_PIN_YELLOW_MASK (1 << PINC3) // Maschera per il bottone giallo su PC3.
#define BUTTON_PIN_RED_MASK (1 << PINC4) // Maschera per il bottone rosso su PC4.
// Crea una maschera che include tutti i bottoni, utile per configurarli o leggerli tutti insieme.
#define BUTTON_ALL_MASK (BUTTON_PIN_GREEN_MASK | BUTTON_PIN_BLUE_MASK | BUTTON_PIN_YELLOW_MASK | BUTTON_PIN_RED_MASK)
// Definisce il tempo in millisecondi per il debouncing dei bottoni.
#define DEBOUNCE_TIME_MS 50
// ========= VARIABILI GLOBALI =========
// La parola chiave 'volatile' dice al compilatore che queste variabili possono cambiare inaspettatamente
// (es. da una routine di interrupt) e quindi non deve ottimizzare le letture/scritture.
volatile uint8_t co2_level = 0; // Memorizza l'ultimo livello di CO2 letto (valore 0-255).
volatile uint8_t adc_ready = 1; // Flag: 1 se l'ADC è pronto per una nuova conversione, 0 se è occupato.
volatile uint8_t new_co2_data_available = 0; // Flag: 1 se è disponibile una nuova lettura di CO2 da processare.
volatile uint8_t update_display = 0; // Flag: 1 se il display LCD ha bisogno di essere aggiornato.
volatile uint8_t read_co2_and_temp_flag = 0; // Flag: 1 per segnalare che è ora di leggere i sensori.
volatile uint8_t system_active = 0; // Flag: 1 se il sistema di ventilazione è attivo, 0 altrimenti.
volatile int16_t temp_internal_x10; // Temperatura interna moltiplicata per 10 (es. 23.5°C diventa 235).
volatile int16_t temp_external_x10; // Temperatura esterna moltiplicata per 10.
volatile uint8_t temp_read_status_internal = 1; // Stato dell'ultima lettura del sensore interno (0 = OK, 1 = Errore).
volatile uint8_t temp_read_status_external = 1; // Stato dell'ultima lettura del sensore esterno (0 = OK, 1 = Errore).
// Definizione di un tipo enumerato per gli stati della macchina a stati finiti (FSM) che gestisce il menu.
typedef enum {
STATE_MENU, // Stato del menu principale.
STATE_MODIFY_CO2, // Stato di modifica della soglia di CO2.
STATE_TEMP_MENU, // Stato del sottomenu delle temperature.
STATE_MODIFY_TEMP_MIN, // Stato di modifica della temperatura minima di benessere.
STATE_MODIFY_TEMP_MAX // Stato di modifica della temperatura massima di benessere.
} ConsoleFSMState;
volatile ConsoleFSMState current_fsm_state = STATE_MENU; // Variabile che tiene traccia dello stato corrente del menu.
volatile uint8_t menu_selection = 0; // Memorizza la selezione corrente nel menu (es. riga 0 o riga 1).
volatile uint8_t co2_threshold = 150; // Soglia di CO2 (valore 0-255) oltre la quale si attiva la ventilazione.
uint8_t editable_value; // Variabile temporanea per contenere un valore mentre viene modificato dall'utente.
volatile uint8_t temp_benessere_min = 20; // Temperatura minima per il recupero di calore.
volatile uint8_t temp_benessere_max = 25; // Temperatura massima per il recupero di calore.
// Flag che indicano la pressione di un bottone, gestiti dagli interrupt.
volatile uint8_t button_green_pressed = 0;
volatile uint8_t button_blue_pressed = 0;
volatile uint8_t button_yellow_pressed = 0;
volatile uint8_t button_red_pressed = 0;
// Variabili per la gestione del debouncing (anti-rimbalzo) dei bottoni.
volatile uint8_t debounce_timer_active = 0; // Flag: 1 se il timer di debounce è attivo.
volatile uint8_t pin_state_at_pcint; // Memorizza lo stato dei pin dei bottoni al momento dell'interrupt.
volatile uint8_t debounce_counter = 0; // Contatore per il timer di debounce.
// ========= FUNZIONI DI DELAY BASATE SU TIMER0 =========
// Funzione per creare un ritardo in microsecondi (us) usando il Timer0.
void timer0_based_delay_us(uint16_t us) {
if (us == 0) return; // Se il ritardo è 0, esce subito.
// TCCR0A: Configura il Timer0 in modalità CTC (Clear Timer on Compare Match).
TCCR0A = (1 << WGM01);
// TCCR0B: Imposta il prescaler a 8 (F_CPU / 8). Con F_CPU=16MHz, il timer incrementa ogni 0.5us.
TCCR0B = (1 << CS01);
TCNT0 = 0; // Azzera il contatore del timer.
// Calcola il numero di "tick" necessari. Ogni tick è 0.5us, quindi moltiplica per 2.
uint16_t ticks = us * 2;
if (ticks > 255) ticks = 255; // Limita il valore a 255 (massimo per un registro a 8 bit).
// Imposta il registro di confronto. OCR0A-1 perché il match avviene al tick successivo.
OCR0A = (ticks > 0) ? (uint8_t)(ticks - 1) : 0;
TIFR0 |= (1 << OCF0A); // Pulisce il flag di interrupt di confronto (scrivendo 1).
while (!(TIFR0 & (1 << OCF0A))); // Attende attivamente finché il flag di confronto (OCF0A) non viene impostato.
TCCR0B = 0; // Spegne il timer.
}
// Funzione per creare un ritardo in millisecondi (ms) usando il Timer0.
void timer0_based_delay_ms(uint16_t ms) {
if (ms == 0) return; // Se il ritardo è 0, esce subito.
// Configura il Timer0 in modalità CTC.
TCCR0A = (1 << WGM01);
// Imposta il prescaler a 64 (F_CPU / 64). Con F_CPU=16MHz, il timer va a 250kHz.
TCCR0B = (1 << CS01) | (1 << CS00);
// Con 250kHz, 250 tick corrispondono a 1ms. Imposto il confronto a 249 (da 0 a 249 sono 250 passi).
OCR0A = 249;
// Esegue un ciclo per ogni millisecondo richiesto.
for (uint16_t i = 0; i < ms; ++i) {
TCNT0 = 0; // Azzera il contatore a ogni ciclo.
TIFR0 |= (1 << OCF0A); // Pulisce il flag di interrupt.
while (!(TIFR0 & (1 << OCF0A))); // Attende che sia passato 1ms.
}
TCCR0B = 0; // Spegne il timer.
}
// ========= FUNZIONE LETTURA DHT22 =========
// Legge il sensore DHT22 e restituisce la temperatura.
// Ritorna 0 in caso di successo, 1 in caso di errore.
uint8_t read_dht22_sensor(uint8_t dht_pin_num, int16_t* temperature_x10_val)
{
uint8_t bits[5] = {0}; // Array per memorizzare i 5 byte di dati ricevuti.
uint16_t pulse_counter; // Contatore per misurare la durata degli impulsi.
cli(); // Disabilita gli interrupt globali per garantire una temporizzazione precisa.
// Fase 1: Segnale di Start dal microcontrollore
DDRD |= (1 << dht_pin_num); // Imposta il pin del sensore come OUTPUT.
PORTD &= ~(1 << dht_pin_num); // Porta il pin a livello BASSO.
timer0_based_delay_ms(5); // Mantieni basso per almeno 1ms (qui 5ms per sicurezza).
PORTD |= (1 << dht_pin_num); // Porta il pin a livello ALTO.
timer0_based_delay_us(30); // Mantieni alto per 20-40us.
// Fase 2: Attesa della risposta dal sensore
DDRD &= ~(1 << dht_pin_num); // Imposta il pin come INPUT per leggere la risposta del sensore.
// Controlla la risposta del sensore (impulso basso di ~80us seguito da impulso alto di ~80us)
pulse_counter = 0;
// Attende che il sensore porti la linea a BASSO.
while ((PIND & (1 << dht_pin_num)) && pulse_counter < 100)
{
timer0_based_delay_us(2);
pulse_counter++;
}
if(pulse_counter >= 100) { sei(); return 1; } // Timeout, errore.
pulse_counter = 0;
// Attende che il sensore porti la linea ad ALTO.
while (!(PIND & (1 << dht_pin_num)) && pulse_counter < 100)
{
timer0_based_delay_us(2);
pulse_counter++;
}
if(pulse_counter >= 100) { sei(); return 1; } // Timeout, errore.
pulse_counter = 0;
// Attende la fine dell'impulso alto di risposta.
while ((PIND & (1 << dht_pin_num)) && pulse_counter < 100)
{
timer0_based_delay_us(2);
pulse_counter++;
}
if(pulse_counter >= 100) { sei(); return 1; } // Timeout, errore.
// Fase 3: Lettura dei 40 bit di dati
// Ogni bit inizia con un impulso basso di 50us.
// Un bit '0' è seguito da un impulso alto di 26-28us.
// Un bit '1' è seguito da un impulso alto di 70us.
for (uint8_t i = 0; i < 40; i++)
{
pulse_counter = 0;
// Attende la fine dell'impulso basso che precede ogni bit.
while (!(PIND & (1 << dht_pin_num)) && pulse_counter < 100)
{
timer0_based_delay_us(2);
pulse_counter++;
}
if(pulse_counter >= 100) { sei(); return 1; } // Timeout, errore.
pulse_counter = 0;
// Misura la durata dell'impulso alto.
while ((PIND & (1 << dht_pin_num)) && pulse_counter < 100)
{
timer0_based_delay_us(2);
pulse_counter++;
}
if(pulse_counter >= 100) { sei(); return 1; } // Timeout, errore.
// Se l'impulso alto è più lungo di ~30us (15 * 2us), è un bit '1'.
if (pulse_counter > 15) {
bits[i / 8] |= (1 << (7 - (i % 8))); // Scrive il bit '1' nella posizione corretta nell'array di byte.
}
}
sei(); // Riabilita gli interrupt globali.
// Fase 4: Verifica del Checksum e conversione dei dati
// Il 5° byte è la somma dei primi 4 byte (ignorando l'overflow).
if (bits[4] == ((bits[0] + bits[1] + bits[2] + bits[3]) & 0xFF)) {
// I dati di temperatura sono nel 3° e 4° byte.
// Unisce i due byte in un valore a 16 bit.
int16_t temp_raw = ((uint16_t)(bits[2] & 0x7F) << 8) | bits[3];
// Il bit più significativo del 3° byte indica il segno.
if (bits[2] & 0x80) {
temp_raw = -temp_raw; // Se il bit è 1, la temperatura è negativa.
}
// Assegna il valore al puntatore fornito come argomento.
*temperature_x10_val = temp_raw;
return 0; // Successo.
}
return 1; // Errore di checksum.
}
// ========= FUNZIONI DI INIZIALIZZAZIONE =========
// Inizializza il Convertitore Analogico-Digitale (ADC).
void init_adc(void) {
// ADMUX: Seleziona la tensione di riferimento (AVCC) e il canale di ingresso (ADC0).
ADMUX = (1 << REFS0);
// ADCSRA: Abilita l'ADC, abilita l'interrupt di fine conversione, e imposta il prescaler a 128.
// Con F_CPU=16MHz, il clock dell'ADC sarà 125kHz, che rientra nel range consigliato (50-200kHz).
ADCSRA = (1 << ADEN) | (1 << ADIE) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
}
// Inizializza il Timer1 per generare un interrupt periodico.
void init_timer1(void) {
TCCR1A = 0; // Imposta il Timer1 in modalità normale (nessuna operazione sui pin di output).
// TCCR1B: Imposta la modalità CTC (WGM12) e il prescaler a 1024 (CS12, CS10).
TCCR1B = (1 << WGM12) | (1 << CS12) | (1 << CS10);
// Con prescaler 1024, il clock del timer è 16MHz/1024 = 15625 Hz.
// Impostando OCR1A a 15624, si ottiene un interrupt ogni 15625 tick, cioè ogni secondo.
OCR1A = 15624;
// TIMSK1: Abilita l'interrupt sul confronto con OCR1A.
TIMSK1 = (1 << OCIE1A);
}
// Inizializza il Timer2 per il debouncing dei bottoni.
void init_timer2_debounce(void) {
TCCR2A = (1 << WGM21); // Modalità CTC.
// TCCR2B: Imposta il prescaler a 1024.
TCCR2B = (1 << CS22) | (1 << CS21) | (1 << CS20);
// Con clock a 15625 Hz, 16 tick corrispondono a circa 1ms (15625/1000 ~= 15.6).
// Imposto OCR2A a 15 per generare un interrupt ogni ~1ms.
OCR2A = 15;
}
// Inizializza i pin dei bottoni.
void init_buttons(void) {
// DDRC: Imposta i pin dei bottoni (PC1-PC4) come INPUT.
DDRC &= ~BUTTON_ALL_MASK;
// PORTC: Abilita le resistenze di pull-up interne sui pin dei bottoni.
// Quando un bottone viene premuto (collegando il pin a massa), il pin leggerà un valore basso.
PORTC |= BUTTON_ALL_MASK;
// PCICR: Abilita gli interrupt su cambiamento di stato per il gruppo di pin PCINT8-PCINT14.
PCICR |= (1 << PCIE1);
// PCMSK1: Abilita l'interrupt specifico per i pin PCINT9 (PC1) a PCINT12 (PC4).
PCMSK1 |= (1 << PCINT9) | (1 << PCINT10) | (1 << PCINT11) | (1 << PCINT12);
}
// Inizializza i pin dei relè.
void init_relays(void) {
// DDRD: Imposta i pin dei relè come OUTPUT.
DDRD |= (1 << RELAY_VENTILATION_PIN_NUM) | (1 << RELAY_HEAT_RECOVERY_PIN_NUM);
// PORTD: Assicura che i relè siano spenti all'avvio (livello basso).
PORTD &= ~((1 << RELAY_VENTILATION_PIN_NUM) | (1 << RELAY_HEAT_RECOVERY_PIN_NUM));
}
// ========= FUNZIONI LCD =========
// Genera un impulso sul pin "Enable" (E) per far leggere i dati all'LCD.
void lcd_pulse_enable(void) {
PORTB |= (1 << LCD_E); // Porta E a livello ALTO.
timer0_based_delay_us(2); // Breve attesa.
PORTB &= ~(1 << LCD_E); // Porta E a livello BASSO.
timer0_based_delay_us(100); // Attesa per permettere all'LCD di processare il comando.
}
// Invia 4 bit di dati all'LCD (usato in modalità 4-bit).
void lcd_write_4bits(uint8_t value) {
// Legge lo stato attuale della porta B e maschera i bit dell'LCD.
uint8_t portb_val = PORTB & ~((1<<LCD_D4)|(1<<LCD_D5)|(1<<LCD_D6)|(1<<LCD_D7));
// Imposta i bit di dati in base al valore da inviare.
if (value & 0x01) portb_val |= (1 << LCD_D4); // bit 0 di 'value' va a D4
if (value & 0x02) portb_val |= (1 << LCD_D5); // bit 1 di 'value' va a D5
if (value & 0x04) portb_val |= (1 << LCD_D6); // bit 2 di 'value' va a D6
if (value & 0x08) portb_val |= (1 << LCD_D7); // bit 3 di 'value' va a D7
PORTB = portb_val; // Scrive il nuovo valore sulla porta B.
lcd_pulse_enable(); // Invia l'impulso di Enable.
}
// Invia un byte intero (comando o dato) all'LCD.
void lcd_write(uint8_t value, uint8_t is_data) {
// Imposta il pin RS: 1 per i dati, 0 per i comandi.
if (is_data) PORTD |= (1 << LCD_RS); else PORTD &= ~(1 << LCD_RS);
// Invia prima i 4 bit più significativi (nibble alto).
lcd_write_4bits(value >> 4);
// Invia poi i 4 bit meno significativi (nibble basso).
lcd_write_4bits(value & 0x0F);
}
// Funzioni di comodo per inviare comandi o dati.
void lcd_command(uint8_t cmd) { lcd_write(cmd, 0); }
void lcd_data(uint8_t data) { lcd_write(data, 1); }
// Inizializza il display LCD secondo la procedura standard per la modalità a 4 bit.
void lcd_init(void) {
// Imposta i pin di controllo dell'LCD (RS, E, D4-D7) come OUTPUT.
DDRD |= (1 << LCD_RS);
DDRB |= (1 << LCD_E) | (1 << LCD_D4) | (1 << LCD_D5) | (1 << LCD_D6) | (1 << LCD_D7);
timer0_based_delay_ms(50); // Attende che l'LCD si accenda e si stabilizzi.
PORTD &= ~(1 << LCD_RS); PORTB &= ~(1 << LCD_E); // Imposta RS e E a basso.
// Sequenza di inizializzazione per la modalità a 4 bit.
lcd_write_4bits(0x03); timer0_based_delay_ms(5);
lcd_write_4bits(0x03); timer0_based_delay_us(150);
lcd_write_4bits(0x03);
lcd_write_4bits(0x02); // Attiva la modalità a 4 bit.
// Ora l'LCD è in modalità 4 bit e possiamo inviare comandi completi.
lcd_command(LCD_FUNCTIONSET | LCD_4BITMODE | LCD_2LINE | LCD_5x8DOTS); // Imposta 2 linee, caratteri 5x8.
lcd_command(LCD_DISPLAYCONTROL | LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF); // Accende display, nasconde cursore.
lcd_command(LCD_CLEARDISPLAY); timer0_based_delay_ms(2); // Pulisce lo schermo.
lcd_command(LCD_ENTRYMODESET | (1 << 1)); // Imposta l'incremento automatico del cursore a destra.
}
// Pulisce lo schermo dell'LCD.
void lcd_clear(void) {
lcd_command(LCD_CLEARDISPLAY); // Invia il comando di pulizia.
timer0_based_delay_ms(2); // Attende il tempo necessario per l'esecuzione del comando.
}
// Imposta la posizione del cursore.
void lcd_set_cursor(uint8_t col, uint8_t row) {
// Indirizzi di partenza per le due righe dell'LCD.
const uint8_t row_offsets[] = { 0x00, 0x40 };
if (row > 1) row = 1; // Limita la riga a 0 o 1.
// Calcola l'indirizzo e invia il comando per posizionare il cursore.
lcd_command(LCD_SETDDRAMADDR | (col + row_offsets[row]));
}
// Stampa una stringa di caratteri sull'LCD.
void lcd_print(const char *str) {
while (*str) lcd_data(*str++); // Invia ogni carattere della stringa come dato.
}
// Stampa un numero intero senza segno a 8 bit (uint8_t).
void lcd_print_u8(uint8_t num) {
char buffer[4]; // Buffer per contenere le cifre come caratteri.
int8_t i = 0;
if (num == 0) { lcd_data('0'); return; } // Caso speciale per il numero 0.
// Converte il numero in una stringa di caratteri, ma in ordine inverso.
do { buffer[i++] = (num % 10) + '0'; num /= 10; } while (num > 0);
// Stampa i caratteri nell'ordine corretto.
while (i > 0) lcd_data(buffer[--i]);
}
// Stampa un numero intero con segno a 8 bit (int8_t).
void lcd_print_s8(int8_t num) {
if (num < 0) {
lcd_data('-'); // Stampa il segno meno.
//lcd_print_u8(-num); // Stampa il valore assoluto. --- ERRORE NEL CODICE ORIGINALE, dovrebbe essere lcd_print_u8(-num)
lcd_print_u8(num); // ERRORE NOTATO: questo stamperà un valore errato. La correzione è sopra.
} else {
lcd_print_u8(num); // Se positivo, stampa come unsigned.
}
}
// Aggiorna l'intero display LCD in base allo stato corrente del sistema e del menu.
void update_lcd_display(void) {
lcd_clear(); // Pulisce lo schermo prima di ridisegnare.
// Mostra lo stato del sistema (ON/OFF) in alto a destra.
lcd_set_cursor(9, 0);
// Controlla se uno dei due relè è attivo.
if ((PORTD & ((1 << RELAY_VENTILATION_PIN_NUM) | (1 << RELAY_HEAT_RECOVERY_PIN_NUM)))) {
lcd_print("SYS:ON ");
} else {
lcd_print("SYS:OFF");
}
// Usa uno switch per disegnare la schermata corretta in base allo stato della FSM.
switch (current_fsm_state) {
case STATE_MENU: // Schermata del menu principale.
lcd_set_cursor(0, 0); lcd_print(menu_selection == 0 ? ">" : " "); lcd_print("CO2 Thr");
lcd_set_cursor(0, 1); lcd_print(menu_selection == 1 ? ">" : " "); lcd_print("Temp Range");
lcd_set_cursor(9, 1); lcd_print("CO2:"); lcd_print_u8(co2_level);
break;
case STATE_MODIFY_CO2: // Schermata di modifica soglia CO2.
lcd_set_cursor(0, 0); lcd_print("Edit CO2 Thr");
lcd_set_cursor(0, 1); lcd_print("Val: "); lcd_print_u8(editable_value);
lcd_set_cursor(9, 1); lcd_print("Act:"); lcd_print_u8(co2_level);
break;
case STATE_TEMP_MENU: // Schermata sottomenu temperature.
lcd_set_cursor(0, 0); lcd_print(menu_selection == 0 ? ">" : " "); lcd_print("Set Min Temp");
lcd_set_cursor(0, 1); lcd_print(menu_selection == 1 ? ">" : " "); lcd_print("Set Max Temp");
lcd_set_cursor(10, 1); lcd_print_u8(temp_benessere_min); lcd_data('-'); lcd_print_u8(temp_benessere_max); lcd_data('C');
break;
case STATE_MODIFY_TEMP_MIN: // Schermata modifica temp minima.
lcd_set_cursor(0, 0); lcd_print("Edit Min Temp");
lcd_set_cursor(0, 1); lcd_print("Val: "); lcd_print_u8(editable_value);
lcd_set_cursor(9, 1); lcd_print("Max:"); lcd_print_u8(temp_benessere_max);
break;
case STATE_MODIFY_TEMP_MAX: // Schermata modifica temp massima.
lcd_set_cursor(0, 0); lcd_print("Edit Max Temp");
lcd_set_cursor(0, 1); lcd_print("Val: "); lcd_print_u8(editable_value);
lcd_set_cursor(9, 1); lcd_print("Min:"); lcd_print_u8(temp_benessere_min);
break;
}
update_display = 0; // Resetta il flag di aggiornamento.
}
// ========= LOGICA E INTERRUPT =========
// Avvia una singola conversione dell'ADC.
void start_adc_conversion(void) {
ADCSRA |= (1 << ADSC); // Imposta il bit ADSC (ADC Start Conversion).
}
// Funzione logica per decidere se il recupero di calore è vantaggioso.
bool recupero_calore(int8_t temp_ext_c, uint8_t min_b, uint8_t max_b) {
// Ritorna vero se la temperatura esterna è compresa nel range di benessere.
return (temp_ext_c >= min_b && temp_ext_c <= max_b);
}
// ISR (Interrupt Service Routine) per l'ADC.
// Viene eseguita automaticamente quando la conversione ADC è completata.
ISR(ADC_vect) {
uint16_t adc_value = ADC; // Legge il risultato a 10 bit dal registro ADC.
// Converte il valore da 10 bit (0-1023) a 8 bit (0-255) scalandolo (shift a destra di 2).
co2_level = (uint8_t)(adc_value >> 2);
adc_ready = 1; // Segnala che l'ADC è di nuovo pronto.
new_co2_data_available = 1; // Segnala che un nuovo dato è disponibile per la logica principale.
}
// ISR per il Timer1 (Compare Match A).
// Viene eseguita ogni secondo (come configurato in init_timer1).
ISR(TIMER1_COMPA_vect) {
static uint8_t counter = 0; // Contatore statico (mantiene il suo valore tra le chiamate).
// Incrementa il contatore ogni secondo.
if (++counter >= 5) {
// Ogni 5 secondi...
counter = 0; // Azzera il contatore.
read_co2_and_temp_flag = 1; // Imposta il flag per richiedere una nuova lettura dei sensori.
}
}
// ISR per il Pin Change Interrupt 1 (per i bottoni).
// Viene eseguita quando c'è un cambiamento di stato su uno dei pin PC1-PC4.
ISR(PCINT1_vect) {
if (debounce_timer_active) return; // Se stiamo già facendo un debounce, ignora questo interrupt.
// Legge lo stato dei pin e lo inverte (i pin sono bassi quando premuti).
pin_state_at_pcint = (~PINC) & BUTTON_ALL_MASK;
// Se almeno un bottone è premuto...
if (pin_state_at_pcint) {
PCICR &= ~(1 << PCIE1); // Disabilita temporaneamente l'interrupt sui pin per evitare rimbalzi.
debounce_timer_active = 1; // Attiva lo stato di debounce.
debounce_counter = 0; // Azzera il contatore del debounce.
TCNT2 = 0; // Azzera il contatore del Timer2.
TIMSK2 |= (1 << OCIE2A); // Abilita l'interrupt del Timer2 (che scatta ogni 1ms).
}
}
// ISR per il Timer2 (Compare Match A).
// Viene eseguita ogni millisecondo (quando il debounce è attivo).
ISR(TIMER2_COMPA_vect) {
// Incrementa il contatore ogni ms.
if (++debounce_counter >= DEBOUNCE_TIME_MS) {
// Dopo 50ms...
TIMSK2 &= ~(1 << OCIE2A); // Disabilita l'interrupt del Timer2.
debounce_timer_active = 0; // Termina lo stato di debounce.
// Legge di nuovo lo stato dei bottoni.
uint8_t current_pressed = (~PINC) & BUTTON_ALL_MASK;
// Se il bottone che ha causato l'interrupt è ancora premuto dopo 50ms, conferma la pressione.
if ((pin_state_at_pcint & BUTTON_PIN_YELLOW_MASK) && (current_pressed & BUTTON_PIN_YELLOW_MASK)) button_yellow_pressed = 1;
if ((pin_state_at_pcint & BUTTON_PIN_GREEN_MASK) && (current_pressed & BUTTON_PIN_GREEN_MASK)) button_green_pressed = 1;
if ((pin_state_at_pcint & BUTTON_PIN_RED_MASK) && (current_pressed & BUTTON_PIN_RED_MASK)) button_red_pressed = 1;
if ((pin_state_at_pcint & BUTTON_PIN_BLUE_MASK) && (current_pressed & BUTTON_PIN_BLUE_MASK)) button_blue_pressed = 1;
PCIFR |= (1 << PCIF1); // Pulisce il flag dell'interrupt sui pin (per sicurezza).
PCICR |= (1 << PCIE1); // Riabilita l'interrupt sui pin per future pressioni.
}
}
// ========= SETUP E LOOP =========
// La funzione setup viene eseguita una sola volta all'avvio del microcontrollore.
void setup(void) {
cli(); // Disabilita gli interrupt globali durante la configurazione.
// Chiama tutte le funzioni di inizializzazione.
init_adc();
init_timer1();
init_timer2_debounce();
init_relays();
lcd_init();
init_buttons();
// Imposta lo stato iniziale del sistema.
system_active = 0;
current_fsm_state = STATE_MENU;
menu_selection = 0;
update_display = 1; // Richiede un aggiornamento del display all'avvio.
read_co2_and_temp_flag = 1; // Richiede una lettura dei sensori all'avvio.
sei(); // Riabilita gli interrupt globali.
Serial.begin(9600); // Inizializza la comunicazione seriale per il debug (funzione non standard AVR-C, tipica di Arduino).
Serial.println("Sistema Avviato.");
start_adc_conversion(); // Avvia la prima conversione ADC.
}
// La funzione loop viene eseguita continuamente dopo il setup.
void loop(void) {
// ----- Gestione dei bottoni -----
// Controlla se uno dei flag di pressione è stato impostato dall'ISR.
if (button_yellow_pressed || button_green_pressed || button_red_pressed || button_blue_pressed) {
cli(); // Disabilita gli interrupt per leggere e resettare i flag in modo atomico.
// Copia i flag in variabili locali e li azzera subito.
uint8_t yellow_f = button_yellow_pressed; button_yellow_pressed = 0;
uint8_t green_f = button_green_pressed; button_green_pressed = 0;
uint8_t red_f = button_red_pressed; button_red_pressed = 0;
uint8_t blue_f = button_blue_pressed; button_blue_pressed = 0;
sei(); // Riabilita gli interrupt.
// Logica della macchina a stati finiti (FSM) per il menu.
switch (current_fsm_state) {
case STATE_MENU: // Se siamo nel menu principale
if (yellow_f || green_f) menu_selection = !menu_selection; // Giallo/Verde: cambia selezione (su/giù).
else if (red_f) { // Rosso: conferma selezione.
if (menu_selection == 0) { editable_value = co2_threshold; current_fsm_state = STATE_MODIFY_CO2; } // Selezionato CO2 -> entra in modifica CO2.
else { menu_selection = 0; current_fsm_state = STATE_TEMP_MENU; } // Selezionato Temp -> entra nel sottomenu Temp.
}
break;
case STATE_MODIFY_CO2: // Se stiamo modificando la soglia CO2
if (green_f && editable_value < 255) editable_value++; // Verde: incrementa valore.
else if (yellow_f && editable_value > 0) editable_value--; // Giallo: decrementa valore.
else if (red_f) { co2_threshold = editable_value; current_fsm_state = STATE_MENU; } // Rosso: salva e torna al menu.
else if (blue_f) { current_fsm_state = STATE_MENU; } // Blu: annulla e torna al menu.
break;
case STATE_TEMP_MENU: // Se siamo nel sottomenu temperature
if (yellow_f || green_f) menu_selection = !menu_selection; // Giallo/Verde: cambia selezione.
else if (red_f) { // Rosso: conferma.
if (menu_selection == 0) { editable_value = temp_benessere_min; current_fsm_state = STATE_MODIFY_TEMP_MIN; } // Entra in modifica temp min.
else { editable_value = temp_benessere_max; current_fsm_state = STATE_MODIFY_TEMP_MAX; } // Entra in modifica temp max.
} else if (blue_f) { current_fsm_state = STATE_MENU; } // Blu: torna al menu principale.
break;
case STATE_MODIFY_TEMP_MIN: // Se stiamo modificando la temp minima
if (green_f && editable_value < temp_benessere_max) editable_value++; // Verde: incrementa (non può superare la max).
else if (yellow_f && editable_value > 0) editable_value--; // Giallo: decrementa.
else if (red_f) { temp_benessere_min = editable_value; current_fsm_state = STATE_TEMP_MENU; } // Rosso: salva e torna al sottomenu.
else if (blue_f) { current_fsm_state = STATE_TEMP_MENU; } // Blu: annulla e torna al sottomenu.
break;
case STATE_MODIFY_TEMP_MAX: // Se stiamo modificando la temp massima
if (green_f && editable_value < 255) editable_value++; // Verde: incrementa.
else if (yellow_f && editable_value > temp_benessere_min) editable_value--; // Giallo: decrementa (non può scendere sotto la min).
else if (red_f) { temp_benessere_max = editable_value; current_fsm_state = STATE_TEMP_MENU; } // Rosso: salva e torna al sottomenu.
else if (blue_f) { current_fsm_state = STATE_TEMP_MENU; } // Blu: annulla e torna al sottomenu.
break;
}
update_display = 1; // Richiede un aggiornamento del display dopo ogni azione.
}
// ----- Lettura periodica dei sensori -----
// Controlla il flag impostato dal Timer1.
if (read_co2_and_temp_flag) {
read_co2_and_temp_flag = 0; // Azzera il flag.
if (adc_ready) { adc_ready = 0; start_adc_conversion(); } // Se l'ADC è pronto, avvia una nuova lettura CO2.
// Legge i sensori di temperatura.
temp_read_status_internal = read_dht22_sensor(DHT1_INTERNAL_PIN, &temp_internal_x10);
temp_read_status_external = read_dht22_sensor(DHT2_EXTERNAL_PIN, &temp_external_x10);
}
// ----- Logica di attivazione del sistema -----
// Controlla se è arrivato un nuovo dato di CO2.
if (new_co2_data_available) {
new_co2_data_available = 0; // Azzera il flag.
// Controlla se il livello di CO2 supera la soglia impostata.
system_active = (co2_level > co2_threshold);
if (system_active)
{
Serial.println("SISTEMA ATTIVO, CO2 supera la SOGLIA MAX");
// Se entrambi i sensori di temperatura sono stati letti correttamente...
if (temp_read_status_internal == 0 && temp_read_status_external == 0) {
int8_t temp_ext_c = temp_external_x10 / 10; // Converte la temperatura esterna in gradi Celsius.
// Se la temperatura esterna è nel range di benessere...
if (recupero_calore(temp_ext_c, temp_benessere_min, temp_benessere_max)) {
Serial.print("T_INT: "); Serial.println(temp_internal_x10 / 10);
Serial.print("T_EXT: "); Serial.println(temp_ext_c);
// Attiva il relè del recuperatore di calore e disattiva l'altro.
PORTD = (PORTD & ~(1 << RELAY_VENTILATION_PIN_NUM)) | (1 << RELAY_HEAT_RECOVERY_PIN_NUM);
Serial.println("Modo: RECUPERO CALORE");
} else {
// Altrimenti, attiva il relè della ventilazione diretta.
PORTD = (PORTD & ~(1 << RELAY_HEAT_RECOVERY_PIN_NUM)) | (1 << RELAY_VENTILATION_PIN_NUM);
Serial.println("Modo: SCAMBIO DIRETTO");
}
}
else
{
// Se c'è un errore nella lettura delle temperature, forza la ventilazione diretta per sicurezza.
PORTD = (PORTD & ~(1 << RELAY_HEAT_RECOVERY_PIN_NUM)) | (1 << RELAY_VENTILATION_PIN_NUM);
Serial.println("Errore lettura temp, modo SCAMBIO DIRETTO forzato.");
}
} else {
// Se il CO2 è sotto la soglia, disattiva entrambi i relè.
Serial.println("SISTEMA DISATTIVATO, CO2 stabile");
PORTD &= ~((1 << RELAY_HEAT_RECOVERY_PIN_NUM) | (1 << RELAY_VENTILATION_PIN_NUM));
}
update_display = 1; // Richiede un aggiornamento del display per mostrare il nuovo stato.
}
// ----- Aggiornamento del display -----
if (update_display) {
update_lcd_display(); // Chiama la funzione che ridisegna lo schermo.
}
}