fixed most upload issues, updated schematic
This commit is contained in:
@@ -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 ~30–300 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 30–300 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user