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
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
- Install PlatformIO (recommended for serious work; Arduino IDE OK for quick exploration)
- Clone the relevant base — Bruce, M5Launcher, or a fresh sketch from M5Cardputer examples
- Set up Zero env in
platformio.ini(Vol 7 § 3) - Configure SDKConfig for ESP32-S3 if using ESP-IDF
- 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
| Pitfall | Cause | Fix |
|---|---|---|
| Code that compiles for ADV fails on Zero | Pin / register references that aren’t on Zero | Use feature flags; conditional compile |
BMI270 library compile error | No IMU on Zero | Wrap library calls in #if HAS_INTERNAL_IMU |
| Audio code produces silence | No codec on Zero | Use PWM speaker fallback |
| EXT-bus reference compiles, runs without effect | No EXT bus pins on Zero | Detect at compile time; warn at runtime |
| Battery low at unexpected times | Smaller battery capacity | Reduce default brightness, conservative sleep timer |
| Wi-Fi tx-power issues | Lower battery sag at TX bursts | Limit TX power or duty cycle |
| OTA fails / partition mismatch | Different flash partition layout vs ADV | Use Zero-specific partitions table |
7. Resources
- Cardputer ADV Vol 11 (custom firmware canonical):
../../../M5Stack Cardputer ADV/03-outputs/Cardputer_ADV_Complete.html - M5Cardputer library: https://github.com/m5stack/M5Cardputer
- M5Unified: https://github.com/m5stack/M5Unified
- PlatformIO docs: https://docs.platformio.org/
- Arduino-ESP32: https://github.com/espressif/arduino-esp32
End of Vol 10. Next: Vol 11 covers operational posture — Zero-specific legal, ethical, and operational considerations for budget/education/fleet-ops deployments.