element14 Community
element14 Community
    Register Log In
  • Site
  • Search
  • Log In Register
  • 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
FPGA
  • Technologies
  • More
FPGA
Blog SystemVerilog Study Notes. FPGA ADSR envelope generator for sound synthesis (I)
  • Blog
  • Forum
  • Documents
  • Quiz
  • Events
  • Polls
  • Files
  • Members
  • Mentions
  • Sub-Groups
  • Tags
  • More
  • Cancel
  • New
Join FPGA to participate - click to join for free!
  • Share
  • More
  • Cancel
Group Actions
  • Group RSS
  • More
  • Cancel
Engagement
  • Author Author: javagoza
  • Date Created: 11 Mar 2023 8:06 PM Date Created
  • Views 4846 views
  • Likes 15 likes
  • Comments 15 comments
  • ADSR
  • arty-s7
  • musictimech
  • AMD XILINX
  • DDFS
  • vivado
  • digilent
  • sound
  • amd
  • spartan-7
  • systemverilog
Related
Recommended

SystemVerilog Study Notes. FPGA ADSR envelope generator for sound synthesis (I)

javagoza
javagoza
11 Mar 2023
SystemVerilog Study Notes. FPGA ADSR envelope generator for sound synthesis (I)

ADSR envelope generator for sound synthesis.

In the previous blog we implemented a Direct Digital Frequency Synthesis module (DDFS ) that can generate an unmodulated audio-frequency tone. In this blog we will implement an ADSR (attack-decay-sustain-release) amplitude envelope module which is a widely used scheme in music synthesizers.

We will use the unmodulated DDFS module to generate a basic tone and with the ADSR module, we will modulate the signal to get closer to how musical instruments produce notes. 

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

Table of Contents

  • ADSR envelope generator for sound synthesis.
    • ADSR Introduction
    • ADSR Envelope Generator Circuit
    • FSMD design of the ADSR Envelope Generator
    • ADSR Envelope Generator SystemVerilog implementation
    • Synthesized Circuit by Vivado
    • Input values calculator designed with Excel
    • ADSRCalculator.xlsx
    • Testbench SystemVerilog Implementation
    • Testbench Scope
    • Test with the Digilent Arty S7 50
    • Examples
    • Conclusion
  • SystemVerilog Study Notes Chapters

ADSR Introduction

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. As can be seen in the figure below:

ADSR envelope

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.

The maximum level will be defined as a constant and the sustain level will be defined as a percentage of the maximum amplitude. So we only need 6 parametres to define an envelope.

ADSR Envelope Generator Circuit

The ADSR Envelope Generator is a circuit that generates an amplitude envelope following the ADSR scheme. Its output is connected to a DDFS module as an external amplitude modulation signal.

image

The ADSR circuit models the sound generation of a musical instrument and its output does not run continuously.

A single envelope is generated at a time and the generation is controlled by an start trigger signal.

The trigger signal can be triggered automatically or in real-time mode:

  • The automatic mode uses the trigger signal as a note start.
  • The real-time mode mimics the operation of a key on the piano keyboard.

In the real-time mode, when the trigger signal is asserted, the envelope circuit will start operation and remain in the sustain segment until the trigger signal is cancelled. We will start by designing a generator for automatic mode.

FSMD design of the ADSR Envelope Generator

We will construct the ADSR envelope generator using a FSMD with an amplitude counter and a sustain time counter. A FSMD is a kind of Finite State Machine with Data Path. 

A Finite-State Machine (FSM) is a sequential circuit that can be in exactly one of a finite number of states at any given time.

It contains five elements:

  • symbolic state register
  • input signal
  • output signal
  • current state
  • next state computed by a function

The symbolic state register contains the state. The FSM has a function to computer the next state and depending on current state has an output (Moore FSM) , and maybe on the input and the current state (Mealy FSM). 

A traditional FSM cannot represent storage elements (register) except the state registers. We can use instead an FSMD. An FSM with a Datapath (FSMD) is an extension of a traditional FSM. The storage and signals can be declared. and within a state expression, comparison, arithmetic or logic operations on these signals can be performed. 

We'll represent the behavior of the ADSR envelope FSMD circuit with an Algorithmic State Machine Chart or ASM chart, a flow-chart-like description constructed from ASM blocks: sate boxes, decision boxes and conditional output boxes.

This is the graphical representation of a generic ASM block for a state.

ASM Block Legend

The unconditional outputs corresponding to that state can be placed inside state box. Moore state machine outputs can also be placed inside state box.

In general, Mealy state machine outputs are represented inside conditional output box.

In addition to the attack, decay, sustain and release states we'll add and idle state for the idle condition and a launch state for the retriggering.

The start signal is the trigger to initiate the amplitude envelope generation.

Once asserted the FSMD goes through the entire envelope generation process unless the start signal is asserted before completion, in that case the FSMD aborts the current operation and starts a new envelope generation. The start signal is checked in every state.

The FSMD uses two registers, amplitude and t_sus, for the amplitude counter and the sustain counter.

ASMD Chart of the Automatic ADSR Envelope Circuit

The FSM uses the the attack, decay, sustain and release states to represent the four segments of the ADS envelope.

The amplitude counter is used to generate the envelope amplitude. It is also used to determine the time spent in the attack, decay and release states. In each state the amplitude is decremented or incremented a specific amount every clock cycle.

The value used for increment or decrement the counter in those states is passed pre-calculated to the ADSR envelope generator module: attack step, decay_step and release_step.

The ADRS envelope can be specified by:

  • Attack_ms: The time interval (t_attack ) between activation and full loudness (MAX).
  • Decay_ms: The time interval (t_decay) for Vmax to drop to the sustain level (Sustain_level).
  • Sustain_level: The constant sound volume (Sustain_level) for the note until it is released.
  • Release_ms: The time interval (t_release ) for the sound to fade from Vs to 0 when a note ends.

Attack_step: amount the amplitude counter must be incremented each clock cycle to reach MAX from zero in t_attack time

Attack_step = (MAX- 0)/(t_attack / t_sys)

  • MAX: the  maximum amplitude as MAX, the sustain amplitude as sustain_level 
  • t_attack: time in the attack segment
  • t_sys: system time, depends on system clock frequency

Decay_step: amount the amplitude counter must be decremented each clock cycle to reach the sustain level amplitude from MAX in t_decay time

 Decay_step = (MAX-sustain_level) / (t_decay / t_sys)

  • sustain_level: amplitude in the sustain segment
  • t_decay: time in the decay segment
  • t_sys: system time, depends on system clock frequency

Release_step: amount the amplitude counter must be decremented each clock cycle to reach the zero level amplitude from the sustain_level in t_release time

Release_step  = (Sustain_level-0) / (t_release / t_sys)

  • sustain_level: amplitude in the sustain segment
  • t_release: time in the release segment
  • t_sys: system time, depends on system clock frequency

Sustain_time: amount of clock cycles in the sustain segment. The amplitude is constant in the sustain segment and its interval is controlled by the sustain time.

sustain_time = t_sustain / t_sys

  • t_sustain: time in the sustain segment
  • t_sys: system time, depends on system clock frequency

The incrementing and decrementing steps appear as external circuit signals, Attack_step, Decay_step, Decay_step. The Sustain_level also appears as an external circuit signal.

  • After start is asserted, the FSMD moves from the idle state to the launch state.
  • Then the FSM moves to the attack state, in which the amplitude register is incremented with the Attack_step until it reaches the MAX amplitude.
  • Then the FSMD transits to the decay state in which the amplitude register is decremented with the Decay_step until the amplitude reaches the Sustain_level.
  • Then the FSMD transits to the sustain state in which the t_sus register is incremented until reaching Sustain_time.
  • Then the FSMD moves to the release state in which the amplitude register is decremented until it becomes 0.
  • Then the FSMD returns to the idle state.

ADSR Envelope Generator SystemVerilog implementation

Following the ASMD Chart we will convert the chart to an HDL (SystemVerilog) description.

This approach is simple and descriptive but we rely on the synthesis software (Vivado) for datapath construction and have less control than coding the data path explicitly.

`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_decay / 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_decay / 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

Graphic representation of input and output ADSR module signals

image

Synthesized Circuit by Vivadoimage

Input values calculator designed with Excel

I have created a small spreadsheet to pre-calculate the input values to the envelope generator module by indicating the times in milliseconds and the amplitude level in the sustain segment as a percentage of the maximum amplitude.

image

ADSRCalculator.xlsx

Testbench SystemVerilog Implementation

`timescale 1ns / 1ps

module adsr_tb;

    localparam T=20;
    localparam PHASE_ACC_WIDTH = 30;
    logic reset;
    logic clk;    
    
    // Attack 100ms, Decay 50ms, Sustain 100ms, Release 50ms. Sustain amplitude level 90%
    
    logic [31:0] attack_step_value  = 32'h0000_00d6;   // precalculated (Amax - 0)/(t_attack - t_sys) steps for the attack segment
    logic [31:0] decay_step_value   = 32'h0000_002a;   // precalculated (A_max-A_sus) / (t_sustain / t_sys) steps for the decay segment
    logic [31:0] sustain_level      = 32'h7333_3332;   // amplitude for the sustain segment
    logic [31:0] sustain_time       = 32'h0098_9680;   // tsustain / t_sys steps for the sustain
    logic [31:0] release_step_value = 32'h0000_0183;   // precalculated (A_sus - 0)/(t_release - t_sys) steps for the release segment
  
    logic adsr_idle;
    logic start;
    logic [15:0] envelope;   
    
    always 
    begin
        clk <= 1'b1;
        #(T/2);
        clk <= 1'b0;
        #(T/2); 
    end
    
    
        // instantiate adsr
    adsr adsr_uut(
       .clk(clk),
       .reset(reset),
       .start(start),
       .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(envelope),
       .adsr_idle(adsr_idle)
    );

    
    
    initial
    begin
        reset = 1'b1;   #40;
        reset = 1'b0;  #20;
        start = 1'b1;   #40;
        start = 1'b0;  #999999999;
        $stop;
    end
endmodule

Testbench Scope

I tried several values. A flute-like ADSR envelope

t-attack (ms) t-decay (ms) t-sustain (ms) t-release  (ms) A-sustain (% over MAX)
100 50 100 50 90%

logic [31:0] attack_step_value = 32'h0000_00d6; // precalculated (Amax - 0)/(t_attack - t_sys) steps for the attack segment
logic [31:0] decay_step_value = 32'h0000_002a; // precalculated (A_max-A_sus) / (t_decay / t_sys) steps for the decay segment
logic [31:0] sustain_level = 32'h7333_3332; // amplitude for the sustain segment
logic [31:0] sustain_time = 32'h0098_9680; // t sustain / t_sys steps for the sustain
logic [31:0] release_step_value = 32'h0000_0183; // precalculated (A_sus - 0)/(t_release - t_sys) steps for the release segment

image

Let's set the attack segment to 0 ms to remove it.
t-attack (ms) t-decay (ms) t-sustain (ms) t-release  (ms) A-sustain (% over MAX)
0 100 200 50 80%

logic [31:0] attack_step_value = 32'h7FFFFFFF; // precalculated (Amax - 0)/(t_attack - t_sys) steps for the attack segment
logic [31:0] decay_step_value = 32'h0000002B; // precalculated (A_max-A_sus) / (t_decay / t_sys) steps for the decay segment
logic [31:0] sustain_level = 32'h66666666; // amplitude for the sustain segment
logic [31:0] sustain_time = 32'h01312D00; // tsustain / t_sys steps for the sustain
logic [31:0] release_step_value = 32'h00000158; // precalculated (A_sus - 0)/(t_release - t_sys) steps for the release segment

image

Test with the Digilent Arty S7 50

Constraints

## 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]}]


## 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]


## 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]

Arty S7 Test Module

image

  • Four slide switches to change octave
  • Four switch buttons to play a note modulated with the selected envelope.

image

`timescale 1ns / 1ps

module checkDDFSModule(
    input clk,        
    input logic [3:0] sw,
    input logic [3:0] btn,
    
    // PMOD 1
    output audio_out, //  audio out
    output gain,     //  Gain
    output pulse_out,
    output neg_shutdown, // ~SHUTDOWN

    output led[3:0]
    );
    
    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  = 32'h0000_00d6;   // precalculated (Amax - 0)/(t_attack - t_sys) steps for the attack segment
    logic [31:0] decay_step_value   = 32'h0000_002a;   // precalculated (A_max-A_sus) / (t_sustain / t_sys) steps for the decay segment
    logic [31:0] sustain_level      = 32'h7333_3332;        // amplitude for the sustain segment
    logic [31:0] sustain_time       = 32'h0098_9680;         // tsustain / t_sys steps for the sustain
    logic [31:0] release_step_value = 32'h0000_0183;   // precalculated (A_sus - 0)/(t_release - t_sys) steps fot the release segment

   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   [15:0]  amp = 16'h2FFF;  // Q2.14 format -1.0 to 1.0
    logic reset;
    logic   [PHASE_ACC_WIDTH-1:0] note;
    
    assign neg_shutdown = sw[0]; // control sound on off with switch B0
    
    assign gain = sw[1];   
    
    //assign reset = btn[0];

    logic   [15:0]      pcm;                                    // pcm signal   
    
    logic [32:0]       counter;  
    logic [32:0]       counter_next;  
    logic start;
    
    
    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
    
    assign counter_next = counter +1;
    
    assign led[0] = counter[24];
    assign led[1] = pcm;
      
      
    // 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)
    );
    
    
    // instantiate ds 1bit dac    
    ds_1bit_dac #(.W(16)) ds_1bit_uut(
        .clk(clk),
        .reset(reset),
        .pcm_in(pcm),
        .pdm_out(audio_out)  
    );

    
endmodule

image

Examples

Waveform captured with the Digilent Analog Discovery 2 and WaveForms virtual instrument suite 

image

Changing octave, higher frequency.

image

Envelop with long attack

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

Piano like envelope using a triangle wave DDFS

image

And using a sine wave DDFS

image

Conclusion

This is the second blog dedicated to generating sounds with the Spartan-7 FPGA within the SystemVerilog Study Notes series that I'm posting on element14.

I have learned to implement a circuit in SystemVerilog HDL that generates a configurable ADSR envelope starting from the design of a finite state machine with data path.

I have used the envelope signal generated by the circuit to amplitude modulate a waveform generated by the DDFS circuit that we built in the previous blog and I have experimented with the 1-bit delta sigma DAC also built in the previous blog.

This modulator circuit can be used not only to modulate the amplitude but also as a Cutoff Envelope to control the cutoff of a low-pass filter and as a Frequency Envelope to control the frequency of an oscillator. I will try to experiment with those uses in future posts.

Project sources and tools

The following zip file s7_ddfs_adsr_sources_tools.zip contains:

  • Arty S7 constraints file
  • SystemVerilog source files
  • SystemVerilog Testbench source files
  • ADSR values calculator spreadsheet
  • Diagrams in draw.io format

SystemVerilog Study Notes Chapters

  1.  Gate-Level Combinational Circuit 
  2.  RTL Combinational Circuit Operators 
  3.  RTL Combinational Circuit - Concurrent and Control Constructs 
  4.  Hex-Digit to Seven-Segment LED Decoder RTL Combinational Circuit 
  5.  Barrel Shifter RTL Combinational Circuit 
  6.  Simplified Floating Point Arithmetic. RTL Combinational Circuit 
  7.  BCD Number Format. RTL Combinational Circuit 
  8.  DDFS. Direct Digital Frequency Synthesis for Sound 
  9.  FPGA ADSR envelope generator for sound synthesis 
  10.  AMD Xilinx 7 series FPGAs XADC 
  11.  Building FPGA-Based Music Instrument Synthesis: A Simple Test Bench Solution 
  • Sign in to reply

Top Comments

  • Jan Cumps
    Jan Cumps over 2 years ago +2
    This is an impressive story. I have the same hardware. I'm going to try and replicate it first. Then maybe translate to VHDL I want to improve my VHDL skills, and porting will also improve my Verilog understanding…
  • javagoza
    javagoza over 2 years ago +1
    I have added a zip file with the sources and other project files.
  • genebren
    genebren over 2 years ago +1
    Great blog! I find this as all interesting and relevant as it tracks with my current synthesizer projects (analog/digital mix). Keep up the good work!
  • dang74
    dang74 over 2 years ago in reply to jc2048

    That must have been a really awarding experience.  Also it certainly shows the important role magazines played before the internet. We are spoiled now with everything at our fingertips.  Back then we had to go to the store too.  The magazines gave us a reason to leave the house and we had to be patient and wait a month for the new issue.  We certainly live in an interesting time now but sometimes I think we've lost little parts of an old world that were special in their own way.

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

    I'll have to try it for sure.  That's right up my ally.  Speaking of the Sinclair ZX81.  One summer my friend went to Portugal and he came back with a ZX81.  If I am not mistaken it was branded Timex Sinclair.  Not sure if that double branding is a Portugal only thing.  Anyway, he only had 3 cassettes, Frogger, some spread sheet program called Vu-Calc and I don't remember what the third cassette was.  Now a days the keyboard would drive me crazy but back then as a kid we had no complaints and it kept us busy and out of the dangerous 1980s sunlight, lol.

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

    Very cool panel. I think I can make a similar one using the Spartan-7's built-in ADC and four potentiometers. The font is cool too!

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

    I'm a bit older than you two, so hardware synths were my first exposure to ADSR. I didn't own one back then, but I had come across the build-your-own designs in magazines. This is how it looks on the panel of a Sequential Circuits Pro-One.

    image

    Not sure why they picked that font. Perhaps they wanted all the prog rock bands to think they were made by elves.

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

    Thanks for comment. Yes, indeed I am using linear increments and it is also true that it does not have a "natural" touch. Perhaps using an exponential scale for attack and then logarithmic for decay and release would sound better. I don't even know if it makes sense but I could try to implement a digital low pass filter at the output of the linear ADSR. I will try to investigate it in another post.

    Once again, thank you very much for your suggestions and contributions. We are lucky to have you in the community.

    • 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