C'è una lunga storia di linguaggi di programmazione del flusso di dati per sistemi reattivi. Negli anni Ottanta, sono state sviluppate diverse lingue nell'ambito dell'approccio Programmazione sincrona .
Penso che Traduzione del tempo discreto SIMULINK
SEGNALARE sarebbe un buon punto di partenza per essere esposto a Simulink, Signal. Inoltre, esiste un caso d'uso del motore DC.
Approccio sincrono
Invece di manipolare i valori, i linguaggi sincroni definiscono equazioni su flussi sincroni. Ad ogni passo del clock logico, gli input vengono letti e propagati istantaneamente a variabili e output locali. L'ipotesi di ritardo zero significa che ci vuole un tempo costante per aggiornare il tuo stato (fino a te per dimostrare che il tempo di esecuzione effettivo si adatta sempre al periodo di tempo fisico richiesto). Ecco un esempio con uno stato interno, in Luster :
node sum (in : int) returns (sum : int);
let
sum = in + (0 -> pre(sum));
tel;
La freccia separa il codice che viene eseguito la prima volta (inizializzazione, a sinistra) e il codice che viene aggiornato in tutti gli altri istanti (caso generale, a destra). A destra della freccia, è quindi consentito fare riferimento ai valori precedenti di sum
: questo è lo scopo di pre
.
Con la definizione di cui sopra, possiamo analizzare le dipendenze in anticipo, fare una schedulazione statica del codice e produrre codice C basato su una funzione init
e una funzione step
, che fondamentalmente sono:
/* local globals */
inline int sum, in, tmp;
/* Interface with other code */
extern int read_in();
extern void write_sum(int);
void sum_init()
{
tmp = 0;
}
void sum_step ()
{
in = read_in(); /* external */
sum = tmp + in;
write_sum(sum); /* external */
/* Update memory */
tmp = sum;
}
Oggi sembra esserci un rinnovato interesse per la programmazione reattiva funzionale. Anche se ci sono alcune ricerche su FRP in tempo reale
, il FRP generalmente fornito non offre lo stesso tipo di garanzie statiche richieste nei sistemi embedded / critici:
-
utilizzo della memoria costante: la memoria viene allocata staticamente e completamente determinata in anticipo (nessun GC, grazie). I valori passati sono accessibili solo tramite gli operatori di ritardo (ad esempio pre
), quindi il numero di operatori di ritardo nidificati indica il numero di versioni precedenti di una variabile da conservare, che è noto staticamente. Potresti immaginare di costruire una finestra scorrevole di memoria per osservare il flusso infinito descritto dalle tue equazioni.
-
tempo di esecuzione costante: il tempo di esecuzione nel caso peggiore di ogni passo temporale è molto più facile da calcolare.
-
analisi di causalità: l'ordine delle operazioni è noto in anticipo. Quando si progettano sistemi reattivi distribuiti, è possibile garantire che non si verificherà un deadlock.
-
I linguaggi sincroni usano anche gli orologi, che determinano quando deve avvenire un calcolo: puoi scrivere x when b
per avere un flusso in cui gli unici valori calcolati esistono quando il flusso dati booleano b
valuta true (e nota che b
è un flusso di dati stesso, che può anche avere un orologio).
Nel linguaggio Segnale , che consente di descrivere unità di calcolo distribuite, gli orologi consentono ad esempio di emettere solo le informazioni di sincronizzazione necessarie tra unità separate, asincrone: se trasmetti un contatore di tick tra due sistemi e le relazioni di clock ti dicono che il flusso A è presente ogni 5 tick e flusso B ogni 7 tick, non è necessario trasmettere anche il reale flusso di clock booleano di A e B : il ricevitore sa esattamente quando leggere un valore per A e uno per B , basato solo sulla spunta. Gli orologi formano relazioni booleane e possono a volte essere dedotti implicitamente dallo stato conosciuto di altri orologi.
Quelle lingue sono sviluppate in contesti industriali, generalmente come parte di una più ampia catena di strumenti: cerca SCADE, ecc. A seconda dell'utilizzo, potresti preferire usare i compilatori accademici.
Che dire di F #?
F # è raccolta dalla spazzatura, per quanto ne so. Io tendo a pensare che puoi eseguire i controller in un ambiente raccolto, se sei abbastanza attento e il tuo dispositivo non richiede tempi precisi. Personalmente modificherei la variabile esistente per evitare di allocare memoria durante l'esecuzione, in modo che il consumo effettivo di memoria nel ciclo di controllo sia zero.
Se la memoria viene continuamente allocata (e quindi raccolta, si spera), si potrebbe avere, in media, un utilizzo costante della memoria con possibili pause GC. Il garbage-collector non è in tempo reale (anche se esistono GC in tempo reale), ma se il periodo fisico è abbastanza grande, avrai una bassa probabilità di perdere una scadenza.
Se manca una scadenza è fuori questione, usa un codice generato dalle lingue sopra, o scrivine uno a mano se hai solo bisogno di un semplice ciclo di controllo. Utilizza un linguaggio di basso livello come C o Ada (magari all'interno di un sistema operativo Real-Time o direttamente sul microcontrollore).