Newsgroup: it.hobby.elettronica.digitale
Titolo: Event-driven programming: questo sconosciuto

Visto che c'è ultimamente poca attività nel gruppo e grazie alla mia 
insonnia, non penso di disturbare proponendo una discussione che sa 
molto di filosofico e non so dove potrà portare.
 
Non so per voi, ma quando scrivo del codice il problema maggiore che 
incontro non è tanto arrivare a qualcosa di funzionante, quanto ad 
organizzarlo nel modo migliore.  Potrei citare paroloni che non mi 
competono, come affidabilità, coerenza, documentabilità, ecc. ecc., ma 
penso ci siamo capiti.
 
Insomma, quello che cerco di evitare è il famoso "spaghetti code" che è 
sempre in agguato e, spesso, sembra essersi impadronito del mio codice. 
Quando questa "sensazione" diventa troppo forte (troppi flag in giro che 
facilmente dimentico di controllare o inizializzare, poca separazione 
logica delle funzionalità, ecc), inizio a farmi mentalmente male, 
cercando di capire come strutturare meglio il codice: forse queste 
funzionalità vanno separate in un altro file, forse è meglio separare la 
parte di HMI dalla parte della logica, forse è meglio una macchina a 
stati, forse è meglio riscrivere tutto da capo.
Ogni tanto questo processo mi porta a migliorare il codice iniziale, ma 
il metodo che uso mi sembra troppo casuale e privo di qualsiasi logica.
Alla fine mi sembra di affrontare sempre gli stessi problemi e di non 
saper mai scegliere la soluzione migliore.
 
Lo so che sembra banale, ma spesso mi faccio molte masturbazioni mentali 
sulla semplice accensione di un LED o di un relè collegato ad un pin del 
micro.  Supponiamo di dover accendere il LED nello stato STATUS_A, cosa 
preferite fare?  Confermare lo stato del LED in una funzione richiamata 
in continuazione nel loop principale, oppure generare un evento di 
transizione quando si entra nello stato STATUS_A ed accendere una sola 
volta il LED nell'handler dell'evento?
 
Io ho sempre preferito la prima soluzione:
 
   void led_refresh(void) {
     if (status == STATUS_A) LED_ON();
     else                    LED_OFF();
   }
 
Anche se ripetutamente metto alto (o basso) il pin del LED, questo non 
mi crea grossi ritardi.  Inoltre così sono sicuro che, prima o poi, quel 
LED si accenderà se mi trovo nello stato A (naturalmente quel LED è 
gestito solo in quella funzione).
 
Purtroppo tale approccio non va bene sempre.  Per esempio, che faccio se 
devo lampeggiare il LED per 5 secondi quando si entra nello stato 
STATUS_A e poi mantenerlo fisso acceso (spento in altri stati)?
Potrei creare lo stato INIT_A che, dopo 5 secondi, termina nello stato 
STATUS_A, ma mi disgusta modificare una macchina a stati del core del 
programma che ha una sua logica, solo per risolvere un problema di 
interfaccia utente.
 
Così ogni tanto ho provato soluzioni di questo tipo:
 
   void led_refresh(void) {
     static old_status;
     static init_A;
     if (status == STATUS_A) {
       if (old_status != STATUS_A) {
         init_A = 1;
         TIMERSET(tmr, 5);
       } else if (TIMEREXPIRED(tmr)) {
         init_A = 0;
       }
     } else {
       init_A = 0;
     }
     old_status = status;
     if (init_A) {
       LED_FLASH();
     } else if (status == STATUS_A) {
       LED_ON();
     } else {
       LED_OFF();
     }
   }
 
In pratica cerco di dedurre localmente alla funzione led_refresh(), con 
variabili statiche, l'informazione di transizione nello stato A.
La cosa funziona, ma non mi piace per tanti motivi.
 
Primo, questo meccanismo dovrei implementarlo in tutti i punti del 
programma dove mi serve rilevare questa transizione di stato, con 
conseguente spreco di codice e RAM.
Secondo, c'è una probabilità che non riesca a rilevare una transizione 
veloce B->A->B se led_refresh() non viene richiamata nel momento giusto.
 
Una soluzione che mi è balenata e che dovrebbe risolvere il problema più 
elegantemente è far generare alla macchina a stati degli eventi, tra cui 
la transizione nello stato A.
 
Facendo qualche ricerca mi sono imbattutto nella tecnica "event-driven 
programming", negli statecharts o macchine a stati estese o macchine a 
stati UML ecc. ecc. che promettono di risolvere parecchi problemi, 
soprattutto in applicazioni embedded.  Ho la sensazione che queste 
tecniche siano paragonabili a quello che voglio fare io.
 
Riorganizzare il mio codice con questo approccio non è semplice, ma 
potrei fare un tentativo se ne vale la pena e se veramente questa è la 
soluzione "di tutti i mali".  Da qui nasce la mia domanda: qualcuno di 
voi utilizza queste "tecniche avanzate" (che nel mio piccolo non ho mai 
studiato) e può confermare la loro bontà?

   Da qui nasce la mia domanda: qualcuno di voi utilizza queste "tecniche avanzate" (che nel mio piccolo non ho mai studiato) e può confermare la loro bontà?
 
Non credo ci siano molti qua dentro che sappiano cosi' approfonditamente di queste cose,
ho visto che molti programmatori firmware pur con una preparazione universitaria sono
sostanzialmente autodidatti nello specifico o al max vengono affiancati (se in aziende grosse),
ma corsi generici di programmazione embedded ne ho visti pochi.
 
Giulia
 

 di programmazione embedded ne ho visti pochi.
 
Considerando le risposte al mio post, forse hai ragione :-)
 
Però mi sembra un campo molto molto interessante. Anziché perder tempo a
smanettare sul firmware con tecniche più o meno affidabili (nel mio caso,
il più delle volte improvvisate) per arrivare ad una soluzione funzionante,
ci si potrebbe concentrare solo sulla modellizzazione mediante statechart
UML. Se poi ci si avvale di tool per la generazione automatica di codice
a partire dal modello, la cosa è ancora più divertente.
 
Quante volte, durante lo sviluppo, devo modificare il comportamento di
una sezione di codice perché mi accorgo di dover aggiungere una pausa
(non bloccante, of course) e quindi necessito di un altro stato.
Immaginate di modificare il modello, disegnando col mouse un nuovo stato,
e di avviare il tool di generazione codice.
Il gioco è fatto senza paura di creare bachi, considerando che il modello
mi obbliga a definire le cose per bene (inizializzazione degli stati,
entry e exit points, guard conditions...).
 
Forse è solo un sogno :-)
 
Stavo dando un'occhiata a questo tool free: 
www.state-machine.com/qm
.
Non so se ne esistono altri simili.

 studiato) e può confermare la loro bontà?
 
Io in passato mi sono trovato a gestire progetti complessi in cui ho
usato pesantemente la tecnica delle macchine a stati, con più task
cooperativi paralleli e subtask con priorità diverse. Avevo sviluppato
un framework cross-micro (per pic18F, atmel atmega, emulazione windows
ed emulazione linux) e cross-compiler (hitech c, iar ew avr, gcc) per
far girare l'applicazione in più ambienti e testare il 95% del codice su
macchine desktop (in quanto non avevo emulatori adatti a disposizione).
Tale framework forniva una base comune, nascondendo la maggior parte
delle differenze dei micro e dei compilatori usati (almeno per quel che
serviva a me) e soprattutto includeva una serie di funzioni non
bloccanti per la gestione di alcuni dispositivi (clock, timer,
comunicazioni seriali, moduli flash esterni, sensori di pressione,
display grafici), in modo da permettere di gestire facilmente più task
paralleli basati su macchine a stati.
 
Qualche mese fa avevo iniziato a reingegnerizzare tale framework (che ho
chiamato CRK - Cooperative Runtime Kit), per renderlo open-source
(eliminando le parti relative alle applicazioni che non posso
pubblicare) ed estenderlo anche a cpu a 16 e 32 bit; in più mi è venuta
l'idea di semplificare ulteriormente la programmazione basata su
macchine a stati, usando un trucco permesso dal C. Vedere ad esempio il
seguente codice:
 
#define YELLOWLED                       PIN(B,5)
 
 
crkTask( T_BLINK_YELLOWLED )
{
    static crkPeriodicTimer_t tp;
 
    crkTaskBegin;
 
        /* Configure the I/O pin of the green led as output low */
        /* (assuming the pin has been initialized as digital).  */
        crkPinInit(YELLOWLED, OUT_LO);
 
        /* SIMULATION ONLY - Display a yellow led.*/
        CRK_SIMUL_PIN_LEDCOLOR(YELLOWLED, YELLOW);
 
        /* Set a timer with a periodic deadline of 1/4s.*/
        crkPeriodicTimerInit(tp, CRK_T_MSEC(250));
 
        /* Give up control to the scheduler.*/
        crkTaskYield();
 
        /* Loop forever.*/
        while(1)
        {
            /* Wait the next deadline (give up control).*/
            crkPeriodicTimerWait(tp);
 
            /* Switch on the led.*/
            crkPinSet(YELLOWLED);
 
            /* Wait the next deadline (give up control).*/
            crkPeriodicTimerWait(tp);
 
            /* Switch off the led.*/
            crkPinClr(YELLOWLED);
        }
 
    crkTaskEnd;
}
 
Questo è un task cooperativo molto semplice, basato su una macchina a
stati, benché la cosa non appaia direttamente. Notare che non c'è nessun
OS cooperativo sottostante, il task switching è fatto interamente usando
delle macro e un scheduler round-robin (non mostrato) che è
semplicemente un ciclo "while(1)" che richiama in modo continuo i vari
task in successione (crkTask(0), crkTask(1), ...).
 
crkTaskBegin e crkTaskEnd sono macro che includono l'infrastruttura per
la macchina a stati mentre crkTaskYield() e crkPeriodicTimerWait() sono
macro che rilasciano il controllo allo scheduler, ma prima definiscono
al loro interno delle label per riprendere l'esecuzione dal punto in cui
è stata interrotta. Il tutto in C puro, senza l'uso di codice assembler.
 
Vi chiederete: come sia possibile una cosa del genere senza un OS che
effettui un context switch vero? La risposta è: usando lo statement
"switch" in modo non convenzionale (ma in perfetto C, riconosciuto da
tutti i compilatori). Per avere un'idea del principio su cui la cosa è
basata, cercare su Internet "Coroutines in C - by Simon Tatham".
 
L'unico inconveniente di questo approccio è l'uso di variabili static,
se devono sopravvivere ad un task switching (poichè non essendoci un
task switching vero, lo stack è in comune tra tutti i task paralleli).
 
Altra caratteristica di questo framework che sto sviluppando è che la
gestione dei timer software e dell'orologio interno è basata su un
approccio tick-less (in pratica sono generate solo poche interruzioni al
secondo per tenere conto dell'overflow del timer hardware usato per il
realtime clock, pur garantendo risoluzioni dell'ordine dei microsecondi).
 
Come detto, avevo intenzione, una volta arrivato a buon punto della
reingegnerizzazione, di rilasciare questo framework con licenza
open-source. Purtroppo mi sono un po' arenato per mancanza di tempo (di
lavoro da fare ce n'è parecchio), perciò se c'è qualcuno interessato a
collaborare, si può creare un progetto condiviso su github o gitorius.
 
Condizioni necessarie per partecipare sono:
 
- ottima conoscenza del C e delle direttive del preprocessore (come
detto il sistema si basa pesantemente su macro, che però poi vengono
ottimizzate dal compilatore, rendendo l'hoverhead minimo)
 
- ottima conoscenza del microcontrollore (o famiglia di
microcontrollori) su cui si vuole collaborare
 
- tecnica di programmazione orientata sia alla chiarezza, sia
all'efficienza (le funzionalità di base del framework, implementate
sulle varie architetture, devono essere ben ottimizzate)
 
- conoscenza di tool di sviluppo quali git, doxygen, cmake (usato per il
supporto dei cross-compilatori e la configurazione dell'ambiente di
compilazione), librerie Qt 4 & 5 (usate per la parte di simulazione in
windows e linux)
 
- buona conoscenza dell'inglese (commenti nel codice e documentazione
vanno scritti in tale lingua)
 
Come ambiente di sviluppo io uso Linux, ma è possibile lavorare anche su
Windows, basta usare un editor che supporti il fine riga degli ambienti
unix (ad esempio notepad++ o qt-creator) ed installare i tool necessari.
 
Giusto per essere chiari:
 
 - essendo un progetto open e non retribuito, chiunque decida di
partecipare potrà lavorare sulla sua parte quando ha tempo e voglia,
purché rispetti eventuali scadenze stabilite di comune accordo
 
 - ai partecipanti verrà riconosciuta la paternità del codice e la
collaborazione al progetto, ma dovranno fornire il loro codice sotto la
licenza che ho scelto per il framework (licenza eCos: è una GPL
modificata che permette l'uso del framework anche in software
commerciali, senza obbligo di rilasciare il sorgente della parte
APPLICATIVA; viceversa eventuali modifiche al framework stesso dovranno
essere rese disponibili alla comunità)
 
 - il codice scritto dovrà conformarsi ad alcune regole, in modo da
avere uno stile comune, che renda semplice la partecipazione al progetto
di altre persone e l'eventuale avvicendamento di chi non è più
interessato a collaborare
 
 - dovranno essere evitate polemiche sterili ed inutili, l'approccio
dovrà essere sempre costruttivo; chi creerà confusione e malcontento
verrà estromesso dal progetto
 
 - mi riservo di decidere quali caratteristiche includere nel framework;
ad ogni modo poiché non sono infallibile se qualcuno ha idee su come
implementare in modo più efficiente qualche parte già scritta, ben vengano
 
 - prima di sollevare questioni su una parte di codice mi aspetto che ci
si documenti bene sulla parte in questione; questo per evitare perdite
di tempo prezioso
 
In sostanza a chi è interessato chiedo serietà, un approccio costruttivo
e voglia di fare. Comunque prima di proporre la propria adesione,
consiglio di valutare bene la cosa, non avrebbe senso attivare il tutto
e poi mollare dopo pochi giorni, sarebbe solo una perdita di tempo per
tutti.
 
Saluti
 
	Flavio
 
 
 
-- 
Indirizzo email non valido, no risposte private grazie.
 

 [...]

Purtroppo io non avrei le competenze per aiutarti, ma sarebbe bello 
comunque poter dare un'occhiata al tuo lavoro.
 

 comunque poter dare un'occhiata al tuo lavoro.
 
Vedrò di mettere in piedi un repository, ma ci vorrà qualche giorno, non
ho molto tempo libero in questo periodo.


 studiato) e può confermare la loro bontà?
 
Non penso risolvano tutti i mali, sicuramente può essere di aiuto ma c'è da 
vedere come viene implementato e integrato. Non facendo uso di task switch l'unico modo che vedo per mantenere il sistema reattivo è quello di impegnare poco tempo CPU all'interno di ogni task/stato/trasizione.
 
Mauro