A Direct Digital Synthesizer (DDS) IP core for FPGA, configured over a 16-bit
SPI slave port (mode 0) and driving one or two 14-bit parallel DACs.
Written in plain Verilog-2001 — no vendor IP, no Block Memory Generator, no
.coe files; all memories are inferred BRAM.
Designed to be driven by an MCU. The companion firmware for an STM32F103, with
a UART command line for the PC, lives in MCU/STM32F1.
- 32-bit frequency tuning word:
fout = FWORD × f_dds / 2^32(23.3 mHz resolution at 100 MHz) - Waveforms: sine / cosine (4096-point quarter-wave LUT, 16384 effective points per period), square with 16-bit programmable duty cycle, triangle, ramp, and a user-defined 4096 × 14-bit waveform RAM loaded over SPI
- Frequency control modes:
- FIXED — phase-continuous retune on a single frame
- SWEEP — hardware linear chirp (start/stop/delta/rate), single, repeating, or up/down; software or external-pin trigger
- PROFILE — 4 pre-programmed frequency+phase profiles, hopped by register
or by the
hop_sel[1:0]input pins (FSK/PSK with zero CPU load)
- 16-bit phase offset (≈ 0.0055° resolution)
- Digital amplitude scaling 0 … 1.0 (Q1.15 multiplier — finer than the DAC's own LSB at every setting)
- Programmable DC offset: 14-bit signed, saturating, added after the amplitude scaler — waveform + bias, or a pure DC level output
- Output conditioning: 14-bit or 12-bit (MSB-aligned, rounded), offset binary or two's complement, polarity invert, mid-scale idle when disabled
- Atomic frame commit: everything a SPI frame writes takes effect together, on the rising edge of chip select. A 32-bit frequency word written as two 16-bit halves never puts an intermediate frequency on the DAC.
- Two channels share one chip select — the channel is a bit in the command word
- Single clock domain: the SPI pins are oversampled, so there is no CDC to constrain and no second bus clock
- Interrupt output per channel (sweep done / wrap, update done)
Mode 0 (CPOL = 0, CPHA = 0), MSB first, 16-bit words, CS active low —
bit-for-bit what an STM32 SPI master produces with DATASIZE_16BIT,
POLARITY_LOW, PHASE_1EDGE and a GPIO chip select.
CS↓ [CMD] [D0] [D1] ... CS↑ CS↑ commits the whole frame atomically
CMD = | RW(1) | CH(1) | ADDR(14) | RW: 1 = write, 0 = read
ADDR auto-increments per data word
write: D0 -> ADDR, D1 -> ADDR+1, ...
read: one turnaround word after CMD, then data:
master [CMD] [dummy] [dummy] [dummy]
slave [0] [0] [ADDR] [ADDR+1]
The pins are oversampled in dds_clk, which requires f_sck ≤ dds_clk / 6
(9 MHz SCK against a 100 MHz dds_clk gives 11× margin).
rtl/ portable Verilog-2001, no vendor primitives, simulatable
dds_top.v the IP core - instantiate this (SPI + 1..2 channels)
dds_spi_slave.v SPI mode-0 slave, oversampled; drives the internal bus
dds_channel.v one channel: registers + datapath + waveform RAM
dds_regs.v 16-bit register file, shadow/commit logic
dds_core.v phase accumulator, sweep engine, waveforms, output stage
dds_sin_lut.v quarter-wave sine ROM (inferred BRAM)
dds_sin_lut.mem ROM init data for $readmemh (add to the Vivado project!)
dds_wave_ram.v user waveform dual-port RAM (inferred BRAM)
board/ board-level, Xilinx primitives - this is the synthesis top
dds_board_top.v PLL + reset sync + IOB registers + ODDR clock forwarding
tb/
tb_dds.sv self-checking testbench (bit-accurate SPI master model)
scripts/
gen_sin_lut.py regenerates dds_sin_lut.mem
Doc/
DDS_Datasheet.md full datasheet: frame format, register map, programming
examples, integration and constraints
DDS_Tutorial_zh.md 中文入门教程: DDS 原理、每个功能、扫频/FSK、命令详解、上手步骤
Vivado_Build.md how to build it: top module, IP, .mem, XDC, bring-up order
AN01_AD9764_Module.md application note for one specific target DAC board
dds.pdf theory reference (ADI DDS tutorial) the design follows
dds_top is the IP core, not the synthesis top. It has no PLL, no clock
forwarding and no I/O primitives on purpose, so it stays board-independent and
simulates under Icarus. The synthesis top is board/dds_board_top.v, which adds
the 100 MHz PLL, the reset synchronizer, IOB output registers and the ODDR that
forwards the DAC clock. See Doc/Vivado_Build.md.
Instantiate dds_top, tie dds_clk to the DAC sample clock (100 MHz), wire the
four SPI pins to the MCU, and connect the DAC buses:
dds_top #(
.NUM_CH(2), .SIN_LUT_FILE("dds_sin_lut.mem")
) u_dds (
.dds_clk(clk100), .dds_rstn(rstn),
.spi_sck(sck), .spi_cs_n(cs_n), .spi_mosi(mosi),
.spi_miso(miso), .spi_miso_oe(), // leave open for a dedicated pin
.ch0_sweep_trig(1'b0), .ch0_hop_sel(2'b00), // optional hardware pacing
.ch1_sweep_trig(1'b0), .ch1_hop_sel(2'b00),
.ch0_dac_data(dac0_data), .ch0_dac_valid(dac0_valid),
.ch1_dac_data(dac1_data), .ch1_dac_valid(dac1_valid),
.dds_irq(dds_irq)
);Generate a 1 MHz full-scale sine on channel 0 (f_dds = 100 MHz):
frame: 0x8004, 0x5C29, 0x028F // FWORD = 1e6 * 2^32 / 100e6, one atomic frame
frame: 0x8002, 0x0001 // CTRL: enable, sine, offset binary (AD9764)
Or, from the STM32 firmware's UART console:
dds> freq 1M
dds> wave sine
dds> on
The datasheet has the complete register map and worked frame sequences for sweeps, FSK profiles, duty-cycle squares, DC bias, and loading user-defined waveforms.
Requires Icarus Verilog:
cd tb
iverilog -g2012 -o tb_dds.vvp tb_dds.sv ../rtl/*.v
vvp tb_dds.vvp # 47 self-checks -> "ALL TESTS PASSED"
The testbench drives the real SPI pins with a bit-accurate mode-0 master at
9 MHz — the same waveform the STM32 firmware produces. Add -DSCK_FAST to rerun
the suite at the documented dds_clk / 6 limit (16 MHz). A tb_dds.vcd
waveform dump is produced.
Per channel: ~4 × BRAM18 (sine LUT + wave RAM), 1 × DSP48 (amplitude multiplier), roughly 600 LUT / 900 FF. The shared SPI slave adds ~80 LUT / 120 FF. Comfortably exceeds 150 MHz on Artix-7 speed grade −1.