/* * 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 #include #include #include // ---- 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(); } }