Files
baby-mobile/baby_mobile_v2/baby_mobile_v2.ino
T
2026-07-04 03:28:12 -07:00

946 lines
28 KiB
Arduino

/*
* Baby Mobile Audio Board v2 — nRF52840 Firmware
*
* Platform: Seeed XIAO nRF52840 (or MDBT50Q module)
* Framework: Adafruit nRF52 Arduino Core
*
* Features:
* - USB Mass Storage: drag-and-drop .raw audio files
* - BLE: wireless audio upload from phone/Web Bluetooth
* - 8-bit 8kHz PWM audio playback from IS25LP128F SPI flash
* - DC motor PWM speed control
* - 8 button inputs with debounce
* - Deep sleep with button wake (~10µA)
* - Auto-shutoff timer
*
* Audio format: unsigned 8-bit PCM, 8000 Hz, mono
* Convert: ffmpeg -i song.mp3 -ar 8000 -ac 1 -f u8 -acodec pcm_u8 song.raw
*
* Board setup in Arduino IDE:
* Board: "Seeed XIAO nRF52840" (or "Adafruit Feather nRF52840")
* Install: Seeed nRF52 Boards via Board Manager
*
* Wiring (XIAO pin names):
* D0 (P0.02) - Button 1
* D1 (P0.03) - Button 2
* D2 (P0.28) - Button 3
* D3 (P0.29) - Button 4
* D4 (P0.04) - Button 5
* D5 (P0.05) - Button 6
* D6 (P1.11) - Button 7
* D7 (P1.12) - Button 8
* D8 (P0.07) - SPI SCK → Flash pin 6
* D9 (P0.06) - SPI MISO → Flash pin 2
* D10 (P0.05) - SPI MOSI → Flash pin 5
* A0 (P0.02) - Audio PWM output → R+C LPF → PAM8302A
* A1 (P0.03) - Amp ~SD (HIGH=on)
* A2 (P0.28) - Motor PWM → MOSFET gate
* A3 (P0.29) - LED output
* A4 (P0.04) - Flash ~CS
* A5 (P0.05) - (spare)
*
* NOTE: Pin assignments above are illustrative. Adjust to your actual
* XIAO pinout and PCB routing. The XIAO has 11 usable GPIOs on the
* edge pins plus additional pads on the bottom.
*/
#include <SPI.h>
#include <Adafruit_TinyUSB.h>
#include <bluefruit.h>
// ============================================================
// PIN DEFINITIONS — adjust for your PCB
// ============================================================
// Using raw nRF52840 GPIO numbers for flexibility.
// Map these to your actual PCB connections.
// Audio
#define PIN_AUDIO_PWM 13 // Any PWM-capable pin
#define PIN_AMP_SD 14 // PAM8302A shutdown (HIGH=enabled)
// Motor
#define PIN_MOTOR_PWM 15 // PWM to MOSFET gate
// SPI Flash
#define PIN_FLASH_CS 5 // Flash chip select
// SPI MOSI/MISO/SCK use default SPI pins
// Buttons (directly to GPIO, active LOW with internal pull-up)
#define PIN_BTN1 2
#define PIN_BTN2 3
#define PIN_BTN3 4
#define PIN_BTN4 28
#define PIN_BTN5 29
#define PIN_BTN6 30
#define PIN_BTN7 31
#define PIN_BTN8 12
// LED
#define PIN_LED 16
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
// ============================================================
// FLASH CONSTANTS
// ============================================================
#define FLASH_SIZE (16UL * 1024UL * 1024UL) // 16MB
#define FLASH_SECTOR 4096
#define FLASH_PAGE 256
#define FLASH_BLOCK_64K (64UL * 1024UL)
// IS25LP128F / W25Q128 compatible commands
#define CMD_READ 0x03
#define CMD_FAST_READ 0x0B
#define CMD_WRITE_EN 0x06
#define CMD_PAGE_PROG 0x02
#define CMD_SECT_ERASE 0x20
#define CMD_BLOCK_ERASE 0xD8
#define CMD_CHIP_ERASE 0xC7
#define CMD_READ_SR1 0x05
#define CMD_JEDEC_ID 0x9F
#define CMD_POWER_DOWN 0xB9
#define CMD_WAKE 0xAB
// ============================================================
// TRACK TABLE FORMAT
// ============================================================
// Sector 0 (first 4KB) holds the track index:
// [0] uint8_t magic = 0xBB (indicates valid table)
// [1] uint8_t num_tracks
// [2..3] reserved
// [4..] Track entries, 12 bytes each:
// uint32_t start_addr (big-endian)
// uint32_t byte_length (big-endian)
// uint8_t sample_rate_khz (8 = 8kHz)
// uint8_t bits (8 = 8-bit unsigned)
// uint16_t reserved
// Audio data starts at AUDIO_START (sector 1 = 0x1000)
#define TRACK_TABLE_ADDR 0x000000
#define AUDIO_START_ADDR 0x001000
#define TABLE_MAGIC 0xBB
#define MAX_TRACKS 32
#define TRACK_ENTRY_SIZE 12
// ============================================================
// AUDIO CONFIG
// ============================================================
#define SAMPLE_RATE 8000
#define AUDIO_BUF_SIZE 512 // larger buffer = fewer SPI transactions
#define SILENCE 128 // 0x80 = center for unsigned 8-bit
// ============================================================
// USB MASS STORAGE
// ============================================================
Adafruit_USBD_MSC usb_msc;
// Simple FAT12 filesystem on the SPI flash
// We present the entire 16MB flash as a USB drive.
// The host can write raw files; firmware scans for audio on boot.
// MSC callbacks
int32_t msc_read_cb(uint32_t lba, void* buffer, uint32_t bufsize);
int32_t msc_write_cb(uint32_t lba, uint8_t* buffer, uint32_t bufsize);
void msc_flush_cb(void);
bool msc_start_stop_cb(uint8_t power_condition, bool start, bool load_eject);
#define MSC_BLOCK_SIZE 512
#define MSC_BLOCK_COUNT (FLASH_SIZE / MSC_BLOCK_SIZE)
// ============================================================
// BLE SERVICE
// ============================================================
BLEService audioSvc = BLEService("12340001-0000-1000-8000-00805f9b34fb");
BLECharacteristic audioCmd = BLECharacteristic("12340002-0000-1000-8000-00805f9b34fb");
BLECharacteristic audioData = BLECharacteristic("12340003-0000-1000-8000-00805f9b34fb");
BLECharacteristic audioStat = BLECharacteristic("12340004-0000-1000-8000-00805f9b34fb");
// BLE upload state
volatile bool g_bleUploading = false;
volatile uint32_t g_bleWriteAddr = 0;
volatile uint32_t g_bleWriteLen = 0;
// ============================================================
// GLOBAL STATE
// ============================================================
volatile bool g_playing = false;
volatile bool g_motorOn = false;
uint8_t g_motorSpeed = 160;
uint8_t g_numTracks = 0;
uint32_t g_trackStart[MAX_TRACKS];
uint32_t g_trackLen[MAX_TRACKS];
uint8_t g_currentTrack = 0;
// Audio double-buffer
uint8_t g_audioBuf[2][AUDIO_BUF_SIZE];
volatile uint8_t g_activeBuf = 0;
volatile uint16_t g_bufPos = 0;
volatile bool g_bufReady[2] = {false, false};
uint32_t g_nextReadAddr = 0;
uint32_t g_playEnd = 0;
// Timers
unsigned long g_lastActivity = 0;
#define AUTO_OFF_MS (30UL * 60UL * 1000UL) // 30 min auto-shutoff
#define IDLE_SLEEP_MS (60UL * 1000UL) // 1 min idle → sleep
// USB connected flag
volatile bool g_usbConnected = false;
// ============================================================
// SPI FLASH DRIVER
// ============================================================
SPISettings flashSPI(8000000, MSBFIRST, SPI_MODE0); // 8MHz
void flashSelect() { digitalWrite(PIN_FLASH_CS, LOW); }
void flashDeselect() { digitalWrite(PIN_FLASH_CS, HIGH); }
void flashWake() {
flashSelect();
SPI.transfer(CMD_WAKE);
flashDeselect();
delayMicroseconds(5);
}
void flashSleep() {
flashSelect();
SPI.transfer(CMD_POWER_DOWN);
flashDeselect();
}
uint32_t flashReadJEDEC() {
SPI.beginTransaction(flashSPI);
flashSelect();
SPI.transfer(CMD_JEDEC_ID);
uint32_t id = (uint32_t)SPI.transfer(0) << 16;
id |= (uint32_t)SPI.transfer(0) << 8;
id |= SPI.transfer(0);
flashDeselect();
SPI.endTransaction();
return id;
}
void flashReadBytes(uint32_t addr, uint8_t *buf, uint32_t len) {
SPI.beginTransaction(flashSPI);
flashSelect();
SPI.transfer(CMD_READ);
SPI.transfer((addr >> 16) & 0xFF);
SPI.transfer((addr >> 8) & 0xFF);
SPI.transfer(addr & 0xFF);
for (uint32_t i = 0; i < len; i++) {
buf[i] = SPI.transfer(0);
}
flashDeselect();
SPI.endTransaction();
}
void flashWaitBusy() {
SPI.beginTransaction(flashSPI);
flashSelect();
SPI.transfer(CMD_READ_SR1);
while (SPI.transfer(0) & 0x01) { /* spin */ }
flashDeselect();
SPI.endTransaction();
}
void flashWriteEnable() {
SPI.beginTransaction(flashSPI);
flashSelect();
SPI.transfer(CMD_WRITE_EN);
flashDeselect();
SPI.endTransaction();
}
void flashEraseSector(uint32_t addr) {
flashWriteEnable();
SPI.beginTransaction(flashSPI);
flashSelect();
SPI.transfer(CMD_SECT_ERASE);
SPI.transfer((addr >> 16) & 0xFF);
SPI.transfer((addr >> 8) & 0xFF);
SPI.transfer(addr & 0xFF);
flashDeselect();
SPI.endTransaction();
flashWaitBusy();
}
void flashEraseBlock64K(uint32_t addr) {
flashWriteEnable();
SPI.beginTransaction(flashSPI);
flashSelect();
SPI.transfer(CMD_BLOCK_ERASE);
SPI.transfer((addr >> 16) & 0xFF);
SPI.transfer((addr >> 8) & 0xFF);
SPI.transfer(addr & 0xFF);
flashDeselect();
SPI.endTransaction();
flashWaitBusy();
}
void flashPageProgram(uint32_t addr, const uint8_t *data, uint16_t len) {
flashWriteEnable();
SPI.beginTransaction(flashSPI);
flashSelect();
SPI.transfer(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();
SPI.endTransaction();
flashWaitBusy();
}
// ============================================================
// TRACK TABLE
// ============================================================
void loadTrackTable() {
uint8_t header[4];
flashReadBytes(TRACK_TABLE_ADDR, header, 4);
if (header[0] != TABLE_MAGIC) {
g_numTracks = 0;
Serial.println("No track table found (flash may be empty)");
return;
}
g_numTracks = header[1];
if (g_numTracks > MAX_TRACKS) g_numTracks = MAX_TRACKS;
uint8_t entry[TRACK_ENTRY_SIZE];
for (uint8_t i = 0; i < g_numTracks; i++) {
flashReadBytes(TRACK_TABLE_ADDR + 4 + i * TRACK_ENTRY_SIZE, entry, TRACK_ENTRY_SIZE);
g_trackStart[i] = ((uint32_t)entry[0] << 24) | ((uint32_t)entry[1] << 16) |
((uint32_t)entry[2] << 8) | entry[3];
g_trackLen[i] = ((uint32_t)entry[4] << 24) | ((uint32_t)entry[5] << 16) |
((uint32_t)entry[6] << 8) | entry[7];
}
Serial.print("Loaded ");
Serial.print(g_numTracks);
Serial.println(" tracks from flash");
for (uint8_t i = 0; i < g_numTracks; i++) {
Serial.print(" Track ");
Serial.print(i + 1);
Serial.print(": addr=0x");
Serial.print(g_trackStart[i], HEX);
Serial.print(", ");
Serial.print(g_trackLen[i] / SAMPLE_RATE);
Serial.println("s");
}
}
void writeTrackTable() {
// Erase sector 0
flashEraseSector(0);
// Build table in RAM
uint8_t table[4 + MAX_TRACKS * TRACK_ENTRY_SIZE];
memset(table, 0xFF, sizeof(table));
table[0] = TABLE_MAGIC;
table[1] = g_numTracks;
table[2] = 0;
table[3] = 0;
for (uint8_t i = 0; i < g_numTracks; i++) {
uint8_t *e = &table[4 + i * TRACK_ENTRY_SIZE];
e[0] = (g_trackStart[i] >> 24) & 0xFF;
e[1] = (g_trackStart[i] >> 16) & 0xFF;
e[2] = (g_trackStart[i] >> 8) & 0xFF;
e[3] = g_trackStart[i] & 0xFF;
e[4] = (g_trackLen[i] >> 24) & 0xFF;
e[5] = (g_trackLen[i] >> 16) & 0xFF;
e[6] = (g_trackLen[i] >> 8) & 0xFF;
e[7] = g_trackLen[i] & 0xFF;
e[8] = 8; // sample rate kHz
e[9] = 8; // bits
e[10] = 0;
e[11] = 0;
}
// Write in pages
uint16_t total = 4 + g_numTracks * TRACK_ENTRY_SIZE;
for (uint16_t offset = 0; offset < total; offset += FLASH_PAGE) {
uint16_t chunk = min((uint16_t)FLASH_PAGE, (uint16_t)(total - offset));
flashPageProgram(offset, &table[offset], chunk);
}
}
// ============================================================
// USB MASS STORAGE CALLBACKS
// ============================================================
int32_t msc_read_cb(uint32_t lba, void* buffer, uint32_t bufsize) {
uint32_t addr = lba * MSC_BLOCK_SIZE;
flashReadBytes(addr, (uint8_t*)buffer, bufsize);
return bufsize;
}
int32_t msc_write_cb(uint32_t lba, uint8_t* buffer, uint32_t bufsize) {
uint32_t addr = lba * MSC_BLOCK_SIZE;
// Erase sector if we're at a sector boundary
if ((addr % FLASH_SECTOR) == 0) {
flashEraseSector(addr);
}
// Write in page-sized chunks
uint32_t written = 0;
while (written < bufsize) {
uint16_t pageOffset = (addr + written) % FLASH_PAGE;
uint16_t chunk = min((uint16_t)(FLASH_PAGE - pageOffset), (uint16_t)(bufsize - written));
flashPageProgram(addr + written, buffer + written, chunk);
written += chunk;
}
return bufsize;
}
void msc_flush_cb(void) {
// After USB write completes, reload the track table
// (host may have written a new FAT filesystem)
// We scan for audio data on eject instead
}
bool msc_start_stop_cb(uint8_t power_condition, bool start, bool load_eject) {
if (!start && load_eject) {
// Host ejected the drive — rescan for tracks
Serial.println("USB ejected, rescanning tracks...");
loadTrackTable();
}
return true;
}
// ============================================================
// BLE SETUP
// ============================================================
void ble_connect_cb(uint16_t conn_handle) {
Serial.println("BLE connected");
g_lastActivity = millis();
}
void ble_disconnect_cb(uint16_t conn_handle, uint8_t reason) {
Serial.println("BLE disconnected");
g_bleUploading = false;
}
// BLE command characteristic: receives commands
// CMD 0x01 [num_tracks] [track_entries...] = write track table
// CMD 0x02 [addr_3bytes] = start writing audio at address
// CMD 0x03 = finish upload, reload tracks
// CMD 0x04 [track_num] = play track
// CMD 0x05 = stop playback
void audioCmd_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
uint8_t* data, uint16_t len) {
if (len < 1) return;
g_lastActivity = millis();
switch (data[0]) {
case 0x01: // Write track table
if (len >= 2) {
g_numTracks = data[1];
if (g_numTracks > MAX_TRACKS) g_numTracks = MAX_TRACKS;
// Parse track entries from data[2..]
for (uint8_t i = 0; i < g_numTracks && (2 + i * 8 + 7) < len; i++) {
uint8_t *e = &data[2 + i * 8];
g_trackStart[i] = ((uint32_t)e[0] << 24) | ((uint32_t)e[1] << 16) |
((uint32_t)e[2] << 8) | e[3];
g_trackLen[i] = ((uint32_t)e[4] << 24) | ((uint32_t)e[5] << 16) |
((uint32_t)e[6] << 8) | e[7];
}
writeTrackTable();
Serial.println("BLE: Track table updated");
}
break;
case 0x02: // Start audio write
if (len >= 4) {
g_bleWriteAddr = ((uint32_t)data[1] << 16) | ((uint32_t)data[2] << 8) | data[3];
g_bleUploading = true;
// Erase 64K block at target
flashEraseBlock64K(g_bleWriteAddr & ~(FLASH_BLOCK_64K - 1));
Serial.print("BLE: Start write at 0x");
Serial.println(g_bleWriteAddr, HEX);
}
break;
case 0x03: // Finish upload
g_bleUploading = false;
loadTrackTable();
Serial.println("BLE: Upload complete");
break;
case 0x04: // Play track
if (len >= 2 && data[1] < g_numTracks) {
audioStart(data[1]);
}
break;
case 0x05: // Stop
audioStop();
motorStop();
break;
}
}
// BLE data characteristic: receives raw audio bytes for upload
void audioData_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
uint8_t* data, uint16_t len) {
if (!g_bleUploading) return;
g_lastActivity = millis();
// Erase new sectors as we cross boundaries
uint32_t endAddr = g_bleWriteAddr + len;
uint32_t currentSector = g_bleWriteAddr / FLASH_SECTOR;
uint32_t endSector = (endAddr - 1) / FLASH_SECTOR;
for (uint32_t s = currentSector + 1; s <= endSector; s++) {
flashEraseSector(s * FLASH_SECTOR);
}
// Write data page by page
uint16_t written = 0;
while (written < len) {
uint16_t pageOff = (g_bleWriteAddr + written) % FLASH_PAGE;
uint16_t chunk = min((uint16_t)(FLASH_PAGE - pageOff), (uint16_t)(len - written));
flashPageProgram(g_bleWriteAddr + written, data + written, chunk);
written += chunk;
}
g_bleWriteAddr += len;
g_bleWriteLen += len;
// Update status characteristic with bytes written
uint8_t stat[4];
stat[0] = (g_bleWriteLen >> 24) & 0xFF;
stat[1] = (g_bleWriteLen >> 16) & 0xFF;
stat[2] = (g_bleWriteLen >> 8) & 0xFF;
stat[3] = g_bleWriteLen & 0xFF;
audioStat.write(stat, 4);
audioStat.notify(stat, 4);
}
void setupBLE() {
Bluefruit.begin();
Bluefruit.setName("BabyMobile");
Bluefruit.setTxPower(0); // 0 dBm — save power, short range is fine
Bluefruit.Periph.setConnectCallback(ble_connect_cb);
Bluefruit.Periph.setDisconnectCallback(ble_disconnect_cb);
// Audio service
audioSvc.begin();
// Command characteristic (write)
audioCmd.setProperties(CHR_PROPS_WRITE);
audioCmd.setPermission(SECMODE_OPEN, SECMODE_OPEN);
audioCmd.setMaxLen(240);
audioCmd.setWriteCallback(audioCmd_write_cb);
audioCmd.begin();
// Data characteristic (write without response for speed)
audioData.setProperties(CHR_PROPS_WRITE_WO_RESP);
audioData.setPermission(SECMODE_OPEN, SECMODE_OPEN);
audioData.setMaxLen(240);
audioData.setWriteCallback(audioData_write_cb);
audioData.begin();
// Status characteristic (read + notify)
audioStat.setProperties(CHR_PROPS_READ | CHR_PROPS_NOTIFY);
audioStat.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
audioStat.setMaxLen(4);
audioStat.begin();
// Start advertising
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
Bluefruit.Advertising.addService(audioSvc);
Bluefruit.Advertising.addName();
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.setInterval(160, 320); // 100-200ms
Bluefruit.Advertising.start(0); // advertise forever
Serial.println("BLE advertising as 'BabyMobile'");
}
// ============================================================
// AUDIO PLAYBACK
// ============================================================
// nRF52840 has hardware PWM (up to 4 instances, 16MHz base clock)
// We use the nrf_pwm peripheral directly for audio output
// and a TIMER for the sample rate interrupt.
// For Arduino compatibility, we use analogWrite for the PWM
// and a software timer for sample feeding.
// Timer callback for audio sample rate
volatile bool g_timerFired = false;
void timerCallback(void) {
if (!g_playing) return;
// Output sample
analogWrite(PIN_AUDIO_PWM, g_audioBuf[g_activeBuf][g_bufPos]);
g_bufPos++;
if (g_bufPos >= AUDIO_BUF_SIZE) {
g_bufReady[g_activeBuf] = false;
g_activeBuf ^= 1;
g_bufPos = 0;
if (!g_bufReady[g_activeBuf]) {
// Buffer underrun
g_playing = false;
analogWrite(PIN_AUDIO_PWM, SILENCE);
}
}
}
// Use nRF52 SoftwareTimer (built into Adafruit BSP)
SoftwareTimer audioTimer;
void audioTimerHandler(TimerHandle_t xTimer) {
timerCallback();
}
void audioInit() {
pinMode(PIN_AUDIO_PWM, OUTPUT);
analogWrite(PIN_AUDIO_PWM, SILENCE);
analogWriteResolution(8); // 8-bit PWM
// Create a FreeRTOS software timer at 8kHz
// Note: For better timing, use a hardware TIMER peripheral
// This works well enough for 8kHz audio
audioTimer.begin(1000 / 8, audioTimerHandler, true); // ~8kHz
// Better approach: use nrf_drv_timer for precise 125µs intervals
}
void audioStart(uint8_t trackNum) {
if (trackNum >= g_numTracks) return;
g_currentTrack = trackNum;
uint32_t start = g_trackStart[trackNum];
g_playEnd = start + g_trackLen[trackNum];
g_nextReadAddr = start;
// Pre-fill both buffers
uint32_t toRead = min((uint32_t)AUDIO_BUF_SIZE, g_playEnd - g_nextReadAddr);
flashReadBytes(g_nextReadAddr, g_audioBuf[0], toRead);
g_nextReadAddr += toRead;
g_bufReady[0] = true;
if (g_nextReadAddr < g_playEnd) {
toRead = min((uint32_t)AUDIO_BUF_SIZE, g_playEnd - g_nextReadAddr);
flashReadBytes(g_nextReadAddr, g_audioBuf[1], toRead);
g_nextReadAddr += toRead;
g_bufReady[1] = true;
} else {
memset(g_audioBuf[1], SILENCE, AUDIO_BUF_SIZE);
g_bufReady[1] = true;
}
g_activeBuf = 0;
g_bufPos = 0;
// Enable amplifier
digitalWrite(PIN_AMP_SD, HIGH);
delay(10);
g_playing = true;
audioTimer.start();
Serial.print("Playing track ");
Serial.println(trackNum + 1);
}
void audioStop() {
audioTimer.stop();
g_playing = false;
analogWrite(PIN_AUDIO_PWM, SILENCE);
// Disable amp
digitalWrite(PIN_AMP_SD, LOW);
Serial.println("Playback stopped");
}
// ============================================================
// MOTOR
// ============================================================
void motorInit() {
pinMode(PIN_MOTOR_PWM, OUTPUT);
analogWrite(PIN_MOTOR_PWM, 0);
}
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
// ============================================================
void buttonsInit() {
for (uint8_t i = 0; i < NUM_BUTTONS; i++) {
pinMode(BTN_PINS[i], INPUT_PULLUP);
}
}
// Returns 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;
}
void waitButtonRelease(uint8_t btn) {
while (buttonRead() == btn) {
delay(10);
}
}
// ============================================================
// SLEEP / WAKE
// ============================================================
void enterDeepSleep() {
Serial.println("Entering deep sleep...");
audioStop();
motorStop();
flashSleep();
digitalWrite(PIN_LED, LOW);
// Stop BLE advertising to save power
Bluefruit.Advertising.stop();
// Configure buttons as wake sources
// On nRF52840, any GPIO can wake from System OFF
// We use System ON sleep (RTOS idle) for quick wake
// For deepest sleep, use sd_power_system_off()
// Use pin sense for wake
for (uint8_t i = 0; i < NUM_BUTTONS; i++) {
nrf_gpio_cfg_sense_input(BTN_PINS[i],
NRF_GPIO_PIN_PULLUP,
NRF_GPIO_PIN_SENSE_LOW);
}
// System OFF — lowest power, wakes via GPIO sense or reset
sd_power_system_off();
// Execution stops here. Device resets on wake.
// (This line is never reached)
}
// ============================================================
// SETUP
// ============================================================
void setup() {
// LED early — visual feedback
pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_LED, HIGH);
Serial.begin(115200);
// Don't block on serial — may not be connected
delay(500);
Serial.println("=== Baby Mobile v2 ===");
Serial.println("nRF52840 + IS25LP128F + PAM8302A");
// Pin setup
pinMode(PIN_FLASH_CS, OUTPUT);
pinMode(PIN_AMP_SD, OUTPUT);
digitalWrite(PIN_FLASH_CS, HIGH);
digitalWrite(PIN_AMP_SD, LOW);
// SPI
SPI.begin();
// Flash
flashWake();
delay(1);
uint32_t jedec = flashReadJEDEC();
Serial.print("Flash JEDEC: 0x");
Serial.println(jedec, HEX);
if (jedec == 0x9D6018 || jedec == 0x9D6017 || // IS25LP128F / IS25LP064
jedec == 0xEF4018 || jedec == 0xEF4017) { // W25Q128 / W25Q064
Serial.println("Flash detected OK");
} else if (jedec == 0x000000 || jedec == 0xFFFFFF) {
Serial.println("WARNING: No flash detected! Check SPI wiring.");
}
// Load tracks
loadTrackTable();
// Buttons
buttonsInit();
// Motor
motorInit();
// Audio
audioInit();
// USB Mass Storage
usb_msc.setID("BabyMobile", "Audio Drive", "2.0");
usb_msc.setReadWriteCallback(msc_read_cb, msc_write_cb, msc_flush_cb);
usb_msc.setStartStopCallback(msc_start_stop_cb);
usb_msc.setCapacity(MSC_BLOCK_COUNT, MSC_BLOCK_SIZE);
usb_msc.setReadOnly(false);
usb_msc.setUnitReady(true);
usb_msc.begin();
// BLE
setupBLE();
g_lastActivity = millis();
digitalWrite(PIN_LED, LOW);
Serial.println("Ready! Plug in USB to upload audio, or press a button.");
}
// ============================================================
// MAIN LOOP
// ============================================================
void loop() {
// ---- Refill audio buffers ----
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;
uint32_t toRead = min((uint32_t)AUDIO_BUF_SIZE, remaining);
flashReadBytes(g_nextReadAddr, g_audioBuf[b], toRead);
// Pad with silence
for (uint32_t i = toRead; i < AUDIO_BUF_SIZE; i++) {
g_audioBuf[b][i] = SILENCE;
}
g_nextReadAddr += toRead;
g_bufReady[b] = true;
}
}
// Track finished? Auto-advance and loop
if (!g_bufReady[0] && !g_bufReady[1] && g_nextReadAddr >= g_playEnd) {
audioStop();
if (g_numTracks > 0) {
g_currentTrack = (g_currentTrack + 1) % g_numTracks;
audioStart(g_currentTrack);
}
}
}
// ---- Handle buttons ----
uint8_t btn = buttonRead();
if (btn > 0) {
g_lastActivity = millis();
switch (btn) {
case 1: // Play / Pause
if (g_playing) {
audioStop();
} else if (g_numTracks > 0) {
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 faster
if (g_motorSpeed < 240) g_motorSpeed += 20;
if (g_motorOn) analogWrite(PIN_MOTOR_PWM, g_motorSpeed);
break;
case 6: // Motor slower
if (g_motorSpeed > 60) 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 && g_numTracks > 0) audioStart(0);
break;
case 8: // Stop all
audioStop();
motorStop();
break;
}
waitButtonRelease(btn);
}
// ---- Auto shutoff ----
if (millis() - g_lastActivity > AUTO_OFF_MS) {
enterDeepSleep();
}
// ---- Idle sleep (if nothing happening, no USB) ----
if (!g_playing && !g_motorOn && !g_usbConnected && !g_bleUploading) {
if (millis() - g_lastActivity > IDLE_SLEEP_MS) {
enterDeepSleep();
}
}
// Small delay to prevent tight-looping
delay(1);
}