M5Stack Tab5

M5Stack-Tab5

The M5Stack Tab5 is a development board for the ESP32-P4 microprocessor. It features a nice touchscreen display, a speaker, microphones, camera, uSD card, usb-a, usb-c, 3.5mm headphone, lithium-ion battery, and a host of expansion headers.

The espp::M5StackTab5 component provides a singleton hardware abstraction for initializing the touch, display, and audio subsystems.

API Reference

Header File

Classes

class M5StackTab5 : public espp::BaseComponent

The M5StackTab5 class provides an interface to the M5Stack Tab5 development board.

The class provides access to the following features:

  • 5” 720p MIPI-DSI Display with GT911 multi-touch

  • Dual audio codecs (ES8388 + ES7210 AEC)

  • BMI270 6-axis IMU sensor

  • MIPI-CSI camera (SC202CS) via the esp_video (V4L2) pipeline

  • ESP32-C6 wireless module (Wi-Fi 6, Thread, ZigBee)

  • USB-A Host and USB-C OTG ports

  • RS-485 industrial interface (not yet implemented)

  • Grove and M5-Bus expansion headers (not yet implemented)

  • microSD card slot

  • NP-F550 removable battery with battery management via INA226

  • Real-time clock (RX8130CE)

  • Multiple buttons and interrupts

The class is a singleton and can be accessed using the get() method.

Example

  espp::M5StackTab5 &tab5 = espp::M5StackTab5::get();
  // tab5.set_log_level(espp::Logger::Verbosity::DEBUG);
  logger.info("Running on M5Stack Tab5");

  // first let's get the internal i2c bus and probe for all devices on the bus
  logger.info("Probing internal I2C bus...");
  auto &i2c = tab5.internal_i2c();
  std::vector<uint8_t> found_addresses;
  for (uint8_t address = 1; address < 128; address++) {
    if (i2c.probe_device(address)) {
      found_addresses.push_back(address);
    }
  }
  logger.info("Found devices at addresses: {::#02x}", found_addresses);

  // Initialize the IO expanders
  logger.info("Initializing IO expanders...");
  if (!tab5.initialize_io_expanders()) {
    logger.error("Failed to initialize IO expanders!");
    return;
  }

  logger.info("Initializing lcd...");
  // initialize the LCD
  if (!tab5.initialize_lcd()) {
    logger.error("Failed to initialize LCD!");
    return;
  }

  // initialize the display with a full-frame pixel buffer (Tab5 is 1280x720 with
  // 2 bytes per pixel). A full-frame LVGL draw buffer (allocated in PSRAM, see
  // initialize_display) lets a full-screen redraw - e.g. a rotation change -
  // flush in a single pass instead of ~72 ten-line strips, so the screen
  // repaints at once rather than wiping progressively. Partial updates still
  // only render/flush their dirty area, so the larger buffer costs nothing
  // there (just PSRAM).
  logger.info("Initializing display...");
  auto pixel_buffer_size = tab5.display_width() * tab5.display_height();
  if (!tab5.initialize_display(pixel_buffer_size)) {
    logger.error("Failed to initialize display!");
    return;
  }

  // make the filter we'll use for the IMU to compute the orientation
  static constexpr float angle_noise = 0.001f;
  static constexpr float rate_noise = 0.1f;
  static espp::KalmanFilter<2> kf;
  kf.set_process_noise(rate_noise);
  kf.set_measurement_noise(angle_noise);
  static constexpr float beta = 0.5f; // higher = more accelerometer, lower = more gyro
  static espp::MadgwickFilter f(beta);

  using Imu = espp::M5StackTab5::Imu;
  auto kalman_filter_fn = [](float dt, const Imu::Value &accel,
                             const Imu::Value &gyro) -> Imu::Value {
    // Apply Kalman filter
    float accelRoll = atan2(accel.y, accel.z);
    float accelPitch = atan2(-accel.x, sqrt(accel.y * accel.y + accel.z * accel.z));
    kf.predict({espp::deg_to_rad(gyro.x), espp::deg_to_rad(gyro.y)}, dt);
    kf.update({accelRoll, accelPitch});
    float roll, pitch;
    std::tie(roll, pitch) = kf.get_state();
    // return the computed orientation
    Imu::Value orientation{};
    orientation.roll = roll;
    orientation.pitch = pitch;
    orientation.yaw = 0.0f;
    return orientation;
  };

  auto madgwick_filter_fn = [](float dt, const Imu::Value &accel,
                               const Imu::Value &gyro) -> Imu::Value {
    // Apply Madgwick filter
    f.update(dt, accel.x, accel.y, accel.z, espp::deg_to_rad(gyro.x), espp::deg_to_rad(gyro.y),
             espp::deg_to_rad(gyro.z));
    float roll, pitch, yaw;
    f.get_euler(roll, pitch, yaw);
    // return the computed orientation
    Imu::Value orientation{};
    orientation.roll = espp::deg_to_rad(roll);
    orientation.pitch = espp::deg_to_rad(pitch);
    orientation.yaw = espp::deg_to_rad(yaw);
    return orientation;
  };

  logger.info("Initializing IMU...");
  // initialize the IMU
  if (!tab5.initialize_imu(kalman_filter_fn)) {
    logger.error("Failed to initialize IMU!");
    return;
  }

  // initialize the uSD card
  using SdCardConfig = espp::M5StackTab5::SdCardConfig;
  SdCardConfig sdcard_config{};
  if (!tab5.initialize_sdcard(sdcard_config)) {
    logger.warn("Failed to initialize uSD card, there may not be a uSD card inserted!");
  } else {
    uint32_t size_mb = 0;
    uint32_t free_mb = 0;
    if (tab5.get_sd_card_info(&size_mb, &free_mb)) {
      logger.info("uSD card size: {} MB, free space: {} MB", size_mb, free_mb);
    } else {
      logger.warn("Failed to get uSD card info");
    }
  }

  logger.info("Initializing RTC...");
  // initialize the RTC
  if (!tab5.initialize_rtc()) {
    logger.error("Failed to initialize RTC!");
    return;
  }

  auto current_time = std::tm{};
  if (!tab5.get_rtc_time(current_time)) {
    logger.error("Failed to get RTC time");
    return;
  }

  // only set the time if the year is before 2024
  if (current_time.tm_year < 124) {
    // set the RTC time to a known value (2024-01-15 14:30:45)
    // Set time using std::tm
    std::tm time = {};
    time.tm_year = 124; // 2024 - 1900
    time.tm_mon = 0;    // January (0-based)
    time.tm_mday = 15;  // 15th
    time.tm_hour = 14;  // 2 PM
    time.tm_min = 30;
    time.tm_sec = 45;
    time.tm_wday = 1; // Monday
    if (!tab5.set_rtc_time(time)) {
      logger.error("Failed to set RTC time");
      return;
    }
  } else {
    logger.info("RTC time is already set to a valid value {:%Y-%m-%d %H:%M:%S}", current_time);
  }

  logger.info("Initializing battery management...");
  // initialize battery monitoring
  if (!tab5.initialize_battery_monitoring()) {
    logger.error("Failed to initialize battery monitoring!");
    return;
  }

  // enable charging
  tab5.set_charging_enabled(true);

  logger.info("Initializing sound...");
  // initialize the sound
  if (!tab5.initialize_audio()) {
    logger.error("Failed to initialize sound!");
    return;
  }

  // unmute the audio and set the volume to 60% (do this before the GUI is
  // created so its volume label shows the right value)
  tab5.mute(false);
  tab5.volume(60.0f);

  // create the GUI: builds the UI (label, buttons, gravity lines, circle
  // layer) and starts the task which updates LVGL. All of its public methods
  // are thread-safe, so the touch callback, button callback, and data display
  // task below can call them directly.
  logger.info("Setting up LVGL UI...");
  static Gui gui({.log_level = espp::Logger::Verbosity::INFO});
  static const std::string instructions =
      fmt::format("Touch the screen to draw!\nPress the {} button to clear circles.\nPress the "
                  "{} button to rotate the display.\nPress the {} button to cycle the "
                  "brightness.\nThe Status and Audio tabs show the other subsystems.",
                  LV_SYMBOL_TRASH, LV_SYMBOL_REFRESH, LV_SYMBOL_EYE_OPEN);
  gui.set_label_text(instructions);

  // Brightness control with the hardware button: cycle through the same
  // 25/50/75/100% levels as the on-screen brightness button
  logger.info("Initializing button...");
  auto button_callback = [&](const auto &state) {
    logger.info("Button state: {}", state.active);
    if (state.active) {
      gui.cycle_brightness();
    }
  };
  if (!tab5.initialize_button(button_callback)) {
    logger.warn("Failed to initialize button");
  }

  // initialize the touchpad; each touch draws a circle (and plays a click
  // sound), while the touchscreen's button clears the circles
  auto touch_callback = [&](const auto &touch) {
    // NOTE: since we're directly using the touchpad data, and not using the
    // TouchpadInput + LVGL, we'll need to ensure the touchpad data is
    // converted into proper screen coordinates instead of simply using the
    // raw values.
    static auto previous_touchpad_data = tab5.touchpad_convert(touch);
    auto touchpad_data = tab5.touchpad_convert(touch);
    if (touchpad_data != previous_touchpad_data) {
      logger.debug("Touch: {}", touchpad_data);
      previous_touchpad_data = touchpad_data;
      // if the button is pressed, clear the circles
      if (touchpad_data.btn_state) {
        gui.clear_circles();
      }
      // if there is a touch point on the Draw tab, draw a circle and play a
      // click sound (touches on the other tabs go to their widgets)
      if (touchpad_data.num_touch_points > 0 && gui.draw_page_active()) {
        play_click(tab5);
        gui.draw_circle(touchpad_data.x, touchpad_data.y, 10);
      }
    }
  };
  logger.info("Initializing touch...");
  // NOTE: this example raises the BSP interrupt-task stack size via
  // sdkconfig.defaults (CONFIG_M5STACK_TAB5_INTERRUPT_STACK_SIZE=8192); the
  // touch controller is read from that task and its error-logging path
  // needs more than the 4 KB BSP default. See the example README.
  if (!tab5.initialize_touch(touch_callback)) {
    logger.error("Failed to initialize touch!");
    return;
  }

  // load the audio file (wav file bundled in memory)
  size_t wav_size = 0;
  size_t wav_sample_rate = 0;
  if (!load_audio(wav_size, wav_sample_rate)) {
    logger.error("Failed to load audio file!");
    return;
  }
  logger.info("Loaded {} bytes of audio", wav_size);

  logger.info("Setting audio sample rate to {} Hz", wav_sample_rate);
  tab5.audio_sample_rate(wav_sample_rate);

  // set the brightness to 75%
  tab5.brightness(75.0f);

  // Keep the analog microphone gain modest: the ES7210 front-end develops a
  // high-frequency whine as the analog gain is raised, so the loudness comes
  // from the RMS software makeup gain applied to the recording on stop (see
  // below) rather than from the analog stage. The mic +/- buttons still adjust
  // the analog gain if desired.
  tab5.microphone_volume(40.0f);

  // Allocate the recording buffer (16-bit interleaved stereo at the current
  // sample rate): prefer PSRAM, fall back to a couple of seconds in internal
  // RAM
  size_t bytes_per_second = tab5.audio_sample_rate() * 2 * sizeof(int16_t);
  recording_capacity = MAX_RECORDING_SECONDS * bytes_per_second;
  recording_buffer = static_cast<uint8_t *>(
      heap_caps_malloc(recording_capacity, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
  if (recording_buffer == nullptr) {
    recording_capacity = FALLBACK_RECORDING_SECONDS * bytes_per_second;
    recording_buffer =
        static_cast<uint8_t *>(heap_caps_malloc(recording_capacity, MALLOC_CAP_8BIT));
  }
  if (recording_buffer == nullptr) {
    gui.set_audio_status("No recording buffer");
    logger.warn("Could not allocate a recording buffer; recording disabled");
    recording_capacity = 0;
  } else {
    logger.info("Recording buffer: {} KB ({} s at {} Hz stereo)", recording_capacity / 1024,
                recording_capacity / bytes_per_second, tab5.audio_sample_rate());
  }

  // The recording callback appends the recorded stereo frames to the buffer
  // and auto-stops when it is full (the main loop notices and updates the
  // GUI)
  auto record_data_callback = [](const uint8_t *data, size_t length) {
    if (!recording) {
      return;
    }
    size_t offset = recording_len;
    size_t to_copy = std::min(length, recording_capacity - offset);
    if (to_copy > 0) {
      memcpy(recording_buffer + offset, data, to_copy);
      recording_last_us = esp_timer_get_time();
      recording_len = offset + to_copy;
    }
    if (recording_len >= recording_capacity) {
      recording = false;
    }
  };

  // The record button toggles recording; the play button toggles playback of
  // the recording (streamed to the speaker by the main loop)
  gui.set_record_callback([&]() {
    if (recording_capacity == 0) {
      logger.warn("Recording unavailable (no buffer)");
      gui.set_audio_status("Recording unavailable (see log)");
      return;
    }
    if (recording) {
      recording = false; // the main loop stops the BSP recording and logs
    } else {
      playing = false;
      gui.set_play_active(false);
      recording_len = 0;
      recording_start_us = esp_timer_get_time();
      recording_last_us = recording_start_us.load();
      recording = true;
      tab5.start_audio_recording(record_data_callback);
      gui.set_record_active(true);
      gui.set_audio_status("Recording...");
    }
  });
  gui.set_play_callback([&]() {
    if (playing) {
      playing = false;
      gui.set_play_active(false);
      gui.set_audio_status("Playback stopped");
    } else if (recording_len > 0) {
      recording = false;
      playing = true;
      gui.set_play_active(true);
      gui.set_audio_status("Playing...");
    } else {
      logger.info("Nothing recorded yet; press the record button first");
      gui.set_audio_status("Nothing recorded yet");
    }
  });

  // make a task to read out various data such as IMU, battery monitoring, etc.
  // and print it to screen
  logger.info("Starting data display task...");
  espp::Task imu_task(
      {.callback = [&](std::mutex &m, std::condition_variable &cv) -> bool {
         // sleep first in case we don't get IMU data and need to exit early
         {
           std::unique_lock<std::mutex> lock(m);
           cv.wait_for(lock, 10ms);
         }
         static auto &tab5 = espp::M5StackTab5::get();
         static auto imu = tab5.imu();

         // Update the Date/Time from the RTC
         std::tm rtc_time;
         std::string rtc_text = "";
         if (tab5.get_rtc_time(rtc_time)) {
           rtc_text = fmt::format("\n{:%Y-%m-%d %H:%M:%S}\n", rtc_time);
         }

         // Update the battery status
         auto battery_status = tab5.read_battery_status();
         std::string battery_text =
             fmt::format("\nBattery: {:0.2f} V, {:0.1f} mA, {:0.1f} %, Charging: {}\n",
                         battery_status.voltage_v, battery_status.current_ma,
                         battery_status.charge_percent, battery_status.is_charging ? "Yes" : "No");

         auto now = esp_timer_get_time(); // time in microseconds
         static auto t0 = now;
         auto t1 = now;
         float dt = (t1 - t0) / 1'000'000.0f; // convert us to s
         t0 = t1;

         // Update the IMU data
         std::error_code ec;
         // update the imu data
         if (!imu->update(dt, ec)) {
           return false;
         }
         // get accel
         auto accel = imu->get_accelerometer();
         auto gyro = imu->get_gyroscope();
         auto temp = imu->get_temperature();
         auto orientation = imu->get_orientation();
         auto gravity_vector = imu->get_gravity_vector();
         // invert the axes to convert from the sensor frame to the display's
         // natural (unrotated) frame
         gravity_vector.y = -gravity_vector.y;
         gravity_vector.x = -gravity_vector.x;

         // separator for imu
         std::string imu_text = "\nIMU Data:\n";
         imu_text += fmt::format("Accel: {:02.2f} {:02.2f} {:02.2f}\n", accel.x, accel.y, accel.z);
         imu_text += fmt::format("Gyro: {:03.2f} {:03.2f} {:03.2f}\n", espp::deg_to_rad(gyro.x),
                                 espp::deg_to_rad(gyro.y), espp::deg_to_rad(gyro.z));
         imu_text += fmt::format("Angle: {:03.2f} {:03.2f}\n", espp::rad_to_deg(orientation.roll),
                                 espp::rad_to_deg(orientation.pitch));
         imu_text += fmt::format("Temp: {:02.1f} C\n", temp);

         // Now show the madgwick filter's estimate of "down"
         auto madgwick_orientation = madgwick_filter_fn(dt, accel, gyro);
         float roll = madgwick_orientation.roll;
         float pitch = madgwick_orientation.pitch;
         float vx = sin(pitch);
         float vy = -cos(pitch) * sin(roll);

         // invert the axes to convert from the sensor frame to the display's
         // natural (unrotated) frame
         vx = -vx;
         vy = -vy;

         std::string text;
         text += battery_text;
         text += rtc_text;
         text += imu_text;

         // update the GUI with the new data; the Gui handles remapping the
         // vectors for the current display rotation
         gui.set_status_text(text);
         gui.set_kalman_down(gravity_vector.x, gravity_vector.y);
         gui.set_madgwick_down(vx, vy);

         return false;
       },
       .task_config = {
           .name = "Data Display Task",
           .stack_size_bytes = 6 * 1024,
           .priority = 10,
           .core_id = 1,
       }});
  imu_task.start();

  // Initialize the on-board MIPI-CSI camera and stream its frames to the Camera
  // tab. The BSP runs a capture task that hands each RGB565 frame to this
  // callback; forward it to the thread-safe GUI. Non-fatal: the rest of the
  // example still runs if the camera is unavailable.
  logger.info("Initializing camera...");
  if (!tab5.initialize_camera(
          [&](const uint8_t *data, int w, int h, size_t) { gui.set_camera_frame(data, w, h); })) {
    logger.warn("Failed to initialize camera; the Camera tab will stay blank");
  }

  // Main loop: stream any active playback to the speaker and notice when a
  // recording stops (either button press or the buffer filling up)
  size_t play_offset = 0;
  bool was_recording = false;
  while (true) {
    // feed the active playback in chunks, advancing by however much the
    // speaker's stream buffer accepted
    if (playing) {
      size_t len = recording_len;
      if (play_offset >= len) {
        playing = false;
        play_offset = 0;
        gui.set_play_active(false);
        gui.set_audio_status("Playback done");
        logger.info("Playback done");
      } else {
        play_offset += tab5.play_audio(recording_buffer + play_offset,
                                       std::min<size_t>(len - play_offset, 16384));
      }
    } else {
      play_offset = 0;
    }
    // notice when the recording stopped (button press or buffer full)
    bool now_recording = recording;
    if (was_recording && !now_recording) {
      tab5.stop_audio_recording();
      gui.set_record_active(false);
      gui.set_audio_status(fmt::format("Recorded {:.1f}s ({} plays)",
                                       static_cast<float>(recording_len) /
                                           (tab5.audio_sample_rate() * 2 * sizeof(int16_t)),
                                       LV_SYMBOL_PLAY));
      // report the measured capture rate: stereo frames recorded over the
      // wall clock they took to arrive should match the nominal sample rate
      size_t num_frames = recording_len / (2 * sizeof(int16_t));
      float elapsed_s = static_cast<float>(recording_last_us - recording_start_us) / 1e6f;
      float effective_hz = elapsed_s > 0.0f ? num_frames / elapsed_s : 0.0f;
      // Post-process the recording for playback on the Tab5's MONO speaker.
      // The ES7210 records 16-bit interleaved stereo (mic 1 -> left slot, mic 2
      // -> right); downmix each frame to the average (the speaker plays one I2S
      // slot, so the result is written to both). The captured level is low, so
      // rather than driving the analog gain hot (which whines), remove the DC
      // offset and apply an RMS-normalized software makeup gain: this makes the
      // recording play back at a consistent, audible level comparable to the
      // click WAV, without a high analog mic gain or a high speaker volume.
      auto *samples = reinterpret_cast<int16_t *>(recording_buffer);
      int16_t peak = 0;
      int64_t sum = 0;
      for (size_t i = 0; i < num_frames; i++) {
        int32_t mono = (static_cast<int32_t>(samples[2 * i]) + samples[2 * i + 1]) / 2;
        samples[2 * i] = static_cast<int16_t>(mono);
        peak = std::max<int16_t>(peak, static_cast<int16_t>(std::abs(mono)));
        sum += mono;
      }
      int32_t dc = num_frames ? static_cast<int32_t>(sum / static_cast<int64_t>(num_frames)) : 0;
      int64_t sum_sq = 0;
      for (size_t i = 0; i < num_frames; i++) {
        int32_t v = samples[2 * i] - dc;
        sum_sq += static_cast<int64_t>(v) * v;
      }
      double rms = num_frames ? std::sqrt(static_cast<double>(sum_sq) / num_frames) : 0.0;
      // target ~-15 dBFS leaves headroom; cap the gain so a near-silent capture
      // is not blown up into noise
      static constexpr double target_rms = 5500.0;
      double gain = rms > 1.0 ? std::clamp(target_rms / rms, 1.0, 64.0) : 1.0;
      for (size_t i = 0; i < num_frames; i++) {
        int32_t v = static_cast<int32_t>((samples[2 * i] - dc) * gain);
        int16_t mono = static_cast<int16_t>(std::clamp<int32_t>(v, -32768, 32767));
        samples[2 * i] = mono;
        samples[2 * i + 1] = mono;
      }
      logger.info("Recorded {} frames in {:.2f} s (~{:.0f} Hz effective, {} Hz nominal); "
                  "peak={} dc={} rms={:.0f}; applied makeup gain {:.1f}x",
                  num_frames, elapsed_s, effective_hz, tab5.audio_sample_rate(), peak, dc, rms,
                  gain);
    }
    was_recording = now_recording;
    std::this_thread::sleep_for(50ms);
  }

Public Types

enum class DisplayController

Enum for display controller type.

Values:

enumerator UNKNOWN
enumerator ILI9881
enumerator ST7123
enum class ExpansionPort

Expansion port configuration.

Values:

enumerator GROVE

Grove connector.

enumerator M5_BUS

M5-Bus connector.

enumerator STAMP

STAMP expansion pads.

enumerator GPIO_EXT

GPIO extension header.

enum class CameraScale

Camera preview scale: the PPA downscales each frame by this factor before it is delivered, trading resolution for lower per-frame CPU / PSRAM cost.

Values:

enumerator FULL

No downscale (native, e.g. 1280x720) - sharpest, heaviest.

enumerator HALF

1/2 (e.g. 640x360) - the default

enumerator QUARTER

1/4 (e.g. 320x180) - lightest

using button_callback_t = espp::Interrupt::event_callback_fn

Alias for the button callback function.

using IoExpander = espp::Pi4ioe5v

Alias for the I/O Expanders (IOX) used by the Tab5.

using Pixel = lv_color16_t

Alias for the pixel type used by the Tab5 display.

using DisplayDriver = espp::display_drivers::Controller

Alias for the low-level display driver interface.

using TouchDriver = espp::Gt911

Alias for the GT911 touch controller used by the Tab5 (ILI9881 variant)

using St7123TouchDriver = espp::St7123Touch

Alias for the ST7123 integrated touch controller (ST7123 variant)

using TouchpadData = espp::TouchpadData

Alias for the touchpad data used by the Tab5 touchpad.

using Rtc = espp::Rx8130ce<>

Alias for the RTC used by the Tab5.

using Imu = espp::Bmi270<espp::bmi270::Interface::I2C>

Alias the IMU used by the Tab5.

using BatteryMonitor = espp::Ina226

Alias the INA226 battery power monitor.

using touch_callback_t = std::function<void(const TouchpadData&)>

Alias for the touch callback when touch events are received.

using camera_frame_callback_t = std::function<void(const uint8_t *data, int width, int height, size_t length)>

Alias for the camera frame callback. Called from the camera task with each captured frame: data is the pixel buffer (RGB565, width x height), valid only for the duration of the callback, and length is its size in bytes. Copy the data if it needs to outlive the call.

Public Functions

inline DisplayController get_display_controller() const

Get the detected display controller type

Returns:

The display controller type

inline const char *get_display_controller_name() const

Get a string name for the display controller

Returns:

String name of the controller

inline const char *get_display_controller_name(DisplayController controller) const

Get a string name for the display controller

Returns:

String name of the controller

inline I2c &internal_i2c()

Get a reference to the internal I2C bus

Note

The internal I2C bus is used for touchscreen, audio codecs, IMU, RTC, and power monitoring

Returns:

A reference to the internal I2C bus

inline espp::Interrupt &interrupts()

Get a reference to the interrupts

Returns:

A reference to the interrupts

bool initialize_lcd()

Initialize the LCD (low level display driver, MIPI-DSI + ST7703)

Returns:

true if the LCD was successfully initialized, false otherwise

bool initialize_display(size_t pixel_buffer_size = 1280 * 720 / 10)

Initialize the LVGL display

Parameters:

pixel_buffer_size – The size of the pixel buffer

Returns:

true if the display was successfully initialized, false otherwise

bool initialize_touch(const touch_callback_t &callback = nullptr)

Initialize the GT911 multi-touch controller

Parameters:

callback – The touchpad callback

Returns:

true if the touchpad was successfully initialized, false otherwise

inline size_t bytes_per_pixel() const

Get the number of bytes per pixel for the display

Returns:

The number of bytes per pixel

inline std::shared_ptr<TouchpadInput> touchpad_input() const

Get the touchpad input

Returns:

A shared pointer to the touchpad input

inline TouchpadData touchpad_data() const

Get the most recent touchpad data

Returns:

The touchpad data

void touchpad_read(uint8_t *num_touch_points, uint16_t *x, uint16_t *y, uint8_t *btn_state)

Get the touchpad data for LVGL integration

Parameters:
  • num_touch_points – The number of touch points

  • x – The x coordinate

  • y – The y coordinate

  • btn_state – The button state (0 = button released, 1 = button pressed)

TouchpadData touchpad_convert(const TouchpadData &data) const

Convert touchpad data from raw reading to display coordinates

Note

Uses the touch_invert_x and touch_invert_y settings to determine if the x and y coordinates should be inverted

Parameters:

data – The touchpad data to convert

Returns:

The converted touchpad data

void brightness(float brightness)

Set the display brightness

Parameters:

brightness – The brightness as a percentage (0-100)

float brightness() const

Get the display brightness

Returns:

The brightness as a percentage (0-100)

void set_backlight_enabled(bool enable)

Enable/disable the LCD backlight (routes through IO expander if mapped)

std::optional<bool> is_backlight_enabled() const

Query backlight enable state if readable

Returns:

true if enabled, false if disabled; std::nullopt if unknown

size_t rotated_display_width() const

Get the display width in pixels, according to the current orientation

Returns:

The display width in pixels, according to the current orientation

size_t rotated_display_height() const

Get the display height in pixels, according to the current orientation

Returns:

The display height in pixels, according to the current orientation

inline const std::shared_ptr<DisplayDriver> &display_driver() const

Get a shared pointer to the low-level display driver

Returns:

A shared pointer to the display driver

void write_lcd_lines(int xs, int ys, int xe, int ye, const uint8_t *data, uint32_t user_data)

Write lines to the LCD

Note

This method queues the panel transfer asynchronously and may return before the write has completed.

Parameters:
  • xs – The x start coordinate

  • ys – The y start coordinate

  • xe – The x end coordinate

  • ye – The y end coordinate

  • data – The data to write

  • user_data – User data to pass to the lower-level transport

bool initialize_audio (uint32_t sample_rate=48000, const espp::Task::BaseConfig &task_config={ .name="tab5_audio",.stack_size_bytes=CONFIG_M5STACK_TAB5_AUDIO_TASK_STACK_SIZE,.priority=20,.core_id=1})

Initialize the dual audio system (ES8388 codec + ES7210 AEC)

Parameters:
  • sample_rate – The audio sample rate (default 48kHz)

  • task_config – The task configuration for the audio task

Returns:

true if the audio system was successfully initialized, false otherwise

void enable_audio(bool enable)

Enable or disable the audio system

Parameters:

enable – True to enable, false to disable

void volume(float volume)

Set the audio volume

Parameters:

volume – The volume as a percentage (0-100)

float volume() const

Get the audio volume

Returns:

The volume as a percentage (0-100)

void mute(bool mute)

Mute or unmute the audio

Parameters:

mute – True to mute, false to unmute

bool is_muted() const

Check if audio is muted

Returns:

True if muted, false otherwise

uint32_t audio_sample_rate() const

Get the audio sample rate

Returns:

The audio sample rate, in Hz

void audio_sample_rate(uint32_t sample_rate)

Set the audio sample rate

Parameters:

sample_rate – The audio sample rate, in Hz

size_t audio_buffer_size() const

Get the audio buffer size

Returns:

The audio buffer size, in bytes

size_t play_audio(const uint8_t *data, uint32_t num_bytes)

Play audio data

Note

This function is non-blocking and queues the data for the audio task to play; to stream data larger than the internal buffer, call it repeatedly, advancing by the returned number of bytes

Note

Must be called from task context, not from an ISR.

Parameters:
  • data – The audio data to play (16-bit signed interleaved stereo)

  • num_bytes – The number of bytes to play

Returns:

The number of bytes actually queued (may be less than num_bytes if the internal stream buffer is full)

size_t play_audio(std::span<const uint8_t> data)

Play audio data

Note

This function is non-blocking and queues the data for the audio task to play; to stream data larger than the internal buffer, call it repeatedly, advancing by the returned number of bytes

Note

Must be called from task context, not from an ISR.

Parameters:

data – The audio data to play (16-bit signed interleaved stereo)

Returns:

The number of bytes actually queued (may be less than the data size if the internal stream buffer is full)

bool start_audio_recording(std::function<void(const uint8_t *data, size_t length)> callback)

Start recording audio

Note

The callback runs in the dedicated microphone task’s context (a separate task from audio playback), so keep it quick and non- blocking and size that task’s stack accordingly

Parameters:

callback – Function to call with recorded audio data: 16-bit signed interleaved stereo (ES7210 microphone 1 on the left slot, microphone 2 on the right) at audio_sample_rate()

Returns:

True if recording started successfully

void stop_audio_recording()

Stop recording audio.

void microphone_volume(float volume)

Set the microphone volume

Parameters:

volume – The volume as a percentage (0 - 100), mapped onto the ES7210 analog microphone gain range (0 dB - +37.5 dB)

float microphone_volume() const

Get the microphone volume

Returns:

The microphone volume as a percentage (0 - 100)

bool initialize_camera (const camera_frame_callback_t &callback, const espp::Task::BaseConfig &task_config={.name="tab5_camera",.stack_size_bytes=6 *1024,.priority=5,.core_id=0})

Initialize the on-board MIPI-CSI camera and start streaming frames.

Brings up the ESP32-P4 camera pipeline (MIPI-CSI receiver + ISP + sensor) via the esp_video (V4L2) framework, configures the ISP to output RGB565, and starts a task that delivers each captured frame to callback. The camera sensor’s SCCB shares the internal I2C bus.

Note

The internal I2C bus must be initialized first (the sensor is on it); initialize_io_expanders() must also have run (camera reset is on an IO expander). The callback runs in the camera task’s context.

Parameters:
  • callback – Function called from the camera task with each RGB565 frame (see camera_frame_callback_t). Keep it quick and non-blocking.

  • task_config – The configuration for the camera task

Returns:

true if the camera was successfully initialized and streaming started, false otherwise

void stop_camera()

Stop the camera stream and release the camera pipeline.

uint16_t camera_width() const

Get the width of the captured camera frames, in pixels

Returns:

The camera frame width (0 if the camera is not initialized)

uint16_t camera_height() const

Get the height of the captured camera frames, in pixels

Returns:

The camera frame height (0 if the camera is not initialized)

bool camera_reset(bool assert_reset)

Assert or release the camera reset line (IO expander 0x43 P6, active-low)

Parameters:

assert_reset – True to hold the camera in reset, false to release it

Returns:

true if the IO expander write succeeded, false otherwise

void set_camera_controls(const CameraControls &controls)

Update the camera controls. Thread-safe: the change is applied by the camera task on its next iteration, so this is safe to call from the GUI.

Parameters:

controls – The desired controls

CameraControls camera_controls() const

Get the current (last requested) camera controls.

Returns:

The camera controls

bool initialize_imu(const Imu::filter_fn &orientation_filter = nullptr)

Initialize the BMI270 6-axis IMU

Parameters:

orientation_filter – Optional orientation filter function

Returns:

True if IMU was successfully initialized

inline std::shared_ptr<Imu> imu() const

Get the IMU instance

Returns:

Shared pointer to the IMU

bool initialize_battery_monitoring()

Initialize battery monitoring (INA226)

Returns:

True if battery monitoring was successfully initialized

BatteryStatus read_battery_status()

Get the latest battery status from the INA226

Returns:

Battery status structure

BatteryStatus get_battery_status() const

Get the most recent cached battery status

Note

This does not read from the INA226, use read_battery_status() to get the latest data

Returns:

Battery status structure

void enable_battery_charging(bool enable)

Enable or disable battery charging

Parameters:

enable – True to enable charging, false to disable

inline std::shared_ptr<BatteryMonitor> battery_monitor() const

Get the Battery Monitor Instance (INA226)

Returns:

Shared pointer to the battery monitor

bool initialize_rtc()

Initialize the RX8130CE real-time clock

Returns:

True if RTC was successfully initialized

bool set_rtc_time(uint64_t unix_timestamp)

Set the RTC time

Parameters:

unix_timestamp – Unix timestamp to set

Returns:

True if time was set successfully

bool set_rtc_time(const std::tm &time)

Set the RTC time

Parameters:

time – The time to set

Returns:

True if time was set successfully

bool get_rtc_time(std::tm &time)

Get the RTC time

Parameters:

time – The time structure to fill

Returns:

True if time was retrieved successfully

uint64_t get_unix_time()

Get the RTC time

Returns:

Unix timestamp, or 0 if RTC not initialized

bool set_rtc_wakeup(uint32_t seconds_from_now)

Enable RTC wake-up interrupt

Parameters:

seconds_from_now – Seconds from now to wake up

Returns:

True if wake-up was set successfully

inline std::shared_ptr<Rtc> rtc() const

Get the RTC instance

Returns:

Shared pointer to the RTC

bool initialize_button(const button_callback_t &callback = nullptr)

Initialize the button

Parameters:

callback – The callback function to call when pressed

Returns:

True if button was successfully initialized

bool button_state() const

Get the button state

Returns:

True if pressed, false otherwise

bool initialize_io_expanders()

Initialize the on-board IO expanders at addresses 0x43 and 0x44 Configures required directions and safe default output states.

bool lcd_reset(bool assert_reset)

Control the LCD reset (active-low) routed via IO expander (0x43 P4)

Parameters:

assert_reset – True drives reset low; false releases reset high.

Returns:

true on success

bool touch_reset(bool assert_reset)

Control the GT911 touch reset (active-low) via IO expander (0x43 P5)

Parameters:

assert_reset – True drives reset low; false releases reset high.

Returns:

true on success

bool set_speaker_enabled(bool enable)

Enable/disable the speaker amplifier (NS4150B SPK_EN on 0x43 P1)

Parameters:

enable – True to enable speaker, false to disable

Returns:

true on success

bool set_charging_enabled(bool enable)

Enable/disable battery charging (IP2326 CHG_EN on 0x44 P7)

Parameters:

enable – True to enable charging, false to disable

Returns:

true on success

bool get_charging_status()

Read battery charging status (IP2326 CHG_STAT on 0x44 P6) Returns true if charging is indicated asserted.

bool set_io_expander_output(uint8_t address, uint8_t bit, bool level)

Generic helpers to control IO expander pins (0x43/0x44) These perform read-modify-write on the output latch.

Parameters:
  • address – 7-bit expander I2C address (e.g. 0x43 or 0x44)

  • bit – Bit index 0..7

  • level – Desired output level

Returns:

true on success

std::optional<bool> get_io_expander_output(uint8_t address, uint8_t bit)

Read a single output bit from the expander output register

Parameters:
  • address – 7-bit expander I2C address (e.g. 0x43 or 0x44)

  • bit – Bit index 0..7

Returns:

std::optional<bool> containing the output state, or std::nullopt on error

std::optional<bool> get_io_expander_input(uint8_t address, uint8_t bit)

Read a single input bit from the expander input register

Parameters:
  • address – 7-bit expander I2C address (e.g. 0x43 or 0x44)

  • bit – Bit index 0..7

Returns:

std::optional<bool> containing the input state, or std::nullopt on error

bool initialize_sdcard(const SdCardConfig &config)

Initialize microSD / uSD card

Parameters:

config – Configuration for the uSD card

Returns:

True if uSD card was successfully initialized

bool is_sd_card_available() const

Check if SD card is present and mounted

Returns:

True if SD card is available

inline sdmmc_card_t *sdcard() const

Get the uSD card

Note

The uSD card is only available if it was successfully initialized and the mount point is valid

Returns:

A pointer to the uSD card

bool get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const

Get SD card info

Parameters:
  • size_mb – Pointer to store size in MB

  • free_mb – Pointer to store free space in MB

Returns:

True if info retrieved successfully

inline const std::string &get_name() const

Get the name of the component

Note

This is the tag of the logger

Returns:

A const reference to the name of the component

inline void set_log_tag(const std::string_view &tag)

Set the tag for the logger

Parameters:

tag – The tag to use for the logger

inline espp::Logger::Verbosity get_log_level() const

Get the log level for the logger

Returns:

The verbosity level of the logger

inline void set_log_level(espp::Logger::Verbosity level)

Set the log level for the logger

Parameters:

level – The verbosity level to use for the logger

inline void set_log_verbosity(espp::Logger::Verbosity level)

Set the log verbosity for the logger

See also

set_log_level

Note

This is a convenience method that calls set_log_level

Parameters:

level – The verbosity level to use for the logger

inline espp::Logger::Verbosity get_log_verbosity() const

Get the log verbosity for the logger

See also

get_log_level

Note

This is a convenience method that calls get_log_level

Returns:

The verbosity level of the logger

inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)

Set the rate limit for the logger

Note

Only calls to the logger that have _rate_limit suffix will be rate limited

Parameters:

rate_limit – The rate limit to use for the logger

Public Static Functions

static inline M5StackTab5 &get()

Access the singleton instance of the M5StackTab5 class.

Returns:

Reference to the singleton instance of the M5StackTab5 class

static inline constexpr size_t display_width()

Get the display width in pixels

Returns:

The display width in pixels

static inline constexpr size_t display_height()

Get the display height in pixels

Returns:

The display height in pixels

Public Static Attributes

static constexpr char mount_point[] = "/sdcard"

Mount point for the uSD card on the TDeck.

struct BatteryStatus

Battery status structure.

Public Members

float voltage_v

Battery voltage in volts.

float current_ma

Battery current in milliamps.

float power_mw

Battery power in milliwatts.

float charge_percent

Estimated charge percentage (0-100)

bool is_charging

True if battery is charging.

bool is_present

True if battery is present.

struct CameraControls

Adjustable camera controls, applied by the camera task.

Only exposes the controls that are effective on the Tab5: the preview scale and the mirror / flip (both done in the PPA). Exposure / white balance / color are driven by the ISP auto pipeline and are not manually overridable here.

Public Members

bool hmirror = {false}

Horizontal mirror.

bool vflip = {false}

Vertical flip.

struct SdCardConfig

Configuration for the uSD card.

Public Members

bool format_if_mount_failed = false

Format the uSD card if mount failed.

int max_files = 5

The maximum number of files to open at once.

size_t allocation_unit_size = 2 * 1024

The allocation unit size in bytes.