Wie Micropython den Arbeitsspeicher nutzt
Speicheraufteilung des RAM in MicroPython
[Bearbeiten | Quelltext bearbeiten]Bei MicroPython auf einem ESP32 muss man zwischen dem physisch vorhandenen RAM des Mikrocontrollers und dem Speicher unterscheiden, den MicroPython tatsächlich für seine Laufzeitumgebung verwendet.
Die genaue Aufteilung ist abhängig von ESP32-Variante, Firmware, MicroPython-Version, aktivierten Funktionen und ggf. vorhandenem PSRAM. Die folgenden Angaben beziehen sich daher auf das grundlegende Speichermodell und nicht auf eine bestimmte Firmware.
1. Physischer RAM des ESP32
[Bearbeiten | Quelltext bearbeiten]Der ESP32 besitzt mehrere unterschiedliche RAM-Bereiche. Ein Teil davon ist für spezielle Hardwarefunktionen reserviert oder wird von der ESP-IDF bzw. MicroPython verwendet.
Beim klassischen ESP32 (ESP32-D0WD) stehen intern ungefähr 520 KiB SRAM zur Verfügung. Diese sind jedoch nicht einfach als ein zusammenhängender Speicherbereich nutzbar.
| Bereich | Typische Größe | Verwendung |
|---|---|---|
| DRAM | ca. 320 KiB | Daten, Variablen, MicroPython-Heap |
| IRAM | ca. 128 KiB | Programmcode, zeitkritischer Code |
| RTC RAM | 8 KiB | Daten, die bestimmte Sleep-Modi überleben können |
Die tatsächlich für MicroPython verfügbaren Größen sind kleiner, da bereits Teile des RAM von Bootloader, ESP-IDF, Hardwaretreibern und MicroPython selbst benötigt werden.
2. MicroPython-Heap
[Bearbeiten | Quelltext bearbeiten]Der wichtigste Speicherbereich für Python-Programme ist der MicroPython-GC-Heap.
MicroPython verwendet einen eigenen Garbage Collector (GC). Python-Objekte wie
- Integer und Float-Objekte
- Strings
- Listen
- Dictionaries
- Tupel
- Klassen und Instanzen
- Bytearrays
- Python-Codeobjekte
- LVGL-Objekte und deren Python-Wrapper
werden grundsätzlich aus diesem Heap angefordert.
Der aktuelle Zustand kann mit
import gc
print(gc.mem_alloc())
print(gc.mem_free())ermittelt werden.
`gc.mem_alloc()` liefert den aktuell belegten Speicher des MicroPython-Heaps.
`gc.mem_free()` liefert den aktuell freien Speicher innerhalb dieses Heaps.
Die Summe aus beiden Werten entspricht ungefähr der aktuellen Größe des GC-Heaps:
GC-Heap = mem_alloc() + mem_free()Dabei handelt es sich nicht um den gesamten freien RAM des ESP32. Es wird ausschließlich der Speicher betrachtet, den der MicroPython-Garbage-Collector verwaltet.
3. Aufbau des MicroPython-GC-Heaps
[Bearbeiten | Quelltext bearbeiten]Der GC-Heap ist wiederum in kleine Blöcke unterteilt. MicroPython verwaltet diese Blöcke mit einer Bitmap.
Auf einem 32-Bit-System entspricht ein GC-Block typischerweise vier Maschinenwörtern, also:
4 × 4 Byte = 16 ByteObjekte werden aus einer oder mehreren solchen Blöcken aufgebaut.
Das bedeutet auch, dass der Heap nicht beliebig fein aufgeteilt werden kann. Kleine Objekte benötigen mindestens einen Heap-Block.
Mit
import micropython
micropython.mem_info()kann man wesentlich detailliertere Informationen über den Heap ausgeben.
Mit
micropython.mem_info(1)kann MicroPython zusätzlich eine Darstellung der Heap-Belegung ausgeben.
Diese Informationen sind besonders nützlich, wenn ein Programm trotz scheinbar ausreichendem freien Speicher keine größeren Objekte mehr anlegen kann.
4. Fragmentierung des Heaps
[Bearbeiten | Quelltext bearbeiten]Ein wichtiger Unterschied besteht zwischen freiem Speicher und zusammenhängendem freiem Speicher.
Beispielsweise kann ein Heap so aussehen:
[####][....][##][....][######][..][###][........]Dabei sind `.` freie Bereiche und `#` belegte Bereiche.
Es kann insgesamt viel freier Speicher vorhanden sein, trotzdem kann eine große Speicheranforderung fehlschlagen, wenn kein ausreichend großer zusammenhängender Bereich verfügbar ist.
Der Garbage Collector kann nicht immer beliebig fragmentierte Speicherbereiche zusammenführen.
Deshalb ist bei größeren Objekten nicht nur
gc.mem_free()interessant, sondern auch die tatsächliche Heap-Struktur.
5. Python-Code und Flash
[Bearbeiten | Quelltext bearbeiten]Python-Quelltext liegt zunächst als Datei im Flash-Dateisystem.
Beim Import wird der Code jedoch verarbeitet und in eine für MicroPython geeignete interne Darstellung gebracht.
Je nach Situation entstehen dabei zusätzliche Objekte im RAM.
Deshalb kann beispielsweise
import lvgldeutlich mehr RAM benötigen als die Größe der eigentlichen Datei vermuten lässt.
Bei großen Modulen ist deshalb die Dateigröße im Flash kein zuverlässiger Hinweis auf den später benötigten RAM.
6. Bytecode und Frozen Modules
[Bearbeiten | Quelltext bearbeiten]MicroPython kann Module als Frozen Modules in die Firmware integrieren.
Der Python-Code wird dabei bereits beim Erstellen der Firmware in die Firmware aufgenommen.
Dadurch muss der entsprechende Quelltext nicht aus dem Dateisystem geladen werden.
Frozen Modules haben deshalb insbesondere bei kleinen Mikrocontrollern einen wichtigen Vorteil:
| Normaler Python-Code | Frozen Module |
|---|---|
| Datei im Flash-Dateisystem | Bestandteil der Firmware |
| Import aus dem Dateisystem | Import aus der Firmware |
| zusätzlicher Dateisystemplatz erforderlich | kein Dateisystemplatz erforderlich |
| Code kann teilweise RAM benötigen | Code kann weitgehend direkt aus dem Flash verwendet werden |
Allerdings benötigen auch Frozen Modules RAM für die tatsächlich erzeugten Python-Objekte und Laufzeitdaten.
7. Stack
[Bearbeiten | Quelltext bearbeiten]Neben dem MicroPython-Heap existieren Stacks für die CPU bzw. die einzelnen ESP32-Tasks.
Der Stack wird unter anderem für
- Funktionsaufrufe
- lokale Variablen auf C-Ebene
- Interrupts
- Treiber
- FreeRTOS-Tasks
verwendet.
Der Stack gehört nicht zum MicroPython-GC-Heap.
Deshalb bedeutet beispielsweise
gc.mem_free() = 30 KiBnicht, dass insgesamt noch 30 KiB RAM frei sind.
Umgekehrt kann ein System bereits an eine RAM-Grenze stoßen, obwohl `gc.mem_free()` noch einen scheinbar komfortablen Wert anzeigt.
8. C-Heap
[Bearbeiten | Quelltext bearbeiten]MicroPython selbst besteht größtenteils aus C-Code.
Auch die ESP-IDF und die Hardwaretreiber verwenden Speicher außerhalb des MicroPython-GC-Heaps.
Dieser Speicher wird über die C-/ESP-IDF-Speicherverwaltung angefordert.
Dazu gehören beispielsweise:
- Netzwerk-Stack
- WLAN
- TCP/IP
- Bluetooth
- SPI-/I2C-Treiber
- Displaytreiber
- DMA-Puffer
- LVGL
- ESP-IDF-Komponenten
- interne MicroPython-Datenstrukturen
Dieser Bereich ist für `gc.mem_free()` unsichtbar.
Das erklärt, warum der tatsächlich noch verfügbare RAM kleiner sein kann als der durch `gc.mem_free()` angezeigte Wert.
9. DMA-fähiger Speicher
[Bearbeiten | Quelltext bearbeiten]Bestimmte Hardwarekomponenten des ESP32 können nicht auf jeden RAM-Bereich zugreifen.
Beispielsweise benötigen DMA-Controller Speicher mit bestimmten Eigenschaften.
Daher können Treiber eigene DMA-fähige Speicherbereiche reservieren.
Das ist insbesondere bei
- SPI
- I2S
- Displaycontrollern
- SD-Karten
- Kameras
- Audio
relevant.
Ein großer freier MicroPython-Heap bedeutet deshalb nicht automatisch, dass ein ausreichend großer DMA-Puffer angelegt werden kann.
10. PSRAM
[Bearbeiten | Quelltext bearbeiten]Viele ESP32-Boards besitzen zusätzlich externes PSRAM.
Beim klassischen ESP32 sind beispielsweise 2 MiB oder 4 MiB PSRAM verbreitet.
PSRAM erweitert den verfügbaren Speicher erheblich, ist aber technisch nicht identisch mit dem internen SRAM.
Je nach MicroPython-Firmware kann PSRAM
- vollständig oder teilweise in den MicroPython-Heap integriert sein,
- für bestimmte C-Datenstrukturen verwendet werden,
- von einzelnen Treibern bevorzugt genutzt werden,
- oder überhaupt nicht für den normalen Python-Heap zur Verfügung stehen.
Deshalb muss bei einer konkreten Firmware geprüft werden, wie PSRAM tatsächlich eingebunden wurde.
11. RTC RAM
[Bearbeiten | Quelltext bearbeiten]Der ESP32 besitzt zusätzlich einen kleinen RTC-Speicher.
Dieser Speicher ist für Funktionen rund um Sleep-Modi vorgesehen.
Ein Teil der Daten kann je nach Sleep-Modus erhalten bleiben.
Der RTC-Speicher gehört nicht zum normalen MicroPython-GC-Heap.
12. Übersicht
[Bearbeiten | Quelltext bearbeiten]Die folgende Darstellung zeigt das grundsätzliche Modell:
ESP32 RAM
│
├── Internes SRAM
│ │
│ ├── DRAM
│ │ │
│ │ ├── MicroPython GC-Heap
│ │ │ ├── Python-Objekte
│ │ │ ├── Strings
│ │ │ ├── Listen
│ │ │ ├── Dictionaries
│ │ │ └── sonstige Python-Daten
│ │ │
│ │ ├── C-Heap
│ │ │ ├── ESP-IDF
│ │ │ ├── Treiber
│ │ │ ├── Netzwerk
│ │ │ └── LVGL
│ │ │
│ │ └── DMA-Puffer
│ │
│ ├── IRAM
│ │ └── Programmcode / zeitkritischer Code
│ │
│ └── RTC RAM
│ └── Sleep-/RTC-Daten
│
└── Externes PSRAM (falls vorhanden)
│
├── C-Heap
├── Treiber
└── ggf. MicroPython GC-Heap13. Was zeigt `gc.mem_free()` tatsächlich?
[Bearbeiten | Quelltext bearbeiten]Für die Praxis ist die wichtigste Erkenntnis:
gc.mem_free()
│
▼
freier Speicher im MicroPython-GC-Heapund nicht:
gesamter noch verfügbarer RAM des ESP32Für eine vollständige Speicheranalyse müssen daher mindestens mehrere Bereiche betrachtet werden:
| Messwert | Bedeutung |
|---|---|
| `gc.mem_alloc()` | Von Python-Objekten belegter GC-Heap |
| `gc.mem_free()` | Freier GC-Heap |
| `micropython.mem_info()` | Detaillierte Struktur des GC-Heaps |
| ESP-IDF Heap-Informationen | Freier/benutzter C-Speicher |
| PSRAM-Informationen | Zustand des externen RAM |
| `os.statvfs("/")` | Speicherplatz des Flash-Dateisystems |
| `esp32.flash_size()` | Physische Größe des Flash |
| `esp32.Partition.info()` | Aufteilung des Flash in Partitionen |
14. Bedeutung für das CYD mit LVGL
[Bearbeiten | Quelltext bearbeiten]Für unser CYD ist insbesondere die Unterscheidung zwischen Flash und RAM wichtig.
Der Flash enthält unter anderem:
- MicroPython-Firmware
- Partitionstabelle
- Bootloader
- Python-Dateien
- LVGL-Firmwarebestandteile
- Dateisystem
Der RAM enthält dagegen während des Betriebs unter anderem:
- MicroPython-GC-Heap
- Python-Objekte
- LVGL-Objekte
- Display-Puffer
- Touch-Daten
- SPI-/DMA-Puffer
- Netzwerkdaten
- C-Datenstrukturen
Gerade LVGL kann relativ viel RAM benötigen. Ein Display-Puffer von beispielsweise 240 × 320 Pixeln benötigt bei 16 Bit Farbtiefe bereits:
240 × 320 × 2 Byte
= 153.600 Byte
≈ 150 KiBZwei solche Buffer würden bereits etwa 300 KiB benötigen.
Damit wird verständlich, warum bei einer LVGL-MicroPython-Firmware der verfügbare RAM wesentlich schneller knapp werden kann als der Flash.
15. Unser `memtools.py`
[Bearbeiten | Quelltext bearbeiten]Für `memtools.py` bedeutet das, dass `mem_view()` künftig sinnvollerweise mehrere voneinander unabhängige Speicherinformationen anzeigen sollte:
mem_view()
│
├── Flash
│ ├── Gesamtgröße
│ └── Partitionen
│
├── Filesystem
│ ├── Gesamt
│ ├── Belegt
│ └── Frei
│
├── MicroPython GC
│ ├── Gesamt
│ ├── Belegt
│ └── Frei
│
├── C-Heap
│ ├── Gesamt
│ ├── Belegt
│ └── Frei
│
└── PSRAM
├── Gesamt
├── Belegt
└── FreiDamit würde `memtools.py` nicht nur den MicroPython-Heap, sondern tatsächlich die wesentlichen Speicherressourcen des ESP32 überwachen.