M5Stack Cardputer Zero · Volume 10

M5Stack Cardputer Zero Volume 10 — Custom Firmware Development

Building custom Cardputer Zero firmware — board target, capability detection, deploying education-focused builds

Contents

SectionTopic
1About this volume
2Custom firmware development workflow
3Worked example: education-focused custom build
4Worked example: fleet-ops collection unit
5Cross-Cardputer-family code organization
6Common build pitfalls
7Resources

1. About this volume

Vol 10 covers custom firmware development for Cardputer Zero. Most of the canonical material — Mayhem-style fork patterns, ESP-IDF deep integration, custom-app patterns — lives in ../../../M5Stack Cardputer ADV/03-outputs/Cardputer_ADV_Complete.html Vol 11 and applies to Zero unchanged once the Zero board target is set up. This volume captures the Zero-distinctive build considerations.

For tjscientist’s expected use: most projects won’t require custom firmware development; existing forks (Bruce, NEMO, M5Launcher, MicroHydra) cover the common cases. Custom firmware matters when you need:

  • A specific feature combination not in existing forks
  • An education / classroom-curated firmware
  • A fleet-ops firmware with specific behavior
  • Research into Zero’s exact hardware behavior

2. Custom firmware development workflow

2.1 Setup

  1. Install PlatformIO (recommended for serious work; Arduino IDE OK for quick exploration)
  2. Clone the relevant base — Bruce, M5Launcher, or a fresh sketch from M5Cardputer examples
  3. Set up Zero env in platformio.ini (Vol 7 § 3)
  4. Configure SDKConfig for ESP32-S3 if using ESP-IDF
  5. Build + flash + iterate

2.2 Standard build cycle

# 1. Edit source code
vim src/main.cpp

# 2. Build
pio run --environment m5stack-cardputer-zero

# 3. Flash
pio run --environment m5stack-cardputer-zero --target upload

# 4. Monitor serial output
pio device monitor --baud 115200

# 5. Iterate

Typical cycle time: ~30-60 seconds for incremental changes.

2.3 Capability detection in firmware

Custom firmware should detect Zero-specific hardware (or lack thereof):

// Detect presence of optional peripherals
bool hasIMU() {
    Wire.beginTransmission(0x68);
    return Wire.endTransmission() == 0;
}

bool hasAudioCodec() {
    Wire.beginTransmission(0x18);
    return Wire.endTransmission() == 0;
}

bool hasIR() {
    return false;  // Compile-time on Zero; runtime check on uncertain hardware
}

// Use to gate features
void initFeatures() {
    if (!hasIMU()) {
        Serial.println("Note: no IMU; tilt features disabled");
    }
    if (!hasAudioCodec()) {
        Serial.println("Note: no audio codec; voice features disabled");
    }
}

3. Worked example: education-focused custom build

Goal: a curated Cardputer Zero firmware for a 30-student embedded systems class. Focus: simplified UI, predefined sketches, easy-to-recover-from-bricked-state.

3.1 Feature list

  • Boot to “education menu” — 6-8 pre-built example apps
  • Apps include: blink, “hello world”, display + keyboard demo, WiFi scan, simple calculator, Pomodoro timer, simple game
  • Each app is self-contained; no external dependencies
  • “Reset to factory” function via Fn+R key combo
  • USB-CDC serial logging for instructor debug
  • No internet connectivity needed (offline-friendly)

3.2 Skeleton code (abridged)

#include "M5Cardputer.h"

void appBlink();
void appHello();
void appDisplay();
void appWiFi();
void appCalc();
void appPomodoro();
void appGame();

struct App {
    const char* name;
    void (*entry)();
};

App apps[] = {
    {"Blink LED", appBlink},
    {"Hello World", appHello},
    {"Display Demo", appDisplay},
    {"WiFi Scan", appWiFi},
    {"Calculator", appCalc},
    {"Pomodoro Timer", appPomodoro},
    {"Simple Game", appGame},
};

const int numApps = sizeof(apps) / sizeof(App);
int selectedApp = 0;

void setup() {
    auto cfg = M5.config();
    M5Cardputer.begin(cfg, true);
    M5Cardputer.Display.setTextSize(2);
    redrawMenu();
}

void redrawMenu() {
    M5Cardputer.Display.clear();
    M5Cardputer.Display.setCursor(10, 10);
    M5Cardputer.Display.println("Cardputer Zero - Edu");
    M5Cardputer.Display.println("");
    for (int i = 0; i < numApps; i++) {
        M5Cardputer.Display.setCursor(10, 30 + 12*i);
        M5Cardputer.Display.print(i == selectedApp ? "> " : "  ");
        M5Cardputer.Display.println(apps[i].name);
    }
}

void loop() {
    M5Cardputer.update();
    if (M5Cardputer.Keyboard.isKeyPressed(KEY_UP)) {
        selectedApp = (selectedApp - 1 + numApps) % numApps;
        redrawMenu();
    } else if (M5Cardputer.Keyboard.isKeyPressed(KEY_DOWN)) {
        selectedApp = (selectedApp + 1) % numApps;
        redrawMenu();
    } else if (M5Cardputer.Keyboard.isKeyPressed(KEY_ENTER)) {
        apps[selectedApp].entry();
        redrawMenu();
    } else if (M5Cardputer.Keyboard.isFnPressed() && M5Cardputer.Keyboard.isKeyPressed('R')) {
        // Soft reset to known-good state
        ESP.restart();
    }
}

// (Each app implementation is short and self-contained)

Build, flash, deploy to 30 units. Each unit boots to this menu; students explore the apps.

3.3 Deployment

# Build once
pio run --environment m5stack-cardputer-zero

# Flash to all 30 units sequentially (script the COM port iteration)
for port in $(ls /dev/cu.usbserial*); do
    pio run --environment m5stack-cardputer-zero \
            --upload-port $port \
            --target upload
done

Total time: ~10-15 minutes for 30 units (depends on flash speed).


4. Worked example: fleet-ops collection unit

Goal: a Cardputer Zero firmware for passive Wi-Fi probe collection. Capture probes to SD; minimal UI; long battery life.

4.1 Features

  • Boot directly into collection mode (no menu)
  • Continuous Wi-Fi probe-request capture
  • Write each probe to SD with timestamp + RSSI
  • Display: minimal — show capture count + uptime
  • Power optimization: reduce CPU clock when possible, dim display
  • Auto-reboot every 24 hours (memory leak protection)

4.2 Skeleton (abridged)

#include "M5Cardputer.h"
#include <SD.h>
#include <WiFi.h>
#include "esp_wifi.h"

File captureFile;
volatile int probeCount = 0;

// Promiscuous-mode packet handler
void probeHandler(void* buf, wifi_promiscuous_pkt_type_t type) {
    if (type != WIFI_PKT_MGMT) return;
    wifi_promiscuous_pkt_t* pkt = (wifi_promiscuous_pkt_t*)buf;
    // Extract probe request from pkt; write to SD
    captureFile.printf("%lu,%d,...probe_data...\n", millis(), pkt->rx_ctrl.rssi);
    probeCount++;
}

void setup() {
    auto cfg = M5.config();
    M5Cardputer.begin(cfg, true);
    M5Cardputer.Display.setTextSize(2);
    M5Cardputer.Display.setBrightness(50);  // Dim for power save

    SD.begin();
    captureFile = SD.open("/probes.log", FILE_APPEND);

    WiFi.mode(WIFI_STA);
    esp_wifi_set_promiscuous(true);
    esp_wifi_set_promiscuous_rx_cb(probeHandler);

    M5Cardputer.Display.println("Capturing...");
}

unsigned long lastUpdate = 0;
unsigned long bootTime = 0;

void loop() {
    if (millis() - lastUpdate > 5000) {
        M5Cardputer.Display.fillRect(0, 50, 240, 80, BLACK);
        M5Cardputer.Display.setCursor(10, 50);
        M5Cardputer.Display.printf("Probes: %d", probeCount);
        M5Cardputer.Display.setCursor(10, 70);
        M5Cardputer.Display.printf("Uptime: %lu min", (millis() - bootTime) / 60000);
        lastUpdate = millis();
    }

    // Auto-reboot after 24 hours
    if (millis() - bootTime > 24L * 60L * 60L * 1000L) {
        captureFile.flush();
        captureFile.close();
        ESP.restart();
    }

    delay(100);  // Reduce CPU activity
}

4.3 Deployment

Pre-flash all units; deploy to collection sites. Power via USB-C wall adapter or large battery pack (for longer-than-internal-battery runtimes).


5. Cross-Cardputer-family code organization

For libraries / firmwares targeting both Zero and ADV:

5.1 Common code pattern

// Library header
#ifdef M5_CARDPUTER_ZERO
    #define HAS_INTERNAL_IMU 0
    #define HAS_INTERNAL_AUDIO_CODEC 0
    #define HAS_INTERNAL_IR 0  // verify on actual Zero
    #define BATTERY_CAPACITY_MAH 700
#else  // ADV
    #define HAS_INTERNAL_IMU 1
    #define HAS_INTERNAL_AUDIO_CODEC 1
    #define HAS_INTERNAL_IR 1
    #define BATTERY_CAPACITY_MAH 1750
#endif

This way, library code can use feature flags without runtime detection overhead.

5.2 Feature-flag namespaces

For features that depend on hardware:

namespace CardputerFeatures {
    bool hasIMU() { return HAS_INTERNAL_IMU; }
    bool hasAudioCodec() { return HAS_INTERNAL_AUDIO_CODEC; }
    bool hasIR() { return HAS_INTERNAL_IR; }
    int batteryCapacityMah() { return BATTERY_CAPACITY_MAH; }
}

UI / feature menus can hide unavailable features cleanly.


6. Common build pitfalls

PitfallCauseFix
Code that compiles for ADV fails on ZeroPin / register references that aren’t on ZeroUse feature flags; conditional compile
BMI270 library compile errorNo IMU on ZeroWrap library calls in #if HAS_INTERNAL_IMU
Audio code produces silenceNo codec on ZeroUse PWM speaker fallback
EXT-bus reference compiles, runs without effectNo EXT bus pins on ZeroDetect at compile time; warn at runtime
Battery low at unexpected timesSmaller battery capacityReduce default brightness, conservative sleep timer
Wi-Fi tx-power issuesLower battery sag at TX burstsLimit TX power or duty cycle
OTA fails / partition mismatchDifferent flash partition layout vs ADVUse Zero-specific partitions table

7. Resources

End of Vol 10. Next: Vol 11 covers operational posture — Zero-specific legal, ethical, and operational considerations for budget/education/fleet-ops deployments.