M5Stick S3 · Volume 7

M5Stack M5StickS3 Volume 7 — Programming Environments

Arduino + PlatformIO + MicroPython + UiFlow 2 + ESP-IDF — the five paths with M5StickS3-specific deltas

Contents

SectionTopic
1About this volume
2Arduino IDE 2.x
3PlatformIO (the recommended default)
4MicroPython + mpremote
5UiFlow 2 (block coding)
6ESP-IDF (low-level)
7M5Unified library reference for M5StickS3
8Build flag essentials (OPI PSRAM is mandatory)
9When to use which environment
10Sketch / project skeletons
11Resources

1. About this volume

Vol 7 covers the programming environments for writing custom code on M5StickS3. Same five paths as Cardputer ADV: Arduino IDE, PlatformIO, MicroPython, UiFlow 2, ESP-IDF. Tooling at the toolchain level is identical (pio run, arduino-cli, etc.) — the differences are in:

  1. PlatformIO board target name (esp32-s3-devkitc-1 or m5stick-s3 if published)
  2. The OPI PSRAM build flag — mandatory for M5StickS3 (Cardputer ADV doesn’t need it; M5StickS3 does)
  3. M5Unified library APIs specific to M5StickS3 features (audio, IR RX, IMU model)

Cross-reference: the Cardputer ADV’s Vol 7 covers all five environments in depth. This volume focuses on the M5StickS3-specific deltas without re-authoring the shared content.


2. Arduino IDE 2.x

Setup:

  1. Install Arduino IDE 2.x from https://www.arduino.cc/en/software.
  2. Add ESP32 board manager URL: Preferences → Additional Board Manager URLs → add https://espressif.github.io/arduino-esp32/package_esp32_index.json.
  3. Install ESP32 board package: Tools → Board → Boards Manager → search “esp32” → install “esp32 by Espressif Systems” (v3.0.0 or newer).
  4. Install M5Unified library: Sketch → Include Library → Manage Libraries → search “M5Unified” → install. Pulls M5GFX as dependency.

Board settings (Tools menu) for M5StickS3:

SettingValueNotes
Board”M5Stack-StickS3” if available, else “ESP32S3 Dev Module”Future M5Stack updates may add native target
Upload Speed1500000Fast and reliable
USB CDC On BootEnabledMandatory for native USB-CDC
USB ModeHardware CDC and JTAG
Flash Size8 MB
Partition Scheme”8M with spiffs (3MB APP/1.5MB SPIFFS)“Or similar
PSRAMOPI PSRAMMandatory — the #1 M5StickS3 setting that gets missed
Core Debug LevelNone (production) or Info (dev)
PortAuto-detect from Tools → Port/dev/ttyACM0 typical

Hello-world sketch:

#include <M5Unified.h>

void setup() {
    auto cfg = M5.config();
    M5.begin(cfg);

    M5.Display.setTextSize(2);
    M5.Display.setCursor(10, 10);
    M5.Display.println("Hello, M5StickS3!");
}

void loop() {
    M5.update();

    if (M5.BtnA.wasPressed()) {
        M5.Speaker.tone(440, 100);   // 440 Hz beep on button A
        M5.Display.printf("BtnA pressed\n");
    }

    delay(10);
}

Upload, see “Hello, M5StickS3!” on screen. Press Button A — beep + log message.


Setup:

  • CLI: pip install -U platformio
  • IDE: VS Code + PlatformIO extension

Project skeleton for M5StickS3:

platformio.ini:

[env:m5sticks3]
platform = [email protected]
board = esp32-s3-devkitc-1                   ; Or "m5stick-s3" if M5Stack publishes
framework = arduino
upload_speed = 1500000
monitor_speed = 115200
board_build.partitions = default_8MB.csv
board_build.arduino.memory_type = qio_opi    ; ← MANDATORY: OPI PSRAM on PICO-1-N8R8
build_flags =
    -DBOARD_HAS_PSRAM
    -DARDUINO_USB_CDC_ON_BOOT=1
    -DARDUINO_USB_MODE=1
    -DESP32S3
    -DCORE_DEBUG_LEVEL=3
lib_deps =
    https://github.com/m5stack/M5Unified.git
    https://github.com/m5stack/M5GFX.git

src/main.cpp:

#include <Arduino.h>
#include <M5Unified.h>

void setup() {
    auto cfg = M5.config();
    M5.begin(cfg);

    M5.Display.setTextSize(2);
    M5.Display.setCursor(10, 10);
    M5.Display.println("Hello PlatformIO");
}

void loop() {
    M5.update();
    delay(100);
}

Build + flash:

cd ~/m5sticks3-project
pio run -e m5sticks3                       # Compile
pio run -e m5sticks3 -t upload             # Compile + flash
pio device monitor                          # Serial monitor

Why PlatformIO over Arduino IDE for non-trivial projects:

  • Dependency pinninglib_deps locks library versions
  • Multi-environment[env:m5sticks3] + [env:cardputer-adv] in the same platformio.ini for cross-platform builds
  • Better source treesrc/ + lib/ matches how Bruce / Evil-M5Project source trees are laid out
  • Integrated debugger — JTAG / SWD via Espressif’s USB-JTAG bridge
  • CI/CD friendlypio run works in GitHub Actions

For modifying existing community firmwares (Evil-M5Project, Bruce, MicroHydra): they all use PlatformIO. Clone the repo, open in VS Code, build.


4. MicroPython + mpremote

Setup:

  1. Flash M5Stack-flavored MicroPython to M5StickS3 via M5Burner (UiFlow firmware = underlying MicroPython runtime).
  2. Install mpremote: pip install -U mpremote
  3. Access REPL: mpremote connect /dev/ttyACM0 repl

Hello-world:

# At the REPL:
import M5
M5.begin()

# Display text
Lcd.setFont(Lcd.FONT_DejaVu24)
Lcd.print("Hello MicroPython", 10, 10)

# Read button
btn_state = M5.BtnA.read()
print(f"Button A: {btn_state}")

# Read audio
import audio
mic_buf = bytearray(256)
audio.record(mic_buf, 256)

# Play tone
M5.Speaker.tone(440, 100)

File operations:

# Upload a script
mpremote connect /dev/ttyACM0 cp myapp.py :/myapp.py

# Run a script
mpremote connect /dev/ttyACM0 exec "exec(open('/myapp.py').read())"

# List files
mpremote connect /dev/ttyACM0 ls /

# Delete a file
mpremote connect /dev/ttyACM0 rm /myapp.py

# Soft reset
mpremote connect /dev/ttyACM0 reset

Why MicroPython over Arduino/PlatformIO:

  • No compile step — write Python, run immediately. Edit-test loop measured in seconds.
  • REPL — interactive exploration of the hardware. Probe sensors, test display, experiment.
  • Easier for non-C++ devs — Python syntax is more accessible.

Why NOT MicroPython:

  • ~50% slower than compiled C++ for typical workloads. CPU-bound code (audio FFT, packet capture at rate) is materially slower.
  • More RAM-hungry (interpreter overhead — but M5StickS3 has 8 MB PSRAM, so headroom is plentiful).
  • Smaller library ecosystem than Arduino.

For M5StickS3: MicroPython is excellent for scripting + learning + custom MicroHydra apps + audio prototypes that benefit from REPL iteration.


5. UiFlow 2 (block coding)

URL: https://flow.m5stack.com

Setup:

  1. Flash UiFlow 2 firmware to M5StickS3 via M5Burner.
  2. Open https://flow.m5stack.com in browser. Pair via USB-CDC (Web Serial).
  3. Drag blocks. Compile to MicroPython under the hood. Run.

Block categories:

  • M5StickS3 blocks: display, buttons, IMU, IR, mic, speaker
  • M5 Units / HATs / Caps: drag-and-drop blocks for every M5Stack peripheral with auto-detected pin assignments
  • Logic blocks: if/else, loops, conditions
  • Math blocks: arithmetic, comparison
  • Variables blocks
  • Functions blocks
  • Audio blocks: recording, playback, FFT (the M5StickS3-distinctive blocks)
  • Wi-Fi / BLE blocks

Why UiFlow over MicroPython / Arduino:

  • Lowest barrier of any path. Education, kids, non-coders, very rapid prototyping.
  • Visual debugging — see program flow as blocks light up during execution.

Power users typically graduate to MicroPython (via mpremote) once they hit UiFlow’s expressiveness ceiling — the underlying MicroPython is the natural next step (UiFlow generates MicroPython, so the transition is gradual).


6. ESP-IDF (low-level)

Setup:

# Clone ESP-IDF
git clone -b master --recursive https://github.com/espressif/esp-idf.git ~/esp-idf
cd ~/esp-idf

# Install toolchain for ESP32-S3
./install.sh esp32s3

# Source the environment (every shell session)
. ./export.sh

Hello-world project:

cd ~/projects
idf.py create-project m5sticks3_hello
cd m5sticks3_hello
idf.py set-target esp32s3
idf.py menuconfig                  # Configure: USB CDC on boot, flash 8 MB, etc.
idf.py build
idf.py -p /dev/ttyACM0 -b 1500000 flash monitor

Why ESP-IDF over Arduino + PlatformIO:

  • Custom partition tables — Arduino’s partition options are predefined; ESP-IDF lets you build any layout
  • Native FreeRTOS — direct task scheduling, custom IPC, priority management
  • Audio framework esp-adf — full integration with Espressif’s audio framework for advanced audio work
  • esp-skainet integration — wake-word + speech-command recognition; ESP-IDF is the cleanest path
  • Secure boot v2 — Arduino doesn’t expose the full toolchain
  • Flash encryption — full ESP-IDF support
  • 802.11 frame injection — works in Arduino too, but ESP-IDF has more reliable APIs

Why NOT ESP-IDF:

  • Steeper learning curve
  • More boilerplate per project
  • Smaller M5Stack-specific community than Arduino + PlatformIO

For M5StickS3: reach for ESP-IDF when audio work needs esp_codec_dev direct access, when implementing wake-word detection via esp-skainet, or when custom partition / secure boot is required.


7. M5Unified library reference for M5StickS3

Key APIs from M5Unified specifically relevant to M5StickS3:

#include <M5Unified.h>

void setup() {
    auto cfg = M5.config();
    M5.begin(cfg);
    // M5StickS3 is auto-detected via M5Unified runtime board-detection
    // Display, mic, speaker, IMU, IR all initialized
}

void loop() {
    M5.update();   // Polls buttons, IMU — call every loop iteration

    // Display (M5GFX-derived)
    M5.Display.setTextColor(WHITE, BLACK);
    M5.Display.setTextSize(2);
    M5.Display.setCursor(10, 20);
    M5.Display.printf("Hello %d", millis());
    M5.Display.drawLine(0, 0, 100, 100, RED);

    // Buttons
    if (M5.BtnA.wasPressed())  { /* Button A */ }
    if (M5.BtnA.isHeld())      { /* Button A held */ }
    if (M5.BtnB.wasPressed())  { /* Button B */ }
    if (M5.BtnPWR.wasReleased()) { /* Power button */ }

    // Audio — Mic
    int16_t mic_buf[256];
    M5.Mic.record(mic_buf, 256);    // 16 kHz mono 16-bit default

    // Audio — Speaker
    M5.Speaker.tone(440, 100);                  // 440 Hz tone for 100 ms
    M5.Speaker.playRaw(audio_buf, length);     // Play raw PCM audio
    M5.Speaker.setVolume(180);                  // 0-255 volume

    // IMU (BMI270 or MPU6886 — M5Unified handles both)
    auto accel = M5.Imu.getAccelData();
    float ax = accel.x, ay = accel.y, az = accel.z;
    auto gyro = M5.Imu.getGyroData();

    // Battery
    int batt_level = M5.Power.getBatteryLevel();        // 0-100
    float batt_voltage = M5.Power.getBatteryVoltage();

    // IR — use M5Unified IR helpers or IRremoteESP8266 library
}

M5Unified runtime board detection: M5Unified inspects the device at boot to determine if it’s a Cardputer ADV, M5StickS3, M5StickC Plus 2, etc. The same code can run on multiple M5Stack devices without source modification — M5Unified handles the pin/peripheral mapping internally.

Display-specific notes:

  • M5StickS3’s ST7789P3 controller is slightly different from Cardputer ADV’s ST7789V2, but M5Unified handles both transparently. Application code is identical.
  • Default rotation is portrait (135 wide × 240 tall). For landscape: M5.Display.setRotation(1).

Audio-specific notes:

  • ES8311 codec is initialized by M5.begin(cfg) — applications don’t need to handle codec init manually.
  • For lower-level audio control (full-duplex mic+speaker, custom sample rates), use esp_codec_dev directly (Vol 5 § 3).

IMU-specific notes:

  • M5.Imu.getType() returns the detected IMU chip type (m5::imu_bmi270_t or m5::imu_mpu6886_t).
  • Both have similar API surfaces; if accessing chip-specific features (BMI270’s on-chip step counter), check the type first.

8. Build flag essentials (OPI PSRAM is mandatory)

The OPI PSRAM build flag is the most-commonly-missed M5StickS3 setting:

board_build.arduino.memory_type = qio_opi

This translates to ESP-IDF as CONFIG_SPIRAM_MODE_OCT=y. Without it:

  • ESP.getFreePsram() returns 0
  • Any firmware that assumes PSRAM availability degrades silently (smaller buffers, fewer features)
  • Some firmwares (esp-skainet wake-word, large audio recording) fail entirely

Critical: every M5StickS3 build must include this flag. PlatformIO’s platformio.ini has the directive; Arduino IDE has it under Tools → PSRAM → “OPI PSRAM”.

Other M5StickS3-specific build flags:

FlagPurposeRequired
board_build.arduino.memory_type = qio_opiOPI PSRAMYes — mandatory
-DBOARD_HAS_PSRAMStandard PSRAM identifierYes
-DARDUINO_USB_CDC_ON_BOOT=1Native USB-CDC at bootYes for serial console
-DARDUINO_USB_MODE=1Device mode (1) or Host OTG (0)1 for typical apps; 0 for USB-host work
-DESP32S3Implicit but explicit for clarityOptional
-DCORE_DEBUG_LEVEL=NSerial debug verbosity (0=none, 5=verbose)0 production, 3 dev
Display flags (TFT_*)If using TFT_eSPI directlyOptional — M5GFX wraps

9. When to use which environment

Use caseEnvironmentWhy
Learning / educationUiFlow 2 → MicroPythonLowest barrier, visual feedback
Quick test / one-off sketchArduino IDECompile + flash in one click
Rapid scripting prototypeMicroPython + mpremoteREPL iteration
Multi-file production projectPlatformIOReproducible builds
Modifying community firmware (Evil-M5Project, Bruce, MicroHydra)PlatformIOAll use PlatformIO
Custom partition tablePlatformIO or ESP-IDFArduino has predefined options only
Wake-word detection (esp-skainet)ESP-IDFCleanest path
Custom audio (esp_codec_dev direct)ESP-IDFFull control
802.11 frame injectionArduino + PlatformIOesp_wifi_set_promiscuous works
Secure boot v2ESP-IDFRequired toolchain access
BadUSB DuckyScript runnerPlatformIO + custom firmwareCompile a BadCard-class firmware
Voice memo recorderPlatformIO + ArduinoM5Unified APIs sufficient

The recommended default for tjscientist: Arduino IDE for first-time learning + PlatformIO for serious development.


10. Sketch / project skeletons

Three canonical project skeletons (would live in ../../04-templates/ once authored):

Arduino single-file .ino (one-off sketches):

#include <M5Unified.h>

void setup() {
    auto cfg = M5.config();
    M5.begin(cfg);
    M5.Display.println("Hello");
}

void loop() {
    M5.update();
    // ... your code ...
    delay(10);
}

PlatformIO multi-file project:

m5sticks3-project/
├── platformio.ini
├── src/
│   ├── main.cpp
│   └── my_module.cpp
├── include/
│   └── my_module.h
└── lib/                    ; auto-fetched via lib_deps

MicroHydra app (.py on SD-via-Hat2 or internal flash at /apps/<AppName>/__init__.py):

from lib.hydra.app import App
from lib.display import Display

class HelloApp(App):
    name = "HelloWorld"
    icon = "rocket"
    def main(self):
        d = Display()
        d.text("Hello", 10, 10)
        while not self.exit_pressed():
            self.tick()

11. Resources

Tools / IDEs

M5Stack libraries

Espressif docs

MicroPython

UiFlow 2

Cross-references

  • Cardputer ADV Vol 7 (broader programming-environment coverage): ../../../M5Stack Cardputer ADV/03-outputs/Cardputer_ADV_Complete.html
  • Custom firmware development: Vol 10
  • Audio APIs deep dive: Vol 5 § 3

This is Volume 7 of a twelve-volume series. Next: Vol 8 covers flashing and updating firmware — web flashers + M5Burner + esptool.py + native USB-CDC discovery + OTA + recovery.