La gestione degli interrupt su Linux

Gli eventi asincroni rappresentano un’importante caratteristica di un sistema embedded. Infatti, è attraverso questo meccanismo che il programma comunica con il mondo circostante.

La gestione degli eventi asincroni basa la sua tecnologia su diversi aspetti: il compilatore permette di definire funzioni richiamabili da un’ISR, mentre, non di secondaria importanza, il sistema operativo, attraverso una sua libreria di runtime, for nisce gli strumenti idonei per permetterne la loro gestione. Per questa ragione, il meccanismo di gestione di un interrupt può divergere da un ambiente all’altro secondo la presenza di librerie o patch di real-time. In questo articolo vedremo alcune considerazioni da tenere presente per gestire interrupt in ambiente Linux e per il nostro scopo ci baseremo sulla versione 2.6, e successiva, del sistema operativo.

Le interruzioni

Gli interrupt sono utilizzati per notificare al processore l’occorrenza di un determinato evento, cioè che si è verificata una condizione di errore o che una periferica richiede l’attenzione della cpu per portare a termine un determinato compito. Gli interrupt si dividono in due gruppi: sincroni e asincroni. Gli interrupt sincroni (o eccezioni) si verificano in maniera “sincrona” rispetto al clock del processore. Sono causati da condizioni anomale o da un errore di programmazione (per esempio un’operazione di divisione per zero). Gli interrupt asincroni invece si possono presentare in qualunque istante di tempo. Questi ultimi sono causati dalle periferiche di I/O, e sono utilizzati per fare comunicare la CPU con il mondo esterno. La CPU gestisce gli interrupt utilizzando un dispositivo dedicato, un Programmable Interrupt Controller (PIC): è infatti attraverso questo circuito che il microprocessore riceverà la relativa notifica. In realtà questa gestione era quella più comune fino a qualche anno fa, ora, con l’introduzione dei microcontrollori, questa gestione può essere fatta, internamente dal componente stesso mediante una serie di registri dedicati che si preoccupano della gestione degli eventi che, per la maggior parte dei casi, sono interni. Gli interrupt asincroni, o interrupt hardware, sono ulteriormente classificabili in interrupt mascherabili o non mascherabili. Gli interrupt mascherabili sono quelli che fanno capo ad un pin dedicato del microprocessore e, normalmente, vengono utilizzabili dai vari dispositivi esterni. Le CPU INTEL distinguono fino a 255 interrupt mascherabili. L’istruzione STI (SeT Interrupt enable flag) abilita l’inoltro di interrupt mascherabili, mentre l’istruzione CLI (CLear Interrupt enable flag) li disabilita.  L’interrupt non mascherabile ha ovviamente una priorità maggiore dei precedenti, in quanto non può essere mai disabilitato. Viene attivato portando a livello alto la linea NMI (Not Maskable Interrupt); la CPU termina l’istruzione in corso e poi genera un interrupt di tipo 2, associato per default all’interrupt NMI. Gli interrupt software, invece, sono determinati dall’esecuzione di una istruzione dedicata: in ambito Intel è INT N o PowerPC con sc, in questo caso viene attivata la corrispondente  ISR il cui indirizzo è all’n-esimo posto della tabella delle interruzioni (interrupt vector table). Le eccezioni si possono suddividere in fault (quando anomalie di funzionamento) rilevate e gestite immediatamente prima dell’esecuzione dell’istruzione che le genera: ad esempio nella gestione della memoria virtuale, quando il gestore genera un miss, cioè quel blocco di memoria non presente in cache. Esistono poi le trap quando le anomalie di funzionamento rilevate e gestite immediatamente dopo dell’esecuzione dell’istruzione che le genera: ad esempio un interrupt software; per finire, abort. In quest’ultimo caso le anomalie di funzionamento che non permettono di individuare esattamente l’istruzione che le hanno determinate: ad esempio un malfunzionamento segnalato da una periferica. La gestione degli interrupt per linee di I/O devono essere flessibili per gestire differenti dispositivi nello stesso momento. In una bus PCI, per esempio, diversi dispositivi possono condividere la stessa linea IRQ. Per questa ragione possiamo notare che nella tabella dei vettori di un PC, il vettore 43 è assegnato sia alla porta USB sia ad una scheda audio, tabella 1.

Tabella 1 – Assegnamenti IRQ per dispositivi di I/O

Tabella 1 – Assegnamenti IRQ per dispositivi di I/O

A prescindere del componete utilizzato, tipicamente il comportamento per una corretta gestione di un interrupt deve garantire le seguenti funzioni di base:

1-Registra  il valore di IRQ con il contenuto dei registri sullo stack

2-Spedisce un acknowledge al PIC che ha servito la linea IRQ. In questo modo è pronto per gestire il prossimo evento.

3-Esegue  la ISR associata all’evento.

4-Termina  la gestione richiamando la funzione ret_from_intr().

La figura 1 mette in risalto il flusso utilizzato per gestire un evento asincrono generato da un PIC.

Figura 1: la gestione degli interrupt.

Figura 1: la gestione degli interrupt.

PIC

Il PIC è il controllore programmabile  delle interruzioni. Uno dei primi dispositivi in questo ambito è sicuramente il modello 8259A della Intel. Questo è stato progettato per minimizzare  il software ed i tempi di risposta per la gestione di livelli multipli di interrupt a diversa priorità. Il dispositivo  è un chip LSI contenuto in un DIP da 28 pin e gestisce fino a 8 livelli di interrupt vettorizzati. Inoltre, è un componente abbastanza flessibile; infatti, permette la gestione di più controllori di interrupt in cascata fino ad un massimo di 64 livelli di interrupt e consente la programmazione di diversi modi di gestione delle  priorità tra i diversi livelli. Oggi possiamo trovare le funzionalità del chip in un FPGA, basta che si inserisca un modulo interrupt controller che supporti questo componente. Riassumendo,  il PIC può gestire fino a 8 richieste di interruzione (IR0..IR7), che possono essere estese a 64 con una connessione in cascata. I  pin INT/INTA* dell’8259 vengono collegati ai pin corrispondenti della CPU. L’8259 emette il segnale associato durante il secondo ciclo INTA e il riconoscimento  delle richieste a livello oppure sul fronte di salita. Inoltre, il dispositivo occupa due locazioni nello spazio di I/O: non è un dispositivo di tipo memory mapped. Esiste, infine, la possibilità di mascherare le richieste e di modificare le priorità.  Il componente, poi, contiene al suo interno 3 registri ad 8 bit:

» IMR (Interrupt Mask Register, registro in lettura e scrittura). Il registro è utilizzato per abilitare o meno le singole linee di richiesta di interruzione; ponendo un bit 1 nella relativa posizione del registro si disabilita l’IRQ associato.

» ISR (In Service  Register,  registro di sola lettura). Questo registro è utilizzato per sapere quali interrupt sono attualmente in gestione dalle rispettive routine. Se un bit di questo registro è a 1 significa che la relativa richiesta è in esecuzione.

» IRR (Interrupt Request Register, registro di sola lettura).  Il registro serve per sapere quali richieste IRQ sono state attivate dai dispositivi esterni. Se un bit di questo registro è a 1 significa che è arrivata una richiesta nella rispettiva linea IRQ.

La tabella 2 pone in evidenza che un IRQ può essere assegnato a qualsiasi vettori nell’intervallo 32-238.

Tabella 2 – Vettori di interrupt su Linux

Tabella 2 – Vettori di interrupt su Linux

Tuttavia, Linux usa il vettore 128 per implementare le system calls. L’architettura dei PC richiedono che i  vari dispositivi devono essere staticamente connessi ad una specifica linea IRQ. In particolare, il timer deve essere connesso alla linea IRQ 0, mentre il PIC 8259A slave deve essere connesso alla IRQ 2, il co-processore matematico esterno deve essere messo alla IRQ 13 e, per concludere, in generale, un dispositivo di I/O può essere connesso ad un limitato numero di linee IRQ. La tabella 1 mostra i vari IRQ in relazione agli diversi I/O presenti in una tipica distribuzione di Linux per PC.

La gestione  delle  interruzioni

A questo punto vediamo come si implementa un gestore delle interruzioni in ambito embedded. In linux si utilizza la struttura irqdesc per gestire un evento asincrono. Particolare attenzione deve essere dedicata a questa struttura; infatti, questa contiene le informazioni che devono essere associate ad ogni sorgente di interrupt (irqdesc si trova in include/asmarm/mach/irq.h). La struttura contiene delle relazioni, in termini di puntatori di funzioni, tra il device driver che deve essere definito e i gestori degli interrupt. È vivamente consigliato di leggere la documentazione associata alla struttura irqdesc, con le sue relazioni (per esempio irqchip), in questo modo possiamo apprezzarne l’uso e il  suo significato. Le gestioni a livello o sul fronte richiamo entrambe la funzione __do_IRQ(). Per mezzo di questa funzione sono soddisfatte tutte le richieste e le azioni associate al dispositivo. Per chi ha già sviluppato driver per sistemi embedded certamente si sarà imbattuto nelle due diverse tipologie di interrupt: sul livello o sul fronte. Esistono nella libreria delle funzioni disponibili due primitive: do_edge_IRQ() e do_level_IRQ() che rispondono a questi due possibili approcci implementativi. In base alle singole esigenze può essere utile, o conveniente, utilizzare una piuttosto di un’altra. É chiaro che per gestire gli interrupt in un sistema dedicato occorre modificare una serie di file che sono presenti in una distribuzione Linux, questo devono essere sistemati per rispecchiare la nostra applicazione e l’hardware utilizzato. Il  file entry-macro.S, contiene una macro, chiamata get_irqnr, che è utilizzata per determinare l’interrupt number ogni qualvolta che un interrupt è generato. Il file entryarmv.S contiene, invece, la definizione di irq_handler(): questa, a sua volta, chiama la funzione asm_do_IRQ() che ha il compito di gestire  I diversi IRQ hardware. Il terzo file, irq.c, contiene una serie di funzioni che sono utilizzate per gestire i diversi interrupt: esistono alcune che servono per abilitare o disabilitare specifici interrupt o altre che sono utilizzati per gestire eventi sul fronte o sul livello.

Gestione  della  richiesta

Quando è rilevato un interrupt la prima cosa che viene eseguita è un piccolo modulo assembler che deve gestire alcune operazioni preliminari; per esempio deve salvare le informazioni critiche per permettere il ripristino,  al termine dell’ISR, delle operazioni interrotte. Al termine di questa piccola porzione assembler, la libreria di run-time si preoccuperà di chiamare la funzione asm_do_IRQ. In questa funzione la linea di codice “handle(irq, desc, regs);” chiamerà, a sua volta, la funzione do_level_IRQ o do_edge_IRQ a seconda del tipo di gestione scelta.

Registrazione di un gestore

Per gestire un interrupt all’interno del sistema operativo occorre definire un driver software: questo è fatto chiamando la funzione request_irq(). La prima di queste ritorna 0 se la registrazione ha avuto successo. Per request_irq il parametro irq indica il numero dell’interruzione  cui si è interessati, il puntatore handler indica la funzione di gestione, flags una bit mask di opzioni relative alla gestione dell’interrupt, inclusi quelli relativi alla condivisione dell’interrupt con altri driver (SA_SHIRQ, SA_INTERRUPT, SA_SAMPLE_RANDOM), dev_name, rappresenta la stringa visualizzata in /proc/interrupts come nome del driver associato alla relativa linea. Proseguendo diciamo che void *dev_id è un puntatore usato per le linee di interrupt condivise; è, infatti, un identificativo univoco. Per concludere, la funzione free_irq()  è semplicemente la funzione per il rilascio dell’interrupt una volta che questo non risulta più utilizzato. Così per gestire un interrupt in ambito PC di un floppy driver è necessario utilizzare la chiamata:

request_irq(6, floppy_interrupt,
SA_INTERRUPT|SA_SAMPLE_RANDOM,
“floppy”, NULL);

mentre per rilasciare  il gestore:

free_irq(6, NULL);

In ambito di un programma embedded si può benissimo utilizzare la stessa chiamata in questo modo:

request_irq(AT91_PIN_PB01,
gpio_pb01_irq,SA_SHIRQ,
“gpio_pb01”, (void*)4)

mentre  con la funzione free_irq(AT91_PIN_PB01,  (void*)4) si rilascia il gestore. In questo caso la routine di gestione assume il  nome gpio_pb01_irq, il  valore dell’interrupt è pari a AT91_PIN_PB01 e il nome della periferica gpio_pb01.

 

 

 

 

 

Scrivi un commento

EOS-Academy

Ricevi GRATIS le pillole di Elettronica

Ricevi via EMAIL 10 articoli tecnici di approfondimento sulle ultime tecnologie