M5Stack Cardputer
M5Stack-Cardputer
The M5Stack Cardputer (K132) and Cardputer ADV are card-sized computers based on the ESP32-S3 StampS3 module. They feature a 56-key QWERTY keyboard, a 1.14” 240x135 IPS display, a mono speaker, a microphone, a micro-SD card slot, an IR transmitter, a Grove port, and a WS2812 RGB LED.
The espp::M5StackCardputer component supports both variants with the same API - the board is detected at runtime - and provides a singleton hardware abstraction for initializing the display, keyboard (74HC138 GPIO matrix on the original, TCA8418 I2C controller on the ADV), audio (NS4168 amplifier on the original, ES8311 codec on the ADV), microphone, uSD card, RGB LED, battery measurement, and button subsystems.
Note
The speaker and the microphone share I2S pins, so they cannot be used at the same time; initializing one while the other is active will fail.
API Reference
Header File
Classes
-
class M5StackCardputer : public espp::BaseComponent
The M5StackCardputer class provides an interface to the M5Stack Cardputer (K132) and Cardputer ADV, ESP32-S3 (StampS3) based card-sized computers with a 56-key QWERTY keyboard. The variant is detected automatically at runtime (see variant()); both share the same 56-key layout and the same API.
The class provides access to the following features:
Display (1.14” 240x135 IPS TFT, ST7789V2)
Keyboard (original: 56-key matrix scanned through a 74HC138 demultiplexer; ADV: TCA8418 I2C keyboard controller)
Audio output (mono speaker; original: NS4168 I2S amplifier; ADV: ES8311 codec + NS4150B amplifier, initialized automatically)
Microphone (original: SPM1423 PDM; ADV: analog MEMS mic via the ES8311 codec)
micro-SD (uSD) card (SPI mode)
RGB LED (WS2812, on the StampS3 module)
Battery voltage measurement
G0 (BOOT) button
IR transmitter and Grove port pin definitions
Internal I2C bus accessor (ADV only; also hosts a BMI270 IMU at 0x68 which can be used with the espp bmi270 component)
LoRa radio + GNSS receiver on the LoRa+GPS Cap (U201 / U214) expansion module for the Cardputer ADV (SX1262 + ATGM336H)
The class is a singleton and can be accessed using the get() method.
Example
espp::M5StackCardputer &cardputer = espp::M5StackCardputer::get(); cardputer.set_log_level(espp::Logger::Verbosity::INFO); // initialize the LCD if (!cardputer.initialize_lcd()) { logger.error("Failed to initialize LCD!"); return; } // initialize the display, using a pixel buffer of 50 lines static constexpr size_t pixel_buffer_size = cardputer.lcd_width() * 50; if (!cardputer.initialize_display(pixel_buffer_size)) { logger.error("Failed to initialize display!"); return; } // initialize the RGB LED and the sound subsystem if (!cardputer.initialize_led()) { logger.error("Failed to initialize RGB LED!"); return; } if (!cardputer.initialize_sound(AUDIO_SAMPLE_RATE_HZ)) { logger.error("Failed to initialize sound!"); return; } cardputer.volume(60.0f); // try to mount the uSD card (warn and continue if it's not inserted) if (!cardputer.initialize_sdcard({})) { logger.warn("Could not mount the uSD card; is one inserted?"); } // create the GUI (a small keyboard-driven text editor) static Gui gui({}); // print the controls (also available on-screen via the fn+1 help popup) logger.info("Controls:\n{}", Gui::HELP_TEXT); // whether the board has a working IMU / microphone / LoRa radio / GPS; set // after keyboard / variant detection below, referenced by the keypress // callback static bool have_imu = false; static bool have_mic = false; static bool have_lora = false; static bool have_gps = false; static std::shared_ptr<espp::Sx126x> lora_radio; // The text being composed lives in the (hidden-when-not-active) Text tab, so // on the LoRa tab mirror it into the status bar as it is typed - otherwise // you cannot see what you are about to send. auto show_lora_compose = [&]() { std::string composed = gui.get_text(); gui.set_status_text(composed.empty() ? "Type a message; fn+0 sends" : ("> " + composed)); }; // the keyboard scanner delivers one event per key state change; use it to // drive the text editor, play key-click sounds, and show what's happening // in the status bar auto keypress_callback = [&](const espp::M5StackCardputer::KeyEvent &event) { if (!event.pressed) { return; } // fn+Tab cycles through the tabs. fn does not change a key's character // value, so fn+Tab arrives as a Tab press ('\t') with the fn modifier held. if (event.modifiers.fn && event.value == '\t') { Gui::Tab tab = gui.next_tab(); if (tab == Gui::Tab::LORA && have_lora) { show_lora_compose(); } play_beep(cardputer, 660.0f); return; } if (event.special == espp::M5StackCardputer::SpecialKey::F1) { // jump to the Help tab gui.select_tab(Gui::Tab::HELP); gui.set_status_text("Help tab"); play_beep(cardputer, 660.0f); } else if (event.special == espp::M5StackCardputer::SpecialKey::F2) { // jump to the IMU tab gui.select_tab(Gui::Tab::IMU); gui.set_status_text(have_imu ? "IMU tab" : "No IMU on this board"); play_beep(cardputer, 660.0f); } else if (event.special == espp::M5StackCardputer::SpecialKey::F3) { // start / stop recording from the microphone if (!have_mic || recording_capacity == 0) { gui.set_status_text("No mic recording on this board"); } else if (recording) { recording = false; gui.set_status_text( fmt::format("Recorded {:.1f}s (fn+4 plays)", static_cast<float>(recording_len) / (cardputer.microphone_sample_rate() * sizeof(int16_t)))); } else { playing = false; recording_len = 0; recording_start_us = esp_timer_get_time(); recording_last_us = recording_start_us; recording = true; gui.set_status_text("Recording... (fn+3 stops)"); } play_beep(cardputer, 660.0f); } else if (event.special == espp::M5StackCardputer::SpecialKey::F4) { // play back the recording (the main loop streams it to the speaker) if (recording_len == 0) { gui.set_status_text("Nothing recorded yet (fn+3 records)"); } else if (playing) { playing = false; gui.set_status_text("Playback stopped"); } else { recording = false; playing = true; gui.set_status_text("Playing... (fn+4 stops)"); } play_beep(cardputer, 660.0f); } else if (event.special == espp::M5StackCardputer::SpecialKey::F5 || event.special == espp::M5StackCardputer::SpecialKey::F6) { // speaker volume down / up (the beep gives immediate feedback) float delta = event.special == espp::M5StackCardputer::SpecialKey::F5 ? -10.0f : 10.0f; cardputer.volume(cardputer.volume() + delta); gui.set_status_text(fmt::format("Speaker volume: {:.0f}%", cardputer.volume())); play_beep(cardputer, 660.0f); } else if (event.special == espp::M5StackCardputer::SpecialKey::F7 || event.special == espp::M5StackCardputer::SpecialKey::F8) { // microphone volume down / up (heard on the next recording) float delta = event.special == espp::M5StackCardputer::SpecialKey::F7 ? -5.0f : 5.0f; cardputer.microphone_volume(cardputer.microphone_volume() + delta); gui.set_status_text( fmt::format("Mic volume: {:.0f}% (75% = 0 dB)", cardputer.microphone_volume())); play_beep(cardputer, 660.0f); } else if (event.special == espp::M5StackCardputer::SpecialKey::F9) { // jump to the LoRa tab gui.select_tab(Gui::Tab::LORA); if (have_lora) { show_lora_compose(); } else { gui.set_status_text("No LoRa module (attach the Cap)"); } play_beep(cardputer, 660.0f); } else if (event.special == espp::M5StackCardputer::SpecialKey::F10) { // send the current text area contents over LoRa (the main loop performs // the actual, blocking transmit) if (!have_lora) { gui.set_status_text("No LoRa module (attach the Cap)"); } else { std::string text = gui.get_text(); if (text.empty()) { gui.set_status_text("Type a message first, then fn+0"); } else { { std::lock_guard<std::mutex> lk(lora_tx_mutex); lora_tx_message = text; } lora_send_requested = true; gui.clear_text(); gui.select_tab(Gui::Tab::LORA); gui.set_status_text("Sending over LoRa..."); } } play_beep(cardputer, 660.0f); } else if (event.special != espp::M5StackCardputer::SpecialKey::NONE) { gui.handle_special_key(event.special); gui.set_status_text(espp::M5StackCardputer::special_key_name(event.special)); play_beep(cardputer, 660.0f); } else if (event.value != 0) { gui.add_char(event.value); // on the LoRa tab, mirror the composed text into the status bar so it is // visible as you type (the text box itself is on the Text tab) if (gui.active_tab() == Gui::Tab::LORA && have_lora) { show_lora_compose(); } play_beep(cardputer, 880.0f); } else { // a modifier key by itself std::string status; if (event.modifiers.fn) status += "fn "; if (event.modifiers.shift) status += "shift "; if (event.modifiers.ctrl) status += "ctrl "; if (event.modifiers.opt) status += "opt "; if (event.modifiers.alt) status += "alt "; gui.set_status_text(status.empty() ? "Ready" : status); } }; // the keyboard scanner auto-detects the board variant: the original's // 74HC138 GPIO matrix or the ADV's TCA8418 I2C keyboard controller if (!cardputer.initialize_keyboard(keypress_callback)) { logger.error("Failed to initialize keyboard!"); return; } logger.info("Board variant: {}", espp::M5StackCardputer::variant_name(cardputer.variant())); // the ADV has a BMI270 IMU on the internal I2C bus; initialize it if we're // on one (warn and continue otherwise - the original has no IMU) if (cardputer.variant() == espp::M5StackCardputer::Variant::ADV) { have_imu = cardputer.initialize_imu(); if (!have_imu) { logger.warn("Could not initialize the IMU!"); gui.set_imu_text("IMU init failed"); } } else { gui.set_imu_text("No IMU on this board\n(Cardputer ADV only)"); } // On the ADV the ES8311 codec runs full duplex, so the microphone can be // used at the same time as the speaker (it shares the speaker's sample // rate). The original cannot: its PDM microphone clock and the speaker // word-select share GPIO 43, and this example uses the speaker. if (cardputer.variant() == espp::M5StackCardputer::Variant::ADV) { auto mic_callback = [](const uint8_t *data, size_t num_bytes) { if (!recording) { return; } size_t offset = recording_len; size_t to_copy = std::min(num_bytes, 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) { // buffer full; the main loop notices recording went false and // updates the status bar recording = false; } }; have_mic = cardputer.initialize_microphone(mic_callback); if (have_mic) { // allocate the recording buffer: prefer PSRAM (neither Cardputer // variant ships with it, but boards / mods that have it get a much // longer recording), fall back to a few seconds in internal RAM size_t bytes_per_second = cardputer.microphone_sample_rate() * 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) { logger.warn("Could not allocate a recording buffer; recording disabled"); recording_capacity = 0; } else { logger.info("Recording buffer: {} KB ({} s at {} Hz)", recording_capacity / 1024, recording_capacity / bytes_per_second, cardputer.microphone_sample_rate()); } } else { logger.warn("Could not initialize the microphone!"); } } // the G0 (BOOT) button cycles the RGB LED color static std::atomic<int> led_hue{0}; auto button_callback = [&](const espp::Interrupt::Event &event) { if (event.active) { led_hue = (led_hue + 60) % 360; cardputer.led(espp::Hsv(static_cast<float>(led_hue), 1.0f, 0.2f)); play_beep(cardputer, 440.0f); } }; if (!cardputer.initialize_button(button_callback)) { logger.error("Failed to initialize button!"); return; } // set the initial LED color cardputer.led(espp::Hsv(static_cast<float>(led_hue), 1.0f, 0.2f)); // Initialize the LoRa radio on the LoRa+GPS Cap (SX1262) and wire it to the // LoRa tab. This uses the same radio settings as the T-Deck example (US // LongFast modulation, private sync word 0x12), so a Cardputer and a T-Deck // each running their example will exchange text messages. The Cap is a // Cardputer ADV accessory; on a board without it, initialization fails and // the LoRa tab reports that it is unavailable. espp::Sx126x::RadioConfig lora_config{}; lora_config.sync_word = 0x12; // private link, matches the t-deck example have_lora = cardputer.initialize_lora(lora_config); if (have_lora) { lora_radio = cardputer.lora(); // deliver received packets to the LoRa tab (runs in the BSP interrupt task) lora_radio->set_receive_callback([&](const espp::Sx126x::RxPacket &packet) { gui.add_lora_message( fmt::format("RX {:.0f}dBm: {}", packet.status.rssi, printable(packet.data))); }); std::error_code ec; if (lora_radio->start_receive(ec)) { gui.set_lora_status(fmt::format("Listening @ {:.3f} MHz, SF11/BW250 (fn+0 sends)", lora_radio->radio_config().frequency_hz / 1e6f)); } else { logger.error("Failed to start LoRa receive: {}", ec.message()); gui.set_lora_status(fmt::format("LoRa RX failed: {}", ec.message())); have_lora = false; } } else { logger.warn("Could not initialize LoRa (is the LoRa+GPS Cap attached? ADV only)"); gui.set_lora_status("LoRa unavailable (attach the Cap)"); } // Initialize the GNSS receiver on the same LoRa+GPS Cap (ATGM336H, 115200 // baud) and show the fix on the GPS tab. GPS and LoRa are both on the Cap, // so use the LoRa result as the "Cap present" signal; the fix callback runs // on the GPS reader task (its GUI calls are thread-safe). if (have_lora) { gui.set_gps_text("GPS: acquiring fix...\n(needs a clear sky view)"); have_gps = cardputer.initialize_gps([&](const espp::GpsFix &fix) { if (fix.valid) { gui.set_gps_text(fmt::format("Fix: {} sats HDOP {:.1f}\n{:.5f}, {:.5f}\nAlt {:.0f} m\n" "{:02d}:{:02d}:{:04.1f} UTC\n{:.1f} kn {:.0f} deg", (int)fix.num_satellites, fix.hdop, fix.latitude, fix.longitude, fix.altitude, (int)fix.hour, (int)fix.minute, fix.second, fix.speed_knots, fix.course_degrees)); } else { gui.set_gps_text(fmt::format("Acquiring fix...\n{} sats in view\n(needs a clear sky view)", (int)fix.num_satellites)); } }); if (!have_gps) { logger.warn("Could not initialize the GPS!"); gui.set_gps_text("GPS init failed (see log)"); } } else { gui.set_gps_text("GPS unavailable\n(attach the LoRa+GPS Cap)"); } // Main loop: service LoRa sends, stream any active playback to the speaker, // update the IMU / Sys tabs, and periodically show the battery voltage / // state of charge in the status bar static constexpr auto loop_period = 50ms; const int loops_per_imu_update = 2; // 100 ms const int loops_per_sys_update = std::chrono::seconds(1) / loop_period; // 1 s const int loops_per_battery_update = std::chrono::seconds(5) / loop_period; size_t play_offset = 0; bool was_recording = false; int loop_count = 0; while (true) { // service a LoRa send requested from the keyboard (fn+0). transmit() // blocks for the packet's time-on-air (a few hundred ms at SF11) then // returns the radio to receive, so doing it here keeps the keyboard and // LVGL tasks responsive. if (have_lora && lora_send_requested.exchange(false)) { std::string msg; { std::lock_guard<std::mutex> lk(lora_tx_mutex); msg = lora_tx_message; } std::span<const uint8_t> payload{reinterpret_cast<const uint8_t *>(msg.data()), msg.size()}; std::error_code ec; if (lora_radio->transmit(payload, 3s, ec)) { gui.add_lora_message("TX: " + msg); gui.set_status_text("Sent over LoRa"); logger.info("LoRa sent: {}", msg); } else { gui.add_lora_message(fmt::format("TX failed: {}", ec.message())); gui.set_status_text("LoRa send failed"); logger.error("LoRa transmit failed: {}", ec.message()); } } // 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_status_text("Playback done"); } else { play_offset += cardputer.play_audio(recording_buffer + play_offset, std::min<size_t>(len - play_offset, 4096)); } } else { play_offset = 0; } // notice when the recording stopped (buffer filled up, or fn+3 / fn+4) bool now_recording = recording; if (was_recording && !now_recording) { // report the measured capture rate: samples recorded over the wall // clock they took to arrive should match the nominal sample rate size_t num_samples = recording_len / sizeof(int16_t); float elapsed_s = static_cast<float>(recording_last_us - recording_start_us) / 1e6f; float effective_hz = elapsed_s > 0.0f ? num_samples / elapsed_s : 0.0f; logger.info("Recorded {} samples in {:.2f} s (~{:.0f} Hz effective, {} Hz nominal)", num_samples, elapsed_s, effective_hz, cardputer.microphone_sample_rate()); gui.set_status_text(fmt::format("Recorded {:.1f}s (fn+4 plays)", static_cast<float>(recording_len) / (cardputer.microphone_sample_rate() * sizeof(int16_t)))); } was_recording = now_recording; // update the IMU tab only while it is the active tab (the readings are // only visible there) if (have_imu && gui.active_tab() == Gui::Tab::IMU && (loop_count % loops_per_imu_update) == 0) { auto imu = cardputer.imu(); std::error_code ec; if (imu->update(std::chrono::duration<float>(loop_period * loops_per_imu_update).count(), ec)) { auto accel = imu->get_accelerometer(); auto gyro = imu->get_gyroscope(); gui.set_imu_text(fmt::format("a {:+.1f} {:+.1f} {:+.1f}\ng {:+5.0f} {:+5.0f} {:+5.0f}", accel.x, accel.y, accel.z, gyro.x, gyro.y, gyro.z)); } } // keep the Sys tab's board info current if ((loop_count % loops_per_sys_update) == 0) { gui.set_system_text(fmt::format( "Board: {}\nBattery: {:.2f} V ({:.0f}%)\nSpeaker {:.0f}% Mic {:.0f}%\nAudio {} Hz\n" "LoRa {} GPS {}", espp::M5StackCardputer::variant_name(cardputer.variant()), cardputer.battery_voltage(), cardputer.battery_soc(), cardputer.volume(), cardputer.microphone_volume(), cardputer.audio_sample_rate(), have_lora ? "on" : "off", have_gps ? "on" : "off")); } // Periodically show the battery in the status bar - but not on the LoRa // tab, where the status bar mirrors the message being composed (see the // keypress callback), nor while recording / playing. if ((loop_count % loops_per_battery_update) == 0 && !recording && !playing && gui.active_tab() != Gui::Tab::LORA) { gui.set_status_text(fmt::format("Battery: {:.2f} V ({:.0f}%)", cardputer.battery_voltage(), cardputer.battery_soc())); } loop_count++; std::this_thread::sleep_for(loop_period); }
Note
On the original Cardputer the speaker and microphone cannot be used at the same time: GPIO 43 doubles as the speaker’s I2S word-select and the PDM microphone’s (MHz-range) clock - two different signals on one physical pin - so initializing one while the other is active will fail. On the Cardputer ADV both go through the ES8311 codec in full duplex on a single I2S bus (shared bit/word clocks, separate data pins), so the speaker and microphone can be used simultaneously; they share the I2S sample rate, which is set by whichever subsystem is initialized first.
Public Types
-
enum class SpecialKey : uint8_t
Special (non-printable) keys, produced by the Fn layer of the keyboard.
Values:
-
enumerator NONE
Not a special key.
-
enumerator ESC
Escape (fn + `)
-
enumerator F1
F1 (fn + 1)
-
enumerator F2
F2 (fn + 2)
-
enumerator F3
F3 (fn + 3)
-
enumerator F4
F4 (fn + 4)
-
enumerator F5
F5 (fn + 5)
-
enumerator F6
F6 (fn + 6)
-
enumerator F7
F7 (fn + 7)
-
enumerator F8
F8 (fn + 8)
-
enumerator F9
F9 (fn + 9)
-
enumerator F10
F10 (fn + 0)
-
enumerator F11
F11 (fn + -)
-
enumerator F12
F12 (fn + =)
-
enumerator DELETE
Delete (fn + backspace)
-
enumerator UP
Up arrow (fn + ;)
-
enumerator DOWN
Down arrow (fn + .)
-
enumerator LEFT
Left arrow (fn + ,)
-
enumerator RIGHT
Right arrow (fn + /)
-
enumerator NONE
-
enum class Variant : uint8_t
The hardware variant of the board.
Values:
-
enumerator ORIGINAL
Original Cardputer (K132): 74HC138 matrix keyboard, NS4168 I2S amplifier, SPM1423 PDM microphone
-
enumerator ADV
Cardputer ADV: TCA8418 I2C keyboard controller, ES8311 codec (speaker + microphone), BMI270 IMU on the internal I2C bus
-
enumerator ORIGINAL
-
using Pixel = lv_color16_t
Alias for the pixel type used by the display.
-
using button_callback_t = std::function<void(const espp::Interrupt::Event&)>
Alias for the button callback function.
-
using keypress_callback_t = std::function<void(const KeyEvent&)>
Alias for the keypress callback function. Called once for each key that changes state during a keyboard scan.
-
using microphone_callback_t = std::function<void(const uint8_t *data, size_t num_bytes)>
Alias for the microphone data callback. Called with 16-bit signed mono samples read from the PDM microphone.
Public Functions
-
espp::Interrupt &interrupts()
Get a reference to the interrupts
- Returns:
A reference to the interrupts
-
Variant variant()
Get the hardware variant of the board.
Note
The first call detects the variant by probing for the ADV’s TCA8418 keyboard controller on the internal I2C bus (GPIO 8/9); on the original those pins are then returned to plain GPIO for the 74HC138 matrix.
- Returns:
The hardware variant of the board
-
I2c *internal_i2c()
Get a pointer to the internal I2C bus (ADV only)
Note
On the ADV the internal bus hosts the TCA8418 keyboard controller (0x34), the ES8311 codec (0x18), and a BMI270 IMU (0x68)
- Returns:
A pointer to the internal I2C bus, or nullptr on the original Cardputer (which has no internal I2C bus)
- bool initialize_keyboard (const keypress_callback_t &callback=nullptr, std::chrono::milliseconds poll_interval=std::chrono::milliseconds(10), const espp::Task::BaseConfig &task_config={ .name="keyboard",.stack_size_bytes=CONFIG_M5STACK_CARDPUTER_KEYBOARD_TASK_STACK_SIZE,.priority=CONFIG_M5STACK_CARDPUTER_KEYBOARD_TASK_PRIORITY,.core_id=CONFIG_M5STACK_CARDPUTER_KEYBOARD_TASK_CORE_ID})
Initialize the keyboard
Note
The keyboard is a 4x14 matrix (a 74HC138-scanned GPIO matrix on the original, a TCA8418 I2C controller on the ADV); a scanner task owned by this class polls it at the given interval and calls the callback once per key state change.
Note
The callback runs in the scanner task’s context, so the task’s stack must be large enough for whatever the callback calls into (e.g. LVGL).
- Parameters:
callback – The callback function to call when a key changes state
poll_interval – The interval at which to scan the keyboard matrix
task_config – The configuration for the keyboard scanner task. The defaults come from the M5STACK_CARDPUTER_KEYBOARD_TASK_* Kconfig options.
- Returns:
true if the keyboard was successfully initialized, false otherwise
-
bool is_key_pressed(uint8_t row, uint8_t col) const
Get whether a key is currently pressed
- Parameters:
row – The row of the key in the matrix
col – The column of the key in the matrix
- Returns:
true if the key is currently pressed, false otherwise
-
Modifiers modifiers() const
Get the current state of the modifier keys
- Returns:
The current state of the modifier keys
-
std::array<uint16_t, KEYBOARD_ROWS> keyboard_state() const
Get the raw state of the keyboard matrix
- Returns:
One entry per row; in each entry bit N is set if the key in column N is currently pressed
-
bool initialize_button(const button_callback_t &callback = nullptr)
Initialize the G0 (BOOT) button
- Parameters:
callback – The callback function to call when the button changes state
- Returns:
true if the button was successfully initialized, false otherwise
-
bool button_state() const
Get the state of the G0 (BOOT) button
- Returns:
true if the button is pressed, false otherwise
-
bool initialize_lcd()
Initialize the LCD (low level display driver)
- Returns:
true if the LCD was successfully initialized, false otherwise
-
bool initialize_display(size_t pixel_buffer_size)
Initialize the display (lvgl display driver)
- Parameters:
pixel_buffer_size – The size of the pixel buffer
- Returns:
true if the display was successfully initialized, false otherwise
-
std::shared_ptr<Display<Pixel>> display() const
Get a shared pointer to the display
- Returns:
A shared pointer to the display
-
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 brightness(float brightness)
Set the brightness of the backlight
Note
This function will only work after initialize_lcd() has been called
- Parameters:
brightness – The brightness of the backlight as a percentage (0 - 100)
-
float brightness() const
Get the brightness of the backlight
Note
This function will only work after initialize_lcd() has been called
- Returns:
The brightness of the backlight as a percentage (0 - 100)
-
Pixel *vram0() const
Get the VRAM 0 pointer (DMA memory used by LVGL)
Note
This is the memory used by LVGL for rendering
Note
This is null unless initialize_display() has been called
- Returns:
The VRAM 0 pointer
-
Pixel *vram1() const
Get the VRAM 1 pointer (DMA memory used by LVGL)
Note
This is the memory used by LVGL for rendering
Note
This is null unless initialize_display() has been called
- Returns:
The VRAM 1 pointer
-
void write_lcd_frame(const uint16_t x, const uint16_t y, const uint16_t width, const uint16_t height, uint8_t *data)
Write a frame to the LCD
Note
This method queues the data to be written to the LCD, only blocking if there is an ongoing SPI transaction
- Parameters:
x – The x coordinate
y – The y coordinate
width – The width of the frame, in pixels
height – The height of the frame, in pixels
data – The data to write
-
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 SPI transaction callback
- bool initialize_sound (uint32_t default_audio_rate=44100, const espp::Task::BaseConfig &task_config={ .name="audio",.stack_size_bytes=CONFIG_M5STACK_CARDPUTER_AUDIO_TASK_STACK_SIZE,.priority=CONFIG_M5STACK_CARDPUTER_AUDIO_TASK_PRIORITY,.core_id=CONFIG_M5STACK_CARDPUTER_AUDIO_TASK_CORE_ID})
Initialize the sound subsystem (mono speaker; NS4168 I2S amplifier on the original, ES8311 codec + NS4150B amplifier on the ADV)
Note
On the original Cardputer this will fail if the microphone has been initialized (see the class notes). On the ADV the speaker and microphone run full duplex and share the I2S sample rate; if the microphone was initialized first, its sample rate is kept and
default_audio_rateis ignored (with a warning).- Parameters:
default_audio_rate – The default sample rate for the audio, in Hz
task_config – The configuration for the audio task. The defaults come from the M5STACK_CARDPUTER_AUDIO_TASK_* Kconfig options.
- Returns:
true if the sound subsystem was successfully initialized, 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
-
void mute(bool mute)
Mute or unmute the audio
- Parameters:
mute – true to mute the audio, false to unmute it
-
bool is_muted() const
Check if the audio is muted
- Returns:
true if the audio is muted, false otherwise
-
void volume(float volume)
Set the volume of the audio
Note
The NS4168 has no volume control, so the volume is applied in software when the samples are written to the I2S peripheral
- Parameters:
volume – The volume as a percentage (0 - 100)
-
float volume() const
Get the volume of the audio
- Returns:
The volume as a percentage (0 - 100)
-
size_t play_audio(const std::vector<uint8_t> &data)
Play the 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 mono samples)
- Returns:
The number of bytes actually queued (may be less than the data size if the internal stream buffer is full)
-
size_t play_audio(const uint8_t *data, uint32_t num_bytes)
Play the 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 mono samples)
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)
- bool initialize_microphone (const microphone_callback_t &callback, uint32_t sample_rate=16000, const espp::Task::BaseConfig &task_config={ .name="microphone",.stack_size_bytes=CONFIG_M5STACK_CARDPUTER_MICROPHONE_TASK_STACK_SIZE,.priority=CONFIG_M5STACK_CARDPUTER_MICROPHONE_TASK_PRIORITY,.core_id=CONFIG_M5STACK_CARDPUTER_MICROPHONE_TASK_CORE_ID})
Initialize the microphone (SPM1423 PDM on the original, analog MEMS via the ES8311 codec on the ADV) and start delivering audio data to the provided callback
Note
The callback runs in the microphone task’s context, so the task’s stack must be large enough for whatever the callback does with the audio data.
Note
On the original Cardputer this will fail if the sound subsystem has been initialized (see the class notes). On the ADV the speaker and microphone run full duplex and share the I2S sample rate; if the sound subsystem was initialized first, its sample rate is kept and
sample_rateis ignored (with a warning) - check microphone_sample_rate() for the actual rate.- Parameters:
callback – The callback to call with recorded audio data (16-bit signed mono samples)
sample_rate – The sample rate for the microphone, in Hz
task_config – The configuration for the microphone task. The defaults come from the M5STACK_CARDPUTER_MICROPHONE_TASK_* Kconfig options.
- Returns:
true if the microphone was successfully initialized, false otherwise
-
uint32_t microphone_sample_rate() const
Get the microphone sample rate
- Returns:
The microphone sample rate, in Hz
-
void microphone_volume(float volume)
Set the microphone volume
Note
On the ADV this adjusts the ES8311 codec’s digital ADC volume (values above 75 amplify, up to +32 dB at 100); on the original the samples are scaled in software in the microphone task (values above 75 amplify with saturation)
- Parameters:
volume – The volume as a percentage (0 - 100); 75 is unity (0 dB, the default)
-
float microphone_volume() const
Get the microphone volume
- Returns:
The microphone volume as a percentage (0 - 100); 75 is unity (0 dB)
-
bool initialize_sdcard(const SdCardConfig &config)
Initialize the uSD card (SPI mode)
- Parameters:
config – The configuration for the uSD card
- Returns:
True if the uSD card was initialized properly.
-
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 initialize_led()
Initialize the RGB LED (WS2812 on the StampS3 module)
- Returns:
true if the LED was successfully initialized, false otherwise
-
bool led(const Hsv &hsv)
Set the color of the LED
- Parameters:
hsv – The color of the LED in HSV format
- Returns:
true if the color was successfully set, false otherwise
-
bool led(const Rgb &rgb)
Set the color of the LED
- Parameters:
rgb – The color of the LED in RGB format
- Returns:
true if the color was successfully set, false otherwise
-
float battery_voltage()
Get the battery voltage
Note
The battery voltage is measured through a 2:1 divider on GPIO 10
- Returns:
The battery voltage, in volts
-
float battery_soc()
Get the battery state of charge
Note
This is estimated from the battery voltage using a typical 1S lithium-ion discharge curve, so it is only an approximation - the voltage sags under load (e.g. with the backlight at full brightness or the speaker playing) which will lower the estimate.
- Returns:
The battery state of charge as a percentage (0 - 100)
- bool initialize_imu (const Imu::filter_fn &orientation_filter=nullptr, const Imu::ImuConfig &imu_config={ .accelerometer_range=Imu::AccelerometerRange::RANGE_4G,.accelerometer_odr=Imu::AccelerometerODR::ODR_100_HZ,.accelerometer_bandwidth=Imu::AccelerometerBandwidth::NORMAL_AVG4,.gyroscope_range=Imu::GyroscopeRange::RANGE_1000DPS,.gyroscope_odr=Imu::GyroscopeODR::ODR_100_HZ,.gyroscope_bandwidth=Imu::GyroscopeBandwidth::NORMAL_MODE,.gyroscope_performance_mode=Imu::GyroscopePerformanceMode::PERFORMANCE_OPTIMIZED})
Initialize the IMU (BMI270; Cardputer ADV only)
Note
The original Cardputer has no IMU, so this fails (with an error log) unless the board is a Cardputer ADV.
- Parameters:
orientation_filter – Optional filter function for orientation (e.g. a kalman or madgwick filter); called by Imu::update()
imu_config – The IMU configuration
- Returns:
true if the IMU was successfully initialized, false otherwise
-
inline std::shared_ptr<Imu> imu() const
Get a shared pointer to the IMU
- Returns:
A shared pointer to the IMU, or nullptr if it has not been (successfully) initialized
-
bool initialize_lora(const Sx126x::RadioConfig &radio_config = {})
Initialize the LoRa radio (SX1262) on the LoRa+GPS Cap
Note
The Cap mounts on the Cardputer ADV’s rear expansion connector; the radio shares the uSD card’s SPI bus with its own chip select, so the radio and uSD card can be used together.
Note
On the U214 Cap the radio’s RF switch is controlled through a PI4IOE5V6408 I2C IO expander, which this method configures to connect the antenna. If no expander is found (e.g. the older U201 Cap), initialization proceeds without it.
Note
The radio’s DIO1 interrupt is automatically serviced via the board’s interrupt handler, so packets are delivered through the callbacks registered on the returned driver (see espp::Sx126x::set_receive_callback and friends).
- Parameters:
radio_config – The radio (modem) configuration to apply
- Returns:
True if the radio was initialized properly
-
inline std::shared_ptr<Sx126x> lora() const
Get the LoRa radio
Note
The radio is only available if initialize_lora() succeeded
- Returns:
A shared pointer to the LoRa radio driver
-
bool initialize_gps(const Gps::fix_callback_fn &fix_cb = nullptr, uint32_t baud_rate = 115200)
Initialize the GNSS receiver (ATGM336H) on the LoRa+GPS Cap
- Parameters:
fix_cb – Optional callback invoked on each fix update
baud_rate – The baud rate of the GPS UART. The Cap ships configured for 115200 baud.
- Returns:
True if the GPS was initialized properly
-
inline std::shared_ptr<Gps> gps() const
Get the GPS
Note
The GPS is only available if initialize_gps() succeeded
- Returns:
A shared pointer to the GPS driver
-
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 M5StackCardputer &get()
Access the singleton instance of the M5StackCardputer class.
- Returns:
Reference to the singleton instance of the M5StackCardputer class
-
static inline const char *variant_name(Variant variant)
Get the name of a variant
- Parameters:
variant – The variant to get the name of
- Returns:
The name of the variant
-
static char key_value(uint8_t row, uint8_t col, const Modifiers &modifiers)
Get the character for a key, given a modifier state
- Parameters:
row – The row of the key in the matrix
col – The column of the key in the matrix
modifiers – The modifier state to apply
- Returns:
The character for the key (with the shift layer applied), or 0 if the key has no character
-
static SpecialKey special_key(uint8_t row, uint8_t col)
Get the special key (fn layer) for a key
- Parameters:
row – The row of the key in the matrix
col – The column of the key in the matrix
- Returns:
The special key for the key, or SpecialKey::NONE if it has none
-
static const char *special_key_name(SpecialKey key)
Get the name of a special key
- Parameters:
key – The special key to get the name of
- Returns:
The name of the special key
-
static inline constexpr size_t lcd_width()
Get the width of the LCD in pixels
- Returns:
The width of the LCD in pixels
-
static inline constexpr size_t lcd_height()
Get the height of the LCD in pixels
- Returns:
The height of the LCD in pixels
-
static inline constexpr auto get_lcd_dc_gpio()
Get the GPIO pin for the LCD data/command signal
- Returns:
The GPIO pin for the LCD data/command signal
-
static inline constexpr auto lora_cs_gpio()
Get the GPIO pin for the LoRa radio chip select
- Returns:
The GPIO pin for the LoRa radio chip select
-
static inline constexpr auto lora_dio1_gpio()
Get the GPIO pin for the LoRa radio DIO1 (interrupt) line
- Returns:
The GPIO pin for the LoRa radio DIO1 line
-
static inline constexpr auto lora_busy_gpio()
Get the GPIO pin for the LoRa radio BUSY line
- Returns:
The GPIO pin for the LoRa radio BUSY line
-
static inline constexpr auto lora_reset_gpio()
Get the GPIO pin for the LoRa radio reset line
- Returns:
The GPIO pin for the LoRa radio reset line
-
static inline constexpr auto gps_tx_gpio()
Get the GPIO pin for the GPS UART TX (ESP32 -> GPS)
- Returns:
The GPIO pin for the GPS UART TX
-
static inline constexpr auto gps_rx_gpio()
Get the GPIO pin for the GPS UART RX (GPS -> ESP32)
- Returns:
The GPIO pin for the GPS UART RX
-
static inline constexpr gpio_num_t ir_tx_gpio()
Get the GPIO pin for the IR transmitter
- Returns:
The GPIO pin for the IR transmitter
-
static inline constexpr gpio_num_t grove_scl_gpio()
Get the GPIO pin for the Grove port SCL / G1 signal
- Returns:
The GPIO pin for the Grove port SCL / G1 signal
-
static inline constexpr gpio_num_t grove_sda_gpio()
Get the GPIO pin for the Grove port SDA / G2 signal
- Returns:
The GPIO pin for the Grove port SDA / G2 signal
Public Static Attributes
-
static constexpr size_t KEYBOARD_ROWS = 4
Number of rows in the keyboard matrix.
-
static constexpr size_t KEYBOARD_COLS = 14
Number of columns in the keyboard matrix.
-
static constexpr size_t SPI_MAX_TRANSFER_BYTES = SPI_LL_DMA_MAX_BIT_LEN / 8
Maximum number of bytes that can be transferred in a single SPI transaction to the display. 32k on the ESP32-S3.
-
static constexpr char mount_point[] = "/sdcard"
Mount point for the uSD card.
-
struct KeyEvent
A single key state change reported by the keyboard scanner.
Public Members
-
uint8_t row
Row of the key in the matrix (0 = top / esc row)
-
uint8_t col
Column of the key in the matrix (0 = leftmost)
-
bool pressed
True if the key is now pressed, false if released.
-
char value
The character for the key, with the shift layer applied (0 if the key has no character, e.g. a modifier or an fn-layer special key). Backspace, tab, enter, and space are reported as ‘
’, ‘\t’, ‘
’, and ‘ ‘.
-
SpecialKey special
The special key (fn layer) if fn was held and the key has one, SpecialKey::NONE otherwise
-
uint8_t row
-
struct Modifiers
The state of the keyboard modifier keys.
-
struct SdCardConfig
Configuration for the uSD card.