CLAUDE.md 18 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. Sessions time out after TCP_IDLE_TIMEOUT_MS (30 s) of inactivity — lastActivityMs is reset on every received byte and checked in the idle path between commands.

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 — NVS + AP Fallback

Credentials are stored in NVS using the Preferences library (namespace "wifi", keys "ssid" and "pass"). There are no hardcoded credentials in the source.

Boot sequence:

  1. loadWifiCreds() reads from NVS.
    • If NVS has credentials, s_ssid/s_pass are populated from NVS.
    • If NVS is empty (first boot or after a reset), s_ssid/s_pass are populated from the compile-time constants DEFAULT_SSID/DEFAULT_PASS — so an unattended device reaches the network without the config portal.
  2. WiFi.begin(s_ssid, s_pass) is called with a 15-second timeout.
  3. If the connection fails (wrong credentials, network out of range), startConfigAP() is called — it never returns.

The compile-time defaults are defined near the top of ModulosDSP_101.ino:

static const char DEFAULT_SSID[] = "alfred";
static const char DEFAULT_PASS[] = "alfred16";

Change these before flashing. NVS credentials (saved via the portal) always take priority over the defaults on subsequent boots.

Config AP (startConfigAP()):

  • Starts WIFI_AP mode with SSID "ModulosDSP-Setup" (no password).
  • Default IP: 192.168.4.1.
  • httpServer serves AP_HTML (from ap_html.h) at GET /, and handles POST /save with ssid + pass form fields.
  • On save: saveWifiCreds() writes to NVS, then ESP.restart(). Never returns.
  • LCD shows: row 0 "WiFi Setup Mode", row 1 "ModulosDSP-Setup", row 2 IP address.

Resetting credentials:

POST /wifi_reset calls clearWifiCreds() then ESP.restart(). The device reboots into AP mode. The /ota page has a Reset WiFi Credentials button that calls this endpoint (with a confirm() dialog before sending).

WiFi.setAutoReconnect(true) is set before WiFi.begin(). maintainWifi() calls WiFi.reconnect() on drop, which reuses the last-configured credentials from the WiFi stack — no need to re-pass s_ssid/s_pass.

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.

GPIO Register — GET /gpio and POST /gpio

handleGpioGet() reads GpioAllRegister (0x0808, 1 byte) and MpCfg0/1 (0x0820–0x0821, 1 byte each) and returns JSON:

{"ok":true,"gpio":0,"mpcfg0":0,"mpcfg1":0}

handleGpioSet() accepts a form-encoded value parameter (0–255) and writes it to GpioAllRegister via DSPWriter::writeRegister. Returns 400 if value is missing or out of range, 200 on success.

The /ota page adds a row of four toggle buttons (GP0–GP3). Each click XORs the corresponding bit in the cached gpioVal and POSTs the new byte to /gpio. The page also polls GET /gpio every 5 seconds to stay in sync with any external state changes. Buttons render green (btn-success) when the bit is high, outline-secondary when low.

Only output-configured pins respond to writes — which pins are outputs is determined by MpCfg registers (read-only in the UI, visible in the JSON response).

DSP Soft Reset — POST /dsp_reset

handleDspReset() writes {0x00, 0x00} (stop) to the Core Register, waits 100 ms, then writes {0x00, 0x01} (run). Parameter and program RAM are preserved — the DSP restarts execution without reloading from EEPROM. Returns 200 "DSP soft reset complete." on success.

DSP Status — GET /dsp_status

handleDspStatus() reads three register regions via I²C and returns a JSON object:

{"ok":true,"running":true,"coreReg":"0x0001","gpio":"0x00","adc":["0x00","0x00","0x00","0x00"]}
Field Source Notes
ok AND of all three read results false if any I²C read failed
running Core Register bit 0 true when DSP is executing
coreReg 0x081C (2 bytes) Full register value as hex string
gpio 0x0808 (1 byte) GPIO All Register
adc[0–3] 0x0809–0x080C (4-byte burst) ADC input values

HTTP status is 200 on success, 500 if any I²C read failed. The /ota page polls this endpoint every 2 seconds via XHR (pollStatus()) and displays a live status card at the top of the page.

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 stored in NVS; the config portal (startConfigAP()) handles first-boot and failed-connect scenarios.
  • 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 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. OpenADAU1701_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 .inoof 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 flagsafeload=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)