fixed most upload issues, updated schematic

This commit is contained in:
zyphlar
2026-07-04 03:28:14 -07:00
parent 12140b7ff6
commit 8cba579780
10 changed files with 1305 additions and 1020 deletions
+166 -46
View File
@@ -183,7 +183,8 @@ BLECharacteristic audioCmd = BLECharacteristic("12340002-0000-1000-8000-00805f9
BLECharacteristic audioData = BLECharacteristic("12340003-0000-1000-8000-00805f9b34fb");
BLECharacteristic audioStat = BLECharacteristic("12340004-0000-1000-8000-00805f9b34fb");
// BLE upload state
// BLE connection / upload state
volatile bool g_bleConnected = false; // true while a GATT connection is active
volatile bool g_bleUploading = false;
volatile uint32_t g_bleWriteLen = 0;
@@ -192,17 +193,20 @@ File g_pocFile(InternalFS); // open file handle (read or write)
uint8_t g_pocWriteTrack = 0; // track slot being written via BLE
#else
// Ring buffer between BLE callbacks and loop() flash writes.
// Callbacks return immediately; flash I/O (especially sector erase ~100ms)
// happens in loop() so the SoftDevice receive buffer never stalls.
// Size: IS25LP128F sector erase takes up to 300 ms; at ~56 KB/s upload rate
// that is ~17 KB of incoming data. 32 KB covers it with margin.
#define BLE_FLASH_BUF 32768u
// Callbacks return immediately; flash I/O happens in loop() so the SoftDevice
// receive buffer never stalls. Sector erase is async (non-blocking): bleFlashTick()
// kicks off the erase and returns; loop() polls completion each iteration. This
// prevents the ring buffer from filling during the ~30300 ms erase window.
// Buffer: 64 KB handles worst-case 300 ms erase at up to ~200 KB/s BLE throughput.
#define BLE_FLASH_BUF 65536u
static uint8_t g_bleBuf[BLE_FLASH_BUF];
static volatile uint32_t g_bleBufHead = 0; // advanced by BLE callback
static volatile uint32_t g_bleBufTail = 0; // advanced by loop()
static uint32_t g_bleFlashAddr = 0; // current flash write head
static uint32_t g_bleFlashStart = 0; // address where this upload began
static uint32_t g_bleFlashErased = 0; // end of last erased sector
static uint32_t g_bleFlashErased = 0; // upper boundary of erased flash
static bool g_bleErasing = false; // async sector erase in progress
static uint32_t g_bleNotifyThresh = 0; // next addr to send progress notify
static uint8_t g_bleWriteTrack = 0;
static volatile bool g_bleFinalizing = false;
@@ -306,6 +310,17 @@ void flashWaitBusy() {
SPI.endTransaction();
}
// Non-blocking busy check: reads the WIP bit without spinning.
bool flashIsBusy() {
SPI.beginTransaction(flashSPI);
flashSelect();
SPI.transfer(CMD_READ_SR1);
uint8_t sr = SPI.transfer(0);
flashDeselect();
SPI.endTransaction();
return (sr & 0x01) != 0;
}
void flashWriteEnable() {
SPI.beginTransaction(flashSPI);
flashSelect();
@@ -564,12 +579,26 @@ bool msc_start_stop_cb(uint8_t power_condition, bool start, bool load_eject) {
void ble_connect_cb(uint16_t conn_handle) {
Serial.println("BLE connected");
g_bleConnected = true;
g_lastActivity = millis();
// Request fast connection parameters and maximum throughput features.
// The central may accept, renegotiate, or ignore these — all safe.
BLEConnection* conn = Bluefruit.Connection(conn_handle);
if (conn) {
conn->requestConnectionParameter(6); // 6×1.25ms = 7.5ms interval
conn->requestMtuExchange(247); // 244-byte ATT payload
conn->requestDataLengthUpdate(); // LE Data Length Extension
conn->requestPHY(BLE_GAP_PHY_2MBPS); // 2M PHY if supported
}
}
void ble_disconnect_cb(uint16_t conn_handle, uint8_t reason) {
Serial.println("BLE disconnected");
g_bleUploading = false;
g_bleConnected = false;
g_bleUploading = false;
g_bleFinalizing = false;
g_bleErasing = false; // stop tracking the in-progress erase; it will finish in HW
}
// BLE command characteristic: receives commands
@@ -626,9 +655,11 @@ void audioCmd_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
g_bleWriteTrack = data[1];
// Track 0 resets allocation; subsequent tracks append
if (g_bleWriteTrack == 0) g_flashNextFree = AUDIO_START_ADDR;
g_bleFlashAddr = g_flashNextFree;
g_bleFlashStart = g_bleFlashAddr; // save start for finalize
g_bleFlashErased = g_bleFlashAddr; // nothing erased yet
g_bleFlashAddr = g_flashNextFree;
g_bleFlashStart = g_bleFlashAddr;
g_bleFlashErased = g_bleFlashAddr; // nothing erased yet
g_bleErasing = false;
g_bleNotifyThresh = g_bleFlashAddr;
g_bleBufHead = g_bleBufTail = 0;
g_bleFinalizing = false;
g_bleUploading = true;
@@ -763,37 +794,92 @@ void audioData_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
// and writes the track table).
}
// Drain the BLE ring buffer to SPI flash one page at a time.
// Called from loop() to keep flash I/O off the BLE callback thread.
// Drain the BLE ring buffer to SPI flash.
// Sector erases are asynchronous: we issue the erase command and return immediately
// so loop() keeps running (and the ring buffer keeps draining from BLE callbacks).
// On the next call we poll the WIP bit to confirm completion before writing.
// This prevents the 30300 ms erase window from filling the ring buffer.
// A pre-erase is also kicked off as soon as we start writing each sector so
// the next sector is ready before we reach it. Writes stop while any erase is
// in progress because the flash ignores page-program when WIP=1.
#ifndef POC_INTERNAL_FLASH
static void bleFlashTick() {
// Drain as much as we can without blocking too long
if (!g_bleUploading && !g_bleFinalizing) return;
// Poll async sector erase completion.
if (g_bleErasing && !flashIsBusy()) {
g_bleErasing = false;
g_bleFlashErased += FLASH_SECTOR;
}
// Drain all available ring-buffer data into flash, one page per iteration.
while (g_bleBufHead != g_bleBufTail) {
uint32_t avail = (g_bleBufHead - g_bleBufTail + BLE_FLASH_BUF) % BLE_FLASH_BUF;
if (g_bleErasing) break; // never write while an erase is in progress
uint32_t avail = (g_bleBufHead - g_bleBufTail + BLE_FLASH_BUF) % BLE_FLASH_BUF;
uint16_t pageOff = (uint16_t)(g_bleFlashAddr % FLASH_PAGE);
uint16_t chunk = (uint16_t)min((uint32_t)(FLASH_PAGE - pageOff), avail);
uint16_t chunk = (uint16_t)min((uint32_t)(FLASH_PAGE - pageOff), avail);
if (chunk == 0) break;
// Lazy erase: erase the next sector just before we write into it
while (g_bleFlashAddr + chunk > g_bleFlashErased) {
flashEraseSector(g_bleFlashErased);
g_bleFlashErased += FLASH_SECTOR;
if (g_bleFlashAddr + chunk > g_bleFlashErased) {
// Write pointer has reached the erased boundary — need another sector erased.
if (!g_bleErasing) {
flashWriteEnable();
SPI.beginTransaction(flashSPI);
flashSelect();
SPI.transfer(CMD_SECT_ERASE);
SPI.transfer((g_bleFlashErased >> 16) & 0xFF);
SPI.transfer((g_bleFlashErased >> 8) & 0xFF);
SPI.transfer( g_bleFlashErased & 0xFF);
flashDeselect();
SPI.endTransaction();
g_bleErasing = true;
// Do NOT advance g_bleFlashErased — wait for WIP confirmation next call.
}
break; // return to loop(); erase runs in HW, ring buffer fills freely
}
// Copy chunk from ring buffer into a temp page buffer
// Erase boundary is ahead — safe to program this page.
uint8_t tmp[FLASH_PAGE];
for (uint16_t i = 0; i < chunk; i++) {
tmp[i] = g_bleBuf[(g_bleBufTail + i) % BLE_FLASH_BUF];
}
flashPageProgram(g_bleFlashAddr, tmp, chunk);
g_bleBufTail = (g_bleBufTail + chunk) % BLE_FLASH_BUF;
g_bleFlashAddr += chunk;
flashPageProgram(g_bleFlashAddr, tmp, chunk); // ~0.5 ms blocking
g_bleBufTail = (g_bleBufTail + chunk) % BLE_FLASH_BUF;
g_bleFlashAddr += chunk;
// Yield after each page so the rest of loop() stays responsive
break;
// Pre-erase: kick off the next sector erase as soon as we start writing
// the current sector so the erase (~30 ms typ) completes before we need it.
// Break immediately — never write while an async erase is in progress since
// the flash chip silently ignores page-program commands when WIP=1.
if (!g_bleErasing && g_bleFlashAddr > g_bleFlashErased - FLASH_SECTOR) {
flashWriteEnable();
SPI.beginTransaction(flashSPI);
flashSelect();
SPI.transfer(CMD_SECT_ERASE);
SPI.transfer((g_bleFlashErased >> 16) & 0xFF);
SPI.transfer((g_bleFlashErased >> 8) & 0xFF);
SPI.transfer( g_bleFlashErased & 0xFF);
flashDeselect();
SPI.endTransaction();
g_bleErasing = true;
break; // wait for erase; ring buffer fills freely in the meantime
}
// Periodic progress notification — keeps the Windows BLE stack from
// dropping the connection during long silent uploads, and gives the
// client real flash-write progress (not just BLE-send progress).
if (g_bleConnected && g_bleFlashAddr - g_bleNotifyThresh >= 4096u) {
g_bleNotifyThresh = g_bleFlashAddr;
uint32_t written = g_bleFlashAddr - g_bleFlashStart;
uint8_t stat[4] = {
(uint8_t)(written >> 24), (uint8_t)(written >> 16),
(uint8_t)(written >> 8), (uint8_t)(written)
};
audioStat.notify(stat, 4);
}
}
// Finalize when CMD 0x03 received and ring buffer is empty
// Finalize when CMD 0x03 received and ring buffer is fully drained.
if (g_bleFinalizing && g_bleBufHead == g_bleBufTail) {
uint32_t startAddr = g_bleFlashStart;
uint32_t bytesStored = g_bleFlashAddr - g_bleFlashStart;
@@ -806,6 +892,7 @@ static void bleFlashTick() {
g_bleUploading = false;
g_bleFinalizing = false;
g_bleErasing = false; // pre-erase of unused sector, if any, will finish in HW
writeTrackTable();
loadTrackTable();
Serial.printf("BLE: Upload finalized — start=0x%08lX stored=%lu bytes\n", startAddr, bytesStored);
@@ -856,11 +943,15 @@ void setupBLE() {
audioStat.setMaxLen(4);
audioStat.begin();
// Start advertising
// Start advertising.
// The 128-bit service UUID (18 bytes) plus flags + TxPower nearly fills the
// 31-byte advertisement, leaving room for only ~5 name characters — which
// truncated the name to "BabyM" over the air. Put the full name in the scan
// response (its own separate 31 bytes) so scanners see "BabyMobile" intact.
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
Bluefruit.Advertising.addService(audioSvc);
Bluefruit.Advertising.addName();
Bluefruit.ScanResponse.addName(); // full name here, not in the ad packet
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.setInterval(160, 320); // 100-200ms
Bluefruit.Advertising.start(0); // advertise forever
@@ -1401,22 +1492,46 @@ 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);
if (InternalFS.remove(fname)) {
Serial.print("DELETED "); Serial.println(fname);
} else {
Serial.print("ERR no file "); Serial.println(fname);
}
loadTrackTable();
} else if (strcmp(g_serLineBuf, "FORMAT") == 0) {
audioStop();
#ifdef POC_INTERNAL_FLASH
Serial.println("Formatting LittleFS...");
InternalFS.format();
g_numTracks = 0;
#else
Serial.println("Clearing track table...");
g_numTracks = 0;
g_flashNextFree = AUDIO_START_ADDR;
writeTrackTable();
loadTrackTable();
#endif
Serial.println("FORMAT OK");
} else if (sscanf(g_serLineBuf, "DELETE %u", &utrk) == 1) {
#ifdef POC_INTERNAL_FLASH
char fname[16];
pocFilename((uint8_t)utrk, fname);
if (InternalFS.remove(fname)) {
Serial.print("DELETED "); Serial.println(utrk);
} else {
Serial.print("ERR no file "); Serial.println(utrk);
}
loadTrackTable();
#else
if ((uint8_t)utrk < g_numTracks) {
for (uint8_t j = (uint8_t)utrk; j < g_numTracks - 1; j++) {
g_trackStart[j] = g_trackStart[j+1];
g_trackLen[j] = g_trackLen[j+1];
g_trackBits[j] = g_trackBits[j+1];
g_trackRate[j] = g_trackRate[j+1];
g_trackDataOff[j] = g_trackDataOff[j+1];
}
g_numTracks--;
writeTrackTable();
loadTrackTable();
Serial.print("DELETED "); Serial.println(utrk);
} else {
Serial.print("ERR no track "); Serial.println(utrk);
}
#endif
} else {
Serial.print("ERR bad cmd: ");
@@ -1430,6 +1545,7 @@ static void serUploadTick() {
}
}
} else { // SER_RECEIVING
g_lastActivity = millis(); // keep device awake during long transfers
uint8_t chunk[64];
while (Serial.available() && g_serBytesReceived < g_serBytesExpected) {
int n = Serial.readBytes(chunk, min((int)sizeof(chunk),
@@ -1498,7 +1614,7 @@ static void serUploadTick() {
}
void loop() {
if (g_playing) g_lastActivity = millis();
if (g_playing || g_bleUploading) g_lastActivity = millis();
// ---- Serial upload ----
serUploadTick();
@@ -1592,14 +1708,18 @@ void loop() {
}
// ---- Auto shutoff / idle sleep ----
if (millis() - g_lastActivity > AUTO_OFF_MS) {
enterDeepSleep();
}
if (!g_playing && !g_motorOn && !g_usbConnected && !g_bleUploading) {
if (millis() - g_lastActivity > IDLE_SLEEP_MS) {
// Never sleep while BLE is connected — upload may be in progress or about
// to begin, and the CMD callback sets g_bleUploading asynchronously.
if (!g_bleConnected) {
if (millis() - g_lastActivity > AUTO_OFF_MS) {
enterDeepSleep();
}
if (!g_playing && !g_motorOn && !g_usbConnected) {
if (millis() - g_lastActivity > IDLE_SLEEP_MS) {
enterDeepSleep();
}
}
}
}