add app, fix ble, use flash

This commit is contained in:
zyphlar
2026-07-04 03:28:14 -07:00
parent e6a2a65b82
commit 24de5d8d39
13 changed files with 1028 additions and 249 deletions
+142 -51
View File
@@ -29,9 +29,10 @@
* 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
* D4 (P0.04) - Flash ~CS (PIN_FLASH_CS)
* D8 (P0.07) - SPI SCK → Flash CLK
* D9 (P0.06) - SPI MISO → Flash DO
* D10 (P0.05) - SPI MOSI → Flash DI
* A0 (P0.02) - Audio PWM output → R+C LPF → PAM8302A
* A1 (P0.03) - Amp ~SD (HIGH=on)
* A2 (P0.28) - Motor PWM → MOSFET gate
@@ -55,7 +56,7 @@
// dynamic bit-depth (8/16-bit) and sample-rate support.
// Disable this define to revert to full SPI flash mode.
// ============================================================
#define POC_INTERNAL_FLASH
//#define POC_INTERNAL_FLASH
#ifdef POC_INTERNAL_FLASH
#include <Adafruit_LittleFS.h>
@@ -407,20 +408,37 @@ void loadTrackTable() {
((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];
}
// Parse WAV header from flash to get format metadata
g_trackBits[i] = 8;
g_trackRate[i] = SAMPLE_RATE;
g_trackDataOff[i] = 0;
if (g_trackLen[i] >= 44) {
uint8_t hdr[44];
flashReadBytes(g_trackStart[i], hdr, 44);
if (hdr[0]=='R' && hdr[1]=='I' && hdr[2]=='F' && hdr[3]=='F') {
uint16_t bits = (uint16_t)hdr[34] | ((uint16_t)hdr[35] << 8);
uint32_t rate = (uint32_t)hdr[24] | ((uint32_t)hdr[25] << 8)
| ((uint32_t)hdr[26] << 16) | ((uint32_t)hdr[27] << 24);
if ((bits == 8 || bits == 16) && rate > 0) {
g_trackBits[i] = (uint8_t)bits;
g_trackRate[i] = rate;
g_trackDataOff[i] = 44;
}
}
}
Serial.print(" track"); Serial.print(i);
Serial.print(": "); Serial.print(g_trackBits[i]); Serial.print("bit ");
Serial.print(g_trackRate[i] / 1000); Serial.print("kHz ");
uint32_t audioBytes = g_trackLen[i] - g_trackDataOff[i];
uint32_t bps = g_trackBits[i] / 8;
Serial.print((audioBytes / bps) / g_trackRate[i]);
Serial.println("s");
}
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");
}
#endif
}
@@ -449,8 +467,8 @@ void writeTrackTable() {
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[8] = (uint8_t)(g_trackRate[i] / 1000);
e[9] = g_trackBits[i];
e[10] = 0;
e[11] = 0;
}
@@ -995,11 +1013,16 @@ uint8_t buttonRead() {
// cooldown from expiring while the button is still held and re-triggering.
void waitButtonRelease(uint8_t btn) {
while (buttonRead() == btn) {
// Keep DMA buffers fed while waiting
if (g_playing) {
// Keep DMA buffers fed while waiting
for (uint8_t b = 0; b < 2; b++) {
if (!g_bufReady[b]) audioFillBuf(b);
}
// Stop amp as soon as track ends — don't wait for loop() to resume
if (g_trackDoneMs != 0 && millis() >= g_trackDoneMs) {
g_trackDoneMs = 0;
audioStop();
}
}
}
delay(20); // debounce after release
@@ -1144,49 +1167,75 @@ void setup() {
// ============================================================
// ============================================================
// SERIAL UPLOAD STATE MACHINE (POC_INTERNAL_FLASH only)
// SERIAL UPLOAD STATE MACHINE
// Works in both POC_INTERNAL_FLASH and SPI flash modes.
// Protocol: UPLOAD <track> <total_wav_bytes> (full WAV file, header included)
// DUMP <track> DUMP <track>
// p / l / u / d / r
// DELETE <track> (POC only)
// FORMAT (POC only)
// ============================================================
#ifdef POC_INTERNAL_FLASH
enum SerUploadState { SER_IDLE, SER_RECEIVING };
static SerUploadState g_serState = SER_IDLE;
static char g_serLineBuf[64] = {0};
static uint8_t g_serLineLen = 0;
static uint8_t g_serTrack = 0;
static SerUploadState g_serState = SER_IDLE;
static char g_serLineBuf[64] = {0};
static uint8_t g_serLineLen = 0;
static uint8_t g_serTrack = 0;
static uint32_t g_serBytesExpected = 0;
static uint32_t g_serBytesReceived = 0;
#ifdef POC_INTERNAL_FLASH
static File g_serFile(InternalFS);
#else
// SPI flash upload: tracks are packed sequentially starting at AUDIO_START_ADDR.
// Uploading track 0 resets the allocation pointer.
static uint32_t g_serFlashCurAddr = 0; // current write head
static uint32_t g_serFlashNextFree = AUDIO_START_ADDR;
static uint32_t g_serFlashErasedThru = 0; // highest erased byte address
#endif
static void serUploadTick() {
if (g_serState == SER_IDLE) {
// Accumulate characters until newline
while (Serial.available()) {
// Serial.print("r");
char c = (char)Serial.read();
if (c == '\n' || c == '\r') {
g_serLineBuf[g_serLineLen] = '\0';
if (g_serLineLen == 0) { g_serLineLen = 0; break; }
// Parse: UPLOAD <track> <len>
unsigned int utrk = 0, ulen = 0;
if (sscanf(g_serLineBuf, "UPLOAD %u %u", &utrk, &ulen) == 2) {
g_serTrack = (uint8_t)utrk;
g_serBytesExpected = (uint32_t)ulen;
g_serBytesReceived = 0;
#ifdef POC_INTERNAL_FLASH
char fname[16];
pocFilename(g_serTrack, fname);
if (g_serFile) g_serFile.close();
// Remove first — FILE_O_WRITE has no truncate flag
InternalFS.remove(fname);
if (!g_serFile.open(fname, FILE_O_WRITE)) {
Serial.print("ERR cannot open ");
Serial.println(fname);
Serial.print("ERR cannot open "); Serial.println(fname);
} else {
g_serState = SER_RECEIVING;
g_usbConnected = true;
Serial.println("READY");
}
#else
// Track 0 resets flash allocation
if (g_serTrack == 0) g_serFlashNextFree = AUDIO_START_ADDR;
g_serFlashCurAddr = g_serFlashNextFree;
g_trackStart[g_serTrack] = g_serFlashCurAddr;
// Erase first sector now; subsequent sectors erased lazily during receive
uint32_t firstSector = (g_serFlashCurAddr / FLASH_SECTOR) * FLASH_SECTOR;
flashEraseSector(firstSector);
g_serFlashErasedThru = firstSector + FLASH_SECTOR - 1;
g_serState = SER_RECEIVING;
g_usbConnected = true;
Serial.println("READY");
#endif
} else if (sscanf(g_serLineBuf, "DUMP %u", &utrk) == 1) {
// Hex-dump first 256 bytes of a track file
#ifdef POC_INTERNAL_FLASH
char fname[16];
pocFilename((uint8_t)utrk, fname);
File df(InternalFS);
@@ -1197,20 +1246,40 @@ static void serUploadTick() {
uint32_t limit = min(fsz, (uint32_t)256);
uint32_t off = 0;
while (off < limit) {
int n = df.read(dbuf, min((uint32_t)sizeof(dbuf), limit - off));
if (n <= 0) break;
for (int j = 0; j < n; j++) {
int rd = df.read(dbuf, min((uint32_t)sizeof(dbuf), limit - off));
if (rd <= 0) break;
for (int j = 0; j < rd; j++) {
if (dbuf[j] < 0x10) Serial.print("0");
Serial.print(dbuf[j], HEX);
Serial.print(j % 16 == 15 || (off + j + 1) == limit ? "\n" : " ");
}
off += n;
off += rd;
}
df.close();
Serial.println("END");
} else {
Serial.println("NO FILE");
}
#else
if ((uint8_t)utrk < g_numTracks) {
Serial.print("SIZE "); Serial.println(g_trackLen[utrk]);
uint8_t dbuf[16];
uint32_t limit = min(g_trackLen[utrk], (uint32_t)256);
for (uint32_t off = 0; off < limit; ) {
uint32_t rd = min((uint32_t)sizeof(dbuf), limit - off);
flashReadBytes(g_trackStart[utrk] + off, dbuf, rd);
for (uint32_t j = 0; j < rd; j++) {
if (dbuf[j] < 0x10) Serial.print("0");
Serial.print(dbuf[j], HEX);
Serial.print(j % 16 == 15 || (off + j + 1) == limit ? "\n" : " ");
}
off += rd;
}
Serial.println("END");
} else {
Serial.println("NO FILE");
}
#endif
} else if (g_serLineLen == 1 && (g_serLineBuf[0] == 'u' || g_serLineBuf[0] == 'd')) {
if (g_serLineBuf[0] == 'u') g_audioGain++;
else if (g_audioGain > 1) g_audioGain--;
@@ -1226,6 +1295,7 @@ static void serUploadTick() {
Serial.println("REBOOT");
delay(10);
NVIC_SystemReset();
#ifdef POC_INTERNAL_FLASH
} else if (sscanf(g_serLineBuf, "DELETE %u", &utrk) == 1) {
char fname[16];
pocFilename((uint8_t)utrk, fname);
@@ -1241,32 +1311,28 @@ static void serUploadTick() {
InternalFS.format();
g_numTracks = 0;
Serial.println("FORMAT OK");
#endif
} else {
Serial.print("ERR bad cmd: ");
Serial.println(g_serLineBuf);
}
g_serLineLen = 0;
// Serial.print("0");
} else {
if (g_serLineLen < (sizeof(g_serLineBuf) - 1)) {
g_serLineBuf[g_serLineLen++] = c;
}
// Serial.print(",");
}
}
} else { // SER_RECEIVING
// Serial.print("e");
uint8_t chunk[64];
while (Serial.available() && g_serBytesReceived < g_serBytesExpected) {
// Serial.print(";");
int n = Serial.readBytes(chunk, min((int)sizeof(chunk),
(int)(g_serBytesExpected - g_serBytesReceived)));
if (n <= 0) break;
#ifdef POC_INTERNAL_FLASH
int32_t wr = g_serFile.write(chunk, (uint16_t)n);
if (wr != n) {
// Write failed (LittleFS full or error) — abort immediately.
// Do NOT loop printing errors; that fills USB CDC TX and hangs.
g_serFile.close();
g_usbConnected = false;
g_serState = SER_IDLE;
@@ -1274,14 +1340,32 @@ static void serUploadTick() {
Serial.println(g_serBytesReceived);
break;
}
g_serBytesReceived += n;
#else
// Lazily erase the next sector as we reach it
uint32_t chunkEnd = g_serFlashCurAddr + (uint32_t)n - 1;
if (chunkEnd > g_serFlashErasedThru) {
uint32_t nextSector = (g_serFlashErasedThru + 1) / FLASH_SECTOR * FLASH_SECTOR;
flashEraseSector(nextSector);
g_serFlashErasedThru = nextSector + FLASH_SECTOR - 1;
}
// Write to flash in page-aligned chunks
uint16_t written = 0;
while (written < (uint16_t)n) {
uint16_t pageOff = (uint16_t)((g_serFlashCurAddr + written) % FLASH_PAGE);
uint16_t pageChunk = min((uint16_t)(FLASH_PAGE - pageOff), (uint16_t)(n - written));
flashPageProgram(g_serFlashCurAddr + written, chunk + written, pageChunk);
written += pageChunk;
}
g_serFlashCurAddr += (uint32_t)n;
#endif
g_serBytesReceived += (uint32_t)n;
}
if (g_serState != SER_RECEIVING) return; // aborted in write-fail handler above
if (g_serState != SER_RECEIVING) return;
if (g_serBytesReceived >= g_serBytesExpected) {
#ifdef POC_INTERNAL_FLASH
g_serFile.close();
// Reopen to read the actual flushed size from LittleFS
char fname2[16]; pocFilename(g_serTrack, fname2);
File tmp(InternalFS);
uint32_t fsz = 0;
@@ -1289,22 +1373,29 @@ static void serUploadTick() {
g_usbConnected = false;
g_serState = SER_IDLE;
loadTrackTable();
Serial.print("OK ");
Serial.print(fsz);
Serial.print("/");
Serial.println(g_serBytesReceived);
Serial.print("OK "); Serial.print(fsz);
Serial.print("/"); Serial.println(g_serBytesReceived);
#else
// Advance free pointer to next sector boundary
g_serFlashNextFree = ((g_serFlashCurAddr + FLASH_SECTOR - 1) / FLASH_SECTOR) * FLASH_SECTOR;
g_trackLen[g_serTrack] = g_serBytesReceived;
if (g_serTrack >= g_numTracks) g_numTracks = g_serTrack + 1;
writeTrackTable();
loadTrackTable();
g_usbConnected = false;
g_serState = SER_IDLE;
Serial.print("OK "); Serial.print(g_serBytesReceived);
Serial.print("/"); Serial.println(g_serBytesExpected);
#endif
}
}
}
#endif
void loop() {
if (g_playing) g_lastActivity = millis();
// ---- Serial upload (POC mode) ----
#ifdef POC_INTERNAL_FLASH
// ---- Serial upload ----
serUploadTick();
#endif
// ---- Refill audio buffers ----
if (g_playing) {