Skip to content

Repository files navigation

StickS3 Tuner

A portable chromatic instrument tuner for the M5Stack StickS3 (ESP32-S3-PICO-1-N8R8), built on ESP-IDF v6.0.1.

mic (ES8311 / I2S) -> FFT autocorrelation pitch -> nearest note + cents -> LCD (LVGL)

The screen shows the nearest note (e.g. A4), a centered cents bar, and the detected frequency. It turns green when within ±5 cents, amber when off pitch, and shows -- when no note is present. Works for guitar, bass, ukulele, voice, etc.

Quick start

StickS3 Tuner — quick start

Interactive: docs/quick-start.html (this one-pager) · docs/user-guide.html (fuller end-user guide). For players — no build details.

Flash it in your browser ⚡

No toolchain needed — flash a StickS3 straight from Chrome or Edge over USB with ESP Launchpad:

Flash with ESP Launchpad

Plug in the StickS3, click Connect, choose its serial port, then Flash — it writes the prebuilt firmware over Web Serial. The config and binary are served from GitHub Pages (the gh-pages branch), which sends the CORS headers the in-browser flasher needs. See Publishing a web-flash build to update it.

Layout

File Role
main/board.h All StickS3 GPIO / I2C / I2S pin assignments
main/power.c M5PM1 PMIC bring-up — enables the L3B rail (codec + LCD backlight)
main/audio.c ES8311 codec + I2S-std RX, audio_read()
main/pitch.c FFT-accelerated autocorrelation pitch estimator (esp-dsp SIMD)
main/note.c Frequency → note name + cents (adjustable A4 reference)
main/buttons.c Front/side buttons → play tone / reference toggle
main/imu.c BMI270 tilt → playback-note selector
main/ir.c IR TX/RX (NEC via RMT) — the prank easter egg
main/ui.c ST7789 + LVGL 9 tuner screen
main/sticks3_tuner.c app_main: power, I2C bus, analysis task

Managed dependencies (auto-fetched on first build): espressif/esp_codec_dev, espressif/esp-dsp, espressif/bmi270, lvgl/lvgl.

Build & flash

idf.py set-target esp32s3
idf.py build
idf.py -p COMx flash monitor      # replace COMx with the StickS3 port

Publishing a web-flash build

The browser button flashes a single merged image (bootloader + partition table + app, written at 0x0), served from GitHub Pages on the gh-pages branch. (Pages sends the CORS headers the in-browser flasher needs — GitHub Release asset downloads don't, so they can't be fetched by esptool-js.) To update the firmware:

idf.py set-target esp32s3
idf.py build
idf.py merge-bin -o sticks3-tuner-esp32s3.bin     # one image, flashed at 0x0

Replace sticks3-tuner-esp32s3.bin on the gh-pages branch with the new build and push — the file name must match image.esp32s3 in launchpad.toml (hyphen-less chip key, per ESP Launchpad).

One-time setup: in repo Settings → Pages, set the source to Deploy from a branchgh-pages / / (root). The site then serves at https://tanyanquan.github.io/sticks3-tuner/.

On-device tuning knobs

These depend on the exact panel/board revision — adjust if needed and rebuild:

  • No sound / mic silent — the ES8311 rail (L3B) is enabled by power.c over the M5PM1 PMIC at boot. The startup I2C scan should list 0x18 (codec), 0x68 (IMU), 0x6e (PMIC); if 0x18 is missing, L3B didn't come up. The codec is configured mono / left slot; a stereo config reads all-zeros on this codec. Mic data pin: the mic (codec DOUT) is on GPIO16, speaker (codec DIN) on GPIO14 — the reverse of the M5 web docs (see board.h). Reading the wrong one gives exact 0x0000. Mic gain: MIC_GAIN_DB in audio.c (42 dB).
  • Speaker hiss (optional) — the codec runs full-duplex (WORK_MODE_BOTH). If the speaker hisses at idle, switch to input-only: WORK_MODE_ADC / DEV_TYPE_IN and a RX-only I2S channel in audio.c.
  • Display shifted — tweak BOARD_LCD_X_GAP / BOARD_LCD_Y_GAP in board.h.
  • Display mirrored / upside-down — adjust esp_lcd_panel_mirror / swap_xy in ui.c.
  • Colors wrong — flip rgb_ele_order (BGR↔RGB) or esp_lcd_panel_invert_color in ui.c.
  • Backlight inverted — flip BOARD_LCD_BL_ON in board.h.
  • Sensitivity / stability — pitch detection is FFT-accelerated autocorrelation (esp-dsp SIMD) over a 4096-sample window, sliding by HOP_SAMPLES (~15 updates/s). To pick up weaker signals, lower RMS_GATE / CLARITY_GATE in pitch.c or raise MIC_GAIN_DB. To reject noise blips, raise them. HOLD_FRAMES in sticks3_tuner.c controls how long a note lingers as a string decays; PITCH_WINDOW (power of two; zero-padded to a 2*PITCH_WINDOW FFT for a linear autocorrelation, so keep 2*PITCH_WINDOW <= CONFIG_DSP_MAX_FFT_SIZE) trades latency for low-note SNR.

Controls

  • KEY1 (front, GPIO11) — play + tilt-bend. Hold to play a reference tone out the speaker; while held, tilt the stick (Y axis) to slide the note up/down semitone by semitone (range D4–D6), faster the more you tilt, hold level to settle. The mic picks up the speaker, so the played note shows on the main display. Tone synth in audio_tone_start/feed/stop(); tilt in imu.c (BMI270 accelerometer, only active while KEY1 is held). The AW8737 amp is enabled on the first press — with a short AMP_SETTLE_MS delay so its power-on transient settles into silence before the tone — and lazily disabled AMP_HOLD_MS after the last tone to save power.
  • KEY2 (side, GPIO12) — toggle the A4 reference between 442 (default, Chinese-orchestra standard) and 440. Shown top-left as A=NNN.
  • Battery level shown top-right (read from the PMIC VBAT register).

Easter egg (IR prank) 🎵

The device always listens on IR (RX GPIO42). Press KEY1 five times quickly while the tuner is reading F4 to enter a hidden prank mode (small IR shows up top):

  • KEY1 transmits an IR signature (TX GPIO46, 38 kHz NEC).
  • KEY2 exits.

Any StickS3 running this firmware that receives the signature flashes a big music note for 2 s — so point it at your orchestra mates' tuners and watch. Signature + trigger note are tunable in ir.c / buttons.c.

Battery measures:

  • Backlight PWM-dimmed to BL_LEVEL_IDLE after BL_IDLE_TIMEOUT_MS (20 s) of no activity, full brightness on any activity — the largest saver (tunables in ui.c). "Activity" = a button, movement, or sustained close-mic playing (ACTIVITY_FRAMES / ACTIVITY_LEVEL in sticks3_tuner.c), not any detected pitch — so stray room noise in a band/orchestra setting doesn't keep it lit (or block the power-off below).
  • Idle auto-power-off — after SHUTDOWN_WARN_MS (2 min) of inactivity a SHUTDOWN_GRACE_MS (15 s) on-screen countdown ("Powering off in Ns, tap to keep awake") appears, and the device powers off when it elapses; any activity cancels it. Off is a true hard-off via the M5PM1 SYS_CMD power-off command (power_off() clears HOLD_CFG first so no rail stays latched, with an ESP deep-sleep/KEY1-wake backstop); the device restarts on the power button. Skipped while on USB-C powerpower_external_present() reads the PMIC PWR_SRC register (5VIN present), so it stays on whether charging or just powered. Timeouts are tunable in ui.c.
  • CPU at 80 MHz (sdkconfig.defaults) — halves active CPU power; FFT + LVGL still fit. Peripheral clocks (I2S/SPI) are PLL-sourced and unaffected.
  • Speaker amp enabled only around tone playback (lazy-disabled after AMP_HOLD_MS), not held on continuously.

Note spelling uses the common band/orchestra mix — sharps for C#/F#/G#, flats for Eb/Bb (NAMES[] in note.c).

Tone too loud/quiet or silent? Adjust TONE_VOL/TONE_AMP in audio.c and AW8737_GAIN_PULSES (1–3) / AW8737_MODE_GPIO in power.c.

License

MIT — see LICENSE. Use it freely (including commercially); just keep the copyright notice.

Third-party code keeps its own license: main/ir_nec_encoder.{c,h} is from the ESP-IDF examples and stays Apache-2.0 (see its SPDX header), and the managed dependencies are under their own terms (esp_codec_dev / esp-dsp / bmi270: Apache-2.0; lvgl: MIT).

About

A portable chromatic instrument tuner for the M5Stack StickS3 (ESP32-S3-PICO-1-N8R8), built on ESP-IDF v6.0.1.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages