BLE on T-display ESP32 runs out of memory

I am using a TENSTAR T-display ESP32 to interface with a bluetooth pulse oximeter.
I have it working fine with an ESP32-devkit as well as with the TENSTAR T-display so long as I don’t use the display.

If I try to include even themmost basic code for using the display, it seems to run out of memory.

I have essentially the simplest possible setup. Two ble sensors set up to read bytes 6 and 7 respectively from the BLE notification strings from the pulse-ox

For the display I just used a Roboto font to display Hello World (I am still testing).
I tried even with a small 12 point size.

I stripped out all extraneous stuff such as OTA updates, web server, AP etc.
I set log level to WARN.

It compiles and uploads fine so I assume the problem is not flash memory.

However, when I run, the device fails to load BLE and/or says it runs out of memory while trying to attach to WiFi.
Often (but not alway) it just gets stuck in boot loops before falling back to safe mode.

  • Is this normal that BLE + Display consumes so much flash/SRAM that it runs out of memory?
  • Any suggestions on how to reduce memory consumption by BLE and display so I don’t run out of memory?
  • Are there any other versions of an integrated esp32 with small display (~1.5") that include more SRAM (and maybe also flash)?

Here is some relevant log info:

[12:23:31]ets Jun  8 2016 00:22:57
[12:23:31]
[12:23:31]rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
[12:23:31]configsip: 0, SPIWP:0xee
[12:23:31]clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
[12:23:31]mode:DIO, clock div:2
[12:23:31]load:0x3fff0030,len:1344
[12:23:31]load:0x40078000,len:13836
[12:23:31]load:0x40080400,len:3608
[12:23:31]entry 0x400805f0
[12:23:32][W][component:157]: Component wifi set Warning flag: scanning for networks
[12:23:37]E (11550) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:
[12:23:37]E (11550) task_wdt:  - loopTask (CPU 1)
[12:23:37]E (11550) task_wdt: Tasks currently running:
[12:23:37]E (11550) task_wdt: CPU 0: IDLE
[12:23:37]E (11550) task_wdt: CPU 1: IDLE
[12:23:37]E (11550) task_wdt: Aborting.
[12:23:37]
[12:23:37]abort() was called at PC 0x400f1ac1 on core 0
[12:23:37]
[12:23:37]
[12:23:37]Backtrace:0x40083885:0x3ffbea3c |<-CORRUPTED
[12:23:37]
[12:23:37]
[12:23:37]
[12:23:37]
[12:23:37]ELF file SHA256: 0000000000000000
[12:23:37]
[12:23:37]Rebooting...
[12:23:37]ets Jun  8 2016 00:22:57
[12:23:37]
[12:23:37]rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
[12:23:37]configsip: 0, SPIWP:0xee
[12:23:37]clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
[12:23:37]mode:DIO, clock div:2
[12:23:37]load:0x3fff0030,len:1344
[12:23:37]load:0x40078000,len:13836
[12:23:37]load:0x40080400,len:3608
[12:23:37]entry 0x400805f0
[12:23:38][W][component:157]: Component wifi set Warning flag: scanning for networks
[12:23:39]
[12:23:39]assert failed: xQueueSemaphoreTake queue.c:1545 (( pxQueue ))
[12:23:39]
[12:23:39]
[12:23:39]Backtrace:0x40083885:0x3fff46c00x40093c15:0x3fff46e0 0x40099191:0x3fff4700 0x40094c19:0x3fff4830 0x4013f3c1:0x3fff4870 0x40115764:0x3fff4890 0x40114e41:0x3fff48b0 0x40144dea:0x3fff48e0 0x401157c7:0x3fff4900 0x4013d631:0x3fff4920 0x4013f4a7:0x3fff4940 
[12:23:39]
[12:23:39]
[12:23:39]
[12:23:39]
[12:23:39]ELF file SHA256: 0000000000000000
[12:23:39]
[12:23:39]Rebooting...
[12:23:39]ets Jun  8 2016 00:22:57
[12:23:39]
[12:23:39]rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
[12:23:39]configsip: 0, SPIWP:0xee
[12:23:39]clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
[12:23:39]mode:DIO, clock div:2
[12:23:39]load:0x3fff0030,len:1344
[12:23:39]load:0x40078000,len:13836
[12:23:39]load:0x40080400,len:3608
[12:23:39]entry 0x400805f0
[12:23:40][W][component:157]: Component wifi set Warning flag: scanning for networks
[12:23:45]E (11548) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:
[12:23:45]E (11548) task_wdt:  - loopTask (CPU 1)
[12:23:45]E (11548) task_wdt: Tasks currently running:
[12:23:45]E (11548) task_wdt: CPU 0: IDLE
[12:23:45]E (11548) task_wdt: CPU 1: IDLE
[12:23:45]E (11548) task_wdt: Aborting.
[12:23:45]
[12:23:45]abort() was called at PC 0x400f1ac1 on core 0
[12:23:45]
[12:23:45]
[12:23:45]Backtrace:0x40083885:0x3ffbea3c |<-CORRUPTED
[12:23:45]
[12:23:45]
[12:23:45]
[12:23:45]
[12:23:45]ELF file SHA256: 0000000000000000
[12:23:45]
[12:23:45]Rebooting...
[12:23:45]ets Jun  8 2016 00:22:57
[12:23:45]
[12:23:45]rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
[12:23:45]configsip: 0, SPIWP:0xee
[12:23:45]clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
[12:23:45]mode:DIO, clock div:2
[12:23:45]load:0x3fff0030,len:1344
[12:23:45]load:0x40078000,len:13836
[12:23:45]load:0x40080400,len:3608
[12:23:45]entry 0x400805f0
[12:23:46][W][component:157]: Component wifi set Warning flag: scanning for networks
[12:23:51]E (11548) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:
[12:23:51]E (11548) task_wdt:  - loopTask (CPU 1)
[12:23:51]E (11548) task_wdt: Tasks currently running:
[12:23:51]E (11548) task_wdt: CPU 0: IDLE
[12:23:51]E (11548) task_wdt: CPU 1: IDLE
[12:23:51]E (11548) task_wdt: Aborting.
[12:23:51]
[12:23:51]abort() was called at PC 0x400f1ac1 on core 0
[12:23:51]
[12:23:51]
[12:23:51]Backtrace:0x40083885:0x3ffbea3c |<-CORRUPTED
[12:23:51]
[12:23:51]
[12:23:51]
[12:23:51]
[12:23:51]ELF file SHA256: 0000000000000000
[12:23:51]
[12:23:51]Rebooting...
[12:23:51]ets Jun  8 2016 00:22:57
[12:23:51]
[12:23:51]rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
[12:23:51]configsip: 0, SPIWP:0xee
[12:23:51]clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
[12:23:51]mode:DIO, clock div:2
[12:23:51]load:0x3fff0030,len:1344
[12:23:51]load:0x40078000,len:13836
[12:23:51]load:0x40080400,len:3608
[12:23:51]entry 0x400805f0
[12:23:52][W][component:157]: Component wifi set Warning flag: scanning for networks
[12:23:53][E][esp32_ble:166]: esp_bluedroid_enable failed: 257
[12:23:53][E][esp32_ble:299]: BLE could not be set up
[12:23:53][E][component:119]: Component esp32_ble was marked as failed.
[12:23:53][E][component:164]: Component esp32_ble set Error flag: unspecified
[12:23:54][W][component:239]: Component display took a long time for an operation (81 ms).
[12:23:54][W][component:240]: Components should block for at most 30 ms.
[12:23:59][W][component:239]: Component display took a long time for an operation (81 ms).
[12:23:59][W][component:240]: Components should block for at most 30 ms.
[12:24:01][W][ble_sensor:123]: [Pulse] Cannot poll, not connected
[12:24:04][W][component:239]: Component display took a long time for an operation (81 ms).
[12:24:04][W][component:240]: Components should block for at most 30 ms.
[12:24:09][W][component:239]: Component display took a long time for an operation (81 ms).
[12:24:09][W][component:240]: Components should block for at most 30 ms.
[12:24:14][W][component:239]: Component display took a long time for an operation (81 ms).
[12:24:14][W][component:240]: Components should block for at most 30 ms.
[12:24:17][W][ble_sensor:123]: [O2Sat] Cannot poll, not connected
[12:24:19][W][component:239]: Component display took a long time for an operation (81 ms).
[12:24:19][W][component:240]: Components should block for at most 30 ms.
[12:24:22][E][wifi:490]: Scan timeout!
[12:24:24][W][component:239]: Component display took a long time for an operation (81 ms).
[12:24:24][W][component:240]: Components should block for at most 30 

More than you have now, which is?
Did you enable psram?

The device reportedly has 4MB Flash and 520KB SRAM.
I don’t believe it has any PSRAM

Bluetooth and display uses a lot of memory.
Combining them usually doesn’t work

I’m not a display guy, so I cant suggest some specific device. Hanging here on this forum I can tell that availability is not a problem. But pick a board with psram.

Atttach your yaml. There are ways you can reduce the display memory requirements.

Thanks. I spent the better part of the day working on ways to reduce memory but ending up giving up.

Instead, I attached an i2c 1602 two-line LCD display and got that working.
Memory and cpu speed were still a bit tight but I got it working now smoothly without issue.

I posted the code to:

By all means, if you think you can get this working on a T-display, then I am happy to try. But meanwhile i at least have a reasonable and simple working solution.

Thanks!!!

I don’t have the pulse ox to test with but all the components build and run and I faked the numbers for the pic. The web server is probably using a decent chunk of RAM and you don’t need the serial improv, but still have 60K free.

[13:48:56][C][wifi:428]:   Local MAC: B0:B2:1C:4F:9F:B8
[13:48:56][C][wifi:433]:   SSID: 'ESPHome'
[13:48:56][C][wifi:436]:   IP Address: 192.168.1.2
[13:48:56][C][wifi:439]:   BSSID: 1E:E8:29:97:15:75
[13:48:57][C][wifi:441]:   Hostname: 't-display'
[13:48:57][C][wifi:443]:   Signal strength: -39 dB ▂▄▆█
[13:48:57][C][wifi:447]:   Channel: 6
[13:48:57][C][wifi:448]:   Subnet: 255.255.252.0
[13:48:57][C][wifi:449]:   Gateway: 192.168.1.254
[13:48:57][C][wifi:450]:   DNS1: 192.168.1.254
[13:48:57][C][wifi:451]:   DNS2: 0.0.0.0
[13:48:57][D][wifi:626]: Disabling AP...
[13:48:57][C][web_server:238]: Setting up web server...
[13:48:57][D][esp-idf:000]: I (7442) mdns_mem: mDNS task will be created from internal RAM
[13:48:57]
[13:48:57][C][api:030]: Setting up Home Assistant API server...
[13:48:57][I][app:062]: setup() finished successfully!
[13:48:57][W][component:182]: Component wifi cleared Warning flag
[13:48:57][W][component:167]: Component api set Warning flag: unspecified
[13:48:57][I][app:101]: ESPHome version 2025.5.0-dev compiled on May  1 2025, 13:47:57
[13:48:57][C][wifi:600]: WiFi:
[13:48:57][C][wifi:428]:   Local MAC: B0:B2:1C:4F:9F:B8
[13:48:57][C][wifi:433]:   SSID: 'ESPHome'
[13:48:57][C][wifi:436]:   IP Address: 192.168.1.2
[13:48:57][C][wifi:439]:   BSSID: 1E:E8:29:97:15:75
[13:48:57][C][wifi:441]:   Hostname: 't-display'
[13:48:57][C][wifi:443]:   Signal strength: -37 dB ▂▄▆█
[13:48:57][C][wifi:447]:   Channel: 6
[13:48:57][C][wifi:448]:   Subnet: 255.255.252.0
[13:48:57][C][wifi:449]:   Gateway: 192.168.1.254
[13:48:57][C][wifi:450]:   DNS1: 192.168.1.254
[13:48:57][C][wifi:451]:   DNS2: 0.0.0.0
[13:48:57][C][logger:177]: Logger:
[13:48:57][C][logger:178]:   Max Level: DEBUG
[13:48:57][C][logger:179]:   Initial Level: DEBUG
[13:48:57][C][logger:181]:   Log Baud Rate: 115200
[13:48:57][C][logger:182]:   Hardware UART: UART0
[13:48:57][C][spi:068]: SPI bus:
[13:48:57][C][spi:069]:   CLK Pin: GPIO18
[13:48:57][C][spi:070]:   SDI Pin:
[13:48:57][C][spi:071]:   SDO Pin: GPIO19
[13:48:57][C][spi:076]:   Using HW SPI: SPI2_HOST
[13:48:57][C][i2c.idf:083]: I2C Bus:
[13:48:57][C][i2c.idf:084]:   SDA Pin: GPIO21
[13:48:57][C][i2c.idf:085]:   SCL Pin: GPIO22
[13:48:57][C][i2c.idf:086]:   Frequency: 50000 Hz
[13:48:57][C][i2c.idf:092]:   Recovery: bus successfully recovered
[13:48:57][I][i2c.idf:102]: Results from i2c bus scan:
[13:48:57][I][i2c.idf:104]: Found no i2c devices!
[13:48:57][C][power_supply:018]: Power Supply:
[13:48:57][C][power_supply:019]:   Pin: GPIO4
[13:48:57][C][power_supply:020]:   Time to enable: 20 ms
[13:48:57][C][power_supply:021]:   Keep on time: 10.0 s
[13:48:57][C][power_supply:023]:   Enabled at startup: True
[13:48:57][C][template.sensor:022]: Template Sensor 'O2Sat'
[13:48:57][C][template.sensor:022]:   State Class: ''
[13:48:57][C][template.sensor:022]:   Unit of Measurement: '%'
[13:48:57][C][template.sensor:022]:   Accuracy Decimals: 0
[13:48:57][C][template.sensor:022]:   Icon: 'mdi:lung'
[13:48:57][C][template.sensor:023]:   Update Interval: 60.0s
[13:48:57][C][template.sensor:022]: Template Sensor 'Pulse'
[13:48:57][C][template.sensor:022]:   State Class: ''
[13:48:57][C][template.sensor:022]:   Unit of Measurement: 'bpm'
[13:48:57][C][template.sensor:022]:   Accuracy Decimals: 0
[13:48:57][C][template.sensor:022]:   Icon: 'mdi:heart-pulse'
[13:48:57][C][template.sensor:023]:   Update Interval: 60.0s
[13:48:57][C][homeassistant.time:010]: Home Assistant Time:
[13:48:57][C][homeassistant.time:011]:   Timezone: 'AEST-10AEDT,M10.1.0,M4.1.0/3'
[13:48:57][C][display.mipi_spi:450]: MIPI_SPI Display
[13:48:57][C][display.mipi_spi:451]:   Model: T-DISPLAY
[13:48:57][C][display.mipi_spi:452]:   Width: 135
[13:48:57][C][display.mipi_spi:453]:   Height: 240
[13:48:57][C][display.mipi_spi:455]:   Offset width: 52
[13:48:57][C][display.mipi_spi:457]:   Offset height: 40
[13:48:57][C][display.mipi_spi:458]:   Swap X/Y: NO
[13:48:57][C][display.mipi_spi:459]:   Mirror X: NO
[13:48:57][C][display.mipi_spi:460]:   Mirror Y: NO
[13:48:57][C][display.mipi_spi:461]:   Color depth: 16 bits
[13:48:57][C][display.mipi_spi:462]:   Invert colors: YES
[13:48:57][C][display.mipi_spi:463]:   Color order: BGR
[13:48:57][C][display.mipi_spi:464]:   Pixel mode: 16bit
[13:48:57][C][display.mipi_spi:469]:   Draw rounding: 1
[13:48:57][C][display.mipi_spi:472]:   CS Pin: GPIO5
[13:48:57][C][display.mipi_spi:474]:   DC Pin: GPIO16
[13:48:57][C][display.mipi_spi:475]:   SPI Mode: 0
[13:48:57][C][display.mipi_spi:476]:   SPI Data rate: 10MHz
[13:48:57][C][display.mipi_spi:477]:   SPI Bus width: 1
[13:48:57][C][ble_sensor:017]: BLE Sensor 'internal_pulseox'
[13:48:57][C][ble_sensor:017]:   State Class: ''
[13:48:57][C][ble_sensor:017]:   Unit of Measurement: ''
[13:48:57][C][ble_sensor:017]:   Accuracy Decimals: 0
[13:48:57][C][ble_sensor:018]:   MAC address        : 00:00:00:03:10:C4
[13:48:57][C][ble_sensor:019]:   Service UUID       : 6E400001-B5A3-F393-E0A9-E50E24DCCA9E
[13:48:57][C][ble_sensor:020]:   Characteristic UUID: 6E400003-B5A3-F393-E0A9-E50E24DCCA9E
[13:48:57][C][ble_sensor:021]:   Descriptor UUID    : 00000000-0000-0000-0000-000000000000
[13:48:57][C][ble_sensor:022]:   Notifications      : YES
[13:48:57][C][ble_sensor:023]:   Update Interval: 60.0s
[13:48:57][C][lvgl:086]: LVGL:
[13:48:57][C][lvgl:087]:   Display width/height: 135 x 240
[13:48:57][C][lvgl:088]:   Rotation: 0
[13:48:57][C][lvgl:089]:   Draw rounding: 2
[13:48:57][C][esp32_ble:410]: ESP32 BLE:
[13:48:57][C][esp32_ble:411]:   MAC address: B0:B2:1C:4F:9F:BA
[13:48:57][C][esp32_ble:413]:   IO Capability: none
[13:48:57][C][esp32_ble_tracker:714]: BLE Tracker:
[13:48:57][C][esp32_ble_tracker:715]:   Scan Duration: 300 s
[13:48:57][C][esp32_ble_tracker:716]:   Scan Interval: 3000.0 ms
[13:48:57][C][esp32_ble_tracker:717]:   Scan Window: 160.0 ms
[13:48:57][C][esp32_ble_tracker:718]:   Scan Type: PASSIVE
[13:48:57][C][esp32_ble_tracker:719]:   Continuous Scanning: YES
[13:48:57][C][esp32_ble_tracker:728]:   Scanner State: RUNNING
[13:48:57][C][esp32_ble_tracker:740]:   Connecting: 0, discovered: 0, searching: 0, disconnecting: 0
[13:48:57][C][ble_client:027]: BLE Client:
[13:48:57][C][esp32_ble_client:048]:   Address: 00:00:00:03:10:C4
[13:48:57][C][esp32_ble_client:049]:   Auto-Connect: TRUE
[13:48:57][C][esp32_ble_client:083]:   State: IDLE
[13:48:57][C][captive_portal:089]: Captive Portal:
[13:48:57][C][web_server:285]: Web Server:
[13:48:57][C][web_server:286]:   Address: t-display.local:80
[13:48:57][C][mdns:120]: mDNS:
[13:48:57][C][mdns:121]:   Hostname: t-display
[13:48:57][C][esphome.ota:073]: Over-The-Air updates:
[13:48:57][C][esphome.ota:074]:   Address: t-display.local:3232
[13:48:57][C][esphome.ota:075]:   Version: 2
[13:48:57][C][safe_mode:018]: Safe Mode:
[13:48:57][C][safe_mode:019]:   Boot considered successful after 60 seconds
[13:48:57][C][safe_mode:021]:   Invoke after 10 boot attempts
[13:48:57][C][safe_mode:022]:   Remain in safe mode for 300 seconds
[13:48:57][C][api:160]: API Server:
[13:48:57][C][api:161]:   Address: t-display.local:6053
[13:48:57][C][api:168]:   Using noise encryption: NO
[13:48:57][C][improv_serial:032]: Improv Serial:
[13:48:57][C][debug:022]: Debug component:
[13:48:57][C][debug:027]:   Free space on heap 'Heap free'
[13:48:57][C][debug:027]:     State Class: ''
[13:48:57][C][debug:027]:     Unit of Measurement: 'B'
[13:48:57][C][debug:027]:     Accuracy Decimals: 0
[13:48:57][C][debug:027]:     Icon: 'mdi:counter'
[13:48:57][D][debug:037]: ESPHome version 2025.5.0-dev
[13:48:57][D][debug:041]: Free Heap Size: 59736 bytes
[13:48:57][D][debug:177]: Chip: Model=ESP32, Features=2.4GHz WiFi, BLE, BT,  Cores=2, Revision=300
[13:48:57][D][debug:186]: CPU Frequency: 160 MHz
[13:48:57][D][debug:194]: Framework: ESP-IDF
[13:48:57][D][debug:201]: ESP-IDF Version: 5.1.6
[13:48:57][D][debug:206]: EFuse MAC: B0:B2:1C:4F:9F:B8
[13:48:57][D][debug:076]: Reset Reason: power-on event
[13:48:57][D][debug:104]: Wakeup Reason: undefined
[13:48:57][C][debug:109]: Partition table:
[13:48:57][C][debug:110]:   Name         Type Subtype  Address    Size
[13:48:57][C][debug:114]:   otadata      1    0        0x00009000 0x00002000
[13:48:57][C][debug:114]:   phy_init     1    1        0x0000B000 0x00001000
[13:48:57][C][debug:114]:   app0         0    16       0x00010000 0x001C0000
[13:48:57][C][debug:114]:   app1         0    17       0x001D0000 0x001C0000
[13:48:57][C][debug:114]:   nvs          1    2        0x00390000 0x0006D000
[13:48:58][D][sensor:093]: 'Heap free': Sending state 60576.00000 B with 0 decimals of accuracy
[13:49:08][D][sensor:093]: 'Heap free': Sending state 60976.00000 B with 0 decimals of accuracy

Interesting… what did you do differently?
Did you do any optimizations? Or leave out any code?

Perhaps my code for writing to the T-display was wrong as I have never used it before.
Would be great if you could share :slight_smile:

Note I posted updated code that works on a 2-line display to:

Thanks

I tried using my TENSTAR t-display esp32 again substituting the following spi, font, and display sections in the above referenced 2-line 1602 display code.
I still get boot loops :frowning:

spi:
  clk_pin: GPIO18
  mosi_pin: GPIO19

font:
  - file: "gfonts://Roboto"
    id: myfont
    size: 16

display:
  - platform: st7789v
    model: TTGO_TDisplay_135x240
    backlight_pin: GPIO4  
    cs_pin: GPIO5
    dc_pin: GPIO16
    reset_pin: GPIO23
    rotation: 270
    update_interval: ${update_interval}
    lambda: |-
      if (id(pc_60fw).connected() && id(connect_start) != -1) {
        id(connect_length) = (millis() / 1000) - id(connect_start);
      }
      int connect_total = id(connect_prevtotal) + id(connect_length);
      static bool separator = true;
      separator = not(separator); //Toggle every other display to show alive
      char sep_char = id(record_to_ha) ? '+' : '-';
      it.printf(0, 0, id(myfont), "%2d:%02d:%02d%c%d:%02d:%02d",
        (connect_total / 3600)%100, (connect_total % 3600) / 60, connect_total % 60,
        separator ? sep_char : ' ',
        (id(connect_length) / 3600)%10, (id(connect_length) % 3600) / 60, id(connect_length) % 60);
      if(id(pulse_current) != 0)
        it.printf(0, 16, id(myfont), "O2:%3d%%  P:%3d", id(o2sat_current), id(pulse_current));

I then tried a truly minimalist display:

spi:
  clk_pin: GPIO18
  mosi_pin: GPIO19

font:
  - file: "gfonts://Roboto"
    id: myfont
    size: 8
  
display:
  - platform: st7789v
    model: TTGO_TDisplay_135x240
    cs_pin: GPIO5
    dc_pin: GPIO16
    reset_pin: GPIO23
    update_interval: ${update_interval}
    lambda: |-
      ESP_LOGW("mem", "Heap before first print: %u", ESP.getFreeHeap()); 
      it.print(0, 0, id(myfont), "Hello, World!");
      ESP_LOGW("mem", "Heap after final print: %u", ESP.getFreeHeap()); 

And still got boot loops.
The logs showed:

[23:29:27]Rebooting...
[23:29:27]ets Jun  8 2016 00:22:57
[23:29:27]
[23:29:27]rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
[23:29:27]configsip: 0, SPIWP:0xee
[23:29:27]clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
[23:29:27]mode:DIO, clock div:2
[23:29:27]load:0x3fff0030,len:1184
[23:29:27]load:0x40078000,len:13132
[23:29:27]load:0x40080400,len:3036
[23:29:27]entry 0x400805e4
[23:29:28][W][mem:235]: Heap before first print: 101040
[23:29:28][W][mem:239]: Heap after final print: 101040
[23:29:28][W][component:239]: Component display took a long time for an operation (136 ms).
[23:29:28][W][component:240]: Components should block for at most 30 ms.
[23:29:28][W][component:157]: Component wifi set Warning flag: associating to network
[23:29:28]Guru Meditation Error: Core  0 panic'ed (StoreProhibited). Exception was unhandled.
[23:29:28]
[23:29:28]Core  0 register dump:
[23:29:29]PC      : 0x40090adc  PS      : 0x00060230  A0      : 0x8015511f  A1      : 0x3fff92a0  
[23:29:29]A2      : 0x00000000  A3      : 0x00000000  A4      : 0x00000130  A5      : 0x00000000  
[23:29:29]A6      : 0x3fff6fcc  A7      : 0x00000013  A8      : 0x8015c928  A9      : 0x3fff9270  
[23:29:29]A10     : 0x3ffc8a80  A11     : 0x00000468  A12     : 0x3ffc8a7c  A13     : 0x3ffc4078  
[23:29:29]A14     : 0x00002aa6  A15     : 0x00000000  SAR     : 0x0000001e  EXCCAUSE: 0x0000001d  
[23:29:29]EXCVADDR: 0x00000000  LBEG    : 0x40090adc  LEND    : 0x40090ae7  LCOUNT  : 0x00000012  
[23:29:29]
[23:29:29]
[23:29:29]Backtrace:0x40090ad9:0x3fff92a00x4015511c:0x3fff92b0 0x40136b01:0x3fff92d0 0x4014fcc7:0x3fff92f0 
[23:29:29]
[23:29:29]
[23:29:29]
[23:29:29]
[23:29:29]ELF file SHA256: 0000000000000000
[23:29:29] 

Seems like there is sufficient heap memory 100K
Not sure what I am doing wrong…

I then tried setting:

color_palette: 8BIT

This mostly the boot loops, but then WiFi became unstable and the display was black.
It showed a free heap size of: about 24864.

I keep coming back to the fact that there just doesn’t seem to be enough SRAM to support this device since it contains maybe as little as 384K (and I believe there is no PSRAM).

Are you using a board with more SRAM or with PSRAM?

No, a TTGO T-Display as per the pic, basically the same as you. Just seems a shame not to use the on-board display. Here’s the yaml I tested. The key is using LVGL with a small buffer size, which avoids having to allocate a full screen-sized buffer for the display.

You presumably can also remove the captive_portal and the wifi ap, and the web_server which will save even more RAM.

substitutions:
    name: pc-60fw
    friendly_name: PC-60FW PulseOx
    update_interval: 1s  # Update frequency for sensors and display

globals:
  - id: ble_connected_at
    type: int
    restore_value: no
    initial_value: '-1'

# Enable Home Assistant API
api:

# Allow Over-The-Air updates
ota:
- platform: esphome


wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: true #Reduce initialization time and memory usage since not scanning
  power_save_mode: LIGHT #Reduce memory/CPU usage at cost of slight delay
  
  # Set up a fallback wifi access point to configure WiFi if can't connect
  ap:
    ssid: "PC-60FW"

# In combination with the `ap` this allows the user
# to provision wifi credentials to the device via WiFi AP.
captive_portal:

web_server:

esp32_ble_tracker:
  scan_parameters:
    interval: 3000ms  # Increased from default (typically 320ms)
    window: 160ms     # Kept the same
    active: false     # Disabled

ble_client:
  - mac_address: 00:00:00:03:10:C4
    id: pc_60fw
    on_connect:
      then:
        - logger.log: 
            level: WARN
            format: "***PC-60FW connected***"
        - lambda: |-
            id(ble_connected_at) = (int) (millis() / 1000);  // store uptime in seconds
    on_disconnect:
      then:
        - logger.log: 
            level: WARN
            format: "***PC-60FW disconnected***"

sensor:
  - platform: debug
    free:
      name: "Heap free"
  # Bluetooth PC-60FW O2Sat & Pulse sensor
  - platform: ble_client
    type: characteristic
    id: internal_pulseox
    internal: true #Don't create HA entity
    ble_client_id: pc_60fw
    service_uuid: '6e400001-b5a3-f393-e0a9-e50e24dcca9e'
    characteristic_uuid: '6e400003-b5a3-f393-e0a9-e50e24dcca9e'
    notify: true  # Enable notifications for the characteristic
    lambda: |-
      // Check if the data is at least 11 bytes and starts with AA 55 0F 08 01
      if (x.size() >= 11 && x[0] == 0xAA && x[1] == 0x55 && x[2] == 0x0F && x[3] == 0x08 && x[4] == 0x01) {
        // 6th byte (index 5) is O2 saturation; 7th byte (index 6) is pulse
        int o2sat_value = x[5];
        int pulse_value = x[6];
        ESP_LOGW("internal_pulseox", "O2Sat: %d\tPulse %d", o2sat_value, pulse_value);
        if (o2sat_value != 0) id(o2sat).publish_state(o2sat_value);
        if (pulse_value != 0) id(pulse).publish_state(pulse_value);
      }
      return {}; // This sensor doesn't report its own state

  - platform: template
    id: o2sat
    name: "O2Sat"
    icon: 'mdi:lung'
    unit_of_measurement: '%'
    accuracy_decimals: 0
    filters:
      - delta: 0.5  # Only publish if the value changes by at least 0.5
      - throttle: ${update_interval}  # Limit update frequency

  - platform: template
    id: pulse
    name: "Pulse"
    icon: 'mdi:heart-pulse'
    unit_of_measurement: 'bpm'
    accuracy_decimals: 0
    filters:
      - delta: 0.5  # Only publish if the value changes by at least 0.5
      - throttle: ${update_interval}  # Limit update frequency
    on_value:
      - lvgl.label.update:
          id: o2_label
          text:
            format: "O2:%3.0f%%  P:%3.0f"
            args: [id(o2sat).state, x]
      - lvgl.label.update:
          id: time_label
          text:
            format: "%s %d:%02d:%02d"
            args: ['id(esptime).now().strftime("%H:%M:%S").c_str()', "5", "20", "45"]


interval:
  - interval: 10s
    then:
      - sensor.template.publish:
          id: pulse
          state: !lambda return 60.0 + random_uint32() % 10;
      - sensor.template.publish:
          id: o2sat
          state: !lambda return 95.0 + random_uint32() % 5;
    
        

time:
  - platform: sntp
    id: esptime
    update_interval: 60s


esphome:
  name: t-display

esp32:
  board: esp32dev
  framework:
    type: esp-idf

logger:

debug:
  update_interval: 10s

external_components:
  - source: github://pr#8383
    components: [mipi_spi, spi, const]
    refresh: 1h

power_supply:
  - id: backlight
    pin: 4
    enable_on_boot: true

spi:
  clk_pin: 18
  mosi_pin: 19

i2c:
  - scl: 22
    sda: 21
    scan: true
    id: i2c_onboard

display:
  - platform: mipi_spi
    model: t-display
    rotation: 90

lvgl:
  buffer_size: 12%
  bg_color: black
  theme:
    label:
      text_font: montserrat_28
      text_align: center
  widgets:
    - label:
        id: time_label
        align: top_mid
        y: 20
        text_color: green
    - label:
        id: o2_label
        align: bottom_mid
        y: -20
        text_color: blue

Yes. Some components like wifi, ble and displays are just heavy.

Seeing how a dev board costs less than a coffee, why don’t you split your solution? Do the ble stuff on a dev board and use the TENSTAR just for its display.

Looks awesome… however, I get the following error when validating yaml:

Failed config

display.mipi_spi: [source /config/esphome/PulseOx-PC60FW-ble-tdisplay.yaml:247]
  
  Platform not found: 'display.mipi_spi'.

Presumably the problem is the line:

  - source: github://pr#8383

Based on a response from you on another thread, I tried:

external_components:
  - source: github://clydebarrow/esphome@mipi-spi
    components: [spi, mipi_spi]

But that gave the same error

Note: I am using ESPHome 2025.4.1
Note: I get the same not found if I try to use display.tft_espi` which is apparently another lightweight driver (but I couldn’t find a source for it)

Also, assuming I can get mipi_spi working, wouldn’t it be better to just use display directly without lvgl to conserve memory (though lvgl seems really cool)

Ah, I was compiling with the current dev, so for compiling against the release version there is a small change needed - I updated the yaml above, but the change is adding const (a newly added component) to the external components.

external_components:
  - source: github://pr#8383
    components: [mipi_spi, spi, const]
    refresh: 1h

No, as I said above: “The key is using LVGL with a small buffer size, which avoids having to allocate a full screen-sized buffer for the display”.

Using the display lambda functions needs more memory.