hardware, firmware, and software source files for a Harp-compatible 4-channel Digital-to-Analog Converter.
- Analog Output Channels: 4
- Bit depth: 16-bit
- Update Rate: 2.288 [KHz] to 500 [KHz] (selectable per-channel)
- this is the rate at which a new output value is selected.
- Voltage Swing: ±10 [V]
- RMS noise at the zero voltage setting: ~±2.5 [mV]
- absolute deviation from 0 [V]: ±2.5 [mV]
- Power Input: 12-24 [V]
- Power Input Plug: 2.1 x 5.5mm barrel jack (positive center)
- Reverse Polarity protected.
- Additional M4 ground lug provided for de-noising.
- Open source hardware, firmware, and software.
- Cost-effective at < $300 USD to manufacture a single unit.
- Preview the hardware design online with KiCanvas.
- Enclosure CAD Model
- Trigger options
- external trigger
- software trigger
- User-selectable external trigger mapping, i.e: any input trigger can be setup to trigger any number of analog output channels.
- True simultaneous triggering in cases where multiple waveforms are triggered at the same time.
The device features 3 ways to play waveforms: either through the two built-in primitive waveform generators or by playing waveforms pre-uploaded to an SD card.
- Max Data Storage: limited by size of SD card
- Max file length for a single file: 4GB
- File names: user-specifiable with a 32-character limit.
- Format: 16-bit Pulse Code Modulation (PCM)
- -10 Volts corresponds to 0
- 0 Volts corresponds to ~32768
- 10 Volts corresponds to 65535
The Quac board is a fully Harp-protocol-compliant device built on top of the Harp Pico Core.
- Harp Device ID: 1411
- Harp Events
- Waveform Start (per-channel)
- Waveform Finished (per-channel)
The harp.device.quac board follows a legacy of many other devices that came before it. For similar devices, have a look at:
Order fully-assembled boards directly from PCBWay.
These printed circuit boards are made on-demand.
There are two ways to generate waveforms: either with one of two primitive waveform generators or by playing files from the SD card. These options are referred to as waveform Players.
Setting up and playing a waveform is a simple process for each Analog Output (AO) channel.
- Specify any external input trigger conditions for the output channel.
- Specify the waveform Player.
- Specify the Player's settings.
- Wait until the Player is ready (< 50[ms] of wait time for the player to apply the settings and arm the waveform).
- Trigger the Player either via software command or by applying external input to the corresponding external input pins.
For fully worked examples of the above steps, see the examples in the software folder.
Note
While waveforms on the SD card will remain on the card until they are deleted, Player settings for any Player do not persist across device power-cycles or resets.
Player Settings are detailed below for each Player.
The following settings are common to each Player.
cycles: number of iterations of the current settings (will probably be 1 in most cases).duration_us: duration in microseconds to play the waveform or 0 to either play-forever (if the source is infinite ie: periodic functions) or play-to-completion (if the source is finite i.e: files on the SD card).update_frequency_hz: rate at which samples are produced (max 500 [KHz]).
frequency_hz: sine wave frequency in hertzamplitude_volts: "center-to-peak" amplitude in voltsvertical_shift_volts: vertical shift in voltsnormalized_phase_shift: period shift normalized to -1.0 (max right shift) to 1.0 (max left shift)
| Setting | Value | Note |
|---|---|---|
cycles |
1 | play the following settings once |
duration_us |
3000000 | play for 3 seconds |
update_frequency_hz |
10000 | rate at which to produce new samples |
frequency_hz |
10 | sine wave frequency |
amplitude_volts |
0.5 | result will be 1 [V] peak-to-peak |
vertical_shift_volts |
0.5 | result will span 0 [V] to 1 [V] |
normalized_phase_shift |
0 | no phase shift |
| Setting | Value | Note |
|---|---|---|
cycles |
1 | |
duration_us |
0 | play forever (until aborted) |
update_frequency_hz |
10000 | |
frequency_hz |
10 | |
amplitude_volts |
0.5 | |
vertical_shift_volts |
0 | |
normalized_phase_shift |
0 |
(Inherits all Common Settings and Sine Player Settings)
ramp_on_us: time in microseconds to rise from lowest to peak value.pulse_width_us: the total pulse width (including ramp-on and ramp-off duration of the waveform in microseconds.ramp_off_us: time in microseconds to fall from peak to lowest value.
path: filepath on the SD card (32-character limit max)
| Setting | Value | Note |
|---|---|---|
cycles |
1 | play the following settings once |
duration_us |
0 | play the file to completion |
update_frequency_hz |
500000 | max update rate (might be different depending on file). |
path |
channel_0.bin | assumes this file exists at the top level folder in the SD card |
| Setting | Value | Note |
|---|---|---|
cycles |
3 | loop back and play the entire file 3 times |
duration_us |
0 | |
update_frequency_hz |
500000 | |
path |
channel_0.bin |
The quac board reads files in 16-bit little-endian Pulse-Code Modulation (PCM) format. There are a few options for generating waveforms in this format.
It's possible to convert existing audio files to a format compatible with the quac board using ffmpeg.
To upscale an existing *.wav file to a 500KHz update rate, use:
ffmpeg -i example.wav -f u16le -ar 500000 output.rawFor more complicated waveforms that do not derive from an existing audio file, we recommend using numpy.
Here's an example that generates the North American Ringing Tone, which is the sum of a 440Hz and 480Hz sine wave.
import numpy as np
NUM_SAMPLES = int(5e6) # 5 million samples @ 500KSs -> 10 seconds of data.
SAMPLES_PER_SECOND = 500000.
FULL_SCALE_RANGE = (1 << 16) - 1 # 16 bit resolution
SECONDS = NUM_SAMPLES/SAMPLES_PER_SECOND
FILENAME = f"channel_0.bin"
t = np.linspace(0, SECONDS, NUM_SAMPLES)
x = np.zeros(NUM_SAMPLES)
# make sine wave. offset it to uint16 range: 0-65535
for freq in [440, 480]:
x += (np.sin(2 * np.pi * freq * t)+1)/2 * FULL_SCALE_RANGE/2
# Write result to file in 16-bit little-endian format.
with open(FILENAME, "wb") as file:
x.astype("<u2").tofile(file)Currently waveforms must be uploaded to the SD card manually.
Warning
Power down the device before removing or inserting the SD card.
There are two ways to trigger a configured waveform to play: via software command or through the device's external triggers labeled DI0, DI1, DI2, and DI3.
Before triggering a waveform, you must
For fully worked examples in both Bonsai and Python, see the software examples folder.
By default the device's power-on-reset behavior is setup to: setup external triggers such that pins DIO - DI3 correspond to playing output pins A0 - A3 respectively.
Trigger mapping is configurable! Any input trigger can be configured to trigger any number of outputs. To alter the trigger mapping, you must use software commands through either Bonsai or Python.
During normal operation, up-to-four waveforms stored on the SD card are read (interleaved) at 4MB per second. With the extra overhead of switching between files, the SD card must be able to support read speeds ≥8MB per second. In theory, any Class 10 SD Card formatted in FAT32 format should be compatible.
But since card performance can vary, here's a list of tested cards:
| Vendor | Model |
|---|---|
| Samsung | Pro Plus 8GB Smart Card |
| GIGASTONE | Industrial 8GB MLC |
The following section details how the underlying firmware generates waveforms.
Each channel features a modular approach to dividing up the work of playing waveforms.
The level closest to the hardware is a driver that wraps a custom PIO program to communicate over SPI to each LTC2641 DAC chip.
The next stage up is a shared double buffer and two DMA channels responsible for ensuring that the driver receives an uninterrupted, steady stream of bytes paced by a DMA Timer setup to match the user-requested playback rate. Inspecting the state of the double buffer is implemented by reading single registers native to the Pico's DMA peripherals to eliminate the need to implement mutex locks to check multiple locations in memory concurrently.
The layer above features one of multiple ways of generating data. These Players are responsible for producing a sequence of bytes up to the limits specified by the user's waveform settings. Each Player is derived from a base class that manages sending data to the corresponding available buffer from the downstream double buffer and manages sequence arming and termination.
In the full architecture, four copies of the above pipeline exist like so:
The MultiTransferManager connects to each double buffer and driver and handles triggering an armed transfer, and it guarantees that multiple simultaneous requests to start a transfer occur simultaneously.
Waveforms are read interleaved from their sources in "chunks" of 16384 samples at a time and then pushed into double buffers such that the resulting output plays waveforms concurrently without interruption.
Buffer size was selected to be large enough such that data from the SD card is can be read faster than it needs to sent to the downstream DAC drivers. This setup was tested in the worst-case scenario where 4 files are being read at once.
A single CPU core is dedicated to this task of reading waveform sources and topping off buffers while the other core handles Harp communication, settings configuration, and waveform start/stop inputs.
New firmware is available on the Releases Page. To upload new firmware to the device, do the following:
- Ensure that the board is connected to a PC with the USB cable.
- Power down the board. (Note that power comes from the barrel jack, not the USB cable.)
- Power on the board with the BOOTSEL button held down. Then release the BOOTSEL button once the board has been powered up. The device will now appear on the connected PC as a flash drive. See the figure below to identify the BOOTSEL button. You may need to use a hex key to access this button with the case lid attached.
- Drag and drop the *.uf2 firmware file into the flash drive's top level directory. The flash drive should disappear indicating that the firmware upload worked. The device now has new firmware.
Currently the device has some known limits, most of which are planned to be eclipsed by future firmware releases. This non-comprehensive list includes:
- FilePlayer cannot deterministically play a subset of a file multiple times
- Waveforms cannot yet be uploaded to the SD card directly over USB
For a full list of issues, head over to the project's issues page.






