Files
baby-mobile/baby_mobile_firmware.ino
T
2026-07-04 03:28:11 -07:00

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();
}
}