Author Topic: 2nd Order Sigma-Delta Digital to Analogue Converter (SD-DAC) in Verilog  (Read 7681 times)

0 Members and 1 Guest are viewing this topic.

Offline SMB784Topic starter

  • Frequent Contributor
  • **
  • Posts: 421
  • Country: us
    • Tequity Surplus
Howdy everyone!

I have a hobby FPGA project that I need some help with.  I am familiar with how to implement a 1st SD-DAC, and have successfully done so (I have included code for this below), but now I would like to convert this to a 2nd SD-DAC.

I understand that I need to chain two 1st order DACs together by feeding the output of one 1st order SD-DAC + the next input bit into the input of the the other 1st order SD-DAC, but I am having trouble actually getting this to work, and I would really appreciate some pointers on how to get this going.

I am implementing this on an Altera Cyclone V (model number 5CSEBA6U23I7) on a Terasic DE10 Nano board

Here is the code for my working 1st order SD-DAC in Verilog:

Code: [Select]
module delta_sigma_DAC
#(
parameter OW,  // output word size
parameter OS=4 // oversampling ratio, 2^7 = 128
)
(
input wire i_clk, // input clock
input wire i_res, // input reset line
input wire [OW-1:0] i_func, // input function
output wire o_DAC // output DAC bit
);

reg [((OW-1)+OS):0] DAC_acc; // DAC accumulator

always @(posedge i_clk or negedge i_res)
begin
if (i_res==0)
begin
DAC_acc<={((OW-1)+OS){1'd0}}; // zeroes output on reset
end
else
DAC_acc<=DAC_acc[((OW-1)+OS)-1:0]+i_func; /* adds value of
i_func to DAC
accumulator
register */
end

assign o_DAC=DAC_acc[((OW-1)+OS)]; // outputs top bit of DAC accumulator

endmodule

Here is the code for my 2nd order SD-DAC that compiles and runs, but gives me garbled output:

Code: [Select]
module delta_sigma_DAC_2nd_order
#(
parameter OW,
parameter [(OW+2):0] max,
parameter [(OW+2):0] min,
parameter [(OW+2):0] mid
)
(
input wire i_clk,
input wire i_res,
input wire [OW-1:0] i_func,
output reg o_DAC
);

reg [(OW+2):0] e2,e1,e0;

always @(posedge i_clk or negedge i_res)
begin
if (i_res==0)
begin
e0<={(OW+2){1'd0}};
e1<={(OW+2){1'd0}};
e2<={(OW+2){1'd0}};
o_DAC<=0;
end
else if(i_clk)
begin
e1<=i_func+e1-e0;
e2<=e2+e1[(OW+2)]>>1-e0;

if ((i_func+(e1+e2-e0-e0))>mid)
begin
e0<=(max<<(OW));

o_DAC<=1'b1;
end
else
begin
e0<=(min<<(OW));

o_DAC<=1'b0;
end
end
end

endmodule

Any idea what I am doing wrong here?
« Last Edit: June 30, 2020, 08:17:27 pm by SMB784 »
 

Offline SMB784Topic starter

  • Frequent Contributor
  • **
  • Posts: 421
  • Country: us
    • Tequity Surplus
So I have made a little progress, but the output still comes out very noisy and not really the way I want it.

The code that I am using is presented below, and I have added screenshots of the signal output at (freq=1000 Hz, signal out = LP_out) and the RTL view of the synthesized logic:
Code: [Select]
module delta_sigma_DAC_2nd_order
#(
parameter OW,
parameter OS=4
)
(
input wire i_clk,
input wire i_res,
input wire [OW-1:0] i_func,
output wire o_DAC
);

reg [((OW-1)+OS):0] DAC_acc_1st;
reg [((OW-1)+OS+1):0] DAC_acc_2nd;

always @(posedge i_clk or negedge i_res)
begin
if (i_res==0)
begin
DAC_acc_1st<={((OW-1)+OS){1'd0}};
DAC_acc_2nd<={((OW-1)+OS){1'd0}};
end

else
begin
DAC_acc_1st<=DAC_acc_2nd[((OW-1)+OS)-2:0]+i_func;
DAC_acc_2nd<=DAC_acc_2nd[((OW-1)+OS)-1:0]+DAC_acc_1st;
end
end

assign o_DAC=DAC_acc_2nd[((OW-1)+OS)];

endmodule
« Last Edit: June 30, 2020, 08:16:46 pm by SMB784 »
 

Offline hamster_nz

  • Super Contributor
  • ***
  • Posts: 2803
  • Country: nz
Hi SMB784

I've been wanting to implement a Delta Sigma 2nd order DAC for a while, but never really understood what is required.

I had a quick look at https://www.beis.de/Elektronik/DeltaSigma/SigmaDelta.html, esp figure 6.

Assuming that the ADC is doesn't actually exist physically, but just converts the '1' or '0' to the positive and negative limits, I can't see how your code maps onto it - you seem to be missing the second difference stage (the output of integrator 1 less the current output value);

Oh, another thing is that logically the values in the accumulator of a "just keep adding and use the overflow as the output" DAC seem to be slightly different to what is in diagrams.
Stripping the overflow is doing half of the first difference - e.g. for an 8-bit accumulator, stripping the overflow equivilent to subtracting the output value of 256 in the first difference stage.

It's not HDL, but here is some C code that seems to implement the 2nd order DAC and give a sensible bitstream for the value (in the range of +/-127) given on the command line.
Code: [Select]
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
   int i;
   int output;
   int accum=0, accum2=0;
   int input_val;

   if(argc != 2) {
      printf("Must supply input value\n");
      return 0;
   }
   input_val = atoi(argv[1]);

   for(i = 0; i < 1000; i++) {
      int output_val = (output==1) ? 128 : -128;
      accum  += input_val - output_val;
      accum2 += accum - output_val;
      output = (accum2 > 0) ? 1 : 0;

      printf("%c", '0'+output);
      if(i%80 == 79||i==999) putchar('\n');
   }
   return 0;
}

Am I on the same page as you?
« Last Edit: June 30, 2020, 02:32:40 am by hamster_nz »
Gaze not into the abyss, lest you become recognized as an abyss domain expert, and they expect you keep gazing into the damn thing.
 
The following users thanked this post: SMB784

Offline hamster_nz

  • Super Contributor
  • ***
  • Posts: 2803
  • Country: nz
I've had a hack at a 1st Order and 2nd Order 12-bit Sigma-Delta DACs, both running from the same ~2kHz Sine wave samples (12-bit samples at 50kHz).

Hooked them up to my Analog Discovery 2, via a RC low pass filter to get the spectrum.

Sadly the code is in VHDL not Verilog, but below is the entire design. It's not suppose to be the most effective or stylish code, or use minimal resources (eg the accumulators are oversized), but it does work and should provide a workable template for your Verilog implementation. Note that it uses VHDL variables in the DAC processes, which have the "update now, not on the clock edge" semantics.

Views / opinions?
Code: [Select]
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

entity top_level is
    Port ( clk : in STD_LOGIC;
           dac1 : out STD_LOGIC;
           dac2 : out STD_LOGIC);
end top_level;

architecture Behavioral of top_level is
    signal sample_count : unsigned(5 downto 0) := (others => '0');
    type a_samples is array (0 to 47) of signed(11 downto 0);
    signal samples    : a_samples := (
        to_signed(   66,12), to_signed(  199,12), to_signed(  329,12),
        to_signed(  452,12), to_signed(  568,12), to_signed(  675,12),
        to_signed(  769,12), to_signed(  851,12), to_signed(  918,12),
        to_signed(  969,12), to_signed( 1004,12), to_signed( 1021,12),
        to_signed( 1021,12), to_signed( 1004,12), to_signed(  969,12),
        to_signed(  918,12), to_signed(  851,12), to_signed(  769,12),
        to_signed(  675,12), to_signed(  568,12), to_signed(  452,12),
        to_signed(  329,12), to_signed(  199,12), to_signed(   66,12),
        to_signed(  -67,12), to_signed( -200,12), to_signed( -330,12),
        to_signed( -453,12), to_signed( -569,12), to_signed( -676,12),
        to_signed( -770,12), to_signed( -852,12), to_signed( -919,12),
        to_signed( -970,12), to_signed(-1005,12), to_signed(-1022,12),
        to_signed(-1022,12), to_signed(-1005,12), to_signed( -970,12),
        to_signed( -919,12), to_signed( -852,12), to_signed( -770,12),
        to_signed( -676,12), to_signed( -569,12), to_signed( -453,12),
        to_signed( -330,12), to_signed( -200,12), to_signed(  -67,12));
    signal count       : unsigned(9 downto 0) := (others => '0');
    signal sample      : signed(11 downto 0)  := (others => '0');
   
    signal dac1_out    : std_logic           := '0';
    signal dac1_accum  : signed(15 downto 0) := (others => '0');

    signal dac2_out    : std_logic           := '0';
    signal dac2_accum1 : signed(15 downto 0) := (others => '0');
    signal dac2_accum2 : signed(15 downto 0) := (others => '0');

begin

    dac1 <= dac1_out;
    dac2 <= dac2_out;
   
dac1_proc: process(clk)
    variable new_val : signed(15 downto 0);
    begin
        if rising_edge(clk) then
           if dac1_out = '1' then
              new_val := dac1_accum + sample - 2048;
           else
              new_val := dac1_accum + sample + 2048;
           end if;
           if new_val > 0 then
              dac1_out <= '1';
           else
              dac1_out <= '0';
           end if;
           dac1_accum <= new_val;
       end if;
    end process;

dac2_proc: process(clk)
    variable new_val1 : signed(15 downto 0);
    variable new_val2 : signed(15 downto 0);
    begin
        if rising_edge(clk) then
           if dac2_out = '1' then
              new_val1 := dac2_accum1 + sample - 2048;
           else
              new_val1 := dac2_accum1 + sample + 2048;
           end if;

           if dac2_out = '1' then
              new_val2 := dac2_accum2 + new_val1 - 2048;
           else
              new_val2 := dac2_accum2 + new_val1 + 2048;
           end if;

           if new_val2 > 0 then
              dac2_out <= '1';
           else
              dac2_out <= '0';
           end if;
           dac2_accum1 <= new_val1;
           dac2_accum2 <= new_val2;
       end if;
    end process;

gen_samples: process(clk)
    begin
        if rising_edge(clk) then
            sample <= samples(to_integer(sample_count));
            if count = 999 then
                if sample_count = 47 then
                   sample_count <= (others => '0');
                else
                   sample_count <= sample_count +1;
                end if;
                count <= (others => '0');
            else
                count <= count + 1;
            end if;
         end if;
    end process;

end Behavioral;

Gaze not into the abyss, lest you become recognized as an abyss domain expert, and they expect you keep gazing into the damn thing.
 
The following users thanked this post: oPossum, SMB784

Offline rstofer

  • Super Contributor
  • ***
  • Posts: 9890
  • Country: us
Nice use of  the AD2, my favorite gadget.  In particular, it does a terrific job on FFTs.

This paper by Analog Devices will probably help me understand what is happening.  The block diagrams, especially figure 6.8, seem useful.
https://www.analog.com/media/en/technical-documentation/application-notes/292524291525717245054923680458171AN283.pdf
 
The following users thanked this post: SMB784

Offline SMB784Topic starter

  • Frequent Contributor
  • **
  • Posts: 421
  • Country: us
    • Tequity Surplus
Hi SMB784

I've been wanting to implement a Delta Sigma 2nd order DAC for a while, but never really understood what is required.

I had a quick look at https://www.beis.de/Elektronik/DeltaSigma/SigmaDelta.html, esp figure 6.

Assuming that the ADC is doesn't actually exist physically, but just converts the '1' or '0' to the positive and negative limits, I can't see how your code maps onto it - you seem to be missing the second difference stage (the output of integrator 1 less the current output value);

Oh, another thing is that logically the values in the accumulator of a "just keep adding and use the overflow as the output" DAC seem to be slightly different to what is in diagrams.
Stripping the overflow is doing half of the first difference - e.g. for an 8-bit accumulator, stripping the overflow equivilent to subtracting the output value of 256 in the first difference stage.

It's not HDL, but here is some C code that seems to implement the 2nd order DAC and give a sensible bitstream for the value (in the range of +/-127) given on the command line.
Code: [Select]
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
   int i;
   int output;
   int accum=0, accum2=0;
   int input_val;

   if(argc != 2) {
      printf("Must supply input value\n");
      return 0;
   }
   input_val = atoi(argv[1]);

   for(i = 0; i < 1000; i++) {
      int output_val = (output==1) ? 128 : -128;
      accum  += input_val - output_val;
      accum2 += accum - output_val;
      output = (accum2 > 0) ? 1 : 0;

      printf("%c", '0'+output);
      if(i%80 == 79||i==999) putchar('\n');
   }
   return 0;
}

Am I on the same page as you?

I think we are on the same page, but I have some questions about your implementation.

The first question is how do you implement your oversampling bits? How many oversampling bits do you employ? Second question is what is the significance of the number 2048? Is that the max absolute size of your input signal?

I'm not really sure why my first order DAC works without an explicit subtraction, but it does seem to work that way and I have verified the output (albeit noisy).

I will try implementing your design in verilog and see where I get.

Offline SMB784Topic starter

  • Frequent Contributor
  • **
  • Posts: 421
  • Country: us
    • Tequity Surplus
I've had a hack at a 1st Order and 2nd Order 12-bit Sigma-Delta DACs, both running from the same ~2kHz Sine wave samples (12-bit samples at 50kHz).

Hooked them up to my Analog Discovery 2, via a RC low pass filter to get the spectrum.

Sadly the code is in VHDL not Verilog, but below is the entire design. It's not suppose to be the most effective or stylish code, or use minimal resources (eg the accumulators are oversized), but it does work and should provide a workable template for your Verilog implementation. Note that it uses VHDL variables in the DAC processes, which have the "update now, not on the clock edge" semantics.

Views / opinions?
Code: [Select]
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

entity top_level is
    Port ( clk : in STD_LOGIC;
           dac1 : out STD_LOGIC;
           dac2 : out STD_LOGIC);
end top_level;

architecture Behavioral of top_level is
    signal sample_count : unsigned(5 downto 0) := (others => '0');
    type a_samples is array (0 to 47) of signed(11 downto 0);
    signal samples    : a_samples := (
        to_signed(   66,12), to_signed(  199,12), to_signed(  329,12),
        to_signed(  452,12), to_signed(  568,12), to_signed(  675,12),
        to_signed(  769,12), to_signed(  851,12), to_signed(  918,12),
        to_signed(  969,12), to_signed( 1004,12), to_signed( 1021,12),
        to_signed( 1021,12), to_signed( 1004,12), to_signed(  969,12),
        to_signed(  918,12), to_signed(  851,12), to_signed(  769,12),
        to_signed(  675,12), to_signed(  568,12), to_signed(  452,12),
        to_signed(  329,12), to_signed(  199,12), to_signed(   66,12),
        to_signed(  -67,12), to_signed( -200,12), to_signed( -330,12),
        to_signed( -453,12), to_signed( -569,12), to_signed( -676,12),
        to_signed( -770,12), to_signed( -852,12), to_signed( -919,12),
        to_signed( -970,12), to_signed(-1005,12), to_signed(-1022,12),
        to_signed(-1022,12), to_signed(-1005,12), to_signed( -970,12),
        to_signed( -919,12), to_signed( -852,12), to_signed( -770,12),
        to_signed( -676,12), to_signed( -569,12), to_signed( -453,12),
        to_signed( -330,12), to_signed( -200,12), to_signed(  -67,12));
    signal count       : unsigned(9 downto 0) := (others => '0');
    signal sample      : signed(11 downto 0)  := (others => '0');
   
    signal dac1_out    : std_logic           := '0';
    signal dac1_accum  : signed(15 downto 0) := (others => '0');

    signal dac2_out    : std_logic           := '0';
    signal dac2_accum1 : signed(15 downto 0) := (others => '0');
    signal dac2_accum2 : signed(15 downto 0) := (others => '0');

begin

    dac1 <= dac1_out;
    dac2 <= dac2_out;
   
dac1_proc: process(clk)
    variable new_val : signed(15 downto 0);
    begin
        if rising_edge(clk) then
           if dac1_out = '1' then
              new_val := dac1_accum + sample - 2048;
           else
              new_val := dac1_accum + sample + 2048;
           end if;
           if new_val > 0 then
              dac1_out <= '1';
           else
              dac1_out <= '0';
           end if;
           dac1_accum <= new_val;
       end if;
    end process;

dac2_proc: process(clk)
    variable new_val1 : signed(15 downto 0);
    variable new_val2 : signed(15 downto 0);
    begin
        if rising_edge(clk) then
           if dac2_out = '1' then
              new_val1 := dac2_accum1 + sample - 2048;
           else
              new_val1 := dac2_accum1 + sample + 2048;
           end if;

           if dac2_out = '1' then
              new_val2 := dac2_accum2 + new_val1 - 2048;
           else
              new_val2 := dac2_accum2 + new_val1 + 2048;
           end if;

           if new_val2 > 0 then
              dac2_out <= '1';
           else
              dac2_out <= '0';
           end if;
           dac2_accum1 <= new_val1;
           dac2_accum2 <= new_val2;
       end if;
    end process;

gen_samples: process(clk)
    begin
        if rising_edge(clk) then
            sample <= samples(to_integer(sample_count));
            if count = 999 then
                if sample_count = 47 then
                   sample_count <= (others => '0');
                else
                   sample_count <= sample_count +1;
                end if;
                count <= (others => '0');
            else
                count <= count + 1;
            end if;
         end if;
    end process;

end Behavioral;


So I couldn't get my verilog translation to produce an output, but I did add a small change to my original verilog code.  Before I describe it, I will give a bit of background: my DAC outputs between 0 and 216, so instead of subtracting or adding 215, instead I add 0 or 216.  The change I have implemented (seen in the code below) involves adding only the top bit of DAC_acc_2nd (which is what gets output to o_DAC) to DAC_acc_1st & DAC_acc_2nd, which should act as adding 0 or adding 216.  This should take care of the DAC step in the diagram described in your reference (I have included an RTL diagram, as before).

Code: [Select]
module delta_sigma_DAC_2nd_order
#(
parameter OW,
parameter OSR=4 // 2^OSR = oversampling ratio
)
(
input wire i_clk,
input wire i_res,
input wire [OW-1:0] i_func,
output wire o_DAC
);

reg [((OW-1)+OSR):0] DAC_acc_1st;
reg [((OW-1)+OSR):0] DAC_acc_2nd;

always @(posedge i_clk or negedge i_res)
begin
if (i_res==0)
begin
DAC_acc_1st<={((OW-1)+OSR){1'd0}};
DAC_acc_2nd<={((OW-1)+OSR){1'd0}};
end

else
begin
DAC_acc_1st<=DAC_acc_1st+i_func+DAC_acc_2nd[((OW-1)+OSR)];
DAC_acc_2nd<=DAC_acc_1st+DAC_acc_2nd[((OW-1)+OSR)];
end
end

assign o_DAC=DAC_acc_2nd[((OW-1)+OSR)];

endmodule

At this point I have gotten quite close to the correct output.  The output is periodic, and the square wave even looks like a good square wave (though with half the p2p amplitude and offset from 0 by half the DNR).  The sine wave is antisymmetric around the oscillation node, the square and triangle waves are symmetric around the oscillation node, and the saw is asymmetric as expected.  Now I just need to figure out what is causing this strange suboscillation behavior.

Thanks again for your help so far, hopefully in one or two more iterations I will have this working.  Once it is working, I will put it up on github so that the whole world has access to a working 2nd order SD-DAC in verilog.

Offline hamster_nz

  • Super Contributor
  • ***
  • Posts: 2803
  • Country: nz
The first question is how do you implement your oversampling bits? How many oversampling bits do you employ? Second question is what is the significance of the number 2048? Is that the max absolute size of your input signal?

I don't implement oversampling. As far as I know (and I may be very wrong), in this scheme oversampling would be to take the the stream of incoming samples, buffer them, and then run a sensible interpolation filter over them to generate extra "in between" samples, that would be feed into the Sigma Delta DAC.

i guess the more formal way would be this process:

- Take the stream of samples (a,b,c, d)

- Insert three zeros between each sample (a,0,0,0,b,0,0,0,c,0,0,0,d,0,0,0....)

- Run that through a low pass filter with gain of 4x to generate new samples
    (a0, a1, a2, a3, b0, b1, b2, b3, c0, c1, c2, c3, d0.....)

Feed that into the DAC at 4x the original sample rate.

Quote
I'm not really sure why my first order DAC works without an explicit subtraction, but it does seem to work that way and I have verified the output (albeit noisy).

I will try implementing your design in verilog and see where I get.

I'll try to implement it in Verilog too later tonight.

The +2048 and -2048 constant are the value represented by a '1' on the output and a '0' on the output. The input samples short be within that range (i.e. it is a 12 bit input value)
Gaze not into the abyss, lest you become recognized as an abyss domain expert, and they expect you keep gazing into the damn thing.
 
The following users thanked this post: SMB784

Offline hamster_nz

  • Super Contributor
  • ***
  • Posts: 2803
  • Country: nz
It was raining at lunchtime, so I gave EDA Playground a crack. This seems to simulate OK. You will need to parameterize the widths of the inputs and registers.

Code: [Select]
module dac
(
  input wire i_clk,
  input wire i_res,
  input wire [11:0] i_func,  // A signed sample - ADJUST WIDTH HERE
  output wire o_DAC
);

  reg this_bit;
 
  reg [15:0] DAC_acc_1st;
  reg [15:0] DAC_acc_2nd;
 
  assign o_DAC = this_bit;

  always @(posedge i_clk or negedge i_res)
    begin
      if (i_res==0)
        begin
          DAC_acc_1st<=16'd0;
          DAC_acc_2nd<=16'd0;
          this_bit = 1'b0;
        end
      else
        begin
          if(this_bit == 1'b1)
            begin
              DAC_acc_1st = DAC_acc_1st + i_func      - 2048;  << ADJUST CONSTANT TO 2^(n-1) of input width
              DAC_acc_2nd = DAC_acc_2nd + DAC_acc_1st - 2048;  << ADJUST CONSTANT TO 2^(n-1) of input width
            end
          else
            begin
              DAC_acc_1st = DAC_acc_1st + i_func      + 2048;  << ADJUST CONSTANT TO 2^(n-1) of input width
              DAC_acc_2nd = DAC_acc_2nd + DAC_acc_1st + 2048;  << ADJUST CONSTANT TO 2^(n-1) of input width
            end
          end
      // When the high bit is set (a negative value) we need to output a 0 and when it is clear we need to output a 1.
      this_bit = ~DAC_acc_2nd[15];   << ADJUST TO HIGHEST BIT OF DAC_acc_2nd.
    end
endmodule

The widths of the accumulators do not have any thinking behind them, however a few bits longer than the input should be fine.
Gaze not into the abyss, lest you become recognized as an abyss domain expert, and they expect you keep gazing into the damn thing.
 
The following users thanked this post: SMB784

Offline SiliconWizard

  • Super Contributor
  • ***
  • Posts: 14472
  • Country: fr
The first question is how do you implement your oversampling bits? How many oversampling bits do you employ? Second question is what is the significance of the number 2048? Is that the max absolute size of your input signal?

I don't implement oversampling. As far as I know (and I may be very wrong), in this scheme oversampling would be to take the the stream of incoming samples, buffer them, and then run a sensible interpolation filter over them to generate extra "in between" samples, that would be feed into the Sigma Delta DAC.
(...)

Yes that's about it!
I have used just linear interpolation for this in a past project, and got pretty decent results. Linear interpolation is relatively cheap. Of course, more advanced filtering will yield better results but I've found that linear interpolation was often adequate if you're not after ultra-low distortion.

Note that "oversampling" makes sense when you implement a DAC that must be fed with an arbitrary stream of samples at a given (and limited) sample rate.

In case you're implementing some kind of DDS meant to generate fixed periodic signals, such as in your example, as long as you have enough room for a larger memory buffer for the period, you can just store samples at a higher sample rate than required and directly feed the modulator without adding any "oversampling" capability per se (filters can be expensive.)

As a quick example: assuming you want to generate a 1kHz sine wave with a "rated" 48kHz sample rate and a "virtual" oversampling ratio of 256. You'd just need to store a whole period consisting of 48*256 = 12KBytes (for 8-bit samples, double that for 16-bit samples, etc), and clock your modulator @12.288MHz. Even small FPGAs often have more than enough block RAM for this, and you'd save many LUTs not implementing the oversampling. Likewise, this clock frequency is pretty modest even for low-end FPGAs as well, and with such a "high" oversampling ratio, the required analog low-pass filter can be as basic as a first-order RC filter.

« Last Edit: July 01, 2020, 04:57:02 pm by SiliconWizard »
 
The following users thanked this post: SMB784

Offline SMB784Topic starter

  • Frequent Contributor
  • **
  • Posts: 421
  • Country: us
    • Tequity Surplus
It was raining at lunchtime, so I gave EDA Playground a crack. This seems to simulate OK. You will need to parameterize the widths of the inputs and registers.

Code: [Select]
module dac
(
  input wire i_clk,
  input wire i_res,
  input wire [11:0] i_func,  // A signed sample - ADJUST WIDTH HERE
  output wire o_DAC
);

  reg this_bit;
 
  reg [15:0] DAC_acc_1st;
  reg [15:0] DAC_acc_2nd;
 
  assign o_DAC = this_bit;

  always @(posedge i_clk or negedge i_res)
    begin
      if (i_res==0)
        begin
          DAC_acc_1st<=16'd0;
          DAC_acc_2nd<=16'd0;
          this_bit = 1'b0;
        end
      else
        begin
          if(this_bit == 1'b1)
            begin
              DAC_acc_1st = DAC_acc_1st + i_func      - 2048;  << ADJUST CONSTANT TO 2^(n-1) of input width
              DAC_acc_2nd = DAC_acc_2nd + DAC_acc_1st - 2048;  << ADJUST CONSTANT TO 2^(n-1) of input width
            end
          else
            begin
              DAC_acc_1st = DAC_acc_1st + i_func      + 2048;  << ADJUST CONSTANT TO 2^(n-1) of input width
              DAC_acc_2nd = DAC_acc_2nd + DAC_acc_1st + 2048;  << ADJUST CONSTANT TO 2^(n-1) of input width
            end
          end
      // When the high bit is set (a negative value) we need to output a 0 and when it is clear we need to output a 1.
      this_bit = ~DAC_acc_2nd[15];   << ADJUST TO HIGHEST BIT OF DAC_acc_2nd.
    end
endmodule

The widths of the accumulators do not have any thinking behind them, however a few bits longer than the input should be fine.

Ok, below is my parametrized implementation of your example.  I had to move my version of this_bit (known as ADC_out) to inside the outer else block because the compiler didn't like it outside.

Code: [Select]
module delta_sigma_DAC_2nd_order
#(
parameter OW,
parameter OSR=4 // 2^OSR = oversampling ratio
)
(
input wire i_clk,
input wire i_res,
input wire [OW-1:0] i_func,
output wire o_DAC
);

reg [((OW-1)+OSR):0] DAC_acc_1st;
reg [((OW-1)+OSR):0] DAC_acc_2nd;
reg ADC_out;

always @(posedge i_clk or negedge i_res)
begin
if (i_res==0)
begin
DAC_acc_1st<={((OW-1)+OSR){1'd0}};
DAC_acc_2nd<={((OW-1)+OSR){1'd0}};
ADC_out=1'b0;
end

else
begin
if(ADC_out==1'b1)
begin
DAC_acc_1st=DAC_acc_1st+i_func-(2**(OW-1)-1);
DAC_acc_2nd=DAC_acc_2nd+DAC_acc_1st-(2**(OW-1)-1);
end
else
begin
DAC_acc_1st=DAC_acc_1st+i_func+(2**(OW-1)-1);
DAC_acc_2nd=DAC_acc_2nd+DAC_acc_1st+(2**(OW-1)-1);
end
ADC_out=DAC_acc_2nd[((OW-1)+OSR)];
end
end

assign o_DAC=ADC_out;
endmodule

I have attached the simulated output, where values of sel=0,1,2,&3 correspond to 1 kHz sine, square, triangle, and sawtooth waveforms.  These waveforms are garbled for half the cycle.  I believe this is because the highest value is overflowing the integer register and causing problems, but I'm not entirely sure.

Were you able to get reasonable output with this implementation?

Offline hamster_nz

  • Super Contributor
  • ***
  • Posts: 2803
  • Country: nz
I have attached the simulated output, where values of sel=0,1,2,&3 correspond to 1 kHz sine, square, triangle, and sawtooth waveforms.  These waveforms are garbled for half the cycle.  I believe this is because the highest value is overflowing the integer register and causing problems, but I'm not entirely sure.

Were you able to get reasonable output with this implementation?

The only check I did on that Verilog code was that for a single (positive) input value both the VHDL and Verilog version produced the same bitstream.

The accumulators should be big enough - you have the OSR bits longer than the input.

I think the issue is that you input sample is defaulting to "unsigned"

   input wire [OW-1:0] i_func

Can you try something like

        input signed wire [OW-1:0] i_func?

As I said, my Verilog skills are very limited... but it seems that this also be a pattern used to sign extend (in this case adding 8 copies of the 7th bit of 'extend'):

     extended[15:0] <= { {8{extend[7]}}, extend[7:0] };
« Last Edit: July 01, 2020, 10:03:08 pm by hamster_nz »
Gaze not into the abyss, lest you become recognized as an abyss domain expert, and they expect you keep gazing into the damn thing.
 
The following users thanked this post: SMB784

Offline SMB784Topic starter

  • Frequent Contributor
  • **
  • Posts: 421
  • Country: us
    • Tequity Surplus
Getting closer (see attachment)!  Still some overflow, and the sine lookup is clearly inverted, but its getting there.  I made DAC_acc_1st, DAC_acc_2nd, and i_func signed wires to account for positive and negative values.  I would like, however, for them to be unsigned because my DAC is implemented with a GPIO pin that as far as I know is only valid for values between 0V and 5V.

Offline hamster_nz

  • Super Contributor
  • ***
  • Posts: 2803
  • Country: nz
Getting closer (see attachment)!  Still some overflow, and the sine lookup is clearly inverted, but its getting there.  I made DAC_acc_1st, DAC_acc_2nd, and i_func signed wires to account for positive and negative values.  I would like, however, for them to be unsigned because my DAC is implemented with a GPIO pin that as far as I know is only valid for values between 0V and 5V.

Alrighty! Getting somewhere...

That looks like the samples coming in are unsigned so there are two options, first and easy wrong one: Flip the high bit in in the incoming samples to convert them to signed values

The better one would be

When you update the accumulators, subtract off of the half scale sample, as that is keeping with the thinking that the accumulators hold a signed value representing the error between the current and desired output. This is then consistent with the DAC is going "too high? output a 0, too low? output a 1".

You will then be able to simplify the math by merging the constants, but that sort of hides what is going on.
« Last Edit: July 02, 2020, 12:33:46 am by hamster_nz »
Gaze not into the abyss, lest you become recognized as an abyss domain expert, and they expect you keep gazing into the damn thing.
 
The following users thanked this post: SMB784

Offline SMB784Topic starter

  • Frequent Contributor
  • **
  • Posts: 421
  • Country: us
    • Tequity Surplus
Just a question before I work on it again, why are you using blocking assignments rather than non-blocking assignments for updating your accumulators (= rather than <=)?  It turns out that if you use non-blocking assignments, the output is garbage, but blocking assignments make it work.  Was that a conscious choice on your part, or just serendipity that it worked out as it was supposed to?
« Last Edit: July 02, 2020, 04:52:47 am by SMB784 »
 

Offline hamster_nz

  • Super Contributor
  • ***
  • Posts: 2803
  • Country: nz
Just a question before I work on it again, why are you using blocking assignments rather than non-blocking assignments for updating your accumulators (= rather than <=)?

If it was non-blocking, the 2nd accumulator will be updated using the original value of the 1st accumulator, not the updated one.

You can work around this and use non-blocking assignments, but it starts to look ugly.

Gaze not into the abyss, lest you become recognized as an abyss domain expert, and they expect you keep gazing into the damn thing.
 
The following users thanked this post: SMB784

Offline SMB784Topic starter

  • Frequent Contributor
  • **
  • Posts: 421
  • Country: us
    • Tequity Surplus
Just a question before I work on it again, why are you using blocking assignments rather than non-blocking assignments for updating your accumulators (= rather than <=)?

If it was non-blocking, the 2nd accumulator will be updated using the original value of the 1st accumulator, not the updated one.

You can work around this and use non-blocking assignments, but it starts to look ugly.

Fair enough, that makes sense.  Thanks for the explanation!

Offline hamster_nz

  • Super Contributor
  • ***
  • Posts: 2803
  • Country: nz
So I coded up a few things and tested.

This Verilog version gives identical results to the VHDL version - set up to use 16-bit signed samples.

Code: [Select]
`timescale 1ns / 1ps
module second_order_dac(
  input wire i_clk,
  input wire i_res,
  input wire [15:0] i_func,
  output wire o_DAC
);

  reg this_bit;
 
  reg [19:0] DAC_acc_1st;
  reg [19:0] DAC_acc_2nd;
  reg [19:0] i_func_extended;
   
  assign o_DAC = this_bit;

  always @(*)
     i_func_extended = {i_func[15],i_func[15],i_func[15],i_func[15],i_func};
   
  always @(posedge i_clk or negedge i_res)
    begin
      if (i_res==0)
        begin
          DAC_acc_1st<=16'd0;
          DAC_acc_2nd<=16'd0;
          this_bit = 1'b0;
        end
      else
        begin
          if(this_bit == 1'b1)
            begin
              DAC_acc_1st = DAC_acc_1st + i_func_extended - (2**15);
              DAC_acc_2nd = DAC_acc_2nd + DAC_acc_1st     - (2**15);
            end
          else
            begin
              DAC_acc_1st = DAC_acc_1st + i_func_extended + (2**15);
              DAC_acc_2nd = DAC_acc_2nd + DAC_acc_1st + (2**15);
            end
          this_bit = ~DAC_acc_2nd[19];
       end
    end
endmodule

I also worked out where the "average" option rather than the "decimate" option was on the Spectrum Analyzer's settings, and got better looking spectrums:
« Last Edit: July 02, 2020, 11:55:12 am by hamster_nz »
Gaze not into the abyss, lest you become recognized as an abyss domain expert, and they expect you keep gazing into the damn thing.
 
The following users thanked this post: SMB784

Offline BrianHG

  • Super Contributor
  • ***
  • Posts: 7733
  • Country: ca

I have attached the simulated output, where values of sel=0,1,2,&3 correspond to 1 kHz sine, square, triangle, and sawtooth waveforms.  These waveforms are garbled for half the cycle.  I believe this is because the highest value is overflowing the integer register and causing problems, but I'm not entirely sure.

Were you able to get reasonable output with this implementation?

If you are using System Verilog, you can use the keyword 'signed' when declaring regs and integers, eg:
(snip from code I working with on another thread)
Code: [Select]
//************************************************
// geometry counters
//************************************************
reg signed [11:0]   geo_x;
reg signed [11:0]   geo_y;
reg signed [11:0]   geo_xdir;
reg signed [11:0]   geo_ydir;

It made all the rest of the coding easy rather than having to worry about 2's compliment for thing like adding/subtracting positive & negative numbers.  Especially with IF(x<0) and While(x<0) where I wanted to check for boundaries where a triangle may go off the negative edge of the screen.  Also, using the analog display waveform in the simulator wouldn't pop the line to the top.  (In my simulator, I had to set the register waveform view as 'signed' register as well...)
 
The following users thanked this post: SMB784

Offline SMB784Topic starter

  • Frequent Contributor
  • **
  • Posts: 421
  • Country: us
    • Tequity Surplus
So I coded up a few things and tested.

This Verilog version gives identical results to the VHDL version - set up to use 16-bit signed samples.

Code: [Select]
`timescale 1ns / 1ps
module second_order_dac(
  input wire i_clk,
  input wire i_res,
  input wire [15:0] i_func,
  output wire o_DAC
);

  reg this_bit;
 
  reg [19:0] DAC_acc_1st;
  reg [19:0] DAC_acc_2nd;
  reg [19:0] i_func_extended;
   
  assign o_DAC = this_bit;

  always @(*)
     i_func_extended = {i_func[15],i_func[15],i_func[15],i_func[15],i_func};
   
  always @(posedge i_clk or negedge i_res)
    begin
      if (i_res==0)
        begin
          DAC_acc_1st<=16'd0;
          DAC_acc_2nd<=16'd0;
          this_bit = 1'b0;
        end
      else
        begin
          if(this_bit == 1'b1)
            begin
              DAC_acc_1st = DAC_acc_1st + i_func_extended - (2**15);
              DAC_acc_2nd = DAC_acc_2nd + DAC_acc_1st     - (2**15);
            end
          else
            begin
              DAC_acc_1st = DAC_acc_1st + i_func_extended + (2**15);
              DAC_acc_2nd = DAC_acc_2nd + DAC_acc_1st + (2**15);
            end
          this_bit = ~DAC_acc_2nd[19];
       end
    end
endmodule

I also worked out where the "average" option rather than the "decimate" option was on the Spectrum Analyzer's settings, and got better looking spectrums:

So I have parametrized everything you have created, and this gets pretty close to what i want (i'm still working with unsigned integers because my output only goes from 0 to 5V), although my sine function still has an issue with the -Pi input and +Pi input yielding inverted results:

Code: [Select]
module delta_sigma_DAC_2nd_order
#(
parameter OW,
parameter OSR=4 // 2^OSR = oversampling ratio
)
(
input wire i_clk,
input wire i_res,
input wire [OW-1:0] i_func,
output wire o_DAC
);

reg [((OW-1)+OSR):0] DAC_acc_1st;
reg [((OW-1)+OSR):0] DAC_acc_2nd;
reg [((OW-1)+OSR):0] i_func_ext;
reg ADC_out;

always @*
begin
i_func_ext={{OSR{i_func[OW-1]}},i_func};
end

always @(posedge i_clk or negedge i_res)
begin
if (i_res==0)
begin
DAC_acc_1st<={((OW-1)+OSR){1'd0}};
DAC_acc_2nd<={((OW-1)+OSR){1'd0}};
ADC_out<=1'b0;
end

else
begin
if(ADC_out==1'b1)
begin
DAC_acc_1st=DAC_acc_1st+i_func_ext+{((OW-1)){1'd1}};
DAC_acc_2nd=DAC_acc_2nd+DAC_acc_1st+{((OW-1)){1'd1}};
end
else
begin
DAC_acc_1st=DAC_acc_1st+i_func_ext-{(OW-1){1'd1}};
DAC_acc_2nd=DAC_acc_2nd+DAC_acc_1st-{(OW-1){1'd1}};
end
ADC_out<=DAC_acc_2nd[((OW-1)+OSR)];
end
end

assign o_DAC=ADC_out;

endmodule

However, there is still some overflow/underflow issues at the waveform maxima/minima, as you can see in the attachment.  For some reason I cant seem to get this to go away.  I am pretty stumped as to what is happening here.

Offline hamster_nz

  • Super Contributor
  • ***
  • Posts: 2803
  • Country: nz
However, there is still some overflow/underflow issues at the waveform maxima/minima, as you can see in the attachment.  For some reason I cant seem to get this to go away.  I am pretty stumped as to what is happening here.

Just change your code to this and it will most likely solve both issues.

Code: [Select]
if(ADC_out==1'b1)
begin
DAC_acc_1st=DAC_acc_1st+i_func_ext;
DAC_acc_2nd=DAC_acc_2nd+DAC_acc_1st;
end
else
begin
DAC_acc_1st=DAC_acc_1st+i_func_ext-{OW{1'd1}};
DAC_acc_2nd=DAC_acc_2nd+DAC_acc_1st-{OW{1'd1}};
end

I've just subtracted {(OW-1){1'd1}} from all equations, and then simplified.
Gaze not into the abyss, lest you become recognized as an abyss domain expert, and they expect you keep gazing into the damn thing.
 
The following users thanked this post: SMB784

Offline SMB784Topic starter

  • Frequent Contributor
  • **
  • Posts: 421
  • Country: us
    • Tequity Surplus
However, there is still some overflow/underflow issues at the waveform maxima/minima, as you can see in the attachment.  For some reason I cant seem to get this to go away.  I am pretty stumped as to what is happening here.

Just change your code to this and it will most likely solve both issues.

Code: [Select]
if(ADC_out==1'b1)
begin
DAC_acc_1st=DAC_acc_1st+i_func_ext;
DAC_acc_2nd=DAC_acc_2nd+DAC_acc_1st;
end
else
begin
DAC_acc_1st=DAC_acc_1st+i_func_ext-{OW{1'd1}};
DAC_acc_2nd=DAC_acc_2nd+DAC_acc_1st-{OW{1'd1}};
end

I've just subtracted {(OW-1){1'd1}} from all equations, and then simplified.

So I have implemented this, and it seems to be worse than the last iteration.  I know this must be frustrating, and thanks for all your help so far, it has been really very useful.  I wonder if maybe we are seeing different results because we are using different simulators (i'm using modelsim packaged with Quartus Prime Lite).

Offline Bassman59

  • Super Contributor
  • ***
  • Posts: 2501
  • Country: us
  • Yes, I do this for a living
Just a question before I work on it again, why are you using blocking assignments rather than non-blocking assignments for updating your accumulators (= rather than <=)?  It turns out that if you use non-blocking assignments, the output is garbage, but blocking assignments make it work.  Was that a conscious choice on your part, or just serendipity that it worked out as it was supposed to?

It's common to use variables in VHDL code to break up a long combinatorial statement (the right-hand-side of an assignment) into two or more intermediate assignments simply for readability. Those variables are evaluated in the order in which they appear in the code.

 
The following users thanked this post: SMB784

Offline SMB784Topic starter

  • Frequent Contributor
  • **
  • Posts: 421
  • Country: us
    • Tequity Surplus
Ok, so I almost have it completely correct.  The first problem with the sine wave was that in the sinewave routine I was already shifting its output so that it would have an average of half the dynamic range (2.5V), and this shifting routine clashed with the existing addition/subtraction in the 2nd order DAC, causing the +Pi input to be shifted out of phase with the -Pi input.  Removing that shift routine fixed the sine wave.

Second thing to say is that the accumulators need to be tuned by adding or subtracting a specific amount so that the waveforms take up the whole output dynamic range but dont overflow.  you can see the value of my shift parameter in the code below under the register "mid_DNR":

Code: [Select]
module delta_sigma_DAC_2nd_order
#(
parameter OW,
parameter OSR=8 // 2^OSR = oversampling ratio
)
(
input wire i_clk,
input wire i_res,
input wire [OW-1:0] i_func,
output wire o_DAC
);

reg [((OW-1)+OSR):0] DAC_acc_1st;
reg [((OW-1)+OSR):0] DAC_acc_2nd;
reg [((OW-1)+OSR):0] i_func_ext;
reg [OW-1:0] mid_DNR={(OW-1){1'd1}}+{(OW-6){1'd1}};
reg ADC_out;

always @*
begin
i_func_ext={{OSR{i_func[OW-1]}},i_func};
end

always @(posedge i_clk or negedge i_res)
begin
if (i_res==0)
begin
DAC_acc_1st<={((OW-1)+OSR){1'd0}};
DAC_acc_2nd<={((OW-1)+OSR){1'd0}};
ADC_out<=1'b0;
end

else
begin
if(ADC_out==1'b1)
begin
DAC_acc_1st=DAC_acc_1st+i_func_ext+mid_DNR;
DAC_acc_2nd=DAC_acc_2nd+DAC_acc_1st+mid_DNR;
end
else
begin
DAC_acc_1st=DAC_acc_1st+i_func_ext-mid_DNR;
DAC_acc_2nd=DAC_acc_2nd+DAC_acc_1st-mid_DNR;
end
ADC_out<=DAC_acc_2nd[((OW-1)+OSR)];
end
end

assign o_DAC=ADC_out;

endmodule

Unfortunately this appears to kill the square wave (see attached waveform output) for some reason that I am still trying to figure out.  However, this implementation is almost totally correct at this point!  Just gotta fix the square wave and then we are good.  Thanks again everyone for all your help on this.

Offline SiliconWizard

  • Super Contributor
  • ***
  • Posts: 14472
  • Country: fr
It's hard to tell what exactly is wrong as we don't see your whole code (and one hidden part was already one source of your problems!)

But from the look of what you get, I guess it might still be overflowing.
Have you tried with a wider accumulator width?
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf