DDFS - Direct Digital Frequency Synthesis
DDFS is a digitally-controlled method of generating multiple frequencies from a reference frequency source. DDFS is a method of producing a tunable digital or analog waveform. First the data points of the waveform are generated in digital format and the converted to analog format with a Digital to Analog Converter (DAC) and a Low Pass Filter (LPF)
Next we examine the synthesis and implementation of three types of signals:
- Digital waves: square wave of constant amplitude
- Unmodulated analog sine wave
- Modulated sine wave, with phase, frequency, and amplitude controlled by other signals.
Table of Contents
- DDFS - Direct Digital Frequency Synthesis
- DDFS module construction
- Testing with the Spartan 7 FPGA with the Digilent Arty S7
- Generating a Triangle Wave
- Next steps
- SystemVerilog Study Notes Chapters
Direct synthesis of a square wave
It uses a fixed clock, an N-bit adder, and a phase register or phase accumulator. The frequency of the signal is controlled by a digital word (fcw) used to increment the phase of the signal. With each clock pulse, the phase register samples the sum of the value in the previous clock cycle and the frequency control word (fcw) to produce the output sequence for that cycle. The phase accumulator continuously accumulates the phase increment, and when it reaches its maximum value, an overflow occurs and the accumulator is reset to zero, completing one cycle of the frequency.
DDFS offers fast switching between output frequencies, fine frequency resolution, and operation over a wide range of frequencies.
Block diagram for synthesizing a digital waveform or square wave with constant amplitude:
To synthesize the digital waveform the DDFS requires a register, the Phase Register or Phase Accumulator in the diagram, and an adder.
The output of the circuit is the MSB of the register output Q. It is a square wave with a duty cycle close to 50%.
The output frequency is controlled by the M frequency N-bits word, known as frequency control word. This value is added to the phase register in every clock cycle.
Main parameters of the digital waveform generator.
- N: word width of the register and the adder.
- fsys and tsys: frequency and period of the system clock
- fout and tout: frequency and period of the output signal.
- M: frequency control word
The Phase Accumulator starts with value 0 and gradually increments to 2^N-1 and then wraps around.
The MSB of Q starts as 0 and then changes to 1 when the Phase Accumulator reaches halfway of 2^N-1. tout, the output signal period, is the duration of incrementing the phase accumulator from 0 to 2^N-1,
M is added to the Phase Accumulator each system clock cycle, it then requires (2^N)/M cycles to complete one cycle, and its duration is: tout = ((2^N)*/M) * tsys
Rewritten in term of frequencies: fout = M * (fsys/2^N)
As N increases the finer frequency can be obtained. In the experiments of this blog we will use a value of 30 bits.
To obtain the target frequency we need to calculate the approximate M value: M = (fout / fsys)*2^N
The value of M must be rounded to a whole integer number. The variation in frequency produced by the rounding error is known as jitter.
Direct synthesis of an unmodulated analog waveform
We can generate an unmodulated analog waveform mapping phases to digitized amplitude points and then converting the values to the analog format by a DAC.
The conceptual diagram of the direct synthesis of an unmodulated analog waveform.
The previous digital waveform scheme uses the MSB of the Phase/Accumulator Register as the output. The Most Significant Bit, MSB, divides tout in two equal regions. Using mor bits we can divide tout to smaller regions or phases of a period. Then we can map the phases to digitized amplitude points.
The Phase to Amplitude Lookup Table performs the mapping. I can be implemented by a ROM or RAM. We will use a synchronous ROM.
The DAC converts the digitized amplitude value to an analog value and the Low Pass Filter removes unwanted high-frequency signals.
The "shape" of the analog waveform is determined by the values stored in the lookup table. We can generate any type of analog waveform. In this blog we will play with sine and triangular waves.
We don't use all the N bits for the lookup table. We'll use 8 MSBs from the N-bit phase register output, S-width in the diagram. A larger S increases the size of the lookup table. A small S puts more constraints on the low pass filter.
Direct synthesis of a modulated analog waveform
Modulation is the process of modifying a carrier signal in accordance with a message signal. We can modulate the analog signal in amplitude, frequency and phase modulation.
If the carrier signal is sin(2*PI*f*t) the modulated signals become the following:
- Amplitude modulation: A(t) * sin(2*PI*f*t)
- Frequency modulation: sin(2*PI*(f + Δf(t) * t)
- Phase modulation: sin(2*PI*f + Δp(t) )
The A(t), Δf(t) and Δp(t) are slow time-varying signals that embed the message
We can then expand the DDFS system incorporating the desired modulation scheme by inserting additional adders or multipliers in its path:
The above diagram shows a conceptual diagram of a DDFS system that supports all the three modulation schemes.
Instead of sin(2* PI * f * t) the extended system generates:
A(t) * sin(2*PI*(f + Δf(t) * t ) + Δp(t))
- Frequency Control Word : Frequency control word (fccw) to to generate the carrier frequency
- Frequency Offset Word: The frequency control word (focw) to generate the offset frequency Δf(t)
- Phase Offset: The phase value corresponding to the desired phase offset, Δp(t)
- Envelope: The digitized value of A(t)
DDFS module construction
We use the following parameters in the design:
- fsys: 100 MHz
- N: 30 bits
- Width of the lookup table 8 bits (256 entries)
- sine wave amplitude resolution: 16 bits in signed format
Synchronous ROM Unit - Table Lookup
The size of the lookup table is 2^8 by 16 (4K bits)and can be implemented by a synchronous ROM.
We will take advantage of the possibility to define initial values of FPGA's internal memory modules. When an SRAM-based FPGA device is programmed, the configuration file is loaded to the device's configuration module. When the configuration is completed the memory modules are initialized as well. If the content of a memory is not updated during the operation, it maintains its original values and behaves like a ROM. This is an efficient way to implement large lookup tables or to store read-only data.
The read operation of a BRAM is controlled and synchronized by a clock signal. The ROM must include a clock signal as in the diagram.
After an address change it takes one clock cycle to output new data. The rom readout is buffered via a register.
The synchronous ROM is constructed with FPGA's BRAM module and its content is loaded into the BRAM's initial values when the device is configured.
We will use an initial block to load the values using $readmemb directive.
Sine lookup table SystemVerilog Implementation
`timescale 1ns / 10ps module sin_rom #( parameter DATA_WIDTH = 16, // number of bits ADDR_WIDTH = 8 // number od address bits ) ( input logic clk, input logic [ADDR_WIDTH-1:0] addr_r, output logic [DATA_WIDTH-1:0] dout ); // signal declaration logic [DATA_WIDTH-1:0] ram [0:2**ADDR_WIDTH-1]; // ascending range logic [DATA_WIDTH-1:0] data_reg; initial $readmemh("sin_table.txt", ram); // read operation always_ff @(posedge clk) begin data_reg <= ram[addr_r]; end assign dout = data_reg; endmodule
Elaborated design by VIvado 2022.2
Synthesized Design With BRAM module
SystemVerilog Test Bench Implementation
`timescale 1ns / 1ps `define ADDR_WIDTH 8 `define DATA_WIDTH 16 // sin table rom test bench module sin_table_tb; localparam T=20; logic clk; logic [`ADDR_WIDTH-1:0] addr_r; logic [`DATA_WIDTH-1:0] dout; sin_rom uut( .clk(clk), .addr_r(addr_r), .dout(dout)); always begin clk = 1'b1; #(T/2); clk = 1'b0; #(T/2); end initial begin addr_r = 16'h00; #40; addr_r = 16'h02; #40; addr_r = 16'h04; #40; addr_r = 16'h08; #40; addr_r = 16'h0f; #40; addr_r = 16'hf0; #40; addr_r = 16'hf2; #40; addr_r = 16'hf4; #40; addr_r = 16'hf8; #40; addr_r = 16'hff; #40; $stop; end endmodule
Test bench scope
Resources Utilization
Creating the sine wave table with Excel.
ROM Sine Wave Table
0000 0324 0648 096b 0c8c 0fab 12c8 15e2 18f9 1c0c 1f1a 2224 2528 2827 2b1f 2e11 30fc 33df 36ba 398d 3c57 3f17 41ce 447b 471d 49b4 4c40 4ec0 5134 539b 55f6 5843 5a82 5cb4 5ed7 60ec 62f2 64e9 66d0 68a7 6a6e 6c24 6dca 6f5f 70e3 7255 73b6 7505 7642 776c 7885 798a 7a7d 7b5d 7c2a 7ce4 7d8a 7e1e 7e9d 7f0a 7f62 7fa7 7fd9 7ff6 7fff 7ff6 7fd9 7fa7 7f62 7f0a 7e9d 7e1e 7d8a 7ce4 7c2a 7b5d 7a7d 798a 7885 776c 7642 7505 73b6 7255 70e3 6f5f 6dca 6c24 6a6e 68a7 66d0 64e9 62f2 60ec 5ed7 5cb4 5a82 5843 55f6 539b 5134 4ec0 4c40 49b4 471d 447b 41ce 3f17 3c57 398d 36ba 33df 30fc 2e11 2b1f 2827 2528 2224 1f1a 1c0c 18f9 15e2 12c8 0fab 0c8c 096b 0648 0324 0000 fcdc f9b8 f695 f374 f055 ed38 ea1e e707 e3f4 e0e6 dddc dad8 d7d9 d4e1 d1ef cf04 cc21 c946 c673 c3a9 c0e9 be32 bb85 b8e3 b64c b3c0 b140 aecc ac65 aa0a a7bd a57e a34c a129 9f14 9d0e 9b17 9930 9759 9592 93dc 9236 90a1 8f1d 8dab 8c4a 8afb 89be 8894 877b 8676 8583 84a3 83d6 831c 8276 81e2 8163 80f6 809e 8059 8027 800a 8001 800a 8027 8059 809e 80f6 8163 81e2 8276 831c 83d6 84a3 8583 8676 877b 8894 89be 8afb 8c4a 8dab 8f1d 90a1 9236 93dc 9592 9759 9930 9b17 9d0e 9f14 a129 a34c a57e a7bd aa0a ac65 aecc b140 b3c0 b64c b8e3 bb85 be32 c0e9 c3a9 c673 c946 cc21 cf04 d1ef d4e1 d7d9 dad8 dddc e0e6 e3f4 e707 ea1e ed38 f055 f374 f695 f9b8 fcdc
Graphic representation of the sine wave table. Values in 16 bits signed format
DDFS module implementation in SystemVerilog
We will build the DDFS module with two outputs pulse_out that is a square wave and the Pulse Code Modulation (PCM) output. This is the digitized sin wave.
The lookup table output (amp) and envelope are 16 bits wide. After multiplication we need to trim the 32-bit multiplication result back to 16 bits.
We use Q2.14 format in which -1.0 and 1.0 are represented as 1100 0000 000 0000 and 0100 0000 0000 0000 respectively.
The multiplication result, modulation,
assign modulation = $signed(envelope) * $signed(amp);
is in the Q18.14 format. We need to select the appropriate portion of the modulation signal and trim it back to the Q16.0 format, 16-bit signed integer.
`timescale 1ns / 1ps module ddfs #(parameter PHASE_ACC_WIDTH = 30) // width of phase accumulator ( input logic clk, input logic reset, input logic [PHASE_ACC_WIDTH-1:0] freq_carrier_ctrl_word, // frequency control word to generate carrier frequency // fccw = fout/fsys * 2^N ; (N = PHASE_ACC_WIDTH) input logic [PHASE_ACC_WIDTH-1:0] freq_offset_ctrl_word, // frequency offset control word // focw = fout/fsys * 2^N ; (N = PHASE_ACC_WIDTH) input logic [PHASE_ACC_WIDTH-1:0] phase_offset, // phase offset // offset/360 * 2^N ; (N = PHASE_ACC_WIDTH) input logic [15:0] envelope, // Amplitude modulation: envelope, digitized value of A(t) in Q2.14 format output logic [15:0] pcm_out, // pcm signal output logic pulse_out // pulse out ); // signal declaration logic [PHASE_ACC_WIDTH-1:0] freq_control_word; // frequency modulation control word logic [PHASE_ACC_WIDTH-1:0] phase_next; // next phase logic [PHASE_ACC_WIDTH-1:0] phase_control_word; // phase control word logic [PHASE_ACC_WIDTH-1:0] phase_reg; // actual phase logic [7:0] p2a_raddr; logic [15:0] amp; logic signed [31:0] modulation; logic [15:0] pcm_reg; // multi-bit PCM (pulse code modulation) t logic [15:0] pcm_next; // multi-bit PCM (pulse code modulation) t // body // instanciate sin() ROM sin_rom rom_unit ( .clk(clk), .addr_r(p2a_raddr), .dout(amp)); // phase register and output buffer // use an output buffer (to shorten crtical path since the o/p feeds dac) // always_ff @(posedge clk) // pcm_reg <= modu[29:14]; always_ff @(posedge clk, posedge reset) begin if (reset) begin phase_reg <= 0; pcm_reg <= 0; end else begin phase_reg <= phase_next; pcm_reg <= pcm_next; end end // frequency modulation assign freq_control_word = freq_carrier_ctrl_word + freq_offset_ctrl_word; // phase accumulation assign phase_next = phase_reg + freq_control_word; assign pcm_next = modulation[29:14]; // phase modulation assign phase_control_word = phase_reg + phase_offset; // phase to amplitude mapping address assign p2a_raddr = phase_control_word[PHASE_ACC_WIDTH-1:PHASE_ACC_WIDTH-8]; // 8 bits // amplitude modulation envelop in Q2.14 // * -1 < env < +1 (between 1100...00 and 0100...00) // * Q16.0 * Q2.14 => modu is Q18.14 // * convert modu back to Q16.0 assign modulation = $signed(envelope) * $signed(amp); assign pcm_out = pcm_reg; assign pulse_out = phase_reg[PHASE_ACC_WIDTH-1]; endmodule
Elaborated design by VIvado 2022.2
Test Bench SystemVerilog Code
`timescale 1ns / 1ps module ddfs_tb; localparam T=20; localparam PHASE_ACC_WIDTH = 30; logic reset; logic clk; logic [PHASE_ACC_WIDTH-1:0] freq_carrier_ctrl_word; // frequency control word to generate carrier frequency logic [PHASE_ACC_WIDTH-1:0] freq_offset_ctrl_word; // frequency offset control word logic [PHASE_ACC_WIDTH-1:0] phase_offset; // phase offset logic [15:0] envelope; // Amplitude modulation: envelope, digitized value of A(t) logic [15:0] pcm_out; // pcm signal logic pulse_out; localparam M = (262.0 / 100_000_000.0) * (1 <<PHASE_ACC_WIDTH); // carrier freq 262 Hz Freq sys 100 Mhz, M = fout/fsys * 2^N // clk signal generation always begin clk <= 1'b1; #(T/2); clk <= 1'b0; #(T/2); end // instanciate ddfs ddfs uut(.clk(clk), .reset(reset), .freq_carrier_ctrl_word(freq_carrier_ctrl_word), .freq_offset_ctrl_word(freq_offset_ctrl_word), .phase_offset(phase_offset), .envelope(envelope), .pcm_out(pcm_out), .pulse_out(pulse_out) ); initial begin reset = 1'b1; #40; reset = 1'b0; freq_carrier_ctrl_word =M; freq_offset_ctrl_word = 0; phase_offset =0; envelope= 16'd1; #1000000000; end endmodule
Test Bench Scope
One-bit delta-sigma DAC
In order to convert the PCM signal to a true analog signal we need a DAC (digital-to-analog converter) and a low-pass-filter.
We will use a one-bit-delta-sigma DAC, which generates PDM (pulse modulation) output. This DAC can be realized in pure digital logic without any analog component.
The frequency of the system clock is 100MHz and the frequency of the generated audio signal is around 20kHz.
Delta-Sigma DACs are actually high-speed single-bit DACs. Using digital feedback, a string of pulses is generated. The average duty cycle of the pulse string is proportional to the value of the binary input. The analog signal is created by passing the pulse string through an analog low-pass filter.
Conceptual Design
Conceptual block diagram of a one-bit delta-sigma DAC.
It consists of a one-bit delta-sigma modulation circuit and a one bit ADC. The term, Delta-Sigma, refers to the arithmetic difference and sum, respectively.
The PCM input is in the 16-bit unsigned integer format. from 0x0000 to 0xffff. Data is expanded to 17 bits internally.
The Sigma Accumulator is the main art of the DAC, composed of an adder and a register. It continuously adds the input PCM data samples.
If the accumulation exceeds the maximum value of 0xffff, the PDM pulse becomes '1' and the amount of 0x1_0000 is subtracted from the accumulation.
The one-bit ADC converts logic '0' and '1' into two PC values of 0x0_0000 and 0x1_0000.
More high pulses will be generated if the PCM amplitude is larger.
One-bit Delta Sigma SystemVerilog Implementation
The comparator can be eliminated, we can use the MSB as the output.
The subtractor also can be eliminated. We can simply append a 0 to the 16 LSBs and use it as the feedback value.
The output of the DDFS lookup table is converted from a 16-bit signed format to a 17-bit unsigned format. It is first sign-extended to 17 bits and then added a bias of 0x0_ 8000
`timescale 1ns / 10ps module ds_1bit_dac #(parameter W = 16) // input width ( input logic clk, input logic reset, input logic [W-1:0] pcm_in, output logic pdm_out ); // signal declarations localparam BIAS = 2 ** (W-1); // {1'b1, (W-2){1'b0}}; logic [W:0] pcm_biased; logic [W:0] acc_next; logic [W:0] acc_reg; // shift the range from [-2^(W-1) -1 , 2^(W-1)-1] to [0, 2^W-1] assign pcm_biased = {pcm_in[W - 1], pcm_in} + BIAS; // signal treated as unsigned number in delta-sigma modulation assign acc_next = {1'b0, acc_reg[W-1:0]} + pcm_biased; // accumulation register always_ff @(posedge clk, posedge reset) begin if (reset) begin acc_reg <= 0; end else begin acc_reg <= acc_next; end end assign pdm_out = acc_reg[W]; endmodule
Elaborated design by VIvado 2022.2
SystemVerilog Test Bench Implementation
`timescale 1ns / 10ps module ds_1bit_dac_tb( ); localparam T = 20; localparam W = 16; localparam PHASE_ACC_WIDTH = 30; //localparam M = 30'h0AFD; // (262.0 / 100_000_000.0) * (1 <<PHASE_ACC_WIDTH); // carrier freq 262 Hz Freq sys 100 Mhz, M = fout/fsys * 2^N //localparam M = 30'((262.0 / 100_000_000.0) * (1 <<PHASE_ACC_WIDTH)); localparam M = 30'h0AFD; // signal declaration logic clk; logic reset; logic pdm_out; logic [PHASE_ACC_WIDTH-1:0] freq_carrier_ctrl_word; // frequency control word to generate carrier frequency logic [PHASE_ACC_WIDTH-1:0] freq_offset_ctrl_word; // frequency offset control word logic [PHASE_ACC_WIDTH-1:0] phase_offset; // phase offset logic [15:0] amp; // Amplitude modulation: envelope, digitized value of A(t) logic [15:0] pcm; // pcm signal logic pulse_out; // instantiate ds 1bit dac ds_1bit_dac #(.W(W)) ds_1bit_uut( .clk(clk), .reset(reset), .pcm_in(pcm), .pdm_out(pdm_out) ); // instanciate ddfs ddfs #( .PHASE_ACC_WIDTH(PHASE_ACC_WIDTH)) ddfs_uut (.clk(clk), .reset(reset), .freq_carrier_ctrl_word(freq_carrier_ctrl_word), .freq_offset_ctrl_word(freq_offset_ctrl_word), .phase_offset(phase_offset), .envelope(amp), .pcm_out(pcm), .pulse_out(pulse_out) ); // registers always_ff @(posedge clk, posedge reset) if (reset) begin freq_carrier_ctrl_word <= 0; freq_offset_ctrl_word <= 0; phase_offset <= 0; amp <= 16'h4000; // 1.00 end else begin freq_carrier_ctrl_word <= M; freq_offset_ctrl_word <= 0; phase_offset <= 0; amp = 16'h4000; end // clock 20 ns clock running for ever always begin clk <= 1'b1; #(T/2); clk <= 1'b0; #(T/2); end // reset for the first cycle initial begin reset = 1'b1; #(T/2); reset = 1'b0; end initial begin // @(negedge reset); // wait reset to deasssert @(negedge clk); // wait for one clock #200; reset = 1'b0; #(T/2); reset = 1'b1; #(T/2); reset = 1'b0; #(T/2); reset = 1'b1; #(T/2); reset = 1'b0; #(T/2); reset = 1'b1; #(T/2); reset = 1'b0; #(T/2); reset = 1'b1; #(T/2); reset = 1'b0; #200; $stop; end endmodule
Test bench scope
More information on One-bit delta-sigma DAC: https://china.xilinx.com/content/dam/xilinx/support/documents/ip_documentation/xps_deltasigma_dac.pdf
Low Pass Filter
A simple passive RC low-pass filter is adequate for most applications. A 16mA LVTTL output buffer is used to provide maximum current drive.
There are three primary considerations in choosing values for the resistor and capacitor:
- Output Source and Sink Current: Unlike normal digital applications, it is important that signal DACout always switch the entire voltage range from 0 V to VCCO (rail-to-rail). If the value of R is too low and signal DACout can not switch rail-to-rail, the analog output is non-linear; i.e., the absolute output voltage change resulting from incrementing or decrementing DACin is not constant.
- Load Impedance: Keep the value of R low relative to the impedance of the load so that the current change through the capacitor due to loading becomes negligible.
- Time Constant: The filter time constant (τ = RC) must be high enough to greatly attenuate the individual pulses in the pulse string. On the other hand, a high time constant may also attenuate the desired low-frequency output signal.
Testing with the Spartan 7 FPGA with the Digilent Arty S7
Constraints file
https://github.com/Digilent/digilent-xdc/blob/master/Arty-S7-50-Master.xdc
## This file is a general .xdc for the Arty S7-50 Rev. E ## Clock Signals set_property -dict {PACKAGE_PIN R2 IOSTANDARD SSTL135} [get_ports clk] create_clock -period 10.000 -name sys_clk_pin -waveform {0.000 5.000} -add [get_ports clk] ## Switches set_property -dict {PACKAGE_PIN H14 IOSTANDARD LVCMOS33} [get_ports {sw[0]}] set_property -dict {PACKAGE_PIN H18 IOSTANDARD LVCMOS33} [get_ports {sw[1]}] set_property -dict {PACKAGE_PIN G18 IOSTANDARD LVCMOS33} [get_ports {sw[2]}] set_property -dict {PACKAGE_PIN M5 IOSTANDARD SSTL135} [get_ports {sw[3]}] ## LEDs set_property -dict {PACKAGE_PIN E18 IOSTANDARD LVCMOS33} [get_ports {led[0]}] set_property -dict {PACKAGE_PIN F13 IOSTANDARD LVCMOS33} [get_ports {led[1]}] set_property -dict {PACKAGE_PIN E13 IOSTANDARD LVCMOS33} [get_ports {led[2]}] set_property -dict {PACKAGE_PIN H15 IOSTANDARD LVCMOS33} [get_ports {led[3]}] ## Buttons set_property -dict {PACKAGE_PIN G15 IOSTANDARD LVCMOS33} [get_ports {btn[0]}] set_property -dict {PACKAGE_PIN K16 IOSTANDARD LVCMOS33} [get_ports {btn[1]}] set_property -dict {PACKAGE_PIN J16 IOSTANDARD LVCMOS33} [get_ports {btn[2]}] set_property -dict {PACKAGE_PIN H13 IOSTANDARD LVCMOS33} [get_ports {btn[3]}] ## Pmod Header JD set_property -dict { PACKAGE_PIN V15 IOSTANDARD LVCMOS33 } [get_ports { audio_out }]; #IO_L20N_T3_A07_D23_14 Sch=jd1/ck_io[33] set_property DRIVE 16 [get_ports audio_out] set_property -dict { PACKAGE_PIN U12 IOSTANDARD LVCMOS33 } [get_ports { gain }]; #IO_L21P_T3_DQS_14 Sch=jd2/ck_io[32] set_property -dict { PACKAGE_PIN V13 IOSTANDARD LVCMOS33 } [get_ports { pulse_out }]; #IO_L21N_T3_DQS_A06_D22_14 Sch=jd3/ck_io[31] set_property -dict { PACKAGE_PIN T12 IOSTANDARD LVCMOS33 } [get_ports { neg_shutdown }]; #IO_L22P_T3_A05_D21_14 Sch=jd4/ck_io[30] #set_property -dict { PACKAGE_PIN T13 IOSTANDARD LVCMOS33 } [get_ports { jd[4] }]; #IO_L22N_T3_A04_D20_14 Sch=jd7/ck_io[29] #set_property -dict { PACKAGE_PIN R11 IOSTANDARD LVCMOS33 } [get_ports { jd[5] }]; #IO_L23P_T3_A03_D19_14 Sch=jd8/ck_io[28] #set_property -dict { PACKAGE_PIN T11 IOSTANDARD LVCMOS33 } [get_ports { jd[6] }]; #IO_L23N_T3_A02_D18_14 Sch=jd9/ck_io[27] #set_property -dict { PACKAGE_PIN U11 IOSTANDARD LVCMOS33 } [get_ports { jd[7] }]; #IO_L24P_T3_A01_D17_14 Sch=jd10/ck_io[26] ## Configuration options, can be used for all designs set_property BITSTREAM.CONFIG.CONFIGRATE 50 [current_design] set_property CONFIG_VOLTAGE 3.3 [current_design] set_property CFGBVS VCCO [current_design] set_property BITSTREAM.CONFIG.SPI_BUSWIDTH 4 [current_design] set_property CONFIG_MODE SPIx4 [current_design] ## SW3 is assigned to a pin M5 in the 1.35v bank. This pin can also be used as ## the VREF for BANK 34. To ensure that SW3 does not define the reference voltage ## and to be able to use this pin as an ordinary I/O the following property must ## be set to enable an internal VREF for BANK 34. Since a 1.35v supply is being ## used the internal reference is set to half that value (i.e. 0.675v). Note that ## this property must be set even if SW3 is not used in the design. set_property INTERNAL_VREF 0.675 [get_iobanks 34]
We will connect the RC LPF to the JD Pmod pin1 and the pulse out signal to the JD Pmod pin 3
Warning: Since the Pmod pins are connected to Spartan-7 FPGA pins using a 3.3V logic standard, care should be taken not to drive these pins over 3.4V.
Pmod JA iand Pmod JB are a High-Speed Pmod: The High-speed Pmods use the standard Pmod connector, but have their data signals routed as impedance matched differential pairs for maximum switching speeds. They have pads for loading resistors for added protection, but the Arty S7 ships with these loaded as 0-Ohm shunts. With the series resistors shunted, these Pmods offer no protection against short circuits, but allow for much faster switching speeds. The signals are paired to the adjacent signals in the same row: pins 1 and 2, pins 3 and 4, pins 7 and 8, and pins 9 and 10. Traces are routed 100 ohm (+/- 10%) differential.
These connectors should be used only when high speed differential signaling is required or the other Pmods are all occupied. If used as single-ended, coupled pairs may have significant crosstalk. In applications where this is a concern, the standard Pmod connector shall be used. Another option would be to ground one of the signals (drive it low from the FPGA) and use its pair for the signal-ended signal.
Since the High-Speed Pmods have 0-ohm shunts instead of protection resistors, the operator must take precaution to ensure that they do not cause any shorts.
System test module in SystemVerilog
`timescale 1ns / 10ps module checkDDFSModule( input clk, input logic [3:0] sw, input logic [3:0] btn, // PMOD 1 output audio_out, // PMOD1_PIN1_R, audio out output gain, // PMOD1_PIN2_R, // Gain output pulse_out, output neg_shutdown, // ~SHUTDOWN output led[3:0] ); localparam T = 2; localparam W = 16; localparam PHASE_ACC_WIDTH = 30; localparam M1 = 30'h0AFD; // (262.0 / 100_000_000.0) * (1 <<PHASE_ACC_WIDTH); // carrier freq 262 Hz Freq sys 100 Mhz, M = fout/fsys * 2^N localparam M2 = 30'h0C54; // (277.0 / 100_000_000.0) * (1 <<PHASE_ACC_WIDTH); // carrier freq 277 Hz Freq sys 100 Mhz, M = fout/fsys * 2^N localparam M3 = 30'h0DD7; // (294.0 / 100_000_000.0) * (1 <<PHASE_ACC_WIDTH); // carrier freq 294 Hz Freq sys 100 Mhz, M = fout/fsys * 2^N localparam M4 = 30'hEAE; // (311.0 / 100_000_000.0) * (1 <<PHASE_ACC_WIDTH); // carrier freq 311 Hz Freq sys 100 Mhz, M = fout/fsys * 2^N logic [PHASE_ACC_WIDTH-1:0] freq_carrier_ctrl_word = M1; logic [PHASE_ACC_WIDTH-1:0] freq_offset_ctrl_word = 30'h0; logic [PHASE_ACC_WIDTH-1:0] phase_offset = 30'h0; //logic [15:0] amp = 16'h4000; // Q2.14 format -1.0 to 1.0 logic [15:0] amp = 16'h2FFF; // Q2.14 format -1.0 to 1.0 logic reset; logic [PHASE_ACC_WIDTH-1:0] note; assign neg_shutdown = sw[0]; // control sound on off with switch B0 assign gain = sw[1]; assign reset = btn[0]; logic [15:0] pcm; // pcm signal logic [32:0] counter; logic [32:0] counter_next; always_comb begin case (btn[3:2]) 2'b00: note = M1; 2'b01: note = M2; 2'b10: note = M3; 2'b11: note = M4; endcase end // registers always_ff @(posedge clk, posedge reset) begin if (reset) begin freq_carrier_ctrl_word <= 0; freq_offset_ctrl_word <= 0; phase_offset <= 0; amp <= 16'h4000; // 1.00 counter <=0; end else begin freq_carrier_ctrl_word <= note; freq_offset_ctrl_word <= 0; phase_offset <= 0; amp <= 16'h4000; counter <= counter_next; end end assign counter_next = counter +1; assign led[0] = counter[24]; assign led[1] = pcm; // instanciate ddfs ddfs #( .PHASE_ACC_WIDTH(30)) ddfs_uut (.clk(clk), .reset(reset), .freq_carrier_ctrl_word(freq_carrier_ctrl_word), .freq_offset_ctrl_word(freq_offset_ctrl_word), .phase_offset(phase_offset), .envelope(amp), .pcm_out(pcm), .pulse_out(pulse_out) ); // instantiate ds 1bit dac ds_1bit_dac #(.W(16)) ds_1bit_uut( .clk(clk), .reset(reset), .pcm_in(pcm), .pdm_out(audio_out) ); endmodule
Checking the signals generated with the oscilloscope.
I don't have an oscilloscope with enough bandwidth to view the digital signals from the DDFS but we can view the analog signals.
Sine waveform and the pulse out analog signals.
FFT
Logic Analyzer- PDM output
The logic analyzer does not have the necessary bandwidth but something is intuited.
Generation of some notes in the 4th octave. I really don't really know what that means. My musical knowledge is something non-existent.
4th octave- C note 261.6 Hz
4th octave- D note 293.7 Hz
4th octave- E note 329.6 Hz
4th octave- F note 349.2 Hz
Combining the 4 waves
Generating a Triangle Wave
The good thing about this DDFS is that we can generate any analog waveform just by changing the table loaded in the ROM.
Later in other blogs we will generate waveforms of various instruments.
ROM Triangle Wave Table
0000 0200 0400 0600 0800 0A00 0C00 0E00 1000 1200 1400 1600 1800 19FF 1BFF 1DFF 1FFF 21FF 23FF 25FF 27FF 29FF 2BFF 2DFF 2FFF 31FF 33FF 35FF 37FF 39FF 3BFF 3DFF 3FFF 41FF 43FF 45FF 47FF 49FE 4BFE 4DFE 4FFE 51FE 53FE 55FE 57FE 59FE 5BFE 5DFE 5FFE 61FE 63FE 65FE 67FE 69FE 6BFE 6DFE 6FFE 71FE 73FE 75FE 77FE 79FD 7BFD 7DFD 7FFD 7DFD 7BFD 79FD 77FE 75FE 73FE 71FE 6FFE 6DFE 6BFE 69FE 67FE 65FE 63FE 61FE 5FFE 5DFE 5BFE 59FE 57FE 55FE 53FE 51FE 4FFE 4DFE 4BFE 49FE 47FF 45FF 43FF 41FF 3FFF 3DFF 3BFF 39FF 37FF 35FF 33FF 31FF 2FFF 2DFF 2BFF 29FF 27FF 25FF 23FF 21FF 1FFF 1DFF 1BFF 19FF 1800 1600 1400 1200 1000 0E00 0C00 0A00 0800 0600 0400 0200 0000 FE00 FC00 FA00 F800 F600 F400 F200 F000 EE00 EC00 EA00 E800 E601 E401 E201 E001 DE01 DC01 DA01 D801 D601 D401 D201 D001 CE01 CC01 CA01 C801 C601 C401 C201 C001 BE01 BC01 BA01 B801 B602 B402 B202 B002 AE02 AC02 AA02 A802 A602 A402 A202 A002 9E02 9C02 9A02 9802 9602 9402 9202 9002 8E02 8C02 8A02 8802 8603 8403 8203 8003 8203 8403 8603 8802 8A02 8C02 8E02 9002 9202 9402 9602 9802 9A02 9C02 9E02 A002 A202 A402 A602 A802 AA02 AC02 AE02 B002 B202 B402 B602 B801 BA01 BC01 BE01 C001 C201 C401 C601 C801 CA01 CC01 CE01 D001 D201 D401 D601 D801 DA01 DC01 DE01 E001 E201 E401 E601 E800 EA00 EC00 EE00 F000 F200 F400 F600 F800 FA00 FC00 FE00
Graphic representation of the data table values:
Triangle waveforms on the scope
Pulse out and triangle waveform and FFT
Filtered and unfiltered DAC out
Here we can see the effect of the RC LFP
and finally we connect the system with an amplified speaker.
The two push buttons on the right change the frequency to another note.
See the video at the beginning of the blog.
Next steps
This is the first blog dedicated to generating sounds with the Spartan-7 FPGA within the SystemVerilog Study Notes series that I'm posting on element14.
The next steps will be to create a driver for Microblaze and design an ADSR attack-delay-sustain-release module.
SystemVerilog Study Notes Chapters
- Gate-Level Combinational Circuit
- RTL Combinational Circuit Operators
- RTL Combinational Circuit - Concurrent and Control Constructs
- Hex-Digit to Seven-Segment LED Decoder RTL Combinational Circuit
- Barrel Shifter RTL Combinational Circuit
- Simplified Floating Point Arithmetic. RTL Combinational Circuit
- BCD Number Format. RTL Combinational Circuit
- DDFS. Direct Digital Frequency Synthesis for Sound
- FPGA ADSR envelope generator for sound synthesis
- AMD Xilinx 7 series FPGAs XADC
- Building FPGA-Based Music Instrument Synthesis: A Simple Test Bench Solution
Top Comments