Skip to content

Lab 3: Music Player
Part B: Multi-Note and Music Player

Lab 3 will give you experience designing, implementing, testing, and prototyping combinational and sequential logic using the Verilog hardware description language. The lab will continue to leverage concepts from Topic 2: Combinational Logic, Topic 3: Boolean Algebra, and Topic 4: Combinational Building Blocks but will also leverage concepts from Topic 6: Sequential Logic, Topic 7: Finite-State Machines, and Topic 8: Sequential Building Blocks. More specifically, the lab will give students experience with: latches, flip-flops, and registers; Moore and Mealy FSMs; and counters. The lab will continue to reinforce three key abstraction principles: modularity, hierarchy, and regularity.

You will be implementing a music player that takes as input a song selection (via the switches) and a start song signal (via a push button). The music player will then play the chosen song by generating a square wave at appropriate note frequencies suitable for use with a piezoelectric buzzer. An idle signal is displayed using an LED so that user knows when the player is ready to play a new song. The music player will make use of the adders and muxes from Lab 2. The song selection and the current note are both displayed using seven-segment displays from Lab 1. This lab also serves as a transition from lower-level gate-level (GL) modeling to higher-level register-transfer-level (RTL) modeling. Some of parts of your design will use explicit GL modeling, while other parts of your design will use RTL modeling. Students will have a chance to appreciate how RTL modeling can improve productivity but with less control over the final hardware implementation. The lab includes five parts:

  • Part A: Counter and Note Player

    • Due 10/19 @ 11:59pm via GitHub
    • Students should work on Part A before, during, and after your assigned lab section during the week of 10/6
    • Pre-lab survey on Canvas is (roughly) due by end of lab section during the week of 10/6
  • Part B: Multi-Note and Music Player

    • Due 10/26 @ 11:59pm via GitHub
    • Plan to work on Part B after fall break and during the week of 10/20
  • Part C: FPGA Prototype v1

    • Due week of 10/20 during assigned lab section
    • This part will focus on prototyping the code developed in Part A
    • Even though completed with a partner, every student must turn in their own paper check-off sheet in their lab section!
  • Part D: FPGA Prototype v2

    • Due week of 10/27 during assigned lab section
    • This part will focus on prototyping the code developed in Part B
    • Even though completed with a partner, every student must turn in their own paper check-off sheet in their lab section!
  • Part E: Report

    • Due week of 10/27, three days after lab section @ 11:59pm via Canvas
    • Post-lab survey on Canvas is due at the same time as the report

All parts of Lab 3 must be done with a partner. You can confirm your partner on Canvas (Click on People, then Groups, then search for your name to find your lab group).

Both students must contribute to all parts!

It is not acceptable for one student to exclusively work on the code while the other student exclusively works on the report. It is not acceptable for one student to exclusively work on hardware design while the other student exclusively works on testing. Both students must contribute to all parts. Student understanding of Verilog design and testing will be assessed on the prelim exams, final exam, and Verilog coding exam. The instructors will also survey the Git commit log on GitHub to confirm that both students are contributing equally. If you are using pair programming, then both students must take turns using their own account so both students have representative Git commits. Students should create commits after finishing each step of the lab, so their contribution is clear in the Git commit log. A student's whose contribution is limited as represented by the Git commit log will receive a significant deduction to their lab score.

This handout assumes that you have read and understand the course tutorials and that you have attended the discussion sections. This handout assumed you have successfully completed Part A. You should have already cloned your individual remote repository, so use git pull to ensure you have any recent updates before working on your lab assignment.

% cd ${HOME}/ece2300/groupXX
% git pull
% tree

where XX should be replaced with your group number. Students must try to get Part B working before their lab section the week of 10/27. If you have to finish Part B at the beginning of your lab section the week of 10/27 then you will likely not be able to finish Part C.

The following table shows all of the hardware modules you will be developing in Lab 3.

1. Interface and Implementation Specification

In Part B, you will need to implement a few more components before using all of the components from Part A plus your new components to implement a multi-note player and then the complete music player. This section describe the required interface (i.e., the ports for the module and the module's functional behavior) before describing the required implementation (i.e., what goes inside the module) for each hardware module.

1.1. Equality Comparator

We need an RTL 16-bit equality comparator to implement the music player. It should be quite straight-forward to implement this module with an assign statement or always_comb block.

1.2. Multiplexors

We need two new multiplexors to implement the multi-note player: a 2-to-1 3-bit RTL multiplexor and a 8-to-1 1-bit multiplexor. Implement both of these multiplexors while carefully avoding both inferred latches and X-optimism.

1.3. Multi-Note Player

The multi-note player can produce one of seven notes or a rest for a specified duration by composing a counter, mux, register, and seven note players.

We will only be implementing an RTL version of the multi-note player, and so we will use the RTL version of each of these hardware modules. This is an example of structural RTL, since it does not directly include any always_comb blocks and instead just instantiates other RTL modules.

The clock and reset signals are not shown in the above block diagram. The note duration and periods are provided as input ports and are assumed to never change after reset. Think carefully about how you should set the inputs to the counter not shown above to appropriate constant values. note duration and periods are provided as an input ports and are assumed to never change after reset. You will not be using the state output of the note players so you can mark those as unused. Think carefully about how you should set the inputs to the counter not shown above to appropriate constant values.

Instead of implementing a Register_3b_RTL just use your Register_16b_RTL to store the note number. You should zero-extend the input from 3-bits to 16-bits and then slice off the bottom three bits of the output. You need to mark the top 13-bits of the output as unused.

The multi-note player uses a latency insensitive valid/ready interface. A val/rdy interface uses the following micro-protocol:

Valid/Ready Micro-Protocol

Assume we have a producer that wishes to send a message to a consumer using the valid/ready micro-protocol. At the beginning of the cycle, the producer determines if it has a new message to send to the consumer. If so, it sets the message bits appropriately and then sets the valid signal high. Also at the beginning of the cycle, the consumer determines if it is able to accept a new message from the producer. If so, it sets the ready signal high. At the end of the cycle, the producer and consumer can independently AND the valid and ready signals together; if both signals are true then the message is considered to have been sent from the producer to the consumer and both sides can update their internal state appropriately. Otherwise, we will try again on the next cycle. To avoid long combinational paths and/or combinational loops, we should avoid making the valid signal depend on the ready signal or the ready signal depend on the valid signal. If you absolutely must, you can make the ready signal depend on the valid signal but it is considered very bad practice to make the valid signal depend on the ready signal. As long as you adhere to this valid/ready micro-protocol, composing modules via the stream interfaces should not cause significant timing issues.

So to play a note using the multi-note player, set play_note_num to the number of the note you want to play (i.e., which of the seven note period inputs to use) and set play_note_val to one. If on that same cycle play_note_rdy is one, then the multi-note player will start playing the corresponding note on the next cycle. If on that same cycle play_note_rdy is zero, then the multi-note player is busy and so we will need to try again on the next cycle. The desired note is produced on the note output and when the duration is done the play_note_rdy output is set high. The note_sel output is provided for debugging. Note that when the note number is zero, then the multi-note player should produce a rest (i.e., the note should stay low).

1.4. Music Player

The music player plays a song stored in memory. We will compose the music player with a memory as shown below.

The note duration and periods are provided as an input ports and are assumed to never change after reset. The music player uses a latency insensitive valid/ready interface. To play a song using the music player, set play_song_num to the number of the song you want to play and set play_song_val to one. If on that same cycle play_song_rdy is one, then the music player will start playing the corresponding song on the next cycle. If on that same cycle play_song_rdy is zero, then the music player is busy and so we will need to try again on the next cycle. The song is produced as a series of notes on the note output and when the song is done the play_song_rdy output is set high. The note_sel output is provided for debugging.

Songs are stored in memory as a sequence of notes. The memory is arranged as 64K 4-byte words. The memory interface has a memory request valid port (mem_val) and a memory address port (mem_addr). The memory address is a byte address. To read a value from memory, we need to set the memory address to the desired byte address and set the memory valid signal high. The data will be returned combinationally (i.e., on that same cycle) on the mem_rdata port.

Our music payer can play songs with up to seven notes and a rest. Each note is stored in one 4-byte word using the following encoding:

 NOTE      Encoding
------------------------
 REST      32'h00000000
 NOTE_G    32'h00000001
 NOTE_A    32'h00000002
 NOTE_B    32'h00000003
 NOTE_C    32'h00000004
 NOTE_D    32'h00000005
 NOTE_E    32'h00000006
 NOTE_F    32'h00000007
 SONG_END  32'h0000ffff

Each song can have a maximum of 128 notes. The value 32'h0000_FFFF is used to indicate the end of a song. The music player can play up to 32 songs; so the music player will use the following amount of memory:

\[32 \text{ songs} \cdot \frac{128 \text{ notes}}{\text{song}} \cdot \frac{4B}{\text{note}} = 16KB\]

The song memory map (i.e., how the songs are arranged in memory) looks like this:

 address song note
-------------------
 0x0000  0    0    start of song 0
 0x0004  0    1
 0x0008  0    2
 ...
 0x01fc  0    127  last possible note of song 0
 0x0200  1    0    start of song 1
 0x0204  1    3
 0x0208  1    2
 ...
 0x03fc  1    127  last possible note of song 1
 0x0400  2    0    start of song 2
 ...

We will divide the implementation of the music player into two parts: the control unit will include a finite-state machine implemented in RTL and the datapath will compose the RTL versions of a counter, an equality comparator, and the multi-note player as shown below.

The clock and reset signals are not shown in the above block diagram. The music player datapath should be implemented using structural RTL (i.e., instantating an RTL counter, equality comparator, and multi-note player0).

  • Address Counter: The address counter is responsible for generating memory addresses starting from the first note of the song. Remember each note is stored in one 4-byte word and all addresses are byte addresses, so the address counter will need to increment the address by four each cycle. The control unit is in charge of starting and enabling the counter. Think carefully about how you should set the inputs to the counter not shown above to appropriate constant values.

  • Equality Comparator: The equality comparator is responsible for detecting the end of the song. It compares the bottom 16 bits of the data returned from memory with the special end song value (i.e., 16'hFFFF)

  • Multi-Note Player: The multi-note player is exactly what we developed earlier in the lab. The control unit is in charge of when to play a note through the play_note_val and play_note_rdy signals while what note to play is determined by the value returned from memory.

The music player control unit should be implemented using the following four-state Mealy FSM.

The music player control unit should be implemented using flat RTL (i.e., a Register_16b_RTL with many unused bits, an always_comb block for the next-state combinational logic, and an always_comb block for the output combinational logic.

The FSM must use the following state encoding:

 State      Encoding
---------------------
 RESET      2'b00
 IDLE       2'b01
 PLAY_NOTE  2'b10
 WAIT_NOTE  2'b11

Only the next state logic is shown in the FSM diagram. You will need to determine the appropriate output logic from the following specification. In the IDLE state, the FSM should initialize the memory address counter to the desired song using addr_counter_load and adder_counter_start. The control unit should then wait until play_song_val is one and then move into the PLAY_NOTE state. In the PLAY_NOTE state, the FSM should read the next note from memory by setting mem_val high. Since the memory is combinational, the PLAY_NOTE state should also handle the memory response. If end_song is true then the note read from memory is END_SONG, and the FSM should return to the IDLE state. Otherwise, the FSM should set the control signals for the multi-note player appropriately, increment the memory address counter, and move into the WAIT_NOTE state. In the WAIT_NOTE state, the FSM should wait until the multi-note player is done before moving back into the PLAY_NOTE state to read the next note from memory.

The control unit will also need to turn the song number into a starting memory address. From the above memory map, we can see that the starting memory address for a song is just:

  { 2'b0, play_song_num, 9'b0 }

2. Testing Strategy

You should copy your tests from EqComparator_16b_GL-test.v into EqComparator_16b_RTL-test.v to verify the RTL equality comparator.

Because we are releasing this part later than we had hoped, we are providing you tests for the multiplexors, multi-note player, music player, and clock divider. We cannot guarantee that if you pass our tests it means your design is perfectly correct, but at least it should give us some confidence that your design is hopefully mostly correct.

2.1. Interactive Simulators

We have provided you two new interactive simulators which will emulate the FPGA prototype you will be demoing in the lab. After finishing implementing and thoroughly testing your multi-note player, you can build and run the simulator like this:

% cd ${HOME}/ece2300/groupXX/build
% make multi-note-player-sim
% ./multi-note-player-sim +switches=000

The switches are connected to the note number. Here is a table showing the mapping from note number to actual notes.

note period freq
num  (ms)   (Hz)   note
1    5.12   195.31 G3
2    4.55   219.95 A3
3    4.06   246.61 B3
4    3.81   262.52 C4
5    3.40   294.15 D4
6    3.03   329.92 E4
7    2.87   348.77 F4

The simulator will generate a VCD file that you can then open using Surfer to look at the note output waveform. You can measure the period between rising edges of the note output to verify that the note player is indeed generating the note at the proper frequency. For example, this is what the output looks like when setting the switches to 001. The period is close to 5.12ms which is 195Hz or the note G3. Try other notes!

After finishing implementing and thoroughly testing your music player, you can build and run the simulator like this:

% cd ${HOME}/ece2300/groupXX/build
% make music-player-sim
% ./music-player-sim +switches=000

The switches are connected to the song number. We currently have two songs you can play using your music player. Here is what the output looks like for the first few notes of song 0.

You can see that the song starts with the notes 3, 2, 1, 2, 3, 3, 3, rest. The simulator uses a very short note duration. We will use a much longer and more realistic note duration in the actual FPGA prototype. You will figure out which song this is in Part D of the lab.

3. Lab Code Submission

To submit your code you simply push your code to GitHub. You can push your code as many times as you like before the deadline. Students are responsible for going to the GitHub website for their repository, browsing the source code, and confirming the code on GitHub is the code they want to submit. Be sure to verify your code is passing your tests both on ecelinux and on GitHub Actions. Your design code will be assessed both in terms of code quality, verification quality, and functionality.

3.1. Part A Revisions

If your design is failing some tests from Part A, then you should fix whatever is wrong. If your design is passing your tests but failing our tests you should browse the staff tests to see how to improve your own testing. Fixing whatever is wrong now can improve your code functionality score and also ensures that you can use your work from this lab in future labs.

To revise your submission, simply push any updates to your tests and/or your hardware designs to GitHub just like normal. You do not need to switch branches; just push your changes to the main branch like normal. Make sure your updates are passing all of your (potentially updated) tests on the main branch on GitHub Actions. After pushing your updated code, you must enter a comment on the grading pull request page that explains what was wrong and how you fixed it. The instructors will then take care of merging your changes into the grading pull request. You should not rely on the instructor merging your revision into the pull request to see if your code passes the staff tests; you should copy whatever staff tests are important into your own test benches. The only way we can increase your score after a revision is if you clearly explain what was wrong and how you fixed it. If you fix your code on the main branch and do not explain it in the grading pull request then you will not receive a revised score!

All Part A revisions need to be finalized by the due date for Part B. No revisions are allowed for Part B code.

3.2. Code and Verification Quality

Code and verification quality will be assessed in the same way as for Part A.

3.3. Functionality

Your functionality score will be determined by running your code against a series of tests developed by the instructors to test its correctness. Note that we will be using the automated build system to test your final code submission as shown below.

% mkdir -p ${HOME}/ece2300
% cd ${HOME}/ece2300
% git clone git@github.com:cornell-ece2300/groupXX
% cd groupXX

% mkdir -p build
% cd build
% ../configure
% make check-lab3-partA
% make check-lab3-partB