BERGSONNE

Sense.MIC Audio Stream

Stream 12-bit ADC samples from a MEMS-microphone tile over USB at roughly 16 ksps, with a sync-framed binary protocol and a live matplotlib waveform — high-speed tile acquisition end-to-end.

Overview

The firmware targets a Core.ST.L4.1 at the maximum clock and drives two tiles: the Sense.MIC on I2C3 at 1 MHz, and a Display.RGBW on I2C1 at 400 kHz for status. Sense.MIC pairs a MAX11645 12-bit I²C ADC with an AMM-2742 MEMS microphone; the firmware reads it one sample at a time over the bus.

  • Firmware (main.c) — sync-framed binary packets, RGBW status LED, s/x start/stop control.
  • Host (host/plot_mic.py) — a real-time waveform with a threaded serial reader and auto port detection.

Binary protocol

The firmware sends two kinds of data over USB CDC: binary sample packets and ASCII status lines. Each binary packet carries 30 samples in 62 bytes — two sync bytes followed by 30 little-endian uint16 samples.

#define SYNC_0            0xAA
#define SYNC_1            0x55
#define SAMPLES_PER_PKT   30
#define PKT_SIZE          (2 + SAMPLES_PER_PKT * 2)  /* 62 bytes */

Why sync bytes?

USB CDC reads don’t align to packet boundaries — one read() might return the tail of one packet and the head of the next. The 0xAA 0x55 header lets the host re-synchronize by scanning for it before parsing samples.

Text messages

Status lines are ASCII prefixed with #, so the host can route them separately — the first byte of a binary packet is always 0xAA, never #:

# dc_offset=712
# ready (send 's' to stream, 'x' to stop)
# rate=16021 sps

Firmware

Tile init with retries

The RGBW LED comes up first on I2C1. On power-up the ADC may not respond immediately, so the firmware probes tile_sense_mic_find() up to ten times with 100 ms delays, then initializes and checks tile_is_ready(). It reports the auto-calibrated DC offset (averaged over 64 samples during init) on success.

/* Find and init Sense.MIC on I2C3 with retries */
uint8_t mic_found = 0;
for (int attempt = 0; attempt < 10; attempt++) {
    if (tile_sense_mic_find(core_tiles_pal(&core_i2c3), 0)) {
        mic_found = 1;
        break;
    }
    core_delay_ms(100);
}

if (!mic_found) {
    tile_display_rgbw_set(&led, 24, 0, 0, 0);  /* Red */
    core_usb_printf("# error: Sense.MIC not found\r\n");
    while (1) core_delay_ms(1000);
}

tile_sense_mic_init(core_tiles_pal(&core_i2c3), 0, &mic, NULL);
if (!tile_is_ready(&mic)) {
    tile_display_rgbw_set(&led, 24, 12, 0, 0);  /* Yellow */
    core_usb_printf("# error: Sense.MIC init failed\r\n");
    while (1) core_delay_ms(1000);
}

/* Report DC offset from calibration */
core_usb_printf("# dc_offset=%u\r\n", tile_sense_mic_get_dc_offset(&mic));
RGBW status states
The LED gives feedback with no serial connection — arguments are (R, G, B, W):Red not found · Yellow init failed · Blue idle, waiting for s · Green streaming.

Sample acquisition

While streaming, the firmware fills a 62-byte packet with 30 samples read via tile_sense_mic_get_raw(), writes it over USB, and reports the measured rate once per second:

/* Fill packet with samples */
for (int i = 0; i < SAMPLES_PER_PKT; i++) {
    uint16_t raw = tile_sense_mic_get_raw(&mic);
    pkt[2 + i * 2]     = (uint8_t)(raw & 0xFF);        /* low byte */
    pkt[2 + i * 2 + 1] = (uint8_t)((raw >> 8) & 0xFF); /* high byte */
    sample_count++;
}

/* Send packet */
core_usb_write(pkt, PKT_SIZE);

/* Report sample rate every ~1 second */
uint32_t now = core_millis();
uint32_t elapsed = now - last_rate_tick;
if (elapsed >= 1000) {
    uint32_t delta_samples = sample_count - last_rate_count;
    uint32_t rate = (delta_samples * 1000) / elapsed;
    core_usb_printf("# rate=%lu sps\r\n", rate);
    last_rate_tick = now;
    last_rate_count = sample_count;
}
~16 ksps over I²C
Each sample is a single I²C transaction against the MAX11645. At the 1 MHz bus speed configured here, back-to-back reads yield about 16,000 samples per second (~12.5 ksps at 400 kHz) — comfortably above the voice band. The firmware prints the live measured rate so you can confirm it on your hardware.

The host controls streaming with single bytes s/x via a core_usb_on_receive() callback that runs in the USB ISR and only sets a flag — the same pattern as the USB streaming example.

Python plotter

host/plot_mic.py opens the port, sends s, and shows a live scrolling waveform with matplotlib. A daemon thread does all serial reading and parsing so the plot stays responsive — it peels off # text lines, scans for the 0xAA 0x55 sync, and extracts 30 samples per packet:

# Look for sync header
idx = buf.find(SYNC)
if idx < 0:
    buf = buf[-1:]          # no sync — keep last byte (maybe partial)
    continue
if idx > 0:
    buf = buf[idx:]         # discard bytes before sync

if len(buf) < PKT_SIZE:
    break                   # wait for the rest of the packet

pkt = buf[:PKT_SIZE]
buf = buf[PKT_SIZE:]

for i in range(SAMPLES_PER_PKT):
    lo = pkt[2 + i * 2]
    hi = pkt[2 + i * 2 + 1]
    sample = lo | (hi << 8)
    self.samples.append(sample)

FuncAnimation redraws at ~30 fps over a rolling deque of 10,000 samples; the y-axis is pinned to a practical 0–1500 ADC-count window. The title bar shows the firmware-reported rate, the host-measured rate, and the packet count. Ctrl+C or closing the window sends x and closes the port.

Dependencies & port
Needs pyserial and matplotlib. Port auto-detection scans /dev/tty.usbmodem*; with several devices it uses the first match, or pass a port as an argument.

config.json

The descriptor wires up both buses and declares the tiles on them, so coregen generates the handles and bus init. I2C3 runs at 1 MHz — the speed behind the ~16 ksps — and I2C1 at 400 kHz for the status LED.

{
  "core": "Core.ST.L4.1",
  "clock": "max",
  "pads": {
    "4": "I2C1.CLK",
    "5": "I2C1.DAT",
    "2": "I2C3.CLK",
    "8": "I2C3.DAT"
  },
  "interfaces": {
    "I2C1": { "speed": 400000 },
    "I2C3": { "speed": 1000000 }
  },
  "tiles": [
    { "tile": "Display.RGBW", "bus": "I2C1", "instance": 0 },
    { "tile": "Sense.MIC", "bus": "I2C3", "instance": 0 }
  ]
}

Build & run

The Makefile sets BOOTLOADER := 1, so flash over DFU — the target performs the 1200-baud touch to enter the bootloader automatically:

cd examples/u1-sense-mic-usb-stream
make              # build (BOOTLOADER=1)
make flash-dfu    # flash over DFU
pip3 install pyserial matplotlib
python3 host/plot_mic.py                          # auto-detect port
python3 host/plot_mic.py /dev/tty.usbmodemXXXX    # explicit port