CS150 Spring 2000

Project

Midi Synthesizer

Gek Siong Low 13364170

Viven Tong 13318388

Lab 17


Project Functional Description

The task is to design a simple, no-frills, single-instrument, single channel MIDI synthesizer that takes in MIDI commands sent from a MIDI keyboard through a MIDI cable to the Xilinx board, processes the information and generates sound through the headphones jack on the board. The basic operation is that single notes played at the keyboard results in sound of the correct pitch , tone and volume to be output to the speakers or headphones. Sound output will be in mono only. Simultaneous multiple notes (polyphony), pitch bends, instrument changes and other effects found in commercial synthesizers will also not be implemented.

Design Requirements

 

1. Clock speeds and sampling rates

The MIDI clock rate is 31.25 kHz. This is the rate at which bits arrive serially from the keyboard. This is also the rate at which sound is output. The rest of the system should run at either 8 MHz or 4 MHz (that is, as fast as is possible).

2. Sound generation

The instrument data is stored in a waveform template stored on an EPROM. Sound of the appropriate pitch is generated by looking up the 16-bit sample data in the EPROM (more on that later) and sending them to the digital-to-analog converter.

3. MIDI command set

Only the note-on and note-off MIDI events will be handled. Channel information in these commands are ignored making this a single-channel synthesizer. One specific requirement is that a note-on message with zero-velocity is to be treated as a note-off message. It is also important that the implementation deals with “real-time?messages which can be inserted anywhere within the byte-stream.

4. Keyboard behavior

Pressing a key will cause the attack section of the instrument to be played, and the sustain portion will be played repeatedly as long as the key is held down. Releasing the key will play the release section. The implementation must be able to deal with pressing multiple keys and releasing multiple keys just like a normal keyboard even though only single notes will be played. It must also be able to handle instruments with no sustain sections but valid attack and release sections (e.g. the harpsichord) correctly.

5. Dealing with velocity

Hitting the key hard should produce a loud sound and hitting it softly should produce a soft sound, just a real keyboard. Velocity is to be ignored when releasing a key.


Theory of Operation

 

1. Receiving the MIDI commands

The serial bit-stream from the MIDI keyboard is over-sampled at 8x the MIDI clock rate (8 x 31.25 = 250 kHz), so that we can recognize the middle of the asynchronous pulses to reliably sample the bits. The bits will be clocked into an 8-bit shift register and a “data-ready?signal pulsed when we receive an entire byte.

2. Recognizing the MIDI commands

Recognizing the MIDI commands is basically a simple finite-state machine that reads in the 3 bytes of the note-on and note-off messages into registers, while ignoring “real-time?messages and resetting itself on all other messages. A “note-on?or “note-off?signal will be pulsed when a complete message is received and recognized.

3. Looking up the samples

This is the most complex part of the system. This is the basic algorithm (implementation and timing details to be discussed later):

On a “note-on?signal:

Use key value to retrieve template starting address and step size into registers

Use template start address to retrieve end-of-attack, end-of-sustain, end-of-release

Current address = template start address + 10 bytes

While not “note-off?(of currently-playing note) nor “note-on?{

          ?Read in 16-bit sample at current address (after word-aligning)

          ?Send “data-ready? signal to parallel-to-serial converter

          ?Next address = current address + step size

          ?If (next address > end-of-sustain)

          ?          ?If (sustain section exists)

                                  ?Next address = next address ?end-of-attack + end-of-sustain

                      ?Else

                                  ? Freeze next address and current address until we break out

          ?Current address = next address

}

If “note-on?restart the procedure for the new note, else if “note-off?we proceed

While not “note-on?nor (next address > end-of-release) {

          ?Read in 16-bit sample at current address (after word-aligning)

          ?Send “data-ready? signal to parallel-to-serial converter

          ?Next address = current address + step size

}

If “note-on?restart the procedure, else we wait for a “note-on?signal


4. Sound generation

The 16-bits samples must be sent to the DAC at a rate of 31.25 kHz. The samples arrive in parallel but the DAC takes in only serial input, so a parallel-to-serial converter is needed. The basic operation is that we load the 16 bits into a left shift register (MSB first) on a “data-ready?and shift them out at 8 MHz. We use a counter to start and stop the shifting and also to produce the “load-down? signal required by the DAC.

5. Velocity control

The choice of implementation is using right-shifts via a table lookup based on the top 4 bits of velocity to reduce the sample amplitude. This function is easily incorporated into the parallel-to-serial converter design. Depending on the amount of the right shift required, we simply start the shift register at a later count, thereby sending the sign bit to the DAC until we start shifting in the data. The lookup table is implemented using 3 4-input ROMs.


High-level Organization

The system is divided into 4 major components:

1. UART

Converts the asynchronous bit-stream from the keyboard into bytes.

Clock: 250 kHz

2. MIDIPARSER

Parses the stream of bytes from the UART and recognizes the note-on and note-off messages.

Clock: 4 MHz

3. NOTEMANAGER

Handles the template lookup and output samples at constant rate of 31.25 kHz.

Clock: 4 MHz and 8 MHz

4. PARALLEL-TO-SERIAL CONVERTER (P2S_CONV) + DAC + AMPLIFER

Converts 16-bit parallel samples into serial output to the DAC, which outputs sound to the amplifier.

Clock: 8MHz

Block diagram


Detailed Description


UART

We basically implemented Nick Weaver’s “ultimate UART design? A counter is started when the serial input goes low and the bits sampled and shifted into a 8-bit shift register in the middle of every eight clock cycles (8x over-sampling). Once we have all eight bits, the UART sends a “data-ready?pulse with the sampled 8 bits.

Midi Parser
 

Finite-State Machine

The midi parser is simple enough to be implemented directly from the finite-state machine. Each “data-ready?signal from the UART is debounced to a 4 MHz pulse and used to transit to the next state. The registers are enabled one cycle before going into the corresponding state, so that the data is clocked in at the same time as when entering the state. A bunch of combinational logic is used to detect the type of messages received and to determine the output. Finally the outputs are debounced to 4 MHz pulses.

Note Manager

 

Finite-State Machine

On a new note-on message, the note manager will stop whatever it is currently playing and proceed immediately to look up the directory template based on the key value to get the template starting address and the step-size. Using the template starting address, it then retrieves the end of attack/sustain/release addresses, after which it starts to play the note, going through the attack and sustain sections and looping the sustain section once it goes past the end of sustain address. A note-off for the currently playing note will cause the note manager to continue playing from whichever address it is at until it reaches the end of release address. We need to ensure that the very last sample is played before the playing stops, so the “Finish Play?has to wait for a DRDY signal before returning to the “Wait?state.


Data Path

A filter is placed before the state machine to filter out note-off messages that is not for the current note. The current key is clocked into a register for this comparison. The current velocity is also clocked into a register for use by the parallel-to-serial converter because a note-off message will destroy the current velocity value.

All the address lookup is done and addresses stored into registers before the playing starts. The special case of zero sustain is also detected during template lookup and the result stored into a 1-bit flip-flop for later use. The registers are clocked at 8 MHz because it takes 2 cycles for each memory reference ?one cycle for the address to be loaded into the current address register, and one cycle for the EPROM to respond with the correct data. Thus the data path is clocked at 4 MHz.

When playing the samples, we begin a new 31.25 kHz clock using a clock divider from 4 MHz on every note-on. “Data-ready?pulses are sent to the parallel-to-serial converter at every positive edge of the 31.25 kHz clock. The samples are read in at every negative edge of the 31.25 kHz clock so as not to clash with the timing of the “data-ready?signals, thereby maintaining a constant 31.25 kHz rate of sample output The next sample address and any loop-back for sustain is performed after the “data-ready?pulse is sent. Sustain loop-back is performed with a simple two-state machine ?one cycle for subtract and one cycle for add.

Use of Counters

One major characteristic of our design for the note manager is the extensive use of a shared counter for almost every function. Using a shared counter is easy to visualize and extremely flexible. An action is associated with each specific state and count. Thus changes in the actions to perform can be made very easily. The counter is reset prior to entering a new state. For playing the samples, the counter is reset on every negative edge of the 31.25 kHz internal MIDI clock.

The counts and corresponding actions are shown below:

COUNTER COUNT

DIR_LOOKUP STATE
TEMPLATE_LOOKUP STATE

PLAY STATES (PLAY_ATTACK_AND_SUSTAIN AND FINISH_PLAY STATE)

0

Clock in key x 4 into next addr

Clock in template_start addr into next addr

Clock in 1st byte of sample

1

Clock in 1st byte of directory entry

Clock in 1st byte of end_of_attack addr

Clock in 2nd byte of sample

2

Clock in 2nd byte of directory entry

Clock in 2nd byte of end_of_attack addr

-

3

Clock in 3rd byte of directory entry

Clock in 3rd byte of end_of_attack addr

-

4

Clock in 4th byte of directory entry and send “done?signal

Clock in 1st byte of end_of_sustain addr

-

5

-

Clock in 2nd byte of end_of_sustain addr

-

6

-

Clock in 3rd byte of end_of_sustain addr

-

7

-

Clock in 1st byte of end_of_release addr

-

8

-

Clock in 2nd byte of end_of_release addr

-

9

-

Clock in 3rd byte of end_of_release addr

-

10

-

Clock one more cycle to leave start_of_attack addr in the next addr register, and send “done?signal

-


Parallel-to-Serial Converter and DAC

This is an asynchronous module, not tied to any particular 31.25 kHz clock. The note manager is responsible for producing the “data-ready?pulses at the constant rate. This implementation eliminates any potential synchronization problems that may arise between the note manager and the parallel-to-serial converter.

The input clock is inverted for the parallel-to-serial converter so everything is performed at the negative edge of the 8 MHz clock. This ensures that the serial output is not changing when the DAC clocks in the bits at every positive edge of the 8 MHz clock.

On a “data-ready?signal, the sample is loaded into the shift register and the counter is started. When the counter reaches 15 (the 16th bit), everything is frozen and it lowers the load line to the DAC one cycle later.

For handling the key velocity, three 16x1 ROMs are used for the lookup tables and the shift register disabled for the appropriate number of cycles. While the shift register is disabled, the sign bit is clocked into the DAC.


Status and Results

All requirements have been met. There are no known bugs or errors.

Extra credit: None

MIDI PARSER

Requirement

Status

Handles “real-time?messages

OK

Recognizes zero-velocity note-on as note-off

OK

 

NOTEMANAGER

Requirement

Status

Ignores “note-off?if it does not correspond to current note that is playing

OK

Handles attack, sustain, release correctly (no clicks in output)

OK

Handles zero sustain correctly

OK

“Note-on?received while playing attack

OK

“Note-on?received while playing sustain

OK

“Note-on?received while playing release

OK

“Note-off?received before getting to sustain section

OK

Correct pitch is played

OK

Pitch remains constant for attack, sustain and release sections

OK

No noise for C notes

OK

No “off-by-one?errors when playing samples

OK

 

PARALLEL-TO-SERIAL CONVERTER

Requirement

Status

Correct shift-table lookup and right-shifting (including sign bit)

OK

Able to hear different sound volumes

OK

Correct peak-to-peak voltage for loudest sounds (about 2V for most ROMS)

OK

No funny noises

OK


Division of Labor

We try to do everything together whenever possible. This way we ensure that both of us know what the design is, and is also a good way to double-check the logic. In fact, we can modify and streamline the design several times in one session, helping us to identify potential bugs early.

If we have to identify which parts each of us concentrated more on, it would probably be something like this:

Gek:

Finite-state machine design (reviews and corrections by Vivien)
Data path design
Simulation
Testing and debugging with oscilloscope

Vivien:

Wire wrapping (very neat)
Detailed timing considerations
Control and enable logic
Optimization


Conclusions

 

Things we have learned, and mistakes we have made:

1.   ?Hardware design is harder than software design.

2.   ?Debugging hardware is even worse.

3.   ?Too much hierarchy may not be good, especially when you have to split up the data path to go into different components. We ran out of wires the first time and had to redesign the entire note manager.

4.   ?When adding a BUFG clock buffer to an input clock, remember to name the other end of the wire.

5.   ?Remember to use clock buffers, and use the line after the buffer, not before.

6.   ?If the circuit behaves weird for no good reason, it’s probably a clock buffer problem.

7.   ?Simulation is your friend, but it might not give you the right timing diagrams. Sometimes it is still best to work out the timing by hand.

8.   ?Counters are easy to use and easy to debug. They are a good choice for linear state machines (one straight-line path), especially when you can use one counter for almost everything.

9.   ?No matter how careful we are to use a flexible design, changes in the specifications can result in big changes to the design. Luckily it only happened once to us.

10. ?Deleting huge chunks of logic can increase the CLB count, and adding just one gate can sometimes decrease the CLB count. Very intuitive?#060;/p>

11. ?Be careful when stepping over MIDI cables stretching across the lab!

Would we do things differently next time?

One thing we would do differently next time is that we would design simple and stupid in the earlier stages of the project, optimizing it only later. A simple design is easier to modify than one with fancy optimizations should the specifications change. Also a design with many levels of hierarchy may look elegant and modular (from a software engineer’s point of view) but it may have so many wires all over the place that the cost of directing the signals to the right places is too high. Next time, we would place everything that shares the same data path into the same component right from the start.