Skip to content

Commit

Permalink
Tone generator based on sigma-delta noise shaper
Browse files Browse the repository at this point in the history
  • Loading branch information
asicsforthemasses committed Dec 14, 2021
1 parent cfc5177 commit adc1273
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ generic/blinky.fasm
generic/blinky.png
generic/blinky.posp
generic/blinky.vm
.vscode
26 changes: 26 additions & 0 deletions examples/tonegen/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
YOSYS ?= yosys
NEXTPNR ?= nextpnr-gowin

all: tonegen-tec0117.fs

unpacked: tonegen-tec0117-unpacked.v

clean:
rm -f *.json *.fs *-unpacked.v

.PHONY: all clean

%-tec0117.fs: %-tec0117.json
gowin_pack -d GW1N-9 -o $@ $<

%-tec0117.json: %-tec0117-synth.json tec0117.cst
$(NEXTPNR) --json $< --write $@ --device GW1NR-LV9QN88C6/I5 --cst tec0117.cst

%-tec0117-synth.json: %.v
$(YOSYS) -D LEDS_NR=8 -p "read_verilog $^; synth_gowin -json $@"

%-tec0117-prog: %-tec0117.fs
openFPGALoader -b tec0117 $^

%-tec0117-unpacked.v: %-tec0117.fs
gowin_unpack -d GW1N-9 -o $@ $^
28 changes: 28 additions & 0 deletions examples/tonegen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Sigma-delta based tone generator

This project generates a noise-shaped bitstream on one digital output pin.
By connecting an RC low-pass filter to this pin, the analogue sinusoidal signal is recovered from the bitstream, making this a simple method to get an analogue output from a digital pin.

Note: the noise performance of the generator is directly dependent on the digital noise present on the digital pin. For optimal performance, the digital signal should be re-clocked by an external flip-flop (74LV74 or similar). The external flip-flop must have a very clean supply, separate from the FPGA for optimal noise isolation.

A 16-bit input second-order noise shaper is used, which has a limited noise performance, in addition to the power supply noise issue outlined above. Do not expect stellar performance.

A suitable RC reconstruction filter can be made from a 100 ohm resistor and a 1uF capacitor:

```
From FPGA
+--------------+
o--------| R = 100 Ohms |--------|-------------------o Output
+--------------+ |
|
+---------+
+---------+ C=1 uFarad
|
|
|
-------
-----
---
```
127 changes: 127 additions & 0 deletions examples/tonegen/cordic.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// pipelined CORDIC algorithm to calculate sin/cos pair from a given angle (0..1)
// Author: Niels A. Moseley
//


// one stage of the cordic iteration with registered outputs
module cordic_stage_16(clk, rst_n, x_in, y_in, angle_in, angle_adj, x_out, y_out, angle_out);
parameter SHIFT = 1;

// inputs
input clk;
input rst_n;
input signed [16-1:0] x_in;
input signed [16-1:0] y_in;
input signed [16-1:0] angle_in;
input signed [16-1:0] angle_adj;

// outputs
output reg signed [16-1:0] x_out;
output reg signed [16-1:0] y_out;
output reg signed [16-1:0] angle_out;

// internal signal
reg signed [16-1:0] new_x;
reg signed [16-1:0] new_y;
reg signed [16-1:0] new_angle;

wire sign;
wire signed [16-1:0] shifted_x;
wire signed [16-1:0] shifted_y;

assign sign = angle_in[16-1]; // angle sign bit
assign shifted_x = x_in >>> SHIFT;
assign shifted_y = y_in >>> SHIFT;

always @(*)
begin
new_x = sign ? (x_in + shifted_y) : (x_in - shifted_y);
new_y = sign ? (y_in - shifted_x) : (y_in + shifted_x);
new_angle = sign ? (angle_in + angle_adj) : (angle_in - angle_adj);
end

always @(posedge clk)
begin
if (rst_n == 1'b0)
begin
x_out <= 0;
y_out <= 0;
angle_out <= 0;
end
else begin
x_out <= new_x;
y_out <= new_y;
angle_out <= new_angle;
end
end

endmodule


module cordic_10_16(clk, rst_n, angle_in, cos_out, sin_out);

// inputs
input clk;
input rst_n;
input signed [16-1:0] angle_in;

// outputs
output signed [16-1:0] cos_out;
output signed [16-1:0] sin_out;

// internal signals
reg signed [16-1:0] x_in;
reg signed [16-1:0] y_in;
reg signed [16-1:0] z_in;

wire signed [16-1:0] xbus [0:10-1];
wire signed [16-1:0] ybus [0:10-1];
wire signed [16-1:0] zbus [0:10-1];

assign cos_out = xbus[10-1];
assign sin_out = ybus[10-1];

always @(*)
begin
case($unsigned(angle_in[16-1:16-2]))
2'b00:
begin
x_in <= 16'd19897;
y_in <= 0;
z_in <= angle_in;
end
2'b11:
begin
x_in <= 16'd19897;
y_in <= 0;
z_in <= angle_in;
end
2'b01:
begin
x_in <= 0;
y_in <= 16'd19897;
z_in <= $signed({2'b00, angle_in[16-3:0]});
end
2'b10:
begin
x_in <= 0;
y_in <= -16'd19897;
z_in <= $signed({2'b11, angle_in[16-3:0]});
end
endcase
end

// generate instances of cordic_stage
cordic_stage_16 #(0) stage0(clk, rst_n, x_in, y_in, z_in, 16'sd8192, xbus[0], ybus[0], zbus[0]);
cordic_stage_16 #(1) stage1(clk, rst_n, xbus[0], ybus[0], zbus[0], 16'sd4836, xbus[1], ybus[1], zbus[1]);
cordic_stage_16 #(2) stage2(clk, rst_n, xbus[1], ybus[1], zbus[1], 16'sd2555, xbus[2], ybus[2], zbus[2]);
cordic_stage_16 #(3) stage3(clk, rst_n, xbus[2], ybus[2], zbus[2], 16'sd1297, xbus[3], ybus[3], zbus[3]);
cordic_stage_16 #(4) stage4(clk, rst_n, xbus[3], ybus[3], zbus[3], 16'sd651, xbus[4], ybus[4], zbus[4]);
cordic_stage_16 #(5) stage5(clk, rst_n, xbus[4], ybus[4], zbus[4], 16'sd326, xbus[5], ybus[5], zbus[5]);
cordic_stage_16 #(6) stage6(clk, rst_n, xbus[5], ybus[5], zbus[5], 16'sd163, xbus[6], ybus[6], zbus[6]);
cordic_stage_16 #(7) stage7(clk, rst_n, xbus[6], ybus[6], zbus[6], 16'sd81, xbus[7], ybus[7], zbus[7]);
cordic_stage_16 #(8) stage8(clk, rst_n, xbus[7], ybus[7], zbus[7], 16'sd41, xbus[8], ybus[8], zbus[8]);
cordic_stage_16 #(9) stage9(clk, rst_n, xbus[8], ybus[8], zbus[8], 16'sd20, xbus[9], ybus[9], zbus[9]);


endmodule
72 changes: 72 additions & 0 deletions examples/tonegen/sddac.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Second order sigma-delta dac
//
// For benchmarking purposes only -- don't use this for an actual design.
// There are far more performant architectures.
//
// Author: Niels A. Moseley, [email protected]
//

`ifdef DEBUG_SDDAC
`include "constants.vams"
`endif

module sddac(clk, rst_n, sig_in, sd_out);

// inputs
input clk; // clock
input rst_n; // synchronous reset, active low
input signed [15:0] sig_in; // 16 bits in Q(1,15) format

// outputs
output reg sd_out = 0;

// internal signals
reg signed [17:0] state1 = 0; // Q(1,17)
reg signed [19:0] state2 = 0; // Q(1,19)
reg signed [16:0] state1_in; // Q(0,17)
reg signed [18:0] state2_in; // Q(0,19)
reg signed [20:0] quant_in; // Q(2,19)
reg signed [16:0] qq;
reg [7:0] lfsr_reg = 0;
reg quantizer;
wire lfsr_fb;

// linear feedback shift register feedback
assign lfsr_fb = (lfsr_reg[4] ^ lfsr_reg[2]);

// combination process
always @(*)
begin
`ifdef DEBUG_SDDAC
qq = $signed(quantizer ? -17'h8000 : 17'h8000);
`endif
quant_in = state2 + $signed(lfsr_fb ? -21'h4000 : 21'h4000);
quantizer = quant_in[20];
state1_in = sig_in - $signed(quantizer ? -17'h8000 : 17'h8000); // Q(-1,17) - Q(0,17) -> Q(0,17)
state2_in = state1 - $signed(quantizer ? -19'h10000 : 19'h10000); // Q(-1,19) - Q(0,19) -> Q(0,19)
end

// clocked process
always @(posedge clk)
begin
if (rst_n == 1'b0)
begin
state1 <= 0;
state2 <= 0;
lfsr_reg <= 8'hff;
end
else begin
`ifdef DEBUG_SDDAC
$display("feedback : %f", qq*$pow(2.0,-15));
$display("state1_in: %f", state1_in*$pow(2.0,-17));
$display("state2_in: %f", state2_in*$pow(2.0,-19));
$display("");
`endif
state1 <= state1 + $signed({ state1_in[16], state1_in});
state2 <= state2 + $signed({ state2_in[18], state2_in});
sd_out <= !quantizer;
lfsr_reg <= {lfsr_reg[6:0], lfsr_fb};
end
end

endmodule
19 changes: 19 additions & 0 deletions examples/tonegen/tec0117.cst
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//Copyright (C)2014-2019 Gowin Semiconductor Corporation.
//All rights reserved.
//File Title: Physical Constraints file
//GOWIN Version: V1.9.1.01Beta
//Part Number: GW1N-LV1QN48C6/I5
//Created Time: Sun Dec 15 11:40:09 2019

IO_LOC "clk" 35;
IO_LOC "rst_n" 77;
IO_LOC "sd_out" 47; // PMOD header pin 1

IO_LOC "led[0]" 86;
IO_LOC "led[1]" 85;
IO_LOC "led[2]" 84;
IO_LOC "led[3]" 83;
IO_LOC "led[4]" 82;
IO_LOC "led[5]" 81;
IO_LOC "led[6]" 80;
IO_LOC "led[7]" 79;
48 changes: 48 additions & 0 deletions examples/tonegen/top.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Top level of signal generator
//
// Author: Niels A. Moseley, [email protected]
//

module top(clk, rst_n, sd_out, led);

// inputs
input clk; // clock 12 MHz
input rst_n; // synchronous reset, active low

// outputs
output sd_out; // noise shaper output
output [7:0] led; // leds for debugging

reg [23:0] phase_accu = 24'd0;

// frequency = 12e6 / 2^32 * phase_inc

reg [23:0] phase_inc = 24'd5592; // approximately 1kHz at 12MHz system clock
wire signed [15:0] sinusoid;

// phase accumulator to drive the cordic
always @(posedge clk)
begin
phase_inc <= phase_inc + 24'd1;
phase_accu <= phase_accu + phase_inc[23:14];
end;

cordic_10_16 cordic
(
.clk(clk),
.rst_n(rst_n),
.angle_in(phase_accu[23:8]),
.cos_out(sinusoid)
);

sddac dac
(
.clk(clk),
.rst_n(rst_n),
.sig_in( {sinusoid[15], sinusoid[15:1]} ),
.sd_out(sd_out)
);

assign led = phase_accu[7:0];

endmodule

0 comments on commit adc1273

Please sign in to comment.