Benjamin Harris 1 сар өмнө
parent
commit
045ecde560
3 өөрчлөгдсөн 198 нэмэгдсэн , 19 устгасан
  1. 3 2
      .gitignore
  2. 164 0
      CLAUDE.md
  3. 31 17
      README.md

+ 3 - 2
.gitignore

@@ -1,5 +1,6 @@
 .claude/
 research/
 
-CLAUDE.md
-desktop.ini
+desktop.ini
+
+sigma_studio/

+ 164 - 0
CLAUDE.md

@@ -0,0 +1,164 @@
+# CLAUDE.md — ModulosDSP_101
+
+## What This Is
+
+Arduino sketch for the **Waveshare ESP32-S3 Zero** that bridges SigmaStudio (Analog Devices' DSP programming GUI) to a physical **ADAU1401/1701 DSP** over WiFi → TCP → I²C. It also hosts an HTTP UI for writing DSP firmware to a 24C256 EEPROM, and an HTTP OTA page for reflashing the ESP32 itself. The sketch runs on the companion **MSD ADAU14-1701 Adapter** PCB (Altium files in the parent directory).
+
+## Build Environment
+
+- **Arduino IDE** 1.8.x or 2.x with the ESP32 Arduino core
+- Board: **ESP32S3 Dev Module** (Flash mode: QIO, 4 MB, Default 4MB with spiffs partition)
+- Both HTML UIs (`index_html.h`, `ota_html.h`) are PROGMEM string literals compiled into flash — no separate LittleFS image upload is required
+- Baud rate for Serial monitor: **115200**
+
+## Architecture
+
+Two servers run in parallel — one TCP, one HTTP:
+
+- `tcpServer` (port 8086): SigmaTCP protocol. `handleTcpBridgeClient()` blocks `loop()` for the duration of a session (single client at a time).
+- `httpServer` (port 80): EEPROM upload (`/`), OTA firmware update (`/ota`), and status endpoints. `httpServer.handleClient()` is called inside the TCP client loop so HTTP stays responsive during a TCP session.
+
+`loop()` only accepts a new TCP client after the previous one disconnects. There is no concurrency and no idle timeout — the connection stays open until SigmaStudio closes it.
+
+`maintainWifi()` is called at the top of every `loop()` iteration and checks WiFi status every 5 seconds, attempting reconnect if the link has dropped.
+
+## Build / Version Scheme
+
+The build identifier uses `YYMMDD_rev` format (e.g. `260304_13`). It appears in two places that must always match:
+
+1. The file header comment: `// Build: 260304_13  (YYMMDD_rev)`
+2. The runtime constant: `const char* version = "VER: 260304_13";`
+
+When cutting a new build, update both. The `version` constant is displayed on the LCD and printed to Serial at boot.
+
+## WiFi Credentials
+
+Hardcoded at the top of `ModulosDSP_101.ino`:
+
+```cpp
+const char* ssid     = "alfred";
+const char* password = "alfred16";
+```
+
+Change before flashing to a different network. No runtime config exists. `WiFi.setAutoReconnect(true)` is set in `setup()` and `maintainWifi()` handles mid-session drops.
+
+## WiFi Watchdog — `maintainWifi()`
+
+Runs every 5 seconds (throttled by `millis()`). On first detection of a drop: logs to Serial, updates LCD row 3, sets `s_wifiLost = true`, calls `WiFi.reconnect()`. On recovery: calls `tcpServer.begin()` to re-register the listening socket (required after the TCP/IP stack resets), then calls `printWifiInfo()` to update the LCD with the current IP. Does not run while `handleTcpBridgeClient()` is blocking — that is fine because an active TCP session implies WiFi is up.
+
+## LCD — Optional Hardware
+
+The LCD is detected at boot via the return value of `lcd.begin(20, 4)` (hd44780 returns 0 on success). The result is stored in `static bool s_lcdOk`. All subsequent `lcd.*` calls throughout the sketch are gated on `s_lcdOk`. If the LCD is absent the sketch boots and runs normally; all status information is available on Serial (115200 baud) instead.
+
+## I²C Addresses
+
+| Device | 8-bit | 7-bit (Wire) |
+|--------|-------|--------------|
+| ADAU1401 DSP | 0x68 | 0x34 |
+| 24C256 EEPROM | 0xA0 | 0x50 |
+
+Both devices share the bus at 400 kHz on SDA=GPIO13, SCL=GPIO12.
+
+## SigmaTCP Protocol Details
+
+SigmaStudio sends `chipAddr` as a **chip index** (0x01 = DSP, 0x02 = EEPROM), not a raw I²C address. `chipAddrTo7bit()` maps these — and also handles legacy 8-bit addresses (0x68, 0xA0) and already-shifted 7-bit addresses (0x34, 0x50).
+
+**WRITE (opcode 0x09) — 10-byte header:**
+
+```
+[0]   0x09       command
+[1]   safeload   1 = use safeload registers, 0 = direct write
+[2]   placement  reserved
+[3-4] totalLen   big-endian: header + payload bytes
+[5]   chipAddr   chip index or I2C address
+[6-7] dataLen    big-endian: payload bytes
+[8-9] address    big-endian: DSP/EEPROM start address
+[10+] payload
+```
+
+`totalLen` is validated against `WRITE_HDR_LEN + dataLen` immediately after parsing. A mismatch drops the connection — do not remove this check; it prevents `dataLen` from being blindly trusted to drive buffer waits or I²C writes when a packet is structurally inconsistent.
+
+ACK response: `{0x09, 0x00, 0x04, status}` (4 bytes). `status` reflects the actual I²C result — `writeRegisterBlock()` returns `bool` and the result is threaded through to the ACK. Do not replace with a hardcoded success.
+
+**READ (opcode 0x0A) — 8-byte header:**
+
+```
+[0]   0x0A
+[1-2] totalLen
+[3]   chipAddr
+[4-5] dataLen
+[6-7] address
+```
+
+Response: `{0x0B, totalLen_hi, totalLen_lo, status, dataLen_hi, dataLen_lo, [payload]}` (6-byte header + data). On I²C failure, zeros fill the payload rather than dropping the connection — this allows SigmaStudio to probe an unresponsive DSP without aborting.
+
+## Safeload — Critical Rules
+
+The DSP has 5 safeload slots. Filling them and writing `0x003C` to the Core Register (0x081C) commits all pending parameters atomically (no audio glitch).
+
+- `s_safeload_count` is **static** in `DSPWriter.cpp` — it survives across `DSPWriter` instances.
+- **`DSPWriter::resetSafeload()` must be called at both the start and end of every TCP session.** It is called at the top of `handleTcpBridgeClient()` (session start) and just before `client.stop()` (session end).
+- `resetSafeload()` now **flushes** before resetting: if `s_safeload_count > 0` it writes `{0x00, 0x3C}` to the Core Register to trigger IST, clearing the DSP's own internal safeload counter. A session ending with `count == 0` (normal case) is a no-op.
+- Safeload writes are always 5 bytes: `{0x00, byte1, byte2, byte3, byte4}`. The leading `0x00` is mandatory padding.
+- When `safeload == 1` in a WRITE packet, `dataLen` must be a multiple of 4 (each parameter is 4 bytes). The sketch validates this and drops the connection if violated.
+
+## `writeRegisterBlock` — Error Propagation
+
+`DSPWriter::writeRegisterBlock()` returns `bool`. On any `Wire.endTransmission()` error it logs the failing address and error code to Serial and returns `false` immediately (stops writing — continuing would advance the address pointer and write misaligned data). The return value is captured at the call site and passed directly to `sendWriteAck()`.
+
+## Register Sizes
+
+`registerSizeForAddress()` derives the I²C bytes-per-register from the address:
+
+| Address range | Register size |
+|---------------|--------------|
+| 0x0000–0x03FF (Parameter RAM) | 4 bytes |
+| 0x0400–0x07FF (Program RAM) | 5 bytes |
+| 0x081C (Core Register), dataLen == 2 | 2 bytes (R0 reset) |
+| 0x081C (Core Register), dataLen == 24 | 1 byte (24 x 1-byte hardware config regs) |
+| Everything else (hardware regs) | 1 byte |
+
+Do not hard-code register sizes elsewhere — always use this function or the constants from `DSPWriter.h`.
+
+## TCP Receive Buffer
+
+`dataBuffer` is 50 KB, statically allocated. The receive loop always drains `client.available()` into it before processing — this keeps the TCP receive window open. If you only drain during processing, SigmaStudio hits ZeroWindow and stalls mid-transfer.
+
+Processing is decoupled from reception: the inner loop works from buffer contents (`readIndex`/`writeIndex`) without checking `client.available()`. Large program blocks that span multiple TCP segments are handled correctly this way.
+
+When the buffer is fully consumed, all three index variables reset to 0 to prevent unbounded growth.
+
+## EEPROM Write Constraints
+
+- Page size: 64 bytes. Writes crossing a page boundary must be split.
+- Max I²C bytes per transaction: 28 (Wire buffer limit minus the 2-byte address overhead).
+- After each chunk, `i2cAckPoll()` polls for ACK up to 120 ms — this is the EEPROM's internal write cycle; do not remove this delay.
+
+## OTA Firmware Update
+
+Routes: `GET /ota` → `handleOtaPage()`, `POST /ota_do` → `handleOtaDone()` + `handleOtaStream()`.
+
+Uses the ESP32 Arduino core's built-in `Update` library (`#include <Update.h>`). The upload handler calls `Update.begin(UPDATE_SIZE_UNKNOWN)` → `Update.write()` per chunk → `Update.end(true)`. On success `handleOtaDone()` sends a 200 response then calls `ESP.restart()` after a 500 ms delay. The LED turns cyan during the flash write.
+
+The firmware binary is produced by Arduino IDE: **Sketch → Export Compiled Binary**. Upload the `.bin` file (not `.elf` or `.map`).
+
+## Fixed-Point Format
+
+ADAU1401 parameter RAM uses **5.23 fixed-point** (4 bytes per word). `DataConversion::floatToFixed()` and `::intToFixed()` convert native C types to the 5-byte safeload form `{0x00, b3, b2, b1, b0}`.
+
+## Known Stubs and Intentional Gaps
+
+- `DSPWriter::downloadProgram()` is commented out. It was intended for standalone firmware loading at boot; this use case is now handled via the EEPROM (DSP reads EEPROM autonomously via SELFBOOT pin).
+- WiFi credentials are compile-time constants — no captive portal or NVS storage.
+- Only one TCP client is served at a time; a second SigmaStudio instance cannot connect simultaneously.
+
+## Files
+
+| File | Role |
+|------|------|
+| `ModulosDSP_101.ino` | Top-level: WiFi, TCP/HTTP servers, I²C helpers, CRC32, OTA |
+| `DSPWriter.h` | I²C address constants, register enums, `DSPWriter` class declaration |
+| `DSPWriter.cpp` | `writeRegister`, `writeRegisterBlock` (returns bool), safeload with flush |
+| `DataConversion.h` / `.cpp` | 5.23 fixed-point to C type conversions |
+| `index_html.h` | Embedded HTML string for the EEPROM upload UI |
+| `ota_html.h` | Embedded HTML string for the OTA firmware update UI |

+ 31 - 17
README.md

@@ -3,7 +3,8 @@
 An Arduino sketch for the **Waveshare ESP32-S3 Zero** that provides a WiFi interface to Analog Devices ADAU1401/1701 DSP chips:
 
 - **SigmaTCP bridge** (port 8086) — lets SigmaStudio connect wirelessly to a physical DSP over I²C
-- **HTTP EEPROM uploader** (port 80) — flash DSP firmware binaries to a 24C256 EEPROM from a browser
+- **HTTP EEPROM uploader** (port 80, path `/`) — flash DSP firmware binaries to a 24C256 EEPROM from a browser
+- **HTTP OTA updater** (port 80, path `/ota`) — update the ESP32 firmware itself over WiFi
 
 This sketch runs on the companion **MSD ADAU14-1701 Adapter** PCB.
 
@@ -16,7 +17,7 @@ This sketch runs on the companion **MSD ADAU14-1701 Adapter** PCB.
 | Microcontroller | Waveshare ESP32-S3 Zero | Onboard WS2812 NeoPixel on GPIO 21 |
 | DSP | ADAU1401 / ADAU1701 | I²C address 0x68 (7-bit: 0x34) |
 | EEPROM | 24C256 (32 KB) | I²C address 0xA0 (7-bit: 0x50) |
-| Display | 20×4 hd44780 I²C LCD | Boot status and IP address |
+| Display | 20×4 hd44780 I²C LCD | Boot status and IP address (optional) |
 
 ### Pin Assignments
 
@@ -26,7 +27,7 @@ This sketch runs on the companion **MSD ADAU14-1701 Adapter** PCB.
 | I²C SCL | 12 |
 | Status LED (NeoPixel) | 21 (built-in) |
 
-The DSP, EEPROM, and LCD all share the same I²C bus at 400 kHz.
+The DSP, EEPROM, and LCD all share the same I²C bus at 400 kHz. The LCD is optional — if not detected at boot the sketch continues without it.
 
 ---
 
@@ -39,6 +40,7 @@ Install via **Sketch → Include Library → Manage Libraries**:
 | `WiFi` | ESP32 WiFi (built-in with ESP32 core) |
 | `WebServer` | HTTP server (built-in) |
 | `Wire` | I²C master (built-in) |
+| `Update` | OTA flash write (built-in with ESP32 core) |
 | `LittleFS` | Embedded filesystem for web assets (built-in) |
 | `Adafruit NeoPixel` | WS2812 status LED |
 | `hd44780` | I²C LCD driver |
@@ -71,6 +73,7 @@ The onboard NeoPixel (GPIO 21) shows current activity at a glance:
 | Blue | DSP read in progress (SigmaTCP) |
 | Yellow | EEPROM write via SigmaTCP |
 | Magenta | EEPROM upload via HTTP |
+| Cyan | OTA firmware flash in progress |
 | Red flash | Error (I²C failure, buffer overflow, bad packet) |
 
 ---
@@ -83,7 +86,7 @@ The onboard NeoPixel (GPIO 21) shows current activity at a glance:
 4. Enter the ESP32's IP address and port **8086**.
 5. Click **Link → Compile Download** — SigmaStudio pushes firmware to the DSP wirelessly.
 
-The bridge handles both **direct writes** and **safeload writes** (glitch-free atomic parameter updates on a live DSP).
+The bridge handles both **direct writes** and **safeload writes** (glitch-free atomic parameter updates on a live DSP). If the connection drops mid-safeload, any partial pending slots are flushed to parameter RAM on disconnect so the DSP's internal safeload counter is clean for the next session.
 
 ---
 
@@ -95,7 +98,18 @@ Navigate to `http://<ESP32-IP>/` in any browser:
 2. Optionally tick **Verify (CRC32)** to read back and confirm the write.
 3. Click **Upload** — the Magenta LED pulses during the write.
 
-The 24C256 holds **32 KB**. The DSP reads this EEPROM at power-up via its SELFBOOT pin, allowing standalone operation without the ESP32 actively present.
+The 24C256 holds **32 KB**. The DSP reads this EEPROM at power-up via its SELFBOOT pin, allowing standalone operation without the ESP32 actively driving it.
+
+---
+
+## Firmware Update (OTA)
+
+Navigate to `http://<ESP32-IP>/ota` in any browser:
+
+1. In Arduino IDE, export the compiled binary: **Sketch → Export Compiled Binary** (produces a `.bin` file).
+2. Click **Choose File** and select the `.bin`.
+3. Click **Flash Firmware** — the Cyan LED indicates the write is in progress.
+4. On success the device reboots automatically. The page waits 5 seconds then redirects back to `/`.
 
 ---
 
@@ -107,8 +121,9 @@ SigmaTCP is the protocol used by Analog Devices' SigmaStudio to talk to hardware
 
 **WRITE (opcode 0x09):**
 - 10-byte header: command, safeload flag, placement, totalLen, chipAddr, dataLen, startAddress
+- `totalLen` is validated against `WRITE_HDR_LEN + dataLen` before buffering — mismatched packets drop the connection
 - Payload forwarded to DSP or EEPROM via I²C
-- 4-byte ACK sent back: `{0x09, 0x00, 0x04, status}`
+- I²C errors are reported in the 4-byte ACK `{0x09, 0x00, 0x04, status}` so SigmaStudio sees real write failures
 
 **READ (opcode 0x0A):**
 - 8-byte header: command, totalLen, chipAddr, dataLen, startAddress
@@ -116,11 +131,15 @@ SigmaTCP is the protocol used by Analog Devices' SigmaStudio to talk to hardware
 - 6-byte response header (`0x0B, totalLen_hi, totalLen_lo, status, dataLen_hi, dataLen_lo`) + payload sent back
 - On I²C failure, zeros are sent rather than dropping the connection (allows SigmaStudio to probe an unresponsive DSP)
 
-**Safeload:**  
-When the safeload flag is set, parameters are queued into the DSP's 5 hardware safeload slots and committed atomically by writing `0x003C` to the Core Register. This avoids audio clicks when updating parameters on a running DSP.
+**Safeload:**
+When the safeload flag is set, parameters are queued into the DSP's 5 hardware safeload slots and committed atomically by writing `0x003C` to the Core Register. This avoids audio clicks when updating parameters on a running DSP. Any pending safeload slots are flushed at both the start and end of every TCP session to keep the DSP's internal counter in sync.
 
 The TCP receive loop drains all available bytes into a 50 KB buffer every iteration (keeping the TCP receive window open), then processes complete packets from the buffer. This correctly handles large program downloads that span multiple TCP segments.
 
+### WiFi Watchdog
+
+The sketch monitors WiFi status every 5 seconds. If the connection drops it attempts `WiFi.reconnect()` on each check. On recovery it restarts the TCP server socket and updates the LCD with the current IP address.
+
 ### EEPROM Writes
 
 Writes are chunked to ≤28 bytes, aligned to 64-byte page boundaries. After each chunk, the firmware polls the EEPROM for ACK (up to 120 ms) to wait for its internal write cycle before continuing.
@@ -140,7 +159,8 @@ ModulosDSP_101/
 ├── DSPWriter.cpp        I²C write and safeload implementations
 ├── DataConversion.h     5.23 fixed-point conversion declarations
 ├── DataConversion.cpp   Fixed-point conversion implementations
-└── index_html.h         Embedded HTML for the EEPROM upload web UI
+├── index_html.h         Embedded HTML for the EEPROM upload web UI
+└── ota_html.h           Embedded HTML for the OTA firmware update web UI
 ```
 
 ---
@@ -163,15 +183,9 @@ ModulosDSP_101/
 
 ---
 
-## Version History
+## Build / Version
 
-| Version | Notes |
-|---------|-------|
-| 1.3.1 | Current stable. `chipAddr 0x01` fix on clean v1.3.0 base |
-| 1.3.0 | Fixed 6-byte SigmaTCP read response header; added write ACK; I²C read failure returns zeros |
-| 1.2.0 | Added WS2812 NeoPixel status LED |
-| 1.1.0 | Buffer overflow checks, explicit `chipAddr` lookup, safeload dataLen validation |
-| 1.4.0 | (Changelog entry) Block-read TCP receive with per-packet timeout for large program downloads |
+The build identifier uses `YYMMDD_rev` format (e.g. `260304_13` = 4 March 2026, revision 13). It is displayed on the LCD and printed to Serial at boot. When cutting a new build, update both the file header comment and the `version` constant in `ModulosDSP_101.ino`.
 
 ---