AUTOSAR-Konfigurationen: Unsauberes Konfigurieren führt zu instabilen Systemen
Zuerst erschienen in der Elektronik automotive, Ausgabe 12/15
Durch unvollständige AUTOSAR Konfigurationen oder Umgehung der Konfiguration durch die Software-Entwickler können instabile Systeme oder sporadisch inkonsistente Daten entstehen. Wie sich das vermeiden lässt, erklärt der nachfolgende Beitrag.
Software für Steuergeräte (ECUs) wird zunehmend komplexer. Um diese Komplexität im Griff zu behalten, kommt in praktisch allen umfangreicheren ECUs AUTOSAR zum Einsatz. Ein solches System bietet eine Vielzahl an Konfigurationsmöglichkeiten und Optionen, die von den Software-Architekten richtig und vollständig konfiguriert werden müssen.
Wesentliche Bestandteile von AUTOSAR Systemen sind dabei Software-Komponenten (SWC) und deren Ports. SWCs wiederum können aus mehreren ausführbaren Teilen bestehen, den sogenannten Runnable Entities, kurz Runnables, welche schließlich während der Integration den Tasks zugewiesen werden. Dabei können auch Runnables derselben Software-Komponente in verschiedenen Tasks ausgeführt werden. Diese Zusammenhänge veranschaulicht Bild 1.
Damit Runnables auch Daten aus Ports auslesen oder in Ports schreiben können, müssen Access Points definiert werden. Aus diesen Zugriffspunkten generiert der RTE-Generator die APIs, die im eigentlichen Code schließlich verwendet werden. Aus dem obigen Beispiel mit den Access Points dataReceivePointByArgument (für Re1) und dataSendPoint (für ReZ) entstehen so für die Runnables die nachfolgenden APIs:
void ReZ( void )
{
Std_ReturnType retValue = RTE_E_OK;
DtSpd1 spd;
/* fill spd with data … */
…
/* write spd to Port PpTx*/
retValue = Rte_Write_PpTx_speed( spd );
}
void Re1( void )
{
Std_ReturnType retValue = RTE_E_OK;
DtSpd1 spd;
/* read spd from port RpRx*/
retValue = Rte_Read_RpRx_speed( &spd );
…
}
Stellt der Entwickler nun fest, dass er in Re2 ebenfalls die Daten aus Port RpRx auslesen muss, so könnte er die für Re1 generierte API Rte_Read_RpRx_speed dafür benutzen, ohne dass eine Fehlermeldung oder eine Warnung ausgegeben werden würde. Tatsächlich ist das jedoch keine gute Idee, denn die konfigurierten Access Points dienen dem RTE-Generator beispielsweise dazu, festzulegen, ob und wie die Zugriffe auf die RTE geschützt werden müssen.
In dem in Bild 2 abgebildeten Beispiel wäre kein konkurrierender Zugriff auf die Daten möglich, weil sowohl das Schreiben in ReZ, als auch das Lesen in Re1 in derselben Task stattfinden und dadurch eben nicht „gleichzeitig“ erfolgen können. Die generierte RTE wird in diesem Fall also vermutlich keinen Schutzmechanismus, beispielsweise durch Interrupt-Sperren oder OS-Resources einfügen. Im einfachsten Fall wird nachfolgender Code für die RTE generiert:
#define Rte_Write_PpTx_speed( data ) (Rte_RxBuf_42 = (data), RTE_E_OK)
#define Rte_Read_RpRx_speed( &data ) (*(data) = Rte_RxBuf_42, RTE_E_OK)
Liest nun aber auch das Runnable Re2 die Daten aus dem Port und hat der Task B eine höhere Priorität als Task A, kann ein Schreibvorgang aus ReZ durch Re2 unterbrochen werden und die in Re2 ausgelesenen Daten wären inkonsistent – vorausgesetzt es handelt sich bei DtSpd1 nicht um ein „atomares“ Datum.
Probleme ergeben sich für ähnliche Szenarien, zum Beispiel für den Fall, dass ReZ in Task C läuft. In diesem Fall müsste der RTE-Generator zwar einen Schutzmechanismus zwischen Re1 aus Task A und ReZ (aus Task C) vorsehen. Auf diesem Schutzmechanismus, wie OS- Resources, hat jedoch Re2 aus Task B keinen Zugriff, was im besten Fall mit einem OS_Error signalisiert wird. Für andere Zugriffsarten, beispielsweise dataReadAccess oder auch Client-Server Kommunikation ergeben sich ähnliche Situationen. Die Konfiguration müsste also korrekterweise um einen Zugriff von Re2 auf Port RpRx ergänzt werden, wie in Bild 3 veranschaulicht ist.
Alle drei abgebildeten Runnables könnten damit auf die Ports über die generierten APIs zugreifen:
void ReZ( void )
{
Std_ReturnType retValue = RTE_E_OK;
DtSpd1 spd;
/* Fill spd with data … */
…
/* write spd to port PpTx */
retValue = Rte_Write_PpTx_speed( spd );
}
void Re1( void )
{
Std_ReturnType retValue = RTE_E_OK;
DtSpd1 spd;
/* read spd from port RpRx */
retValue = Rte_Read_RpRx_speed( &spd );
…
}
void Re2( void )
{
Std_ReturnType retValue = RTE_E_OK;
DtSpd1 spd;
/* read spd from port RpRx */
retValue = Rte_Read_RpRx_speed( &spd );
…
}
Im Vergleich zu vorangegangenem Beispiel wird keine neue API erzeugt. Der generierte Code für die APIs wird damit – je nach RTE-Generator und weiteren Konfigurationsparametern – komplexer:
#define Rte_Write_PpTx_speed( data ) Rte_Write_SWC2_PpTx_speed( data )
#define Rte_Read_RpRx_speed( &data ) Rte_Read_SWC1_RpRx_speed( &data )
FUNC(Std_ReturnType, RTE_CODE) Rte_Write_SWC2_PpTx_speed( DtSpd1 data )
{
Std_ReturnType Rte_Status = RTE_E_OK;
Rte_IntLock();
Rte_RxBuf_42 = (data);
Rte_IntUnlock();
return Rte_Status;
}
FUNC(Std_ReturnType, RTE_CODE) Rte_Read_SWC1_RpRx_speed( P2VAR(DtSpd1, AUTOMATIC, RTE_APPL_DATA) data )
{
Std_ReturnType Rte_Status = RTE_E_OK;
Rte_IntLock();
(*data) = Rte_RxBuf_42;
Rte_IntUnlock();
return Rte_Status;
}
Durch Umgehen der Konfiguration durch das Verwenden generierter APIs für „andere“ Runnables können sporadisch inkonsistente Daten entstehen. Die Fehlersuche zu einem solchen Problem kann sehr viel – vermeidbare – Zeit kosten. Deshalb ist es wichtig, das System vollständig zu konfigurieren und sich bei der Implementierung strikt an die Konfiguration zu halten. Das bedeutet nur die APIs zu verwenden, welche auch für das entsprechende Runnable generiert wurden.