From Python to silicon
 

DSX1000 ΔΣ DAC Core

The DSX1000 is a first-order Delta-Sigma (“1-bit”) Digital-to-Analog converter.

A complete description and example follows below.

Benefits

  • Simple
  • Easy to implement
  • Easy to learn from
  • Uses small amount of logic resources
  • Only a single resistor and capacitor needed off-chip!
  • Verified on FPGA using MyHDL's Verilog output (VHDL coming soon)
  • You get to play with analog electronics
  • Makes cool sounds

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 8-)

The example “test harness” includes a sawtooth-wave tone generator for your instant gratification 8-). 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).

ΔΣ DAC Overview

Block Diagram of a First Order Digital Delta Sigma Modulator (Thanks to Uwe Beis)

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"

Xilinx XAPP154 Version 1.1, Virtex(tm) Synthesizable Delta-Sigma DAC (PDF). See section about the external RC filter

fpga4fun.com, "PWM and one-bit DAC"

Delta-Sigma Modulation (Wikipedia article)

Contact and License Info

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

:projects:gp_email.png

Tips

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

  • Connect to bitstream (PDM) output
  • Only a single resistor and capacitor needed
  • Good RC values to start with (for audio):
    • R = 3.3KΩ
    • C = 2nF

Xilinx Hardware Implementations

Xilinx Spartan 3 FPGA implementation

Xilinx Spartan 3 FPGA implementation

Altera Hardware Implementations

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 8-)

The Xylo series from fpga4fun.com look interesting to me.

MyHDL Hardware Description

"""                                                                            
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 Tests

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 :-D.

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

Verilog Output

(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
projects/dsx1000.txt · Last modified: 2006/11/20 15:38 by themaxx
 
Except where otherwise noted, content on this wiki is licensed under the following license: CC Attribution-Share Alike 3.0 Unported
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki