CLAUDE.md 9.4 KB

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:

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 /otahandleOtaPage(), POST /ota_dohandleOtaDone() + 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