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 multi-touch (auto-detects the fitted revision: ILI9881C + GT911 touch, ST7123 TDDI, or ST7121 TDDI)
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 only when a battery is actually in the bay. The NP-F550 is // optional (kit-dependent); with the bay empty, the IP2326 charger keeps // retrying to charge nothing and every retry puts an audible pop on the // speaker (every 5 s to 2 min, idle or not), while charging an inserted pack // is silent. if (auto battery = tab5.read_battery_status(); battery.is_present) { logger.info("Battery detected ({:.2f} V), enabling charging", battery.voltage_v); tab5.set_charging_enabled(true); } else { logger.warn("No battery detected ({:.2f} V on the pack line), leaving charging disabled", battery.voltage_v); tab5.set_charging_enabled(false); } 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; // stream the click (if one was started by a tap) the same way if (click_active) { size_t off = click_offset.load(); if (off >= audio_bytes.size()) { click_active = false; } else { off += tab5.play_audio(audio_bytes.data() + off, std::min<size_t>(audio_bytes.size() - off, 16384)); click_offset = off; } } } // 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; // Pump cadence: the speaker's stream buffer holds a few blocks (about // 70 ms), so top it up well within that or playback runs dry on any // loop jitter. std::this_thread::sleep_for(20ms); }
Public Types
-
enum class DisplayController
Enum for display controller type. The Tab5 has shipped with three display revisions: ILI9881C + separate GT911 touch (original, pre Oct-2025), ST7123 TDDI (integrated touch), and ST7121 TDDI (newest). The two ST parts need different init sequences / DSI timing and are told apart by their touch firmware version (1 = ST7121, 3 = ST7123).
Values:
-
enumerator UNKNOWN
-
enumerator ILI9881
-
enumerator ST7123
-
enumerator ST7121
-
enumerator UNKNOWN
-
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.
-
enumerator GROVE
-
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
-
enumerator FULL
-
using button_callback_t = espp::Interrupt::event_callback_fn
Alias for the button callback function.
-
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 Sitronix integrated touch controller (ST7123 and ST7121 variants — both TDDI parts speak the same touch protocol at I2C 0x55)
-
using TouchState = espp::TouchState
Alias for the multi-touch state (every finger) of the Tab5 touchpad.
-
using TouchpadData = espp::TouchpadData
Alias for the touchpad data used by the Tab5 touchpad.
-
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:
datais the pixel buffer (RGB565,widthxheight), valid only for the duration of the callback, andlengthis 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
-
inline TouchState touch_state() const
Get the most recent multi-touch state: every finger the controller reported, in raw (native) coordinates, as of the same update that produced touchpad_data(). The touch callback may call this to see all the fingers behind the primary point it was handed.
- Returns:
The touch state
-
TouchState touch_state_convert(const TouchState &state) const
Convert a multi-touch state from raw readings to display coordinates (every point, the way touchpad_convert() converts the primary one)
- Parameters:
state – The touch state to convert
- Returns:
The converted touch state
-
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
Synchronous: serialized with the LVGL flush and returns once the panel driver has finished copying `data` (or after a bounded wait if the completion never arrives), so the caller may reuse `data` immediately afterwards.
- 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_bytesif 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
-
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
-
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)
Note
This is the requested state. While speaker playback pauses charging (see set_charging_pause_on_playback_enabled()), a request to enable is remembered and applied when playback has stopped.
- Parameters:
enable – True to enable charging, false to disable
- Returns:
true on success
-
void set_charging_pause_on_playback_enabled(bool enable)
Whether speaker playback pauses battery charging (default: enabled). With the IP2326 charger enabled, every audio transient on the speaker comes with a pop, whatever the charge current (measured on the Tab5: clean with CHG_EN low, popping with it high even when the pack is full and the charger idles). So while audio is being played the charger is switched off, and switched back on CHARGING_RESUME_DELAY_MS after the last audio block; charging a pack between sounds is not affected. Disabling this leaves the charger alone while audio plays.
- Parameters:
enable – True to pause charging during playback
-
inline bool charging_pause_on_playback_enabled() const
- Returns:
Whether speaker playback pauses battery charging
-
bool set_usb_a_power(bool enable)
Switch the 5 V supply of the USB-A host jack (USB_5V_EN on the 0x44 IO expander). On by default after initialize_io_expanders(); turn it off and back on to power-cycle an attached device (e.g. so a device attached at boot sees the same VBUS + host sequence as a hot-plug).
- Parameters:
enable – True to power the jack.
- Returns:
True on success (requires initialize_io_expanders()).
-
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 the SD card is initialized and its volume is currently mounted
- Returns:
True if the SD card is available (false after sdcard_component()->unmount())
-
inline sdmmc_card_t *sdcard() const
Get the uSD card
Note
nullptr until initialize_sdcard() succeeded. The card stays valid while its volume is unmounted (sdcard_component()->unmount()), e.g. to hand it to a USB host through espp::UsbDevice’s MSC function.
- Returns:
A pointer to the uSD card
-
inline espp::SdCard *sdcard_component() const
Get the SD card component: mount() / unmount() (e.g. to hand the card to a USB host with the usb_device MSC function), format(), card_info(), volume_info(), …
- Returns:
The component, or nullptr until initialize_sdcard() succeeded
-
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
See also
See also
- Returns:
The verbosity level of the logger
-
inline void set_log_level(espp::Logger::Verbosity level)
Set the log level for the logger
See also
See also
- 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
See also
See also
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
See also
See also
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
See also
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 constexpr bool is_st_tddi(DisplayController controller)
Whether the controller is one of the Sitronix TDDI parts (ST7123/ST7121), which integrate the touch engine (same protocol, I2C 0x55) with the display driver.
-
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
-
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 a battery is in the bay (pack voltage reads at least 4 V; an empty bay reads the charger’s ~2 V open-circuit output). Leave charging disabled when false: the IP2326 retrying to charge an empty bay puts a periodic pop on the speaker.
-
float voltage_v
-
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.
-
struct SdCardConfig
Configuration for the uSD card.