Jump to content

Second Order Sigma Delta DACs implemented in a FPGA.


hamster

Recommended Posts

I played around with a 1st and 2nd order 12-bit Sigma Delta DAC implemented on the FPGA.

I found the results quite interesting, as the change is pretty simple to implement and the change to the noise on the output spectrum is quite significant, with lowered 2nd harmonic and a much smother noise floor.

VHDL code is on GitHub at https://github.com/hamsternz/second_order_sigma_delta

first_order.jpg

second_order.jpg

Link to comment
Share on other sites

@hamster,

Impressive!

Would you mind sharing the hardware you chose to use?  The clock speed of the delta-sigma converter?  It also looks like you biased your sine wave negative by a half a sample.  Out of curiosity, was this on purpose or just the result of the way you truncated floats to integers when generating your table?

I'm a bit surprised by the second harmonic you show in your pictures.  I had rather thought this technique would've  done better than that.  Do you have any suggestions as to what might have caused that harmonic?

Either way, good fun, and thanks for sharing!

Dan

Link to comment
Share on other sites

BASYS3 + PMOD Breadboard + Analog Discovery 2.

It was just a hack, so the table was a quick formula in a spreadsheet, yes, I assume the + and - sides are both rounding towards zero causing some asymmetry, but with 11 significant bits that should be somewherere about -60dB at a guess.

Most of the noise is just the shoddy physical implementation. Flying wires on a breadboard, on PMODs, just the shielded wires on the AD2 and so on.  If I leave a wire hanging around  it will pick up most the noise too, maybe 6dB lower than on the channel that is under measurement.

This was just a quick experiment, just using the 100MHz clock rate. If I use a slower clock (e.g. update only every 8 cycles so 12.5MHz) the noise floor actually drops a lot..

Also using the AD2 on a different laptop to the one programming/powering the FPGA removes a lot of noise too. I assume that this is due to voltage drops and noise on the USB cables.

Plenty of room for experimentation and improvement.

Link to comment
Share on other sites

Measurement technique is certainly critical to successful analysis of how your hardware is performing. So is the selection of test equipment.

I see that your most recent plots show a high DC component. As Sigma-Delta Modulators aren't DC accurate is appears that your are trying to do measurements using direct coupling. An AC coupled load would seem to me to be more appropriate. This  raises the topic of source and load impedance.

Edited by zygot
Link to comment
Share on other sites

I glanced at your code.

The lines like this one bother me: new_val := dac1_accum + sample - 2048;

I wonder how Vivado implemented this. Usually, in my experience a better way to do this is to pipeline so that each '+' operation is performed on a separate pipeline version of the signal. From a coding viewpoint it appears to be straightforward. From an implementation viewpoint it looks like you have in implied latch, at least. Regardless, you are trying to perform 2 add or one add and 1 subtract in a clock cycle. Even for a quick prototype exercise this isn't a good idea. Sometimes our HDL code gets written by the parts of our brain wired for C. It's dangerous.

Link to comment
Share on other sites

7 hours ago, zygot said:

I glanced at your code.

The lines like this one bother me: new_val := dac1_accum + sample - 2048;

I wonder how Vivado implemented this. Usually, in my experience a better way to do this is to pipeline so that each '+' operation is performed on a separate pipeline version of the signal. From a coding viewpoint it appears to be straightforward. From an implementation viewpoint it looks like you have in implied latch, at least. Regardless, you are trying to perform 2 add or one add and 1 subtract in a clock cycle. Even for a quick prototype exercise this isn't a good idea. Sometimes our HDL code gets written by the parts of our brain wired for C. It's dangerous.

I can't see the latching issues, but agree that if the +/-2048 was a separate signal, then the code could be simplified quite a bit... however, the optimizer should be doing that at the moment. "Premature optimization is the root of all evil" and so on.

One other finer point. As currently written +full scale value will generate a stream of all ones output but a -full scale won't generate all zero outputs, but a zero value gives a perfect 50:50 mix of ones and zeros.

Others might need it that -full scale gives all zeros, and +full scale gives all ones, but a zero value will give slightly more zeros than ones.

 

Link to comment
Share on other sites

26 minutes ago, zygot said:

Mull over your sine LUT. BTW, why did you replicate 2 cycles instead of just using 1 in your LUT?

 

Because I am quite happy to use a whole block of RAM rather than debugging indexing and sign-flipping code... :D It's actually one full cycle (half positive, half negative). It is jsut a column from a Google docs spreadsheet https://docs.google.com/spreadsheets/d/13srKHRNCD2dfbMglMvvCUESHR23kHWlzAJ24MJ_erzc/edit?usp=sharing

I could have also got away with just one quadrant, but then it would be more 'active' code on what I'm not interested in playing with

Link to comment
Share on other sites

Hi,

for a DA converter hack, you could also have a look at the one I used here:

There was quite a bit of discussion about reconstruction filtering :). The shown measurements are taken using the on-board XADC, which acts as lowpass filter. I think the first two plot labels are wrong (plot 1, 2 show a sine wave. Plot 3, 4 show an 8-tone test signal)

It's not sigma-delta but open-loop PWM with creative dithering.
The idea is

- I generate a PWM signal simply by comparing the output value MSBs against a periodic ramp (e.g. 4 bit)
- for the lower bits, use pseudorandom numbers

It modulates most of the quantization error up to higher frequencies, where it can be filtered by analog means.

Sigma-Delta is more sophisticated (this one might be 8..10 bit equivalent for an audio signal). The main advantage of the PWM scheme is that it works rail-to-rail, where sigma-delta does not.

Quote

// pseudorandom generation with sufficiently long period

module pn24(i_clk, o_out);
   input wire         i_clk;
   output reg [23:0]     o_out = 24'h1;
   
   always @(posedge i_clk) begin
      o_out <= o_out >> 1;
      o_out[23] <= o_out[0];
      o_out[22] <= o_out[23] ^ o_out[0];
      o_out[21] <= o_out[22] ^ o_out[0];
      o_out[16] <= o_out[17] ^ o_out[0];
   end   
endmodule

// construct PWM reference with
// - large periodic component (quantization error is modulated up to the repetition frequency and harmonics)
// - small random component (dithering to break up systematic quantization error)

  wire [23:0]     dither2;
   pn24 iDither(.i_clk(CLKDAC), .o_out(dither2));
   reg [23:0]     dither = 24'd0;   
   reg [4:0]     dacCount = 5'd0;
   reg [4:0]     dacCountA = 5'd0;
   reg [4:0]     dacCountB = 5'd0;
   reg [17:0]     pwm = 18'd0;


   always @(posedge CLKDAC) begin
      dither <= dither2;      
      dacCount <= dacCount + 5'd1;      
      dacCountA <= dacCount;      
      dacCountB <= dacCount ^ 5'b01010; // break the linear ramp
      pwm <= {dacCountA[3:0], dither[13:0]};  
   end

Missing above: "outputBitReg <= myDacVal >= pwm;

Note that it's feedforward-only so it should be clocked fairly high e.g. 250 MHz on Artix.
The code allows also a 5-bit ramp but I got best results using 4 bits (if in doubt, experiment - depends on the lowpass).

Edited by xc6lx45
Link to comment
Share on other sites

  • 1 year later...

Hello,

I've been experimenting with one bit DAC recently to get a very easy way to output sound from the Basys3.

So, here it is.
It uses a 100MHz clock (even if the output one bit modulation is 12.5MHz).
The input is 20 bit.
The output bit is meant to be sent directly to a physical port.

Before going Verilog I experimented it in a Java benchmark to check stability
- the first integrator u is leaky ( hence the - (u>>>3) ) to limit chaotic behavior for low level sounds.

This is not an intent to replace a genuine converter.
But I think it can be handy if you don't have one at hand and want to experiment with sound generation.

 

`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Engineer: Thierry Rochebois
// 
// Create Date: 22.12.2021 10:31:45
// Module Name: DAC
// Description: A second order one bit audio DAC.
//      inputs: clk   100 MHz clock
//              in    20 bit signed input
//                    (can be refreshed at a rate up to 200kHz)
//      output: out   1 bit out modulated at 12.5MHz 
// 
// Dependencies: NONE
// Revision 0.01 - File Created
// Additional Comments:
// 
//////////////////////////////////////////////////////////////////////////////////

module DAC(
    input clk,               // 100MHz clock
    input signed [19:0] in,  // input (update rate up to 200kHz)
    output reg out           // one bit out modulated at 12.5MHz
    );
    
  reg signed [2:-21] x = 24'b0;  // input cast to q3.21
  reg signed [2:-21] u = 24'b0;  // first integrator
  reg signed [2:-21] v = 24'b0;  // second integrator
  wire signed [2:0] y;  // output for feedback q3.0   -1 or +1 

  always @(posedge clk) begin
    x <= {{4{in[19]}}, in};  // 20 bit input -> q3.21 [-0.5 0.5[
  end

 // 1/8 counter -> 12.5MHz
  reg [2:0] cpt8;
  always @(posedge clk) begin
    cpt8 <= cpt8 - 1;
  end

  //  for feedback    -1 q3.0   +1 q3.0
  assign y = (v > 0) ? 3'b111  : 3'b001;
  
  wire signed [2:-20] s1;
  wire signed [2:-21] s1h;
  assign s1 = {x[2:0] + y, x[-1:-20]};
  assign s1h = {s1[2], s1[2:-20]};
  
  wire signed [2:-20] s2;
  wire signed [2:-21] s2h;
  assign s2 = {u[2:0] + y, u[-1:-20]};
  assign s2h = {s2[2], s2[2:-20]};
  
  //                   100MHz / 8 = 12.5MHz  for the 1 bit DAC
  always @(posedge clk && (cpt8 == 0)) begin
      u <= u + s1h - (u>>>3);     // first integrator with feedback              
      v <= v + s2h;     // second integrator with feedback                 
      out <= v[2];      // output
  end
  
endmodule


Here is an example with some triangle waves :

 

Edited by SmashedTransistors
Link to comment
Share on other sites

  • 3 weeks later...

You can try to add and alter substract noise (or use a noisy DSC) in order to flatten the noise spectrum. Focussing on audio one can reach up to almost a 16 bit quality for music signals following pink noise spectrum. Calibration of the input behaviour has to be done too (Schmitt Trigger issue).

 

Link to comment
Share on other sites

  • 1 year later...
Quote

// for feedback -1 q3.0 +1 q3.0

assign y = (v > 0) ? 3'b111 : 3'b001;

I think there might be a small error here. y should track v[2], the DAC's output, i.e. if v[2]==1 y should be 1, if v[2]==0 y should be -1. In the code above, if v==0, v[2] == 0, but y will be 1. Testing against v[2]==0 or (v>=0) would be more accurate.

I tested with a square wave. I recorded the bitstream, sent it through a low-pass filter, and plotted the magnitude spectrum. In the 'fixed' version (sqr_fft_after), the noise floor is cleaner than before (sqr_fft_before).

I'm not a signal-processing expert by any means. I was just wondering why the output of the 2nd order DAC above was noisier than that of another implementation I tested against (https://github.com/briansune/Delta-Sigma-DAC-Verilog), and it turned out to be the (v>0) test.

Best regards,

Ruben.

sqr_fft_after.png

sqr_fft_before.png

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...