LIVE GITHUB PUBLIC github.com/borenw/dd-cli

Step 1 · Install

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
Why these blocks carry no # 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
Why the 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.

Step 2 · Rise and fall a pin

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
What VIO actually does. It sets the logic level of the DIN/DIO interface (1.2–3.3 V), so set it to match whatever you wire up. It is not required for the device to see its own signals: measured with the rail at 0.009 V, the pattern-generator → logic-analyzer loopback still produced the same 38 edges as at 3.3 V. So a clean internal capture is not proof that your external levels are right — check those with a meter.

Step 3 · Prove it without a scope

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
Figure 1 · Measured DIO24 edges — 10 kHz square wave captured by the device that generated it
DIO24 149 199 249 299 349 399 449 time [µs] ↑↑↑↑ 3.3 V 0 V
Edge times are the real values printed by the example above: rises at 149, 249, 349, 449 µs and falls at 199, 299, 399 µs — a 50 µs half-period, i.e. 10.0000 kHz measured against 10.0000 kHz driven. Captured at 2 MSa/s (200× oversampling) through the DIO loopback.

Pins

Figure 2 · Pin map — what can be driven, and how the SDK indexes it
DIN0 … DIN23 24 pins · input only · cannot be driven DIO24 … DIO39 16 pins · bidirectional · drivable, powered by VIO pattern generator + static IO index these 0…15 DIO24 → index 0 · DIO39 → index 15 Use pin_to_index() — or just pass DIO24 to the CLI and let it translate.
Silkscreen numbers are used everywhere in 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.

Commands

CommandWhat it does
dd listEnumerate attached devices (--json)
dd infoClocks, buffer size, index mapping, rail measurements
dd selftestValidate the binding and parsing — works with nothing plugged in
dd captureRecord logic → .vcd, .csv, .bin, or a terminal table
dd patternClock / PWM / random / custom bit sequences
dd read · dd writeStatic pin levels — DIO24=1, =0, =z; --verify reads back and confirms
dd vioVIO voltage, DIN pulls, DIO drive strength and slew
dd i2cscan, read, write, xfer
dd spiMaster transfer, modes 0–3
dd uarttx / rx
dd stopHalt the generator, release pins (--vio-off)

Capturing

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.

VCD is value-change encoded and compact: a 20 000-sample, 2-channel capture is 8.7 KB as VCD versus 396 KB as CSV.

Generating

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)

Things the hardware does that surprise people

Tests

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.

Alternatives

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.