How to generate sine wave in FPGA?

 Here are the most practical ways to generate a sine wave on an FPGA—from “quick & clean” to “resource-lean.” I’ll give you design rules, formulas, and drop-in HDL.



The main approaches (pick one)

MethodWhen to useNotes
DDS/NCO + LUTEasiest, precise frequency controlPhase accumulator → lookup table. Best with an external DAC or 1-bit ΣΔ + RC.
CORDICYou want to save BRAM / make sin/cos on the flyUses add/shift stages; needs scaling factor compensation.
PWM or ΣΔ DACNo external DAC availableFilter the pin with RC/active LPF. ΣΔ generally cleaner than plain PWM.

1) DDS / NCO + LUT (golden path)

How it works

  • Phase accumulator: phase <= phase + phase_inc;

  • Top M bits of phase index a sine LUT (ROM/BRAM).

  • Frequency:

    fout=phase_inc2Nfclkf_\text{out} = \frac{\text{phase\_inc}}{2^N}\,f_\text{clk}
  • Resolution example (N=32, fclk=100 MHz): ~0.0233 Hz steps.

Compute phase_inc: round(f_out * 2^N / f_clk)
– 1 MHz @ 100 MHz, N=32 → 42949673
– 440 Hz @ 100 MHz, N=32 → 18898

Minimal SystemVerilog (NCO + LUT, optional linear interpolation)

module nco_sine #( parameter int N = 32, // phase accumulator width parameter int ADDR_BITS = 12, // LUT points = 2^ADDR_BITS parameter int AMP_BITS = 12 // sample width )( input logic clk, input logic [N-1:0] phase_inc, output logic [AMP_BITS-1:0] sample // unsigned 0..(2^AMP_BITS-1) (shift if you need signed) ); // Phase accumulator logic [N-1:0] phase; always_ff @(posedge clk) phase <= phase + phase_inc; // Use top ADDR_BITS as LUT address; next L bits as frac for interpolation localparam int FRAC_BITS = 4; // tweak 0..4; 0 = no interpolation logic [ADDR_BITS-1:0] addr = phase[N-1 -: ADDR_BITS]; logic [FRAC_BITS-1:0] frac = phase[N-1-ADDR_BITS -: FRAC_BITS]; // ROM: 2^ADDR_BITS × AMP_BITS; quarter-wave compression is optional. // Initialize from a hex file generated offline (0..(2^AMP_BITS-1) sine). logic [AMP_BITS-1:0] lut [0:(1<<ADDR_BITS)]; initial $readmemh("sine12_4096.hex", lut); // Fetch two adjacent samples for linear interpolation logic [AMP_BITS-1:0] y0, y1; always_ff @(posedge clk) begin y0 <= lut[addr]; y1 <= lut[addr + 1]; // ROM has one extra guard entry copied from index 0 end // Linear interpolation (optional) logic signed [AMP_BITS:0] diff = $signed({1'b0,y1}) - $signed({1'b0,y0}); logic [AMP_BITS+FRAC_BITS:0] interp = ({y0,{FRAC_BITS{1'b0}}}) + (diff <<< FRAC_BITS) * frac; // scaled always_ff @(posedge clk) sample <= interp[AMP_BITS+FRAC_BITS -: AMP_BITS]; // drop frac bits endmodule

Notes

  • If you prefer quarter-wave LUT to save BRAM: store 1/4 cycle and mirror/negate by the two MSBs (quadrant).

  • To output signed samples, center them around 0 (e.g., subtract 2^(AMP_BITS-1)).

  • For sin & cos, just tap different phase offsets (cos = sine with +90° phase).

Getting it out of the FPGA

  • Best quality: send samples to a parallel or SPI DAC, then analog-filter.

  • No DAC? Use a 1-bit ΣΔ DAC to a pin + RC/active low-pass filter (see below).

  • Clocking: Derive f_clk from a PLL/MMCM with low jitter if spurs matter.


2) 1-bit ΣΔ DAC (cleaner than PWM)

A simple second-order ΣΔ pushes noise up in frequency; RC filter recovers the sine.

module sd_dac_1bit #( parameter int W = 16 // input width )( input logic clk, input logic [W-1:0] x, // unsigned or signed; keep consistent output logic bitout // drive to a GPIO pin ); // Two accumulators (MASH-like) logic [W:0] acc1, acc2; always_ff @(posedge clk) begin acc1 <= acc1[W-1:0] + x; // integrate input acc2 <= acc2[W-1:0] + acc1[W:0]; // integrate again bitout <= acc2[W]; // MSB as 1-bit output end endmodule

RC example for ~10 kHz cutoff: R=3.3 kΩ, C=4.7 nF (fc ≈ 10.3 kHz). Use a higher ΣΔ clock (e.g., 25–100 MHz) so most noise is above the audio band. For better THD/IMD, consider a 2-pole or active LPF.


3) CORDIC (no LUT)

Computes sin/cos by iterative rotate-add-shift. Great when BRAM is tight or you need variable amplitude/phase with small footprint.

  • Scale factor: rotation mode gains 1/K ≈ 1.64676; compensate by pre-scaling inputs or right-shifting after the pipeline (K ≈ 0.607252935).

  • Latency: ≈ number of iterations (e.g., 16–20 stages), but fully pipelinable (1 result/cycle).

  • Excellent for NCO/mixer chains (sin/cos from a single phase word).


4) Plain PWM (works, but noisier)

  • Use a fast carrier (e.g., ≥200 kHz) and modulate duty with your sine samples.

  • THD is generally worse than ΣΔ; filter must strongly attenuate the carrier.


Practical design checklist

  • Pick your clock (f_clk) and accumulator width (N). Wider N → finer frequency steps & lower phase-truncation spurs.

  • LUT depth vs. spurs: 1–4 k points is typical. Interpolation (even 4 frac bits) helps a lot.

  • Dither the truncated phase bits to reduce pattern spurs if needed.

  • Amplitude scaling: fixed-point multiply your sine by a gain for volume control/AM.

  • Output stage: prefer a real DAC if you need low noise; otherwise ΣΔ + LPF.

  • Constraints: add timing constraints for the NCO/ΣΔ paths; keep ROM and adders near each other to meet Fmax.


Tiny “full path” example (NCO → ΣΔ → pin)

module sine_out_top( input logic clk100, // 100 MHz output logic audio_pin // 1-bit DAC out ); // 1 kHz tone @ 100 MHz, N=32 localparam int N=32; localparam int ADDR_BITS=12, AMP_BITS=16; localparam int PHASE_INC = 32'd42949673; // ≈1 MHz -> change to 42950 for ~1 kHz, or compute offline logic [AMP_BITS-1:0] sample; nco_sine #(.N(N), .ADDR_BITS(ADDR_BITS), .AMP_BITS(AMP_BITS)) nco(.clk(clk100), .phase_inc(PHASE_INC), .sample(sample)); // 1-bit ΣΔ DAC sd_dac_1bit #(.W(AMP_BITS)) dac(.clk(clk100), .x(sample), .bitout(audio_pin)); endmodule

Hardware: route audio_pin through R=3.3 kΩ to an RC low-pass (e.g., 3.3 kΩ + 4.7 nF to GND). For line-level, buffer with an op-amp LPF.


Vendor IP (if you like clicks over code)

  • AMD/Xilinx DDS Compiler IP (Vivado) and CORDIC IP.

  • Intel/Altera NCO IP.
    These give you parameter GUIs, interpolation, dither, and AXI-Stream ports out of the box.

评论

此博客中的热门博文

Detailed Explanation of STM32 HAL Library Clock System

How To Connect Stm32 To PC?

How to add a GPS sensor to ESP32 for Wokwi?