// ============================================================ // INTERMISSION OBJECT — Board Test // XIAO ESP32-S3 · Custom PCB // ============================================================ // Tests all five subsystems in sequence, looping forever. // // 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 haptic driver // 6 D5/SCL I2C SCL → TM6605 haptic driver // 7 D8 Motor PWM → MOSFET gate (Q2) // 8 D9/ADC1 Hall IN → A3144 output (digital, active-low) // ============================================================ #include #include #include "driver/i2s.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 // ── LEDC (PWM) ─────────────────────────────────────────────── #define PWM_FREQ_MOTOR 20000 // 20 kHz — inaudible motor whine #define PWM_FREQ_LED 1000 // 1 kHz — smooth LED dimming #define PWM_RES 8 // 8-bit → 0–255 // ── Motor test ─────────────────────────────────────────────── #define MOTOR_MEDIUM 140 // ~55 % duty — medium speed // ── I2S / audio ────────────────────────────────────────────── #define SAMPLE_RATE 22050 #define TONE_HZ 120 // low, resonant tone #define TONE_AMPLITUDE 8000 // 0–32767; modest for speaker safety // ── TM6605 haptic driver (I2C) ─────────────────────────────── // DFRobot Gravity TM6605 default I2C address #define TM6605_ADDR 0x5A // TM6605 register map (subset used here) #define REG_STATUS 0x00 #define REG_MODE 0x01 #define REG_RTP 0x02 // real-time playback level #define REG_LIB 0x03 // waveform library select #define REG_WAVEFORM_1 0x04 // first waveform sequence slot #define REG_GO 0x0C // ──────────────────────────────────────────────────────────── // Hall sensor ISR // ──────────────────────────────────────────────────────────── volatile uint32_t hallPulseCount = 0; volatile uint32_t lastPulseMs = 0; void IRAM_ATTR hallISR() { hallPulseCount++; lastPulseMs = millis(); } // ──────────────────────────────────────────────────────────── // I2S helpers // ──────────────────────────────────────────────────────────── void i2s_init() { i2s_config_t cfg = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), .sample_rate = SAMPLE_RATE, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 8, .dma_buf_len = 256, .use_apll = false, .tx_desc_auto_clear = true, }; i2s_pin_config_t pins = { .bck_io_num = PIN_I2S_BCLK, .ws_io_num = PIN_I2S_LRC, .data_out_num = PIN_I2S_DIN, .data_in_num = I2S_PIN_NO_CHANGE, }; i2s_driver_install(I2S_NUM_0, &cfg, 0, NULL); i2s_set_pin(I2S_NUM_0, &pins); } // Synthesise a sine tone and stream it for durationMs milliseconds void i2s_play_tone(uint32_t durationMs) { const int bufSamples = 256; int16_t buf[bufSamples * 2]; // stereo uint32_t start = millis(); double phase = 0.0; double phaseInc = 2.0 * PI * TONE_HZ / SAMPLE_RATE; while (millis() - start < durationMs) { for (int i = 0; i < bufSamples; i++) { int16_t s = (int16_t)(TONE_AMPLITUDE * sin(phase)); buf[i * 2] = s; // L buf[i * 2 + 1] = s; // R phase += phaseInc; if (phase > 2.0 * PI) phase -= 2.0 * PI; } size_t written; i2s_write(I2S_NUM_0, buf, sizeof(buf), &written, portMAX_DELAY); } i2s_zero_dma_buffer(I2S_NUM_0); } // ──────────────────────────────────────────────────────────── // TM6605 helpers // ──────────────────────────────────────────────────────────── void tm_write(uint8_t reg, uint8_t val) { Wire.beginTransmission(TM6605_ADDR); Wire.write(reg); Wire.write(val); uint8_t err = Wire.endTransmission(); if (err) { Serial.printf(" [TM6605] I2C error %d on reg 0x%02X\n", err, reg); } } uint8_t tm_read(uint8_t reg) { Wire.beginTransmission(TM6605_ADDR); Wire.write(reg); Wire.endTransmission(false); Wire.requestFrom((uint8_t)TM6605_ADDR, (uint8_t)1); return Wire.available() ? Wire.read() : 0xFF; } // Soft pulse: triangle-envelope the RTP level up then back down void tm_soft_pulse(uint32_t durationMs) { tm_write(REG_MODE, 0x05); // enter RTP (real-time playback) mode uint32_t start = millis(); while (millis() - start < durationMs) { float t = (float)(millis() - start) / durationMs; // 0 → 1 float env = (t < 0.5f) ? (t * 2.0f) : (2.0f - t * 2.0f); // triangle tm_write(REG_RTP, (uint8_t)(env * 80.0f)); // cap at 80/127 — gentle delay(10); } tm_write(REG_RTP, 0x00); // silence tm_write(REG_MODE, 0x40); // standby } // ──────────────────────────────────────────────────────────── // setup() // ──────────────────────────────────────────────────────────── void setup() { Serial.begin(115200); delay(600); Serial.println("\n=== INTERMISSION OBJECT — Board Test ===\n"); // PWM channels 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 has onboard pull-ups, no external R needed Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL); // I2S — MAX98357A i2s_init(); // Hall sensor — A3144 output is active-low pinMode(PIN_HALL, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(PIN_HALL), hallISR, 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) ───────────────────────"); Serial.printf(" duty=%d/255 freq=%d kHz\n", MOTOR_MEDIUM, PWM_FREQ_MOTOR / 1000); ledcWrite(PIN_MOTOR_PWM, MOTOR_MEDIUM); delay(3000); ledcWrite(PIN_MOTOR_PWM, 0); Serial.println(" Motor off.\n"); delay(500); // ══════════════════════════════════════════════════════════ // TEST 2 — LED filaments, soft sine-wave pulse (two cycles) // ══════════════════════════════════════════════════════════ Serial.println("── TEST 2: LED filaments (soft pulse) ───────────────"); uint32_t ledStart = millis(); while (millis() - ledStart < 4000) { // Sine envelope: one full pulse every 2 s float t = (float)(millis() - ledStart) / 2000.0f; // 0→2 float env = 0.5f * (1.0f - cosf(t * PI)); // 0→1→0 uint8_t pwm = (uint8_t)(env * 220.0f); // cap at ~86 % ledcWrite(PIN_LED_PWM, pwm); delay(10); } ledcWrite(PIN_LED_PWM, 0); Serial.println(" LED pulse complete.\n"); delay(500); // ══════════════════════════════════════════════════════════ // TEST 3 — Speaker, 120 Hz sine tone via I2S // ══════════════════════════════════════════════════════════ Serial.println("── TEST 3: Speaker (I2S, 120 Hz) ────────────────────"); Serial.printf(" Playing %d Hz for 3 s\n", TONE_HZ); i2s_play_tone(3000); Serial.println(" Tone complete.\n"); delay(500); // ══════════════════════════════════════════════════════════ // TEST 4 — LRA haptic, two soft pulses via I2C // ══════════════════════════════════════════════════════════ Serial.println("── TEST 4: LRA haptic (I2C, TM6605) ────────────────"); uint8_t status = tm_read(REG_STATUS); if (status == 0xFF) { Serial.println(" WARNING: no ACK from TM6605 — check address / wiring"); } else { Serial.printf(" TM6605 status=0x%02X (device OK)\n", status); for (int i = 0; i < 2; i++) { Serial.printf(" Pulse %d/2\n", i + 1); tm_soft_pulse(600); delay(400); } Serial.println(" Haptic complete."); } Serial.println(); delay(500); // ══════════════════════════════════════════════════════════ // TEST 5 — Hall effect sensor // Spin the rotor with magnets past the sensor during this window. // ══════════════════════════════════════════════════════════ Serial.println("── TEST 5: Hall effect sensor (5 s window) ──────────"); Serial.println(" → Spin the rotor now..."); uint32_t prevCount = hallPulseCount; uint32_t hallStart = millis(); while (millis() - hallStart < 5000) { if (hallPulseCount != prevCount) { Serial.printf(" ★ Magnet pass! count=%lu\n", hallPulseCount); prevCount = hallPulseCount; } delay(20); } Serial.printf(" Total pulses detected this session: %lu\n\n", hallPulseCount); // ══════════════════════════════════════════════════════════ Serial.println("══ All tests complete. Repeating in 2 s... ══\n"); delay(2000); }