Skip to content

Commit eafe4cf

Browse files
committed
app: PAGE_AUDIO live mic meter via capture thread; ES8311 mic capture HW-verified (#6)
ES8311 ADC + on-board MEMS mic capture is HW-verified (HW-016d): a loud external sound drives mic -> ES8311 ADC -> I2S0 RX -> DSP from the noise floor to full scale. The long-standing "capture silent" was a weak-on-board-beep stimulus confound, not a mic/ADC fault. PAGE_AUDIO is now a live on-screen mic meter. A dedicated capture thread streams one continuous full-duplex session while the page is up and publishes the per-window peak RMS; the UI thread only reads the level and never touches I2S. An inline per-render capture corrupted the next SPI write and wedged I2S on hardware (HW-016e), so capture runs in its own thread, gated to the page and enabled only after the page is painted; bar and RMS read once, shown on separate lines. HW-verified: live bar tracks speech ([####] rms=20680), clean header, device responsive (no freeze observed). native_sim es8311 11/11 + audio_dsp 6/6; demo and default blob-free builds green. Signed-off-by: Hsiu-Chi Tsai <hctsai@linux.com>
1 parent 4fa40be commit eafe4cf

16 files changed

Lines changed: 541 additions & 79 deletions

app/src/audio.c

Lines changed: 114 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,15 @@ K_MEM_SLAB_DEFINE_STATIC(rx_slab, BLOCK_SIZE, BLOCK_COUNT, 32);
8282
#define LOOP_IO_TIMEOUT_MS 200
8383

8484
/*
85-
* Acoustic-loopback capture (issue #6). The ES8311 ADC is mono and lands on one
86-
* I2S slot, so AUDIO_MIC_SLOT (a slot INDEX 0 or 1, not a channel count) picks
87-
* it - confirm which slot carries the ADC at HW-016. AUDIO_MIC_FULL is an
88-
* empirical full-scale RMS for the PAGE_AUDIO bar, tuned at HW-016 once a real
89-
* mic level is known.
85+
* Mic capture (issue #6). The ES8311 ADC is mono and lands on one I2S slot, so
86+
* AUDIO_MIC_SLOT (a slot INDEX 0 or 1, not a channel count) picks it; HW-016d
87+
* confirmed slot 0 carries the ADC. AUDIO_MIC_FULL is the empirical full-scale
88+
* RMS for the PAGE_AUDIO live bar: HW-016d measured quiet ~70-100, a normal
89+
* voice a few hundred to a few thousand, and a loud clap >= 12000, so 2000 gives
90+
* visible bars for speech without pinning the bar to 4 on every breath.
9091
*/
9192
#define AUDIO_MIC_SLOT 0U
92-
#define AUDIO_MIC_FULL 8000U
93+
#define AUDIO_MIC_FULL 2000U
9394
#define LOOP_SIL_BLOCKS 4U
9495
#define LOOP_BEEP_BLOCKS 12U
9596

@@ -101,7 +102,10 @@ static const struct device *const i2s_dev = DEVICE_DT_GET(DT_NODELABEL(i2s0));
101102
static const struct gpio_dt_spec amp_gpio =
102103
GPIO_DT_SPEC_GET(DT_NODELABEL(sound_amp), amp_gpios);
103104

104-
static bool ready;
105+
/* Written by audio_init() on the main thread, read by the capture thread too, so
106+
* volatile (a plain bool could be cached in the capture loop and never seen set).
107+
*/
108+
static volatile bool ready;
105109

106110
/*
107111
* Precomputed one-block 440 Hz mono sine, 16-bit signed, sampled at 16 kHz
@@ -348,11 +352,16 @@ void audio_beep(void)
348352
audio_codec_stop_output(codec_dev);
349353
}
350354

351-
/* Capture scratch + the latest loopback peak RMS (single-threaded use). */
355+
/* Capture scratch + the latest mic level. The capture thread (below) is the only
356+
* active user of these on hardware (audio_loopback() is a dormant bring-up
357+
* primitive); mic_rms_peak is WRITTEN by that thread and READ by the UI thread
358+
* via audio_mic_level()/_bars(), so it is volatile (a 16-bit aligned access is
359+
* atomic on this unicore SoC).
360+
*/
352361
static int16_t zero_block[BLOCK_FRAMES * AUDIO_CHANNELS];
353362
static int16_t rx_buf[BLOCK_FRAMES * AUDIO_CHANNELS];
354363
static int16_t mono_buf[BLOCK_FRAMES];
355-
static uint16_t mic_rms_peak;
364+
static volatile uint16_t mic_rms_peak;
356365

357366
BUILD_ASSERT(sizeof(rx_buf) == BLOCK_SIZE, "rx_buf must hold exactly one I2S block");
358367

@@ -516,14 +525,108 @@ void audio_loopback(void)
516525
audio_codec_stop_output(codec_dev);
517526
}
518527

528+
/*
529+
* PAGE_AUDIO live mic meter -- a dedicated CONTINUOUS-capture thread (issue #6).
530+
*
531+
* While capture_on is set (the AUDIO page is up), run ONE full-duplex session and
532+
* stream the ADC, updating mic_rms_peak with the peak RMS of each ~128 ms window
533+
* so audio_mic_bars()/audio_mic_level() track sound LIVE and fall when quiet.
534+
* This is the HW-016d-proven continuous-capture shape (one START, steady reads,
535+
* one DROP) -- NOT a per-render start/stop, which corrupted the very next SPI
536+
* display write and wedged I2S on hardware (HW-016e). The UI thread only READS
537+
* mic_rms_peak and NEVER touches I2S, so redrawing the meter can't break the
538+
* display from the UI side. Gated to the page (one START on enter, one DROP on
539+
* leave -- bounded churn, like the HW-013-proven audio_loopback). Any bounded-I/O
540+
* failure breaks to a DROP-only stop (skips DRAIN) so a wedged clock can't hang.
541+
*/
542+
#define CAPTURE_STACK_SIZE 3072
543+
#define CAPTURE_PRIORITY 7
544+
#define CAPTURE_WINDOW_BLOCKS 8U /* publish the level every ~128 ms */
545+
546+
static volatile bool capture_on;
547+
548+
void audio_capture_set(bool on)
549+
{
550+
capture_on = on;
551+
}
552+
553+
static void audio_capture_thread(void *p1, void *p2, void *p3)
554+
{
555+
ARG_UNUSED(p1);
556+
ARG_UNUSED(p2);
557+
ARG_UNUSED(p3);
558+
559+
for (;;) {
560+
uint16_t wpeak = 0U;
561+
uint32_t wc = 0U;
562+
563+
if (!ready || !capture_on) {
564+
mic_rms_peak = 0U;
565+
k_msleep(50);
566+
continue;
567+
}
568+
569+
/* Start ONE full-duplex session (amp stays OFF for capture). */
570+
(void)i2s_trigger(i2s_dev, I2S_DIR_BOTH, I2S_TRIGGER_DROP);
571+
audio_codec_start_output(codec_dev);
572+
if (loop_tx(zero_block) < 0 || loop_tx(zero_block) < 0 ||
573+
i2s_trigger(i2s_dev, I2S_DIR_BOTH, I2S_TRIGGER_START) < 0) {
574+
(void)i2s_trigger(i2s_dev, I2S_DIR_BOTH, I2S_TRIGGER_DROP);
575+
audio_codec_stop_output(codec_dev);
576+
k_msleep(50);
577+
continue;
578+
}
579+
580+
/* Stream while still on the page. TX must keep feeding silence or
581+
* the shared clock stops and RX stalls.
582+
*/
583+
while (ready && capture_on) {
584+
size_t size = sizeof(rx_buf);
585+
size_t frames;
586+
uint16_t rms;
587+
588+
if (loop_tx(zero_block) < 0) {
589+
break;
590+
}
591+
if (i2s_buf_read(i2s_dev, rx_buf, &size) < 0) {
592+
break;
593+
}
594+
if (size > sizeof(rx_buf)) {
595+
size = sizeof(rx_buf);
596+
}
597+
frames = size / AUDIO_FRAME_BYTES;
598+
audio_deinterleave(rx_buf, frames, AUDIO_MIC_SLOT, mono_buf);
599+
rms = audio_rms_i16(mono_buf, frames);
600+
if (rms > wpeak) {
601+
wpeak = rms;
602+
}
603+
if (++wc >= CAPTURE_WINDOW_BLOCKS) {
604+
mic_rms_peak = wpeak;
605+
wpeak = 0U;
606+
wc = 0U;
607+
}
608+
}
609+
610+
/* Stop: DROP only (skip DRAIN so a wedged clock can't hang), mute
611+
* the DAC, and clear the level for the next visit.
612+
*/
613+
(void)i2s_trigger(i2s_dev, I2S_DIR_BOTH, I2S_TRIGGER_DROP);
614+
audio_codec_stop_output(codec_dev);
615+
mic_rms_peak = 0U;
616+
}
617+
}
618+
619+
K_THREAD_DEFINE(audio_capture_tid, CAPTURE_STACK_SIZE, audio_capture_thread,
620+
NULL, NULL, NULL, CAPTURE_PRIORITY, 0, 0);
621+
519622
uint16_t audio_mic_level(void)
520623
{
521624
return mic_rms_peak;
522625
}
523626

524-
uint8_t audio_mic_bars(void)
627+
uint8_t audio_mic_bars(uint16_t level)
525628
{
526-
return audio_level_bars(mic_rms_peak, AUDIO_MIC_FULL, 4U);
629+
return audio_level_bars(level, AUDIO_MIC_FULL, 4U);
527630
}
528631

529632
#endif /* CONFIG_APP_AUDIO */

app/src/audio.h

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,30 +28,42 @@ bool audio_ready(void);
2828

2929
/*
3030
* Play a short 440 Hz tone on the speaker. The amplifier is enabled only for
31-
* the duration of the tone (anti-pop) and disabled again afterwards.
32-
*
33-
* The PAGE_AUDIO entry now runs audio_loopback() instead; audio_beep() is kept
34-
* as the standalone, HW-006-verified playback primitive (and the eventual
35-
* codec-sample reference) and is exercised by tests / future callers.
31+
* the duration of the tone (anti-pop) and disabled again afterwards. The
32+
* PAGE_AUDIO entry plays one beep as a speaker self-check; HW-006-verified.
3633
*/
3734
void audio_beep(void);
3835

3936
/*
40-
* Acoustic loopback self-test (issue #6): play the 440 Hz tone on the speaker
41-
* while capturing the on-board mic over I2S, printing per-block RMS/peak on
42-
* serial (the RMS rises during the beep vs the surrounding silence, so the mic
43-
* is verified to hear the speaker). Blocking, ~0.3 s; the amp is on only for the
44-
* beep phase.
37+
* Acoustic loopback self-test (issue #6, bring-up primitive; not auto-run by the
38+
* UI): play the 440 Hz tone while capturing the on-board mic over I2S full-duplex,
39+
* printing per-block RMS/peak on serial. NOTE: at the codec's low playback volume
40+
* the tiny on-board speaker is too weak to couple acoustically to the adjacent
41+
* mic, so the beep phase reads ~0 - this is the weak-stimulus confound that
42+
* HW-016d resolved, NOT a mic fault (a loud external sound drives the same path
43+
* to full scale; see the live mic capture thread, audio_capture_set()). Blocking,
44+
* ~0.3 s; amp on only for the beep phase.
4545
*/
4646
void audio_loopback(void);
4747

4848
/*
49-
* Peak per-block mic RMS (0..32767) from the last audio_loopback() run (0 before
50-
* the first run); audio_mic_bars() maps it to a 0..4 level for the PAGE_AUDIO
51-
* meter.
49+
* Enable/disable the live mic-capture thread (issue #6). Set true while the
50+
* PAGE_AUDIO page is up: a dedicated thread then runs ONE continuous full-duplex
51+
* capture session and updates the level reported by audio_mic_level()/_bars()
52+
* ~8x/sec, so the meter tracks sound live. Gated to the page (one start on enter,
53+
* one stop on leave) so the I2S/codec only run while in use. The UI thread itself
54+
* never touches I2S -- it only reads the level -- which is what keeps the SPI
55+
* display stable (a per-render capture in the UI thread corrupted it; HW-016e).
56+
*/
57+
void audio_capture_set(bool on);
58+
59+
/*
60+
* The latest mic RMS (0..32767) from the capture thread (0 when the page is not
61+
* up / capture is off). audio_mic_bars() maps a GIVEN level to a 0..4 bar -- pass
62+
* one audio_mic_level() read to both so the bar and the printed RMS always agree
63+
* (the capture thread updates the level asynchronously).
5264
*/
5365
uint16_t audio_mic_level(void);
54-
uint8_t audio_mic_bars(void);
66+
uint8_t audio_mic_bars(uint16_t level);
5567

5668
#endif /* CONFIG_APP_AUDIO */
5769

app/src/main.c

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,27 @@ int main(void)
127127
struct app_status st;
128128
enum app_page page = app_page_get();
129129

130+
#ifdef CONFIG_APP_AUDIO
131+
/*
132+
* Stop capture BEFORE painting a non-AUDIO page (the start is
133+
* deferred until AFTER the AUDIO page is painted, below) so a page's
134+
* first paint is never concurrent with the I2S session start/stop,
135+
* which corrupts the in-flight SPI display write (HW-016e).
136+
* Steady-state body redraws while the stream already runs are fine.
137+
*/
138+
if (page != PAGE_AUDIO) {
139+
audio_capture_set(false);
140+
}
141+
#endif
142+
130143
status_sample(&st);
131144
ui_render(page, &st);
145+
146+
#ifdef CONFIG_APP_AUDIO
147+
if (page == PAGE_AUDIO) {
148+
audio_capture_set(true);
149+
}
150+
#endif
132151
#ifdef CONFIG_APP_BLE
133152
ble_update(&st);
134153
#endif
@@ -229,8 +248,17 @@ int main(void)
229248
st.accel[1].val1, abs(st.accel[1].val2),
230249
st.accel[2].val1, abs(st.accel[2].val2));
231250

232-
/* Wake early on a button press; otherwise tick every LOOP_MS. */
233-
k_sem_take(&nav_sem, K_MSEC(LOOP_MS));
251+
/*
252+
* Wake early on a button press; otherwise tick every LOOP_MS, but
253+
* faster on the AUDIO page so the live mic meter updates smoothly.
254+
*/
255+
uint32_t tick_ms = LOOP_MS;
256+
#ifdef CONFIG_APP_AUDIO
257+
if (page == PAGE_AUDIO) {
258+
tick_ms = 250;
259+
}
260+
#endif
261+
k_sem_take(&nav_sem, K_MSEC(tick_ms));
234262
}
235263

236264
return 0;

app/src/ui.c

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -208,15 +208,32 @@ static void render_ble_body(const struct app_status *s)
208208
static void render_audio_body(const struct app_status *s)
209209
{
210210
char line[24];
211+
char meter[6];
212+
uint16_t lvl;
213+
uint8_t bars;
211214

212215
ARG_UNUSED(s);
213216

214-
gfx_draw_text(MARGIN_X, body_line_y(0), HOME_FG, HOME_BG, "audio loopback");
215-
gfx_draw_text(MARGIN_X, body_line_y(1), HOME_FG, HOME_BG,
216-
audio_ready() ? "enter=beep+mic" : "init failed");
217-
/* Mic level from the last loopback run (bar 0..4 + raw RMS). */
218-
snprintf(line, sizeof(line), "mic bar=%u rms=%u ", audio_mic_bars(),
219-
audio_mic_level());
217+
/* Read the level ONCE so the bar and the printed RMS are always from the
218+
* same sample (the capture thread updates mic_rms_peak asynchronously).
219+
*/
220+
lvl = audio_mic_level();
221+
bars = audio_mic_bars(lvl);
222+
for (uint8_t i = 0; i < 4U; i++) {
223+
meter[i] = (i < bars) ? '#' : '.';
224+
}
225+
meter[4] = '\0';
226+
227+
gfx_draw_text(MARGIN_X, body_line_y(0), HOME_FG, HOME_BG,
228+
audio_ready() ? "speak now" : "init failed");
229+
/*
230+
* Bar and RMS on SEPARATE lines: the 10px font fits only ~13 chars on the
231+
* 135px panel, so "[####] rms=NNNNN" clips the RMS to ~2 digits. The fixed
232+
* %-6u field clears stale digits as the level swings 1..32767.
233+
*/
234+
snprintf(line, sizeof(line), "[%s]", meter);
235+
gfx_draw_text(MARGIN_X, body_line_y(1), HOME_FG, HOME_BG, line);
236+
snprintf(line, sizeof(line), "rms=%-6u", lvl);
220237
gfx_draw_text(MARGIN_X, body_line_y(2), HOME_FG, HOME_BG, line);
221238
}
222239
#endif /* CONFIG_APP_AUDIO */
@@ -369,20 +386,15 @@ void ui_render(enum app_page page, const struct app_status *s)
369386
if (page_changed) {
370387
gfx_clear(HOME_BG);
371388
draw_header(page);
372-
render_audio_body(s);
373-
/*
374-
* Run the acoustic loopback on ENTER only (page-change
375-
* edge), so the nav keys KEY1/KEY2 are not hijacked. It is
376-
* blocking (~0.3 s: silence, then the 440 Hz beep with the
377-
* amp on, then silence) and prints per-block mic RMS on
378-
* serial - the RMS spikes during the beep (mic hears speaker).
379-
*/
380-
if (audio_ready()) {
381-
audio_loopback();
382-
}
383-
} else {
384-
render_audio_body(s);
385389
}
390+
/*
391+
* Live mic meter. The capture thread (enabled for this page from
392+
* main.c via audio_capture_set) streams the ADC and updates the
393+
* level; the UI only READS it here and never touches I2S, so
394+
* redrawing the bar cannot disturb the SPI display (HW-016e). The
395+
* main loop ticks faster on this page so the bar updates smoothly.
396+
*/
397+
render_audio_body(s);
386398
break;
387399
#endif
388400
#ifdef CONFIG_APP_BLE

docs/00_RESEARCH_SNAPSHOT_UPDATED.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ All pins used in this repo's DTS were confirmed against the official PinMap:
3131
| IMU BMI270 | I2C 0x68, SCL G48, SDA G47 | confirmed; **runtime-verified ready** |
3232
| PMIC M5PM1 | I2C 0x6e, SCL G48, SDA G47 | confirmed |
3333
| Audio ES8311 | I2C 0x18; I2S MCLK G18, DOUT G14, BCLK G17, LRCK G15, DIN G16 | confirmed |
34-
| Mic (analog MEMS) | MSM381A3729H9BPC (U21) into ES8311 MIC1P/MIC1N (analog ADC input) | confirmed (schematic V0.6); capture not yet HW-verified, issue #6 / HW-016 |
34+
| Mic (analog MEMS) | MSM381A3729H9BPC (U21) into ES8311 MIC1P/MIC1N (analog ADC input) | confirmed (schematic V0.6); capture HW-verified (HW-016d, issue #6) |
3535
| Speaker amp | AW8737, gated by M5PM1 PYG3 (`sound_amp`) | confirmed; HW-006 |
3636
| Buttons | KEY1 G11, KEY2 G12 | confirmed |
3737
| IR | TX G46, RX G42 | confirmed |

0 commit comments

Comments
 (0)