Răsfoiți Sursa

Save to EEPROM

Benjamin Harris 1 lună în urmă
părinte
comite
f873d6b3aa
3 a modificat fișierele cu 345 adăugiri și 3 ștergeri
  1. 143 0
      CLAUDE.md
  2. 118 3
      ModulosDSP_101.ino
  3. 84 0
      index_html.h

+ 143 - 0
CLAUDE.md

@@ -223,3 +223,146 @@ ADAU1401 parameter RAM uses **5.23 fixed-point** (4 bytes per word). `DataConver
 | `index_html.h` | Main device management UI: DSP status, EEPROM upload, DSP/GPIO/WiFi control |
 | `ota_html.h` | OTA firmware flash form only (no other controls) |
 | `ap_html.h` | SoftAP WiFi config portal (first-boot / failed-connect) |
+
+
+
+---
+
+## EXTRACTED FROM   https://github.com/aasayag-hash/ADAU-TCPi-ESP32
+
+## **Project Purpose**
+
+Firmware for ESP32 that replaces ICP1/ICP3/ICP5 USB dongles (~\$30–40) with a ~\$3 solution. It allows SigmaStudio (DSP design software) to communicate with SigmaDSP processors via WiFi or USB Serial, implementing the TCPi over TCP/Serial protocol and supporting hardware safeloading for real-time parameter updates without audio glitches.
+
+## **Supported Chips**
+
+|     |     |     |     |
+| --- | --- | --- | --- |
+| **Chip** | **I2C addr** | **Prog RAM** | **Notes** |
+| ADAU1701 | 0x34 | 0x0400–0x07FF | default chip |
+| ADAU1401 | 0x34 | 0x0400–0x07FF | identical to 1701 |
+| ADAU1401A | 0x34 | 0x0400–0x07FF | automotive version |
+| ADAU1702 | 0x34 | 0x0400–0x05FF | Reduced RAM Program |
+
+They all share the same register map (safeload in `0x0810–0x081C`, Core Control in `0x081C`). The chip is selected from the web UI in `/config`→ "DSP Chip" section and saved in NVS as `chip_index`.
+
+The chip table is located `CHIP_TABLE[]`within the `.ino`. To add a new chip, simply add an entry to that table.
+
+## **Compilation and Flashing**
+
+**Build**: Arduino IDE
+
+1. Open`ADAU1701_TCPi_ESP32/ADAU1701_TCPi_ESP32.ino`
+2. Tools → Board → ESP32 Dev Module
+3. Sketch → Export Compiled Binary
+4. Merge with esptool:`esptool.py --chip esp32 merge_bin -o firmware/ADAU1701_TCPi_ESP32.bin 0x1000 bootloader.bin 0x8000 partitions.bin 0x10000 firmware.bin`
+
+**Flashing pre-compiled firmware** :
+
+```
+esptool.py --chip esp32 --baud 460800 write_flash 0x0 firmware/ADAU1701_TCPi_ESP32.bin
+```
+
+**Via web installer** : Chrome/Edge at https://rarranzb.github.io/ADAU1701-TCPi-ESP32
+
+**There are no automated tests** — validation is functional with real hardware.
+
+## **General Architecture**
+
+All the firmware resides in a single file `.ino`of approximately 800 lines. The main components are:
+
+### **WiFi operating modes**
+
+- **AP Mode** (first start): Hotspot `ADAU1701-ESP32`/`adau1701`
+- **STA Mode** (normal operation): Connected to the user's network
+- **Dual Mode** (during WiFi saving): Simultaneous AP+STA while attempting to connect
+
+### **TCPi Protocol**
+
+TCP server on port 8086. The parser recognizes three opcodes:
+
+- `0x09`: Write request (SigmaStudio → DSP)
+- `0x0A`: Read request
+- `0x0B`: Read response
+
+Handles fragmented packets and multi-frame reassembly. 16 KB receive buffer.
+
+### **Pipeline de Hardware Safeload**
+
+For glitch-free updates while the DSP is running ( `DSPRUN=1`):
+
+1. SigmaStudio sends flag`safeload=1`
+2. Firmware detects that DSP is active (registration `0x081C`)
+3. Divide data into atomic units of 5 words
+4. Write to safeload logs ( `0x0810–0x081C`)
+5. Activate IST (Immediate Synchronous Transfer) bit to apply in the next audio frame
+
+### **Selfboot Capture (EEPROM)**
+
+During the Download phase ( `safeload=0`), it captures all writes in selfboot format from ADAU1701: `[0x01][len_hi][len_lo][0x00][addr_hi][addr_lo][data...]`
+
+Upon detection `DSPRUN=1`(end of Download), `captureReady=true`. The user can then write the buffer to EEPROM 24LC256 from the web UI.
+
+### **I2C scripts by chunks**
+
+Avoid silent corruption by dividing large scripts according to memory region:
+
+- Param RAM ( `0x0000–0x03FF`): 4-byte words
+- Prog RAM ( `0x0400–0x07FF`): 5-byte words
+- Control registers ( `≥0x0800`): 2-byte words
+- Maximum 30 words per I2C transaction
+
+## **Hardware Configuration**
+
+**Default GPIO pins** (reconfigurable from web UI):
+
+|     |     |
+| --- | --- |
+| **Sign** | **GPIO** |
+| SCL | 17  |
+| SDA | 16  |
+| RESET | 21  |
+| SELFBOOT | 19  |
+| LED | 2   |
+| BOOT button | 0   |
+
+**I2C Addresses** :
+
+- `0x34`: ADAU1701 DSP
+- `0x50`: EEPROM 24LC256
+
+**I2C**: 400 kHz, buffer 2048 bytes, timeout 50 ms
+
+## **Persistence (NVS)**
+
+Namespace `"tcpi"`. Stored keys:
+
+- `ssid`, `password`: WiFi credentials
+- `pin_scl`, `pin_sda`, `pin_reset`, `pin_selfboot`, `pin_led`: pin assignment
+
+## **Web Routes**
+
+|     |     |     |
+| --- | --- | --- |
+| **URL** | **Method** | **Function** |
+| `/` | GET | Status, Save EEPROM button, DSP reset |
+| `/config` | GET | Configure WiFi and GPIO pins |
+| `/status` | GET | JSON: `dspRunning`, `apMode`, `captureReady`, etc. |
+| `/save_wifi` | POST | Save credentials and restart |
+| `/save_pins` | POST | Save PINs and restart |
+| `/save_chip` | POST | Save selected chip and restart |
+| `/reset_dsp` | POST | Pulso RESET (10 ms low, 50 ms high) |
+| `/save_eeprom` | POST | Write capture buffer to EEPROM |
+| `/factory_reset` | POST | Clean the entire NVS |
+
+## **Dependencies**
+
+Only Arduino ESP32 core libraries: `WiFi.h`, `Wire.h`, `WebServer.h`, `Preferences.h`. No external dependencies.
+
+## **Memory Map ADAU1701**
+
+- RAM Parameter:`0x0000–0x03FF`
+- Prog RAM: `0x0400–0x07FF`
+- Control records:`0x0800+`
+- Safeload logs:`0x0810–0x081C`
+- Capture buffer: maximum 28 KB (4 KB margin on 32 KB EEPROM)

+ 118 - 3
ModulosDSP_101.ino

@@ -356,6 +356,50 @@ static volatile bool     uploadFailed = false;
 static volatile uint32_t uploadBytes  = 0;
 static volatile uint32_t uploadCrc    = 0;
 static char              uploadErrorMsg[80] = "";
+static uint8_t           s_uploadFirstBytes[16];
+static uint8_t           s_uploadFirstCount = 0;
+
+//=============================================================
+// Capture-to-EEPROM state
+// captureBuffer holds the selfboot image built during each TCP
+// session.  It is populated by captureWrite() for every sl=0
+// DSP write and is committed to 24C256 by handleSaveEeprom().
+//=============================================================
+static constexpr int CAPTURE_MAX_SIZE = 28 * 1024;
+static uint8_t captureBuffer[CAPTURE_MAX_SIZE];
+static int     captureLen   = 0;
+static bool    captureReady = false;
+
+// Encode one sl=0 DSP write into the selfboot EEPROM format
+// (Analog Devices datasheet Table 19: [0x01][len_hi][len_lo][0x00][addr_hi][addr_lo][data...])
+// and append it to captureBuffer.  Splits large writes into 32-word chunks.
+static void captureWrite(uint16_t address, const uint8_t* data, int dataLen)
+{
+  int wordSize = 4;                                       // parameter RAM
+  if (address >= 0x0400 && address <= 0x07FF) wordSize = 5; // program RAM
+  else if (address >= 0x0800)                 wordSize = 1; // hardware regs
+
+  int maxChunk = 32 * wordSize;
+  int offset   = 0;
+  while (offset < dataLen) {
+    int      bytes = min(dataLen - offset, maxChunk);
+    uint16_t addr  = address + (uint16_t)(offset / wordSize);
+    if (captureLen + 6 + bytes + 2 > CAPTURE_MAX_SIZE) {
+      Serial.println("CAP: buffer full");
+      return;
+    }
+    uint16_t count = (uint16_t)(bytes + 3); // chipAddr(1) + regAddr(2) + data
+    captureBuffer[captureLen++] = 0x01;
+    captureBuffer[captureLen++] = (uint8_t)(count >> 8);
+    captureBuffer[captureLen++] = (uint8_t)(count & 0xFF);
+    captureBuffer[captureLen++] = 0x00;
+    captureBuffer[captureLen++] = (uint8_t)(addr >> 8);
+    captureBuffer[captureLen++] = (uint8_t)(addr & 0xFF);
+    memcpy(&captureBuffer[captureLen], data + offset, bytes);
+    captureLen += bytes;
+    offset     += bytes;
+  }
+}
 
 //=============================================================
 // HTTP handlers
@@ -418,7 +462,7 @@ static void handleUploadStream() {
 
   if (up.status == UPLOAD_FILE_START) {
     uploadActive = true; uploadFailed = false; uploadBytes = 0; uploadCrc = 0;
-    uploadErrorMsg[0] = '\0';
+    uploadErrorMsg[0] = '\0'; s_uploadFirstCount = 0;
     uploadVerify = httpServer.hasArg("verify");
     Serial.println(); Serial.print("HTTP upload start: "); Serial.println(up.filename);
     Serial.print("Verify: "); Serial.println(uploadVerify ? "yes" : "no");
@@ -442,6 +486,10 @@ static void handleUploadStream() {
                "EEPROM I2C write failed at offset %u", (uint32_t)uploadBytes);
       Serial.println(uploadErrorMsg); uploadFailed = true; return;
     }
+    if (s_uploadFirstCount == 0) {
+      s_uploadFirstCount = up.currentSize < 16 ? up.currentSize : 16;
+      memcpy(s_uploadFirstBytes, up.buf, s_uploadFirstCount);
+    }
     uploadCrc = esp_rom_crc32_le(uploadCrc, up.buf, up.currentSize);
     uploadBytes += up.currentSize;
     ledMagenta();
@@ -450,6 +498,20 @@ static void handleUploadStream() {
     Serial.print("HTTP upload end, bytes="); Serial.println((uint32_t)uploadBytes);
     if (uploadVerify && !uploadFailed) {
       Serial.println("Verify start (CRC32)...");
+      // Diagnostic: compare first bytes uploaded vs first bytes in EEPROM
+      if (s_uploadFirstCount > 0) {
+        uint8_t eepFirst[16];
+        Serial.print("Uploaded[0]: ");
+        for (uint8_t i = 0; i < s_uploadFirstCount; i++) Serial.printf("0x%02X ", s_uploadFirstBytes[i]);
+        Serial.println();
+        if (i2cReadBlock(EEPROM_7BIT, 0, eepFirst, s_uploadFirstCount)) {
+          Serial.print("EEPROM [0]: ");
+          for (uint8_t i = 0; i < s_uploadFirstCount; i++) Serial.printf("0x%02X ", eepFirst[i]);
+          Serial.println();
+        } else {
+          Serial.println("EEPROM [0]: read failed");
+        }
+      }
       uint32_t crc = 0;
       static uint8_t tmp[256];
       uint32_t remaining = uploadBytes; uint16_t addr = 0;
@@ -706,6 +768,42 @@ static void handleOtaStream() {
   }
 }
 
+//=============================================================
+// Capture-to-EEPROM HTTP handlers
+//=============================================================
+static void handleCaptureStatus() {
+  char buf[64];
+  snprintf(buf, sizeof(buf), "{\"ready\":%s,\"bytes\":%d}",
+           captureReady ? "true" : "false", captureLen);
+  httpServer.send(200, "application/json", buf);
+}
+
+static void handleSaveEeprom() {
+  if (!captureReady || captureLen == 0) {
+    httpServer.send(400, "application/json",
+      "{\"ok\":false,\"error\":\"No capture — connect SigmaStudio and download first.\"}");
+    return;
+  }
+  Serial.printf("Save to EEPROM: %d bytes\n", captureLen);
+  // Append selfboot end marker
+  captureBuffer[captureLen]     = 0x00;
+  captureBuffer[captureLen + 1] = 0x00;
+  int totalLen = captureLen + 2;
+  ledYellow();
+  bool ok = eepromWriteBlock(0, captureBuffer, (uint16_t)totalLen);
+  ledOff();
+  if (ok) {
+    char buf[64];
+    snprintf(buf, sizeof(buf), "{\"ok\":true,\"bytes\":%d}", totalLen);
+    httpServer.send(200, "application/json", buf);
+    Serial.printf("Save to EEPROM OK: %d bytes written\n", totalLen);
+  } else {
+    httpServer.send(500, "application/json",
+      "{\"ok\":false,\"error\":\"EEPROM I2C write failed\"}");
+    Serial.println("Save to EEPROM FAILED");
+  }
+}
+
 //=============================================================
 // Setup
 //=============================================================
@@ -782,8 +880,10 @@ void setup() {
   httpServer.on("/gpio",       HTTP_POST, handleGpioSet);
   httpServer.on("/wifi_reset", HTTP_POST, handleWifiReset);
   httpServer.on("/params",     HTTP_GET,  handleParamsPage);
-  httpServer.on("/param",      HTTP_GET,  handleParamGet);
-  httpServer.on("/param",      HTTP_POST, handleParamSet);
+  httpServer.on("/param",          HTTP_GET,  handleParamGet);
+  httpServer.on("/param",          HTTP_POST, handleParamSet);
+  httpServer.on("/capture_status", HTTP_GET,  handleCaptureStatus);
+  httpServer.on("/save_eeprom",    HTTP_POST, handleSaveEeprom);
   httpServer.onNotFound([]() {
     String uri = httpServer.uri();
     if (streamFromFS(uri)) return;
@@ -813,6 +913,8 @@ static void handleTcpBridgeClient(WiFiClient& client)
 {
   Serial.println("TCP new connection");
   DSPWriter::resetSafeload();
+  captureLen   = 0;    // fresh capture for this session
+  captureReady = false;
 
   int      writeIndex        = 0;          // next free slot in dataBuffer
   int      readIndex         = 0;          // start of current unprocessed command
@@ -947,6 +1049,19 @@ static void handleTcpBridgeClient(WiFiClient& client)
           writeOk = DSPWriter::writeRegisterBlock(regAddress, writeHeader.dataLen,
                                                   &dataBuffer[readIndex], registerSize);
           if (!writeOk) Serial.println("TCP DSP block write failed");
+          // Capture sl=0 writes into selfboot image (captureWrite before DSPRUN check
+          // so the final "start DSP" CoreRegister write is included in the image).
+          if (writeOk && !captureReady) {
+            captureWrite(writeHeader.address, &dataBuffer[readIndex], (int)writeHeader.dataLen);
+          }
+          // Detect DSPRUN bit going high = download sequence complete
+          if (writeHeader.address == dspRegister::CoreRegister && writeHeader.dataLen >= 2) {
+            bool running = (dataBuffer[readIndex + writeHeader.dataLen - 1] & 0x01) != 0;
+            if (running && !captureReady) {
+              captureReady = true;
+              Serial.printf("CAP: capture ready — %d bytes\n", captureLen);
+            }
+          }
         }
 
         readIndex += writeHeader.dataLen;

+ 84 - 0
index_html.h

@@ -159,6 +159,27 @@ static const char INDEX_HTML[] PROGMEM = R"HTML(
       <!-- ── Right column ────────────────────────────────────── -->
       <div class="col-lg-6">
 
+        <!-- Capture to EEPROM -->
+        <div class="card mb-3">
+          <div class="card-header py-2 d-flex justify-content-between align-items-center">
+            <span class="small fw-semibold"><i class="fa fa-database"></i> Capture to EEPROM</span>
+            <span id="capBadge" class="badge bg-secondary">Checking&hellip;</span>
+          </div>
+          <div class="card-body py-2 px-3">
+            <p class="text-muted small mb-2">
+              Connect SigmaStudio via TCP and run <em>Link &rarr; Compile &rarr; Download</em>.
+              When capture is ready, click <strong>Save to EEPROM</strong> to write the selfboot image.
+              The DSP will load this program autonomously on next power-up.
+            </p>
+            <button id="saveEepromBtn" class="btn w-100 mb-2"
+                    style="background-color:#114378;border-color:#114378;color:#e8b434;"
+                    onclick="saveEeprom()" disabled>
+              <i class="fa fa-save"></i>&nbsp; Save to EEPROM
+            </button>
+            <div id="saveResult" style="display:none;" class="alert alert-sm mt-1" role="alert"></div>
+          </div>
+        </div>
+
         <!-- EEPROM Upload -->
         <div class="card">
           <div class="card-header py-2">
@@ -414,6 +435,69 @@ function doUpload(blob, name) {
   xhr.send(formData);
 }
 
+function pollCapture() {
+  var xhr = new XMLHttpRequest();
+  xhr.open('GET', '/capture_status', true);
+  xhr.timeout = 1800;
+  xhr.onload = function() {
+    if (xhr.status !== 200) return;
+    try {
+      var d = JSON.parse(xhr.responseText);
+      var badge = document.getElementById('capBadge');
+      var btn   = document.getElementById('saveEepromBtn');
+      if (d.ready) {
+        badge.className   = 'badge bg-success';
+        badge.textContent = Math.round(d.bytes / 1024 * 10) / 10 + ' kB ready';
+        btn.disabled = false;
+      } else {
+        badge.className   = 'badge bg-secondary';
+        badge.textContent = 'No capture';
+        btn.disabled = true;
+      }
+    } catch(e) {}
+  };
+  xhr.send();
+}
+
+function saveEeprom() {
+  var btn = document.getElementById('saveEepromBtn');
+  var box = document.getElementById('saveResult');
+  btn.disabled = true;
+  box.style.display = 'none';
+  var xhr = new XMLHttpRequest();
+  xhr.open('POST', '/save_eeprom', true);
+  xhr.timeout = 30000;
+  xhr.onload = function() {
+    box.style.display = 'block';
+    if (xhr.status === 200) {
+      try {
+        var d = JSON.parse(xhr.responseText);
+        box.className = 'alert alert-success alert-sm';
+        box.innerHTML = '<i class="fa fa-check"></i> Saved ' + d.bytes + ' bytes to EEPROM.';
+      } catch(e) {
+        box.className = 'alert alert-success alert-sm';
+        box.innerHTML = '<i class="fa fa-check"></i> Saved to EEPROM.';
+      }
+    } else {
+      var reason = '';
+      try { var d = JSON.parse(xhr.responseText); if (d.error) reason = d.error; } catch(e) {}
+      box.className = 'alert alert-danger alert-sm';
+      box.innerHTML = '<i class="fa fa-times"></i> ' + (reason || 'Save failed.');
+    }
+    btn.disabled = false;
+  };
+  xhr.ontimeout = xhr.onerror = function() {
+    box.style.display = 'block';
+    box.className = 'alert alert-danger alert-sm';
+    box.innerHTML = '<i class="fa fa-times"></i> Request timed out.';
+    btn.disabled = false;
+  };
+  xhr.send();
+}
+
+pollCapture();
+setInterval(pollCapture, 3000);
+
 function wifiReset() {
   if (!confirm('Clear WiFi credentials and reboot into setup mode?')) return;
   var box = document.getElementById('wifiResetResult');