Evitare le variabili globali quando si utilizzano gli interrupt nei sistemi incorporati


13

Esiste un buon modo per implementare la comunicazione tra un ISR e il resto del programma per un sistema incorporato che evita le variabili globali?

Sembra che lo schema generale sia quello di avere una variabile globale che è condivisa tra l'ISR e il resto del programma e usata come una bandiera, ma questo uso delle variabili globali va contro per me. Ho incluso un semplice esempio usando gli ISR ​​in stile avr-libc:

volatile uint8_t flag;

int main() {
    ...

    if (flag == 1) {
        ...
    }
    ...
}

ISR(...) {
    ...
    flag = 1;
    ...
}

Non riesco a capire cosa sia essenzialmente un problema di scoping; eventuali variabili accessibili sia dall'ISR che dal resto del programma devono essere intrinsecamente globali, sicuramente? Ciononostante, ho visto spesso persone dire cose secondo le linee "le variabili globali sono un modo per implementare la comunicazione tra ISR e il resto del programma" (enfasi mia), che sembra implicare che ci siano altri metodi; se ci sono altri metodi, quali sono?



1
Non è necessariamente vero che TUTTO il resto del programma avrebbe accesso; se hai dichiarato la variabile come statica, la vedrebbe solo il file in cui è stata dichiarata la variabile. Non è affatto difficile avere variabili visibili nell'intero file, ma non nel resto del programma e ciò può essere d'aiuto.
DiBosco,

1
inoltre, il flag deve essere dichiarato volatile, poiché lo stai utilizzando / modificando al di fuori del normale flusso di programma. Ciò costringe il compilatore a non ottimizzare alcuna lettura / scrittura su flag ed eseguire l'operazione di lettura / scrittura effettiva.
prossimo hacking

@ next-hack Sì, è assolutamente corretto, mi dispiace, stavo solo cercando di trovare rapidamente un esempio.

Risposte:


18

Esiste un modo di fatto standard per farlo (presupponendo la programmazione in C):

  • Interrupt / ISR sono di basso livello e pertanto devono essere implementati solo all'interno del driver relativo all'hardware che genera l'interrupt. Non dovrebbero trovarsi altrove ma all'interno di quel driver.
  • Tutte le comunicazioni con l'ISR vengono eseguite solo dal conducente e dal conducente. Se altre parti del programma hanno bisogno di accedere a tali informazioni, devono richiederle al conducente tramite funzioni setter / getter o simili.
  • Non si devono dichiarare variabili "globali". Variabili dell'ambito del file di significato globale con collegamento esterno. Cioè: variabili che potrebbero essere invocate con externparole chiave o semplicemente per errore.
  • Invece, per forzare l'incapsulamento privato all'interno del conducente, devono essere dichiarate tutte queste variabili condivise tra il conducente e l'ISR static. Tale variabile non è globale ma limitata al file in cui è dichiarata.
  • Per evitare problemi di ottimizzazione del compilatore, tali variabili devono essere dichiarate come volatile. Nota: questo non dà accesso atomico o risolve il rientro!
  • Nel driver è spesso necessario un meccanismo di rientro, nel caso in cui l'ISR scriva nella variabile. Esempi: disabilitazione interruzione, maschera di interruzione globale, semaforo / mutex o letture atomiche garantite.

Nota: potrebbe essere necessario esporre il prototipo della funzione ISR tramite un'intestazione, al fine di posizionarlo in una tabella vettoriale situata in un altro file. Ma questo non è un problema fintanto che si documenta che si tratta di un interrupt e che non dovrebbe essere chiamato dal programma.
Lundin,

Cosa diresti se il contro-argomento fosse il maggiore overhead (e il codice extra) dell'uso del setter / ottenere le funzioni? Ci ho pensato da solo, pensando agli standard di codice per i nostri dispositivi integrati a 8 bit.
Leroy105,

2
@ Leroy105 Il linguaggio C ha ormai supportato le funzioni in linea per un'eternità. Sebbene anche l'uso di inlinestia diventando obsoleto, poiché i compilatori diventano sempre più intelligenti nell'ottimizzazione del codice. Direi che preoccuparsi dell'overhead è "l'ottimizzazione pre-matura" - nella maggior parte dei casi l'overhead non ha importanza, se è del tutto presente anche nel codice macchina.
Lundin,

2
Detto questo, nel caso della scrittura di driver ISR, circa l'80-90% di tutti i programmatori (non esagerando qui) ha sempre qualcosa di sbagliato in loro. Il risultato sono bug sottili: flag cancellati in modo errato, ottimizzazione errata del compilatore dovuta a volatile mancante, condizioni di gara, pessime prestazioni in tempo reale, overflow dello stack ecc. Ecc. Nel caso in cui l'ISR non sia correttamente incapsulato all'interno del driver, la possibilità di tali bug sottili è ulteriormente aumentato. Concentrati sulla scrittura di un bug free-driver prima di preoccuparti di cose di interesse periferico, come se setter / getter introducessero un po 'di sovraccarico.
Lundin,

10
questo uso delle variabili globali è in contrasto con me

Questo è il vero problema Farsene una ragione.

Ora, prima che gli sfigati del ginocchio si lamentino immediatamente di come questo sia sporco, lasciatemi qualificare un po '. Vi è certamente pericolo nell'utilizzare in eccesso le variabili globali. Ma possono anche aumentare l'efficienza, che a volte conta in piccoli sistemi con risorse limitate.

La chiave è pensare a quando è possibile utilizzarli ragionevolmente e è improbabile che ti mettano nei guai, contro un bug che aspetta solo che accada. Ci sono sempre dei compromessi. Mentre generalmente evitare le variabili globali per comunicare tra il codice di interrupt e di primo piano è una guida indicabile, portarla, come la maggior parte delle altre linee guida, a un estremo di religioni è controproducente.

Alcuni esempi in cui a volte utilizzo variabili globali per passare informazioni tra codice di interrupt e di primo piano sono:

  1. Contatori di tick di clock gestiti dall'interrupt di clock di sistema. Di solito ho un interrupt di clock periodico che viene eseguito ogni 1 ms. Ciò è spesso utile per vari tempi nel sistema. Un modo per ottenere queste informazioni dalla routine di interrupt in cui il resto del sistema può utilizzarle è quello di mantenere un contatore di tick di clock globale. La routine di interruzione incrementa il contatore ad ogni tick di clock. Il codice di primo piano può leggere il contatore in qualsiasi momento. Spesso lo faccio per 10 ms, 100 ms e anche 1 secondo tick.

    Mi assicuro che i tick da 1 ms, 10 ms e 100 ms abbiano una dimensione di parola che può essere letta in una singola operazione atomica. Se si utilizza un linguaggio di alto livello, assicurarsi di dire al compilatore che queste variabili possono cambiare in modo asincrono. In C, li dichiarate volatili esternamente , per esempio. Naturalmente questo è qualcosa che va inserito in un file di inclusione fisso, quindi non è necessario ricordarlo per ogni progetto.

    A volte faccio il contatore delle 1 s il contatore del tempo totale trascorso, quindi faccio largo 32 bit. Questo non può essere letto in una singola operazione atomica su molti dei micro micro che uso, quindi non è reso globale. Viene invece fornita una routine che legge il valore di più parole, gestisce i possibili aggiornamenti tra le letture e restituisce il risultato.

    Certo che ci potrebbe state delle routine per ottenere anche i contatori di tick più piccoli da 1 ms, 10 ms, ecc. Tuttavia, ciò fa davvero poco per te, aggiunge molte istruzioni al posto della lettura di una sola parola e utilizza un'altra posizione dello stack di chiamate.

    Qual è il rovescio della medaglia? Suppongo che qualcuno potrebbe fare un errore di battitura che scrive accidentalmente su uno dei contatori, che quindi potrebbe incasinare altri tempi nel sistema. Scrivere deliberatamente su un contatore non avrebbe senso, quindi questo tipo di bug dovrebbe essere qualcosa di involontario come un refuso. Sembra molto improbabile. Non ricordo che sia mai successo in oltre 100 piccoli progetti di microcontrollori.

  2. Valori A / D finali filtrati e regolati. Una cosa comune da fare è avere una routine di interruzione per gestire le letture da un A / D. Di solito leggo i valori analogici più velocemente del necessario, quindi applico un po 'di filtro passa-basso. Spesso vengono inoltre applicati ridimensionamento e offset.

    Ad esempio, l'A / D potrebbe leggere l'uscita da 0 a 3 V di un partitore di tensione per misurare l'alimentazione a 24 V. Le numerose letture vengono eseguite attraverso alcuni filtri, quindi ridimensionate in modo che il valore finale sia espresso in millivolt. Se l'alimentazione è a 24,015 V, il valore finale è 24015.

    Il resto del sistema vede solo un valore aggiornato in tempo reale che indica la tensione di alimentazione. Non sa né ha bisogno di preoccuparsi quando viene aggiornato esattamente, soprattutto perché viene aggiornato molto più spesso del tempo di assestamento del filtro passa basso.

    Anche in questo caso, una routine di interfaccia potrebbe essere utilizzata, ma si ottiene molto poco beneficio da questo. Usare la variabile globale ogni volta che è necessaria la tensione di alimentazione è molto più semplice. Ricorda che la semplicità non è solo per la macchina, ma che più semplice significa anche meno possibilità di errore umano.


Ho iniziato la terapia, in una settimana lenta, cercando davvero di indovinare il mio codice. Vedo il punto di Lundin sulla limitazione dell'accesso alle variabili, ma guardo i miei sistemi attuali e penso che sia una possibilità così remota QUALSIASI PERSONA potrebbe effettivamente manipolare una variabile globale critica del sistema. Le funzioni Getter / Setter finiscono per costarti in termini di costi rispetto al solo utilizzo di un globale e accettare questi sono programmi piuttosto semplici ...
Leroy105,

3
@ Leroy105 Il problema non sono i "terroristi" che abusano intenzionalmente della variabile globale. L'inquinamento dello spazio dei nomi potrebbe essere un problema in progetti più grandi, ma ciò può essere risolto con una buona denominazione. No, il vero problema è che il programmatore sta cercando di utilizzare la variabile globale come previsto, ma non riesce a farlo correttamente. O perché non si rendono conto del problema delle condizioni di gara che esiste con tutti gli ISR, o perché sbagliano l'implementazione del meccanismo di protezione obbligatorio, o semplicemente perché sputano l'uso della variabile globale in tutto il codice, creando un accoppiamento stretto e codice illeggibile.
Lundin,

I tuoi punti sono validi Olin, ma anche in questi esempi, la sostituzione extern int ticks10mscon inline int getTicks10ms()non farà assolutamente alcuna differenza nell'assieme compilato, mentre d'altra parte ti renderà difficile cambiare accidentalmente il suo valore in altre parti del programma, e ti permetterà anche di un modo per "agganciarsi" a questa chiamata (ad esempio per deridere il tempo durante i test delle unità, per accedere a questa variabile o altro). Anche se si sostiene che la possibilità che un programmatore san cambi questa variabile in zero, non vi è alcun costo per un getter in linea.
Groo

@Groo: questo è vero solo se stai usando un linguaggio che supporta le funzioni di allineamento e significa che la definizione della funzione getter deve essere visibile a tutti. In realtà, quando uso un linguaggio di alto livello, utilizzo di più le funzioni getter e meno le variabili globali. Nell'assemblaggio, è solo molto più facile afferrare il valore di una variabile globale piuttosto che preoccuparsi di una funzione getter.
Olin Lathrop

Certo, se non puoi inline, allora la scelta non è così semplice. Volevo dire che con le funzioni incorporate (e molti compilatori pre-C99 già supportavano le estensioni incorporate), le prestazioni non possono essere un argomento contro i vincitori. Con un compilatore di ottimizzazione ragionevole, si dovrebbe finire con lo stesso assieme prodotto.
Groo

2

Qualsiasi interruzione particolare sarà una risorsa globale. A volte, tuttavia, può essere utile che più interrupt condividano lo stesso codice. Ad esempio, un sistema potrebbe avere diversi UART, ognuno dei quali dovrebbe utilizzare una logica di invio / ricezione simile.

Un buon approccio da gestire è quello di posizionare le cose usate dal gestore di interrupt, o puntatori ad esse, in un oggetto struttura, e quindi avere i gestori di interrupt hardware reali essere qualcosa del tipo:

void UART1_handler(void) { uart_handler(&uart1_info); }
void UART2_handler(void) { uart_handler(&uart2_info); }
void UART3_handler(void) { uart_handler(&uart3_info); }

Gli oggetti uart1_info, uart2_infoecc. Sarebbero variabili globali, ma sarebbero le uniche variabili globali utilizzate dai gestori di interrupt. Tutto il resto che i gestori toccheranno sarebbe gestito all'interno di quelli.

Si noti che tutto ciò a cui si accede sia dal gestore degli interrupt che dal codice della linea principale deve essere qualificato volatile. Potrebbe essere più semplice dichiarare semplicemente volatiletutto ciò che verrà utilizzato dal gestore degli interrupt, ma se le prestazioni sono importanti si potrebbe voler scrivere un codice che copi le informazioni su valori temporanei, operi su di esse e poi le riscriva. Ad esempio, invece di scrivere:

if (foo->timer)
  foo->timer--;

Scrivi:

uint32_t was_timer;
was_timer = foo->timer;
if (was_timer)
{
  was_timer--;
  foo->timer = was_timer;
}

Il primo approccio può essere più facile da leggere e comprendere, ma sarà meno efficiente del secondo. Se questa è una preoccupazione dipenderebbe dall'applicazione.


0

Ecco tre idee:

Dichiarare la variabile flag come statica per limitare l'ambito a un singolo file.

Rendi privata la variabile flag e usa le funzioni getter e setter per accedere al valore flag.

Utilizzare un oggetto di segnalazione come un semaforo anziché una variabile flag. L'ISR imposta / pubblica il semaforo.


0

Un interrupt (ovvero il vettore che punta al gestore) è una risorsa globale. Quindi, anche se usi qualche variabile nello stack o nell'heap:

volatile bool *flag;  // must be initialized before the interrupt is enabled

ISR(...) {
    *flag = true;
}

o codice orientato agli oggetti con una funzione 'virtuale':

HandlerObject *obj;

ISR(...) {
    obj->handler_function(obj);
}

... il primo passo deve coinvolgere una variabile globale (o almeno statica) effettiva per raggiungere gli altri dati.

Tutti questi meccanismi aggiungono un riferimento indiretto, quindi di solito questo non viene fatto se si desidera spremere l'ultimo ciclo dal gestore di interrupt.


dovresti dichiarare flag come volatile int *.
prossimo hacking

0

Sto codificando per Cortex M0 / M4 al momento e l'approccio che stiamo usando in C ++ (non esiste un tag C ++, quindi questa risposta potrebbe essere off-topic) è il seguente:

Usiamo una classe CInterruptVectorTableche contiene tutte le routine di servizio di interrupt che sono memorizzate nel vettore di interrupt effettivo del controller:

#pragma location = ".intvec"
extern "C" const intvec_elem __vector_table[] =
{
  { .__ptr = __sfe( "CSTACK" ) },           // 0x00
  __iar_program_start,                      // 0x04

  CInterruptVectorTable::IsrNMI,            // 0x08
  CInterruptVectorTable::IsrHardFault,      // 0x0C
  //[...]
}

La classe CInterruptVectorTableimplementa un'astrazione dei vettori di interruzione, quindi è possibile associare diverse funzioni ai vettori di interruzione durante il runtime.

L'interfaccia di quella classe si presenta così:

class CInterruptVectorTable  {
public :
    typedef void (*IsrCallbackfunction_t)(void);                      

    enum InterruptId_t {
        INTERRUPT_ID_NMI,
        INTERRUPT_ID_HARDFAULT,
        //[...]
    };

    typedef struct InterruptVectorTable_t {
        IsrCallbackfunction_t IsrNMI;
        IsrCallbackfunction_t IsrHardFault;
        //[...]
    } InterruptVectorTable_t;

    typedef InterruptVectorTable_t* PinterruptVectorTable_t;


public :
    CInterruptVectorTable(void);
    void SetIsrCallbackfunction(const InterruptId_t& interruptID, const IsrCallbackfunction_t& isrCallbackFunction);

private :

    static void IsrStandard(void);

public :
    static void IsrNMI(void);
    static void IsrHardFault(void);
    //[...]

private :

    volatile InterruptVectorTable_t virtualVectorTable;
    static volatile CInterruptVectorTable* pThis;
};

È necessario creare le funzioni memorizzate nella tabella vettoriale staticpoiché il controller non può fornire un thispuntatore poiché la tabella vettoriale non è un oggetto. Quindi per pThisovviare a questo problema abbiamo il puntatore statico all'interno di CInterruptVectorTable. Entrando in una delle funzioni di interruzione statica, può accedere al pThis-pointer per accedere ai membri dell'uno oggetto di CInterruptVectorTable.


Ora nel programma, è possibile utilizzare il SetIsrCallbackfunctionper fornire un puntatore a una staticfunzione che deve essere chiamata quando si verifica un interrupt. I puntatori sono memorizzati nel file InterruptVectorTable_t virtualVectorTable.

E l'implementazione di una funzione di interruzione è simile alla seguente:

void CInterruptVectorTable::IsrNMI(void) {
    pThis->virtualVectorTable.IsrNMI(); 
}

In questo modo verrà chiamato un staticmetodo di un'altra classe (che può essere private), che quindi può contenere un altro static thispuntatore per ottenere l'accesso alle variabili membro di quell'oggetto (solo uno).

Suppongo che potresti costruire e interfacciare come IInterruptHandlere memorizzare puntatori agli oggetti, quindi non hai bisogno del static thispuntatore in tutte quelle classi. (forse lo proveremo nella prossima iterazione della nostra architettura)

L'altro approccio funziona bene per noi, poiché gli unici oggetti autorizzati a implementare un gestore di interrupt sono quelli all'interno del livello di astrazione hardware e di solito abbiamo solo un oggetto per ogni blocco hardware, quindi funziona bene con static this-pointers. E il livello di astrazione hardware fornisce ancora un'altra astrazione agli interrupt, chiamato ICallbackche viene quindi implementato nel livello del dispositivo sopra l'hardware.


Accedete ai dati globali? Sicuro, ma puoi rendere privati ​​la maggior parte dei dati globali necessari come i thispuntatori e le funzioni di interruzione.

Non è a prova di proiettile e aggiunge sovraccarico. Farai fatica a implementare uno stack IO-Link usando questo approccio. Ma se non si è estremamente stretti con i tempi, questo funziona abbastanza bene per ottenere un'astrazione flessibile di interrupt e comunicazione nei moduli senza usare variabili globali accessibili da ogni parte.


1
"In modo da poter associare diverse funzioni ai vettori di interruzione durante il runtime" Sembra una cattiva idea. La "complessità ciclomatica" del programma sarebbe passata attraverso il tetto. Tutte le combinazioni di casi d'uso dovrebbero essere testate in modo che non vi siano conflitti di tempi o di utilizzo dello stack. Un sacco di mal di testa per una funzionalità con utilità IMO molto limitata. (A meno che tu non abbia una custodia per bootloader, questa è un'altra storia) Nel complesso, questo ha un odore di meta programmazione.
Lundin,

@Lundin Non vedo davvero il tuo punto. Lo usiamo per associare ad esempio l'interruzione DMA al gestore di interruzioni SPI se il DMA è in uso per la SPI e al gestore di interruzioni UART se è in uso per l'UART. Entrambi i gestori devono essere testati, certo, ma non è un problema. E sicuramente non ha nulla a che fare con la meta programmazione.
Arsenale,

DMA è una cosa, l'assegnazione in fase di esecuzione dei vettori di interruzione è qualcos'altro. Ha senso lasciare che un'installazione del driver DMA sia variabile, in fase di esecuzione. Una tabella vettoriale, non così tanto.
Lundin,

@Lundin Immagino che abbiamo opinioni diverse su questo, potremmo iniziare una chat al riguardo, perché ancora non vedo il tuo problema con esso - quindi potrebbe essere la mia risposta scritta così male, che l'intero concetto è frainteso.
Arsenal,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.