4.19. Pool di memoria

Una camera che mantiene tre frame a piena risoluzione in un pool di framebuffer, gestisce contemporaneamente un buffer di anteprima separato e ha ancora spazio per uno script Python e i suoi oggetti sta gestendo più memoria di quanta un singolo blocco di RAM sull’MCU potrebbe fornire. MicroPython riesce a far stare tutto distribuendolo tra i diversi tipi distinti di memoria che l’MCU mette a disposizione, e indirizzando ogni tipo di allocazione verso il tipo di memoria di cui ha effettivamente bisogno.

4.19.1. Tipi di memoria

Un MCU moderno di OpenMV Cam espone quattro tipi distinti di memoria. Il primo è invisibile all’applicazione; gli altri tre sono pool da cui possono provenire le allocazioni.

  • La cache dati della CPU – una regione di memoria piccola e molto veloce che si trova tra la CPU e il resto della RAM. Quando la CPU legge o scrive un valore dalla memoria principale, la cache ne mantiene automaticamente una copia, così gli accessi ripetuti agli stessi dati rimangono nella cache e non pagano mai il costo di andare verso la memoria più lenta. La cache non è un pool da cui provengono le allocazioni. È trasparente all’applicazione – fa semplicemente sì che il resto della RAM risulti in pratica più veloce di quanto suggerirebbe la sua latenza grezza, fino al punto in cui un working set smette di starci dentro.

  • Memoria del processore strettamente accoppiata – un piccolo blocco di RAM cablato direttamente alla CPU senza alcun bus in mezzo. Accesso a ciclo singolo, mai un miss, mai un’attesa. Le allocazioni che hanno realmente bisogno della memoria più veloce possibile – dove ogni ciclo di latenza conta – provengono da questo pool.

  • Memoria veloce on-chip – da qualche centinaio di kilobyte fino a circa un megabyte di RAM, integrata nel package dell’MCU. Bassa latenza, alta banda, ma limitata in dimensione. Qui risiede l’heap di MicroPython così che gli accessi agli oggetti Python rimangano rapidi; i buffer di lavoro più piccoli che la CPU tocca molto condividono il pool.

  • Memoria di massa più lenta – sulle schede che abbinano l’MCU a un die di memoria esterno, decine di megabyte di RAM off-chip raggiunti tramite il bus esterno. Molto più grande, ma ogni accesso richiede più tempo rispetto alla memoria on-chip; la cache dati nasconde gran parte di questo costo per i working set che riesce a contenere, e il divario emerge nelle operazioni che attraversano dati troppo grandi per essere messi in cache. Usata per allocazioni che devono essere grandi e che la CPU può tollerare a velocità inferiore – la più importante delle quali è il pool di framebuffer.

Le schede della famiglia si collocano lungo uno spettro: alcune hanno solo RAM on-chip; altre abbinano la RAM on-chip a un blocco esterno molto più grande. Ognuno dei tre tipi allocabili è trattato come un pool di memoria – un blocco da cui provengono le allocazioni – ed etichettato in modo che ogni richiesta possa chiedere il tipo di memoria di cui ha effettivamente bisogno.

4.19.2. Il framebuffer primario

Il framebuffer che supporta snapshot() non chiede memoria veloce. Chiede memoria sufficiente – nulla di più. Questo lo colloca in qualsiasi pool sia più grande, così su una scheda dotata sia di memoria on-chip che esterna il framebuffer finisce nel blocco esterno.

Un framebuffer a piena risoluzione, con triplo buffer, è di gran lunga troppo grande per stare nel pool veloce on-chip sulla maggior parte dei componenti; il pool più grande è l’unico in grado di contenerlo. La cache dati della CPU nasconde gran parte del costo per accesso quando l’applicazione elabora l’immagine, e il motore DMA che riempie il framebuffer dal sensore tiene comunque il passo con la velocità dei dati del sensore.

La dimensione esatta che il framebuffer occupa viene scelta in base agli attuali pixformat(), framesize() e al conteggio framebuffers(); cresce o si riduce ogni volta che uno di questi cambia.

4.19.3. Framebuffer dei sensori secondari

Una seconda istanza di CSI ottiene il proprio framebuffer, allocato dallo stesso pool usato dal primario. Il pool è condiviso; i buffer sono indipendenti. L’ingombro del secondario è normalmente molto più piccolo di quello del primario, perché i sensori secondari funzionano a risoluzioni inferiori, quindi la memoria extra occupata dal secondo framebuffer è una piccola frazione di quella del primario.

4.19.4. Il framebuffer di streaming

Il buffer di anteprima dell’immagine è l’eccezione. Non viene allocato da nessuno dei pool a runtime; è una regione fissa riservata in fase di build, con un indirizzo noto e una dimensione nota. Questo tiene il percorso di anteprima fuori dai piedi di ogni altra allocazione – la regione esiste fin dall’avvio e non si sposta mai.

4.19.5. L’heap di MicroPython

Gli oggetti Python – variabili, liste, dizionari, istanze di classe, il wrapper Image restituito da una chiamata a snapshot(), ogni stringa e tupla create dall’applicazione – risiedono nell”heap con garbage collection di MicroPython, che è separato dai pool di memoria della camera. L’heap con garbage collection (GC) è una regione di memoria che MicroPython gestisce autonomamente: il codice Python vi alloca implicitamente ogni volta che viene creato un oggetto, e MicroPython scansiona periodicamente l’heap e recupera lo spazio occupato dagli oggetti che l’applicazione non referenzia più, così che l’applicazione non debba mai liberare nulla a mano.

All’avvio viene riservata una regione dedicata per l’heap GC, tipicamente collocata nella memoria veloce on-chip così che l’accesso da Python rimanga rapido, con un’eventuale espansione opzionale nel blocco esterno più grande sulle schede che necessitano di più margine per strutture dati di grandi dimensioni.

L’oggetto Image restituito da snapshot() è un piccolo oggetto wrapper sull’heap GC; i dati dei pixel sottostanti risiedono nel framebuffer in uno dei pool della camera. I due non competono mai per la stessa memoria.

4.19.6. Mettendo tutto insieme

Indirizzare ogni tipo di allocazione verso il pool giusto – i buffer grandi verso il pool più grande dove ci stanno, i dati sensibili alla latenza verso i pool più veloci, l’heap Python verso la propria regione, l’anteprima verso il suo slot riservato – è ciò che rende possibile eseguire una pipeline di acquisizione a piena risoluzione, un canale di anteprima e uno script Python non banale fianco a fianco su componenti che dispongono complessivamente solo di pochi megabyte di memoria veloce.