Newsgroup: it.hobby.elettronica.digitale
Titolo: Accesso ad una zona di memoria condivisa in interrupt

Alla ricerca di nuove soluzioni che mi semplificano la vita durante la
scrittura dei firmware, stavo provando ad implementare una lista di
eventi.
In parole molto semplici un evento è una funzione da richiamare con un
puntatore da passare.
Una funzione evetns_handler(), richiamata in continuazione, scorre la
lista e, se trova un evento attivo, esegue la funzione relativa.
Fin qui è abbastanza semplice.
Naturalmente c'è una funzione event_add() che si preoccupa di
aggiungere un evento alla lista. Il problema è che questa funzione può
essere richiamata sia durante la normale esecuzione del programma, sia
durante gli interrupt (pensate ad eventi relativi ad un timer che si
esaurisce, oppure ad un byte ricevuto dalla seriale).
Per semplificarmi la vita stavo implementando la lista degli eventi
come array static di puntatori ad eventi. Quando il puntatore è NULL,
nessun evento è associato, altrimenti punta ad un evento attivo. La
funzione event_add() sarebbe molto semplice:
---
Event *events[MAX_EVENTS];
void
event_add (Event *ev)
{
  int i;
  for (i = 0; i < MAX_EVENTS; i++) {
    if (events[i] == NULL) {
      events[i] = ev;
    }
  }
}
---
Cosa succede però se la event_add() viene interrotta da un interrupt
che vuole, a sua volta, aggiungere un evento in lista? Se scatta
l'interrupt proprio mentre si esegue l'istruzione di if nel ciclo for,
si aggiungeranno i due eventi nella stessa posizione dell'array.
Per risolvere questo conflitto, la soluzione più semplice che ho
trovato è disabilitare globalmente gli interrupt. Ma quando? Ho due
alternative:
---
  for (i = 0; i < MAX_EVENTS; i++) {
    DISABLE_IRQ();
    if (events[i] == NULL) {
      events[i] = ev;
    }
    ENABLE_IRQ();
  }
---
  DISABLE_IRQ();
  for (i = 0; i < MAX_EVENTS; i++) {
    if (events[i] == NULL) {
      events[i] = ev;
    }
  }
  ENABLE_IRQ();
---
Nel primo caso gli interrupt vengono in continuazione, durante il
ciclo for, abilitati e disabilitati, ma dovrebbe esserci una minore
latenza di esecuzione dell'interrupt.
Il fatto è che entrambe le soluzioni mi lasciano l'amaro in bocca. In
applicazioni realt-time sappiamo che la CPU deve intervenire in modo
deterministico e, molto probabilmente, in brevissimo tempo rispetto ad
eventi asincroni che generalmente fanno scattare interrupt. Andare a
disabilitare globalmente gli interrupt vuol dire aumentare questo
tempo di risposta e aggiungere una variabile casuale al calcolo di
questo tempo (dobbiamo supporre che siamo nel caso peggiore, cioè
nella event_add() e che abbiamo appena disabilitato gli interrupt).
Esiste qualche altra soluzione a questo problema che non mi faccia
rimanere con l'amaro in bocca e che riesca "servire" in modo
prioritario gli interrupt piuttosto che le chiamate da "userspace"?
Stavo pensando a qualche flag che indica che un interrupt ha
modificato l'array degli eventi, ma non saprei come farlo.
Qualcuno mi aiuta a superare questo problema?

 wrote:
Praticamente devi implementare uno scheduler, ma esistono varie
filosofie.
In assembler mi sono già imbattutto in simili problematiche, ovvero,
la situazione che un interrupt cadesse mentre la risposta al
precedente non era ancora risolta.
Nel tuo caso utilizzerei dei contatori e dei semafori nelle routine di
risposta in modo che se queste vengono invocate prima che siano
terminate vengano semplicemente conteggiate.
All'uscita della routine si verifica che non debba essere reinvocata
di nuovo finché il contatore si azzera.
I semafori servono per non far rientrare il codice, ma basterebbe
credo verificare che il contatore sia a zero.
Nel corpo principale potrai valutare se invocare la routine o no in
base al fatto che sia già in esecuzione da qualche evento esterno.
In questo momento ho un problema di simile complessità con un
telecomando IR sviluppato su AVR.
Devo invocare l'interrupt da software e, una volta invocato, dovrà
uscire e rientrare per i fatti suoi a mezzo di timer.
I rientri e le uscite sono sempre diversi in funzione di tabelle.
Per rendere chiaro e trasportabile il codice, la routine che invoca
l'interrupt sembra appartenere al corpo principale ma di fatto verrà
eseguita a pezzi nei vari rientri su interrupt.
E' una situazione ibrida e a suo modo ingannevole per chi la legge e
dovrebbe avvalersi di due stack per essere "pulita".
Di fatto ne creo uno virtuale.
I vantaggi in termini di carico sulla CPU sono infiniti.
Comunque vedo che non sono l'unico che tenta di implementare filosofie
da sistema operativo nei propri prg.  :)
Il difficile comincia quando hai solo un paio di kb di FLASH e qualche
centinaio (per non dire decine) di byte di RAM.
Infatti il più delle volte si risolve con soluzioni funzionanti ma
inclassificabili!  :(
Piccio.

 inclassificabili!  :(
In genere cerco di rendere il programma molto leggibile e quindi di
creare
una sottostruttura tipicamente da sistema operativo minimale.
Però quando hai decine di byte di RAM eviterei proprio....

 Fin qui � abbastanza semplice.
Io farei una coda circolare, con un contatore e due indici, uno relativo
all'ultimo evento servito e uno all'ultimo evento processato.
Per aggiungere un evento disabiliti gli interrupt, controlli che il
contatore sia < MAXevents, lo incrementi, incrementi l'indice, gli fai
l'AND, inserisci il puntatore alla funzione di handling nella tabella e
abiliti gli interrupt. Abilitazione e disabilitazione interrupt, se fatto
in un interrupt handler, non sono necessari.
Nel main task controlli se il contatore � > 0. In tal caso disabiliti gli
interrupt, decrementi il contatore, abiliti gli interrupt, incrementi
l'indice ultimo evento processato, gli fai l'AND e chiami la funzione
puntata.
Se hai abbastanza RAM puoi surdimensionare la tabella e lasciar stare il
controllo contatore < MAXevents, cosa che farei comunque, perch�, se
l'evento non sta nella tabella, che fai? Lo butti, o butti via quello pi�
vecchio (cosa che peraltro puoi decidere di voler fare, con una coda
circolare)? Se la fai di 256 elementi ed utilizzi quindi indici a 8 bit ti
risparmi pure l'AND.
Se poi hai pi� priorit�, puoi fare varie code, una per ogni livello di
priorit�.

 in un interrupt handler, non sono necessari.
Potrebbero essere comunque necessari se ci sono interrupt con priorità
che possono interrompere un ISR di un interrupt con più bassa
priorità.

 priorit�.
Se l'operazione da fare � inserire un evento in coda mi pare abbia poco
senso riabilitare gli interrupt. La latenza sarebbe aumentata di pochi
clock, assai meno di quelli necessari a salvare i registri.

 wrote:
cut...
Credo che sia fruttuoso implementare 2 vettori per i puntatori a
funzione o per meglio dire, due code per i task.
La prima relativa alle chiamate dal corpo principale (scarsa priorità)
e l'altra per gli interrupt.
Più in generale, tante code quanti sono i livelli di priorità.
Quando arriva un interrupt, questo inserisce il puntatore a funzione
nella coda specifica senza accavallarsi con altri inserimenti.
Ogni livello ha un suo strato.
I primi task ad essere eseguiti devono essere quelli della coda a
priorità più alta; una volta esauriti si può passare a quelli a
priorità inferiore.
In questo modo, nessun conflitto.
Una subroutine può essere invocata, quindi, da più code diverse ma il
tempo di risposta dipenderà sempre dalla frequenza di scansione delle
code stesse.
Altro accorgimento: un interrupt dovrebbe analizzare se la sua routine
di servizio è già in esecuzione tramite semafori.
Se non la è, la si esegue subito, sennò la si mette in coda interrupt.
Allo stesso modo, lo scheduler dovrà verificare che la routine non sia
già in uso ed invocarla in modo atomico.
Praticamente dovresti ad ogni scansione disabilitare gli interrupt,
settare un semaforo di impegno subroutine, analizzare ed eventualmente
invocare previa riabilitazione dell'int.
Questo perché potrebbe sopraggiungere l'int tra la verifica della
funzione e l'esecuzione.
Non so se mi sono capito; ora mi rileggo e semmai vi chiedo
spiegazioni su quello che ho detto...
Piccio.

 funzione e l'esecuzione.
Questo non mi convince. Un IRQ può solo aggiungere eventi alla lista e
non cancellarli, mentre la routine di scansione della lista può solo
cancellare eventi dalla lista dopo la loro esecuzione.
In questo caso penso che l'eventuale IRQ in un punto qualsiasi della
routine di scansione non può creare nessun conflitto.

 clock, assai meno di quelli necessari a salvare i registri.
Scusami, ma non ti seguo. Tu proponevi di disabilitare gli interrupt
solo nel
tempo necessario per modificare gli indici della coda. Poi dicevi che
era
possibile evitare la disabilitazione/abilitazione interrupt se
l'aggiunta
dell'evento avveniva già in un interrupt, ma su questo non sono
convinto.
Immagina un IRQ a più bassa priorità che vuole aggiungere un evento
alla coda. Tu suggerisci di modificare gli indici della coda senza
disabilitare
in toto gli interrupt. Scatta un altro IRQ a più alta priorità che va
a sua volta
ad aggiungere un evento alla coda. Qui la coda si può incasinare di
brutto
perchè magari prima si aggiunge l'evento dell'IRQ più prioritario in
un posto
della coda e poi lo si sovrascrive con l'evento dell'IRQ meno
prioritario.
Mi sbaglio?

 [...]
Partendo da quello che mi hai detto e abbandonando la gestione delle
priorità,
ho ottenuto questo:
---
void
event_scheduler (void)
{
	int idx;
	if (nevents > 0) {
//		IRQ_DISABLE();
		nevents--;
//		IRQ_ENABLE();
		idx = idx_busy;
		idx_busy = ++idx_busy % MAX_EVENTS;
		(events_queue[idx]->func)(events_queue[idx]->user_data);
	}
}
void
event_add (Event *ev)
{
	IRQ_DISABLE();
	nevents = ++nevents % MAX_EVENTS;
	events_queue[idx_free] = ev;
	idx_free = ++idx_free % MAX_EVENTS;
	IRQ_ENABLE();
}
---
Siccome nella event_scheduler() il decremento di events è atomico ed è
l'unica istruzione da "proteggere contro gli interrupt", penso che la
disabilitazione
degli interrupt siano inutili (vedi i commenti).
Ho preferito incrementare idx_busy prima di eseguire la funzione
dell'evento,
così ho dovuto utilizzare la variabile temporanea int.
La event_add() viene chiamata sia dal main task per aggiungere un
evento
"utente", sia dalle ISR. Tu mi dicevi che in quest'ultimo caso non è
necessaria
la disabilitazione degli interrupt, ma io non credo.

 disabilitare in toto gli interrupt. Scatta un altro IRQ a pi� alta
Lo pu� fare solo ed esclusivamente se tu hai riabilitato gli interrupt.

 degli interrupt, ma io non credo.
Non ci credi perch� ti mancano alcune nozioni abbastanza fondamentali.

 Lo può fare solo ed esclusivamente se tu hai riabilitato gli interrupt.
Non mi sembra...

 Non ci credi perché ti mancano alcune nozioni abbastanza fondamentali.
:(

 Qualcuno mi aiuta a superare questo problema?
Io penso che probabilmente non hai bisogno davvero di tutto questo 
real-time.
Tutte le periferiche fanno gi� quello che serve per permetterti di 
lavorare in tranquillit�.
La seriale, per esempio, quando riceve un carattere, gi� in hardware 
mette il carattere in una sua fifo da almeno 1 locazione. Quindi hai 
tutto il tempo tra un carattere e il successivo per gestirlo.
Stessa cosa per ADC, SPI, PWM, timer,  e tutte le periferiche che conosco.
Quindi io, fossi in te, non mi porrei il problema.
-- 
  _|/ Francesco Sacchi - Develer S.r.l., R&D dept.
   |\ 
http://www.develer.com/
 - 
http://www.bertos.org

 real-time.
Ad oggi forse no... magari un domani chissà. Stavo cercando di crearmi
una struttura da riutilizzare anche in futuro.

 complicata degli analoghi per l'array.
Il codice originario usava un array. Poi dopo, quello modificato, di 
fatto � una lista, anche se sotto sotto usa un array.
L'importante � non dover scorrere tutto ogni volta che devi inserire 
un elemento, come facevi nel codice iniziale.
Il vantaggio � enorme: il tempo di inserimento/rimozione in coda/testa 
non dipende da quanti elementi ci sono dentro ed � costante.
Se poi hai bisogno di rimuovere/aggiungere elementi a caso nel mezzo 
dell'array, anche l'ultima implementazione non va pi� bene e devi 
davvero passare ad una lista linkata.


 una struttura da riutilizzare anche in futuro.
Ne trovi a decine, fatte da gente estremamente in gamba e pi� ferrata di
te.
Se, ad esempio, clicchi il secondo link della signature di Sacchi...

 interrupt a pi� alta priorit�?
Un processore moderno ti d� la possibilit� di scegliere: se vuoi lo stesso
comportamento di un clone dell'antico 8088, ti basta mettere, come prima
istruzione dell'handler, una "enable interrupts".

 istruzione dell'handler, una "enable interrupts".
Non conosco l'8088. A me interessa, in alcuni casi, dare priorità ad
alcuni
IRQ piuttosto che ad altri.

 Non conosco l'8088.
Infatti ho scritto "clone".

 davvero passare ad una lista linkata.
Sì, ora tutto questo mi è chiaro. Bisogna mantenere bassi i tempi di
disabilitazione degli interrupt (visto che non si può non
disabilitarli)
e bisogna quindi evitare cicli su tutto un array. Meglio una struttura
ordinata.
Visto che ci siamo, ti faccio una domanda sul tuo Bertos e sui
problemi
che si potrebbero creare con gli interrupt.
Nel file timer.c, dove utilizzi una lista di timer attivi da
controllare nell'ISR
del timer, hai la possibilità di rimuovere un elemento mediante la
timer_abort().
Come scrivi anche nei commenti, eliminare un timer già scaduto
potrebbe
portare a comportamenti non definiti.
Immagina che nel programma principale si richiami la timer_abort()
perchè
il chiamante è sicuro che il timer è ancora attivo in quel momento.
Durante
le istruzioni di chiamata scatta l'IRQ del timer e, guarda caso,
proprio quel
timer scade e viene servito e rimosso dalla lista all'interno
dell'ISR. Quando
si ritorna dall'ISR si procede con la normale esecuzione e quindi si
chiama
la timer_abort() che tenterà di eliminare un timer già scaduto, con
ovvi
problemi.
Sbaglio io oppure c'è questa condizione (sicuramente molto sfortunata)
che
può portare a comportamenti non definiti durante l'esecuzione?
Un modo per risolvere e dare la responsabilità della chiamata alla
timer_abort()
al chiamante che, però, non potrebbe sempre sapere quanto tempo manca
alla "morte" del timer che vuole abortire.

 pu� portare a comportamenti non definiti durante l'esecuzione?
S�, questa condizione c'�, ed � questo il motivo del commento :-)
Per� tieni presente che ci sono due casi di uso tipico del timer:
- Timer ripetitivo
- Timer one-shot
Nel primo caso, anche se scatta un IRQ, il timer viene rimosso e poi 
reinserito all'interno della ISR stessa, proprio perch� � ripetitivo.
Quindi il timer � sempre attivo in userspace e la rimozione atomica 
dalla lista � sufficiente.
Nel secondo caso, di solito i timer one-shot non vengono rimossi e 
quindi il problema � poco sentito.
Se proprio serve rimuovere un timer one-shot prima della scadenza, 
l'unica possibilit� �: disabilitare gli IRQ, fare il controllo se il 
timer � ancora attivo, eventualmente rimuoverlo e infine riabilitare 
gli IRQ.
Sono tante cose da fare e potenzialmente possono richiedere molto tempo.
Ma, ti ripeto, questo non � il pattern di uso tipico dei timer, quindi 
il codice � ottimizzato per essere efficiente nei casi pi� frequenti.
Per quello che vuoi fare tu, in genere noi usiamo il kernel che 
gestisce tutti questi problemi di concorrenza con per� un overhead 
maggiore.
I timer li usiamo davvero per cose real-time e che devono funzionare 
in parallelo ai processi principali.
Le callback devono essere comunque brevissime (vengono eseguite nella 
ISR) per cui i casi di uso tipici sono: far lampeggiare un led, 
generazione segnali, controllo buzzer, controllo di fase di triac, 
controllo termostati, etc...


 controllo termostati, etc...
Io non arriverei neanche a questo punto. Per ora potrebbe non
interessarmi
eseguire un codice dall'interno dell'ISR, ma semplicemente aggiungere
un
evento alla todo list. E' chiaro che se voglio chiamare un callback
dall'ISR,
questa sarà molto breve.
Ulteriore domanda (se non ti va, mandami a quel paese :):
la tua IRQ_SAVE_DISABLE() salva lo stato di qualcosa e disabilita gli
interrupt.
La richiami, per esempio, nella ISR del timer.
Mi domando a cosa serva. Se siamo nell'ISR vuol dire che
precedentemente
gli interrupt erano abilitati, quindi non basterebbe fare un semplice
disabilita e
riabilita gli interrupt alla fine dell'ISR? Perchè ti interessa
salvare e ripristinare
lo stato degli interrupt se in realtà lo conosci già in partenza?

 La richiami, per esempio, nella ISR del timer.
S�, salva lo stato degli IRQ e in ogni caso li disabilita, ma 
guardando il codice, non viene usata nella ISR del timer.
Dove lo avevi visto?


 Dove lo avevi visto?
Hai ragione, stavo dormendo. Era nella timer_add(), giustamente... :)

 Vi riempirei soltanto di domande :)
S�, fino a poco fa c'erano dei problemi con il DNS. In ogni caso 
adesso dovrebbe funzionare.
Per quanto riguarda le domande, la lista � fatta a posta per questo :-)
In questi giorni, un mio collega, sta implementando il kernel 
preemptive, e poi, appena abbiamo tempo, pensavamo di aggiungere il 
supporto per altre CPU, un consiglio un pi� da chi � nel settore fa 
sempre bene.
Ciao!


 Per quanto riguarda le domande, la lista è fatta a posta per questo :-)
Per esempio, gli archivi della mailing list non sembrano funzionare.

 Per esempio, gli archivi della mailing list non sembrano funzionare.
Anche adesso?
Stamani c'era questo problema, adesso dovrebbero funzionare.


 Stamani c'era questo problema, adesso dovrebbero funzionare.
Sì, ora sembra funzionare...

 timer_add(). Non moltissimo direi.
Se la ISR fa cose non necessarie, dura di pi�. E se dura di pi�, mentre 
� in esecuzione lei non gira il codice utente o altre ISR.
Quindi avere una ISR del timer pi� complessa di quello che serve � 
equivalente a disabilitare gli IRQ per tanto tempo.
Secondo la mia esperienza mi sento di dire che questo tempo � di gran 
lunga superiore a quello dovuto per l'accesso al contatore a 32bit.
-- 
  _|/ Francesco Sacchi - Develer S.r.l., R&D dept.
   |\ 
http://www.develer.com/
 - 
http://www.bertos.org/

 contatore a 32bit :-)
Beh, in realtà con un contatore a 32bit hai soltanto ritardato il wrap-
around.
Con un incremento ogni ms, mezzo periodo è pari a circa 25 giorni.
Nel mondo embedded è facile avere applicazioni funzionanti 24/24 7/7,
quindi 25 giorni non è un tempo abissale.
Se vuoi accendere un LED quando un timer è scaduto e scrivi:
if (TIMEREXPIRED(tmr)) {
  led_on();
}
e TIMEREXPIRED() fa solo la differenza, avrai un LED che lampeggia
ogni
24 giorni, anche se il timer è scaduto la prima volta e non più
ricaricato.
Bisognerebbe avere quasi sempre un flag che dice se il timer è in
running:
if (TIMEREXPIRED(tmr) && timer_is_running) {
  timer_is_running = 0;
  led_on();
}
Solo passando a 64bit ti togli da qualsiasi problema.
A parte questo problema, sono d'accordo con te. Meglio tenere l'IRQ
bello
snello da utilizzare per roba veramente real-time ;)

 quindi 25 giorni non � un tempo abissale.
S�, hai ragione :-P
BTW, poi ho inviato la patch in mailing list per mettere in bertos i 
timer sincroni.
Penso che la includeremo a breve, se ti va puoi darci un'occhiata.


 Penso che la includeremo a breve, se ti va puoi darci un'occhiata.
Sicuramente darò un'occhiata appena sarà disponibile.
Pensi ci sia anche nella tua patch il problema dei "25 giorni"?

 Come risolveresti questo problema?
Purtroppo se devi misurare del tempo che va oltre il massimo ammesso 
dal contatore, non ci sono soluzioni oltre quella di avere un 
contatore pi� grande.
Nel caso indicato sopra, per�, non c'� nessun problema perch� prima 
che il timer wrappi, cnt viene azzerato. E' sufficiente che il tuo T 
sia minore della met� del massimo e il codice far� sempre la cosa 
corretta, anche se la scheda rimane attiva per sempre.
Puoi pensare che sia un po' "brutto", ma la cosa � perfettamente 
funzionante.
