Arbeit mit dem QGEN-GUI: Codegenerierung aus einem Simulink-Modell.

Arbeit mit dem QGEN-GUI: Codegenerierung aus einem Simulink-Modell. Adacore

Modellbasierte Entwicklung und automatische Codegenerierung gehören zu den wichtigsten Innovationen bei der Entwicklung von Embedded-Systemen. Denn: domainspezifische Modellierungssprachen erlauben es Domain-Experten, Software-Spezifikationen zu schreiben; automatische Codegeneratoren erzeugen aus der Spezifikation direkt ausführbaren Code. Dennoch brachten diese Innovationen keineswegs das Ende der Handarbeit: Wie Untersuchungen herausfanden, übersetzen sechs von zehn Unternehmen, die High-Integrity-Software herstellen, ihre Modelle immer noch entweder manuell in den Quellcode oder bearbeiten den durch einen vorhandenen Codegenerator von der Stange erzeugten Code aufwendig nach. Diese Erkenntnis führte zur Entwicklung von QGEN.

Eine manuelle Bearbeitung oder Nachbereitung von generiertem Code ist bisher erforderlich, weil die Generatoren ihre Strategie zur Codeerzeugung nicht individuell anpassen. Unternehmen verwenden aber meist interne Standards für das Coding, die nur teilweise mit internationalen Standards wie MISRA-C übereinstimmen. Da also die Standardlösungen nicht perfekt zu spezifischen Einsatzbedingungen passen, werden In-House-Werkzeuge für die Nachbearbeitung des generierten Codes entwickelt, oder – noch schlimmer – die Übersetzung von Modellen in Quellcode erfolgt manuell.

Eckdaten

Automatisch generierter Code ist oft nicht so aufgebaut, wie es sich Entwickler wünschen. Da der Generator die jeweiligen Wünsche nicht kennt, bietet zum Beispiel QGEN eine Menge an Optionen bis hin zu komplexen Python-Skripts, die den erzeugten C-Quellcode an die eigenen Bedürfnisse anpasst.

QGEN ist ein Codegenerator für Simulink- und Stateflow-Modelle, der nicht nur von Haus aus unterschiedliche Strategien der Codegenerierung unterstützt, sondern Nutzern auch tiefgreifende Anpassungen ermöglicht. Damit können Anwender ihre spezifischen Bedürfnisse erfüllen. Wie derlei Anpassungen aussehen und wie sie sich auswirken, zeigt nachfolgend ein einfaches Simulink-Beispiel. Die Anpassungsmöglichkeiten reichen von einfachen Konfigurationsoptionen bis zu solchen, mit denen Power-User Teile der Codegenerierung von QGEN ersetzen können.

Strategien der Codegenerierung

Entsprechend der klassischen Codegenerierungs-Strategie für synchrone Modellierungssprachen besteht die gesamte Strategie der Codegenerierung von QGEN in der Herstellung von zwei Hauptfunktionen: erstens die Initialisierung eines Systems (init) und zweitens die Ausführung durch eine zugrunde liegende Laufzeit-Umgebung in einer konstanten Periode (compute).

Klassische Hauptfunktionen

extern void init(); /* initialize the persistent state */

extern void compute
(int32 const In1[2][3]; float const In2; /* inputs */
float Out1[3]; bool* const Out2); /* outputs */

Ausgehend von dieser Strategie ist es möglich, QGEN so zu konfigurieren, dass er anderen Schnittstellencode für das System erzeugt. Anwender können alle Codegenerierungs-Optionen ohne Änderung des Modells oder seiner Konfiguration in Simulink verändern, das heißt, dass sie das Modell ohne Modifikation in verschiedenen Kontexten wiederverwenden können.

Zum Beispiel ist es mit den Optionen –wrap-state und –wrap-io möglich, den persistenten Zustand und die formalen Input-/Output-Parameter in Datenstrukturen einzubinden (Structs). Die Option –wrap-state ermöglicht die einfache Wiederverwendung des gleichen Algorithmus mit mehreren Daten, während es –wrap-io erlaubt, mehrere Parameter mit einer einzigen Datenstruktur zu übergeben. Das behebt potenzielle Probleme, etwa wenn der Ziel-Prozessor nur über eine begrenzte Anzahl von Registern für die Speicherung von Funktionsparametern verfügt. Das kombinierte Ergebnis beider Optionen ist der folgende Code:

Parameter in Structs einbinden

typedef struct {
float var1;
float var2 [2][3]
} state;

typedef struct {
int32 In1[2][3];
float In2
} input;

typedef struct {
float Out1[3];
bool Out2;
} output;

extern void init(state* const State);

extern void compute (state* const State;
input const* const Input;
output* const Output);

Durch die Option –global-io ist es stattdessen möglich, die formalen Parameter einer Funktion in globalen Variablen abzulegen, um sie so beispielsweise bestimmten Speicheradressen zuzuordnen:

Globale Variable

state myState;
input myInput;
output myOutput;

extern void init(); /* will access myState */
extern void compute(); /* will access myState, myInput and myOutput */

Blockspezifische Anpassungen

Die Schalter –wrap-state, –wrap-io und –global-io sind nur einige der möglichen Optionen für die Codegenerierung, die die Art beeinflusst, wie die Code-Interfaces aussehen. Für den eigentlichen Funktionscode bietet QGEN mindestens zwei Gestaltungsmöglichkeiten: Die erste arbeitet auf der Ebene der arithmetischen Operationen. Angenommen im generierten Code befindet sich eine Matrix-Summierung mit Integer-Saturation. Der typische Code sieht dann etwa so aus:

Matrix-Summierung

for (i = 0; i < rows-1; i++) {
for (j = 0; j < columns-1; j++) {
if (A[i][j] > 0) {
if (B[i][j] > MAX – A[i][j]) {
C[i][j] = MAX;
} else {
C[i][j] = A[i][j] + B[i][j];
}
} else {
if (B[i][j] < MIN – A[i][j]) {
C[i][j] = MIN;
} else {
C[i][j] = A[i][j] + B[i][j];
}
}
}
}

Dieser Code ist zwar semantisch korrekt, bietet aber keine hohe Performance, vor allem dann, wenn der Zielprozessor intrinsische Funktionen unterstützt, die das gleiche Ergebnis möglicherweise mit Vektorisierungsfunktionen erzielen. Dies ist unter anderem der Fall für ARM-, Power-PC- oder Renesas-Prozessoren. Hier ist es mit dem Schalter –arith-conf und einer Konfigurationsdatei möglich, QGEN so einzustellen, dass intrinsische Funktionen aufgerufen werden. Beispielsweise weist die folgende Zeile QGEN an, jedes Mal int32_sat_add aufzurufen, wenn zwei int32 summiert werden:

Integer-Saturation als Funktion

int32 = int32 + int32 :
saturated : y1 = int32_sat_sum(int32 u1, int32 u2) : myLib.h

Mit dieser Zeile in der Konfigurationsdatei wird der generierte Code für das obige Matrix-Beispiel so aussehen:

Matrix-Summierung mit Funktion

#include „myLib.h“

for (i = 0; i < rows-1; i++) {
for (j = 0; j < columns-1; j++) {
C[i][j] = int32_sat_sum(A[i][j], B[i][j]);
}
}

Es ist natürlich möglich, intrinsische Operationen auch für Matrizen zu verwenden, beispielsweise indem man die folgende Zeile der Konfigurationsdatei zufügt:

Matrix-Operation in einer Funktion

int32[][] = int32[][] + int32[][] :
saturated : vint32_sat_sum(int32 u1[][], int32 u2[][], int32[][] y1)
: myLib.h

In diesem Fall verkürzt sich der erzeugte Code auf:

Matrix-Operation aufrufen

#include „myLib.h“

vint32_sat_sum(A, B, C);

Die Substitution von arithmetischen Operationen mit Aufrufen intrinsischer Funktionen wird für alle Datentypen und Dimensionen, einschließlich der realen komplexen Typen unterstützt.

Externe Funktionen

Neben der Konfiguration der Rechenoperationen erlaubt QGEN auch, externe Funktionen für spezifische Bausteine aufzurufen. Vielleicht möchte der Benutzer eine eigene Funktion für Lookup-Tabellen verwenden. Er könnte zwar mit S-Funktionen das gleiche Ergebnis erzielen, aber dazu müsste er das Modell ändern, um den Baustein der Lookup-Tabellen durch S-Funktionen zu ersetzen. Durch die QGEN-Option –block-conf kann er bei der Codegenerierung den nativen Simulink-Baustein beim Aufruf einer externen Funktion beibehalten. Beispielsweise ist es möglich, die folgende Zeile in einer Konfigurationsdatei einzusetzen:

Externe Funktion einbinden

Lookup_n-D : double<-double,double :
NumberOfTableDimensions => 2 : :
double y1 = lookup_comp
(double u1, double u2, double BreakpointsForDimension1[],
uint32 size(BreakpointsForDimension1,1),
double BreakpointsForDimension2[],
uint32 size(BreakpointsForDimension2,1), double Table[][],
uint32 size(Table,1), uint32 size(Table,2)) : :
liblookup.h

Jedesmal wenn das Modell einen Lookup_n-D-Baustein mit einer Anzahl von Dimensionen gleich 2 enthält, erzeugt diese Konfiguration einen Anruf für lookup_com. Die Parameter, die der Funktion übergeben werden, sind diejenigen, die in der Konfigurationsdatei spezifiziert wurden. Sie umfassen die Parameter der Bausteine und die Dimensionen des Inputs.

Customizing in der Tiefe

Über die bisher dargestellten Möglichkeiten hinaus lässt sich QGEN durch benutzerspezifische Plug-ins auch sehr tiefgreifend anpassen. QGEN arbeitet immer mit einer standardisierten internen Darstellung, die es in einer XML-Datei veröffentlichen kann. Das Schema der XML-Datei wird formalisiert als Ecore-Metamodell. In der Praxis kann QGEN eine XML-Datei ausgeben, die die intermediäre Darstellung des Simulink-Modells für die verschiedenen Durchgänge der Modellzusammenstellung umfasst. Wird die XML-Datei gespeichert, so ist es möglich, sie zu modifizieren und sie wieder in QGEN zu importieren. Auf diese Weise lassen sich anwenderspezifische Plug-Ins entwickeln. Diese laufen ohne den QGEN-Quellcode neu zu kompilieren, sie dürfen auch in anderen Sprachen geschrieben sein, zum Beispiel Ada, C, C++, Java, Python oder Modelltransformationswerkzeuge unter Eclipse wie QVT oder ATL.

Ein Beispiel: Angenommen der Modellierungsstandard für ein bestimmtes Projekt erfordert es, alle Simulink-Variablen, deren Name mit dem Präfix „obs_“ beginnt, auf nicht-optimierbare globale Variablen abzubilden. Dies wäre in Simulink etwa durch Speicherklassen möglich. Allerdings erfordert das erstens, alle Signale mit „obs_“ zu suchen und zweitens die Speicherklasse entsprechend zu ändern. Modifiziert man das Modell auf diese Weise, so schränkt man die Wiederverwendbarkeit des ursprünglichen Modells ein. In QGEN ist es möglich, das gleiche Ergebnis so zu erreichen:

1. QGEN mit einem Simulink-Modell ausführen, aber vor der Ausgabe von C oder Ada mit dem Schalter –steps anhalten. In diesem speziellen Fall sollen erst Preprocessing, Sequenzierung, Codemodell-Generierung, -Optimierung und -Expansion ablaufen und dann QCEN das XML exportieren.

2. Das XML-Dokument bearbeiten. Dabei kann der Benutzer seine bevorzugte Programmiersprache, zum Beispiel Python, oder sein bevorzugtes Werkzeug verwenden. In diesem speziellen Fall ist es ausreichend, für alle „“-Knoten, deren Name mit „obs_“ beginnt, die Sichtbarkeit zu ändern, zum Beispiel so:

Sichtbarkeit von Variablen per Skript ändern

for e in doc.getElementsByTagName(‚variable‘):
if e.getAttribute(‘name‘).startWith(‚obs_‘):
e.setAttribute(‚visibility‘, ‚GLOBAL‘)

3. Das XML-Dokuments in einer neuen Datei speichern.

4. QGEN erneut ausführen und dabei die neue XML-Datei als Input übergeben.

Es ist also ganz einfach, eigene Plug-Ins in QGEN zu entwickeln. Diese können nicht nur die Codegenerierung beeinflussen, sondern zum Beispiel auch Makefiles erzeugen, Analysetools anbinden oder Dokumentationen erzeugen. QGEN erweist sich so als überaus flexibles Werkzeug, mit dem sich auch sehr spezielle Anforderungen abdecken lassen.

Johannes Kanig

ist Senior Software Engineer bei Adacore in Paris.

(lei)

Sie möchten gerne weiterlesen?