M5Stack Core Black

[Blueprint/Showcase] M5Stack Faces Smart Remote Control via ESPHome (Fixed ILI9342 & Memory Crashes)

Hi everyone,

I would like to share a fully optimized and tested ESPHome configuration for the M5Stack Core v1 (Basic/Gray) combined with the Faces Encoder Panel (Rotary encoder + 12 RGB LED ring).

Using newer ESPHome versions (2025/2026) with this specific old-school hardware usually triggers major headaches: blank screens, vertical color lines, or immediate boots loops due to memory allocation failures (Failed to init Memory: YES!), because the original Core device lacks PSRAM.

After systematic troubleshooting, we successfully mapped out the undocumented pinout and register settings for the built-in ILI9342 controller under the Arduino framework. Everything now runs fast, stable, and completely in crisp landscape mode!

:glowing_star: Key Solutions Included in This Template:

  1. DRAM Crash Fix: Implemented the modern mipi_spi driver with a restricted buffer_size: 16%. This splits the graphics buffer and keeps the device from running out of RAM.
  2. Undocumented Pinout: Fixed the hidden wiring used in these Core revisions: CS: 14, DC: 27, and Reset: 33.
  3. ILI9342 Signal Calibration: Locked the SPI bus to spi_mode: 3 at 20MHz. Removed vertical yellow/magenta artifacts by enforcing color_order: BGR and invert_colors: true with rotation: 270.
  4. C++ Puffer Safety: Corrected text buffer declarations (char raum_buffer[32]) inside lambdas to prevent silent memory fragmentation during runtime snprintf actions.
  5. Nativ I2C Encoder Logic: Direct register scanning of the Faces keyboard base (0x5E) for ultra-responsive wheel turns, button presses, and synchronized LED-ring feedbacks.

:floppy_disk: Part 1: Core Framework, Display Sync & Fonts

Paste this into your ESPHome configuration file:

yaml

esphome:
  name: m5stack-faces-remote
  on_boot:
    priority: 100
    then:
      - light.turn_on:
          id: back_light
          brightness: 100%

esp32:
  board: m5stack-core-esp32
  framework:
    type: arduino  # Required for solid data_rate divider mappings

logger:

wifi:
  ssid: "YOUR_SSID"
  password: "YOUR_PASSWORD"

api:
  id: ha_api

ota:
  - platform: esphome

i2c:
  sda: GPIO21
  scl: GPIO22
  scan: true

i2c_device:
  id: faces_hardware
  address: 0x5E

spi:
  id: spi_bus
  clk_pin: 18
  mosi_pin: 23

output:
  - platform: ledc
    pin: 32
    id: gpio_32_backlight_pwm
  - platform: ledc
    pin: GPIO25
    id: buzzer_output
    frequency: 4000Hz

light:
  - platform: monochromatic
    output: gpio_32_backlight_pwm
    name: "Display Backlight"
    id: back_light
    restore_mode: ALWAYS_ON
    default_transition_length: 0s

display:
  - platform: mipi_spi
    id: tft_display
    spi_id: spi_bus
    model: ILI9341
    cs_pin: 14
    dc_pin: 27
    reset_pin: 33
    data_rate: 20000000Hz
    spi_mode: 3
    dimensions:
      width: 320
      height: 240
      offset_height: 0
      offset_width: 0
    buffer_size: 16%
    color_depth: 16bit
    color_order: BGR
    invert_colors: true   
    rotation: 270
    transform:
      mirror_x: false      
      mirror_y: false      
      swap_xy: false
    lambda: |-
      it.fill(Color::BLACK);
      it.print(160, 10, id(font_normal), Color::WHITE, TextAlign::TOP_CENTER, "M5Stack Faces Remote");
      it.line(10, 35, 310, 35, Color(180, 180, 180));

      if (id(aktueller_modus) == 0) {
        it.print(20, 50, id(font_normal), Color(0, 255, 255), TextAlign::TOP_LEFT, "Modus: RAUM AUSWAHL");
      } else if (id(aktueller_modus) == 1) {
        it.print(20, 50, id(font_normal), Color(0, 255, 0), TextAlign::TOP_LEFT, "Modus: GERAET AUSWAHL");
      } else if (id(aktueller_modus) == 2) {
        it.print(20, 50, id(font_normal), Color(255, 165, 0), TextAlign::TOP_LEFT, "Modus: DIMMEN");
      }

      const char* raum_name = "Unbekannt";
      const char* raum_icon = "\U000F02DC";
      
      switch(id(gewaehlter_raum)) {
        case 0: raum_name = "Kueche"; raum_icon = "\U000F0190"; break;
        case 1: raum_name = "Schlafzimmer"; raum_icon = "\U000F009F"; break;
        case 2: raum_name = "Esszimmer"; raum_icon = "\U000F05A0"; break;
        case 3: raum_name = "Wohnzimmer"; raum_icon = "\U000F0538"; break;
        case 4: raum_name = "Pool"; raum_icon = "\U000F0604"; break;
        case 5: raum_name = "Terrasse West"; raum_icon = "\U000F03AF"; break;
        case 6: raum_name = "Terrasse Sued"; raum_icon = "\U000F03AF"; break;
      }
      
      it.print(20, 90, id(font_icons), Color::WHITE, TextAlign::TOP_LEFT, raum_icon);
      
      char raum_buffer[32]; 
      snprintf(raum_buffer, sizeof(raum_buffer), " %s", raum_name);
      it.print(65, 95, id(font_normal), Color::WHITE, TextAlign::TOP_LEFT, raum_buffer);

      bool ist_dimmer = (id(gewaehlter_raum) == 0 || id(gewaehlter_raum) == 1 || id(gewaehlter_raum) == 3);
      if (ist_dimmer && id(gewaehltes_geraet) == 0) {
        it.print(20, 140, id(font_icons), Color(255, 255, 0), TextAlign::TOP_LEFT, "\U000F1417");
        it.print(65, 145, id(font_normal), Color::WHITE, TextAlign::TOP_LEFT, "Typ: Dimmer (Licht)");
      } else if (id(gewaehlter_raum) == 4) {
        it.print(20, 140, id(font_icons), Color(0, 255, 255), TextAlign::TOP_LEFT, "\U000F0D84");
        it.print(65, 145, id(font_normal), Color::WHITE, TextAlign::TOP_LEFT, "Typ: Poolpumpe");
      } else {
        it.print(20, 140, id(font_icons), Color(144, 238, 144), TextAlign::TOP_LEFT, "\U000F0312");
        it.print(65, 145, id(font_normal), Color::WHITE, TextAlign::TOP_LEFT, "Typ: Schalter (Licht)");
      }

      if (id(aktueller_modus) == 2) {
        char wert_buffer[16]; 
        snprintf(wert_buffer, sizeof(wert_buffer), "%d %%", id(einstell_wert));
        it.print(160, 200, id(font_large), Color(255, 165, 0), TextAlign::TOP_CENTER, wert_buffer);
      }

font:
  - file: "gandhi_sans.ttf"
    id: font_normal
    size: 18
    glyphs: " !_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,:-%ÄÖÜäöüß()"
  - file: "gandhi_sans.ttf"
    id: font_large
    size: 32
    glyphs: " !_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,:-%ÄÖÜäöüß()"
  - file: "materialdesignicons-webfont.ttf"
    id: font_icons
    size: 32
    glyphs:
      - "\U000F02DC"
      - "\U000F0190"
      - "\U000F009F"
      - "\U000F05A0"
      - "\U000F0538"
      - "\U000F0604"
      - "\U000F03AF"
      - "\U000F1417"
      - "\U000F0312"
      - "\U000F0D84"

Verwende Code mit Vorsicht.


:floppy_disk: Part 2: Automations, Home Assistant Links & Encoder Registers

Append this directly to the end of the script:

yaml

globals:
  - id: aktueller_modus
    type: int
    initial_value: '0' 
  - id: gewaehlter_raum
    type: int
    initial_value: '0' 
  - id: gewaehltes_geraet
    type: int
    initial_value: '0' 
  - id: einstell_wert
    type: int
    initial_value: '50'
  - id: rgb_r
    type: int
    initial_value: '255'
  - id: rgb_g
    type: int
    initial_value: '255'
  - id: rgb_b
    type: int
    initial_value: '255'

sensor:
  - platform: homeassistant
    id: ha_brightness_wohnzimmer
    entity_id: light.wohnzimmer_hauptlicht
    attribute: brightness
  - platform: homeassistant
    id: ha_brightness_kueche
    entity_id: light.kueche_led_stripe
    attribute: brightness
  - platform: homeassistant
    id: ha_brightness_schlafzimmer
    entity_id: light.schlafzimmer_licht
    attribute: brightness

text_sensor:
  - platform: homeassistant
    id: led_stripe_farbe
    entity_id: light.kueche_led_stripe
    attribute: rgb_color
  - platform: homeassistant
    id: ha_status_esszimmer
    entity_id: light.esszimmer_lampe
  - platform: homeassistant
    id: ha_status_pool
    entity_id: switch.pool_pumpe
  - platform: homeassistant
    id: ha_status_terrasse_west
    entity_id: light.terrasse_west_wand
  - platform: homeassistant
    id: ha_status_terrasse_sued
    entity_id: light.terrasse_sued_spot

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO38
      inverted: true
    name: "M5Stack Button B (Back)"
    on_press:
      then:
        - lambda: |-
            if (id(aktueller_modus) > 0) id(aktueller_modus)--;

rtttl:
  output: buzzer_output
  id: rtttl_player

script:
  - id: toggle_esszimmer
    then:
      - homeassistant.service:
          service: light.toggle
          data:
            entity_id: light.esszimmer_lampe
  - id: toggle_pool
    then:
      - homeassistant.service:
          service: switch.toggle
          data:
            entity_id: switch.pool_pumpe
  - id: toggle_terrasse_west
    then:
      - homeassistant.service:
          service: light.toggle
          data:
            entity_id: light.terrasse_west_wand
  - id: toggle_terrasse_sued
    then:
      - homeassistant.service:
          service: light.toggle
          data:
            entity_id: light.toggle_terrasse_sued
  - id: dimmer_kueche
    then:
      - homeassistant.service:
          service: light.turn_on
          data:
            entity_id: light.kueche_led_stripe
            brightness_pct: !lambda "return id(einstell_wert);"
  - id: dimmer_schlafzimmer
    then:
      - homeassistant.service:
          service: light.turn_on
          data:
            entity_id: light.schlafzimmer_licht
            brightness_pct: !lambda "return id(einstell_wert);"
  - id: dimmer_wohnzimmer
    then:
      - homeassistant.service:
          service: light.turn_on
          data:
            entity_id: light.wohnzimmer_hauptlicht
            brightness_pct: !lambda "return id(einstell_wert);"

interval:
  - interval: 50ms
    then:
      - lambda: |-
          auto setLED = [](uint8_t index, uint8_t r, uint8_t g, uint8_t b) {
            uint8_t data[] = {index, r, g, b};
            id(faces_hardware).write(data, 4);
          };

          auto playTone = [](bool is_click) {
            if (is_click) id(rtttl_player).play("click:d=4,o=6,b=160:32c");
            else id(rtttl_player).play("confirm:d=4,o=6,b=180:16e");
          };

          uint8_t schritte_reg = 0x00; 
          uint8_t schritte_wert = 0;
          id(faces_hardware).read_register(schritte_reg, &schritte_wert, 1);
          int8_t encoder_schritte = (int8_t)schritte_wert;

          if (encoder_schritte != 0) {
            playTone(true);
            if (id(aktueller_modus) == 0) {
              id(gewaehlter_raum) += encoder_schritte;
              if (id(gewaehlter_raum) > 6) id(gewaehlter_raum) = 6;
              if (id(gewaehlter_raum) < 0) id(gewaehlter_raum) = 0;
            } else if (id(aktueller_modus) == 1) {
              id(gewaehltes_geraet) += encoder_schritte;
              if (id(gewaehltes_geraet) > 1) id(gewaehltes_geraet) = 1;
              if (id(gewaehltes_geraet) < 0) id(gewaehltes_geraet) = 0;
            } else if (id(aktueller_modus) == 2) {
              id(einstell_wert) += (encoder_schritte * 5);
              if (id(einstell_wert) > 100) id(einstell_wert) = 100;
              if (id(einstell_wert) < 0) id(einstell_wert) = 0;
            }
          }

          uint8_t button_reg = 0x10;
          uint8_t button_state = 1;
          id(faces_hardware).read_register(button_reg, &button_state, 1);
          static uint8_t vorheriger_status = 1;

          if (button_state == 0 && vorheriger_status == 1) {
            playTone(false);
            if (id(aktueller_modus) == 0) {
              id(gewaehltes_geraet) = 0;
              id(aktueller_modus) = 1;
            } else if (id(aktueller_modus) == 1) {
              switch(id(gewaehlter_raum)) {
                case 0: 
                  if (id(gewaehltes_geraet) == 0) {
                    if (id(ha_brightness_kueche).has_state()) id(einstell_wert) = (id(ha_brightness_kueche).state * 100) / 255; else id(einstell_wert) = 0;
                    id(aktueller_modus) = 2;
                  }
                  break;
                case 1: 
                  if (id(gewaehltes_geraet) == 0) {
                    if (id(ha_brightness_schlafzimmer).has_state()) id(einstell_wert) = (id(ha_brightness_schlafzimmer).state * 100) / 255; else id(einstell_wert) = 0;
                    id(aktueller_modus) = 2;
                  }
                  break;
                case 2: 
                  id(toggle_esszimmer)->execute();
                  break;
                case 3: 
                  if (id(gewaehltes_geraet) == 0) {
                    if (id(ha_brightness_wohnzimmer).has_state()) id(einstell_wert) = (id(ha_brightness_wohnzimmer).state * 100) / 255; else id(einstell_wert) = 0;
                    id(aktueller_modus) = 2;
                  }
                  break;
                case 4: 
                  id(toggle_pool)->execute();
                  break;
                case 5: 
                  if (id(gewaehltes_geraet) == 0) id(toggle_terrasse_west)->execute();
                  break;
                case 6: 
                  if (id(gewaehltes_geraet) == 0) id(toggle_terrasse_sued)->execute();
                  break;
              }
            } else if (id(aktueller_modus) == 2) {
              if (id(gewaehlter_raum) == 0 && id(gewaehltes_geraet) == 0) id(dimmer_kueche)->execute();
              if (id(gewaehlter_raum) == 1 && id(gewaehltes_geraet) == 0) id(dimmer_schlafzimmer)->execute();
              if (id(gewaehlter_raum) == 3 && id(gewaehltes_geraet) == 0) id(dimmer_wohnzimmer)->execute();
              id(aktueller_modus) = 1;
            }
          }
          vorheriger_status = button_state;

          static int last_wert = -1, last_modus = -1, last_raum = -1;
          if (id(einstell_wert) != last_wert || id(aktueller_modus) != last_modus || id(gewaehlter_raum) != last_raum) {
            last_wert = id(einstell_wert); last_modus = id(aktueller_modus); last_raum = id(gewaehlter_raum);
            for (int i = 0; i < 12; i++) {
              if (id(aktueller_modus) == 0) {
                if (i == (id(gewaehlter_raum) * 11) / 6) setLED(i, 0, 102, 204); else setLED(i, 0, 0, 0);
              } else if (id(aktueller_modus) == 1) {
                if (i == id(gewaehltes_geraet) * 6) setLED(i, 0, 200, 100); else setLED(i, 0, 0, 0);
              } else if (id(aktueller_modus) == 2) {
                int leds_to_light = (id(einstell_wert) * 12) / 100;
                if (i < leds_to_light) setLED(i, 255, 150, 0); else setLED(i, 0, 0, 0);
              }
            }
          }

Verwende Code mit Vorsicht.

1 Like