Un kernel per il System-On-Chip

Con l’introduzione dei System-On-Chip si è sempre più affermata l’esigenza di sfruttare l’enorme potenzialità che offre una tecnologia del genere: i requisiti funzionali e prestazionali del nostro sistema possono essere così realmente modellati con l’ambiente.

Le applicazioni di tipo embedded di tipo real-time sono sempre più presenti nelle nostre attività quotidiane, dalle soluzioni di tipo automotive a quelle più squisitamente domestiche. Il segmento dei sistemi embedded devono rispondere sostanzialmente, a tre requisiti: devono essere in grado di svolgere il proprio compito utilizzando delle risorse limitate, devono essere in grado di rispondere in un tempo predicibile alle mutate condizioni ambientali e, per ultimo, il loro sviluppo deve seguire, in maniera estremamente rigida, un ciclo di lavoro estremamente particolare con precise norme di riferimento. Il cuore di un sistema del genere è il run-time system (RTS). Un RTS deve essere in grado di fornire e gestire le risorse necessarie per il corretto funzionamento del sistema: dalla creazione e gestione dei task ai meccanismi di comunicazione inter-task.

Un sistema di tipo real-time deve garantire che un task deve terminare entro una deadline predeterminata. Per rispondere a questa esigenza è necessario che il processo di schedulazione deve rispondere a precisi criteri progettuali. La letteratura divide i task di un sistema real-time in due grandi famiglie: soft real-time e hard real-time. Nel primo caso si assume che non è importante che il task concluda la sua attività nella sua deadline; infatti, in un sistema del genere non rispettare questo vincolo è sufficientemente tollerato. In alternativa, in un sistema di tipo hard, è assolutamente necessario che un task concluda le sue operazioni all’interno della sua deadline a meno di provocare danni al sistema. Si assume che i sistemi che devono rispettare le esigenze di un Hard Real Time devono essere costituiti da un RTS minimale e deterministico. In genere, non esistono linguaggi corredati da una libreria di tipo multitasking a meno di riferirsi al linguaggio Ada, definito da apposite specifiche dal Dipartimento della Difesa degli USA. Nella maggioranza dei casi un sistema di tipo multitasking è fornito a parte mediante un’apposita libreria. Successivamente è stato definito un modello più formale che ha preso il nome di Ravenscar profile. Mettere a punto un design di un sistema embedded per applicazioni di tipo safety-critical è un’operazione per nulla semplice. In effetti, occorre considerare una molteplicità di aspetti: occorre definire le specifiche di un RTK che sia in grado di rispondere a requisiti funzionali, determinare il modello comportamentale e definire l’analisi del sistema e, per ultimo non perché è di minore importanza, decidere la tolleranza ai diversi fault e le procedure di recovery da associare al fine di garantire una degradazione tollerabile del suo normale funzionamento. Per cercare di dare delle risposte in questo senso sono stati svolti diversi lavori di studio e ricerca che, nel tempo, hanno avuto, chi più e chi meno, delle applicazioni pratiche. La quasi totalità di questi lavori prevedono l’uso del profilo Ravenscar, ma la maggior parte degli analisti concordano ad affermare che il miglio profilo è sicuramente quello hardware: ovvero, l’implementazione del profilo Ravenscar direttamente in Hardaware utilizzando i diversi costrutti che un linguaggio comportamentale può disporre, come ad esempio il VHDL. Un’implementazione del genere è sicuramente in grado di garantire un controllo del sistema in maniera più deterministica e prevedibile. Quando ci riferiamo al termine di Ravernscar di solito ci si riferisce al profilo Ravenscar presente in Ada95. Il linguaggio definito dal DoD ha subito diverse evoluzioni nel tempo e per ultimo, con la versione Ada95, è in grado di coniugare l’efficienza con le caratteristiche deterministiche che un linguaggio deve sempre utilizzare. Come si è detto, alcuni ritengono che la migliore implementazione del profilo Ravenscar è quello che si ottiene utilizzando direttamente l‘hardware. Questa è probabilmente l’idea più interessante: definire e realizzare un kernel facendo ricorso direttamente all’hardware tanto da essere così facilmente inseribile in un Field Programmable Gate Array (FPGA). In questo modo possiamo garantire le condizioni operative ideali per un’applicazione di questo tipo. Rispetto ad un kernel tradizionale scritto con un linguaggio come il C, e posto in esecuzione direttamente dal processore, un approccio del genere è totalmente diverso. Realizzare il kernel direttamente in hardware produce un valore prestazionale sicuramente più interessante perché garantisce un’alta risposta agli eventi e, soprattutto, una migliore predecibilità e un utilizzo più razionale delle risorse disponibili insieme ad un determinismo che difficilmente si riuscirebbe a garantire con un approccio tipicamente software. In un sistema di tipo tradizionale, kernel in un ambiente di tipo multitasking con un processore singolo, il sistema operativo stesso deve essere messo in esecuzione, com’è ovvio, dallo stesso processore. Il kernel è fermato ad intervalli regolari, in base al tick, per aggiornare i suoi contatori e variabili di sistema. In questo modo, il task corrente è sospeso per permettere alle funzioni del kernel di essere eseguite. In seguito, lo schedulatore verifica se, nella coda dei task pronti, è presente un task con una priorità più alta. Proviamo a cambiare il nostro punto di vista. Possiamo senz’altro affermare che un kernel scritto in VHDL aumenta sicuramente il determinismo del sistema e migliora le prestazioni complessive. Possiamo pensare di far gestire ad un apposito modulo VHDL i servizi di task handling (creazione, schedulazione e abort di un task), sincronismo (semafori, flag e risorse condivise), timing (delay asincrono, interrupts, watchdog, allarmi periodici). Con un gestore di task direttamente in hardware, le corrispondenti operazioni di scheduling possono essere eseguite senza interrompere le operazioni del task corrente. Un approccio del genere permette, ad esempio, di eliminare un tick di sistema.

Kernel in VHDL

A questo punto possiamo anche pensare come realizzare un sistema del genere. Il kernel è scritto direttamente in VHDL ed è localizzato nell’FPGA, mentre i task sono realizzati in un linguaggio software ad alto livello, ad esempio Ada o C, e sono presenti nella memoria direttamente accessibile dal processore. Questi comunicano attraverso meccanismi di sincronizzazione e possono anche condividere variabili. I task, che sono residenti nella memoria principale, comunicano con il kernel attraverso il bus: inviano e ricevono istruzioni e parametri. La figura 1 mostra un possibile sistema del genere.

Figura 1: componenti connessi tramite un Bus.

Figura 1: componenti connessi tramite un Bus.

Quando parliamo di istruzioni ci possiamo riferire alle system call che il kernel è in grado di gestire: creazioni di task o meccanismi di delay. I meccanismi di comunicazione inter-task possono sono realizzati attraverso sistemi sincroni di controllo. In questo modo i task che sono messi in esecuzione devono utilizzare un sistema basato su una schedulazione di tipo preemptive. Per gestire in maniera corretta uno scheduler realizzato in questo modo il kernel deve tenere traccia le chiamate a funzioni di delay e a quelle di sincronizzazione. La figura 2 mostra l’architettura del kernel e le relazioni con l’applicazione. In cima ci sono i task software compilati e caricati nella memoria visibile del processore.

Figura 2: architettura del kernel e dell’applicazione

Figura 2: architettura del kernel e dell’applicazione

Realizzare l’interfaccia

Possiamo pensare di realizzare l’interfaccia in due parti. La prima si occupa di gestire l’accesso ai registri (BISM, Bus Interface State Machine) attraverso il bus, mentre la seconda (KISM, Kernel Interface State Machine) gestisce il protocollo di comunicazione, l’acknowledgement e il processamento delle istruzioni e dei dati tra registri, oltre alla generazione di eventuali interrupt verso il processore e gli eventuali dati che sono necessari alla corretta esecuzione dei task presenti sullo strato processore. Il bus è costituito da un registro a 32-bit e da un registro di output, sempre a 32-bit. Il BSIM, mostrato in figura 3, dispone solo di due stati: wait_state e ack_state.

Figura 3: macchina a stati per l’interfaccia con il bus.

Figura 3: macchina a stati per l’interfaccia con il bus.

Quando siamo in wait_state, la funzionalità BISM si pone in attesa di un task in running mode per rispondere alle sollecitazioni richieste. Per accedere in modalità di lettura o di scrittura, il segnale di chip select, cs_n, deve essere correttamente pilotato dal task. In lettura il dato deve essere inserito nello specifico registro per permetterne la lettura da parte del processore. Viceversa, nelle operazioni di scrittura, il kernel deve leggere il dato che il processore ha inserito nel registro. Dopo un’operazione di lettura o di scrittura su registro, il segnale ack_n è messo a stato logico basso dallo strato BISM. Ponendo il segnale ack_n a livello basso si intende informare il task che è stata gestita un’operazione di lettura o scrittura. Questo tipo di riconoscimento, o acknowledge, deve essere realizzato sia per operazioni di scrittura che lettura al PPC perchè in questa configurazione il processore si comporta da bus master  e deve conoscere la transizione dei dati per verificare la reale disponibilità del dato o il processamento di questi dall’FPGA. Per implementare correttamente l’interfaccia sono necessari otto registri ognuno è posizionato
ad un indirizzo specifico.

Registri

Dalla tabella 1 alla 8 sono evidenziarti i vari registri disponibili in questa implementazione.

Tabella 1 – Command Register (input), address 0x100

Tabella 1 – Command Register (input), address 0x100

 

Tabella 2 – Interrupt Acknowledge Register (Input), address ox10000

Tabella 2 – Interrupt Acknowledge Register (Input), address ox10000

 

Tabella 3 – Higher Parameter Register (Input), Address 0x1100

Tabella 3 – Higher Parameter Register (Input), Address 0x1100

 

Tabella 4 - Lower Parameter/Barrier Register (Input), Address 0x1000

Tabella 4 – Lower Parameter/Barrier Register (Input), Address 0x1000

 

Tabella 5 – Status Register (Output), Address 0x10100

Tabella 5 – Status Register (Output), Address 0x10100

 

Tabella 6 – New Task ID Register (Output), Address 0x11000

Tabella 6 – New Task ID Register (Output), Address 0x11000

 

Tabella 7 – Lower Time Register (Output), Address 0x11100

Tabella 7 – Lower Time Register (Output), Address 0x11100

 

Tabella 8 – Higher Time Register (Output), Address 00100000

Tabella 8 – Higher Time Register (Output), Address 00100000

In ogni tabella si vuole mettere in evidenza il numero dei bit necessari per realizzare la funzionalità e l’indirizzo del registro. In questa implementazione si intende realizzare un sistema con quattro task, quattro sistemi di sincronizzazione e otto livelli di priorità. Lo strato KISM è l’interfaccia tra il bus register e le funzionalità del kernel ed è in pratica una macchina a 11 stati, da i0 a i10. Nello stato i0 si aspetta fino a quando un task spedisce un comando verso il registro Command Register. Tutti i comandi utilizzati in questa implementazione sono mostrati nella tabella 10. Lo strato KISM è in grado di discernere se il dato ricevuto è un comando perché BISM pone a 1 il valore nel relativo campo. Una volta che lo strato KISM riconosce un comando valido questo aspetta dal task un dato mediante il registro Parameter Register, qualora fosse previsto dall’insieme dei comandi. I comandi GetTime e FindTask non hanno dati, o parametri. Quando un task spedisce un commando, magari con dei parametri, deve aspettare la risposta del kernel mediante il registro di stato. I messaggi che il kernel può inserire in questo registro sono identificati dalla tabella 9.

Tabella 9 – Status Signal

Tabella 9 – Status Signal

 

Tabella 10 – Command Register

Tabella 10 – Command Register

In questo modo il kernel informa il task l’esito dell’operazione corrente. Lo strato KISM pone il valore nel registro di stato e successivamente BISM informa KISM mediante lo stato del segnale stat_out a 1 una volta che il task ha completato la lettura del registro di stato. La figura 4 mostra il meccanismo di handshaking tra il processore e il kernel presente in FPGA per una sequenza tipica di comando, dalla figura possiamo notare che per ogni lettura o scrittura da parte del PPC, lo strato BISM deve inviare il corrispondente acknowledged e il processore deve ricevere uno status valido associate ad ogni sequenza di comandi prima di proseguire.

Figura 4: handshaking tra PPC e Kernel.

Figura 4: handshaking tra PPC e Kernel.

 

Codice

La porzione che presentiamo nel listato 1 è un estratto e permette di gestire l’interfaccia verso un PowerPc.

entity interface_ppc is
   Port ( clk : in std_logic;
reset_n      : in std_logic;
write_n      : in std_logic;
cs_n          : in std_logic;
addr          : in std_logic_vector(NREGS-1 downto 0);
din           : in std_logic_vector(REG_LENGTH-1 downto 0);
dout          : out std_logic_vector(REG_LENGTH-1 downto 0);
ack_n         : out std_logic;
interrupt    : out std_logic);
end interface_ppc;

architecture Behavioral of interface_ppc is

———- state variables for bus interface ————————
type bus_state_type is (wait_state, ack_state);
signal bus_state : bus_state_type;

———- regs holding input from bus ———————————
signal create : std_logic;
signal findtask : std_logic;
signal delayuntil : std_logic;
signal setfreq : std_logic;
signal Tcpu : std_logic_vector(BITTASKS-1 downto 0);
signal Pcpu : std_logic_vector(BITPRIO-1 downto 0);
signal DUntil : std_logic_vector(MAXDUNTIL-1 downto 0);
signal TaskID : std_logic_vector(BITTASKS-1 downto 0);
signal TaskPrio : std_logic_vector(BITPRIO-1 downto 0);
signal CeilPrio : std_logic_vector(BITPRIO-1 downto 0);
signal POId : std_logic_vector(BITPO-1 downto 0);
———- state variables for interface ——————————
type state_type is (i0, i1, i2, i3, i4, i5, i6, i7, i8, i9, i10);
signal state : state_type;
— connection to rest of kernel
component kernel_internal
port ( clk : in std_logic;
reset_n : in std_logic;
DelayUntil : in std_logic;
SetFreq : in std_logic;
Ex : in std_logic_vector(NUMPO-1 downto 0);
Create : in std_logic;
FindTask: in std_logic;
POId : in std_logic_vector(BITPO-1 downto 0);
TaskId : in std_logic_vector(BITTASKS-1 downto 0);
TaskPrio : in std_logic_vector(BITPRIO-1 downto 0);
BarrierGet : out std_logic;
NPcpu : out std_logic_vector(BITPRIO-1 downto 0);
NTcpu : out std_logic_vector(BITTASKS-1 downto 0);
Status : out std_logic;
sTime : out std_logic_vector(MAXTIME-1 downto 0));
end component;
begin
kernel_main : kernel_internal
port map (clk=>clk, reset_n=>reset_n, DelayUntil=>delayuntil, SetFreq=>setfreq, Freq=>freq, DUntil=>Duntil, Tcpu=>Tcpu,
Pcpu=>Pcpu, CeilPrio=>CeilPrio, FPs=>fps, Upe=>upe, Ufe=>ufe, Ufpx=>ufpx, UPxe=>upxe, UFxe=>ufxe, Barrier=>barrier,
BarrierNew=>barrierNew, FPe=>fpe, FPx=>fpx, Es=>es, UEb=>ueb, UEe=>uee, UEx=>uex, Ex=>ex, Create=>create, Find-
Task=>findtask, POId=>POId, TaskId=>TaskId, TaskPrio=>TaskPrio,BarrierGet=>BarrierGet, NPcpu=>NPcpu, NTcpu=>NTcpu, Status=>
kernel_status, sTime=>sTime);
— Bus Interface State Machine
BISM : process(clk, reset_n)
begin
if(reset_n=’0’) then
bus_state <= wait_state;
dout <= (others => ‘0’);
ack_n <= ‘1’;
when others =>
— error state
bus_state <= wait_state;
end case;
end if;
end process BISM;
— Kernel Interface State Machine
KISM : process(clk, reset_n)
begin
if(reset_n=’0’) then
state <= i0;
interrupt <= ‘0’;
status <= NOSTAT;
newtask <= (others => ‘0’);
curTime_high <= (others => ‘0’);
curTime_low <= (others => ‘0’);
barrier <= ‘0’;
barrierNew <= ‘0’;
create <= ‘0’;
findtask <= ‘0’;
delayuntil <= ‘0’;
setfreq <= ‘0’;
Pcpu <= (others => ‘0’);
DUntil <= (others => ‘0’);
TaskID <= (others => ‘0’);
TaskPrio <= (others => ‘0’);
CeilPrio <= (others => ‘0’);
POId <= (others => ‘0’);
elsif(clk’event and clk=’1’) then
case state is
when i0 =>
— new cmd, no interrupt
status <= NOSTAT;
if(new_cmd=’1’ and Tcpu=NTcpu) then
interrupt <= ‘0’;
state <= i3;
— interrupt occurs - don’t care if new_cmd or not
— sw just needs to know if it sent one, and got
— interrupt before CMDDONE
elsif(Tcpu/=NTcpu) then
POID <= (others => ‘0’);
interrupt <= ‘1’;
state <= i1;
else
interrupt <= ‘0’;
state <= i0;
end if;
— if Prio changed due to Ceil Prio change,
— but same task
if(Pcpu/=NPcpu) then
Pcpu <= NPcpu;
end if;
Listato – estratto gestione interfaccia con PPC

Il modulo scritto in VHDL contiene il Bus Interface State Machine (BISM) e il Kernel Interface State Machine (KISM), la tabella 11 pone in evidenza il significato dei segnali di I/O. Per maggiori informazioni vorrei proporvi di visitare il sito del MIT al laboratorio di Real-Time.

Tabella 11 – Segnali di I/O

Tabella 11 – Segnali di I/O

Una risposta

  1. Andrea Salvatori IU6FZL Andrea Salvatori IU6FZL 4 febbraio 2019

Scrivi un commento

EOS-Academy
Abbonati ora!