Cache auf einem Mikrocontroller – Was es zu beachten gibt

Wir haben vor kurzem für einen Kunden eine Firmwareerweiterung programmiert, welche der Anwendung ermöglicht Daten auf dem Mikrocontrollerflash zu speichern und zu lesen. Die Firmware läuft auf einem PIC32MZ, welcher mit einem Daten- und einem Instruktionscache ausgestattet ist. Wer schon mal versucht hat Software für einen PC mit Multi-Core CPU zu parallelisieren weiß, dass die Caches hierbei eine wichtige Rolle spielen. Greifen – z.B. in einer Schleife – alle Kerne gleichzeitig auf denselben Cache zu, wird der Code hierdurch langsamer anstatt schneller. Paradoxerweise je langsamer, je mehr Kerne man einsetzt. Der PIC32MZ hat allerdings nur eine Single-Core CPU. Man sollte also erwarten, dass diese Probleme hier nicht auftreten.

Tatsächlich kann der Einsatz eines Cache die Ausführungszeit bei Mikrocontrollern deutlich beschleunigen, da Zugriffszeiten auf den RAM und vor allem auf den Flash deutlich höher sind als die Zugriffszeiten auf den Cache. Der Instruktionscache des PIC32MZ ist 16kB groß. Theoretisch könnte bei kleineren Programmen der gesamte Programmflash gecached werden. Da sich die CPU so Instruktionen fast ausschließlich aus dem Cache holt, kann ein Programm   dementsprechend schneller ablaufen. Ein Ausführen des Codes aus dem RAM ist demnach kaum nötig.

Trotz aller Vorteile gibt es jedoch bei Einsatz eines Caches einiges zu beachten. Vor allem wenn Daten am Cache vorbei geschrieben und gelesen werden. Dies passiert immer dann, wenn nicht die CPU den Vorgang ausführt, sondern eine Hardwareinstanz. Hierbei kann es sich zum Beispiel um den DMA- oder den Flashcontroller handeln. Stellen wir uns als Beispiel folgendes Szenario vor: Eine Anwendung, welche auf einem PIC32MZ läuft, soll regelmäßig Daten auf dem Flash abspeichern. Um die Lebenszeit des Flashs zu verlängern, werden alte Daten nicht bei jedem Schreibvorgang gelöscht, sondern stattdessen solange auf einen freien Speicherbereich geschrieben, bis der gesamte Speicher voll ist. Die Anwendung muss also bevor neue Daten geschrieben werden können, zuerst den Speicher auslesen um einen freie Stelle zu finden. Die Daten beinhalten eine CRC (cyclic redundancy check) welche über den kompletten Datensatz (mit Ausnahme der CRC selber) gerechnet wurde. Berechnet nun die Anwendung die CRC über den gerade geschriebenen Speicherbereich und vergleicht diese mit der geschriebenen CRC auf dem Flash, wird die Anwendung feststellen, dass die CRCs nicht identisch sind.

Bei der Berechnung der CRC ergibt sich in diesem Beispiel folgendes Problem: Bei der Suche nach einem freien Speicherplatz wurde der Mikrocontrollerflash zunächst gelesen. Die Daten wurden dafür zunächst in den Cache übertragen. Da die Adressen, an denen die Daten gespeichert wurden, nun zumindest teilweise im Cache vorhanden sind, wird die CPU versuchen bei der Berechnung der CRC möglichst viele Daten aus dem Cache zu lesen. Die Daten im Cache sind allerdings zu diesem Zeitpunkt nicht mehr aktuell, da der Flashcontroller bereits die entsprechenden Adressen beschrieben hat. Aus diesem Grund kann die Anwendung die korrekte CRC nicht berechnen.

Ihr Ansprechpartner:

M.Sc. Björn Schmitz, Software Entwickler
E-Mail: schmitz@medtech-ingenieur.de
Tel.:  +49 9131 691 240
 

Benötigen Sie Unterstützung bei der Entwicklung Ihres Medizingeräts? Wir helfen gerne! Die MEDtech Ingenieur GmbH bietet Hardware-Entwicklung, Software-Entwicklung, Systems Engineering, Mechanik-Entwicklung und Beratung aus einer Hand. Nehmen Sie Kontakt mit uns auf.

Kontakt aufnehmen


Die Abbildung unten zeigt (vereinfacht) die Implementierung der Caches beim PIC32MZ. Die Abbildung wurde dem Microchip Application Note AN1600 entnommen. DMA- und Flashcontroller wurden nachträglich farbig markiert. Wie in der Abbildung zu erkennen ist, schreiben diese völlig unabhängig vom Cache Daten auf den Flash. So kann es passieren, dass Daten im RAM oder im Flash aktueller sind als Daten im Cache. Außerdem könnte es prinzipiell passieren, das Daten welche mit DMA auf den RAM geschrieben wurden, anschließend bei einem Cache flush überschrieben werden.

Cache Implementierung beim PIC32MZ. Quelle: Microchip Applicaiton Note AN1600 – Using L1 Cache on PIC32MZ Devices

 

Um dies zu verhindern, hat der Programmierer mehrere Möglichkeiten. Eine Möglichkeit wäre es ein Softwarereset nach jedem Schreibvorgang durchzuführen. Nach einem Neustart ist der Cache leer und muss daher neu geladen werden. Diese Möglichkeit ist in der Regel natürlich nicht praktikabel.

Sinnvoller wäre es nach jedem Schreibvorgang auf dem Flash eine Cache invalidation durchzuführen, die geschriebenen Adressen also als „ungültig“ zu erklären. So müssen die entsprechenden Adressen beim nächsten Lesevorgang neu gecached werden. Vor jedem DMA Transfer sollte ein Cache flush durchgeführt werden. Hierbei wird der Cache geleert und die gegenwärtig gecachten Daten werden auf den RAM zurückgeschrieben. So kann sichergestellt werden, dass wirklich nur aktuelle Daten durch den DMA übertragen werden. Mit Cache flush und Cache invalidation lassen sich schon die meisten Probleme lösen. Wichtig ist allerdings, dass beides vor dem Schreibvorgang passiert. Würde die Anwendung auf das Ende eines Flashzugriffs warten und anschließend eine Cache invalidation ausführen, könnten bei einem Kontextwechsel veraltete Daten aus dem Cache gelesen werden. So ein Kontextwechsel könnte zum Beispiel durch einen Interrupt erfolgen.

Eine Möglichkeit grundsätzlich zu verhindern, und gleichzeitig Cache invalidation und Cache flush – welche ggf. überflüssig seien können – zu vermeiden, ist es Daten direkt in einem Bereich abzulegen, welche nicht gecached werden. Der PIC32MZ bietet hierfür zwei virtuell getrennte Adressräume an, von denen einer gecached und einer nicht gecached wird. Dies ist im Bild weiter unten dargestellt, welches dem PIC32MZ Datenblatt entnommen wurde. Wie zu erkennen ist, teilen sich die Adressbereiche KSEG0 und KSEG1 den gleichen physikalischen Adressraum und decken jeweils den kompletten Flash und den kompletten RAM ab. Während Daten in KSEG0 jedoch gecached werden, werden Daten in KSEG1 nicht gecached. Es ist also möglich, durch geeignete Adressierung, ein Cachen von Daten zu verhindern. Um den Code lesbarer zu gestalten, ist es möglich Daten direkt im KSEG1 Bereich anzulegen (standardmäßig werden Daten auf dem RAM in KSEG0 angelegt). Schreibt man:

uint32_t _attribute__((coherent)) dataBuffer[100];

wird das Array “dataBuffer“ in KSEG1 angelegt. DMA Transfers aus oder in das Array können demnach gefahrlos durchgeführt werden. Werden Daten vom Flash gelesen kann deren physikalische Adresse einfach in eine KSEG1 Adresse konvertiert werden, um zu verhindern, dass die Daten aus dem Cache gelesen werden.

Speicherorganisation des PIC32MZ. Quelle: Datenblatt des PIC32 MZ, PIC32MZ Embedded Connectivity with Floating Point Unit (EF) Family (DS60001320D)

 

 

Fazit

Caches auf Mikrocontrollern stellen heute keine Seltenheit mehr da. Neben Mikrocontrollern der PIC32MZ Serie verwenden diese auch Mikrocontroller, welche auf dem ARM Cortex-M7 basieren. Aus diesem Grund sollten sich heutzutage auch (oder vielleicht sogar vor allem) Embedded Entwickler mit der Funktionsweise von Caches auseinandersetzen. Trotz möglicher Fehlerquellen bieten Caches auf einem Mikrocontroller viele Vorteile, da die Zugriffszeiten auf Daten und Programmspeicher deutlich reduziert werden kann.

Die hier erklärten Methoden beziehen sich zwar alle auf den PIC32MZ, sind aber zum größten Teil auch auf andere Mikrocontroller mit Cache zu übertragen. In der Regel stellen die Hersteller der Mikrocontroller Informationen darüber zur Verfügung, was bei Verwendung des Caches zu beachten ist. Analog zum oben bereits erwähnten Application Note von Microchip, bietet zum Beispiel ST ein ähnliches Dokument an (AN4839, Level 1 cache on STM32F7 Series).

Natürlich gibt es zum Thema Cache viel mehr Theorie als in diesen Blogeintrag passen würde. Aus diesem Grund sind einige Punkte sehr vereinfacht dargestellt. Neben Kommentaren freue ich mich daher auch über Ergänzungen zu meinem Text.

Kontaktieren Sie uns!

Autor

  • Björn Schmitz

    Seit Juli 2017 gehöre ich zum MEDtech-Ingenieur Team und bin hier vor allem als Firmwareentwickler tätig. Schon in kürzester Zeit konnte ich an vielen spannenden Projekten aus dem Bereich Medizintechnik, aber auch aus anderen Bereichen mitwirken.

Auch interessant:

Jahresrückblick und Neuigkeiten

Wieder einmal neigt sich das Jahr dem Ende entgegen und wieder einmal war es für uns ein sehr spannendes und erfolgreiches Jahr. Ich freue mich Ihnen mitteilen zu können, dass wir zum 01.01.2019 offiziell umfirmieren zur MEDtech Ingenieur GmbH. Diesen Schritt haben wir dieses Jahr konsequent umgesetzt und stellen damit…
Getagged mit: , , ,