Install Digilent WaveForms first —
it provides the libdwf runtime this wraps.
1a. On macOS, once: put the SDK where the runtime actually looks for it. This asks for your password, so it is its own one-line block — nothing follows it that the password prompt could swallow.
sudo cp -R /Applications/WaveForms.app/Contents/Frameworks/dwf.framework /Library/Frameworks/ && sudo xattr -dr com.apple.quarantine /Library/Frameworks/dwf.framework
1b. Then install dd and check it — paste the whole block:
git clone https://github.com/borenw/dd-cli.git
mkdir -p ~/.local/bin
ln -sf "$PWD/dd-cli/bin/dd" ~/.local/bin/dd
export PATH="$HOME/.local/bin:$PATH"
dd selftest
dd list
dd selftest needs no hardware; dd list needs the device plugged in.
To keep dd on your PATH in new shells:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
# comments. zsh does not enable
interactive_comments at an interactive prompt, so a pasted comment line fails with
zsh: command not found: # — and a trailing comment is worse, silently
passing # … through as extra arguments. Every shell block on this page is
comment-free so it pastes cleanly into zsh and bash alike.
A healthy dd list looks like this:
libdwf 3.25.1 (/Library/Frameworks/dwf.framework/dwf)
idx type name serial rev open
0 Digital Discovery Digital Discovery SN:210321BE819D 2 no
sudo lines matter on macOS. Dragging WaveForms.app out of the DMG
does not create /Library/Frameworks/dwf.framework, and without it every SDK
client — this tool, pydwf, dwfpy, Digilent's own samples — enumerates
zero devices and fails with Adept NOK / dmgrEnumDevicesEx ERC: 3090. The WaveForms
GUI still works, because it finds its Adept data inside its own bundle, so
"the GUI sees my device" does not mean the SDK will. Add
export PATH="$HOME/.local/bin:$PATH" to your shell profile to make it stick.
The smallest useful thing the device can do — drive DIO24 high, then low:
dd vio --set 3.3 --on
dd write DIO24=1 --verify
dd write DIO24=0 --verify
dd write DIO24=z
Line by line: set the interface level, rise ↑, fall ↓, then release the pin
back to an input. --verify reads the pin back from the input register:
set DIO24=1 (output-enable 0x1, value 0x1)
verify: reads back DIO24=1 — confirmed :)
Same thing from Python, pulsing a few times — this is examples/01_toggle_pin.py:
import time
from ddcli.device import DigitalDiscovery
with DigitalDiscovery(keep_running=True) as dd:
dd.vio_set(voltage=3.3, enable=True)
dd.vio_settle() # the rail needs ~1 s to ramp
bit = 1 << dd.pin_to_index(24) # DIO24 is bit 0, not bit 24
for _ in range(5):
dd.io_write(bit, bit); time.sleep(0.5) # HIGH
dd.io_write(bit, 0); time.sleep(0.5) # LOW
dd.io_write(0, 0) # release
DIO24–DIO39 are shared between the pattern generator and the logic
analyzer, so the device can capture the very signal it is generating — no wiring, no probe.
That is examples/02_verify_edges.py:
$ python3 examples/02_verify_edges.py DIO24 10k
Drive: DIO24 at 10000 Hz (requested 10000 Hz)
Capture: 20000 samples at 2 MSa/s
Edges: 198 total (99 rising, 99 falling)
Freq: 10.0000 kHz measured vs 10.0000 kHz driven
Duty: 49.50 %
First edges:
t = 149.000 us RISE ^
t = 199.000 us FALL v
t = 249.000 us RISE ^
t = 299.000 us FALL v
dd: DIN5, DIO24,
or a bare integer. The SDK's 0-based indexing of DIO24–39 is derived at runtime from the
device's own reported channel count, not hardcoded — dd info prints the mapping.
| Command | What it does |
|---|---|
dd list | Enumerate attached devices (--json) |
dd info | Clocks, buffer size, index mapping, rail measurements |
dd selftest | Validate the binding and parsing — works with nothing plugged in |
dd capture | Record logic → .vcd, .csv, .bin, or a terminal table |
dd pattern | Clock / PWM / random / custom bit sequences |
dd read · dd write | Static pin levels — DIO24=1, =0, =z; --verify reads back and confirms |
dd vio | VIO voltage, DIN pulls, DIO drive strength and slew |
dd i2c | scan, read, write, xfer |
dd spi | Master transfer, modes 0–3 |
dd uart | tx / rx |
dd stop | Halt the generator, release pins (--vio-off) |
dd capture -r 200M -n 20000 --order dio --pins DIO24,DIO25 -o trace.vcd
dd capture -r 1M -n 1M -t "rise:DIN0,low:DIN1" --position 500000 -o trace.csv
Format comes from the file extension — .vcd opens in GTKWave or PulseView.
Sampling is 800 MHz / divider, so not every rate is reachable; the actual
rate is printed and used for all timestamps. Every capture ends with an activity
summary (which lines toggled, how many changes), so an idle-looking first screenful is
never ambiguous.
dd pattern DIO24=clock:1MHz DIO25=pwm:1kHz:25 DIO26=random:2M
dd pattern DIO27=custom:10110010@500k
dd stop
Output frequency is 100 MHz / (divider × ticks). dd searches for a
divider that divides the period exactly, and warns when a request is off that grid
rather than silently handing back a different clock:
warning: DIO24 requested 1.2e+07 Hz but the device can only produce 1.25e+07 Hz (4.17% off)
dd vio waits for the setpoint.dd call is its own process; if closing stopped the device, dd capture would kill a generator dd pattern had just started. Use dd stop to tear down.python3 tests/test_offline.py
python3 tests/test_mock.py
The first covers parsing, pin mapping and the VCD/CSV writers; the second drives the full
hardware path against a mock libdwf.
tests/mock_dwf.c is a fake libdwf written in C that impersonates a
Digital Discovery. The mock tests drive the real code through real ctypes calls
and then assert on the values that arrived — so a mis-declared argtype (a double in the
wrong register, a 64-bit trigger mask truncated to 32) fails there rather than mysteriously on
hardware. 145 assertions, and it runs with nothing plugged in. It caught a genuine 13 ppm
pattern-frequency bug during development.
pydwf and
dwfpy are mature libraries wrapping the same
libdwf, both with their own CLI — use them if you want a full Python API surface.
dd is for driving the Digital Discovery from a shell with zero install, silkscreen
pin names, and VCD out of the box.