Skip to content
Weiß Rechteck
pcbreg

Memory-Tests in C: Grundlegende Strukturen und Überlegungen

Ein Memory-Test ist ein Stück Software, das fast jeder Embedded-Entwickler irgendwann in seiner Karriere schreiben muss. Sobald die Prototyp-Hardware fertig ist, erwartet man die Gewissheit, dass die Adress- und Datenleitungen richtig verdrahtet sind und dass die verschiedenen Speicherchips ordnungsgemäß funktionieren. Es ist also Aufgabe des Entwicklers herauszufinden, was potenziell schief gehen kann, und eine Reihe von Tests zu entwerfen, die potenzielle Probleme aufdecken.


Auf den ersten Blick mag das Schreiben eines Speichertests ein recht einfaches Unterfangen sein, da es darum geht Prüfdaten aus z.B. einen RAM hineinzuschreiben und auszulesen. Eine Betrachtung jedoch, was beim Zugriff auf den in der Regel parallelen RAM-Port schiefgehen kann, eröffnet den Horizont für Speichertests. Egal, ob es sich um einen externen SRAM-Baustein, einen Dual-Port-RAM, einen OCM oder einen NAND-Flash handelt, im Wesentlichen definieren der Adressport und der Datenport die Funktionalität des Speicherbausteins. Die Kontroll-Signalleitung spielt zwar auch ei

Ausgehend von zwei im Wesentlichen theoretischen Defekten, die den Zugriff auf den Speicher stören könnten, lassen sich folgende Grundüberlegungen annehmen:

  • Eine Verbindung des Adress- oder Datenbus ist getrennt
  • Eine Verbindung des Adress- oder Datenbus ist zu lang, es treten daher Delays auf
  • Eine Verbindung des Adress- oder Datenbus hat eine nicht-isolierte Verbindung zu einem anderen Signal
  • Die Kontroll-Signalleitung ist Defekt, z.B. (Write-Enable, Output-Enable, Port Enable)

 

Probleme mit den elektrischen Verbindungen zum Prozessor führen zu fehlerhaftem Verhalten des Speicher-Bausteins. Die Daten können falsch, an der falschen Adresse oder gar nicht gespeichert werden. Jedes dieser Symptome lässt sich durch Verdrahtungsprobleme auf den Daten-, Adress- und Steuerleitungen erklären.

Liegt das Problem bei einer Datenleitung, können mehrere Datenbits "zusammenhängen" (z. B. enthalten zwei oder mehr Bits immer denselben Wert, unabhängig von den übertragenen Daten). Ebenso kann ein Datenbit entweder "stuck high" (immer 1) oder "stuck low" (immer 0) sein. Diese Probleme lassen sich feststellen, indem eine Folge von Datenwerten geschrieben wird, um zu testen, ob Datenpins unabhängig von den anderen auf 0 und 1 gesetzt werden kann.

Wenn eine Adressleitung ein Verdrahtungsproblem hat, kann es sein, dass sich die Inhalte von zwei Speicherplätzen überlappen. Mit anderen Worten: Daten, die an eine Adresse geschrieben werden, überschreiben stattdessen den Inhalt einer anderen Adresse. Dies geschieht, weil ein kurzgeschlossenes oder offenes Adressbit dazu führt, dass der Speicherbaustein eine andere als die vom Prozessor ausgewählte Adresse sieht.

Die folgende Abbildung zeigt die Grundprinzipien dieser Problematik:
Idealisierte Memory-Fehler durch Probleme mit elektrischen Kontakten

Idealisierte Memory-Fehler durch Probleme mit elektrischen Kontakten

Eine andere Möglichkeit ist, dass eine der Steuerleitungen kurzgeschlossen oder offen ist. Obwohl es theoretisch möglich ist, spezifische Tests für Steuerleitungsprobleme zu entwickeln, ist es nicht möglich, einen allgemeinen Test dafür zu beschreiben. Die Funktionsweise vieler Steuersignale ist entweder für den Prozessor oder die Speicherarchitektur spezifisch. Vermutlich funktioniert der Speicher bei einem Problem mit einer Steuerleitung wahrscheinlich überhaupt nicht. Eine Kombination aus Adress- und Daten-Tests wird damit wohl aufdecken, dass im Worst-Case, also einem Fehlschlagen aller Tests, vermutlich auch die Steuerleitung in den Verdacht kommt. Ein Gesamttest sollte folglich verschiedene Testfälle abdecken und eine finale Bewertung ermöglichen.

Hardware-spezifische Routinen

Wir nehmen an dieser Stelle an, dass das Schreiben und Lesen von Speicher und Adressen in irgendeiner Form Low-Level-Routinen folgt, die - im besten Falle - einfachen Pointer-Dereferenzierungen nachkommen. Allerdings, bei älteren Dual-Port-RAM-Bausteinen muss die Lese- und Schreibroutine über das Setzen von Pins am Speicherbaustein (Addresse, Output-Enable, Read/Write) über Funktionen gelöst werden. Wir nehmen also zunächst an, dass es folgende Funktionen gibt:

void setMemory(uint32_t * pAddress, uint32_t data);
uint32_t getMemory(uint32_t * pAddress);
Test des Adressbus

Für den Test des Adressbus werden in einfachster Form zunächst die Power-of-Two-Adressen getestet. Zu den jeweiligen Adressen wird ein Pattern eingeschrieben und ausgelesen. Es wird verifiziert, dass das Geschriebene in den Anfangswerten steht, geht man von einer Startadresse von 0 aus. Um sicherzustellen, dass sich keine zwei Speicherplätze überschneiden, sollte zunächst ein Anfangsdatenwert in jeden Offset der Power-of-Two-Adresse innerhalb des Geräts geschrieben werden. Dann wird ein neuer Wert in den ersten Test-Offset geschrieben und überprüft , ob der Anfangsdatenwert immer noch an jedem anderen Offset der Power-of-Two-Adresse gespeichert ist. Wenn man an anderer Stelle als der gerade beschreibenen einen neuen Datenwert findet, gibt es offensichtlich ein Problem mit dem aktuellen Adressbit. Der Test ist iterativ für die möglichen Power-of-Twos zu wiederholen. Sollte also ein Bit high oder low bleiben, wird entweder im ersten Testteil oder im zweiten Testteil eine fehlerhafte Speicheradresse entdeckt.

_Bool memtestAddressBus(uint32_t * pBaseAddress, uint32_t numBytes, uintptr_t * ppFailAddr)
{
  uint32_t addressMask = (numBytes - 1);
  uint32_t offset = 0;
  uint32_t testOffset = 0;    
  uint32_t pattern = (uint32_t) PATTERN_ADDRESS;
*ppFailAddr = (uintptr_t)NULL; uint32_t antipattern = (uint32_t) ~pattern; _Bool result = 1 /* Write the default pattern at each of the power-of-two offsets. */ for (offset = sizeof(uint32_t); (offset & addressMask) != 0; offset <<= 1) { setMemory(&(pBaseAddress[offset]), pattern); } /* Check for address bits stuck high. */ for (offset = sizeof(uint32_t); offset & addressMask; offset <<= 1) { if (getMemory(&(pBaseAddress[offset])) != pattern) { *ppFailAddr = (uintptr_t) &pBaseAddress[offset]; ppFailAddr++; result = 0; DEBUG_PRINT("Address bit stuck high test unsuccessful at address 0x%p\n",(void*) &pBaseAddress[offset]); } else { DEBUG_PRINT("Address bit stuck high test successful at address 0x%p\n",(void*) &pBaseAddress[offset]); } } /* Check for address bits stuck low or shorted. */ for (testOffset = sizeof(uint32_t); testOffset & addressMask; testOffset <<= 1) { setMemory(&(pBaseAddress[testOffset]), antipattern); for (offset = sizeof(uint32_t); offset & addressMask; offset <<= 1) { if ((getMemory(&(pBaseAddress[offset])) != pattern) && (offset != testOffset)) { *ppFailAddr = (uintptr_t) &(pBaseAddress[offset]); ppFailAddr++; result = 0; DEBUG_PRINT("Address bit stuck low test unsuccessful at address 0x%p\n",(void*) &pBaseAddress[offset]); } else { DEBUG_PRINT("Address bit stuck low test successful at address 0x%p\n",(void*) &pBaseAddress[offset]); } } pBaseAddress[testOffset] = pattern; } return result; } volatile uint32_t mem[1024]; int main (int argc, char const *argv[]) { uintptr_t failed_addr[256]; const uint32_t test = 0; memtestAddressBus((uint32_t *)mem, sizeof(mem)/sizeof(mem[0]),failed_addr); return 0; }
Test des Datenbus

Eine gute Möglichkeit, jedes Bit unabhängig zu testen, ist der so genannte "Walking 1's Test". Der Name dieses Tests rührt daher, dass ein einzelnes Datenbit auf 1 gesetzt wird und durch das gesamte Datenwort "wandert". Die Anzahl der zu prüfenden Datenwerte ist gleich der Breite des Datenbusses. Dadurch reduziert sich die Anzahl der Testmuster von 2n auf n, wobei n die Breite des Datenbusses ist.

Da wir an dieser Stelle nur den Datenbus testen, können alle Datenwerte an dieselbe Adresse geschrieben werden. Jede beliebige Adresse innerhalb des Speicherbausteins ist geeignet. Um den "Walking 1's"-Test durchzuführen wird somit lediglich ein Zweierpotenz-Wert auf die Adresse geschrieben, daraufhin wird die 1 geshiftet und der nächste Wert wird hineingeschrieben. Durch das sofortige Auslesen sorgt man dafür, dass der Wert im Speicher beständig bleibt. Wenn Sie das Ende der Tabelle erreichen, ist der Test abgeschlossen.


uint32_t memTestDataBus(volatile uint32_t * address)
{
  uint32_t pattern;

  /* Perform a walking 1's test at the given address. */
  for (pattern = 1; pattern != 0; pattern <<= 1)
  {
    /* Write the test pattern. */
    *address = pattern;

    /* Read it back */
    if (*address != pattern) 
    {
      DEBUG_PRINT("Failed testing databus for address 0x%p\n",(void*) address);
      return (*address);          
    }
    else
    {
      DEBUG_PRINT("Done testing databus for address 0x%p\n",(void*) address);
    }
  }
  return (0);

}   /* memTestDataBus() */


Speicherbereichstest

An dieser Stelle ist somit klar, dass die Adress- und Datenbusverkabelung funktioniert, nun muss die Integrität des Speichers an sich getest werden. Zum Beispiel kann geprüft werden, ob jedes Bit im Gerät sowohl 0 als auch 1 halten kann. Dieser Test grundsätzlich einfach zu implementieren, ist aber in Sachen Ausführungszeit deutlich länger als die beiden vorherigen.

Um einen vollständigen Gerätetest sicherzustellen, muss jede Speicherstelle zweimal akzessiert werden, durch schreiben und verifizieren. Für den ersten Durchlauf kann ein beliebiger Datenwert gewählt werden, solange dieser Wert beim zweiten Durchlauf vollständig geflippt wird. Und da die Möglichkeit besteht, dass durch Latching, Caching oder Delays ein Speicherbereich nicht rechtzeitig rückzulesen ist, ist ein Datensatz zu wählen, der sich mit der Adresse ändert. Ein einfaches Beispiel ist ein Inkrement-Test.


uint32_t * memTestRegion(volatile uint32_t * baseAddress, unsigned long nBytes)
{
    unsigned long offset;
    unsigned long nWords = nBytes / sizeof(uint32_t);

    uint32_t pattern;
    uint32_t antipattern;


    /*
     * Fill memory with a known pattern.
     */
    for (pattern = 1, offset = 0; offset < nWords; pattern++, offset++)
    {
        baseAddress[offset] = pattern;
    }

    /*
     * Check each location and invert it for the second pass.
     */
    for (pattern = 1, offset = 0; offset < nWords; pattern++, offset++)
    {
        if (baseAddress[offset] != pattern)
        {
            return ((uint32_t *) &baseAddress[offset]);
        }

        antipattern = ~pattern;
        baseAddress[offset] = antipattern;
    }

    /*
     * Check each location for the inverted pattern and zero it.
     */
    for (pattern = 1, offset = 0; offset < nWords; pattern++, offset++)
    {
        antipattern = ~pattern;
        if (baseAddress[offset] != antipattern)
        {
            return ((uint32_t *) &baseAddress[offset]);
        }
    }

    return (NULL);

}   /* memTestDevice() */


Zusammenfassung und Ausblick

Die vorgestellten Struktur sind vergleichbar einfach und sollten nur in Ergänzung zu einem automatischen Fehlererkennungsmechanismus gesehen werden, der das Programm zyklisch überwacht. Deshalb eignen sie sich vor allem im Rahmen der Aufstartphase. Da vollständige RAM-Test sehr lange dauert, kann man ihn mitunter auch im Hintergrund ausführen lassen. So sollte der Fokus zunächst auf den Bereichen sein, die in der Programminitialisierung vom Stack gefüllt werden. Die Endadresse lässt sich über den Linker automatisch herausfinden. Der Bereich, der im Folgenden nicht verwendet wird, kann im Hintergrund getestet werden. Nur im Ausnahmefall eines Fehlers sollte fortan reagiert werden.

RAM-Tests, egal in welcher Form und egal für welchen Baustein sind essenzielle Elemente in der Sicherstellung der Gesamtintegrität eines Embedded Systems und unabdingbar für Anwendungen im Bereich der funktionalen Sicherheit. Mit den vorliegenden Snippets lässt sich ein hardware-unabhängiges Grundgerüst schaffen, welches im Folgenden für einen konkreten Controller angepasst werden kann.

Sie möchten mehr darüber wissen?

Fragen kostet nichts. Wie und warum wir die Dinge tun, beantworten wir Ihnen gerne in einem persönlichen Gespräch. Lassen Sie uns dazu gerne kurz persönlich sprechen.