Lab 7: Play a Song

In this lab, you will wire a speaker to the PIC32 and play a song. Along the way, you will write functions in assembly code, work with arrays stored in memory, and use instructions for multiplication and division.

Groups

Work on this lab in the following groups:

  • Tanner, Maddie, and Blake
  • Devin and Hattie
  • Jerry and Theo
  • Adam W. and Linda
  • Kamal and Matt
  • Fengyuan and Ryan
  • Eli and Giang
  • Bea and Adam H.
  • Charlie and Hamza
  • Sophie and Ana
  • Jacob and Sara

Running MPLAB X

I was able to fix the issue with MPLAB X complaining about your project names, but you will need to run it from a different location. To start the IDE, run the following command:

$ /home/curtsinger/bin/mplab_ide &

Briefly, the issue was that the IDE was creating a temporary directory owned by the current user, which it later tries to reuse when you log back in. However, you do not have write permissions for this directory, so you get an unhelpful error message. Restarting fixed the issue because the OS removes temporary directories on each reboot. My updated launcher just uses a default temporary directory which should be writable by all users.

I have not been able to track down the source of the connection issues yet, but I will keep trying.

Part A: Debugging an arithmetic function

Recall that our delayloop function from Lab 6 takes as its parameter a number of loop iterations to execute. As a first step to playing musical notes, we’d like to be able to convert units of time into delay loop iterations. For example, consider the following function in C:

int time_to_iter(int microsec) {
    return (microsec * CLOCK_RATE) / (MICROSEC_PER_SEC * IC * CPI);
}

I translated this C code into assembly. To test my implementation, I wrote a simple program that calls time_to_iter and delayloop to blink the onboard LED at a rate of 60 Hz. To test my program, I simultaneously pushed the PIC32 reset button and started a 60 second timer. I verified that the blinking stopped after the 60 second timer was complete.

Here is my test program:

# timetest.S
# Test harness for the time_to_iter function
# Written by Janet Davis, 23 October 2013
# Last revised by Janet Davis, 24 October 2013

#define OFF                 0x0
#define ON                  0x1
#define CLOCK_RATE          4000000 // Cycles per second for this CPU
#define CPI                 1       // Cycles per instruction
#define IC                  5       // Instructions per delay loop
#define MICROSEC_PER_SEC    1000000 // Microseconds per second

    .set noreorder          # Avoid reordering instructions
    .text                   # Start generating instructions
    .globl main             # The label should be globally known
    .ent main               # The label marks an entry point

# Compute number of delay loops in a given period of time 
# (measured in microseconds)
time_to_iter: 
   # $t0 = time * CLOCK_RATE
   li       $t2, CLOCK_RATE     # Load CLOCK_RATE constant
   multu    $a0, $t2            # Multiply argument with CLOCK_RATE
   mflo     $t0                 # Copy result from lo to $t0

   # $t1 = MICROSEC_PER_SEC * IC * CPI
   li       $t2, MICROSEC_PER_SEC # Load MICROSEC_PER_SEC constant
   li       $t3, IC             # Load IC constant
   multu    $t2, $t3            # Multiply IC * MICROSEC_PER_SEC
   mflo     $t1                 # Copy result from lo to $t1
   li       $t4, CPI            # Load CPI constant
   multu    $t1, $t4            # Multiply previous result by CPI
   mflo     $t1                 # Copy result from lo to $t1

   # result = (time * CLOCK_RATE) / (MICROSEC_PER_SEC * IC * CPI)
   divu     $t0, $t1            # Divide $t0 by $t1
   mflo     $v0                 # Copy quotient from lo to return val reg

   jr       $ra                 # Return to caller
   nop

# Delay for the given number of loop iterations (5 cycles/iteration)
delayloop:
    beq     $a0, $zero, delayloopend
    nop
    addi    $a0, $a0, -1
    j delayloop
    nop
delayloopend:
    jr      $ra
    nop

# Main program.
# Test time_to_iter computation.
# The light should blink at a rate of 1 Hz.
main:
    # Set up port A for output
    la      $s0, TRISA          # Load the address mapped to TRISA 
    li      $t0, 0x0000         # Output on all pins
    sw      $t0, 0($s0)         # Write to TRISA
    la      $s0, LATA           # Load the address mapped to LATA 

    # Compute number of delayloop iterations for blinking at 60Hz
    li      $a0, 500000         # Half a second, in microsec
    jal     time_to_iter        # Call time_to_iter procedure.
    nop
    add     $s1, $v0, $zero     # Store result in $s1

    # Blink on and off 60 times; should take 60 seconds to finish blinking.
    li      $s2, 60             # Set countdown to 60.
loop:
    li      $t0, ON             # Turn LED on.
    sw      $t0, 0($s0)
    add     $a0, $s1, $zero     # Call delayloop on the the computed value.
    jal     delayloop
    nop
    li      $t0, OFF            # Turn LED off.
    sw      $t0, 0($s0)
    add     $a0, $s1, $zero     # Call delayloop on the computed value.
    jal     delayloop
    nop
    addi    $s2, -1             # Decrement countdown.
    bne     $s2, $zero, loop    # Loop while countdown > 0    
    nop
forever:
    j forever                   # Infinite loop that does nothing.
    nop
    .end main                   # Marks the end of the program

Unfortunately, I made a significant error when writing the time_to_iter function; the blinking doesn’t even start. It’s your job to identify and fix my bug.

  1. Create a new project. Copy and paste the code above into a new assembly file, timetest.S
  2. Run the program and verify that it does not work.
  3. Using whatever means to deem appropriate, identify the bug in this program.
  4. Revise the time_to_iter function so it works correctly.

Part B: Playing a song

Next, you will use your debugged time_to_iter function to play a song. The overall algorithm is:

  1. Read a note and duration from memory
  2. Call a function to play that note for the specified duration
  3. Move on to the next note and repeat until there are no more notes to play

Hardware setup

Get a couple of long wires, one red and one black. Connect the red wire to RA0 (pin 2) and the black wire to VSS (pin 8). Connect the other end of one wire to the top row of holes in the protoboard’s speaker connection, and the other wire to the bottom row. It does not matter which wire goes in which spot.

If you run the timetest.S program now, you will hear a series of quiet, dull clicks. This is because 1Hz is much too slow an oscillation for the human mind to perceive as a tone. If you change the time passed in to time_to_iter so it is much shorter, you will hear a tone.

Assembly code

Use this assembly program as a starting point for your work:

# song.S
# Plays a song
# Written by Janet Davis, 23 October 2013
# Last revised by YOUR NAME(S), THE DATE

#define OFF                 0x0
#define ON                  0x1
#define CLOCK_RATE          4000000 // Cycles per sec for this CPU
#define CPI                 1       // Cycles per instruction
#define IC                  5       // Instructions per loop
#define MICROSEC_PER_SEC    1000000 // Microseconds per second

# C-major scale
# Reference: http://en.wikipedia.org/wiki/Piano_key_frequencies
#define REST        0       // Use 0 to represent rests (no sound played)
#define C           3822    // Period in microseconds for middle C
#define CSHRP       3608    // Period in microseconds for middle C#
#define D           3405    // Period in microseconds for D above middle C
#define DSHRP       3214    // Period in microseconds for D# above middle C
#define E           3034    // Period in microseconds for E above middle C
#define F           2863    // Period in microseconds for F above middle C
#define FSHRP       2703    // Period in microseconds for F# above middle C
#define G           2551    // Period in microseconds for G above middle C
#define GSHRP       2408    // Period in microseconds for G# above middle C
#define A           2273    // Period in microseconds for A above middle C
#define Bee         2025    // Period in microseconds for B above middle C
#define CC          1911    // Period in microseconds for C above middle C
                            // (Note this is half of middle C!)
# Durations
#define WHOLE       2000000  // Whole note (microseconds)
#define DTHALF      1500000  // Dotted half note
#define HALF        1000000  // Half note
#define DTQUART      750000  // Dotted quarter note
#define QUARTER      500000  // Quartner note
#define EIGHTH       250000  // Eighth note
#define SIXTEENTH    125000  // Sixteenth note
#define BREATH        10000  // A breath between phrases

# The song (Rodgers & Hammerstein, 1959)
# notes and durations are arrays stored in memory.
# songlength, also stored in memory, gives the size of the arrays.
# Reference for data segment format: 
# http://www.cs.umd.edu/class/sum2003/cmsc311/Notes/Mips/dataseg.html
    .data
    songlength: .word   65
    notes:      .word   C,      REST,   D,      E,      REST, \
                        C,      E,      C,      E, \
                        REST, \
                        D,      E,      F,      F,      E,      D,      F, \
                        REST, \
                        E,      F,      G,      E,      G,      E,      G, \
                        REST, \
                        F,      G,      A,      A,      G,      F,      A, \
                        REST, \
                        G,      C,      D,      E,      F,      G,      A, \
                        REST, \
                        A,      D,      E,      FSHRP,  G,      A,      Bee, \
                        REST, \
                        Bee,    E,      FSHRP,  GSHRP,  A,      Bee,    CC, \
                        REST, \
                        F,      F,      A,      F,      Bee,    G,      CC


    durations:  .word   QUARTER,EIGHTH, EIGHTH, QUARTER,EIGHTH, \
                        EIGHTH, QUARTER,QUARTER,HALF, \
                        BREATH, \
                        DTQUART,EIGHTH, EIGHTH, EIGHTH, EIGHTH, EIGHTH, WHOLE, \
                        BREATH, \
                        DTQUART,EIGHTH, DTQUART,EIGHTH, QUARTER,QUARTER,HALF,  \
                        BREATH, \
                        DTQUART,EIGHTH, EIGHTH, EIGHTH, EIGHTH, EIGHTH, WHOLE, \
                        BREATH, \
                        DTQUART,EIGHTH, EIGHTH, EIGHTH, EIGHTH, EIGHTH, WHOLE, \
                        BREATH, \
                        DTQUART,EIGHTH, EIGHTH, EIGHTH, EIGHTH, EIGHTH, WHOLE, \
                        BREATH, \
                        DTQUART,EIGHTH, EIGHTH, EIGHTH, EIGHTH, EIGHTH, DTHALF,\
                        BREATH, \
                        EIGHTH, EIGHTH, QUARTER,QUARTER,QUARTER,QUARTER,DTHALF

# The program starts here
    .set noreorder          # Avoid reordering instructions
    .text                   # Start generating instructions
    .globl main             # The label should be globally known
    .ent main               # The label marks an entry point

# Play the specified note for the specified length of time
# $a0: period in microsections for the note (0 specifies a rest)
# $a1: duration in microseconds to play the note
playnote:
    # TODO: YOUR CODE HERE

# Compute number of delay loops in a given period of time
# (measured in microseconds)
time_to_iter:
    # TODO: YOUR CODE HERE

# Delay for the given number of loop iterations (5 cycles/iteration)
delayloop:
    beq     $a0, $zero, delayloopend
    nop
    addi    $a0, $a0, -1
    j delayloop
    nop
delayloopend:
    jr      $ra
    nop

# Main program
# Plays the first few notes of the song stored in memory.
# TODO: PLAY THE ENTIRE SONG
main:
    # Set port A for output.
    la      $s0, TRISA          # Load the address mapped to TRISA 
    li      $t0, 0x0000         # Output on all pins
    sw      $t0, 0($s0)         # Write to TRISA

    # Pause for a moment so that the song does not begin playing while
    # MPLAB is programming the microprocessor.
    li      $a0, REST
    li      $a1, WHOLE
    jal     playnote
    nop

    # Load data addresses.
    la      $s5, songlength
    la      $s6, notes     
    la      $s7, durations

    # Play the first five notes/rests of the song.
    # TODO: CHANGE THIS TO A LOOP 
    lw      $a0, 0($s6)         # Play the first note or rest.
    lw      $a1, 0($s7)
    jal     playnote
    nop
    lw      $a0, 4($s6)         # Play the second note or rest.
    lw      $a1, 4($s7)
    jal     playnote
    nop
    lw      $a0, 8($s6)         # Play the third note or rest.
    lw      $a1, 8($s7)
    jal     playnote
    nop
    lw      $a0, 12($s6)        # Play the fourth note or rest.
    lw      $a1, 12($s7)
    jal     playnote
    nop
    lw      $a0, 16($s6)        # Play the fifth note or rest.
    lw      $a1, 16($s7)
    jal     playnote
    nop
forever:
    j forever                   # Infinite loop that does nothing.
    nop
    .end main                   # Marks the end of the program

Step 1: Setup

Create a new project. Copy and paste the code above into a new assembly file song.S. Fill in the time_to_iter function with your corrected implementation.

Step 2: Playing a note

Implement the playnote function. To help you get started, here is a C function that does the same thing:

/*
 * Play a musical note for a specified duration.
 * period:   
 *    The period of the note (1/frequency), in microseconds.
 *    0 indicates a rest (silence).
 * duration: 
 *    The duration to play the note, in microseconds.
 * Preconditions:  
 *    period >= 0, duration >= 0
 *    Port A, pin 0 is configured for output, and is connected to a speaker.
 *    Port A, pin 0 is off (0).
 * Postconditions: 
 *    The note (or rest) has been played.
 *    Port A, pin 0 is off (0).
 */
void playnote(int period, int duration) {
    int delay, count;
    if (period == 0) {
        delay = time_to_iter(duration);
        delayloop(delay);
    } else {
        count = duration/period;
        delay = time_to_iter(period/2);
        while (count > 0) {
            LATA = ON;
            delayloop(delay);
            LATA = OFF;
            delayloop(delay);
            count--;
        }
    }
}

Test your program. You should hear the first few notes of a song.

Step 3: Looping over notes

Revise the body of the main program so it will play the entire song instead of just the first few notes. It may be helpful to write the loop in C before translating it to assembly.

Step 4: A new song (optional)

If you have time, revise the data segment to play a different song.

Acknowledgements

This lab was developed by Janet Davis for CSC 211L in 2013. Parts of the lab were inspired by exercises written by Marge Coahran.

license This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 United States License.