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 + /)

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

using Pixel = lv_color16_t

Alias for the pixel type used by the display.

using DisplayDriver = espp::St7789

Alias for the display driver.

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.

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

Alias for the IMU (BMI270) on the Cardputer ADV’s internal I2C bus.

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_rate is 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_bytes if 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_rate is 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

Returns:

The verbosity level of the logger

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

Set the log level for the logger

Parameters:

level – The verbosity level to use for the logger

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

Set the log verbosity for the logger

See also

set_log_level

Note

This is a convenience method that calls set_log_level

Parameters:

level – The verbosity level to use for the logger

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

Get the log verbosity for the logger

See also

get_log_level

Note

This is a convenience method that calls get_log_level

Returns:

The verbosity level of the logger

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

Set the rate limit for the logger

Note

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

Parameters:

rate_limit – The rate limit to use for the logger

Public Static Functions

static inline 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

Modifiers modifiers

The modifier state when the event was generated.

struct Modifiers

The state of the keyboard modifier keys.

Public Members

bool fn = {false}

Fn key.

bool shift = {false}

Left shift key.

bool ctrl = {false}

Left ctrl key.

bool opt = {false}

Opt key.

bool alt = {false}

Left alt key.

struct SdCardConfig

Configuration for the uSD card.

Public Members

bool format_if_mount_failed = false

Format the uSD card if mount failed.

int max_files = 5

The maximum number of files to open at once.

size_t allocation_unit_size = 2 * 1024

The allocation unit size in bytes.