// ============================================================ // INTERMISSION OBJECT — Board Test (rev 3) // XIAO ESP32-S3 · Custom PCB // ============================================================ // // GPIO Label Function // ---- -------- ------------------------------------------ // 1 D0/A0 I2S DIN → MAX98357A // 2 D1/A1 I2S LRC → MAX98357A // 3 D2/A2 I2S BCLK → MAX98357A // 4 D3 LED PWM → MOSFET gate (Q1) // 5 D4/SDA I2C SDA → TM6605 // 6 D5/SCL I2C SCL → TM6605 // 7 D8 Motor PWM → MOSFET gate (Q2) // 8 D9/ADC1 Hall IN → A3144 OUT (10kΩ pullup to 3V3 on PCB) // // Libraries required: // - "AudioTools" by pschatzmann (Library Manager) // - "DFRobot_TM6605" (Library Manager) // ============================================================ #include "AudioTools.h" #include #include "DFRobot_TM6605.h" // ── Pin definitions ────────────────────────────────────────── #define PIN_I2S_DIN 1 #define PIN_I2S_LRC 2 #define PIN_I2S_BCLK 3 #define PIN_LED_PWM 4 #define PIN_I2C_SDA 5 #define PIN_I2C_SCL 6 #define PIN_MOTOR_PWM 7 #define PIN_HALL 8 // ── PWM ────────────────────────────────────────────────────── #define PWM_FREQ_MOTOR 20000 // 20 kHz — inaudible motor whine #define PWM_FREQ_LED 24989 // prime, above camera flicker #define PWM_RES 8 // 8-bit → 0–255 #define MOTOR_MEDIUM 140 // ~55 % duty // ── Audio ───────────────────────────────────────────────────── #define TONE_HZ 240 #define SAMPLE_RATE 44100 #define CHANNELS 2 #define BITS 16 #define TONE_AMPLITUDE 18000 // 0–32767 // ── Hall sensor debounce ────────────────────────────────────── #define DEBOUNCE_MS 5 // ── Globals ─────────────────────────────────────────────────── DFRobot_TM6605 TM6605; I2SStream i2s; SineWaveGenerator sine; GeneratedSoundStream sound(sine); StreamCopy copier(i2s, sound); AudioInfo audioInfo(SAMPLE_RATE, CHANNELS, BITS); volatile bool pulseFlag = false; volatile uint32_t lastPulseUs = 0; unsigned long lastPulseTime = 0; int pulseCount = 0; // ──────────────────────────────────────────────────────────── // Hall ISR // ──────────────────────────────────────────────────────────── void IRAM_ATTR onPulse() { uint32_t now = micros(); if (now - lastPulseUs > (DEBOUNCE_MS * 1000)) { lastPulseUs = now; pulseFlag = true; } } // ──────────────────────────────────────────────────────────── // LRA helper — plays an effect and waits, then stops // ──────────────────────────────────────────────────────────── void playEffect(DFRobot_TM6605::eEffect_t effect, int duration) { TM6605.selectEffect(effect); TM6605.play(); delay(duration); TM6605.stop(); delay(50); } // ──────────────────────────────────────────────────────────── // setup() // ──────────────────────────────────────────────────────────── void setup() { Serial.begin(115200); AudioToolsLogger.begin(Serial, AudioToolsLogLevel::Warning); delay(600); Serial.println("\n=== INTERMISSION OBJECT — Board Test (rev 3) ===\n"); // ── PWM ── ledcAttach(PIN_MOTOR_PWM, PWM_FREQ_MOTOR, PWM_RES); ledcAttach(PIN_LED_PWM, PWM_FREQ_LED, PWM_RES); ledcWrite(PIN_MOTOR_PWM, 0); ledcWrite(PIN_LED_PWM, 0); // ── I2C + TM6605 ── Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL); Serial.print("TM6605 init... "); int attempts = 0; while (TM6605.begin() != 0) { Serial.println("not found, retrying..."); delay(1000); if (++attempts >= 5) { Serial.println("TM6605 failed after 5 attempts — continuing without haptics."); break; } } if (attempts < 5) Serial.println("success"); // ── I2S ── auto cfg = i2s.defaultConfig(TX_MODE); cfg.pin_bck = PIN_I2S_BCLK; cfg.pin_ws = PIN_I2S_LRC; cfg.pin_data = PIN_I2S_DIN; cfg.copyFrom(audioInfo); i2s.begin(cfg); sine.begin(audioInfo, TONE_HZ); sine.setAmplitude(TONE_AMPLITUDE); sound.begin(audioInfo); // ── Hall sensor ── pinMode(PIN_HALL, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(PIN_HALL), onPulse, FALLING); Serial.println("All peripherals initialised. Starting test loop.\n"); } // ──────────────────────────────────────────────────────────── // loop() // ──────────────────────────────────────────────────────────── void loop() { // TEST 1 — Motor at medium PWM Serial.println("── TEST 1: Motor (medium PWM) ───────────────────────"); ledcWrite(PIN_MOTOR_PWM, MOTOR_MEDIUM); delay(3000); ledcWrite(PIN_MOTOR_PWM, 0); Serial.println(" Motor off.\n"); delay(500); // TEST 2 — LED filaments, two slow sine-envelope pulses Serial.println("── TEST 2: LED filaments (soft pulse) ───────────────"); uint32_t ledStart = millis(); while (millis() - ledStart < 4000) { float t = (float)(millis() - ledStart) / 2000.0f; float env = 0.5f * (1.0f - cosf(t * PI)); uint8_t pwm = (uint8_t)(env * 220.0f); ledcWrite(PIN_LED_PWM, pwm); delay(10); } ledcWrite(PIN_LED_PWM, 0); Serial.println(" LED pulse complete.\n"); delay(500); // TEST 3 — Speaker: 240 Hz via I2S Serial.println("── TEST 3: Speaker (I2S, 240 Hz) ────────────────────"); uint32_t toneStart = millis(); while (millis() - toneStart < 3000) { copier.copy(); } i2s.end(); { auto cfg2 = i2s.defaultConfig(TX_MODE); cfg2.pin_bck = PIN_I2S_BCLK; cfg2.pin_ws = PIN_I2S_LRC; cfg2.pin_data = PIN_I2S_DIN; cfg2.copyFrom(audioInfo); i2s.begin(cfg2); } Serial.println(" Tone complete.\n"); delay(500); // TEST 4 — LRA haptic: one breath cycle via DFRobot_TM6605 Serial.println("── TEST 4: LRA haptic (TM6605) ──────────────────────"); Serial.println(" Breathe in..."); playEffect(DFRobot_TM6605::eShortSlowBoostTransition2, 800); playEffect(DFRobot_TM6605::eMediumSlowBoostTransition2, 1000); playEffect(DFRobot_TM6605::eLongSlowBoostTransition1, 1500); delay(300); Serial.println(" Breathe out..."); playEffect(DFRobot_TM6605::eLongSlowFadeTransition1, 1500); playEffect(DFRobot_TM6605::eMediumSlowFadeTransition2, 1000); playEffect(DFRobot_TM6605::eShortSlowFadeTransition2, 800); Serial.println(" Haptic complete.\n"); delay(500); // TEST 5 — Hall effect sensor (5 s window) Serial.println("── TEST 5: Hall effect sensor (5 s window) ──────────"); Serial.println(" → Spin the rotor now..."); pulseCount = 0; unsigned long lastPrint = millis(); lastPulseTime = millis(); uint32_t hallWindowStart = millis(); while (millis() - hallWindowStart < 5000) { unsigned long now = millis(); if (pulseFlag) { pulseFlag = false; pulseCount++; lastPulseTime = now; } if (now - lastPrint >= 500) { lastPrint = now; float pulsesPerSec = pulseCount * 2.0f; float rpm = (pulsesPerSec / 10.0f) * 60.0f; bool dialMoving = (now - lastPulseTime) < 300; String status = dialMoving ? "MOVING" : "STILL"; Serial.print(" Pulses/s: "); Serial.print(pulsesPerSec, 1); Serial.print(" \tRPM: "); Serial.print(rpm, 1); Serial.print(" \tStatus: "); Serial.println(status); pulseCount = 0; } delay(5); } Serial.println("\n══ All tests complete. Repeating in 2 s... ══\n"); delay(2000); }