Ciao
boiler,
boiler ha scritto:Grazie mille per la risposta,
GuidoB.
Apprezzo il fatto che ti sia dato da fare per scrivere un post così esteso.
È un piacere e anche un'occasione per riflettere su concetti che a volte per la fretta trascuriamo

.
boiler ha scritto:Moduli a cui accedono molti altri moduli mettono a disposizione nel loro header file troppa roba:
Qui la banana ha accesso al frullatore, ma anche all'affettatrice (che non le serve). E viceversa per il salame.
In questo esempio piuttosto astratto e molto OOP vedo una classe
Cucina un po' troppo eterogenea. Vedrei meglio una classe base
ElettrodomesticoDiCucina da cui derivano (ereditano) le classi
Affettatrice,
Frullatore,
Forno ecc.
Poi vedrei una classe
Ingrediente da cui derivano
Salame e
Banana.
A questo punto
Banana non dovrebbe usare un generico
ElettrodomesticoDiCucina, ma solo un
Frullatore per farsi frullare (
Banana::frulla()). E così abbiamo fatto un po' d'ordine.
Bisognerebbe calarsi in un caso concreto per capire se sarebbe ancora più ordinato, invece di dare alla
Banana la capacità di frullarsi e al
Salame la capacità di affettarsi, inventare una nuova classe
Cuoco che usa sia
ElettrodomesticoDiCucina (con tutte le sue classi derivate) che
Ingrediente (con tutte le sue classi derivate).
A questo punto il
Cuoco potrebbe usare il
Frullatore (che ha il metodo
frulla per frullare un generico
Ingrediente) per frullare una
Banana. Nel codice del metodo
Cuoco::preparaFrappè() potrebbe esserci l'istruzione:
frullatore.frulla(banana); 
, molto espressiva.
boiler ha scritto:Non ci avevo mai pensato, ma leggendo il tuo intervento mi è venuta l'idea che si potrebbero suddividere i prototipi:
Questo è un approccio che permetterebbe di fare un po' di pulizia. È simile al concetto di
interface nell'OOP.
Oppure si potrebbero fare (stavolta ragioniamo in C) tanti moduli separati (
Affettatrice,
Frullatore,
Forno ecc.), ciascuno con le sue funzioni specifiche (
affettatriceAffetta(),
frullatoreFrulla(),
fornoCuoci() ecc.) e che per le parti comuni si appoggino al modulo ElettrodomesticoDiCucina, che avrà per esempio le funzioni
ElettrodomesticoDiCucinaAccendi() e
ElettrodomesticoDiCucinaSpegni().
Per far questo i moduli
Affettatrice,
Frullatore ecc. includeranno
ElettrodomesticoDiCucina.h nei loro .c e useranno le sue funzioni
ElettrodomesticoDiCucinaAccendi(),
ElettrodomesticoDiCucinaSpegni() ecc. per implementare le proprie
affettatriceAccendi(),
frullatoreSpegni() ecc.
Anche qui, per capire bene come è meglio fare, dovrei calarmi in un esempio concreto.
boiler ha scritto:Veniamo però all'hot-topic
Per capire a che moduli sono destinati, all'inizio di ogni pacchetto ci sarà una intestazione (un enum per esempio) che indica il tipo di pacchetto.
Questo (se interpreto correttamente) è un approccio che eviterei, perché sposta parte dell'architettura nel protocollo di comunicazione. Per restare in tema OOP, viola il principio dell'
information hiding.
O intendevi qualcosa come il numero di registro in Modbus? Questo è quello che faccio già ora.
Non conosco Modbus, comunque secondo me si può mantenere l'
information hiding in questo modo:
una classe
Pacchetto contiene l'attributo
tipo, al quale si può accedere con
pacchetto.getTipo().
Da
Pacchetto derivano le classi
PacchettoTemperatura,
PacchettoCalibrazioneTermometro ecc, ciascuna con gli attributi addizionali che necessita e le corrispondenti funzioni getters e setters.
Allo smistatore di pacchetti interessa solo accedere al tipo, attributo comune a tutti i pacchetti, e in base al valore del tipo lo invierà ai moduli interessati.
Calato in C, un pacchetto potrebbe essere un modulo che esporta in un .h una
typedef struct contenente il
tipo (un campo di tipo
typedef enum { ... } pacchettoTipo) e un puntatore a
void per il corpo del pacchetto. Per ciascun tipo sarà definita in un .h una
typedef struct contenente tutti i campi necessari in quel caso.
Definirà anche una funzione
inline pacchettoTipo pacchettoGetTipo(const Pacchetto *) e l'information hiding è salva.
Volendo, se non ci sono pacchetti molto più lunghi degli altri, si può evitare il puntatore a
void, e definire tutti i pacchetti in un solo .h, in un solo
typedef struct che contiene il
tipo, seguito da una
union di tutti i tipi di corpi dei pacchetti.
Come potrebbe funzionare lo smistatore? Quando riceve un pacchetto, legge il tipo, e con quello seleziona la riga di una sua tabella che contiene la lista dei puntatori alle callback function di tutti i moduli che vogliono ricevere quel tipo di pacchetto. Chiamerà in sequenza tutte le callback function della lista, passando come argomento
(const Pacchetto * pacchetto).
La tabella si può costruire dinamicamente all'inizializzazione o anche durante il funzionamento (ogni modulo chiama una funzione dello smistatore passandogli i tipi di pacchetti che vuole ricevere e le corrispondenti callback function), oppure staticamente (in questo caso non ho molto chiaro come si potrebbe fare per mantenere un buon isolamento tra i moduli, cioè che lo smistatore non debba conoscerli tutti nel suo codice).
boiler ha scritto:Ho fatto uno schizzo dell'architettura che suggerisci. Correggimi se ti ho frainteso:
La mia domanda principale era "come fare se una variabile va usata da due moduli distinti".
Non ho un esempio concreto, ma non è difficile generarne uno: in un sistema con piú interfacce voglio poter fornire la temperatura sull'uart, sulla ethernet, scriverla periodicamente in una scheda SD e mostrarla su un display. Inoltre c'è un modulo di allarme che la richiede anche lui una volta al secondo.
È facile immaginarsi che il tutto non si limita alla temperatura, ma vale anche per i valori di calibrazione e tutto il resto. L'architettura di cui sopra diventa un piatto di spaghetti.
Si potrebbe risolvere mettendo uno smistatore unico per tutte le interfacce e tutti i moduli, ma la sua logica diventerebbe piuttosto complessa (per finire l'architettura è quella che è, ne posso nascondere una parte in una black-box, ma è come nascondere la polvere sotto al tappeto

).
L'altra proposta che facevo nel mio messaggio iniziale e che mi sembra piú semplice è la seguente:
Le variabili sono in data.c, mentre una serie di data_*.h mettono a disposizione dei moduli i prototipi dei getter/setter ai quali hanno accesso.
Ovviamente questa struttura la userei solo per le variabili che devono essere condivise tra diversi moduli. Quelle interne sono nel modulo stesso.
Ci vedi dei problemi?
Io ho avuto esperienze negative col polling, per cui se posso lo evito. Inoltre ogni modulo è "sdoppiato", avendo una parte (il valore del dato che fornisce) memorizzato fuori, in un modulo
Data che lo unisce a tutti gli altri... non è molto bello.
Invece un modulo
DistributoreDati che non memorizzi dati ma funzioni come "colla" che tiene insieme tutto il sistema secondo me va meglio.
Come
Data, permette ai diversi moduli (
Termometro, Uart, Ethernet, Sd, Display, Allarme) di non doversi conoscere (interfacciare) tra loro. Ognuno dovrà conoscere solo il modulo
DistributoreDati.
Una grossa semplificazione (non indispensabile) per il
DistributoreDati (ma ancor più per
Data) sarebbe rappresentare i dati che devono essere distribuiti con un unico tipo, per esempio un intero
long (
int32_t).
Nel caso fosse necessario rappresentare le temperature al decimo di grado, per contenerle in un
long userei il decimo di grado come unità di misura, e una temperatura di -23,4 gradi sarebbe rappresentata da -234.
Il
DistributoreDati non dovrà conoscere il significato di ciascun dato, ma solo a chi deve comunicarlo quando è necessario.
Modulo
Termometro: si occuperà di leggere il sensore (via ADC o SPI o quel che sia, usando i moduli driver opportuni, magari con un suo polling interno se non è possibile evitarlo), e di fornire la temperatura al modulo
DistributoreDati, all'inizio del funzionamento e ogni volta che questa cambia. Per comunicare il dato, chiamerà la funzione
DistributoreDatiNuovoValore(TipoDato tipoDato, uint32_t valore);TipoDato sarà un
enum con tanti valori quanti sono i tipi di dati da distribuire.
Comunicare un dato solo quando cambia permette di evitare il polling, e permette anche di essere tempestivi nella comunicazione (non deve scadere il periodo di polling prima che gli altri moduli conoscano il nuovo valore). Poi in casi particolari si può fare un ibrido (comunicare un dato ogni volta che cambia ma non più spesso di una volta al secondo...).
Non è necessario che il modulo
DistributoreDati mantenga una copia dei valori dei dati (potrebbe anche farlo se risultasse utile, ma cercherei di evitarlo).
Infatti l'unica cosa che deve fare, ogni volta che riceve un nuovo dato, è comunicarlo a chi lo deve ricevere.
Per farlo, potrebbe utilizzare il
tipoDato ricevuto per accedere alla riga corrispondente di una tabella, da cui accede alla lista delle funzioni callback dei vari moduli da chiamare per comunicare il nuovo valore.
Invece di usare una tabella di callback si può fare hardcoded, utilizzando uno
switch:
- Codice: Seleziona tutto
switch(tipoDato) {
case tipoDatoTemperatura:
uartNuovoValore(tipoDato, valore); /* Comunica nuovo valore al modulo Uart */
ethernetNuovoValore(tipoDato, valore); /* Comunica nuovo valore al mod. Ethernet */
sdNuovoValore(tipoDato, valore); /* Comunica nuovo valore al modulo Sd */
displayNuovoValore(tipoDato, valore); /* Comunica nuovo valore al modulo Display */
...
Oppure ancora, per ogni modulo si può tenere una mappa di bit (uno per elemento dell'enum
TipoDati) in cui porre a 1 (true) quelli corrispondenti ai dati delle cui variazioni deve essere aggiornato. Poi in un ciclo scorrere tutti i moduli e chiamare la funzione
<nomeModulo>NuovoValore(...) solo se il bit è a 1.
Il modulo
Sd, che deve scrivere periodicamente il valore della temperatura, manterrà localmente una variabile
temperaturaAttuale che verrà aggiornata ogni volta che il modulo
DistributoreDati chiamerà
sdNuovoValore(tipoDatoTemperatura, valore). Ogni volta che dovrà scrivere su SD la temperatura, il modulo Sd potrà leggerla localmente dalla sua variabile
temperaturaAttuale.
Il modulo Sd potrà eseguire localmente anche i filtraggi e le conversioni di unità di misura che gli dovessero servire.
In caso di cambio del periodo di scrittura, del metodo di filtraggio, dell'unità di misura l'unico modulo impattato sarà Sd.
Questo metodo di scatenare chiamate di funzioni da un modulo all'altro (esempio: cambia la temperatura, il modulo
Termometro chiama
distributoreDatiNuovoValore(...) che a sua volta chiama
uartNuovoValore(...) che a sua volta chiama il driver della Uart per trasmetterlo, e così via per
ethernetNuovoValore(...), sdNuovoValore(...), displayNuovoValore(..), per tornare finalmente al
Termometro chiamante) è semplice, tempestivo, ma funziona solo se il codice è monolitico.
Inoltre bisogna fare attenzione a che una di queste chiamate non provochi a sua volta un cambio in un valore, che scatenerebbe una nuova catena di chiamate quando la precedente non è ancora terminata.
Se c'è pericolo che succeda, o se ci sono vari processi, queste chiamate dovranno essere trasformate in "eventi" da passare tra processi utilizzando una coda o i servizi del sistema operativo sottostante, per essere elaborati in sequenza, uno alla volta.
Se ci sono chiamate a funzioni bloccanti, per esempio se la Uart non ritorna il controllo prima di aver trasmesso tutti i caratteri, l'esecuzione potrebbe essere rallentata, e quindi sarebbe il caso di rendere le chiamate non bloccanti, o almeno memorizzare il dato da trasmettere e solo in seguito trasmetterlo.
Vantaggi del
DistributoreDati rispetto a un modulo
Data che contiene i dati a cui si accede con polling:
1) si evita il polling, che carica il processore per far qualcosa solo poche volte;
2) la distribuzione dei nuovi valori è tempestiva (non bisogna attendere il ciclo di polling);
3) il
DistributoreDati non deve conoscere i dati che distribuisce e non deve immagazzinarli per conto di altri (maggiore indipendenza tra moduli).
Se dovessi partire da zero farei qualcosa del genere. Poi bisogna sempre fare i conti con quel che c'è già.
Saluti,
GuidoB