The DSX1000 is a first-order Delta-Sigma (“1-bit”) Digital-to-Analog converter.
A complete description and example follows below.
Benefits
While a first-order Delta-Sigma DAC isn't exactly audiophile quality, that might not matter depending on your goals. It's an easy and fun way to get started and learn more, for one thing. Also, while the sound quality could matter for audio reproduction, when it comes to music synthesis, you have much more freedom. Any “imperfections” can be used to your advantage and add character and uniqueness to your sound. Case in point: The MOS6581 'SID' chip and PhoenixSID 65x81
The example “test harness” includes a sawtooth-wave tone generator for your instant gratification . From the example you should be able to synthesize the hardware, download to your FPGA and hear an 'A' note (440Hz) on your speaker (assuming CLK_HZ is set correctly).
Block Diagram of a First Order Digital Delta Sigma Modulator (Thanks to Uwe Beis)
The part created on the FPGA/CPLD is the Delta-Sigma Modulator shown above. Adding the external analog filter makes it a DAC. This external low-pass filter can be as simple as a single resistor and capacitor (RC filter)
More Information
Uwe Beis, "An Introduction to Delta Sigma Converters"
Free for non-commercial use. Please mention my name, website and contribution(s) in your documentation. For other license arrangements, please contact me.
Have a great time and feel free to write me – George Pantazopoulos
Clock Speed
When it comes to Delta-Sigma converters, faster is better! Even 1MHz can sound ok, but I'd recommend at least an 8MHz clock, and 16MHz or more is better. Assuming a PCM sample rate of 44.1KHz, a 16MHz DAC clock provides ∼360X oversampling.
Even if you don't want to run your other logic at that speed, the main clock can be divided down for the logic (eg. using a counter) and you can keep the fast clock for the DAC. As long as the slower clock is derived from the faster one you should be ok.
External Analog low-pass RC filter
I don't have an Altera-based board just yet. If you see a decent, affordable Cyclone-II board, please let me know!
Or test out the SDX1000 on your own board and let me know how it went
The Xylo series from fpga4fun.com look interesting to me.
""" dac_dsx1000_hd.py DSX1000 core hardware description First-order Delta-Sigma DAC with variable bit-width MyHDL implementation by George Pantazopoulos http://www.gammaburst.net Nov 2006 """ from myhdl import * from math import log, ceil # ---------------------------------------------------------------------------- def dac_dsx1000(clk_i, rst_i, pcm_i, pdm_o): """ GJP Enterprises DSX1000 Multiplatform First-order Delta-Sigma DAC core with variable bit width. Inputs: clk_i clock (The faster, the better!) rst_i active high reset pcm_i digital PCM value to convert to PDM stream (adjustable width) Outputs: pdm_o Pulse-density modulated bitstream. Range [0,1] Note: The bit resolution of this converter depends on the width of the PCM input Analog output ------------- To complete the DAC, an external RC analog low-pass filter at the output is usually needed. See XAPP154 for more information. Verification ------------ Tested successfully on the Digilent Spartan3 dev board with Xilinx Spartan3 FPGA XC3S1000-4FT256 Tools used: MyHDL 0.5.1 Xilinx WebPACK ISE 8.1.03i Python 2.4.3 (cygwin) Eclipse IDE w/PyDev and PyDev Extensions License ------- Free for non-commercial use. Please mention my name, website and contribution(s) in your documentation. For other license arrangements, please contact me. References ---------- Uwe Beis, http://www.beis.de/Elektronik/DeltaSigma/DeltaSigma.html Xilinx Application Note XAPP154. """ __author__ = "George Pantazopoulos http://www.gammaburst.net" __version__ = "1.0.2" __revision__ = "" __date__ = "14 Nov 2006" RES = len(pcm_i) DREF_NEG = 0 DREF_POS = (2**RES)-1 MIN = -2**(RES-1) MAX = +2**(RES-1) diff_o = Signal(intbv(0, min=4*MIN, max=4*MAX)) adder_o = Signal(intbv(0, min=4*MIN, max=4*MAX)) reg_o = Signal(intbv(0, min=4*MIN, max=4*MAX)) comp_o = Signal(bool(0)) ddc_o = Signal(intbv(0, min=4*MIN, max=4*MAX)) # Difference block ("Delta") @always_comb def Difference(): diff_o.next = pcm_i - ddc_o # The adder and register blocks comprise the integrator ("Sigma") # Adder block @always_comb def Adder(): adder_o.next = diff_o + reg_o # Register block @always(clk_i.posedge) def Reg(): if rst_i: reg_o.next = 0 else: reg_o.next = adder_o # Comparator block @always_comb def Comparator(): if reg_o > 0: comp_o.next = 1 else: comp_o.next = 0 # 1-bit Digital-digital conveter # Creates a signed value from the comparator output @always_comb def DDC(): if comp_o: ddc_o.next = DREF_POS else: ddc_o.next = DREF_NEG # Bitstream output @always(clk_i.posedge) def BitStreamOut(): if rst_i: pdm_o.next = 0 else: pdm_o.next = comp_o return instances()
""" dac_dsx1000_syn.py DSX1000 test harness and synthesis-related code MyHDL implementation by George Pantazopoulos http://www.gammaburst.net Nov 2006 """ from myhdl import * from math import log, ceil from dac_dsx1000_hd import dac_dsx1000 # ---------------------------------------------------------------------------- def SawWaveGen(clk_i, rst_i, fval_i, pcm_o, ACCUM_WIDTH=24): """ Sawtooth wave generator with adjustable frequency Inputs: ------- clk_i - clock rst_i - active-high reset fval_i - Frequency Value (see below) ACCUM_WIDTH - Width of the Phase Accumulator (constant) Outputs: -------- pcm_o - PCM waveform output (variable bit-width) Calculation of fval_i: ---------------------- fval_i = Desired Freq (Hz) * pow(2,ACCUM_WIDTH) -------------------------------------- Fclk (Hz) Contact ------- George Pantazopoulos http://www.gammaburst.net """ __author__ = "George Pantazopoulos http://www.gammaburst.net" __version__ = "1.0.1" __revision__ = "" __date__ = "15 Nov 2006" OUTPUT_WIDTH = len(pcm_o) # Phase Accumulator accum = Signal(intbv(0)[ACCUM_WIDTH:]) # At each clock, increment the accumulator by the value of fval_i @always(clk_i.posedge) def accumDrive(): if rst_i: accum.next = 0 else: accum.next = (accum + fval_i) % 2**ACCUM_WIDTH # Take the top N significant bits of the accumulator as the PCM output @always_comb def pcmOut(): pcm_o.next = accum[ACCUM_WIDTH:ACCUM_WIDTH-OUTPUT_WIDTH] return instances() # ---------------------------------------------------------------------------- def top(clk, pdm_o, CLK_HZ): """ Top-level module. Test Harness for DSX1000 Delta-Sigma DAC """ __author__ = "George Pantazopoulos http://www.gammaburst.net" __version__ = "1.0.1" __revision__ = "" __date__ = "15 Nov 2006" # Desired tone frequency OUTPUT_FREQ_HZ = 440 # Tone-generator-specific parameter ACCUM_WIDTH = 24 # Compute the required fval for the SawWaveGen FCLK = CLK_HZ FVAL = int(ceil(OUTPUT_FREQ_HZ * pow(2,ACCUM_WIDTH)) / float(FCLK)) print "CLK_HZ = ", CLK_HZ print "OUTPUT_FREQ_HZ = ", OUTPUT_FREQ_HZ print "FVAL = ", FVAL # Reset signal rst = Signal(bool(0)) # Dummy reset driver @always_comb def rstDrv(): rst.next = False # PCM Audio signal. # Note that we control the (theoretical) resolution of the converter # by setting the bit width of this signal. DAC_RESOLUTION = 8 pcm_audio = Signal(intbv(0)[DAC_RESOLUTION:]) # PCM Audio source (Saw wave generator) PCM_SRC = SawWaveGen(clk_i=clk, rst_i=rst, fval_i=FVAL, pcm_o = pcm_audio, ACCUM_WIDTH=ACCUM_WIDTH) # Instantiation of our Delta-Sigma DAC DAC = dac_dsx1000(clk_i=clk, rst_i=rst, pcm_i=pcm_audio, pdm_o=pdm_o) return instances() # ---------------------------------------------------------------------------- # Synthesis support # Set this to your FPGA's clock frequency CLK_HZ = 16000000 # Signals connected to the FPGA/CPLD clk = Signal(bool(0)) pdm_o = Signal(bool(0)) # MyHDL to Verilog conversion toVerilog.name = "dsx1000" toVerilog(top, clk, pdm_o, CLK_HZ)
Unit testing each component gives valuable piece of mind, especially when many such components are put together in a larger design or when you haven't touched it in a while .
Even though the Delta-Sigma converter is a dynamic system that really ought to be verified in hardware, we can still do some basic tests at the MyHDL level that will give us confidence that our design is correct.
test_static()
In test_static(), we pick some reference output values in the range [0,1]. For each reference value we let the (simulated) DAC run for a while and then compute the average number of 1's in the bitstream. The simulated output needs to be close enough (within an error bound) for that test to pass.
TODO: Make the MyHDL-based unit test automated. I'm almost there…
""" dac_dsx1000_ut.py DSX1000 MyHDL unit tests MyHDL implementation by George Pantazopoulos http://www.gammaburst.net Nov 2006 """ from math import pow, log, ceil from myhdl import * from dac_dsx1000_hd import dac_dsx1000 # ---------------------------------------------------------------------------- def bench_static(): """ Setup the DAC and check its output against a list of static values Since we are testing a Delta-Sigma modulator, we can take the average value of its output bitstream and use that to verify basic functionality. """ __author__ = "George Pantazopoulos http://www.gammaburst.net" __version__ = "1.1.0" __revision__ = "" __date__ = "15 Nov 2006" # DAC resolution DAC_RES = 16 # Signal setup clk = Signal(bool(0)) dat_i = Signal(intbv(0, min=0, max=2**DAC_RES)) pdm_o = Signal(bool(0)) # Clock generator definition @always(delay(10)) def clkgen(): clk.next = not clk # Instance of the device under test dac_inst = dac_dsx1000(clk_i=clk, rst_i=False, pcm_i=dat_i, pdm_o=pdm_o) # Allowable Error bound percentage (+/-) ERROR_BOUND = 0.02 INTEGRATION_TIME = 512 # List of DAC input values to test (relative to full-scale) refvals = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] # Generate the actual DAC codes based on the DAC resolution dac_codes = list() for value in refvals: dac_code = int(value * 2**DAC_RES) if dac_code == 2**DAC_RES: dac_code = 2**DAC_RES-1 elif dac_code > 2**DAC_RES: raise Exception dac_codes.append(dac_code) def print_banner(): print print "bench_static()" + " (version " + __version__ + ")" print "--------------" + "----------" + "-----" + "-" print print "DAC Resolution (bits) = ", DAC_RES print "INTEGRATION_TIME (clocks)= ", INTEGRATION_TIME print "ERROR_BOUND +/- %s" % str(ERROR_BOUND * 100.0) + " %" def make_table_head(): s = "-" * 75 + '\n' + \ "Reference value".ljust(17) + \ "DAC Code".ljust(10) + \ "PDM Avg.".ljust(10) + \ "Allowed range".ljust(17) + \ "Status".rjust(17) + \ '\n' + "-" * 75 return s def make_table_row(rvs, dcs, avs, ars, sts): return str(rvs).ljust(17,'.') + \ str(dcs).ljust(10,'.') + \ str(avs).ljust(10,'.') + \ ars.ljust(17,'.') + \ sts.rjust(17,'.') @instance def stimulus(): """ Drive the DAC with each DAC code (runs in parallel with monitor()) """ for dac_code in dac_codes: for i in range(INTEGRATION_TIME): yield clk.negedge dat_i.next = dac_code yield clk.posedge raise StopSimulation @instance def monitor(): """ Check the average value of the DAC bitstream output (runs in parallel with stimulus()) """ print_banner() if ERROR_BOUND < 0.0 or ERROR_BOUND > 1.0: raise Exception, "ERROR_BOUND must be in range [0,1]" print print make_table_head() # For each DAC code to test for i in range(len(dac_codes)): dac_code = dac_codes[i] refval = refvals[i] accum = 0 avg = 0.0 # Count the number of 1's in the bitstream during a given # run length. for j in range(INTEGRATION_TIME): yield clk.posedge accum += pdm_o #print pdm_o # Compute the average value of the bitstream # Range should be in [0..1] avg = float(accum) / float(INTEGRATION_TIME) # Calculate the allowable range upper_bound = min(1.0, refval + ERROR_BOUND) lower_bound = max(0.0, refval - ERROR_BOUND) # Check that it falls within the allowable error bound. if avg >= lower_bound and avg <= upper_bound: in_range = True else: in_range = False dac_code_str = "0x%06X" % dac_code refval_str = "%.4f" % refval avg_str = "%.4f" % avg allowed_range_str = "[%.4f..%.4f]" % (lower_bound, upper_bound) if in_range: status_str = "PASS" else: status_str = "FAIL" print make_table_row(refval_str, dac_code_str, avg_str, allowed_range_str, status_str) return clkgen, dac_inst, stimulus, monitor # ---------------------------------------------------------------------------- def test_static(): sim = Simulation(bench_static()) sim.run() if __name__ == '__main__': test_static()
Unit test output for test_static():
$ python dac_dsx1000_ut.py bench_static() (version 1.1.0) ------------------------------ DAC Resolution (bits) = 16 INTEGRATION_TIME (clocks)= 512 ERROR_BOUND +/- 2.0 % --------------------------------------------------------------------------- Reference value DAC Code PDM Avg. Allowed range Status --------------------------------------------------------------------------- 0.0000...........0x000000..0.0000....[0.0000..0.0200]..............PASS 0.1000...........0x001999..0.0996....[0.0800..0.1200]..............PASS 0.2000...........0x003333..0.1992....[0.1800..0.2200]..............PASS 0.3000...........0x004CCC..0.3008....[0.2800..0.3200]..............PASS 0.4000...........0x006666..0.3984....[0.3800..0.4200]..............PASS 0.5000...........0x008000..0.5000....[0.4800..0.5200]..............PASS 0.6000...........0x009999..0.5996....[0.5800..0.6200]..............PASS 0.7000...........0x00B333..0.6992....[0.6800..0.7200]..............PASS 0.8000...........0x00CCCC..0.7988....[0.7800..0.8200]..............PASS 0.9000...........0x00E666..0.9004....[0.8800..0.9200]..............PASS 1.0000...........0x00FFFF..1.0000....[0.9800..1.0000]..............PASS
(autogenerated from dac_dsx1000_syn.py by MyHDL 0.5.1)
module dsx1000 ( clk, pdm_o ); input clk; output pdm_o; reg pdm_o; wire rst; wire [7:0] pcm_audio; reg signed [9:0] _DAC_ddc_o; reg _DAC_comp_o; reg signed [9:0] _DAC_reg_o; wire signed [9:0] _DAC_diff_o; wire signed [9:0] _DAC_adder_o; reg [23:0] _PCM_SRC_accum; always @(posedge clk) begin: _dsx1000_PCM_SRC_accumDrive if (rst) begin _PCM_SRC_accum <= 0; end else begin _PCM_SRC_accum <= ((_PCM_SRC_accum + 461) % (2 ** 24)); end end assign pcm_audio = _PCM_SRC_accum[24-1:(24 - 8)]; always @(posedge clk) begin: _dsx1000_DAC_Reg if (rst) begin _DAC_reg_o <= 0; end else begin _DAC_reg_o <= _DAC_adder_o; end end assign _DAC_diff_o = ($signed({1'b0, pcm_audio}) - _DAC_ddc_o); assign _DAC_adder_o = (_DAC_diff_o + _DAC_reg_o); always @(posedge clk) begin: _dsx1000_DAC_BitStreamOut if (rst) begin pdm_o <= 0; end else begin pdm_o <= _DAC_comp_o; end end always @(_DAC_comp_o) begin: _dsx1000_DAC_DDC if (_DAC_comp_o) begin _DAC_ddc_o <= 255; end else begin _DAC_ddc_o <= 0; end end always @(_DAC_reg_o) begin: _dsx1000_DAC_Comparator if ((_DAC_reg_o > 0)) begin _DAC_comp_o <= 1; end else begin _DAC_comp_o <= 0; end end assign rst = 0; endmodule