Introduction: Basys3 FPGA Digital Audio Synthesizer

This digital sine wave keyboard synthesizer will take user inputs via a series of momentary switches laid out like a keyboard and output an audio wave through a speaker. Based on user inputs, the device will generate sine waves of various frequencies from C4 to C6. The user can input notes from C4 up to C6 (25 notes total), and up to four keys at the same time -- if more than four keys are pressed, the four lowest tones will be played.

This project was done by Ryan Morris and Mavis Tsoi for our Cal Poly CPE 133 Digital Design class :)

Step 1: Theory

An FPGA board can only output digital signals. In other words, it can only produce a high (3.3V) voltage or a low (0V) voltage. However, audio signals are analog and can have infinitely many increments in voltage. To get around this, we will use a PWM (pulse width modulation) signal to emulate an analog wave. If you don’t know what PWM is, check this out: https://learn.sparkfun.com/tutorials/pulse-width-modulation.

Step 2: Ingredients & Tools

  • Computer with Vivado installed
  • We will be using Vivado version 2017.2
  • Basys3 FPGA Board
  • 25 SPDT Limit Switches (we used these)
  • 30 jumper wires (one end male, other end doesn’t matter), 12 inch
  • Wire cutters
  • Wire strippers
  • Spare wire for soldering
  • Resin-core Solder
  • Soldering iron
  • ¼” female audio jack
  • Amplifier/speaker
  • Something to mount the switches on (we used protoboard + wooden box)

Step 3: Wiring & Hardware Setup

Picture of Wiring & Hardware Setup

System Architecture

See Figure 1: 25 available inputs → Basys3 Board → amplifier & speaker.

Output

See Figure 2: Basys3 Board → 1/2" Female Audio Jack → Speaker (with Amplifier)

Input

The pmod connections on the Basys3 board must be connected to ground in order to see a low input and will not function properly if left as an open circuit. Because of this, we have to use SPDT switches for all of our note keys. An SPDT switch basically allows the user to switch between circuits when pressed, so we will use them as our “buttons” to input low (0V) or high (3.3V) signals to the Basys3 board.

Each switch will have the NO (normally opened) terminal connected to 3.3V, NC (normally closed) terminal connected to GND, and COM (common) terminal connected to the FPGA input. See Figure 3.

Because we have 25 limit switches, they will all share a common 3.3V line and a common GND line. Then, the signal line from each limit switch will be bundled up in groups of 8 and hooked up to the pmod connections on the Basys3 board using zippable jumper wires to minimize the monumental mess we’ll make. See Figure 4 or an example of the first eight keys.

Step 4: VHDL Setup (Vivado)

Picture of VHDL Setup (Vivado)

The sine wave generator and PWM generator were first tested to make sure our concept worked, then the input limiter and amplitude adder/shifter were integrated. Details of the function and I/O of each process block is as shown in the Figure. The code is shown below, but also attached as VHD and txt files. If there are discrepancies, go with the VHD files.

BTW: we probably should've made our lines shorter but code embedding on Instructables also turned out to be quite annoying to deal with, so the spacing isn't the greatest and there's no syntax highlighting. If you have Vivado and would like to follow along the code, we highly recommend you to just download the file.

First, let's look at the Sine Wave Generator module.

library IEEE;<br>use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity Wave_Generator is
    Port ( Trigger : in STD_LOGIC;                       -- Key press
          Freq_Cnt : in STD_LOGIC_VECTOR(15 downto 0);   -- Counter value = 100MHz / (Note Frequency*64 Divisions of Sine Wave) (round to nearest num)
                                                            -- renamed from Freq
        wavegenCLK : in STD_LOGIC;                       -- Basys3 100MHz CLK
           WaveOut : out STD_LOGIC_VECTOR(9 downto 0));  -- Signed amplitude of wave
end Wave_Generator;
architecture Behavioral of Wave_Generator is
    signal i : integer range 0 to 64 := 0;                           -- index of amplitude memory bank
    type memory_type is array (0 to 63) of integer range -64 to 63;  -- create memory bank (ROM) to hold amplitude values
                                                                      -- is this RAM or ROM just wondering...
    signal amplitude : memory_type := ( 0,  7, 13, 19, 25, 30, 35, 40, 45, 49, 52, 55, 58, 60, 62, 63, 
                                       63, 63, 62, 60, 58, 55, 52, 49, 45, 40, 35, 30, 25, 19, 13,  7, 
                                        0, -7,-13,-19,-25,-30,-35,-40,-45,-49,-52,-55,-58,-60,-62,-63,  
                                      -63,-63,-62,-60,-58,-55,-52,-49,-45,-40,-35,-30,-25,-19,-13, -7); -- amplitude memory bank for sine wave
begin
    process (wavegenCLK, Trigger)
        variable counter : unsigned (15 downto 0) := to_unsigned(0, 16); -- clock divider counter, renamed from count1
    begin
        if (rising_edge(wavegenCLK)) then
            if (Trigger = '1') then -- key is pressed
                counter := counter + 1;
                if (counter = unsigned(Freq_Cnt)) then  -- Freq_Cnt = 100Mhz / (note freq * 64 divisions of the sine wave)   
                    -- reset counter and assign amplitude data to output
                    counter := to_unsigned(0, 16);
                    WaveOut <= STD_LOGIC_VECTOR (to_signed(amplitude(i), 10));
                    -- increment i for next reading
                    i <= i + 1;
                    -- reset i if one sine wave has been completed
                    if(i = 63) then
                        i <= 0;
                    end if;
                end if; -- (counter = unsigned(Freq_Cnt))
            else -- key is not pressed
                -- reset output, amplitude index, and counter
                WaveOut <= "0000000000";
                i <= 0;
                counter := to_unsigned(0, 16); --output Amplitude = -64 when no note is played
            end if; -- (Trigger = '1')
        end if; -- (rising_edge(CLK))
    end process;
end Behavioral;

We will generate a digital sine wave in the Basys3 by using the internal clock and a ROM. This ROM will store 64 values that represent 64 amplitudes on a sine wave. See Figure 1. The 64 values we use emulate a sine wave with pretty good resolution.

Using the internal clock, we count to a value that represents the clock speed divided by the frequency of the wave we want and 64: Clk div = 100MHz / (Freq * 64) Every time our counter reaches that value, we call a number from the ROM and send that out of our wave generator module. The frequency of our wave will depend on how fast we call these amplitudes.

We will have 25 sub-modules, each associated with one frequency/note.

Here's the remaining of the code that calls the Sine Wave Generator modules:

library IEEE;<br>use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;


entity Two_Octave_Synth is
    Port ( CLK : in STD_LOGIC; 
           O4 : in STD_LOGIC_VECTOR(11 downto 0);
           O5 : in STD_LOGIC_VECTOR(12 downto 0);
           output : out STD_LOGIC);
end Two_Octave_Synth;


architecture Behavioral of Two_Octave_Synth is
    component Wave_Generator is
        Port ( Trigger : in STD_LOGIC;
           Freq_Cnt : in STD_LOGIC_VECTOR(15 downto 0);
           wavegenCLK : in STD_LOGIC;
           WaveOut : out STD_LOGIC_VECTOR(9 downto 0));
    end component;


---------------------------output signals from wave generator-----------------------
    signal WaveC4, WaveCs4, WaveD4, WaveDs4, WaveE4, WaveF4, WaveFs4, WaveG4, WaveGs4, WaveA4, WaveAs4, WaveB4, 
           WaveC5, WaveCs5, WaveD5, WaveDs5, WaveE5, WaveF5, WaveFs5, WaveG5, WaveGs5, WaveA5, WaveAs5, WaveB5, WaveC6  : signed(9 downto 0);


--------------------------------for note selection logic--------------------
    signal C4, Cs4, D4, Ds4, E4, F4, Fs4, G4, Gs4, A4, As4, B4, C5, Cs5, D5, Ds5, E5, F5, Fs5, G5, Gs5, A5, As5, B5, C6 : unsigned(4 downto 0);
    signal cntC4, cntCs4, cntD4, cntDs4, cntE4, cntF4, cntFs4, cntG4, cntGs4, cntA4, cntAs4, cntB4, cntC5, cntCs5, cntD5, 
           cntDs5, cntE5, cntF5, cntFs5, cntG5, cntGs5, cntA5, cntAs5, cntB5, cntC6 : unsigned(4 downto 0);
    signal error : STD_LOGIC;


-----------------------------------for adding sine waves--------------------------
    signal Wave0, Wave1, Wave2, Wave3 : signed(9 downto 0); --signals from Wave Generator module output
    signal WaveSum : STD_LOGIC_VECTOR(9 downto 0); --signal for summed sine waves (2's compliment -512 to 511)
    signal positiveWaveSum : STD_LOGIC_VECTOR(9 downto 0); --unsigned 0 to 1023, for use in PWM generator


-----------------------------------for generating PWM-------------------------------
    signal ping_length : unsigned (9 downto 0) := unsigned(positiveWaveSum);
    --signal off_length : unsigned (6 downto 0) := to_unsigned(127, 7) - unsigned(WAVE);
    signal PWM : unsigned (9 downto 0) := to_unsigned(0, 10);


begin
    Note_C4  : Wave_Generator port map (Trigger => O4(0), Freq_Cnt => X"1755", wavegenCLK => CLK, signed(WaveOut) => WaveC4); --5973, 261.63 Hz
    Note_Cs4 : Wave_Generator port map (Trigger => O4(1), Freq_Cnt => X"1606", wavegenCLK => CLK, signed(WaveOut) => WaveCs4);--5638, 277.18 Hz
    Note_D4  : Wave_Generator port map (Trigger => O4(2), Freq_Cnt => X"14C9", wavegenCLK => CLK, signed(WaveOut) => WaveD4); --5321, 293.66 Hz
    Note_Ds4 : Wave_Generator port map (Trigger => O4(3), Freq_Cnt => X"139F", wavegenCLK => CLK, signed(WaveOut) => WaveDs4);--5023, 311.13 Hz
    Note_E4  : Wave_Generator port map (Trigger => O4(4), Freq_Cnt => X"1285", wavegenCLK => CLK, signed(WaveOut) => WaveE4); --4741, 329.63 Hz
    Note_F4  : Wave_Generator port map (Trigger => O4(5), Freq_Cnt => X"117B", wavegenCLK => CLK, signed(WaveOut) => WaveF4); --4475, 349.23 Hz
    Note_Fs4 : Wave_Generator port map (Trigger => O4(6), Freq_Cnt => X"1080", wavegenCLK => CLK, signed(WaveOut) => WaveFs4);--4224, 369.99 Hz
    Note_G4  : Wave_Generator port map (Trigger => O4(7), Freq_Cnt => X"0F92", wavegenCLK => CLK, signed(WaveOut) => WaveG4); --3986, 392.00 Hz
    Note_Gs4 : Wave_Generator port map (Trigger => O4(8), Freq_Cnt => X"0EB3", wavegenCLK => CLK, signed(WaveOut) => WaveGs4);--3763, 415.30 Hz
    Note_A4  : Wave_Generator port map (Trigger => O4(9), Freq_Cnt => X"0DE0", wavegenCLK => CLK, signed(WaveOut) => WaveA4); --3552, 440.00 Hz
    Note_As4 : Wave_Generator port map (Trigger => O4(10),Freq_Cnt => X"0D18", wavegenCLK => CLK, signed(WaveOut) => WaveAs4);--3352, 466.16 Hz
    Note_B4  : Wave_Generator port map (Trigger => O4(11),Freq_Cnt => X"0C5C", wavegenCLK => CLK, signed(WaveOut) => WaveB4); --3164, 493.88 Hz
-------------------------------------------------------------------------------------------------------------------------                                                                  
    Note_C5  : Wave_Generator port map (Trigger => O5(0), Freq_Cnt => X"0BAB", wavegenCLK => CLK, signed(WaveOut) => WaveC5); --2987, 523.25 Hz
    Note_Cs5 : Wave_Generator port map (Trigger => O5(1), Freq_Cnt => X"0B03", wavegenCLK => CLK, signed(WaveOut) => WaveCs5);--2819, 554.37 Hz
    Note_D5  : Wave_Generator port map (Trigger => O5(2), Freq_Cnt => X"0A65", wavegenCLK => CLK, signed(WaveOut) => WaveD5); --2661, 587.33 Hz
    Note_Ds5 : Wave_Generator port map (Trigger => O5(3), Freq_Cnt => X"09D0", wavegenCLK => CLK, signed(WaveOut) => WaveDs5);--2512, 622.25 Hz
    Note_E5  : Wave_Generator port map (Trigger => O5(4), Freq_Cnt => X"0943", wavegenCLK => CLK, signed(WaveOut) => WaveE5); --2371, 659.25 Hz
    Note_F5  : Wave_Generator port map (Trigger => O5(5), Freq_Cnt => X"08Be", wavegenCLK => CLK, signed(WaveOut) => WaveF5); --2238, 698.46 Hz
    Note_Fs5 : Wave_Generator port map (Trigger => O5(6), Freq_Cnt => X"0840", wavegenCLK => CLK, signed(WaveOut) => WaveFs5);--2112, 739.99 Hz
    Note_G5  : Wave_Generator port map (Trigger => O5(7), Freq_Cnt => X"07CA", wavegenCLK => CLK, signed(WaveOut) => WaveG5); --1994, 783.99 Hz
    Note_Gs5 : Wave_Generator port map (Trigger => O5(8), Freq_Cnt => X"075A", wavegenCLK => CLK, signed(WaveOut) => WaveGs5);--1882, 830.61 Hz
    Note_A5  : Wave_Generator port map (Trigger => O5(9), Freq_Cnt => X"06F0", wavegenCLK => CLK, signed(WaveOut) => WaveA5); --1776, 880.00 Hz
    Note_As5 : Wave_Generator port map (Trigger => O5(10),Freq_Cnt => X"068C", wavegenCLK => CLK, signed(WaveOut) => WaveAs5);--1676, 932.33 Hz
    Note_B5  : Wave_Generator port map (Trigger => O5(11),Freq_Cnt => X"062E", wavegenCLK => CLK, signed(WaveOut) => WaveB5); --1582, 987.77 Hz
    Note_C6  : Wave_Generator port map (Trigger => O5(12),Freq_Cnt => X"05D6", wavegenCLK => CLK, signed(WaveOut) => WaveC6); --1494, 1046.5 Hz


------------note selection logic------------
    C4  <= "0000" & O4(0);
    Cs4 <= "0000" & O4(1);
    D4  <= "0000" & O4(2);
    Ds4 <= "0000" & O4(3);
    E4  <= "0000" & O4(4);
    F4  <= "0000" & O4(5);
    Fs4 <= "0000" & O4(6);
    G4  <= "0000" & O4(7);
    Gs4 <= "0000" & O4(8);
    A4  <= "0000" & O4(9);
    As4 <= "0000" & O4(10);
    B4  <= "0000" & O4(11);
    C5  <= "0000" & O5(0);
    Cs5 <= "0000" & O5(1);
    D5  <= "0000" & O5(2);
    Ds5 <= "0000" & O5(3);
    E5  <= "0000" & O5(4);
    F5  <= "0000" & O5(5);
    Fs5 <= "0000" & O5(6);
    G5  <= "0000" & O5(7);
    Gs5 <= "0000" & O5(8);
    A5  <= "0000" & O5(9);
    As5 <= "0000" & O5(10);
    B5  <= "0000" & O5(11);
    C6  <= "0000" & O5(12);  
    cntC4 <= C4;
    cntCs4 <= C4 + Cs4;
    cntD4  <= C4 + Cs4 + D4;
    cntDs4 <= C4 + Cs4 + D4 + Ds4;
    cntE4  <= C4 + Cs4 + D4 + Ds4 + E4;
    cntF4  <= C4 + Cs4 + D4 + Ds4 + E4 + F4;
    cntFs4 <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4;
    cntG4  <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4 + G4;
    cntGs4 <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4 + G4 + Gs4;
    cntA4  <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4 + G4 + Gs4 + A4;
    cntAs4 <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4 + G4 + Gs4 + A4 + As4;
    cntB4  <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4 + G4 + Gs4 + A4 + As4 + B4;
    cntC5  <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4 + G4 + Gs4 + A4 + As4 + B4 + C5;
    cntCs5 <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4 + G4 + Gs4 + A4 + As4 + B4 + C5 + Cs5;
    cntD5  <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4 + G4 + Gs4 + A4 + As4 + B4 + C5 + Cs5 + D5;
    cntDs5 <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4 + G4 + Gs4 + A4 + As4 + B4 + C5 + Cs5 + D5 + Ds5;
    cntE5  <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4 + G4 + Gs4 + A4 + As4 + B4 + C5 + Cs5 + D5 + Ds5 + E5;
    cntF5  <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4 + G4 + Gs4 + A4 + As4 + B4 + C5 + Cs5 + D5 + Ds5 + E5 + F5;
    cntFs5 <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4 + G4 + Gs4 + A4 + As4 + B4 + C5 + Cs5 + D5 + Ds5 + E5 + F5 + Fs5;
    cntG5  <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4 + G4 + Gs4 + A4 + As4 + B4 + C5 + Cs5 + D5 + Ds5 + E5 + F5 + Fs5 + G5;
    cntGs5 <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4 + G4 + Gs4 + A4 + As4 + B4 + C5 + Cs5 + D5 + Ds5 + E5 + F5 + Fs5 + G5 + Gs5;
    cntA5  <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4 + G4 + Gs4 + A4 + As4 + B4 + C5 + Cs5 + D5 + Ds5 + E5 + F5 + Fs5 + G5 + Gs5 + A5;
    cntAs5 <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4 + G4 + Gs4 + A4 + As4 + B4 + C5 + Cs5 + D5 + Ds5 + E5 + F5 + Fs5 + G5 + Gs5 + A5 + As5;
    cntB5  <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4 + G4 + Gs4 + A4 + As4 + B4 + C5 + Cs5 + D5 + Ds5 + E5 + F5 + Fs5 + G5 + Gs5 + A5 + As5 + B5;
    cntC6  <= C4 + Cs4 + D4 + Ds4 + E4 + F4 + Fs4 + G4 + Gs4 + A4 + As4 + B4 + C5 + Cs5 + D5 + Ds5 + E5 + F5 + Fs5 + G5 + Gs5 + A5 + As5 + B5 + C6;


    Selection : process (WaveC4, WaveCs4, WaveD4, WaveDs4, WaveE4, WaveF4, WaveFs4, WaveG4, WaveGs4, WaveA4, WaveAs4, WaveB4, 
                         WaveC5, WaveCs5, WaveD5, WaveDs5, WaveE5, WaveF5, WaveFs5, WaveG5, WaveGs5, WaveA5, WaveAs5, WaveB5, WaveC6)


    begin  
        if (cntC6 = "00000") then ---------------if no signals being generated
            Wave0 <= "0000000000";
            Wave1 <= "0000000000";
            Wave2 <= "0000000000";
            Wave3 <= "0000000000";
        else
            if (O4(0) = '1') then -------------------note C4 played
                Wave0 <= WaveC4;
            end if;
            if (O4(1) = '1') then -------------------note Cs5 played
                case(cntCs4) is
                    when "00001" => Wave0 <= WaveCs4;
                    when "00010" => Wave1 <= WaveCs4;
                    when others => error <= '1';
                end case;
            end if;
            if (O4(2) = '1') then--------------------note D4 played
                case(cntD4) is
                    when "00001" => Wave0 <= WaveD4;
                    when "00010" => Wave1 <= WaveD4;
                    when "00011" => Wave2 <= WaveD4;
                    when others => error <= '1';
                end case;
            end if;
-------------------------------------------------------------------------------------------------------------------------
            if (O4(3) = '1') then--------------------note Ds4 played
                case(cntDs4) is
                    when "00001" => Wave0 <= WaveDs4;
                    when "00010" => Wave1 <= WaveDs4;
                    when "00011" => Wave2 <= WaveDs4;
                    when "00100" => Wave3 <= WaveDs4;
                    when others => error <= '1';
                end case;
            end if;
            if (O4(4) = '1') then--------------------note E4 played
                case(cntE4) is
                    when "00001" => Wave0 <= WaveE4;
                    when "00010" => Wave1 <= WaveE4;
                    when "00011" => Wave2 <= WaveE4;
                    when "00100" => Wave3 <= WaveE4;
                    when others => error <= '1';
                end case;
            end if;
            if (O4(5) = '1') then--------------------note F4 played
                case(cntF4) is
                    when "00001" => Wave0 <= WaveF4;
                    when "00010" => Wave1 <= WaveF4;
                    when "00011" => Wave2 <= WaveF4;
                    when "00100" => Wave3 <= WaveF4;
                    when others => error <= '1';
                end case;
            end if;
            if (O4(6) = '1') then--------------------note Fs4 played
                case(cntFs4) is
                    when "00001" => Wave0 <= WaveFs4;
                    when "00010" => Wave1 <= WaveFs4;
                    when "00011" => Wave2 <= WaveFs4;
                    when "00100" => Wave3 <= WaveFs4;
                    when others => error <= '1';
                end case;
            end if;
            if (O4(7) = '1') then--------------------note G4 played
                case(cntG4) is
                    when "00001" => Wave0 <= WaveG4;
                    when "00010" => Wave1 <= WaveG4;
                    when "00011" => Wave2 <= WaveG4;
                    when "00100" => Wave3 <= WaveG4;
                    when others => error <= '1';
                end case;
            end if;
            if (O4(8) = '1') then--------------------note Gs4 played
                case(cntGs4) is
                    when "00001" => Wave0 <= WaveGs4;
                    when "00010" => Wave1 <= WaveGs4;
                    when "00011" => Wave2 <= WaveGs4;
                    when "00100" => Wave3 <= WaveGs4;
                    when others => error <= '1';
                end case;
            end if;
            if (O4(9) = '1') then--------------------note A4 played
                case(cntA4) is
                    when "00001" => Wave0 <= WaveA4;
                    when "00010" => Wave1 <= WaveA4;
                    when "00011" => Wave2 <= WaveA4;
                    when "00100" => Wave3 <= WaveA4;
                    when others => error <= '1';
                end case;
            end if;
            if (O4(10) = '1') then--------------------note As4 played
                case(cntAs4) is
                    when "00001" => Wave0 <= WaveAs4;
                    when "00010" => Wave1 <= WaveAs4;
                    when "00011" => Wave2 <= WaveAs4;
                    when "00100" => Wave3 <= WaveAs4;
                    when others => error <= '1';
                end case;
            end if;
            if (O4(11) = '1') then--------------------note B4 played
                case(cntB4) is
                    when "00001" => Wave0 <= WaveB4;
                    when "00010" => Wave1 <= WaveB4;
                    when "00011" => Wave2 <= WaveB4;
                    when "00100" => Wave3 <= WaveB4;
                    when others => error <= '1';
                end case;
            end if;
            if (O5(0) = '1') then--------------------note C5 played
                case(cntC5) is
                    when "00001" => Wave0 <= WaveC5;
                    when "00010" => Wave1 <= WaveC5;
                    when "00011" => Wave2 <= WaveC5;
                    when "00100" => Wave3 <= WaveC5;
                    when others => error <= '1';
                end case;
            end if;
            if (O5(1) = '1') then--------------------note Cs5 played
                case(cntCs5) is
                    when "00001" => Wave0 <= WaveCs5;
                    when "00010" => Wave1 <= WaveCs5;
                    when "00011" => Wave2 <= WaveCs5;
                    when "00100" => Wave3 <= WaveCs5;
                    when others => error <= '1';
                end case;
            end if;
            if (O5(2) = '1') then--------------------note D5 played
                case(cntD5) is
                    when "00001" => Wave0 <= WaveD5;
                    when "00010" => Wave1 <= WaveD5;
                    when "00011" => Wave2 <= WaveD5;
                    when "00100" => Wave3 <= WaveD5;
                    when others => error <= '1';
                end case;
            end if;
            if (O5(3) = '1') then--------------------note Ds5 played
                case(cntDs5) is
                    when "00001" => Wave0 <= WaveDs5;
                    when "00010" => Wave1 <= WaveDs5;
                    when "00011" => Wave2 <= WaveDs5;
                    when "00100" => Wave3 <= WaveDs5;
                    when others => error <= '1';
                end case;
            end if;
            if (O5(4) = '1') then--------------------note E5 played
                case(cntE5) is
                    when "00001" => Wave0 <= WaveE5;
                    when "00010" => Wave1 <= WaveE5;
                    when "00011" => Wave2 <= WaveE5;
                    when "00100" => Wave3 <= WaveE5;
                    when others => error <= '1';
                end case;
            end if;
            if (O5(5) = '1') then--------------------note F5 played
                case(cntF5) is
                    when "00001" => Wave0 <= WaveF5;
                    when "00010" => Wave1 <= WaveF5;
                    when "00011" => Wave2 <= WaveF5;
                    when "00100" => Wave3 <= WaveF5;
                    when others => error <= '1';
                end case;
            end if;
            if (O5(6) = '1') then--------------------note Fs5 played
                case(cntFs5) is
                    when "00001" => Wave0 <= WaveFs5;
                    when "00010" => Wave1 <= WaveFs5;
                    when "00011" => Wave2 <= WaveFs5;
                    when "00100" => Wave3 <= WaveFs5;
                    when others => error <= '1';
                end case;
            end if;
            if (O5(7) = '1') then--------------------note G5 played
                case(cntG5) is
                    when "00001" => Wave0 <= WaveG5;
                    when "00010" => Wave1 <= WaveG5;
                    when "00011" => Wave2 <= WaveG5;
                    when "00100" => Wave3 <= WaveG5;
                    when others => error <= '1';
                end case;
            end if;
            if (O5(8) = '1') then--------------------note Gs5 played
                case(cntGs5) is
                    when "00001" => Wave0 <= WaveGs5;
                    when "00010" => Wave1 <= WaveGs5;
                    when "00011" => Wave2 <= WaveGs5;
                    when "00100" => Wave3 <= WaveGs5;
                    when others => error <= '1';
                end case;
            end if;
            if (O5(9) = '1') then--------------------note A5 played
                case(cntA5) is
                    when "00001" => Wave0 <= WaveA5;
                    when "00010" => Wave1 <= WaveA5;
                    when "00011" => Wave2 <= WaveA5;
                    when "00100" => Wave3 <= WaveA5;
                    when others => error <= '1';
                end case;
            end if;
            if (O5(10) = '1') then--------------------note As5 played
                case(cntAs5) is
                    when "00001" => Wave0 <= WaveAs5;
                    when "00010" => Wave1 <= WaveAs5;
                    when "00011" => Wave2 <= WaveAs5;
                    when "00100" => Wave3 <= WaveAs5;
                    when others => error <= '1';
                end case;
            end if;
            if (O5(11) = '1') then--------------------note B5 played
                case(cntB5) is
                    when "00001" => Wave0 <= WaveB5;
                    when "00010" => Wave1 <= WaveB5;
                    when "00011" => Wave2 <= WaveB5;
                    when "00100" => Wave3 <= WaveB5;
                    when others => error <= '1';
                end case;
            end if;
---------------------------------------------------------------------------------------------------------------------------------    
            if(O5(12) = '1') then--------------------note C6 played
                case(cntC6) is
                    when "00001" => 
                        Wave0 <= WaveC6;
                        Wave1 <= "0000000000";
                        Wave2 <= "0000000000";
                        Wave3 <= "0000000000";
                    when "00010" => 
                        Wave1 <= WaveC6;
                        Wave2 <= "0000000000";
                        Wave3 <= "0000000000";
                    when "00011" => 
                        Wave2 <= WaveC6;
                        Wave3 <= "0000000000";
                    when "00100" => Wave3 <= WaveC6;
                    when others => error <= '1';
                end case;   
            else
                case(cntC6) is--------------------------fill in extra waves
                    when "00001" => 
                        Wave1 <= "0000000000";
                        Wave2 <= "0000000000";
                        Wave3 <= "0000000000";
                    when "00010" => 
                        Wave2 <= "0000000000";
                        Wave3 <= "0000000000";
                    when "00011" => 
                        Wave3 <= "0000000000";
                    when others => error <= '1';
                end case;   
            end if;    
        end if;     
    end process;


-------------sine wave adder--------------------
    WaveSum <= STD_LOGIC_VECTOR(Wave0 + Wave1 + Wave2 + Wave3);


---------make sine wave positive for pwm---------------------
    positiveWaveSum <= not WaveSum(9) & WaveSum(8 downto 0);


-------------PWM generator---------------------
    process(CLK) 
            --variable count : unsigned (1 downto 0) := to_unsigned(0, 2);
        begin
            if (rising_edge(CLK)) then
                --count := count + 1;
                --if (count = to_unsigned(4, 2)) then
                    --count := to_unsigned(0, 2);
                    --if (PWM = to_
                    if (PWM < ping_length) then
                        output <= '1';
                    else
                        output <= '0';
                    end if;
                    PWM <= PWM + 1;
                    ping_length <= unsigned(positiveWaveSum);
                --end if;
            end if;
        end process;
end Behavioral; 


4 Note Selector
The trickiest part of this project is selecting just four frequencies. We did it with a whole lotta IF statements, and we used signals instead of variables so that the process can be simulated and debugged. We tried other methods using variables and FOR loops, but ran into run-time errors. So, in the end, we decided that if it works, we’ll leave it alone. Don’t fix what ain’t broken amirite?

The four output waves are labeled Wave0, Wave1, Wave2, Wave3 -- these are what will be added together to form the final output.

Looking at the code, you’ll see a bunch of signals labeled C4, Cs4, D4, Ds4, etc. These are 5-bit signals that take the corresponding trigger from O4 (octave 4) or O5 (octave 5) and make them 5-bit for adding.

Next the cntC4, cntCs4, etc variables represent how many notes lower than the target note have been played, including the target note. For example, if C4, E4, G4, A#4, and D5 are played (C9 chord) cntC4 will be 1, cntE4 will be 2, cntG4 will be 3, etc.

Then, whenever a note is played, the count for the target note will be examined to see where to hook the note signal up to. For example, if D5 note is played (which means O5(2) is high) and cntD5 is 3, then there are currently 3 notes being played, with 2 notes lower than D5, so we will hook waveD5 to Wave2 (the third wave signal counting from Wave0). Alternatively, if cntD5 is 5, then there are currently 5 notes being played, with 4 notes lower than D5, so we will just leave waveD5 hanging and not do anything with it.

The IF statements are then repeated to cover the cases for all 25 notes.


Amplitude Adder

After the lowest 4 waves are selected we have to add them together. The reason we will only add four notes together is because the PWM idea we are using for our output can only have a certain resolution until the PWM is running too slow and the speaker will start picking up the PWM square wave. For example, if we were to use a resolution of 8192 (13 bit), each of those 8192 points has to correspond to a rising edge of the onboard clock. So, 100MHz / 8192 = 12.2kHz, which is well within the range of human hearing.

The actual addition of the amplitudes is super simple, you just have to make sure it can run really fast.


PWM Output

The PWM’s duty cycle will represent the amplitude of our output wave at that instant. For example, if we have an amplitude range of 0 to 128, 0 would be a 0% duty cycle, 64 would be 50%, 128 would be 100%, etc. This PWM will run extremely fast (ours is 97.6 kHz), so fast that the speaker will not recognize the individual square waves and instead look at the average voltage, creating our “analog” signal.

Constraints File

You may have hooked up your hardware differently, so just make sure the constraints file matches up.

Step 5: Code Downloads

Below is the code, both in .txt format and .vhd for Vivado. Wave_Generator is the wave generator sub-module, and Two_Octave_Synth is the top module with everything else.

Comments

Swansong (author)2017-11-29

That's a neat idea :)

MavisT2 (author)Swansong2017-12-01

Thanks Swansong. We just finished the project and added a video of our synthesizer in action. Check it out.

About This Instructable

71views

2favorites

More by MavisT2:Basys3 FPGA Digital Audio Synthesizer
Add instructable to: