DSP to ASIC

FFE Implementation

M. A. Kossel et al., "8.3 An 8b DAC-Based SST TX Using Metal Gate Resistors with 1.4pJ/b Efficiency at 112Gb/s PAM-4 and 8-Tap FFE in 7nm CMOS," 2021 IEEE International Solid-State Circuits Conference (ISSCC), San Francisco, CA, USA, 2021, pp. 130-132 [https://sci-hub.ru/10.1109/ISSCC42613.2021.9365784]

TODO πŸ“…

DFE Implementation

Synopsys Italia, Tech Talk: Introduction to DSP-based SerDes [https://youtu.be/puEP0DlVZGI]

Chen, Kuan-Chang (2022) Energy-Efficient Receiver Design for High-Speed Interconnects. Dissertation (Ph.D.), California Institute of Technology. [https://thesis.library.caltech.edu/14318/9/chen_kuan-chang_2022_thesis_final.pdf]

image-20250524224829419

Parallel implementation

image-20250524235031104

image-20250525101922485

Loop-Unrolling DFE

image-20250525105017605

image-20250525191101301

Corresponding to the three distinct voltage thresholds in the PAM4 systems, it would need 12 slicers, 3 multiplexers, and one thermometer-to-binary decoder in each deserialized data path, even if only one tap of the DFE is unrolled

Look-Ahead Multiplexing DFE

image-20250525151918214

The look-ahead multiplexing technique brings the key benefit that the timing constraint can be significantly relaxed, as the iteration bound is doubled at the expense of extra hardware

image-20250525192228275

time-division data conversion

wide_to_narrow

image-20261024105219428

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
module wide_to_narrow
(
clk2x,
clk1x,
datain,
dataout_clk2x
);

input clk2x;
input clk1x;
input [31:0] datain;
output [15:0] dataout_clk2x;

// **********************************************

reg [31:0] datain_sync;
reg [15:0] dataout_clk2x;
wire [15:0] dataout_clk2x_nxt;

// Flop the data first with clk2x. Reset is not required as it is a datapath and
// default (reset) value of the flops are don't care.
// ************************************************

always @(posedge clk2x)
begin
datain_sync <= datain;
end

// Select the lower and upper halves from datain_sync
// ************************************************

assign dataout_clk2x_nxt = !clk1x ? datain_sync[15:0] : datain_sync[31:16];

// Flop the selected 16 bit data with clk2x and drive out
// ************************************************

always @(posedge clk2x)
begin
dataout_clk2x <= dataout_clk2x_nxt;
end

endmodule

wide_to_narrow

narrow_to_wide

image-20261024105422220

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
module narrow_to_wide
(
clk2x,
clk1x,
data16,
data32_clk1x
);

input clk2x;
input clk1x;
input [15:0] data16;
output [31:0] data32_clk1x;

// ************************************************

reg [15:0] data16_tmp;
reg [31:0] data32_clk1x, data32_clk1x_nxt;

/*
Store temporary data into a register so that we can use 16 bits
of incoming data along with the 16 bits of tmp data stored in
previous cycle to form 32 bits of data.

These 32 bits of data is flopped with clk1x, and this happens
for every two-clock periods of clk2x.

When data is passed from 2x (fast) to the 1x (slow) domain,
make sure that there is enough delay in the data path to avoid
set-up/hold violation in the immediate rising edge of 1x clock.
The data should have more delay in the path to pass beyond
the immediate rising edge of 1x clock.
*/

// ************************************************

always @(posedge clk2x)
begin
data16_tmp <= #2 data16;
end

// Form the 32-bit data
assign data32_clk1x_nxt = {data16[15:0], data16_tmp[15:0]};

// Flop the selected 16 bit data with clk2x and drive out
always @(posedge clk1x)
begin
data32_clk1x <= data32_clk1x_nxt;
end

endmodule

narrow_to_wide

MASH 1-1-1 Implementation

MASH-1-1-1 Noise-Shaping Truncator (DLF β†’ DAC) [https://github.com/raytroop/dsmwk]

Digital block between a 20-bit digital loop filter (DLF) output and an 8-bit DAC input. The 12 truncated LSBs are pushed through a MASH-1-1-1 delta-sigma modulator so the truncation error is 3rd-order noise-shaped instead of lost:

1
2
3
dac(z) = dlf(z)/2^12  -  (1 - z^-1)^3 Β· q(z)              (Γ— z^-1 latency)

q β‰œ e3/2^12 ∈ [0,1) LSBβ‚ˆ, σ² β‰ˆ 1/12 LSBβ‚ˆΒ²

where e3 is the stage-3 accumulator residue in raw 20-bit-LSB counts (0…4095, quantizer step 2ΒΉΒ²) and q is the same error referred to the 8-bit output grid β€” the unit-step quantization noise of the effective 1-LSBβ‚ˆ quantizer.

dac(z) = dlfMSB8 (z)/212 + dlfLSB12 (z)/212+ q(z) Β· (1 - z-1)3 = dlf(z)/212 + q(z) Β· (1 - z-1)3

1
2
3
4
5
6
7
dlf_in[19:12] ─────────────────────────────────┐
β–Ό
dlf_in[11:0] ──► ACC1 ──c1──────────────────► (+) ──sat[0,255]──► reg ──► dac_out[7:0]
β”‚r1 β–²
└──► ACC2 ──c2──(1-z⁻¹)───────
β”‚r2 β”‚
└──► ACC3 ──c3──(1-z⁻¹)Β²
flowchart LR
    IN["dlf_in<br/>20 bits"] -->|"upper 8 bits"| XMSB["x_msb<br/>coarse value"]
    IN -->|"lower 12 bits"| A1["Stage 1<br/>acc1 + x_frac"]

    R1[("acc1<br/>register")] --> A1
    A1 -->|"residue r1"| R1
    A1 -->|"r1"| A2["Stage 2<br/>acc2 + r1"]
    A1 -->|"carry c1"| NC["Noise cancellation<br/>c1 + c2 βˆ’ c2_d1<br/>+ c3 βˆ’ 2c3_d1 + c3_d2"]

    R2[("acc2<br/>register")] --> A2
    A2 -->|"residue r2"| R2
    A2 -->|"r2"| A3["Stage 3<br/>acc3 + r2"]
    A2 -->|"carry c2"| NC
    A2 -->|"c2"| C2D1[("c2_d1<br/>z⁻¹")]
    C2D1 -->|"βˆ’c2_d1"| NC

    R3[("acc3<br/>register")] --> A3
    A3 -->|"residue r3"| R3
    A3 -->|"carry c3"| NC
    A3 -->|"c3"| C3D1[("c3_d1<br/>z⁻¹")]
    C3D1 -->|"βˆ’2c3_d1"| NC
    C3D1 --> C3D2[("c3_d2<br/>z⁻²")]
    C3D2 -->|"+c3_d2"| NC

    NC -->|"signed correction y<br/>βˆ’3…+4"| ADD["x_msb + y"]
    XMSB --> ADD
    ADD --> SAT["Clamp to 0…255"]
    SAT --> OUTREG[("Output register")]
    OUTREG --> OUT["dac_out<br/>8 bits"]
Branch Expression Range
stage 1 c1 {0, 1}
stage 2 (1βˆ’z⁻¹)Β·c2 = c2 βˆ’ c2_d1 {βˆ’1, 0, +1}
stage 3 (1βˆ’z⁻¹)Β²Β·c3 = c3 βˆ’ 2Β·c3_d1 + c3_d2 [βˆ’2, +2]
total y [βˆ’3, +4]
1
2
3
4
5
wire signed [5:0] y =
$signed({5'b0, c1})
+ $signed({5'b0, c2}) - $signed({5'b0, c2_d1})
+ $signed({5'b0, c3}) - $signed({4'b0, c3_d1, 1'b0}) // 2*c3_d1
+ $signed({5'b0, c3_d2});

Each carry is a bare bit {0,1}. {5'b0, c1} zero-extends it to 6 bits (value unchanged), and $signed(...) marks it for signed arithmetic. The term {4'b0, c3_d1, 1'b0} is c3_d1 with a zero appended β€” a shift-left-by-1, i.e. 2Β·c3_d1 (value 0 or 2) β€” so the multiply costs nothing.

The reachable values of y and their actual bit patterns:

value y[5:0] value y[5:0]
+4 000100 0 000000
+3 000011 βˆ’1 111111
+2 000010 βˆ’2 111110
+1 000001 βˆ’3 111101

Worked example: c1=0, c2=0, c2_d1=1, c3=0, c3_d1=1, c3_d2=0 β†’ y = 0 + (0βˆ’1) + (0βˆ’2+0) = βˆ’3 β†’ 111101.

clamp [-3, 259] to [0, 255]

For msb ∈ [3, 251], x_msb + y never leaves [0,255], the mux always takes the pass-through leg, and dac_sat ≑ dac_sum bit-for-bit.

1
2
3
4
5
6
7
8
9
10
11
12
13
//-------------------------------------------------------------------------
// Recombine and saturate: dac = clamp(x_msb + y, 0, 2^OUT_W-1)
// sum range [-3, 2^OUT_W-1+4] -> (OUT_W+2)-bit signed is sufficient
// (needs 2^(OUT_W+1) >= 2^OUT_W + 4, true for any OUT_W >= 2)
//-------------------------------------------------------------------------
// explicit sign-extension of y to adder width (assumes OUT_W >= 5)
wire signed [OUT_W+1:0] y_ext = {{(OUT_W-4){y[5]}}, y};
wire signed [OUT_W+1:0] dac_sum = $signed({2'b0, x_msb}) + y_ext;

wire [OUT_W-1:0] dac_sat =
dac_sum[OUT_W+1] ? {OUT_W{1'b0}} : // negative -> 0
dac_sum[OUT_W] ? {OUT_W{1'b1}} : // > max -> 2^OUT_W-1
dac_sum[OUT_W-1:0];

In a locked PLL the DLF integrator sits mid-range and the clamp never fires β€” cost: zero. It engages only during acquisition/slew, where noise is irrelevant and its job is exactly right: drive the DAC monotonically to the rail without wrapping



overall ENOB calculation

1
2
SNR  = P_sig / (P_q,in + P_dsm)          (powers add β€” sources are independent)
ENOB = (SNR_dB βˆ’ 1.76) / 6.02

with everything in the same units (LSBβ‚ˆΒ² is convenient) and β€” critically β€” both noise terms integrated in-band only (0 … f_bw, i.e. what survives the analog filter):

Term Expression (LSBβ‚ˆΒ², fs = f_clk)
Signal (full-scale sine convention) P_sig = FSΒ²/8 = 256Β²/8 = 8192
DSM shaped noise, in-band P_dsm = (1/12)Β·(π⁢/7)Β·OSR⁻⁷
Input quantization noise, in-band P_q,in = (2⁻¹²)²/12 · κ

Sanity check at OSR = 50 (static input, ΞΊ=1): P_q,in = 4.97e-9, P_dsm = 1.47e-11 β†’ SNR = 122.1 dB β†’ ENOB = 19.98 β€” the DSM costs only 0.13 dB against the ideal 20.00. At OSR = 10: DSM dominates, ENOB = 16.1

RTL module

Wei Yet, MakerCode RTL Challenge [https://github.com/Weiyet/MakerCode_RTLChallenge]

decimation_filter

Filter Equation

1
2
filtered[n] = (1*x[n] + 3*x[n-1] + 3*x[n-2] + 1*x[n-3]) / 8
y[k] = filtered[k*DECIMATION_FACTOR]
1
2
3
4
5
iverilog -g2012 -o sim.vvp solution.sv tb.sv

vvp sim.vvp +VCDFILE=output.vcd


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
module decimation_filter #(
parameter DATA_WIDTH = 8,
parameter DECIMATION_FACTOR = 4
)(
input wire clk,
input wire reset,
input wire signed [DATA_WIDTH-1:0] data_in,
input wire data_valid_in,
output wire signed [DATA_WIDTH-1:0] data_out,
output wire data_valid_out
);

localparam COUNTER_WIDTH = $clog2(DECIMATION_FACTOR);

// FIR filter coefficients: [1, 3, 3, 1] (normalized by 8)
reg signed [DATA_WIDTH-1:0] x1, x2, x3;
reg signed [DATA_WIDTH+3:0] filtered;
reg [COUNTER_WIDTH-1:0] sample_counter;
reg valid_out_reg;
reg signed [DATA_WIDTH-1:0] output_reg;

always @(posedge clk) begin
if (reset) begin
x1 <= {DATA_WIDTH{1'b0}};
x2 <= {DATA_WIDTH{1'b0}};
x3 <= {DATA_WIDTH{1'b0}};
sample_counter <= {COUNTER_WIDTH{1'b0}};
valid_out_reg <= 1'b0;
output_reg <= {DATA_WIDTH{1'b0}};
end else if (data_valid_in) begin
// Update delay line
x3 <= x2;
x2 <= x1;
x1 <= data_in;

// Apply FIR filter: (1*x[n] + 3*x[n-1] + 3*x[n-2] + 1*x[n-3]) / 8
filtered = data_in + (x1 << 1) + x1 + (x2 << 1) + x2 + x3;

// Increment sample counter
if (sample_counter == DECIMATION_FACTOR - 1) begin
sample_counter <= {COUNTER_WIDTH{1'b0}};
valid_out_reg <= 1'b1;
output_reg <= filtered >>> 3; // Divide by 8
end else begin
sample_counter <= sample_counter + 1'b1;
valid_out_reg <= 1'b0;
end
end else begin
valid_out_reg <= 1'b0;
end
end

assign data_out = output_reg;
assign data_valid_out = valid_out_reg;

endmodule

Line 34 uses non-blocking assignment:

1
2
3
x1 <= data_in;   // line 34: scheduled, NOT applied yet
...
filtered = data_in + (x1<<1) + x1 + ...; // line 37: x1 still holds OLD value

Non-blocking assignments don't take effect until the end of the current time step (after all the blocking statements in the block have run). So at line 37:

  • x1 still holds its previous value β€” i.e. the sample from the last data_valid_in cycle, which is x[n-1].
  • data_in is the current sample, x[n].

They are different values. That's deliberate and necessary for the FIR math to be correct:

1
2
filtered = 1*x[n]   + 3*x[n-1] + 3*x[n-2] + 1*x[n-3]
= data_in + 3*x1 + 3*x2 + 1*x3
Symbol Value at line 37 Filter tap
data_in x[n] (current) coeff 1
x1 x[n-1] coeff 3
x2 x[n-2] coeff 3
x3 x[n-3] coeff 1

image-20260619230018566

gray-code

1
2
3
function automatic logic [3:0] bin2gray(input logic [3:0] bin);
bin2gray = bin ^ (bin >> 1);
endfunction
1
2
3
4
5
6
function automatic logic [3:0] bin2gray(input logic [3:0] bin);
assign bin2gray[3] = bin[3];
assign bin2gray[2] = bin[3] ^ bin[2];
assign bin2gray[1] = bin[2] ^ bin[1];
assign bin2gray[0] = bin[1] ^ bin[0];
endfunction

Sign Extension In Verilog

[https://www.ece.ucdavis.edu/~bbaas/281/notes/Handout.sign.extension.pdf]

image-20260815014502338

image-20260815014531762

image-20260815015049513

reference

K. K. Mishra, Advanced Chip Design: Practical Examples in Verilog. CreateSpace Independent Publishing Platform, 2013

Wei Yet, MakerCode RTL Challenge [https://github.com/Weiyet/MakerCode_RTLChallenge]

Bevan Baas, EEC281 VLSI Digital Signal Processing, [https://www.ece.ucdavis.edu/~bbaas/281/]

Mark Horowitz. EE371: Advanced VLSI Circuit Design Spring 2006-2007 [https://web.stanford.edu/class/archive/ee/ee371/ee371.1066/]

Tinoosh Mohsenin. CMPE 691: Digital Signal Processing Hardware Implementation [https://userpages.cs.umbc.edu/tinoosh/cmpe691/]

Keshab K. Parhi [http://www.ece.umn.edu/users/parhi/]

謝秉璇. 2019 η©ι«”ι›»θ·―θ¨­θ¨ˆε°Žθ«– [link]