640 lines
18 KiB
Arduino
640 lines
18 KiB
Arduino
/*
|
|
* Baby Mobile Audio Board — Firmware
|
|
*
|
|
* ATmega328P @ 8MHz internal oscillator
|
|
* Plays 8-bit unsigned PCM audio from W25Q128 SPI flash
|
|
* Controls DC motor via PWM
|
|
* 8 button inputs with sleep/wake support
|
|
*
|
|
* Board: ATmega328P, 8MHz internal, BOD 1.8V
|
|
*
|
|
* Pin mapping:
|
|
* PB1 (OC1A, pin 9) - Audio PWM output
|
|
* PB2 (pin 10) - Flash ~CS
|
|
* PB3 (pin 11) - SPI MOSI
|
|
* PB4 (pin 12) - SPI MISO
|
|
* PB5 (pin 13) - SPI SCK
|
|
* PD5 (OC0B, pin 5) - Motor PWM
|
|
* PC2 (A2) - Amp ~SD (HIGH=on, LOW=off)
|
|
* PC3 (A3) - Status LED
|
|
* PC0 (A0) - Button 1
|
|
* PC1 (A1) - Button 2
|
|
* PD2 (pin 2) - Button 3 (INT0)
|
|
* PD3 (pin 3) - Button 4 (INT1)
|
|
* PD4 (pin 4) - Button 5
|
|
* PD6 (pin 6) - Button 6
|
|
* PD7 (pin 7) - Button 7
|
|
* PB0 (pin 8) - Button 8
|
|
*/
|
|
|
|
#include <avr/sleep.h>
|
|
#include <avr/power.h>
|
|
#include <avr/interrupt.h>
|
|
#include <SPI.h>
|
|
|
|
// ---- Pin definitions ----
|
|
#define PIN_AUDIO_PWM 9 // PB1 / OC1A
|
|
#define PIN_FLASH_CS 10 // PB2
|
|
#define PIN_MOTOR_PWM 5 // PD5 / OC0B
|
|
#define PIN_AMP_SD A2 // PC2 - amp shutdown (active HIGH = enabled)
|
|
#define PIN_LED A3 // PC3
|
|
#define PIN_BTN1 A0 // PC0
|
|
#define PIN_BTN2 A1 // PC1
|
|
#define PIN_BTN3 2 // PD2 / INT0
|
|
#define PIN_BTN4 3 // PD3 / INT1
|
|
#define PIN_BTN5 4 // PD4
|
|
#define PIN_BTN6 6 // PD6
|
|
#define PIN_BTN7 7 // PD7
|
|
#define PIN_BTN8 8 // PB0
|
|
|
|
// ---- Audio config ----
|
|
#define SAMPLE_RATE 8000 // Hz
|
|
#define FLASH_PAGE_SIZE 256
|
|
#define FLASH_SECTOR 4096
|
|
#define AUDIO_BUF_SIZE 256 // double buffer
|
|
|
|
// ---- Flash commands (W25Q128) ----
|
|
#define FLASH_CMD_READ 0x03
|
|
#define FLASH_CMD_FAST_READ 0x0B
|
|
#define FLASH_CMD_WRITE_EN 0x06
|
|
#define FLASH_CMD_PAGE_PROG 0x02
|
|
#define FLASH_CMD_SECT_ERASE 0x20
|
|
#define FLASH_CMD_CHIP_ERASE 0xC7
|
|
#define FLASH_CMD_READ_SR1 0x05
|
|
#define FLASH_CMD_JEDEC_ID 0x9F
|
|
#define FLASH_CMD_POWER_DOWN 0xB9
|
|
#define FLASH_CMD_WAKE 0xAB
|
|
|
|
// ---- Track table ----
|
|
// First 4KB (sector 0) of flash stores a track index:
|
|
// Offset 0x000: uint8_t num_tracks
|
|
// Offset 0x004: uint32_t track_start[32] (byte address in flash)
|
|
// Offset 0x084: uint32_t track_length[32] (length in bytes = samples)
|
|
// Audio data begins at sector 1 (address 0x1000).
|
|
|
|
#define TRACK_TABLE_ADDR 0x000000
|
|
#define AUDIO_START_ADDR 0x001000
|
|
#define MAX_TRACKS 32
|
|
|
|
// ---- State ----
|
|
volatile bool g_playing = false;
|
|
volatile bool g_motorOn = false;
|
|
volatile bool g_wakeFlag = false;
|
|
volatile uint8_t g_buttonPressed = 0;
|
|
|
|
uint8_t g_numTracks = 0;
|
|
uint32_t g_trackStart[MAX_TRACKS];
|
|
uint32_t g_trackLen[MAX_TRACKS];
|
|
uint8_t g_currentTrack = 0;
|
|
uint32_t g_playPos = 0; // current position in track (byte offset)
|
|
uint32_t g_playEnd = 0; // end position
|
|
uint8_t g_motorSpeed = 180; // PWM duty cycle (0-255)
|
|
|
|
// Double buffer for audio DMA-style playback
|
|
uint8_t g_audioBuf[2][AUDIO_BUF_SIZE];
|
|
volatile uint8_t g_activeBuf = 0; // buffer currently being played
|
|
volatile uint16_t g_bufPos = 0; // position in active buffer
|
|
volatile bool g_bufReady[2] = {false, false};
|
|
uint32_t g_nextReadAddr = 0;
|
|
|
|
// Auto-shutoff timer (milliseconds)
|
|
#define AUTO_OFF_MS (30UL * 60UL * 1000UL) // 30 minutes
|
|
unsigned long g_lastActivity = 0;
|
|
|
|
|
|
// ============================================================
|
|
// FLASH FUNCTIONS
|
|
// ============================================================
|
|
|
|
void flashSelect() { digitalWrite(PIN_FLASH_CS, LOW); }
|
|
void flashDeselect() { digitalWrite(PIN_FLASH_CS, HIGH); }
|
|
|
|
void flashWake() {
|
|
flashSelect();
|
|
SPI.transfer(FLASH_CMD_WAKE);
|
|
flashDeselect();
|
|
delayMicroseconds(5);
|
|
}
|
|
|
|
void flashSleep() {
|
|
flashSelect();
|
|
SPI.transfer(FLASH_CMD_POWER_DOWN);
|
|
flashDeselect();
|
|
}
|
|
|
|
uint32_t flashReadJEDEC() {
|
|
flashSelect();
|
|
SPI.transfer(FLASH_CMD_JEDEC_ID);
|
|
uint32_t id = (uint32_t)SPI.transfer(0) << 16;
|
|
id |= (uint32_t)SPI.transfer(0) << 8;
|
|
id |= SPI.transfer(0);
|
|
flashDeselect();
|
|
return id;
|
|
}
|
|
|
|
void flashReadBytes(uint32_t addr, uint8_t *buf, uint16_t len) {
|
|
flashSelect();
|
|
SPI.transfer(FLASH_CMD_READ);
|
|
SPI.transfer((addr >> 16) & 0xFF);
|
|
SPI.transfer((addr >> 8) & 0xFF);
|
|
SPI.transfer(addr & 0xFF);
|
|
for (uint16_t i = 0; i < len; i++) {
|
|
buf[i] = SPI.transfer(0);
|
|
}
|
|
flashDeselect();
|
|
}
|
|
|
|
void flashWaitBusy() {
|
|
flashSelect();
|
|
SPI.transfer(FLASH_CMD_READ_SR1);
|
|
while (SPI.transfer(0) & 0x01) { /* spin */ }
|
|
flashDeselect();
|
|
}
|
|
|
|
void flashWriteEnable() {
|
|
flashSelect();
|
|
SPI.transfer(FLASH_CMD_WRITE_EN);
|
|
flashDeselect();
|
|
}
|
|
|
|
void flashEraseSector(uint32_t addr) {
|
|
flashWriteEnable();
|
|
flashSelect();
|
|
SPI.transfer(FLASH_CMD_SECT_ERASE);
|
|
SPI.transfer((addr >> 16) & 0xFF);
|
|
SPI.transfer((addr >> 8) & 0xFF);
|
|
SPI.transfer(addr & 0xFF);
|
|
flashDeselect();
|
|
flashWaitBusy();
|
|
}
|
|
|
|
void flashPageProgram(uint32_t addr, const uint8_t *data, uint16_t len) {
|
|
flashWriteEnable();
|
|
flashSelect();
|
|
SPI.transfer(FLASH_CMD_PAGE_PROG);
|
|
SPI.transfer((addr >> 16) & 0xFF);
|
|
SPI.transfer((addr >> 8) & 0xFF);
|
|
SPI.transfer(addr & 0xFF);
|
|
for (uint16_t i = 0; i < len; i++) {
|
|
SPI.transfer(data[i]);
|
|
}
|
|
flashDeselect();
|
|
flashWaitBusy();
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// TRACK TABLE
|
|
// ============================================================
|
|
|
|
void loadTrackTable() {
|
|
uint8_t header[4];
|
|
flashReadBytes(TRACK_TABLE_ADDR, header, 4);
|
|
g_numTracks = header[0];
|
|
if (g_numTracks == 0 || g_numTracks == 0xFF || g_numTracks > MAX_TRACKS) {
|
|
g_numTracks = 0;
|
|
return;
|
|
}
|
|
|
|
// Read start addresses
|
|
uint8_t buf[4];
|
|
for (uint8_t i = 0; i < g_numTracks; i++) {
|
|
flashReadBytes(TRACK_TABLE_ADDR + 4 + i * 4, buf, 4);
|
|
g_trackStart[i] = ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) |
|
|
((uint32_t)buf[2] << 8) | buf[3];
|
|
}
|
|
// Read lengths
|
|
for (uint8_t i = 0; i < g_numTracks; i++) {
|
|
flashReadBytes(TRACK_TABLE_ADDR + 4 + MAX_TRACKS * 4 + i * 4, buf, 4);
|
|
g_trackLen[i] = ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) |
|
|
((uint32_t)buf[2] << 8) | buf[3];
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// AUDIO PLAYBACK (Timer1 ISR)
|
|
// ============================================================
|
|
|
|
void audioInit() {
|
|
// Timer1: Fast PWM, 8-bit, TOP=255
|
|
// At 8MHz clock: 8000000 / 256 = 31250 Hz PWM frequency
|
|
// Good enough — well above audible range
|
|
|
|
// We use a separate Timer1 compare match for sample rate:
|
|
// Timer1 in Fast PWM mode for output,
|
|
// Timer2 for sample rate interrupt
|
|
|
|
// Timer1: Fast PWM 8-bit on OC1A (PB1)
|
|
TCCR1A = _BV(COM1A1) | _BV(WGM10); // Clear on match, 8-bit fast PWM
|
|
TCCR1B = _BV(WGM12) | _BV(CS10); // No prescaler
|
|
OCR1A = 128; // 50% = silence for unsigned 8-bit
|
|
|
|
// Timer2: CTC mode for sample rate interrupt
|
|
// 8MHz / 8 / 125 = 8000 Hz
|
|
TCCR2A = _BV(WGM21); // CTC mode
|
|
TCCR2B = _BV(CS21); // prescaler /8
|
|
OCR2A = 124; // 8000000/8/(124+1) = 8000 Hz
|
|
TIMSK2 = 0; // interrupt disabled until playing
|
|
}
|
|
|
|
void audioStart(uint8_t trackNum) {
|
|
if (trackNum >= g_numTracks) return;
|
|
|
|
g_currentTrack = trackNum;
|
|
g_playPos = g_trackStart[trackNum];
|
|
g_playEnd = g_playPos + g_trackLen[trackNum];
|
|
|
|
// Pre-fill both buffers
|
|
flashReadBytes(g_playPos, g_audioBuf[0], AUDIO_BUF_SIZE);
|
|
g_playPos += AUDIO_BUF_SIZE;
|
|
if (g_playPos < g_playEnd) {
|
|
flashReadBytes(g_playPos, g_audioBuf[1], AUDIO_BUF_SIZE);
|
|
g_playPos += AUDIO_BUF_SIZE;
|
|
}
|
|
|
|
g_activeBuf = 0;
|
|
g_bufPos = 0;
|
|
g_bufReady[0] = true;
|
|
g_bufReady[1] = true;
|
|
g_nextReadAddr = g_playPos;
|
|
|
|
// Enable amp
|
|
digitalWrite(PIN_AMP_SD, HIGH);
|
|
delay(10); // amp startup time
|
|
|
|
g_playing = true;
|
|
TIMSK2 = _BV(OCIE2A); // enable sample rate interrupt
|
|
}
|
|
|
|
void audioStop() {
|
|
TIMSK2 = 0; // disable interrupt
|
|
OCR1A = 128; // silence
|
|
g_playing = false;
|
|
|
|
// Disable amp to save power
|
|
digitalWrite(PIN_AMP_SD, LOW);
|
|
}
|
|
|
|
// Timer2 Compare Match A — fires at 8000 Hz
|
|
ISR(TIMER2_COMPA_vect) {
|
|
if (!g_playing) return;
|
|
|
|
// Output sample to PWM
|
|
OCR1A = g_audioBuf[g_activeBuf][g_bufPos];
|
|
g_bufPos++;
|
|
|
|
if (g_bufPos >= AUDIO_BUF_SIZE) {
|
|
// Switch to other buffer
|
|
g_bufReady[g_activeBuf] = false; // mark for refill
|
|
g_activeBuf ^= 1;
|
|
g_bufPos = 0;
|
|
|
|
if (!g_bufReady[g_activeBuf]) {
|
|
// Buffer underrun — stop playback
|
|
g_playing = false;
|
|
OCR1A = 128;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// MOTOR CONTROL
|
|
// ============================================================
|
|
|
|
void motorInit() {
|
|
pinMode(PIN_MOTOR_PWM, OUTPUT);
|
|
analogWrite(PIN_MOTOR_PWM, 0); // off
|
|
}
|
|
|
|
void motorStart(uint8_t speed) {
|
|
g_motorSpeed = speed;
|
|
g_motorOn = true;
|
|
analogWrite(PIN_MOTOR_PWM, speed);
|
|
}
|
|
|
|
void motorStop() {
|
|
g_motorOn = false;
|
|
analogWrite(PIN_MOTOR_PWM, 0);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// BUTTONS
|
|
// ============================================================
|
|
|
|
const uint8_t BTN_PINS[] = {
|
|
PIN_BTN1, PIN_BTN2, PIN_BTN3, PIN_BTN4,
|
|
PIN_BTN5, PIN_BTN6, PIN_BTN7, PIN_BTN8
|
|
};
|
|
#define NUM_BUTTONS 8
|
|
|
|
void buttonsInit() {
|
|
for (uint8_t i = 0; i < NUM_BUTTONS; i++) {
|
|
pinMode(BTN_PINS[i], INPUT_PULLUP);
|
|
}
|
|
}
|
|
|
|
// Returns button number 1-8, or 0 if none pressed
|
|
uint8_t buttonRead() {
|
|
for (uint8_t i = 0; i < NUM_BUTTONS; i++) {
|
|
if (digitalRead(BTN_PINS[i]) == LOW) {
|
|
delay(20); // debounce
|
|
if (digitalRead(BTN_PINS[i]) == LOW) {
|
|
return i + 1;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// SLEEP / WAKE
|
|
// ============================================================
|
|
|
|
// Pin change interrupts for wake-from-sleep
|
|
ISR(PCINT0_vect) { g_wakeFlag = true; } // PB0 (BTN8)
|
|
ISR(PCINT1_vect) { g_wakeFlag = true; } // PC0, PC1 (BTN1, BTN2)
|
|
ISR(PCINT2_vect) { g_wakeFlag = true; } // PD2-7 (BTN3-BTN7)
|
|
ISR(INT0_vect) { g_wakeFlag = true; }
|
|
ISR(INT1_vect) { g_wakeFlag = true; }
|
|
|
|
void enterSleep() {
|
|
// Stop everything
|
|
audioStop();
|
|
motorStop();
|
|
flashSleep();
|
|
digitalWrite(PIN_LED, LOW);
|
|
|
|
// Enable pin change interrupts for wake
|
|
PCICR = _BV(PCIE0) | _BV(PCIE1) | _BV(PCIE2);
|
|
PCMSK0 = _BV(PCINT0); // PB0
|
|
PCMSK1 = _BV(PCINT8) | _BV(PCINT9); // PC0, PC1
|
|
PCMSK2 = _BV(PCINT18) | _BV(PCINT19) | _BV(PCINT20) |
|
|
_BV(PCINT22) | _BV(PCINT23); // PD2,3,4,6,7
|
|
|
|
// Also enable INT0/INT1 for PD2/PD3
|
|
EICRA = 0; // LOW level trigger
|
|
EIMSK = _BV(INT0) | _BV(INT1);
|
|
|
|
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
|
|
sleep_enable();
|
|
sei();
|
|
sleep_cpu();
|
|
|
|
// --- ZZZ --- wakes up here ---
|
|
sleep_disable();
|
|
|
|
// Disable wake interrupts
|
|
PCICR = 0;
|
|
EIMSK = 0;
|
|
|
|
// Wake flash
|
|
flashWake();
|
|
|
|
g_lastActivity = millis();
|
|
g_wakeFlag = false;
|
|
|
|
// Blink LED to indicate wake
|
|
digitalWrite(PIN_LED, HIGH);
|
|
delay(100);
|
|
digitalWrite(PIN_LED, LOW);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// SERIAL FLASH PROGRAMMER
|
|
// ============================================================
|
|
|
|
// Simple protocol for uploading audio over serial:
|
|
// 'I' → respond with flash JEDEC ID
|
|
// 'E' → erase entire flash chip
|
|
// 'W' addr(3 bytes) len(2 bytes) data... → write page
|
|
// 'R' addr(3 bytes) len(2 bytes) → read bytes back
|
|
// 'T' → read track table
|
|
|
|
void handleSerial() {
|
|
if (!Serial.available()) return;
|
|
|
|
uint8_t cmd = Serial.read();
|
|
|
|
switch (cmd) {
|
|
case 'I': {
|
|
uint32_t id = flashReadJEDEC();
|
|
Serial.print("JEDEC: 0x");
|
|
Serial.println(id, HEX);
|
|
break;
|
|
}
|
|
case 'E': {
|
|
Serial.println("Erasing flash...");
|
|
flashWriteEnable();
|
|
flashSelect();
|
|
SPI.transfer(FLASH_CMD_CHIP_ERASE);
|
|
flashDeselect();
|
|
flashWaitBusy(); // takes ~40 seconds for 16MB
|
|
Serial.println("Done");
|
|
break;
|
|
}
|
|
case 'W': {
|
|
// Wait for address (3 bytes) and length (2 bytes)
|
|
while (Serial.available() < 5) {}
|
|
uint32_t addr = (uint32_t)Serial.read() << 16;
|
|
addr |= (uint32_t)Serial.read() << 8;
|
|
addr |= Serial.read();
|
|
uint16_t len = (uint16_t)Serial.read() << 8;
|
|
len |= Serial.read();
|
|
if (len > 256) len = 256;
|
|
|
|
uint8_t buf[256];
|
|
uint16_t received = 0;
|
|
while (received < len) {
|
|
if (Serial.available()) {
|
|
buf[received++] = Serial.read();
|
|
}
|
|
}
|
|
flashPageProgram(addr, buf, len);
|
|
Serial.print("OK ");
|
|
Serial.println(addr, HEX);
|
|
break;
|
|
}
|
|
case 'R': {
|
|
while (Serial.available() < 5) {}
|
|
uint32_t addr = (uint32_t)Serial.read() << 16;
|
|
addr |= (uint32_t)Serial.read() << 8;
|
|
addr |= Serial.read();
|
|
uint16_t len = (uint16_t)Serial.read() << 8;
|
|
len |= Serial.read();
|
|
|
|
uint8_t buf[256];
|
|
while (len > 0) {
|
|
uint16_t chunk = (len > 256) ? 256 : len;
|
|
flashReadBytes(addr, buf, chunk);
|
|
Serial.write(buf, chunk);
|
|
addr += chunk;
|
|
len -= chunk;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// MAIN
|
|
// ============================================================
|
|
|
|
void setup() {
|
|
// Disable unused peripherals for power saving
|
|
power_adc_disable(); // we don't use ADC
|
|
power_twi_disable(); // we don't use I2C
|
|
|
|
// Pin setup
|
|
pinMode(PIN_FLASH_CS, OUTPUT);
|
|
pinMode(PIN_AMP_SD, OUTPUT);
|
|
pinMode(PIN_LED, OUTPUT);
|
|
|
|
digitalWrite(PIN_FLASH_CS, HIGH); // deselect flash
|
|
digitalWrite(PIN_AMP_SD, LOW); // amp off
|
|
digitalWrite(PIN_LED, LOW);
|
|
|
|
// Init peripherals
|
|
Serial.begin(9600);
|
|
SPI.begin();
|
|
SPI.setClockDivider(SPI_CLOCK_DIV2); // 4MHz SPI (8MHz/2)
|
|
|
|
buttonsInit();
|
|
motorInit();
|
|
audioInit();
|
|
|
|
// Init flash
|
|
flashWake();
|
|
delay(1);
|
|
|
|
uint32_t jedec = flashReadJEDEC();
|
|
if (jedec == 0xEF4018) {
|
|
// W25Q128 detected
|
|
digitalWrite(PIN_LED, HIGH);
|
|
delay(200);
|
|
digitalWrite(PIN_LED, LOW);
|
|
}
|
|
|
|
// Load track table
|
|
loadTrackTable();
|
|
|
|
g_lastActivity = millis();
|
|
|
|
Serial.print("Baby Mobile v1.0 — ");
|
|
Serial.print(g_numTracks);
|
|
Serial.println(" tracks loaded");
|
|
}
|
|
|
|
void loop() {
|
|
// ---- Handle serial commands (for programming) ----
|
|
handleSerial();
|
|
|
|
// ---- Refill audio buffer in main loop ----
|
|
if (g_playing) {
|
|
for (uint8_t b = 0; b < 2; b++) {
|
|
if (!g_bufReady[b] && g_nextReadAddr < g_playEnd) {
|
|
uint32_t remaining = g_playEnd - g_nextReadAddr;
|
|
uint16_t toRead = (remaining > AUDIO_BUF_SIZE) ? AUDIO_BUF_SIZE : remaining;
|
|
|
|
flashReadBytes(g_nextReadAddr, g_audioBuf[b], toRead);
|
|
// Zero-pad if short
|
|
for (uint16_t i = toRead; i < AUDIO_BUF_SIZE; i++) {
|
|
g_audioBuf[b][i] = 128; // silence
|
|
}
|
|
g_nextReadAddr += toRead;
|
|
g_bufReady[b] = true;
|
|
}
|
|
}
|
|
|
|
// Check if playback finished
|
|
if (!g_bufReady[0] && !g_bufReady[1]) {
|
|
audioStop();
|
|
// Auto-advance to next track or loop
|
|
g_currentTrack = (g_currentTrack + 1) % g_numTracks;
|
|
audioStart(g_currentTrack);
|
|
}
|
|
}
|
|
|
|
// ---- Read buttons ----
|
|
uint8_t btn = buttonRead();
|
|
if (btn > 0) {
|
|
g_lastActivity = millis();
|
|
|
|
switch (btn) {
|
|
case 1: // Play / Pause
|
|
if (g_playing) {
|
|
audioStop();
|
|
} else {
|
|
audioStart(g_currentTrack);
|
|
}
|
|
break;
|
|
|
|
case 2: // Next track
|
|
if (g_numTracks > 0) {
|
|
g_currentTrack = (g_currentTrack + 1) % g_numTracks;
|
|
if (g_playing) {
|
|
audioStop();
|
|
audioStart(g_currentTrack);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 3: // Previous track
|
|
if (g_numTracks > 0) {
|
|
g_currentTrack = (g_currentTrack == 0) ? g_numTracks - 1 : g_currentTrack - 1;
|
|
if (g_playing) {
|
|
audioStop();
|
|
audioStart(g_currentTrack);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 4: // Motor toggle
|
|
if (g_motorOn) {
|
|
motorStop();
|
|
} else {
|
|
motorStart(g_motorSpeed);
|
|
}
|
|
break;
|
|
|
|
case 5: // Motor speed up
|
|
if (g_motorSpeed < 235) g_motorSpeed += 20;
|
|
if (g_motorOn) analogWrite(PIN_MOTOR_PWM, g_motorSpeed);
|
|
break;
|
|
|
|
case 6: // Motor speed down
|
|
if (g_motorSpeed > 80) g_motorSpeed -= 20;
|
|
if (g_motorOn) analogWrite(PIN_MOTOR_PWM, g_motorSpeed);
|
|
break;
|
|
|
|
case 7: // Play all (music + motor)
|
|
motorStart(g_motorSpeed);
|
|
if (!g_playing) audioStart(0);
|
|
break;
|
|
|
|
case 8: // Stop all
|
|
audioStop();
|
|
motorStop();
|
|
break;
|
|
}
|
|
|
|
// Wait for button release
|
|
while (buttonRead() == btn) { delay(10); }
|
|
}
|
|
|
|
// ---- Auto shutoff ----
|
|
if (millis() - g_lastActivity > AUTO_OFF_MS) {
|
|
enterSleep();
|
|
}
|
|
|
|
// ---- Sleep if idle (no audio, no motor) ----
|
|
if (!g_playing && !g_motorOn && (millis() - g_lastActivity > 60000)) {
|
|
enterSleep();
|
|
}
|
|
}
|