How to build a simple sound synthesizer using PURRR --------------------------------------------------- In this tutorial we will explore the CAT/PURRR system by building an application on the microcontroller. It will illustrate the basic flow of working and shows you how things fit together. The test application is a very simple audio synthesizer, which will give you some idea of making programs that deal with time. configuring the PIC ------------------- The default PURRR boot loader sets all pins that are not used to outputs. The reason for this is simple: leaving a pin 'floating', which means having it configured as input but not connected to anything, can cause the chip to go crazy, because it amplifies the slightest noise present on the input pins and uses is as input. | TIP: Floating pins cause problems. Each input should be connected | to the output of something else. The chip we will be using is the 18f1220 or 18f1320. These are basically the same except for program memory size. Download the data sheet at www.microchip.com At page 2 of this document (DS39605C) you'll find the pin diagram in the upper left corner. To drive the speaker, we'll have to choose an output pin. How to choose? Practically all pins are configurable as digital output. In additiona to that, they can be configured to do other things. Let's see what the acronyms mean in the data sheet RAx bit x of port A RBx bit x of port B ANx analog input x for A/D converter VREF+ positive voltage reference for A/D converter VREF- negative ... INTx external interrupt x TX (asynchronous serial port) transmit RX (asynchronous serial port) receive VSS 0 volts VDD 5 volts KBIx interrupt on change pin x MCLR reset pin The acronyms not listed can be ignored for the time being. So, what's necessary? TX/RX connection to host PC AN0-AN4 analog controller input (AN5=TX) KBI1-KBI3 external sync input (KBI0=RX) MCLR reset pin This leaves RA4, RA6, RA7, RB2 and RB3 free to use. Scratch RA4, since it does not have a CMOS output. The other 2 A ports are used for an external oscillator/chrystal, so we'll reserve them too. This leaves RB2 and RB3. These happen to be two PWM outputs, which is a happy coincidence. PWM or pulse width modulation is in fact what we will be using as one of the algorithms to generate sound. The hardware PWM generator in the PIC is rather limited for our purpose, which is why we'll do it in software. However, choosing one of these pins enables us to use the hardware PWM later without modifying the circuit. RB3/P1A speaker out / PWM This is pin 18 in of the 18 pin DIP package. | TIP: When allocating pins to do experiments, pick a pin with the | least amount of functionality. If you mess things up by blowing the | output or input amplifiers, you can probably still use the chip for | something else by taking another pin. building the circuit -------------------- We're going to keep it simple here. Just connect a small speaker (or the line input of an amplifier using an RCA or headphone connector) to one of the output pins of the PIC. PIN >----\ | /| [@] | speaker | \| | === 22uF capacitor | | V ground When connecting directly to a speaker, it is a good idea to add a capacitor in series. The capacitor wil block the DC current path, leaving the AC path intact. Direct current can damage a speaker, because it looses all it's energy as heat instead of sound. Audio amplifiers have the DC path blocked internally. | TIP: A capacitor connected to ground acts like a physical object | attached to something: you can usually jiggle it a bit (alternating | current), but it cannot be moved (DC current). setting up PURRR ---------------- I will refer to 'the monitor' in subsequent sections. This consists of two parts: a small piece of code which runs on the microcontroller, and a larger piece of code which runs on the PC connected to the microcontroller. Both pieces behave as one thing, meaning you can type commands on the host PC monitor prompt, which will affect the microcontroller. In order to use PURRR, the microcontroller needs to have this monitor installed. It is a small program that executes when the chip starts up, and makes sure it understands commands sent by the host PC. Check the file 'purrr/seed/sheepsint.cat' in the brood distro. Running this file as (purrr seed sheepsint) use will build a new monitor from scratch. The data in this file creates a monitor for the 18f1220 chip, running an 8Mhz oscillator (internal) and uses a 19200 baud serial link to the host pc. clicks ------ The command line you are looking at has two modes 'host' mode and 'target' mode. We're going to use the latter one, since our main interest is in interacting with the target chip. Type target to put the console in forth mode. To check if all is well, you can use the word 'ping'. The chip should reply with 'PING'. If not, refer to the section 'Troubleshooting'. Now type this: ( : click PORTB 3 toggle ; ) record First, the symbols '(' and ')' are used to group things together into a single container. The things that are grouped here are symbols and numbers representing forth code. The 'record' command takes this chunk of forth code, compiles it, and sends it to the microcontroller. In addition it saves the current state of the microcontroller to disk on the host. 'record' can be abbreviated to 'r'. After recording the code, the microcontroller contains the tiny program called 'click' which will toggle the polarity of the speaker output pin. You can execute this program from the monitor prompt. Doing so results in an audible click. To hear this, type: click Let's dissect the forth code between the parenthesis: : click PORTB 3 toggle ; The first word is ':', which reads as "Take the next word in the stream of words, and record this as a name for the code that follow after the name." The word ';' means something like "The current subprogram is done, continue where you left off before running this piece of code." Or shorter: "Return to caller". The remaining words 'PORTB 3 toggle' mean 'toggle the voltage of pin 3 at port B from high to low, or vice versa'. language and notation --------------------- With some tangible examples at hand, it's time to explain the basic ideas behind the development system. You might have noticed that all commands, both the commands in the monitor, and the forth code sent to the microcontroller, have the arguments BEFORE the name of the command, like click PORTB 3 toggle ( : click PORTB 3 toggle ; ) record This is called 'Reverse Polish Notation', or RPN. You could view "click", "PORTB 3 toggle" and "( : click PORTB 3 toggle ; ) record" as 3 distinct sentences, where the last word denotes the action to be taken, and the words that come before further specify the action. However, a more correct picture is to view the words that come before the action also as actions. More specificly, they will leave data for the 'real' action to be collected later. For example, 'click' does not collect any extra data, but 'toggle' collects 2 elements (port address and bit number), and 'record' collects a composite data atom which is a list containing forth commands. These commands can be further distinguished. 'click' is a direct command for the microcontroller, it will execute some code on the PIC. You can think of 'toggle' to be similar to 'click' for now. (In in reality it is a macro, which means it can only be used inside a definition.) The command 'record' is actually a META command, meaning it is executed on the host PC instead of the microcontroller: it will compile the chunk of forth code provided as data, and send it to the microcontroller over the serial link. The words communicate through a stack of data items. This is so both on the target PIC where data items are simply 8-bit numbers, and on the host machine where they can be virtually anything. The stack behaves as short term memory, or context. Things to remember: The language used to define code for the microcontroller is an 8-bit Forth dialect called PURRR. It is a low level language which only knows 8-bit numbers as datatypes, and can be used to program the peripherals and I/O pins on the microcontroller. In addition to that, there is a more sophisticated language called CAT which runs on the host PC, and can be used to control the PIC chip from the outside. The 'record' command is an example of this. It understands more elaborate data structures like 'lists representing forth code' and the like. delays ------ Back to the practical work. What we have is a microcontroller which understands the command 'click'. We're going to use this to generate more complicated sounds. One of the key ingredients missing at this point is a way to express the passing of time. The simplest way to do so is to just do nothing for a while. The simplest way to do nothing a hundred times is 100 for next Here 'for' takes one number from the parameter stack (100) and loops the code between 'for' and 'next' that many times. Here there's no code between 'for' and 'next' so it will do nothing but counting. Let's give this function a name : wait100 100 for next ; and record it ( : wait100 100 for next ; ) record I'll leave out the '( xxx ) record' construct from now on and assume you know how to record code on the microcontroller. A code snippet which has a name is called 'word', 'function' or 'subprogram', which all mean the same. A single iteration of this do-nothing loop will take 3 instruction cycles - one for decrementing a counter, and two for performing a jump back to the beginning of the loop - so the entire loop will take a little over 300 machine cycles when adding the setup/cleanup code. | TIP: To see the machine code which performs a word, type '(word) | isee'. You can look up the meaning of the opcodes in the datasheet. Using this word to wait between consecutive speaker on/off toggles, we can create a simple beep like this: : beep100 100 for click wait100 next ; Which 'draws' 50 periods of a square wave. Recording and running the code '(beep100) trun' should give you a high pitched beep. The frequency of this beep is roughly 8000000 4 / 2 / 300 / p or 3333 Hz. The different factors are 8000000 chip clock frequency 4 / number of clocks/instruction 2 / number of clicks/period 300 / number of instructions/click Now we can go on creating a different beeps using other wait words : wait1000 10 for wait100 next ; : beep1000 100 for click wait1000 next ; bytes ----- Purrr is an 8 bit forth, reflecting the basic 8 bit architecture of the PIC18F chips. For some applications, this is rather limiting: numbers can not be bigger than 255, or when using positive/negative numbers, the limits are -128 and 127. We already saw a consequence of this in the 'wait' words in the last section. In plain purrr, it is not possible to do 1000 for next to loop for 1000 times, since 1000 will not fit in a byte. Purrr will assume you know what you're doing. The code above is equivalent to 232 for next Since the low 8 bits of 1000 are 232, which can be seen by doing 1000 0xFF and p To get a loop of 1000 times, we had to resort to a nested loop like 10 for 100 for next next which in the example above was represented as : wait100 100 for next ; 10 for wait100 next time hierarchy -------------- FIXME: revise from here down Here I'll introduce the concept of 'time scales', which we will use to structure the code sequencing for the synthesizer we're building. We'll be using a sample clock of 8kHz, which is high enough for interesting sounds, and low enough so it can run on a simple PIC chip. scale frequency subdivision -------------------------------------------------- chip clock 8 MHz instruction 2 MHz (4 /) sample 8 kHz (250 /) control 200 Hz (40 /) 1/4 note 8 Hz (25 /) beat 2 Hz -- 120 bpm (4 /) bar .5 Hz (4 /) The first column gives the different time scales, the second column the absolute frequency of the clock at that scale, and the third column gives the time division with respect to the parent clock. The first row gives the chip clock, which is divided by 4 beyond our control to give the instruction clock. This means the chip will run at 2 MIPS. The _sample_ clock is the maximum rate at which we will toggle the output of the speaker: it is the rate at which the synth engine runs. This means the absolute maximum frequency that can be produced by our synth will be 4 kHz. The subdivision factor is 250, which means we can execute a bit less than 250 instructions per sample to compute the sounds. The _control_ rate is the maximum rate at which we will update the parameters of the synth engine, which runs at sample rate. For example, this time scale is where we'll introduce _modulation_ computations like pitch modulation. The _note_ rate is the frequency scale associated with note events. Written as a nested 'for' loop, which performs a single 'note' sequence, this looks like : seq-note run-note 25 for run-control 40 for run-synth 83 for next next next ; Or in more readable expanded form, together with the beat and bar time scales, we have the words : seq-synth run-synth 83 for next ; : seq-control run-control 40 for seq-synth next ; : seq-note run-note 25 for seq-control next ; : seq-beat run-beat 4 for seq-note next ; : seq-bar run-bar 4 for seq-bar next ; This basically solves our time problem. But what do the functions 'run-xxx' actually do here? Before I answer that, let's first have a look at a better way to do timing. hardware timer -------------- There's a problem with the previous timing code run-synth 83 for next If the execution time of 'run-synth' is small, the total execution time will still be close to 250 cycles. This works for illustration purposes, but we will run into cases where the 'run-synth' function will be something else which does takes more time, so the total execution time will be way over 250 cycles, which results in a sample rate lower than 8kHz, messing up any pitch calculations which are based on the assumption the sample rate is fixed. Luckily, there is a "right" way to keep time, which is to use the the hardware timer of the PIC chip. A timer is nothing more than a dedicated on chip counter register which has its value incremented or decremented every instruction cycle. The main synth loop would now be run-synth wait-timer assuming we will get to 'wait-timer' before the timer register overflows. We'll be using the TIMER2 module, which is an 8 bit timer, reserving the 3 other 16 bit timers for different uses. This timer is also connected to the hardware PWM. See page 109 in the datasheet DS39605C for more information. This timer counts continuously from 0 to the value stored in the PR2 register. To configure the timer we need to write the desired period to PR2, and store 0x04 to the T2CON register, which disables the pre/post scalers and turns the timer on. : init-timer 250 PR2 ! 0x4 T2CON ! ; Run this as '(init-timer) trun' to start the timer. You can retrieve the current timer using : get-timer TMR2 @ ; The words '!' and '@' mean store and fetch respectively. '!' will store the 2nd value on the stack to the address present on the top of stack, here the address of the register PR2 and T2CON, while '@' will take the address on the top of stack, here the address of the register TMR2 and replace it with its value. Upon reset to 0x00, the timer will set the associated interrupt flag high. We will not use hardware interrupts just yet, but will explicitly wait on this flag as in macro : timer-if PIF1 TMR2IF ; ( timer 2 interrupt flag address ) forth : wait-timer timer-if low ( clear interrupt flag ) begin timer-if high? until ; ( loop until flag is set ) The first lines create the macro 'timer-if' which will expand into 'PIR1 TMR2IF', which is a bit address consisting of two elements: the register address of the peripherial interrupt request register 1, and the bit representing the timer 2 interrupt flag in this register. Using this to make the maximum frequency beep: : nyquist 100 for click wait-timer next ; patching -------- One of the things I'm trying to show in this tutorial, is that writing a (configurable) application is really writing a new language on top of another one. The simplest form of such languages is a plain old data structure, of which the simplest for would be the configuration file or configuration panel. Like Bill Gosper said: "A data structure is just a very stupid programming language." The simple sound synth we're building has some fixed structure, the time hierarchy, which is what makes it a synth: it's most important task is to "Do something interesting over time, preferably in response to external events." What exactly will be done at key point's in the time hierarchy -- the 'run-xxx' words -- will be configurable at run time: there will be a number of bytes in RAM that determine what code will run at any these points. We'll call this configuration a 'patch', it is just a data structure -- a collection of bytes -- that determines the behaviour of the synth. So we can 'program' the synth by changing these bytes: a very stupid programming language indeed. Later on, we'll create some more elaborate control language on top of this very simple patch layer. So, how is it done in PURRR? The word to implement behaviour that changes at runtime is the 'route' word. The function : seq-synth route snap ; crackle ; pop ; This word will take one element off the data stack ( n -- ) and use it to jump to the n-th instruction in the list, with zero being the first. The two words 'snap ;' compile to the instruction 'bra snap', which will jump to the code that implements the 'snap' word, so here the number n maps to lines. If we combine this with a variable, residing in RAM, that contains the index, which we can express with the PURRR code variable synth we could have the code for the seq-synth word be : run-synth route snap ; crackle ; pop ; : seq-synth synth @ run-synth wait-timer ; More abstractly, this converts a number into a different behaviour (function). When 'synth' contains 2, the 'pop' word will run when 'run-synth' is called, etc.. loading forth code ------------------ Loading all the code allows us to do from CAT: 2 (synth) t! (seq-bar) t The first command sets the synth engine variable, while the second command runs the synth for one bar of time, which is 4 beats or 16 notes. A simpler way exist using the 'forth-mode' for emacs, provided in the script emacs/purrr.el in the brood distribution. You can use the key combination C-c C-r to send the current selection as forth code to a running *cat* session for recording to the microcontroller. WORKSHOP NOTES -------------- Every time you get an error, and you suspect the state is messed up, it is safest to just reset the chip from the monitor typing the 'cold' command. If this doesn't work, reset the chip externally by interrupting the power for a second. (Either install a switch, or pull out the power line from the breadboard.) The resistor which connects pin 4 to either 0V or 5V determines debug mode (0V) or application mode (5V). If for some reason a simple reset does not work, the application has been corrupted. You can switch to debug mode to reload it, then switch back to application mode. Troubleshooting connection -------------------------- Whenever you don't get a reply when you type the command 'ping', something is wrong with your connection or the code on the PIC chip. Check the following: * does 'ping' still not work after pressing CTRL-C? * does the serial port have the correct baud rate setting? * is the serial cable connected? * are the RX/TX signals connected properly? * is the target chip powered correctly? * does 'ping' work in debug mode (pin 4) ? If the chip gets hot and starts to smell like burned plastic, you have the polarity wrong. PIC chips are tough, and will usually survive a limited amount of reversed polarity current, but they will die when they get too hot, and their quality degrades.