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/xstart/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 spsFirmware
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));(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;
}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.
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 DFUpip3 install pyserial matplotlib
python3 host/plot_mic.py # auto-detect port
python3 host/plot_mic.py /dev/tty.usbmodemXXXX # explicit port
