SoundMat (7) Firmware Design

Firmware here means software on the ESP32 MCU. It bridges sensor/electronics and sonification: raw sensor readings go to the host computer for music generation; LED commands from the host drive the strip on SoundMat.

Firmware architecture splits into four modules:

loop() {
  scan_sensors();      // Sensor scan module
  send_sensor_data();  // Sensor data transmit module
  receive_led_data();  // LED data receive module
  update_leds();       // LED refresh module
}
Initialization (pins, ADC, Serial, LEDs)

Main loop:
  1. Scan sensor array → pressure_matrix[ring][slice]
  2. Send pressure_matrix to host
  3. Receive LED state from host
  4. Update LED display

Sensor scan module

Detection principles are in SoundMat (5) Sensor Principles and Design. To read coordinate (ring, slice), drive the active ring HIGH and all others LOW, configure the MUX to route that slice, then read slice-side voltage.

Decisions:

  1. ADC can be averaged over multiple samples for noise reduction; for now we use a single sample per cell and results are acceptable.

Pseudocode:

scan_sensors() {
    // Clear pressure matrix; -1 = not sampled
    for all [ring][slice] {
        pressure[ring][slice] = -1
	}

    for ring in 0..NUM_RINGS-1 {
        // Activate current ring, others LOW (prevent ghost paths)
        set_all_rings(LOW)
        set_ring(ring, HIGH)
        delay_ms(RING_SETTLE_MS)  // Wait for ring line to settle

        for slice in 0..NUM_SLICES-1 {
            // Set MUX address lines for current slice channel
            mux_select(slice)
            delay_ms(MUX_SETTLE_MS)  // Wait for MUX to settle

            // Read ADC
            raw = adc_read(SIG_PIN)
			pressure[ring][slice] = raw
		}
        // Done with this ring; drive LOW before next ring
        set_ring(ring, LOW)
	}
	return pressure
}

We use CD4067B; enable is active low.

Serial data module

The ESP32 sends raw ADC values to the host (no calibration on-device). The host sends per-LED control data.

Basic settings

ItemSetting
Baud rate921600
EncodingASCII text

Frame format

All frames share:

TYPE:PAYLOAD*XX\n
FieldDescription
TYPESingle-character frame type
:Separator
PAYLOADPayload; format depends on type
*Checksum separator
XX2-digit hex checksum
\nFrame terminator

Checksum: XOR all bytes from TYPE through the character before * (inclusive of TYPE and :). Encode result as 2 uppercase hex digits.

Frame types

DirectionHeaderPurpose
ESP32 → PiSSensor data
Pi → ESP32LLED control
Both#Comment/debug (receiver ignores)

S frame: sensor data (ESP32 → Pi)

S:v0,v1,v2,...,v255*XX\n

256 decimal integers, comma-separated, range -1–4095 (12-bit ADC raw; -1 = invalid). Ring-major order:

index = ring × 32 + slice

Indices 0–31 are Ring 0 slices 0–31; 32–63 Ring 1; and so on. Inner rings 0–1 have only 16 physical zones but still send 32 values; merging is done on the Pi.

Frame size: ~1.2 KB (≈3–4 chars per value + commas + header/footer).

Example:

S:4095,4093,4090,2031,...,3500,3498*A7\n

L frame: LED control (Pi → ESP32)

L:RRGGBB,RRGGBB,...,RRGGBB*XX\n

108 six-digit hex RGB values, comma-separated, in physical strip order (index 0 = first LED).

Frame size: ~760 bytes.

Example:

L:000000,0A0500,1A0F00,...,000000*3E\n

Value range 0–4095 (ESP32 12-bit ADC raw). Inner rings 0–1 have 16 effective zones but still send 32 values (adjacent slices may cover the same physical region); merge logic stays on the Pi so firmware stays simple.

# frame: debug (bidirectional)

# any text\n

Lines starting with # have no checksum; discard or log. On boot the ESP32 may send:

# SoundMat firmware v1.0 ready\n

Timing

Both sides send asynchronously without blocking.

After each scan the ESP32 sends an S frame without waiting for the Pi. The Pi computes audio/LED asynchronously and sends L frames when ready. Each loop iteration the ESP32 non-blocking checks Serial; a complete L frame updates the LED buffer, otherwise the previous frame persists.

Right after USB connect, during ESP32 boot, LED commands are ineffective—wait until the link is stable before relying on traffic.

Claude:

Robustness and engineering constraints

Buffer sizing: ESP32 Serial RX buffer must be 2048 bytes (default 256 cannot hold a full ~760-byte L frame). Pi pyserial default RX is sufficient.

Partial frames: Read strictly by line. Process only after \n; leave incomplete data in the buffer for the next loop.

Checksum failures: Drop the frame; no retry. For ambient music, losing one frame is imperceptible; retries add latency and complexity—not worth it for this prototype.

Frame-rate budget: 921600 baud ≈ 92 KB/s. Full duplex ~2 KB/frame → theoretical ~46 fps. With scan + processing overhead, expect 20–30 fps, ample for responsive ambient music.

LED refresh module

Write RGB values from the LED buffer to the WS2812B strip.

Driver: WS2812B needs an 800 kHz one-wire protocol. We use ESP-IDF led_strip.h, which uses the RMT peripheral for timing.

ESP32 program layout

firmware/
├── main/
│   ├── main.c
│   ├── config.h
│   ├── sensor.h / sensor.c
│   ├── serial_comm.h / serial_comm.c
│   └── led_ctrl.h / led_ctrl.c
// ========== Variables ==========
pressure_matrix[NUM_RINGS][NUM_SLICES]
led_buffer[NUM_LEDS]

app_main():
	// ========== Initialization ==========
	serial_init()
	uart_write("# SoundMat firmware v1.0 booting\n")
	sensor_init()
	led_init()
	uart_write("# init complete, entering main loop\n")
	
	while true:
		sensor_scan()
		serial_send_sensor_data(pressure_matrix)
		serial_try_receive_led_data(led_buffer) // non-blocking
		led_update()
		vTaskDelay(pdMS_TO_TICKS(LOOP_DELAY_MS)) // feed watchdog
	

Constant table

ConstantValueMeaning
NUM_RINGS8Ring electrode count
NUM_SLICES32Slice electrode count
NUM_LEDS108LED count
ADC_SAMPLES1Samples per point for averaging; 1 = single sample
ADC_BIT12ADC resolution (bits)
UART_BAUD921600Serial baud rate
UART_RX_BUF_SIZE2048RX buffer; must fit full L frame
UART_TX_BUF_SIZE0TX buffer; blocking send for complete frames
RING_SETTLE_US20Delay after ring select (µs)
MUX_SETTLE_US50Delay after MUX address change (µs)
LOOP_DELAY_MS1Min delay at end of main loop; watchdog + yield CPU
LED_COLOR_ORDERGRBWS2812B color order

Latency analysis

Pin map

Pin assignments match SoundMat (11) Electronics Manufacturing.

Rings

RingMCU IO
Ring0D19
Ring1D5
Ring2D21
Ring3TX2
Ring4D22
Ring5D4
Ring6D23
Ring7D2

MUX

SliceMUX PINS
Slice 0-15MUX 0, C0-C15
Slice 16-31MUX 1, C0-C15
MUX PINSMCU IOTYPE
S0D26OUTPUT
S1D25OUTPUT
S2D33OUTPUT
S3D32OUTPUT
EN0D14OUTPUT
EN1D27OUTPUT
SIGVPINPUT

LED

PINSMCU IO
LED DIND13