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
-
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 ST7123 integrated touch controller (ST7123 variant)
-
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
-
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_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)
- 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
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 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 battery is present.
-
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.