\title{myHDL 1Bit $\Delta\Sigma$ Pulse Density Modulated Digital to Analog Converter} \author{Steven K Armour} \maketitle
This is revamp of the Original myHDL $\Delta\Sigma$ DAC Core done by the late George Pantazopoulos
@article{cheung_raj_2005, title={Implementation of 12-bit delta-sigma DAC with MSC12xx controller}, volume={Q1}, url={http://www.ti.com/lit/an/slyt076/slyt076.pdf}, journal={Analog Applications Journal}, author={Cheung, Hugo and Raj, Sreeja}, year={2005}, pages={27-32} },
@misc{pantazopoulos_2006, title={DSX1000 ?S DAC Core [MyHDL]}, url={http://old.myhdl.org/doku.php/projects:dsx1000}, author={Pantazopoulos, George}, year={2006} }
In [1]:
from myhdl import *
import pandas as pd
from myhdlpeek import Peeker
import numpy as np
In [2]:
#helper functions to read in the .v and .vhd generated files into python
def VerilogTextReader(loc, printresult=True):
with open(f'{loc}.v', 'r') as vText:
VerilogText=vText.read()
if printresult:
print(f'***Verilog modual from {loc}.v***\n\n', VerilogText)
return VerilogText
def VHDLTextReader(loc, printresult=True):
with open(f'{loc}.vhd', 'r') as vText:
VerilogText=vText.read()
if printresult:
print(f'***VHDL modual from {loc}.vhd***\n\n', VerilogText)
return VerilogText
In [3]:
BitWidth=16
#the max in excluded in intbv
MaxV=int(2**(BitWidth-1)); MinV=-int(2**(BitWidth-1))
a=intbv(0)[BitWidth:]; a=a.signed()
len(a), a.min, MinV, a.max, MaxV
Out[3]:
The Delta ($\Delta$) section takes in the input Digital word $x(n)$ signal and subtracts from the input word the feedback word which is
$$\Delta(n)=x(n)-\begin{cases} 2^{W}-1, & \text{if}\ y(n)=1 \\ 0, & \text{if}\ y(n)=0 \end{cases}$$where $y(n)$ is the output one bit from the DAC that after leaving the digital device still needs to processed by a Analog low pass filter. And $W$ is the architecture word length. The conversion from the the 1bit output to a word is done by the DigitalRailScaler and the output is feed to the Integrator ($\Sigma$) section.
In [4]:
x1N=np.random.randint(MinV, MaxV, 20)
x2N=np.random.randint(MinV, MaxV, 20)
ResultsDiff=pd.DataFrame(columns=['x1', 'x2', 'yN'])
ResultsDiff['x1']=x1N; ResultsDiff['x2']=x2N
ResultsDiff['yN']=x1N-x2N
ResultsDiff=ResultsDiff[ResultsDiff['yN']>=MinV]
ResultsDiff=ResultsDiff[ResultsDiff['yN']<=MaxV]
ResultsDiff.reset_index(drop=True, inplace=True)
ResultsDiff
Out[4]:
In [5]:
@block
def Difference(x1, x2, y):
"""
Prototype Delta Section such that `x1` is th input word,
`x2` is the Feedback word and `y` is the ouput to the Sigma Section
Inputs:
x1 (2's): The From
x2 (2's): the suptraction amount
Outputs:
y (2's): x1-x2
"""
@always_comb
def logic():
y.next=x1-x2
return logic
In [6]:
Peeker.clear()
x1=Signal(modbv(0, min=MinV, max=MaxV)); Peeker(x1, 'x1')
x2=Signal(modbv(0, min=MinV, max=MaxV)); Peeker(x2, 'x2')
y=Signal(modbv(0, min=MinV, max=MaxV)); Peeker(y, 'y')
DUT=Difference(x1, x2, y)
def Difference_TB():
for i, Row in ResultsDiff.iterrows():
x1.next=int(Row['x1']); x2.next=int(Row['x2'])
yield (delay(1))
raise StopSimulation
sim=Simulation(DUT, Difference_TB(), *Peeker.instances()).run()
Peeker.to_wavedrom()
In [7]:
ResultsDiff=pd.merge(ResultsDiff, Peeker.to_dataframe().astype(int, copy=False), how='left')
Comp=(ResultsDiff['yN']==ResultsDiff['y']).all()
print(f'Compersion of Sim and myHDL equal: {Comp}')
ResultsDiff
Out[7]:
In [8]:
if Comp:
DUT.convert()
VerilogTextReader('Difference')
The Comparator section performers the same actions as would a Analog Computer does. It takes a input and compares that values to a reference where if the input is greater then the reference then the output is 1 else 0. Thus the Comparator performs a 1Bit quantization of the result of the Integration ($\Sigma$) section.
Mathematically the 1Bit Digital Comparator is
$$y(n)=\begin{cases} 1, & \text{if}\ x>\text{Ref} \\ 0, & \text{otherwise} \end{cases}$$
In [9]:
xN=np.random.randint(MinV, MaxV, 20)
Ref=np.random.randint(MinV, MaxV, 20)
ResultsComp=pd.DataFrame(columns=['x', 'Ref', 'yN'])
ResultsComp['x']=x1N; ResultsComp['Ref']=Ref
for i , Row in ResultsComp.iterrows():
ResultsComp['yN'].loc[i]=int(Row['x']>Row['Ref'])
ResultsComp
Out[9]:
In [10]:
@block
def Comparator(x, Ref, y):
"""
Prototype Comparator section used to convert word to binary
ouput
Inputs:
x (2's): Input Word
Ref (2's): Reference to compare too
Outputs:
y(bool): Outcome of Comparison; True if Input (`x`) is
Greater Than the Reference, False Otherwise
"""
@always_comb
def logic():
if x>Ref:
y.next=1
else:
y.next=0
return logic
In [11]:
Peeker.clear()
x=Signal(intbv(0, min=MinV, max=MaxV)); Peeker(x, 'x')
Ref=Signal(intbv(0, min=MinV, max=MaxV)); Peeker(Ref, 'Ref')
y=Signal(bool(0)); Peeker(y, 'y')
DUT=Comparator(x, Ref, y)
def Comparator_TB():
for i, Row in ResultsComp.iterrows():
x.next=int(Row['x']); Ref.next=int(Row['Ref'])
yield (delay(1))
raise StopSimulation
sim=Simulation(DUT, Comparator_TB(), *Peeker.instances()).run()
Peeker.to_wavedrom()
In [12]:
ResultsComp=pd.merge(ResultsComp, Peeker.to_dataframe().astype(int, copy=False), how='left')
Comp=(ResultsComp['yN']==ResultsComp['y']).all()
print(f'Compersion of Sim and myHDL equal: {Comp}')
ResultsComp
Out[12]:
In [13]:
if Comp:
DUT.convert()
VerilogTextReader('Comparator')
The Rail to Rail Digital Scaler is part of the feedback loop of the $\Delta\Sigma$ DAC where it takes the 1Bit output and converts it to digital word that is feed to the Difference ($\Delta$) Section. Is is called a Rail to Rail since the output range is to constrained to two extremes; where is this is similar to Rail to Rail behavior in Analog Circuits.
The Rail to Rail Digital Scaler can be modeled as $$y(n)=\begin{cases} \text{+Rail} , & \text{if}\ x(n)=1 \\ \text{-Rail}, & \text{if}\ x(n)=0 \end{cases}$$
In [14]:
x=np.random.randint(0, 2, 20)
PRail=MaxV-1; NRail=MinV+1
DigitalRailScaleComp=pd.DataFrame(columns=['x', 'yN'])
DigitalRailScaleComp['x']=x
DigitalRailScaleComp['yN']=[PRail if i is 1 else NRail for i in DigitalRailScaleComp['x']]
DigitalRailScaleComp
Out[14]:
In [15]:
@block
def DigitalRailScaler(x, y, PRail, NRail):
"""
Prototype RailToRailDigital Scaler
Inputs:
x (bool): Binary Bit input select scaling
Ouputs:
y (2's): the Rail ouput word will be Positive Rail value
if `x` is the else will be Negative Rail value
Parms:
PRail (int): Positive Word Value
NRail (int): Negative Word Value
"""
@always_comb
def logic():
if x:
y.next=PRail
else:
y.next=NRail
return logic
In [16]:
Peeker.clear()
x=Signal(bool(0)); Peeker(x, 'x')
y=Signal(intbv(0, min=MinV, max=MaxV)); Peeker(y, 'y')
DUT=DigitalRailScaler(x, y, PRail, NRail)
def DigitalRailScaler_TB():
for i, Row in DigitalRailScaleComp.iterrows():
x.next=int(Row['x'])
yield (delay(1))
raise StopSimulation
sim=Simulation(DUT, DigitalRailScaler_TB(), *Peeker.instances()).run()
Peeker.to_wavedrom()
In [17]:
SimData=Peeker.to_dataframe().astype(int, copy=False, inplace=True)
DigitalRailScaleComp=DigitalRailScaleComp.loc[SimData.index]
DigitalRailScaleComp=pd.merge(DigitalRailScaleComp, SimData, left_index=True, right_index=True, how='left')
DigitalRailScaleComp.drop('x_y', axis=1, inplace=True)
DigitalRailScaleComp.rename(columns={'x_x':'x'}, inplace=True)
Comp=(DigitalRailScaleComp['yN']==DigitalRailScaleComp['y']).all()
print(f'Compersion of Sim and myHDL equal: {Comp}')
DigitalRailScaleComp
Out[17]:
In [18]:
if Comp:
DUT.convert()
VerilogTextReader('DigitalRailScaler')
The Integrator ($\Sigma$) section is not a true integrator in the calculus since but instead acts as a accumulator of of the error from the $\Delta$ sections feedback. Where then the accumulated error is feed to the Comparator to convert to a binary value.
In the discrete domain the Integrator is expressed as $$y(n)=y(n-1)+x(n)$$ and in the frequency domain as either $$\dfrac{Y}{X}=\dfrac{1}{1-Z^{-1}}$$ or $$Y=Z^{-1}Y+X$$ where the $z^{-1}$ indicates the use of a D Register to store the past value of $y$
In [19]:
x=np.arange(-11, 13+1)
y=[]
for i, xi in enumerate(x):
if i==0:
y.append(0)
else:
y.append(y[-1]+xi)
IntegratorComp=pd.DataFrame(columns=['x', 'yN'])
IntegratorComp['x']=x; IntegratorComp['yN']=y
IntegratorComp
Out[19]:
In [20]:
@block
def Integrator(x, y, clk, rst):
'''
Prototype Simple Integrator/ Accumultor for the Sigma Section
Inputs:
x (2's): the x(n) data in feed
------------------------
clk(bool): clock feed
rst(bool): reset feed
Outputs:
y (2's): the y(n) output of y(n)=y(n-1)+x(n)
'''
@always(clk.posedge)
def logic():
if rst:
y.next=0
else:
#y(n)=y(n-1)+x(n)
y.next=y+x
return logic
In [21]:
Peeker.clear()
x=Signal(intbv(0, min=MinV, max=MaxV)); Peeker(x, 'x')
y=Signal(intbv(0, min=MinV, max=MaxV)); Peeker(y, 'y')
clk, rst=[Signal(bool(0)) for _ in range(2)]
Peeker(clk, 'clk'); Peeker(rst, 'rst')
DUT=Integrator(x, y, clk, rst)
def Integrator_TB():
@always(delay(1)) ## delay in nano seconds
def clkGen():
clk.next = not clk
@instance
def stimulus():
for i, Row in IntegratorComp.iterrows():
if i==0:
x.next=0
elif i<(IntegratorComp.shape[0]-2):
x.next=int(Row['x'])
else:
x.next=int(Row['x'])
rst.next=True
yield clk.posedge
raise StopSimulation
return instances()
sim = Simulation(DUT, Integrator_TB(), *Peeker.instances()).run()
Peeker.to_wavedrom()
In [22]:
SimData=Peeker.to_dataframe()
SimData=SimData.reindex(columns=['x', 'y', 'rst', 'clk'])
SimData
Out[22]:
In [23]:
#remove clk and rst
SimData=SimData[SimData.clk!=0]
SimData=SimData[SimData.rst!=1]
SimData.drop(['clk', 'rst'], axis=1, inplace=True)
SimData.reset_index(drop=True, inplace=True)
SimData.drop([SimData.index[-1]], axis=0, inplace=True)
SimData
Out[23]:
In [24]:
IntegratorComp['x']=IntegratorComp.x.shift(-1)
IntegratorComp=IntegratorComp.loc[SimData.index]
IntegratorComp=IntegratorComp.astype(int, copy=False)
IntegratorComp
Out[24]:
In [25]:
IntegratorComp=pd.merge(IntegratorComp, SimData,how='left')
Comp=(IntegratorComp['yN']==IntegratorComp['y']).all()
print(f'Compersion of Sim and myHDL equal: {Comp}')
IntegratorComp
Out[25]:
In [26]:
if Comp:
DUT.convert()
VerilogTextReader('Integrator')
The final topology of the 1Bit $\Delta\Sigma$-DAC is shown in it's block diagram where the section that where developed above are are stitched via the following
The Delta Section, which consists the DAC input and the feedback subtraction after the binary output is converted to the feed back word performs the following
$$\Delta(n)=x(n)-\begin{cases} 2^{W}-1, & \text{if}\ y(n)=1 \\ 0, & \text{if}\ y(n)=0 \end{cases}$$The Sigma section is the error integrator performing (note: $\Sigma(n)$ is a variable and not summation from calculus) $$\Sigma(n+1)=\Sigma(n)+\Delta(n)$$
The Comparator converts the output error to a binary output based on wither the sight of the $\Sigma$ sections accumulated error is positive or negative where in this case 0 is defined to be negative resulting in
$$y(n)=\begin{cases} 1, & \text{if}\ \Sigma(n)>0 \\ 0, & \text{otherwise} \end{cases}$$
In [27]:
TestData=pd.DataFrame(columns=['Ref', 'QRef'])
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**(BitWidth-2))
if dac_code == 2**BitWidth:
dac_code = 2**BitWidth-1
elif dac_code > 2**BitWidth:
raise Exception
dac_codes.append(dac_code)
TestData['Ref']=refvals; TestData['QRef']=dac_codes
TestData
Out[27]:
In [28]:
class DACSim:
def __init__(self):
self.yStore=[]
self.FB=0
self.Sigma=0
def Action(self, x):
self.DeltaAction(x, self.FB)
self.SigmaAction(self.Delta)
self.CompAction(self.Sigma)
self.yStore.append(self.y)
self.FBScale(self.y)
def DeltaAction(self, x, FB):
self.Delta=x-FB
def SigmaAction(self, Delta):
self.Sigma=self.Sigma+Delta
def CompAction(self, Sigma):
if Sigma>0:
self.y=1
else:
self.y=0
def FBScale(self, y):
Scale=2**BitWidth-1
if y==1:
self.FB=Scale
else:
self.FB=0
In [29]:
DAC=DACSim()
DAC.Action(0)
print(f'x: {0}, Delta: {DAC.Delta}, Sigma: {DAC.Sigma}, y: {DAC.y}, FB: {DAC.FB}')
for i in TestData['QRef']:
DAC.Action(i)
print(f'x: {i}, Delta: {DAC.Delta}, Sigma: {DAC.Sigma}, y: {DAC.y}, FB: {DAC.FB}')
In [30]:
@block
def DAC_1Bit(x, y, clk, rst):
"""
A 1bit SigmaDelta DAC based on
http://old.myhdl.org/doku.php/projects:dsx1000
using a Instantiation approach
Inputs:
x (2's): Input Digital word to be translated to PDM ouput
-----------------------------
clk (bool): system clock input
rst (bool); reset signal
Ouput:
y (bool): the 1BIt Sigma Delta PDM output
"""
#Internal Parameter Calculations; Dont show up in conversion
#Though values are set to the various subcomponets in the
#Conversion
RES = len(x)
DREF_NEG = 0
DREF_POS = (2**RES)-1
MIN = -2**(RES-1)
MAX = +2**(RES-1)
#Buss and Wires
#Comparter Ref: 0
Ref=Signal(intbv(0)[RES:])
#bus from the Delta Section to the Sigma Section
diff_o = Signal(intbv(0, min=4*MIN, max=4*MAX))
#But from the Sigma Section to the Comparator
Intgral_o = Signal(intbv(0, min=4*MIN, max=4*MAX))
#Wire from Compater to synchronized output
comp_o = Signal(bool(0))
#bus feedback from the DigitalRailScaler to the Delta Section
ddc_o = Signal(intbv(0, min=4*MIN, max=4*MAX))
# Delta
Diff=Difference(x, ddc_o, diff_o)
#Sigma
Intgrat=Integrator(diff_o, Intgral_o,clk, rst)
#Comparator agenst 0
Comp=Comparator(Intgral_o, Ref, comp_o)
#Digital Scale for FB: Scale is (2**RES)-1:0
DDC=DigitalRailScaler(comp_o, ddc_o, DREF_POS, DREF_NEG)
#create synchronized ouput
@always(clk.posedge)
def Ouput():
if rst:
y.next=0
else:
y.next=comp_o
return instances()
In [31]:
Peeker.clear()
clk, rst=[Signal(bool(0)) for i in range(2)]
Peeker(clk, 'clk'); Peeker(rst, 'rst')
x=Signal(intbv(0, min=MinV, max=MaxV)); Peeker(x, 'x')
y=Signal(bool(0)); Peeker(y, 'y')
DUT=DAC_1Bit(x, y, clk, rst)
def DAC_TB():
@always(delay(1)) ## delay in nano seconds
def clkGen():
clk.next = not clk
@instance
def Stimules():
for i, Row in TestData.iterrows():
x.next=int(Row['QRef'])
yield clk.posedge
raise StopSimulation
return instances()
sim=Simulation(DUT, DAC_TB(), *Peeker.instances()).run()
Peeker.to_wavedrom()
In [32]:
SimData=Peeker.to_dataframe()
SimData=SimData[SimData.clk!=0]
SimData.drop(['clk', 'rst'], axis=1, inplace=True)
SimData.reset_index(drop=True, inplace=True)
SimData
Out[32]:
In [33]:
DUT.convert()
VerilogTextReader('DAC_1Bit');