Kai Hirota

Project4 min read

Networked music sequencer in ARM assembly

Bare-metal Cortex-M7 assembly projects for MIDI-note playback and a three-wire music command protocol.

Two bare-metal assembly projects from COMP6300 Computer Organisation and Program Execution at the Australian National University. The first project plays music directly from MIDI note data. The second turns that into a networked instrument: one Discoboard sends MIDI-like note commands over physical wires, and another receives, validates, and plays them.

Source repository · Project 2 design document · Project 3 design document

Hardware Context#

The projects ran on the Discoboard MCU used in the course, built around an Arm Cortex-M7 processor. The code is almost entirely assembly, with direct interaction with GPIO, timers, external interrupt lines, the floating point unit, and audio output routines.

That constraint is what made the project interesting. Playing a note is not a call to a sound library; it is a timing problem. Sending a command is not a socket write; it is voltage on a pin, sampled on an interrupt edge.

Project 2: Musical Sequencer#

The first project reads a sequence of MIDI note numbers and durations, converts each note into a frequency, and generates a square wave at the right interval.

MIDI note-to-frequency conversion uses the standard equal-temperament formula:

f=2(d69)/12×440f = 2^{(d - 69) / 12} \times 440

where dd is the MIDI note number and A4A4 is note 69 at 440 Hz440\text{ Hz}.

The awkward part is the fractional exponent. ARM assembly does not give you a convenient instruction for 2m/122^{m / 12}, so the implementation rewrites the expression using the twelfth root of two:

2(d69)/12=(212)d692^{(d - 69) / 12} = \left(\sqrt[12]{2}\right)^{d - 69}

That changes the problem from "compute a fractional exponent" into "raise a precomputed constant to an integer exponent." The code approximates 212\sqrt[12]{2} as:

2121+297350000\sqrt[12]{2} \approx 1 + \frac{2973}{50000}

and uses the Cortex-M7 floating point registers to preserve enough precision before converting back to an integer frequency scaled by 100.

Timing the Sound#

Once the note is converted, the sequencer computes three values:

ValuePurpose
wavelengthCPU-cycle interval between waveform transitions
on_lengthDuty-cycle-controlled high portion of the square wave
cutoffDuration of the note, expressed in CPU cycles

The execution shape is deliberately simple:

main
  -> play_music
    -> play_note
      -> convert_midi_to_frequency
      -> initialize_play_sound
        -> play_sound
          -> sound_on / sound_off loop

The design is primitive in the useful embedded-systems sense: the clock, loops, and counters are the timing model. If a note needs to play for 0.150.15 seconds, that duration is converted into loop iterations against the board's known execution rate.

Project 3: Networked Sequencer#

The second project adds a physical communication protocol. A sender board transmits fixed-width MIDI-like messages, and the receiver board reconstructs them one bit at a time.

The protocol uses three lines:

LineSender pinReceiver pinRole
ControlPE12PH0Marks the start and end of a message
ClockPE13PH1Tells the receiver when to sample the data line
DataPD0PE14Carries the next payload bit

The message format is a fixed 96-bit packet:

32-bit command
32-bit MIDI note number
32-bit velocity

Only two commands are handled: note on and note off. The receiver turns a valid note-on message into a frequency and amplitude, then updates the audio waveform.

Protocol Flow#

The sender raises the control line, writes each data bit, pulses the clock line, and clears the control line when the packet is complete.

Sender MCU                         Receiver MCU
 
control high  -------------------> enable clock interrupt
data = next bit ------------------> wait
clock pulse   -------------------> sample data pin
repeat 96 times
control low   -------------------> validate + execute message

On the receiver, the control line is handled by EXTI0_IRQHandler, while the clock line is handled by EXTI1_IRQHandler. When the control line rises, the receiver enables the clock-line interrupt. Each clock edge samples the data line and shifts the bit into the current command buffer. When the control line falls, the receiver checks that exactly 96 bits were received before executing the command.

Invalid packets are discarded and leave the red LED on, which makes transmission errors visible during testing.

Interrupt Priorities#

The sender uses the TIM7 timer to schedule note messages. That timer controls the interval between transmissions, so each note can have a duration with a precision limit of roughly 0.010.01 seconds.

The receiver's clock-line interrupt has to run immediately when the sender pulses the clock line. The project therefore gives the clock-line interrupt higher priority than the timer interrupt. Otherwise the receiver could miss a bit while doing unrelated timing work.

This is the part of the project that felt most like real embedded programming: the correctness of the protocol depends on interrupt ordering, not just on data structures.

MIDI Parameters#

The networked version also converts velocity into amplitude. Since MIDI velocity is a value from 0 to 127, the project maps it linearly onto the maximum audio amplitude:

a=(amax127)va = \left(\frac{a_{\max}}{127}\right) v

where amaxa_{\max} is 0x7FFF and vv is the received velocity.

The frequency path reuses the same MIDI note conversion as the sequencer, so the receiver can interpret a command like "note on, D4, velocity 127" without the sender transmitting any precomputed audio parameters.

Reflection#

The clearest improvement is packet size. The project sends three 32-bit words even though MIDI commands, note numbers, and velocities are naturally 8-bit values. A tighter protocol could pack the whole message into 24 or 32 bits:

8-bit command | 8-bit note | 8-bit velocity | optional checksum/parity

That would reduce transmission time and lower the chance of bit errors. I would also add explicit parity or a checksum instead of only checking packet length, and I would make the command parser support more of MIDI rather than just note on and note off.

Still, the project was a useful low-level exercise: it connected musical representation, CPU-cycle timing, floating point conversion, GPIO signalling, and interrupt-driven communication in one small system.