Skip to content

Latest commit

 

History

History
 
 

README.md

Testing: STM32F405 / STM32F407 DAC AudioOut

These tests exercise the DAC-based audioio.AudioOut implementation added for STM32F405xx and STM32F407xx.

Automated vs Manual Tests

Test Automated Requires audio/scope
1 — WAV File Playback Yes Yes (audio check)
2 — Pause / Resume Yes Yes (audio check)
3 — Looping Sine Wave Yes Yes (audio check)
4 — deinit and Re-init Yes No
5 — Stereo Playback Yes Yes (audio check)

run_serial_tests.py automates Tests 1–5: it copies the necessary files to the board and runs each script over the serial REPL, comparing the printed output to the expected patterns. You still need to listen to the audio (and optionally use an oscilloscope) for the audio-quality checks.

Quick start

# Install the one dependency (if not already present)
pip install mpremote

# Run all automated tests (board must be connected and CIRCUITPY mounted)
python3 tests/circuitpython-manual/audioio/run_serial_tests.py

# Skip file copy if files are already on the board
python3 tests/circuitpython-manual/audioio/run_serial_tests.py --no-copy

# Override the serial port or CIRCUITPY path (auto-detected on macOS/Linux/Windows)
python3 tests/circuitpython-manual/audioio/run_serial_tests.py \
    --port /dev/cu.usbmodem1234 \
    --circuitpy /Volumes/CIRCUITPY          # macOS (auto-detected)
python3 tests/circuitpython-manual/audioio/run_serial_tests.py \
    --port /dev/ttyACM0 \
    --circuitpy /media/user/CIRCUITPY       # Linux (auto-detected)
python3 tests/circuitpython-manual/audioio/run_serial_tests.py \
    --port COM5 \
    --circuitpy D:\                         # Windows (auto-detected)

# Run only specific tests
python3 tests/circuitpython-manual/audioio/run_serial_tests.py --tests 3,4,5

The script exits 0 if all selected tests pass, 1 otherwise — suitable for CI.


Hardware Required

  • An STM32F405 or STM32F407 board running the freshly-built firmware (e.g. Feather STM32F405 Express).
  • A passive speaker or audio amplifier connected to pin A0 (PA04, DAC channel 1) and GND.
    • A simple test: 100 Ω resistor in series with a small speaker between A0 and GND.
    • For better audio: connect A0 to a small amp module (e.g. PAM8403) then to a speaker.
  • Optional: an oscilloscope or logic analyser probe on D4 (used as a trigger output by the test scripts).
  • A USB cable for REPL/filesystem access (CircuitPython storage must not be read-only).

Build

Enable the feature by building for an F405 or F407 target. CIRCUITPY_AUDIOIO is set to 1 automatically for those variants.

make -C ports/stm BOARD=feather_stm32f405_express -j

Verify the option is enabled in the generated build config:

grep CIRCUITPY_AUDIOIO ports/stm/build-feather_stm32f405_express/mpconfigport.mk
# Expected output: CIRCUITPY_AUDIOIO = 1

Flash the resulting .bin to the board using your preferred method (e.g. dfu-util):

dfu-util -a 0 --dfuse-address 0x08000000:force:mass-erase -D ports/stm/build-feather_stm32f405_express/firmware.bin

File Setup

If using run_serial_tests.py this step is done automatically — skip ahead.

Copy the WAV test samples onto the board's CIRCUITPY drive root:

cp \
  tests/circuitpython-manual/audiocore/jeplayer-splash-8000-8bit-mono-unsigned.wav \
  tests/circuitpython-manual/audiocore/jeplayer-splash-8000-16bit-mono-signed.wav \
  tests/circuitpython-manual/audiocore/jeplayer-splash-44100-16bit-mono-signed.wav \
  tests/circuitpython-manual/audiocore/jeplayer-splash-8000-16bit-stereo-signed.wav \
  tests/circuitpython-manual/audiocore/jeplayer-splash-44100-16bit-stereo-signed.wav \
  /Volumes/CIRCUITPY/

These five files cover every exercised code path:

  • 8000-8bit-mono-unsigned — 8-bit unsigned decode path, audibly lo-fi
  • 8000-16bit-mono-signed — 16-bit signed decode path at lowest sample rate
  • 44100-16bit-mono-signed — 16-bit at highest sample rate (DMA reconfiguration, audible quality difference)
  • 8000-16bit-stereo-signed — stereo decode path, left→A0, right→A1
  • 44100-16bit-stereo-signed — stereo at 44.1 kHz

The 16 kHz files in the audiocore test set are skipped because they exercise no code paths beyond 8 kHz and 44.1 kHz. 24-bit WAVs are not supported by audiocore.WaveFile and will raise OSError if loaded — they are omitted on purpose.

Copy the test scripts to the board as well (or paste them into the REPL):

cp tests/circuitpython-manual/audioio/wavefile_playback.py      /Volumes/CIRCUITPY/
cp tests/circuitpython-manual/audioio/wavefile_pause_resume.py  /Volumes/CIRCUITPY/
cp tests/circuitpython-manual/audioio/single_buffer_loop.py     /Volumes/CIRCUITPY/
cp tests/circuitpython-manual/audioio/stereo_playback.py        /Volumes/CIRCUITPY/

Test 1 — WAV File Playback (wavefile_playback.py) (automated)

Verifies that AudioOut(board.A0) can play WAV files at 8 kHz and 44.1 kHz with 8-bit unsigned and 16-bit signed encodings. Stereo WAVs are played here through the mono AudioOut (only the left channel is mixed to A0); the stereo path is exercised separately in Test 5.

Note: 24-bit WAV files are not supported by audiocore.WaveFile and will print an OSError if any are present on the filesystem. That is expected.

Run from the REPL:

import os
os.chdir("/")
exec(open("wavefile_playback.py").read())

Expected output (order may vary by filename sort):

playing jeplayer-splash-44100-16bit-mono-signed.wav
playing jeplayer-splash-8000-16bit-mono-signed.wav
playing jeplayer-splash-8000-8bit-mono-unsigned.wav
done

What to listen for:

  • Each supported WAV plays the "jeplayer splash" jingle to completion before the next starts.
  • No loud pops at the start or end of each file (the DAC ramp-in / ramp-out should suppress them).
  • Audio pitch should match the sample rate: the 44100 Hz file sounds the most natural; the 8000 Hz file sounds lower fidelity.

Test 2 — Pause and Resume (wavefile_pause_resume.py) (automated)

Verifies AudioOut.pause() / AudioOut.resume() by toggling every 100 ms during playback. The audio will sound choppy — that is intentional.

Run from the REPL:

exec(open("wavefile_pause_resume.py").read())

Expected output (repeating for each WAV):

playing with pause/resume: jeplayer-splash-44100-16bit-mono-signed.wav
  paused
  resumed
  ...
playing with pause/resume: jeplayer-splash-8000-16bit-mono-signed.wav
  paused
  resumed
  ...
playing with pause/resume: jeplayer-splash-8000-8bit-mono-unsigned.wav
  paused
  resumed
  ...
done

What to verify:

  • The REPL prints alternating "paused" / "resumed" lines.
  • Audio cuts in and out in sync with the prints.
  • Playback eventually completes (the while dac.playing loop exits normally).
  • No hard fault or hang.

Test 3 — Looping Sine Wave (single_buffer_loop.py) (automated)

Verifies RawSample with loop=True and tests all four sample formats: unsigned 8-bit, signed 8-bit, unsigned 16-bit, signed 16-bit.

Each sample generates one cycle of a 440 Hz sine wave and loops for 1 second.

Run from the REPL:

exec(open("single_buffer_loop.py").read())

Expected output:

unsigned 8 bit

signed 8 bit

unsigned 16 bit

signed 16 bit

done

What to listen for:

  • A 440 Hz tone (concert A) for approximately 1 second for each format.
  • All four formats use the same 8 kHz sample rate and should sound essentially identical in pitch and volume — the test is comparing format-conversion paths, not playback rates.
  • No pops or glitches during the loop.
  • Clean silence between tones (quiescent DAC value holds between stop() calls).

Test 4 — deinit and Re-init (automated)

Verifies that AudioOut can be deconstructed and reconstructed without rebooting, and that pin A0 is properly released.

import audioio, analogio, board

# Construct and immediately deinit AudioOut
dac = audioio.AudioOut(board.A0)
dac.deinit()

# PA04 should now be free for AnalogOut
aout = analogio.AnalogOut(board.A0)
aout.value = 32768  # mid-scale
aout.deinit()

# Re-create AudioOut on the same pin
dac2 = audioio.AudioOut(board.A0)
dac2.deinit()
print("pass")

Expected output: pass with no exceptions.

Test 5 — Stereo Playback (stereo_playback.py) (automated)

Verifies that AudioOut(board.A0, right_channel=board.A1) drives both DAC channels independently: left on A0 (PA04, DAC_CH1), right on A1 (PA05, DAC_CH2), both clocked by TIM6.

The script runs four phases in order:

  1. Left-only 440 Hz tone (~1 s) — only A0 should produce audio.
  2. Right-only 440 Hz tone (~1 s) — only A1 should produce audio.
  3. Both-channel 440 Hz tone (~1 s) — equal amplitude on both pins.
  4. Pan sweep L → R (~3 s) — continuous equal-power (cos/sin) crossfade from A0 to A1 in a single non-looped buffer.
  5. Then plays each stereo WAV (44100 and 8000 Hz) in full.

Hardware required: connect a stereo headphone/amp to A0 (left) and A1 (right) with common ground, or scope-probe each pin separately.

Run from the REPL:

import os
os.chdir("/")
exec(open("stereo_playback.py").read())

Expected output:

channel test: left only
channel test: right only
channel test: both channels
pan sweep: left to right
playing stereo: jeplayer-splash-44100-16bit-stereo-signed.wav
playing stereo: jeplayer-splash-8000-16bit-stereo-signed.wav
done

What to listen / look for:

  • "left only" → tone in left ear, silence in right.
  • "right only" → tone in right ear, silence in left.
  • "both channels" → centered tone in both ears.
  • "pan sweep" → tone smoothly travels left → right over ~3 s.
  • Stereo WAVs play with proper L/R separation; no cross-contamination.
  • On a scope: probing A0 and A1 simultaneously during phases 1 and 2 should show one channel idle (mid-scale DC) while the other carries the sine.

Oscilloscope Checks (Optional)

Each test script drives board.D4 (pin D4) low at the start of each playback and high when it ends. This provides a clean trigger edge for a scope.

  • Test 1: Probe A0 — should show a sampled waveform at the correct sample rate. Probe D4 for a gate signal that spans the file duration.
  • Test 3: Probe A0 — should show a 440 Hz staircase-sine at the DAC output (12-bit steps visible at 44.1 kHz; fewer at 8 kHz). A simple RC low-pass filter (1 kΩ + 100 nF) on the A0 output will smooth the staircase significantly.

Known Limitations

  • Left channel must be A0 (PA04). Any other pin raises ValueError: AudioOut requires pin A0 (PA04).
  • Right channel must be A1 (PA05) when used. Any other pin raises ValueError: AudioOut right channel requires pin A1 (PA05).
  • 24-bit WAV files are not supported by audiocore.WaveFile and will raise OSError when opened.
  • Only one AudioOut instance can be active at a time.