element14 Community
element14 Community
    Register Log In
  • Site
  • Search
  • Log In Register
  • About Us
  • Community Hub
    Community Hub
    • What's New on element14
    • Feedback and Support
    • Benefits of Membership
    • Personal Blogs
    • Members Area
    • Achievement Levels
  • Learn
    Learn
    • Ask an Expert
    • eBooks
    • element14 presents
    • Learning Center
    • Tech Spotlight
    • STEM Academy
    • Webinars, Training and Events
    • Learning Groups
  • Technologies
    Technologies
    • 3D Printing
    • FPGA
    • Industrial Automation
    • Internet of Things
    • Power & Energy
    • Sensors
    • Technology Groups
  • Challenges & Projects
    Challenges & Projects
    • Design Challenges
    • element14 presents Projects
    • Project14
    • Arduino Projects
    • Raspberry Pi Projects
    • Project Groups
  • Products
    Products
    • Arduino
    • Avnet Boards Community
    • Dev Tools
    • Manufacturers
    • Multicomp Pro
    • Product Groups
    • Raspberry Pi
    • RoadTests & Reviews
  • Store
    Store
    • Visit Your Store
    • Choose another store...
      • Europe
      •  Austria (German)
      •  Belgium (Dutch, French)
      •  Bulgaria (Bulgarian)
      •  Czech Republic (Czech)
      •  Denmark (Danish)
      •  Estonia (Estonian)
      •  Finland (Finnish)
      •  France (French)
      •  Germany (German)
      •  Hungary (Hungarian)
      •  Ireland
      •  Israel
      •  Italy (Italian)
      •  Latvia (Latvian)
      •  
      •  Lithuania (Lithuanian)
      •  Netherlands (Dutch)
      •  Norway (Norwegian)
      •  Poland (Polish)
      •  Portugal (Portuguese)
      •  Romania (Romanian)
      •  Russia (Russian)
      •  Slovakia (Slovak)
      •  Slovenia (Slovenian)
      •  Spain (Spanish)
      •  Sweden (Swedish)
      •  Switzerland(German, French)
      •  Turkey (Turkish)
      •  United Kingdom
      • Asia Pacific
      •  Australia
      •  China
      •  Hong Kong
      •  India
      •  Korea (Korean)
      •  Malaysia
      •  New Zealand
      •  Philippines
      •  Singapore
      •  Taiwan
      •  Thailand (Thai)
      • Americas
      •  Brazil (Portuguese)
      •  Canada
      •  Mexico (Spanish)
      •  United States
      Can't find the country/region you're looking for? Visit our export site or find a local distributor.
  • Translate
  • Profile
  • Settings
Music Time
  • Challenges & Projects
  • Project14
  • Music Time
  • More
  • Cancel
Music Time
P14 Music Time Blog Building FPGA-Based Music Instrument Synthesis: A Simple Test Bench Solution
  • Blog
  • Forum
  • Documents
  • Events
  • Polls
  • Files
  • Members
  • Mentions
  • Sub-Groups
  • Tags
  • More
  • Cancel
  • New
Join Music Time to participate - click to join for free!
  • Share
  • More
  • Cancel
Group Actions
  • Group RSS
  • More
  • Cancel
Engagement
  • Author Author: javagoza
  • Date Created: 14 Apr 2023 9:11 PM Date Created
  • Views 8211 views
  • Likes 11 likes
  • Comments 3 comments
  • ADSR
  • musictimech
  • square wave
  • fpga_projects
  • DDFS
  • Direct Digital Frequency Synthesis
  • xilinx
  • sawtooth wave
  • fpga
  • wave
  • triangle wave
  • digilent
  • music time
  • sound
  • arty s7
  • amd
  • noise
  • systemverilog
  • Echo Processor
  • synthesizer
Related
Recommended

Building FPGA-Based Music Instrument Synthesis: A Simple Test Bench Solution

javagoza
javagoza
14 Apr 2023
Building FPGA-Based Music Instrument Synthesis: A Simple Test Bench Solution

Introduction

For the past two months, I have been publishing a series of blogs introducing concepts such as Direct Digital Frequency Synthesis (DDFS), 1-bit Delta Sigma Digital to Analog Converters (DAC), and Attack, Decay, Sustain, Release (ADSR) envelope generators, with the upcoming topic of random noise generation. This project serves as a way to consolidate my initial learnings and create a small test bench for experimentation with time-based sound synthesis and noise generation.

  •  DDFS. Direct Digital Frequency Synthesis for Sound 
  •  FPGA ADSR envelope generator for sound synthesis (I) 
  •  AMD Xilinx 7 series FPGAs XADC 

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

Music Time: Justifying the FPGA-based Music Instrument Synthesis System Design

This system designed as a test bed for the synthesis of musical instruments based on FPGA is closely linked to the concept of "musical time". Timing plays a critical role in the precise generation and synchronization of waveforms, to ensure that musical notes are played at the right time and in precise pitch.

By using the Direct Digital Synthesis (DDFS) technique, digital waveform data points are generated and converted to analog signals, which are then filtered and shaped to produce an audio output.

Additionally, the system uses techniques such as an ADSR envelope generator to shape sound waves over time, allowing for dynamic and expressive musical sounds, allowing something close to the sound of real instruments to be mimicked.

The synchronization of waveforms and effects within the FPGA enables the creation of complex audio effects or multi-timbral musical instruments, further reinforcing the close relationship between the system design and the concept of "music time".


The system

The FPGA-based music instrument synthesis system presented here can produce different types of sound waves, like sine, square, sawtooth, triangle, and noise. These waves can be changed and shaped using a technique called ADSR, which adjusts how the sound starts, fades, sustains, and stops. It uses a method called Direct Digital Synthesis (DDFS) to create these waves digitally. Then, it converts them to analog signals using a 1-bit Delta Sigma Digital to Analog Converter (DAC) and a Low Pass Filter (LPF). This allows us to have precise control over the generated sounds and mimic musical instruments and effects.

The system is based on the Digilent Arty S7 50  development board, which features an AMD Spartan-7 FPGA. It incorporates a simple RC low-pass filter and utilizes six potentiometers for parameter control. The readings from the potentiometers are captured using the XADC component from the series 7 FPGA, allowing control over the audio synthesis parameters. The audio output is connected to an amplified speaker, allowing for the synthesized sounds to be heard in real-time with amplified volume.

image


User interface

The user interface of the system consists of four buttons for selecting different notes, four slide switches for changing the octave, and six analog potentiometers for adjusting various parameters, including the ADSR parameters such as attack time, decay time, sustain level, sustain time, and release time, as well as the waveform type (such as sine, square, sawtooth, triangle, and random noise). This interface allows for control over the synthesized sounds, enabling users to shape the sound and create different musical effects.

6 analog potentiometers to control sound parameters

User interface


HDL Design

The project is heavily inspired by the book "FPGA Prototyping by SystemVerilog Examples" by Pong Chu, which has been instrumental in my introduction to FPGA hardware design using the SystemVerilog hardware description language (HDL).

All the module development in SystemVerilog, as well as the test bench for the modules and synthesis, have been carried out using the AMD Xilinx Vivado development tool.


Main Module

The main module:

  • Instantiates one ILA module for debugging
  • Instantiates one xadc ip module for reading the analog inputs from the Potentiometers voltage dividers
  • Instantiates one ADSR module
  • Instantiates one DDFS module
  • Instantiates one Moving Average LPF module
  • Instantiates one Delta Sigma 1-bit DAC module
  • Converts 5 ADC readings to ADSR parameters for the ADSR module
  • Converts 1 ADC reading to waveform type for the DDFS module
  • Responds to slide switches events: changing the octave of the note frequency of the DDFS
  • Respond to push button events: changing the control frequency of the DDFS

SystemVerilog implementation

`timescale 1ns / 1ps


module checkXadcAdsrNoise(
    input clk,        
    input logic [3:0] sw,
    input logic [3:0] btn,    
    input logic vp_in,               // input wire vp_in  
    input logic vn_in,               // input wire vn_in  
    input logic vauxp0,              // input wire vauxp0 
    input logic vauxn0,              // input wire vauxn0 
    input logic vauxp1,              // input wire vauxp1 
    input logic vauxn1,              // input wire vauxn1 
    input logic vauxp2,              // input wire vauxp2 
    input logic vauxn2,              // input wire vauxn2 
    input logic vauxp3,              // input wire vauxp3 
    input logic vauxn3,              // input wire vauxn3 
    input logic vauxp8,              // input wire vauxp8  
    input logic vauxn8,              // input wire vauxn8  
    input logic vauxp9,              // input wire vauxp9  
    input logic vauxn9,              // input wire vauxn9  
    input logic vauxp10,             // input wire vauxp10 
    input logic vauxn10,             // input wire vauxn10 
    input logic vauxp11,             // input wire vauxp11 
    input logic vauxn11,             // input wire vauxn11 
    
    output audio_out, // audio out
    output gain,      // Gain
    output pulse_out,
    output neg_shutdown, // ~SHUTDOWN                                     
    output logic [5:0] led

    );

    
    
    
    localparam T = 2;
    localparam W = 16;
    localparam PHASE_ACC_WIDTH = 30;
    
    
    
    localparam M1 = 30'h0AFD; // (262.0 / 100_000_000.0) * (1 <<PHASE_ACC_WIDTH); // carrier freq 262 Hz Freq sys 100 Mhz, M = fout/fsys * 2^N
    localparam M2 = 30'h0C54; // (277.0 / 100_000_000.0) * (1 <<PHASE_ACC_WIDTH); // carrier freq 277 Hz Freq sys 100 Mhz, M = fout/fsys * 2^N
    localparam M3 = 30'h0DD7; // (294.0 / 100_000_000.0) * (1 <<PHASE_ACC_WIDTH); // carrier freq 294 Hz Freq sys 100 Mhz, M = fout/fsys * 2^N
    localparam M4 = 30'hEAE; // (311.0 / 100_000_000.0) * (1 <<PHASE_ACC_WIDTH); // carrier freq 311 Hz Freq sys 100 Mhz, M = fout/fsys * 2^N
    
    logic [31:0] attack_step_value, attack_step_value_reg   ;   // precalculated (Amax - 0)/(t_attack - t_sys) steps for the attack segment
    
    logic [31:0] decay_step_value, decay_step_value_reg     ;   // precalculated (A_max-A_sus) / (t_sustain / t_sys) steps for the decay segment
    logic [31:0] sustain_level, sustain_level_reg           ;   // amplitude for the sustain segment
    logic [31:0] sustain_time, sustain_time_reg             ;   // tsustain / t_sys steps for the sustain
    logic [31:0] release_step_value, release_step_value_reg ;   // precalculated (A_sus - 0)/(t_release - t_sys) steps fot the release segment"
    logic [2:0] wave_type, wave_type_reg;
        
    logic [15:0] adc_data;
    
    logic [15:0] adc0_out_reg;
    logic [15:0] adc1_out_reg;
    logic [15:0] adc2_out_reg;
    logic [15:0] adc3_out_reg;   
    logic [15:0] adc4_out_reg;   
    logic [15:0] adc5_out_reg;   
        
    
   logic enable;  
   logic ready;
   logic ready_reg;
   logic ready_rising;
   logic ready_falling;
  
   logic [6:0] address_in; 
   logic [4:0] channel_out; 
   logic eos_out;   
    
  
    
    logic adsr_idle;
    logic   [PHASE_ACC_WIDTH-1:0] freq_carrier_ctrl_word = M1;
    logic   [PHASE_ACC_WIDTH-1:0]  freq_offset_ctrl_word = 30'h0;
    logic   [PHASE_ACC_WIDTH-1:0]  phase_offset = 30'h0;
    logic   [15:0]  amp; // = 16'h4000;  // Q2.14 format -1.0 to 1.0
    logic reset;
    logic   [PHASE_ACC_WIDTH-1:0] note;
    
    
    logic   [15:0]      pcm;                                    // pcm signal   
    logic   [15:0]      pcm_lpf;                                    // pcm signal   
    
    logic [32:0]       counter;  
    logic [32:0]       counter_next;  
    logic start;
    

    
    
       // ila instantiation
   ila_0 ila
    (   .clk,
        .probe0(adc_data),
        .probe1(channel_out),
        .probe2(enable),
        .probe3(ready),
        .probe4(eos_out)
    );

    
    
      //xadc instantiation connect the eoc_out .den_in to get continuous conversion

    xadc_adsr xadc
        (
        .daddr_in(address_in),   // Address bus for the dynamic reconfiguration port
        .dclk_in(clk),           // Clock input for the dynamic reconfiguration port
        .den_in(enable),         // Enable Signal for the dynamic reconfiguration port
        .di_in(0),               // Input data bus for the dynamic reconfiguration port
        .dwe_in(0),              // Write Enable for the dynamic reconfiguration port
        .reset_in(0),            // Reset signal for the System Monitor control logic
        
        .vp_in(vp_in),          // input wire vp_in
        .vn_in(vn_in),          // input wire vn_in
        .vauxp0(vauxp0),        // input wire vauxp0
        .vauxn0(vauxn0),        // input wire vauxn0
        .vauxp1(vauxp1),        // input wire vauxp1
        .vauxn1(vauxn1),        // input wire vauxn1
        .vauxp2(vauxp2),        // input wire vauxp2
        .vauxn2(vauxn2),        // input wire vauxn2
        .vauxp3(vauxp3),        // input wire vauxp3
        .vauxn3(vauxn3),        // input wire vauxn3
        
        .vauxp8(vauxp8),        // input wire vauxp8
        .vauxn8(vauxn8),        // input wire vauxn8
        .vauxp9(vauxp9),        // input wire vauxp9
        .vauxn9(vauxn9),        // input wire vauxn9
        .vauxp10(vauxp10),      // input wire vauxp10
        .vauxn10(vauxn10),      // input wire vauxn10
        .vauxp11(vauxp11),      // input wire vauxp11
        .vauxn11(vauxn11),      // input wire vauxn11
        
        .busy_out(),            // ADC Busy signal
        .channel_out(channel_out),         // Channel Selection Outputs
        .do_out(adc_data),          // Output data bus for dynamic reconfiguration port
        .drdy_out(ready),       // Data ready signal for the dynamic reconfiguration port
        
        .eoc_out(enable),       // End of Conversion Signal
        .eos_out(eos_out),             // End of Sequence Signal
        .alarm_out()            // OR'ed output of all the Alarms         

    );
    
        // instantiate adsr
    adsr adsr_uut(
       .clk(clk),
       .reset(reset),
       .start(start & adsr_idle),
       .attack_step_value( attack_step_value), // precalculated (Amax - 0)/(t_attack - t_sys) steps for the attack segment
       .decay_step_value(decay_step_value),  // precalculated (A_max-A_sus) / (t_sustain / t_sys) steps for the decay segment
       .sustain_level(sustain_level), // amplitude for the sustain segment
       .release_step_value(release_step_value),  // precalculated (A_sus - 0)/(t_release - t_sys) steps fot the release segment
       .sustain_time(sustain_time), // tsustain / t_sys steps for the sustain
       .envelope(amp),
       .adsr_idle(adsr_idle)
    );


    // instantiate ddfs
    ddfs #( .PHASE_ACC_WIDTH(30)) ddfs_uut 
        (.clk(clk),
        .reset(reset),
        .freq_carrier_ctrl_word(freq_carrier_ctrl_word),
        .freq_offset_ctrl_word(freq_offset_ctrl_word),
        .phase_offset(phase_offset),
        .envelope(amp),
        .pcm_out(pcm),
        .pulse_out(pulse_out),
        .wave_type(wave_type)
    );
    
//    moving_average_lpf lpf_uut(
//       .clk(clk),
//       .reset(reset),
//       .envelope(pcm),
//       .lpf_out(pcm_lpf)
//    );
    
    
    // instantiate ds 1bit dac    
    ds_1bit_dac #(.W(16)) ds_1bit_uut(
        .clk(clk),
        .reset(reset),
        .pcm_in(pcm),
        .pdm_out(audio_out)  
    );
    

    

    
    assign address_in = channel_out;
    assign neg_shutdown = sw[0]; // control sound on off with switch B0
    
    assign gain = sw[1];   
    assign counter_next = counter +1;
    
    assign led[0] = counter[24];
    assign led[1] = pcm;
    
    always_ff @(posedge clk)
    begin
        ready_reg <= ready;
    end
      
    assign ready_rising = ready && !ready_reg ? 1'b1 : 1'b0;
    assign ready_falling = !ready && ready_reg ? 1'b1 : 1'b0;
    
       
    always_ff @(posedge clk, posedge reset)
        if (reset) begin               
           attack_step_value_reg <= 32'h0000_0000;
           decay_step_value_reg <= 32'h0000_0000;
           sustain_level_reg <= 32'h0000_0000;
           sustain_time_reg <= 32'h0000_0000; 
           wave_type_reg <= 3'b000;  
        end else begin
        if (ready_rising == 1 && channel_out == 8'h10) begin // A1 Attack step value change
          case (adc_data[15:13])
            2:  attack_step_value_reg <= 32'h000053E3;
            3:  attack_step_value_reg <= 32'h00000863;
            4:  attack_step_value_reg <= 32'h000000D7; 
            5:  attack_step_value_reg <= 32'h00000015;
            6:  attack_step_value_reg <= 32'h00000004;
            7:  attack_step_value_reg <= 32'h00000001;
            default: attack_step_value_reg <= 32'h7FFFFFFF; 
          endcase
        end else begin
          attack_step_value_reg <= attack_step_value_reg;
        end
        
        if (ready_rising == 1 && channel_out == 8'h11) begin // A1 Decay step value change
          case (adc_data[15:13])
            2:  decay_step_value_reg <= 32'h000053E3;   
            3:  decay_step_value_reg <= 32'h00000863;   
            4:  decay_step_value_reg <= 32'h000000D7;   
            5:  decay_step_value_reg <= 32'h00000015;   
            6:  decay_step_value_reg <= 32'h00000004;   
            7:  decay_step_value_reg <= 32'h00000001;   
            default: decay_step_value_reg <= 32'h7FFFFFFF; 
          endcase
        end else begin
          decay_step_value_reg <= decay_step_value_reg;
        end
        
        if (ready_rising == 1 && channel_out == 8'h19) begin // A2 Amplitude for the sustain segment value change
          case (adc_data[15:13])
            2:  sustain_level_reg <= 32'h26666666 ;
            3:  sustain_level_reg <= 32'h33333333 ;
            4:  sustain_level_reg <= 32'h40000000 ; 
            5:  sustain_level_reg <= 32'h4CCCCCCC ;
            6:  sustain_level_reg <= 32'h59999999 ;
            7:  sustain_level_reg <= 32'h66666666 ;
            default: sustain_level_reg <= 32'h19999999; 
          endcase
        end else begin
          sustain_level_reg <= sustain_level_reg;
        end
        
        if (ready_rising == 1 && channel_out == 8'h12) begin // A3 time for the sustain segment value change
          case (adc_data[15:13])
            2:  sustain_time_reg <= 32'h000186A0;   
            3:  sustain_time_reg <= 32'h000F4240;   
            4:  sustain_time_reg <= 32'h00989680;   
            5:  sustain_time_reg <= 32'h05F5E100;   
            6:  sustain_time_reg <= 32'h0DC033E0;   
            7:  sustain_time_reg <= 32'h0DC1BA80;   
            default: sustain_time_reg <= 32'h00000000; 
          endcase
        end else begin
          sustain_time_reg <= sustain_time_reg;
        end
        
        if (ready_rising == 1 && channel_out == 8'h1A) begin // A4 release time for the sustain segment value change
          case (adc_data[15:13])
            2:  release_step_value_reg <= 32'h000053E3;   
            3:  release_step_value_reg <= 32'h00000863;   
            4:  release_step_value_reg <= 32'h000000D7;   
            5:  release_step_value_reg <= 32'h00000015;   
            6:  release_step_value_reg <= 32'h00000004;   
            7:  release_step_value_reg <= 32'h00000001;   
            default: release_step_value_reg <= 32'h7FFFFFFF; 
          endcase
        end else begin
          release_step_value_reg <= release_step_value_reg;
        end
        
        if (ready_rising == 1 && channel_out == 8'h1B) begin // A5 wave type
          case (adc_data[15:13])
            2: wave_type_reg <= 3'b001; // saw
            3: wave_type_reg <= 3'b010; // square
            4: wave_type_reg <= 3'b011; // triangle
            5: wave_type_reg <= 3'b100; // noise
            default: wave_type_reg <= 3'b000; // sin
          endcase
        end else begin
          wave_type_reg <= wave_type_reg;
        end          
    end


    assign attack_step_value  = attack_step_value_reg;   // precalculated (Amax - 0)/(t_attack - t_sys) steps for the attack segment
    assign decay_step_value   = decay_step_value_reg;   // precalculated (A_max-A_sus) / (t_sustain / t_sys) steps for the decay segment
    assign sustain_level      = sustain_level_reg;   // amplitude for the sustain segment
    assign sustain_time       = sustain_time_reg ;   // tsustain / t_sys steps for the sustain
    assign release_step_value = release_step_value_reg;  // precalculated (A_sus - 0)/(t_release - t_sys) steps fot the release segment"
    assign wave_type          = wave_type_reg;
    
    always_ff @(posedge clk)
    begin
        start <= btn[0] | btn[1] | btn[2] | btn[3];
        case (btn)
          4'b1000: note <= M1 << sw;
          4'b0100: note <= M2 << sw;
          4'b0010: note <= M3 << sw;
          4'b0001: note <= M4 << sw;
        endcase
    end    
    
       // registers
   always_ff @(posedge clk, posedge reset)
   begin
      if (reset) begin
         freq_carrier_ctrl_word <= 0;
         freq_offset_ctrl_word <= 0;
         phase_offset <= 0;
         //amp <= 16'h4000;    // 1.00
         counter <=0;
      end 
      else begin
            freq_carrier_ctrl_word <= note;
            freq_offset_ctrl_word <= 0;
            phase_offset <= 0;
            //amp <= 16'h4000;
            counter <= counter_next;
      end
    end
    
endmodule


DDFS module

Direct synthesis of a modulated analog waveform. DDFS is a digitally-controlled method of generating multiple frequencies from a reference frequency source. DDFS is a method of producing a tunable digital or analog waveform. First the data points of the waveform are generated in digital format and the converted to analog format with a Digital to Analog Converter (DAC) and a Low Pass Filter (LPF). See: One-bit Delta Sigma SystemVerilog Implementation

The DDFS module can produce five different types of waveforms: Sinusoidal, Triangle, Square, Saw-Tooth and Noise and any other wave based in Phase to Amplitude Lookup Table as waveforms captured by a microphone.

image

See (+) SystemVerilog Study Notes. DDFS. Direct Digital Frequency Synthesis for Sound - Blog - FPGA - element14 Community for more detail of this module

image

The system generates a waveform modulated this way:

modulated_wave(t) = A(t) * sin(2*PI*(f + Δf(t) * t )  + Δp(t))

  • Frequency Control Word : Frequency control word (fccw) to to generate the carrier frequency
  • Frequency Offset Word: The frequency control word (focw) to generate the offset frequency Δf(t) 
  • Phase Offset: The phase value corresponding to the desired phase offset, Δp(t)
  • Envelope: The digitized value of A(t)

SystemVerilog implementation. This implementation differs from de previous one as I added four more wave types to the module

`timescale 1ns / 1ps


module ddfs
#(parameter PHASE_ACC_WIDTH = 30) // width of phase accumulator
( 
    input   logic               clk,
    input   logic               reset,
    input   logic   [PHASE_ACC_WIDTH-1:0]    freq_carrier_ctrl_word,     // frequency control word to generate carrier frequency
                                                                         // fccw = fout/fsys * 2^N ; (N = PHASE_ACC_WIDTH)
    input   logic   [PHASE_ACC_WIDTH-1:0]    freq_offset_ctrl_word,      // frequency offset control word
                                                                         // focw = fout/fsys * 2^N  ; (N = PHASE_ACC_WIDTH)
    input   logic   [PHASE_ACC_WIDTH-1:0]    phase_offset,               // phase offset
                                                                         // offset/360 * 2^N ; (N = PHASE_ACC_WIDTH)
    input   logic   [15:0]      envelope,                                // Amplitude modulation: envelope, digitized value of A(t) in Q2.14 format
    
    input   logic   [2:0]       wave_type,                               // wave type 0 sine, 1 triangle, 2 square , 3 noise, 4 saw
    output  logic   [15:0]      pcm_out,                                 // pcm signal 
    output  logic               pulse_out                                // pulse out
    );
    
    // signal declaration
    logic   [PHASE_ACC_WIDTH-1:0]    freq_control_word;  // frequency modulation control word
    logic   [PHASE_ACC_WIDTH-1:0]    phase_next;         // next phase
    logic   [PHASE_ACC_WIDTH-1:0]    phase_control_word; // phase control word
    logic   [PHASE_ACC_WIDTH-1:0]    phase_reg;          // actual phase
    logic   [7:0]       p2a_raddr;
    logic   [15:0]      amp;
    logic   [15:0]      sin_amp;
    logic   [15:0]      triangle_amp;
    logic   [15:0]      noise_amp;
    logic   [15:0]      square_amp;
    logic   [15:0]      saw_amp;
    logic   signed  [31:0] modulation;
    logic   [15:0] pcm_reg;  // multi-bit PCM (pulse code modulation) t    
    logic   [15:0] pcm_sin_next;  // multi-bit PCM (pulse code modulation) t
    logic   [15:0] pcm_noise_next;  // multi-bit PCM (pulse code modulation) t
    logic   [15:0] pcm_square_next;  //   
    logic   [15:0] pcm_triangle_next;  //
    logic   [15:0] pcm_saw_next;  //
    logic   pulse_noise;
    
    // body
    // instanciate sin() ROM
    sin_rom sin_rom_unit
    (   .clk(clk),
        .addr_r(p2a_raddr),
        .dout(sin_amp));
        
    triangle triangle_unit
    (   .clk(clk),
        .addr_r(p2a_raddr),
        .dout(triangle_amp));
        
    saw_tooth saw_tooth_unit
    (   .clk(clk),
        .addr_r(p2a_raddr),
        .dout(saw_amp));
        
    square_wave square_unit
    (   .clk(clk),
        .addr_r(p2a_raddr),
        .dout(square_amp));
        
        
    lfsr lfsr_unit(.clk(pulse_noise),
             .enable(1'b1),
             .lfsr_data(noise_amp),
             .reset(reset));
                  
    // phase register and output buffer
    // use an output buffer (to shorten crtical path since the o/p feeds dac) 
    // always_ff @(posedge clk)
    //    pcm_reg <= modu[29:14];
    always_ff @(posedge clk, posedge reset)
    begin
        if (reset) 
            begin
                phase_reg <= 0;
                pcm_reg <= 0;
            end
        else
            begin
                phase_reg <= phase_next;
                pcm_reg <= pcm_next;                
            end
    end
    
    // frequency modulation
    assign freq_control_word = freq_carrier_ctrl_word + freq_offset_ctrl_word;
    
    // phase accumulation
    assign phase_next = phase_reg + freq_control_word;
    
    assign pcm_next = modulation[29:14];

    // phase modulation
    assign phase_control_word = phase_reg + phase_offset;
    
    // phase to amplitude mapping address
    assign p2a_raddr = phase_control_word[PHASE_ACC_WIDTH-1:PHASE_ACC_WIDTH-8]; // 8 bits
    
    // amplitude modulation envelop in Q2.14 
    //    * -1 < env < +1  (between 1100...00 and 0100...00) 
    //    * Q16.0 * Q2.14 => modu is Q18.14
    //    * convert modu back to Q16.0  

    always_comb begin
      case (wave_type[2:0])
          3'b000: amp = sin_amp;                  
          3'b010: amp = square_amp;
          3'b001: amp = saw_amp;
          3'b011: amp = triangle_amp;
          3'b100: amp = noise_amp;      
       endcase 
       modulation = $signed(envelope) * $signed(amp);
    end
    
    assign pcm_out = pcm_reg;
    assign pulse_out = phase_reg[PHASE_ACC_WIDTH-1];  
    assign pulse_noise = phase_reg[PHASE_ACC_WIDTH-5];  
    
endmodule



ADSR module

The ADSR scheme is based on the observation that when a real instrument generates a note, the volume of the musical note changes over time. It rises rapidly from zero and then steadily decays.

image

The ADSR scheme is based on the observation that when a real instrument generates a note, the volume of the musical note changes over time. It rises rapidly from zero and then steadily decays. To model this effect we can multiply the constant tone produced by the DDFS module by an amplitude ADSR envelope that contains the segments, attack, decay, sustain and release

ADSR envelope

See: (+) SystemVerilog Study Notes. FPGA ADSR envelope generator for sound synthesis (I) - Blog - FPGA - element14 Community for more info about this module

The ADRS envelope can be specified by two amplitude parameters, A max and Asus, and four time parameters, Attack time, Decay time, Sustain time and Release time.

  • A max: maximum amplitude level the envelope can reach.
  • A sus: the amplitude of the sustain segment.
  • Attack time: the time interval of the Attack segment.
  • Decay time: the time interval of the Decay segment.
  • Sustain time: the time interval of the Sustain segment.
  • Release time: the time interval of the Release segment.

SystemVerilog implementation. 

`timescale 1ns / 1ps


// fsm to generate ADSR envelop

// start (trigger) signal:
//     - starts the "attack" when asserted
//     - restarts the epoch if aseerted before the current epoch ends 

// amplitudes:
//     - 32-bit unsigned   
//     - use 32 bits to accommodate the needed resolution for "step"
//     - intepreted as Q1.31 format:
//     - range artificially limited between 0.0 and 1.0
//     - i.e., 0.0...0 to 1.0...0 (1.0)
//     - 1.1xx...x not allowed

// output: Q2.14 for range (-1.0 to 1.0)

// special atk_step values
//     - atk_step = 11..11: bypass adsr; i.e., envelop=1.0
//     - atk_step = 00..00: output 0; i.e., envelop = 0.0

// Width selection: 
//   max attack time = 2^31 * clock period = 2^31 *(1/100e6) = 21,47483648 seconds

// Attack_time
// t_attack desired attack time
// t_sys sytem clock period
// maximum amplitude Amax
// need t_attack / t_sys clock cycles in the attact segment
// the counter must be incremented (Amax - 0)/(t_attack - t_sys) each clock cycle to reach A_max from zero in t_attack

// Decrementing amount in decay segment for t_decay
// t_decay and A_sus.  (A_max-A_sus) / (t_sustain / t_sys)
// 

// Decrementing amount in release segment
// (A_sus - 0)/(t_release - t_sys)

// The amplitude is constant in the sustain segment
// There are are tsustain / t_sys cycles in the sustain segment

// Amax  |    /\
//       |   /  \
//       |  /    \
// Asus  | /      ------------
//       |/                   \
//       ---------------------------
//        | attack
//             | decay
//                | sustain
//                            | release


 module adsr(
input logic clk,
input logic reset,
input logic start, // generate a pulse to start the envelope generation
input logic [31:0] attack_step_value, // precalculated (Amax - 0)/(t_attack - t_sys) steps for the attack segment
input logic [31:0] decay_step_value,  // precalculated (A_max-A_sus) / (t_sustain / t_sys) steps for the decay segment
input logic [31:0] sustain_level, // amplitude for the sustain segment
input logic [31:0] release_step_value,  // precalculated (A_sus - 0)/(t_release - t_sys) steps fot the release segment
input logic [31:0] sustain_time, // tsustain / t_sys steps for the sustain
output logic [15:0] envelope,
output logic adsr_idle

    );
    
    // constants
    localparam MAX = 32'h8000_0000;
    localparam BYPASS = 32'hffff_ffff;
    localparam ZERO = 32'h0000_0000;
    
    // fsm state type
    typedef enum {idle, launch, attack, decay, sustain, rel} state_type;
    
    // declaration
    state_type state_reg;
    state_type state_next;
    logic [31:0] amplitude_counter_reg;
    logic [31:0] amplitude_counter_next;
    logic [31:0] sustain_time_reg;
    logic [31:0] sustain_time_next;
    logic [31:0] n_tmp;
    logic fsm_idle;
    logic [31:0] envelope_i;
    
    // state and data registers
    always_ff @(posedge clk, posedge reset)
    begin
        if(reset) 
          begin
            state_reg <= idle;
            amplitude_counter_reg <= 32'h0;
            sustain_time_reg <= 32'h0;
          end
         else
         begin
            state_reg <= state_next;
            amplitude_counter_reg <= amplitude_counter_next;
            sustain_time_reg <= sustain_time_next;
         end
    end
    
    // fsmd next-state logic and data path logic
    always_comb
    begin
        state_next = state_reg;
        amplitude_counter_next = amplitude_counter_reg;
        sustain_time_next = sustain_time_reg;
        fsm_idle = 1'b0;        
        case (state_reg)         
            idle: begin
                fsm_idle = 1'b1;
                if (start) begin
                    state_next = launch;
                  end
              end
            launch: begin
                state_next = attack;
                amplitude_counter_next = 32'b0;
              end  
            attack: begin
                if(start) begin
                    state_next = launch;
                end else begin
                    n_tmp = amplitude_counter_reg + attack_step_value;
                    if (n_tmp < MAX) begin
                        amplitude_counter_next = n_tmp;
                    end else begin
                        state_next = decay;
                    end
                 end
              end
            decay: begin
                if (start) begin
                    state_next = launch;
                end else begin
                    n_tmp = amplitude_counter_reg - decay_step_value;
                    if(n_tmp > sustain_level) begin
                        amplitude_counter_next = n_tmp;
                    end else begin
                        amplitude_counter_next = sustain_level;
                        state_next = sustain;
                        sustain_time_next = 32'b0;  // start timer
                    end
                 end
              end              
            sustain: begin
                if (start) begin
                    state_next = launch;
                end else begin
                    if(sustain_time_reg < sustain_time) begin
                        sustain_time_next = sustain_time_next + 1;
                    end else begin
                        state_next = rel;
                    end
                 end
              end              
             default: begin
                if (start) begin
                    state_next = launch;
                  end else begin
                    if(amplitude_counter_reg > release_step_value) begin
                        amplitude_counter_next = amplitude_counter_reg - release_step_value;
                    end else begin
                        state_next = idle;
                    end
                  end
               end    
        endcase

    end
    
    assign adsr_idle = fsm_idle;
    
    assign envelope_i = (attack_step_value == BYPASS) ? MAX :
                   (attack_step_value == ZERO) ? 32'b0 :
                   amplitude_counter_reg;
                   
    assign envelope = {1'b0, envelope_i[31:17]};
endmodule


Noise Generator Module

Noise is characterized as being aperiodic or having a non-repetitive pattern. White noise is a type of sound that contains all audible frequencies in equal amounts. It sounds like a hiss or a static noise. In sound synthesizers, white noise is used as a starting point for creating other sounds. By adding filters or modulating the sound, the synthesizer can create different tones or textures.

image

A few examples of sounds that can be created using white noise as a starting point are: the sound of a snare drum, the sound of the wind. the sound of ocean waves, the sound of cymbals...

For noise generation this module uses a linear-feedback shift register (LFSR). An LFSR is a shift register whose input bit is a linear function of its previous state. The most commonly used linear function of single bits is exclusive-or (XOR). Thus, an LFSR is most often a shift register whose input bit is driven by the XOR of some bits of the overall shift register value. Any long LFSR counter generates a long pseudo-random sequence of zeros and ones. The sequence is not exactly random since it repeats eventually, and it also follows a mathematically predictable sequence.

Vivado synthesis for the SystemVerilog module

image 

The appropriate taps for Maximum-Length LFSR Counters are extracted from this Xilinx Technical Note:

 xapp052.pdf • Viewer • AMD Adaptive Computing Documentation Portal (xilinx.com)

SystemVerilog implementation of the Noise module by means of an Linear Feedback Shift Register.

`timescale 1ns / 1ps

// Description: 
//  This module generates an 16-bit random number. A LFSR or Linear Feedback Shift Register
//  is a quick and easy way to generate pseudo-random data inside of an FPGA.

module lfsr #(parameter NUM_BITS = 31)
  (
   input logic clk,
   input logic reset,
   input logic enable,
   output logic [NUM_BITS-1:0] lfsr_data
   );
 
  logic [NUM_BITS:1] lfsr_reg = 0;
  logic polinomial; 

  // run LFSR when enabled.
  always @(posedge clk, posedge reset)
    begin
      if (enable == 1'b1)
        begin
            if (reset == 1'b1) begin
                lfsr_reg <= 1'b0;
            end else begin
                lfsr_reg <= {lfsr_reg[NUM_BITS-1:1], polinomial};
            end
        end else begin
            lfsr_reg <= lfsr_reg;
        end
    end
 
  // Feedback Polynomial based on Xilinx Application Note:
  // http://www.xilinx.com/support/documentation/application_notes/xapp052.pdf
  // for 16 bits
  // assign  polinomial = lfsr_reg[16] ^~ lfsr_reg[15] ^~ lfsr_reg[13] ^~ lfsr_reg[4];
  // for 31 bits
   assign  polinomial = lfsr_reg[31] ^~ lfsr_reg[28];
 
  assign lfsr_data = lfsr_reg[16:1];
 
endmodule

Test Bench: Synthesis Simulation in VIvado

image

Waveform and spectrogram using noise as source

Noise Generator

Zoomed view

image


Sine Wave Generator

A sine wave, sinusoidal wave, or just sinusoid is a mathematical curve defined in terms of the sine trigonometric function, of which it is the graph. It is a type of continuous wave and also a smooth periodic function. By adding different sine waves we get a different waveform, this changes the timbre of the sound. When the sound is made up of more than one sinusoidal then it has more harmonics that are perceptible to the human ear.

image

The presence of higher harmonics in addition to the fundamental causes variation in the timbre, which is the reason why the same musical note (the same frequency) played on different instruments sounds different. We can generate several modules with different variations in frequency and mix them.

SystemVerilog implementation

`timescale 1ns / 10ps

module sin_rom
#( parameter DATA_WIDTH = 16,  // number of bits
             ADDR_WIDTH = 8    // number od address bits

)
(
    input logic clk,
    input logic [ADDR_WIDTH-1:0] addr_r,
    output logic [DATA_WIDTH-1:0] dout
    );
    
    // signal declaration
    logic [DATA_WIDTH-1:0] ram [0:2**ADDR_WIDTH-1];  // ascending range
    logic [DATA_WIDTH-1:0] data_reg;
    
    initial
       // $readmemh("triangle_table.mem", ram);
       // $readmemh("sawtooth.mem", ram);
       $readmemh("sin_table.txt", ram);
    
    // read operation
    always_ff @(posedge clk)
    begin
        data_reg <= ram[addr_r];
    end
    
    assign dout = data_reg;    
endmodule

Waveform and spectrogram using one sin module as source

Triangle Wave Generator

Zoomed view:

image


Square Wave Generator

A square wave is a non-sinusoidal periodic waveform in which the amplitude alternates at a steady frequency between fixed minimum and maximum values, with the same duration at minimum and maximum. In an ideal square wave, the transitions between minimum and maximum are instantaneous, Square waves contain a wide range of harmonics. The ideal square wave contains only components of odd-integer harmonic frequencies.

image

In music they are described as hollow sounds. They can be used as the basis for wind instrument sounds created using subtractive synthesis.
Also for performing distortion effects by clipping the outermost regions of the waveform, making it more and more like a square wave as more distortion is applied.

SystemVerilog implementation for the square wave generator.

`timescale 1ns / 1ps

/**
A square wave is a non-sinusoidal periodic waveform 
in which the amplitude alternates at a steady frequency between
fixed minimum and maximum values, with the same duration at minimum and maximum. 
n musical terms, they are often described as sounding hollow, and are therefore 
used as the basis for wind instrument sounds created using subtractive synthesis.
Additionally, the distortion effect used on electric guitars clips the outermost
regions of the waveform, causing it to increasingly resemble 
a square wave as more distortion is applied.
*/
module square_wave
#( parameter DATA_WIDTH = 16,  // number of bits
             ADDR_WIDTH = 8    // number od address bits

)
(
    input logic clk,
    input logic [ADDR_WIDTH-1:0] addr_r,
    output logic [DATA_WIDTH-1:0] dout
    );
    
    // signal declaration
    logic [DATA_WIDTH-1:0] data_reg;
    logic [DATA_WIDTH-1:0] data;
    
    
    // read operation
    always_ff @(posedge clk)
    begin
        data_reg <= data;
    end
    
    always_comb
    begin
       data = addr_r[7]?16'b1100_0000_0000_0000:16'b0100_0000_0000_0000;
    end
    
    assign dout = data_reg;    
endmodule

Waveform and spectrogram using one square module as source

Square wave generator

Zoomed view

image

image


Triangle Wave Generator 

A triangular wave or triangle wave is a non-sinusoidal waveform named for its triangular shape. It is a periodic, piecewise linear, continuous real function. Like a square wave, the triangle wave contains only odd harmonics. However, the higher harmonics roll off much faster than in a square wave (proportional to the inverse square of the harmonic number as opposed to just the inverse).

image

ROM look-up table based SystemVerilog Implementation

`timescale 1ns / 10ps

module triangle_rom
#( parameter DATA_WIDTH = 16,  // number of bits
             ADDR_WIDTH = 8    // number od address bits

)
(
    input logic clk,
    input logic [ADDR_WIDTH-1:0] addr_r,
    output logic [DATA_WIDTH-1:0] dout
    );
    
    // signal declaration
    logic [DATA_WIDTH-1:0] ram [0:2**ADDR_WIDTH-1];  // ascending range
    logic [DATA_WIDTH-1:0] data_reg;
    
    initial
        $readmemh("triangle_table.mem", ram);
     //  $readmemh("sawtooth.mem", ram);
     //  $readmemh("triangle_table.txt", ram);
    
    // read operation
    always_ff @(posedge clk)
    begin
        data_reg <= ram[addr_r];
    end
    
    assign dout = data_reg;    
endmodule

image

Triangle Wave Generator from Equations

`timescale 1ns / 10ps

/**
A triangular wave or triangle wave is a non-sinusoidal waveform 
named for its triangular shape. It is a periodic, piecewise linear, 
continuous real function. Like a square wave, the triangle wave
contains only odd harmonics. 
The higher harmonics roll off much faster than in a square wave.
*/

module triangle
#( parameter DATA_WIDTH = 16,  // number of bits
             ADDR_WIDTH = 8    // number od address bits

)
(
    input logic clk,
    input logic [ADDR_WIDTH-1:0] addr_r,
    output logic [DATA_WIDTH-1:0] dout
    );
    
    // signal declaration
    logic [DATA_WIDTH-1:0] data_reg;
    logic [DATA_WIDTH-1:0] data;    
    
    // read operation
    always_ff @(posedge clk)
    begin
        data_reg <= data;
    end
    
    always_comb
    begin
      if (addr_r > 8'h7F) begin
           data = {8'h00, 8'hFE - addr_r}; // decaying
      end else begin
           data = {8'h00, addr_r}; // raising
      end    
    end   
    
    assign dout =  $signed(data << 9) - $signed(16'h8000);  // offset biass  
endmodule

Triangle wave generator


Saw-Tooth Generator Module

Sawtooth waves and real-world signals contain all integer harmonics.

`timescale 1ns / 10ps


module saw_tooth
#( parameter DATA_WIDTH = 16,  // number of bits
             ADDR_WIDTH = 8    // number od address bits

)
(
    input logic clk,
    input logic [ADDR_WIDTH-1:0] addr_r,
    output logic [DATA_WIDTH-1:0] dout
    );
    
    // signal declaration
    logic [DATA_WIDTH-1:0] data_reg;
    logic [DATA_WIDTH-1:0] data;
    logic [DATA_WIDTH-1:0] amp;
    
    
    // read operation
    always_ff @(posedge clk)
    begin
        data_reg <= data;
    end
    
    
    always_comb
    begin
      amp = {8'h00, addr_r} << 8;
      data = $signed(amp) - $signed(16'h8000);   
    end  

    
    assign dout = data_reg;    
endmodule

sawtooth wave generator

image


XADC Sequencer Mode

The reading of the potentiometers to control the attack time, the decay time, the sustain amplitude value, the sustain time and the release time of the ADSR module as well as the type of wave to be generated is done with the XADC module of the AMD Spartan 7 FPGA configured in sequencer mode, gives us the readings of the six potentiometers in sequence. See the ILA capture, when the ready signal is asserted we can read the value corresponding to the active sequence channel.

image

Verilog XADC configuration module

`timescale 1ns / 1 ps

(* CORE_GENERATION_INFO = "xadc_adsr,xadc_wiz_v3_3_8,
{component_name=xadc_adsr,
enable_axi=false,
enable_axi4stream=false,
dclk_frequency=100,
enable_busy=true,
enable_convst=false,
enable_convstclk=false,
enable_dclk=true,
enable_drp=true,
enable_eoc=true,
enable_eos=true,
enable_vbram_alaram=false,
enable_vccddro_alaram=false,
enable_Vccint_Alaram=false,
enable_Vccaux_alaram=false,
enable_vccpaux_alaram=false,
enable_vccpint_alaram=false,
ot_alaram=false,
user_temp_alaram=false,
timing_mode=continuous,
channel_averaging=None,
sequencer_mode=on,
startup_channel_selection=contineous_sequence}" *)


module xadc_adsr
          (
          daddr_in,            // Address bus for the dynamic reconfiguration port
          dclk_in,             // Clock input for the dynamic reconfiguration port
          den_in,              // Enable Signal for the dynamic reconfiguration port
          di_in,               // Input data bus for the dynamic reconfiguration port
          dwe_in,              // Write Enable for the dynamic reconfiguration port
          reset_in,            // Reset signal for the System Monitor control logic
          vauxp0,              // Auxiliary channel 0
          vauxn0,
          vauxp1,              // Auxiliary channel 1
          vauxn1,
          vauxp2,              // Auxiliary channel 2
          vauxn2,
          vauxp3,              // Auxiliary channel 3
          vauxn3,
          vauxp8,              // Auxiliary channel 8
          vauxn8,
          vauxp9,              // Auxiliary channel 9
          vauxn9,
          vauxp10,             // Auxiliary channel 10
          vauxn10,
          vauxp11,             // Auxiliary channel 11
          vauxn11,
          busy_out,            // ADC Busy signal
          channel_out,         // Channel Selection Outputs
          do_out,              // Output data bus for dynamic reconfiguration port
          drdy_out,            // Data ready signal for the dynamic reconfiguration port
          eoc_out,             // End of Conversion Signal
          eos_out,             // End of Sequence Signal
          alarm_out,           // OR'ed output of all the Alarms    
          vp_in,               // Dedicated Analog Input Pair
          vn_in);

          input [6:0] daddr_in;
          input dclk_in;
          input den_in;
          input [15:0] di_in;
          input dwe_in;
          input reset_in;
          input vauxp0;
          input vauxn0;
          input vauxp1;
          input vauxn1;
          input vauxp2;
          input vauxn2;
          input vauxp3;
          input vauxn3;
          input vauxp8;
          input vauxn8;
          input vauxp9;
          input vauxn9;
          input vauxp10;
          input vauxn10;
          input vauxp11;
          input vauxn11;
          input vp_in;
          input vn_in;

          output busy_out;
          output [4:0] channel_out;
          output [15:0] do_out;
          output drdy_out;
          output eoc_out;
          output eos_out;
          output alarm_out;

        wire FLOAT_VCCAUX;
        wire FLOAT_VCCINT;
        wire FLOAT_TEMP;
          wire GND_BIT;
    wire [2:0] GND_BUS3;
          assign GND_BIT = 0;
    assign GND_BUS3 = 3'b000;
          wire [15:0] aux_channel_p;
          wire [15:0] aux_channel_n;
          wire [7:0]  alm_int;
          assign alarm_out = alm_int[7];
          assign aux_channel_p[0] = vauxp0;
          assign aux_channel_n[0] = vauxn0;

          assign aux_channel_p[1] = vauxp1;
          assign aux_channel_n[1] = vauxn1;

          assign aux_channel_p[2] = vauxp2;
          assign aux_channel_n[2] = vauxn2;

          assign aux_channel_p[3] = vauxp3;
          assign aux_channel_n[3] = vauxn3;

          assign aux_channel_p[4] = 1'b0;
          assign aux_channel_n[4] = 1'b0;

          assign aux_channel_p[5] = 1'b0;
          assign aux_channel_n[5] = 1'b0;

          assign aux_channel_p[6] = 1'b0;
          assign aux_channel_n[6] = 1'b0;

          assign aux_channel_p[7] = 1'b0;
          assign aux_channel_n[7] = 1'b0;

          assign aux_channel_p[8] = vauxp8;
          assign aux_channel_n[8] = vauxn8;

          assign aux_channel_p[9] = vauxp9;
          assign aux_channel_n[9] = vauxn9;

          assign aux_channel_p[10] = vauxp10;
          assign aux_channel_n[10] = vauxn10;

          assign aux_channel_p[11] = vauxp11;
          assign aux_channel_n[11] = vauxn11;

          assign aux_channel_p[12] = 1'b0;
          assign aux_channel_n[12] = 1'b0;

          assign aux_channel_p[13] = 1'b0;
          assign aux_channel_n[13] = 1'b0;

          assign aux_channel_p[14] = 1'b0;
          assign aux_channel_n[14] = 1'b0;

          assign aux_channel_p[15] = 1'b0;
          assign aux_channel_n[15] = 1'b0;
XADC #(
        .INIT_40(16'h0000), // config reg 0
        .INIT_41(16'h21AF), // config reg 1
        .INIT_42(16'h0400), // config reg 2
        .INIT_48(16'h0800), // Sequencer channel selection
        .INIT_49(16'h0F0F), // Sequencer channel selection
        .INIT_4A(16'h0000), // Sequencer Average selection
        .INIT_4B(16'h0000), // Sequencer Average selection
        .INIT_4C(16'h0000), // Sequencer Bipolar selection
        .INIT_4D(16'h0000), // Sequencer Bipolar selection
        .INIT_4E(16'h0000), // Sequencer Acq time selection
        .INIT_4F(16'h0000), // Sequencer Acq time selection
        .INIT_50(16'hB5ED), // Temp alarm trigger
        .INIT_51(16'h57E4), // Vccint upper alarm limit
        .INIT_52(16'hA147), // Vccaux upper alarm limit
        .INIT_53(16'hCA33),  // Temp alarm OT upper
        .INIT_54(16'hA93A), // Temp alarm reset
        .INIT_55(16'h52C6), // Vccint lower alarm limit
        .INIT_56(16'h9555), // Vccaux lower alarm limit
        .INIT_57(16'hAE4E),  // Temp alarm OT reset
        .INIT_58(16'h5999), // VCCBRAM upper alarm limit
        .INIT_5C(16'h5111),  //  VCCBRAM lower alarm limit
        .SIM_DEVICE("7SERIES"),
        .SIM_MONITOR_FILE("design.txt")
)

inst (
        .CONVST(GND_BIT),
        .CONVSTCLK(GND_BIT),
        .DADDR(daddr_in[6:0]),
        .DCLK(dclk_in),
        .DEN(den_in),
        .DI(di_in[15:0]),
        .DWE(dwe_in),
        .RESET(reset_in),
        .VAUXN(aux_channel_n[15:0]),
        .VAUXP(aux_channel_p[15:0]),
        .ALM(alm_int),
        .BUSY(busy_out),
        .CHANNEL(channel_out[4:0]),
        .DO(do_out[15:0]),
        .DRDY(drdy_out),
        .EOC(eoc_out),
        .EOS(eos_out),
        .JTAGBUSY(),
        .JTAGLOCKED(),
        .JTAGMODIFIED(),
        .OT(),
        .MUXADDR(),
        .VP(vp_in),
        .VN(vn_in)
          );

endmodule


Moving Average LPF

Another design of mine for a configurable low pass filter based on a moving average that hardly uses FPGA resources.

`timescale 1ns / 1ps
///////////////////////////////////////////////////////////////////////


module moving_average_lpf(
    input logic clk,
    input logic reset,
    output logic [15:0] envelope,
    output logic [15:0] lpf_out
);

    logic [31:0] ma_new, ma_new_reg;
    logic [31:0] ma_old, ma_old_reg;
    logic [31:0] envelope_i;
    
    
    always_ff @(posedge clk, posedge reset) begin
        if (reset) begin
            ma_new_reg <=0;
            ma_old_reg <=0;
        end else begin
            ma_new_reg <= ma_new;
            ma_old_reg <= ma_old;
        end
    end
    assign envelope_i = {envelope[14:0] , 17'b0};
    assign ma_old = ma_new_reg;
    assign ma_new = ( envelope_i  >> 17) + (ma_old_reg - (ma_old_reg >> 17)) ;
    
    assign lpf_out =  {1'b0, ma_new_reg[31:17]};

endmodule


Echo Delay

Still in development this is the first version to implement an echo or feedback delay module.

`timescale 1ns / 1ps

module echo #(DELAY_LINE_SIZE = 1024)(
  input logic clk,
  input logic reset,
  input logic [15:0] in,
  input logic [7:0] delay,
  input logic [7:0] feedback,
  output logic [15:0] out
);

  
  logic [15:0] delay_line [DELAY_LINE_SIZE];
  int write_ptr = 0, read_ptr = 0;
  logic [15:0] echo_signal;

  always_ff @(posedge clk, posedge reset) begin
    if(reset) begin
        write_ptr = 0;
        read_ptr = 0;
    end else begin
      // Write input to delay line and compute echo signal
      delay_line[write_ptr] = in;
      echo_signal = delay_line[read_ptr] * feedback;
      read_ptr = (read_ptr + delay) % DELAY_LINE_SIZE;
      write_ptr = (write_ptr + 1) % DELAY_LINE_SIZE;
      // Mix input and echo signals
      out = in + echo_signal;
    end
  end

endmodule


HDL Sources

s7_ddfs.srcs.zip


Vivado Project Structure

The project contains all the modules indicated in the blog as well as test bench modules for simulations.

image


Constraints File

This constraints file is adapted to the current system configuration, 4 monocolor LEDs, 2 RGB LEDs, 4 slide switches, 4 push buttons, 6 analog potentiometers used with the XADC module of the Spartan 7 FPGA and the PDM digital output to the RC low-pass analog filter. .

## This file is a general .xdc for the Arty S7-50 Rev. B

## Clock Signals
set_property -dict {PACKAGE_PIN R2 IOSTANDARD SSTL135} [get_ports clk]
create_clock -period 10.000 -name sys_clk_pin -waveform {0.000 5.000} -add [get_ports clk]

## Switches
set_property -dict {PACKAGE_PIN H14 IOSTANDARD LVCMOS33} [get_ports {sw[0]}]
set_property -dict {PACKAGE_PIN H18 IOSTANDARD LVCMOS33} [get_ports {sw[1]}]
set_property -dict {PACKAGE_PIN G18 IOSTANDARD LVCMOS33} [get_ports {sw[2]}]
set_property -dict {PACKAGE_PIN M5 IOSTANDARD SSTL135} [get_ports {sw[3]}]

## LEDs
set_property -dict {PACKAGE_PIN E18 IOSTANDARD LVCMOS33} [get_ports {led[0]}]
set_property -dict {PACKAGE_PIN F13 IOSTANDARD LVCMOS33} [get_ports {led[1]}]
set_property -dict {PACKAGE_PIN E13 IOSTANDARD LVCMOS33} [get_ports {led[2]}]
set_property -dict {PACKAGE_PIN H15 IOSTANDARD LVCMOS33} [get_ports {led[3]}]

## RGB LEDs
#set_property -dict { PACKAGE_PIN J15   IOSTANDARD LVCMOS33 } [get_ports { led0_r }]; #IO_L23N_T3_FWE_B_15 Sch=led0_r
set_property -dict { PACKAGE_PIN G17   IOSTANDARD LVCMOS33 } [get_ports { led[4] }]; #IO_L14N_T2_SRCC_15 Sch=led0_g
#set_property -dict { PACKAGE_PIN F15   IOSTANDARD LVCMOS33 } [get_ports { led0_b }]; #IO_L13N_T2_MRCC_15 Sch=led0_b
#set_property -dict { PACKAGE_PIN E15   IOSTANDARD LVCMOS33 } [get_ports { led1_r }]; #IO_L15N_T2_DQS_ADV_B_15 Sch=led1_r
set_property -dict { PACKAGE_PIN F18   IOSTANDARD LVCMOS33 } [get_ports { led[5] }]; #IO_L16P_T2_A28_15 Sch=led1_g
#set_property -dict { PACKAGE_PIN E14   IOSTANDARD LVCMOS33 } [get_ports { led1_b }]; #IO_L15P_T2_DQS_15 Sch=led1_b



## Buttons
set_property -dict {PACKAGE_PIN G15 IOSTANDARD LVCMOS33} [get_ports {btn[0]}]
set_property -dict {PACKAGE_PIN K16 IOSTANDARD LVCMOS33} [get_ports {btn[1]}]
set_property -dict {PACKAGE_PIN J16 IOSTANDARD LVCMOS33} [get_ports {btn[2]}]
set_property -dict {PACKAGE_PIN H13 IOSTANDARD LVCMOS33} [get_ports {btn[3]}]


## Pmod Header JD
set_property -dict {PACKAGE_PIN V15 IOSTANDARD LVCMOS33} [get_ports audio_out]
set_property DRIVE 16 [get_ports audio_out]
set_property -dict {PACKAGE_PIN U12 IOSTANDARD LVCMOS33} [get_ports gain]
set_property -dict {PACKAGE_PIN V13 IOSTANDARD LVCMOS33} [get_ports pulse_out]
set_property -dict {PACKAGE_PIN T12 IOSTANDARD LVCMOS33} [get_ports neg_shutdown]
#set_property -dict { PACKAGE_PIN T13   IOSTANDARD LVCMOS33 } [get_ports { jd[4] }]; #IO_L22N_T3_A04_D20_14 Sch=jd7/ck_io[29]
#set_property -dict { PACKAGE_PIN R11   IOSTANDARD LVCMOS33 } [get_ports { jd[5] }]; #IO_L23P_T3_A03_D19_14 Sch=jd8/ck_io[28]
#set_property -dict { PACKAGE_PIN T11   IOSTANDARD LVCMOS33 } [get_ports { jd[6] }]; #IO_L23N_T3_A02_D18_14 Sch=jd9/ck_io[27]
#set_property -dict { PACKAGE_PIN U11   IOSTANDARD LVCMOS33 } [get_ports { jd[7] }]; #IO_L24P_T3_A01_D17_14 Sch=jd10/ck_io[26]


## ChipKit Single Ended Analog Inputs
## NOTE: The ck_an_p pins can be used as single ended analog inputs with voltages from 0-3.3V (Chipkit Analog pins A0-A5). 
## These signals should only be connected to the XADC core. When using these pins as digital I/O, use pins ck_io[14-19].
set_property -dict { PACKAGE_PIN B13   IOSTANDARD LVCMOS33 } [get_ports { vauxp0 }]; #IO_L1P_T0_AD0P_15 Sch=ck_an_p[0]
set_property -dict { PACKAGE_PIN A13   IOSTANDARD LVCMOS33 } [get_ports { vauxn0 }]; #IO_L1N_T0_AD0N_15 Sch=ck_an_n[0]
set_property -dict { PACKAGE_PIN B15   IOSTANDARD LVCMOS33 } [get_ports { vauxp1 }]; #IO_L3P_T0_DQS_AD1P_15 Sch=ck_an_p[1]
set_property -dict { PACKAGE_PIN A15   IOSTANDARD LVCMOS33 } [get_ports { vauxn1 }]; #IO_L3N_T0_DQS_AD1N_15 Sch=ck_an_n[1]
set_property -dict { PACKAGE_PIN E12   IOSTANDARD LVCMOS33 } [get_ports { vauxp9 }]; #IO_L5P_T0_AD9P_15 Sch=ck_an_p[2]
set_property -dict { PACKAGE_PIN D12   IOSTANDARD LVCMOS33 } [get_ports { vauxn9 }]; #IO_L5N_T0_AD9N_15 Sch=ck_an_n[2]
set_property -dict { PACKAGE_PIN B17   IOSTANDARD LVCMOS33 } [get_ports { vauxp2 }]; #IO_L7P_T1_AD2P_15 Sch=ck_an_p[3]
set_property -dict { PACKAGE_PIN A17   IOSTANDARD LVCMOS33 } [get_ports { vauxn2 }]; #IO_L7N_T1_AD2N_15 Sch=ck_an_n[3]
set_property -dict { PACKAGE_PIN C17   IOSTANDARD LVCMOS33 } [get_ports { vauxp10 }]; #IO_L8P_T1_AD10P_15 Sch=ck_an_p[4]
set_property -dict { PACKAGE_PIN B18   IOSTANDARD LVCMOS33 } [get_ports { vauxn10 }]; #IO_L8N_T1_AD10N_15 Sch=ck_an_n[4]
set_property -dict { PACKAGE_PIN E16   IOSTANDARD LVCMOS33 } [get_ports { vauxp11 }]; #IO_L10P_T1_AD11P_15 Sch=ck_an_p[5]
set_property -dict { PACKAGE_PIN E17   IOSTANDARD LVCMOS33 } [get_ports { vauxn11 }]; #IO_L10N_T1_AD11N_15 Sch=ck_an_n[5]

## Dedicated Analog Inputs
set_property -dict { PACKAGE_PIN J10   } [get_ports { vp_in }]; #IO_L1P_T0_AD4P_35 Sch=v_p
set_property -dict { PACKAGE_PIN K9    } [get_ports { vn_in }]; #IO_L1N_T0_AD4N_35 Sch=v_n

## ChipKit Digital I/O On Inner Analog Header
## NOTE: These pins will need to be connected to the XADC core when used as differential analog inputs (Chipkit analog pins A6-A11)
set_property -dict { PACKAGE_PIN B14   IOSTANDARD LVCMOS33 } [get_ports { vauxp8 }]; #IO_L2P_T0_AD8P_15 Sch=ad_p[8]
set_property -dict { PACKAGE_PIN A14   IOSTANDARD LVCMOS33 } [get_ports { vauxn8 }]; #IO_L2N_T0_AD8N_15 Sch=ad_n[8]
set_property -dict { PACKAGE_PIN D16   IOSTANDARD LVCMOS33 } [get_ports { vauxp3 }]; #IO_L9P_T1_DQS_AD3P_15 Sch=ad_p[3]
set_property -dict { PACKAGE_PIN D17   IOSTANDARD LVCMOS33 } [get_ports { vauxn3 }]; #IO_L9N_T1_DQS_AD3N_15 Sch=ad_n[3]


## Configuration options, can be used for all designs
set_property BITSTREAM.CONFIG.CONFIGRATE 50 [current_design]
set_property CONFIG_VOLTAGE 3.3 [current_design]
set_property CFGBVS VCCO [current_design]
set_property BITSTREAM.CONFIG.SPI_BUSWIDTH 4 [current_design]
set_property CONFIG_MODE SPIx4 [current_design]

## SW3 is assigned to a pin M5 in the 1.35v bank. This pin can also be used as
## the VREF for BANK 34. To ensure that SW3 does not define the reference voltage
## and to be able to use this pin as an ordinary I/O the following property must
## be set to enable an internal VREF for BANK 34. Since a 1.35v supply is being
## used the internal reference is set to half that value (i.e. 0.675v). Note that
## this property must be set even if SW3 is not used in the design.
set_property INTERNAL_VREF 0.675 [get_iobanks 34]




set_property BITSTREAM.GENERAL.COMPRESS TRUE [current_design]


Vivado Integrated Logic Analyzer

During the development of the project I have found very useful the incorporation to the system, while debugging, of Vivado's Integrated Logic Analyzer. Here is a sample of how easy it is to integrate. In this example we spy on signals from AMD's XADC IP module.

Instantiating the ILA for debugging the XADC IP

       // ila instantiation
   ila_0 ila
    (   .clk,
        .probe0(adc_data),
        .probe1(channel_out),
        .probe2(enable),
        .probe3(ready),
        .probe4(eos_out)
    );

Programming device attaching debug probes file

image


Conclusions

This project is a new objective fulfilled for the final goal of learning SystemVerilog HDL by creating my own synthesizer designed with FPGA. Thanks to dougw  and genebren  for posting their blogs on the subject that I am learning and being inspired by.


Project blogs

  1. Building FPGA-Based Music Instrument Synthesis: A Simple Test Bench Solution
  2. DDFS. Direct Digital Frequency Synthesis for Sound
  3. ADSR envelope generator for sound synthesis.
  4. AMD Xilinx 7 series FPGAs XADC
  • Sign in to reply
  • aspork42
    aspork42 over 2 years ago

    That is really cool! I did the first to Path 2 Programmable FPGA courses but never got too far in making own projects. Nice work on this - looks like it took quite a bit of work to get all the pieces together.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • javagoza
    javagoza over 2 years ago in reply to dougw

    Thanks dougw I have already been able to upload a small demo video. I don't have time to edit it anymore.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • dougw
    dougw over 2 years ago

    Great work.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
element14 Community

element14 is the first online community specifically for engineers. Connect with your peers and get expert answers to your questions.

  • Members
  • Learn
  • Technologies
  • Challenges & Projects
  • Products
  • Store
  • About Us
  • Feedback & Support
  • FAQs
  • Terms of Use
  • Privacy Policy
  • Legal and Copyright Notices
  • Sitemap
  • Cookies

An Avnet Company © 2025 Premier Farnell Limited. All Rights Reserved.

Premier Farnell Ltd, registered in England and Wales (no 00876412), registered office: Farnell House, Forge Lane, Leeds LS12 2NE.

ICP 备案号 10220084.

Follow element14

  • X
  • Facebook
  • linkedin
  • YouTube