You need to do a memory free check before importing any of the display modules and then once the display is fully initialized. Make sure you do a garbage collection prior to checking the amount of free memory.
From your example without frame buffers just the driver is using 9,024 bytes of memory. That is actually a lot of memory use for a device that only has 320K of memory that is able to be used as heap memory. That’s almost 3% of the total of available memory. IDK what percentage it is of the available memory after MicroPython is loaded but considering MicroPython also has to reside in that same 320K that percentage is only going to go up. For some reason I want to say it’s around 100K. I do know that it’s initial amount allocated is 64K so the best case scenario is you would have 256K available for user use.
A single 128x128 PNG is going to take up 7,415 bytes of ram. once loaded into LVGL it is going to take up
65,536 bytes of memory. Every single byte that can be spared should be spared.
So here is the memory use for the structures involved in setting up an SPI bus along with the size of the structure.
esp_lcd_panel_io_t = esp_lcd_panel_io_handle_t = 20 bytes
esp_lcd_panel_t = 36 bytes
esp_lcd_panel_io_spi_config_t = 56 bytes
spi_bus_config_t = 48 bytes
160 bytes
That’s a really far cry from 9k. I don’t believe that the esp_lcd component is going to use up 9k of that either. I know there is a lot that will get chewed up because of MicroPython. Need to see what differences can be made to trim that amount down…
Getting rid of the large number of integer constants used in the setting of the buffer sizes is one place to cut some bytes off.
{MP_ROM_QSTR(MP_QSTR_EXEC), MP_ROM_INT(MALLOC_CAP_EXEC)},
{MP_ROM_QSTR(MP_QSTR_32BIT), MP_ROM_INT(MALLOC_CAP_32BIT)},
{MP_ROM_QSTR(MP_QSTR_8BIT), MP_ROM_INT(MALLOC_CAP_8BIT)},
{MP_ROM_QSTR(MP_QSTR_DMA), MP_ROM_INT(MALLOC_CAP_DMA)},
{MP_ROM_QSTR(MP_QSTR_SPIRAM), MP_ROM_INT(MALLOC_CAP_SPIRAM)},
{MP_ROM_QSTR(MP_QSTR_INTERNAL), MP_ROM_INT(MALLOC_CAP_INTERNAL)},
{MP_ROM_QSTR(MP_QSTR_DEFAULT), MP_ROM_INT(MALLOC_CAP_DEFAULT)},
{MP_ROM_QSTR(MP_QSTR_IRAM_8BIT), MP_ROM_INT(MALLOC_CAP_IRAM_8BIT)},
{MP_ROM_QSTR(MP_QSTR_RETENTION), MP_ROM_INT(MALLOC_CAP_RETENTION)},
{MP_ROM_QSTR(MP_QSTR_RTCRAM), MP_ROM_INT(MALLOC_CAP_RTCRAM)},
A single integer in MicroPython takes up 32 bytes of memory. That right there is using up 320 bytes by itself. I am not 100% on this but I believe that the memory is not doing to get used unless the CAPS attribute is accessed, But the question becomes is that allocation to the modules global namespace or is it to the locals namespace where it might get freed if the call was made from the inside of a function or method. I do not have an answer to that question and you would need to run some tests to find out. But that is a hell of a lot of memory use I can say that.
I ran a test to see the sizes of the different types that can be used for frame buffer objects.
MicroPython v1.21.0-dirty on 2023-11-26; linux [GCC 11.4.0] version
Use Ctrl-D to exit, Ctrl-E for paste mode
>>> import gc
>>> import array
>>> gc.collect()
20
>>> gc.mem_free()
2069568
>>> test = array.array('B', [0] * 1024)
>>> gc.collect()
29
>>> gc.mem_free()
2068480
>>>
an array.array takes up 1,088 total bytes for a buffer that is 1024 bytes in size
so there is 64 bytes of overhead just for the array itself.
and here is for a bytearray
MicroPython v1.21.0-dirty on 2023-11-26; linux [GCC 11.4.0] version
Use Ctrl-D to exit, Ctrl-E for paste mode
>>> import gc
>>> gc.collect()
12
>>> gc.mem_free()
2069600
>>> test = bytearray([0] * 1024)
>>> gc.collect()
29
>>> gc.mem_free()
2068480
>>>
1,120 bytes used for a bytearray that is 1024 bytes. That’s 96 bytes of overhead. So 32 additional bytes are being used compared to a an array.
I know it’s not a lot but there is an amount to it that is being used. Whenever possible you want to reuse buffers or keep variables that don’t get used all that often inside a function where it is apart of the global namespace and can be garbage collected.
If you look at the current drivers that are available for a display you will see that the initialization commands are all stored in a classes namespace. That means that once the driver has been initialized those parameters just sit there never being used again taking up memory.
class ili9341(ili9XXX):
def __init__(self,
miso=5, mosi=18, clk=19, cs=13, dc=12, rst=4, power=14, backlight=15, backlight_on=0, power_on=0,
spihost=esp.HSPI_HOST, spimode=0, mhz=40, factor=4, hybrid=True, width=240, height=320, start_x=0, start_y=0,
colormode=COLOR_MODE_BGR, rot=PORTRAIT, invert=False, double_buffer=True, half_duplex=True,
asynchronous=False, initialize=True, color_format=lv.COLOR_FORMAT.NATIVE_REVERSED
):
# Make sure Micropython was built such that color won't require processing before DMA
if lv.color_t.__SIZE__ != 2:
raise RuntimeError('ili9341 micropython driver requires defining LV_COLOR_DEPTH=16')
self.display_name = 'ILI9341'
self.init_cmds = [
{'cmd': 0xCF, 'data': bytes([0x00, 0x83, 0X30])},
{'cmd': 0xED, 'data': bytes([0x64, 0x03, 0X12, 0X81])},
{'cmd': 0xE8, 'data': bytes([0x85, 0x01, 0x79])},
{'cmd': 0xCB, 'data': bytes([0x39, 0x2C, 0x00, 0x34, 0x02])},
{'cmd': 0xF7, 'data': bytes([0x20])},
{'cmd': 0xEA, 'data': bytes([0x00, 0x00])},
{'cmd': 0xC0, 'data': bytes([0x26])}, # Power control
{'cmd': 0xC1, 'data': bytes([0x11])}, # Power control
{'cmd': 0xC5, 'data': bytes([0x35, 0x3E])}, # VCOM control
{'cmd': 0xC7, 'data': bytes([0xBE])}, # VCOM control
{'cmd': 0x36, 'data': bytes([
self.madctl(colormode, rot, ORIENTATION_TABLE)])}, # MADCTL
{'cmd': 0x3A, 'data': bytes([0x55])}, # Pixel Format Set
{'cmd': 0xB1, 'data': bytes([0x00, 0x1B])},
{'cmd': 0xF2, 'data': bytes([0x08])},
{'cmd': 0x26, 'data': bytes([0x01])},
{'cmd': 0xE0, 'data': bytes([0x1F, 0x1A, 0x18, 0x0A, 0x0F, 0x06, 0x45, 0X87, 0x32, 0x0A, 0x07, 0x02, 0x07, 0x05, 0x00])},
{'cmd': 0XE1, 'data': bytes([0x00, 0x25, 0x27, 0x05, 0x10, 0x09, 0x3A, 0x78, 0x4D, 0x05, 0x18, 0x0D, 0x38, 0x3A, 0x1F])},
{'cmd': 0x2A, 'data': bytes([0x00, 0x00, 0x00, 0xEF])},
{'cmd': 0x2B, 'data': bytes([0x00, 0x00, 0x01, 0x3f])},
{'cmd': 0x2C, 'data': bytes([0])},
{'cmd': 0xB7, 'data': bytes([0x07])},
{'cmd': 0xB6, 'data': bytes([0x0A, 0x82, 0x27, 0x00])},
{'cmd': 0x11, 'data': bytes([0]), 'delay':100},
{'cmd': 0x29, 'data': bytes([0]), 'delay':100}
]
Those init commands there use of 5,856 bytes of ram the entire time the program is running when it only gets used a single time when the program first starts up.
Just by changing the way things are stored in those init commands the memory use changes to 4,064 bytes. A savings of 1,792 bytes.
init_cmds = (
array.array('B', [0xCF, 0x00, 0x83, 0X30, 0]),
array.array('B', [0xED, 0x64, 0x03, 0X12, 0X81, 0]),
array.array('B', [0xE8, 0x85, 0x01, 0x79, 0]),
array.array('B', [0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02, 0]),
array.array('B', [0xF7, 0x20, 0]),
array.array('B', [0xEA, 0x00, 0x00, 0]),
array.array('B', [0xC0, 0x26, 0]),
array.array('B', [0xC1, 0x11, 0]),
array.array('B', [0xC5, 0x35, 0x3E, 0]),
array.array('B', [0xC7, 0xBE, 0]),
array.array('B', [0x36, 0x00, 0]),
array.array('B', [0x3A, 0x55, 0]),
array.array('B', [0xB1, 0x00, 0x1B, 0]),
array.array('B', [0xF2, 0x08, 0]),
array.array('B', [0x26, 0x01, 0]),
array.array('B', [0xE0, 0x1F, 0x1A, 0x18, 0x0A, 0x0F, 0x06, 0x45, 0X87, 0x32, 0x0A, 0x07, 0x02, 0x07, 0x05, 0x00, 0]),
array.array('B', [0XE1, 0x00, 0x25, 0x27, 0x05, 0x10, 0x09, 0x3A, 0x78, 0x4D, 0x05, 0x18, 0x0D, 0x38, 0x3A, 0x1F, 0]),
array.array('B', [0x2A, 0x00, 0x00, 0x00, 0xEF, 0]),
array.array('B', [0x2B, 0x00, 0x00, 0x01, 0x3f, 0]),
array.array('B', [0x2C, 0x00, 0]),
array.array('B', [0xB7, 0x07, 0]),
array.array('B', [0xB6, 0x0A, 0x82, 0x27, 0x00, 0]),
array.array('B', [0x11, 0x00, 100]),
array.array('B', [0x29, 0x00, 100])
)