Sunday, 19 December 2010

Battlezone With Lasers Part 2

Still an ongoing project to play Atari Battlezone via a laser projector (for no other reason than I like a challenge :)).

I stumbled for a long time over pushing data fast enough to a microcontroller, with a USB serial port proving too slow and a USB isosynchronous device being very complicated to program. Now I am using an mbed microcontroller board (with a decent amount of RAM and fast clock) to receive data and drive the DACs, and with the ethernet support of the mbed, I am able to send data over a TCP socket, which seems to be plenty fast enough.

The mbed's speed and RAM also allowed me to do a lot of the number crunching on the microcontroller (in particular the plotting of lines) so all I need is to push the lists of vertices to the mbed and it will generate "in between" points along a line (required so that the laser galvos can be moved a bit more incrementally - and with less probems of intertia -than just throwing them at the raw vertices. Also means I can keep plotting a frame while the next one arrives.

The "hard bit" - the actual Battlezone game - is actually easy, since I am using the wonderful open-source MAME emulator and was simply able to locate the vector display emulation code and hook my stuff into it. Luckily Battlezone does output vectors with decent continuity (i.e. where lines join up they tend to be sent consecutively, which saved me sorting list into some kind of optimised plot order). So basically my hooks in the MAME vector code just convert the floating point vector coordinates into 12 bit integer values (for my ADC) and flag "move" vs "draw" actions. Then the list can be dumped to the TCP socket at the end of the refresh cycle. At the moment I am only outputting every 10th frame to let the microcontroller breathe.

On the mbed side I am using a MCP4922 12-bit dual Digital to Analog Converter (DAC) with SPI serial interface, which can easily be driven from the mbed. The DACs drive a 20kpps Galvo set purchased from ebay (about £100 including PSU and drivers). I also have a 6N139 opto isolator driving the TTL blanking for the laser (a 50mW green DPSS module from Aixiz).

I am still working on the mbed code... at the moment I use a kind of double buffering with 2x16kb buffers, one of which can receive the frame buffer from TCP/IP socket while the mbed is plotting the other. Between plots the mbed checks if the receive frame is complete and switches the buffers if this is the case.

The frame rate I can plot is too slow, and I need to work out how to make it better.  I need to force delays during plotting to allow the mechanical galvos to catch up with the driver signal. These are all <1ms but they add up and reduce the frame rate. Also the lines are "plotted" with "in-between" point calculation to try to keep galvo movement rate reasonable. I need to work out the best combination of delays and divisions to get the best plot with the best framerate. I am also thinking about recognising the text at the top of the display (status text and scanner) and missing them out of the plot (text kills the plot rate)

Still a work in progress, but fun!

Saturday, 27 November 2010

Tesla coil diary - part 2


I've been playing on and off with the coil for the last couple of months, but the first test a few weeks ago was a little bit disappointing, only managing sparks of 6" or so and then only to ground (no air streamer breakout event with a breakout point on the top)

At first I thought the problem was positioning of the primary coil tap (i.e. change the number of active turns on the bottom coil) but I still could not get any better than 6" sparks to ground. Then I started to play with the spark gap and found that using a spacer to force a larger gap between the lengths of copper tube making up my static gap, the sparks improved a bit. That is until the spacers caught fire :)

So, I decided to try with a simple rotary spark gap and holy crap did it make a difference! Here it is...


I used 5mm acrylic sheet as a base material and a nominal 3000rpm shaded pole motor running at 240VAC. In theory 3000rpm is 50 revs per second. With the spark gap opening twice per rev (i.e. presenting at zero and 180 degrees) this should give 100 breaks a second I think... in theory this is perfect for discharging the capacitor bank just as it reaches maximum charge twice (positive and negative) on each mains AC 50Hz cycle.

Thats the theory, but its not a proper synchronous motor and the load of the spinning gaps looks likely slow it down a bit... if I get on to tuning things further I might try to work out its actual RPM. The motor is fitted to a base plate which can be rotated so the phase (i.e. when in mains cycle the breaks happen) can be changed, but I think this is really only going to be of value if the break rate really is 100Hz. I haven't played with moving the motor yet, something for another day.

Well... here are the results, without any serious attempt at tuning the primary tap.


Some other recent additions are a Terry Filter (safety spark gap and surge filter which protects the Neon Sign Transformer getting fried)

And a wooden base for it all to sit on


When I was packing it all up I was rather perturbed to receive small electric shocks every time I touched the secondary connections. At first I thought I had done something dumb like leave the mains connected or that the primary capacitors had remained charged somehow. However I got shocks handling the secondary even after I had completely removed it from the rest of the setup and I think the PVC pipe and/or the varnish layer were storing a static charge which leaked slowly over to the copper wire. The outside of the varnish certainly had a static charge like a TV screen right after the coil had been run. Interesting!

Sunday, 24 October 2010

Solenoid Drum Machine

Just a quick project to try out some new push solenoids...

Based on PIC16F688 and building on the MIDI input code used on my earlier POKEY project


include <system.h>
#include <memory.h>

#pragma DATA _CONFIG, _MCLRE_OFF & _WDT_OFF & _INTRC_OSC_NOCLKOUT
#pragma CLOCK_FREQ 8000000
typedef unsigned char byte;

// define the pins
//#define P_LED portc.0

// MIDI defs
#define MIDIMSG(b) ((b)>>4)
#define MIDICHAN(b) ((b)&0xf)
#define MIDIMSG_NOTEON 0x09
#define MIDIMSG_NOTEOFF 0x08

// MIDI message registers
byte runningStatus = 0;
int numParams = 0;
byte midiParams[2] = {0};

#define SZ_RXBUFFER 20
byte rxBuffer[SZ_RXBUFFER];
byte rxHead = 0;
byte rxTail = 0;
int tmr[4] = {0};

////////////////////////////////////////////////////////////
// INTERRUPT HANDLER CALLED WHEN CHARACTER RECEIVED AT
// SERIAL PORT
void interrupt( void )
{
// check if this is serial rx interrupt
if(pir1.5)
{

// get the byte
byte b = rcreg;

// calculate next buffer head
byte nextHead = (rxHead + 1);
if(nextHead >= SZ_RXBUFFER)
{
nextHead -= SZ_RXBUFFER;
}

// if buffer is not full
if(nextHead != rxTail)
{
// store the byte
rxBuffer[rxHead] = b;
rxHead = nextHead;
}
}
}


////////////////////////////////////////////////////////////
// INITIALISE SERIAL PORT FOR MIDI
void init_usart()
{
pir1.1 = 1; //TXIF
pir1.5 = 0; //RCIF

pie1.1 = 0; //TXIE no interrupts
pie1.5 = 1; //RCIE interrupt on receive

baudctl.4 = 0; // SCKP synchronous bit polarity
baudctl.3 = 1; // BRG16 enable 16 bit brg
baudctl.1 = 0; // WUE wake up enable off
baudctl.0 = 0; // ABDEN auto baud detect

txsta.6 = 0; // TX9 8 bit transmission
txsta.5 = 1; // TXEN transmit enable
txsta.4 = 0; // SYNC async mode
txsta.3 = 0; // SEDNB break character
txsta.2 = 0; // BRGH high baudrate
txsta.0 = 0; // TX9D bit 9

rcsta.7 = 1; // SPEN serial port enable
rcsta.6 = 0; // RX9 8 bit operation
rcsta.5 = 1; // SREN enable receiver
rcsta.4 = 1; // CREN continuous receive enable

spbrgh = 0; // brg high byte
spbrg = 15; // brg low byte (31250)

}


byte rxInc(byte *pbIndex)
{
// any data in the buffer?
if((*pbIndex) == rxHead)
return 0;

// move to next char
if(++(*pbIndex) >= SZ_RXBUFFER)
(*pbIndex) -= SZ_RXBUFFER;
return 1;
}

////////////////////////////////////////////////////////////
// RECEIVE MIDI MESSAGE
// Return the status byte or 0 if nothing complete received
// caller must check midiParams array for byte 1 and 2
byte receiveMessage()
{
// buffer overrun error?
if(rcsta.1)
{
rcsta.4 = 0;
rcsta.4 = 1;
}

// any data in the buffer?
if(rxHead == rxTail)
return 0;

// peek at next char in buffer
byte rxPos = rxTail;
byte q = rxBuffer[rxPos];

// is it a channel msg
if((q&0x80)>0)
{
runningStatus = 0;
switch(q&0xf0)
{
case 0x80: // Note-off 2 key velocity
case 0x90: // Note-on 2 key veolcity
case 0xA0: // Aftertouch 2 key touch
case 0xB0: // Continuous controller 2 controller # controller value
case 0xC0: // Patch change 2 instrument #
case 0xE0: // Pitch bend 2 lsb (7 bits) msb (7 bits)
runningStatus = q;
numParams = 2;
break;
case 0xD0: // Channel Pressure 1 pressure
runningStatus = q;
numParams = 1;
break;
case 0xF0: // (non-musical commands) - ignore all data for now
return q;
}
// step over the message
if(!rxInc(&rxPos))
return 0;

}

// do we have an active channel message
if(runningStatus)
{

// read params
for(int thisParam = 0; thisParam < numParams; ++thisParam)
{
midiParams[thisParam] = rxBuffer[rxPos];
if(!rxInc(&rxPos))
return 0;
}
// commit removal of message
rxTail = rxPos;
return runningStatus;
}
else
{
// remove char from the buffer
rxInc(&rxTail);
return q;
}
return 0;
}


void main()
{
// osc control / 8MHz / internal
osccon = 0b01110001;

// timer0... configure source and prescaler
cmcon0 = 7;

// enable serial receive interrupt
intcon = 0b11000000;
pie1.5 = 1;

// configure io
trisa = 0b00010000;
trisc = 0b00110000;
ansel = 0b00000000;

porta=0;
portc=0;
memset(tmr,0,sizeof(tmr));

// initialise MIDI comms
init_usart();


// loop forever
for(;;)
{
// get next MIDI note
byte msg = receiveMessage();
if(msg)
{
byte note = midiParams[0];
if(note >= 48 && note < 52)
{
int which = note-48;
// 0x90 note on
// 0x80 note ff
if((msg & 0xf0) == 0x90)
{
tmr[which] = 200;
}
}
}

for(int i=0;i<4;++i)
{
if(tmr[i] > 0)
tmr[i]--;
}

porta.2 = (tmr[0]>0)?1:0;
portc.0 = (tmr[1]>0)?1:0;
portc.1 = (tmr[2]>0)?1:0;
portc.2 = (tmr[3]>0)?1:0;
}
}

Saturday, 18 September 2010

Tesla Coil Diary - Part 1

I've wanted to own a Tesla coil pretty much since I knew what one was, but I always thought it would be a bit difficult to make and possibly a little bit dangerous... Anyway, I decided to bite the bullet and started gathering the bits a couple of months back and actually started the building of it a couple of weeks ago after I got the courage to fire up the 10kV Neon Sign Transformer I got off ebay.

I got all the information and many great tips of websites of other coilers and have borrowed many ideas. I will try to remember and credit as much as I can.

I started off by getting this neon transformer

its a 10kV 50mA F.A.R.T. (oh the fun!) Resinblock. Here is my first try out of it, making a Jacobs ladder from coat hanger wire....

So far so good...

After a bit of online reading I eventually decided to wind my secondary coil on 125mm PVC ducting pipe (After deciding the 68mm drainpipe I first bought was just too wimpy). I got a nice 2Kg reel of 0.71mm magnet wire from ebay and got winding. I initially rigged up a motor driven jig to wind the wire but ended up winding by hand so I could keep a good tension on the wire (The motor came in very useful later while varnishing). I wound 800 turns in a couple of hours then put on about 5 coats of polyurethane gloss varnish. I used kapton tape to secure the outer windings (I owe http://deepfriedneon.com/ for many tips I used ... given my DIY prowess, without this information I would have ended in disaster I am sure)

Here is my winding rig...

I was quite pleased with the end result, which looked nicely like something from a B-movie mad scientists lab.

I originally bought 6mm microbore copper tubing to make my primary coil, but realised that I had not bought enough, and it was rather too expensive to go and buy another, longer length, so I looked around for alternatives and found some 4.75mm solid aluminium wire at the much better price of £12 for 20 metres. The wire was a lot softer and easier to kink than I expected and at first I wasn't too happy with the result. But after some reworking I think its OK. Here is the result (set on MDF with acrylic stands secured with nylon zipties)


I managed about 17 turns. I think I need to tap the coil after about 10 turns based on what Tesla Coil CAD told me - but I thought it would be good to have the extra tunability offered by the extra coils

For the top load I got some 10mm semi-rigid aluminium ducting hose (as is a coiler traditional I believe) and set it between two 34cm steel trays from the local 99p shop (these have to be the best bargain of this build!). This whole procedure was more difficult than it probably sounds... I needed to bolt the trays together first, with a wood spacer, then use tensioned strips of gaffer tape to hold the 2 plates parallel to each other. Then I threaded a rubber bungee cord through the flexible duct and after putting the ducting around the rim of the trays I hooked up the bungee and spread the out ducting round the rim until it joined up. I used a bit of flexible plastic sheet to make a sleeve to put inside one end of the ducting and help join the ends together. A bit of aluminium tape joined the ends, then more aluminimum tape to secure the ducting on the rim of the trays (helpful tips from http://www.hvtesla.com/toroid.html)




It might not be the best looking toroid, but it does look pretty meaty and mean!

Next steps are to add a strike rail to the primary (finally getting some use out of that microbore copper pipe), build a chassis (in progress - out of chipboard and softwood) and add fittings to secure it all together properly without danger of it collapsing. In the mean time here are the parts stacked on top of each other for a photo opportunity....


And so onto the electrics... I threw caution to the wind and got some Cornell Dubilier 942C20P15K-F capacitors, which a bit of online research seemed to indicate were THE choice for a good coil, even though they are not cheap, especially when factoring postage from US to UK. My capacitor bank has 28 of these 0.15uF, 2000V capacitors arranged in two parallel strings of 14. I mounted them on clear acrylic and wired them up with 1M bleeder resistors using high current wire taken from a set of car jump leads.
I just made my spark gap this weekend... I initially intended making a cylindrical "Richard Quick" type design but I preferred the idea I saw on a web site (which I would credit if I could find it again) where the multiple copper tubes are laid out side by side on the top of a box, and air is drawn through them by fans mounted on the ends of the box. Airflow keeps the gaps cool, but also sucks away ionised air from the gaps and quenches the sparks more quickly, which (so I read) results in better operation of the coil.

I made the spark gap box from acrylic sheets with an 85mm 240VAC fans (from Maplin) mounted at each end. 10 copper tubes of 15mm diameter and 90mm length form the gap. Two layers of kapton tape at each end of each copper tube provide spacing of ~0.4mm between tubes when they are laid side by side. Connection is made (at the moment) by a plug of compressed aluminium foil around which is wound the connection wire (car jump lead) inside the two "terminal" tubes. I am not sure how well this arrangment is going to work... we shall see.. The tubes are not permanently attached to the box, so my idea is that the number of tubes included in the gap can be varied by moving one of the tubes to which the wire is attached (the other being fixed at one side) by simply removing and shuffling the tubes.


I still need to complete build of the Terry Filter circuit (which I hope will protect the neon transformer from voltage surges) as well as the structural bits and pieces, however I hope I can get this thing up and running before too much longer. Watch this space for the results....

Wednesday, 30 June 2010

DIY Boss PC-2 / Amdek PCK100 Analog Percussion Synth

Back at the end of the 80's I went in a music shop in London and on impulse I bought this wierd BOSS box that made weird bloopy noises... a PC-2 Percussion Synthesizer. It wasn't a difficult decision at the time since it only cost me £10 brand new boxed (I think they were trying to get rid of them) and it came with a free brand new BOSS HC-2 Hand Clapper pedal, although that seemed even less useful (though the guy did a good sales pitch involving a tale about a friend with no arms who had trouble applauding at gigs... :o)


Apart from a bit of novelty value I gotta be honest they didn't get a lot of use... I think the HC-2 got chucked during a house cleanout and the PC-2 was sold on ebay a few years back (and was amazed to get £100 for it). Well now I am kicking myself and wished I'd held on to these two collectables.

So when I saw the schematic of the Amdek PCK100 online (actually a kit form of the PC-2 sold by a Roland affiliate) I decided to try to make one and once again re-live boingy noise heaven.


On the page http://www.effectsdatabase.com/model/amdek/pck100 where I found the schematic I also found a photo of the PCB and decided to try to use it directly. With some manipulation in Paint Shop Pro and the use of Press'n'Peel PCB etching film I was able to make a copy of the board and after working out some workarounds (e.g. using BA6110 instead of ultra-rare BA662A VCA chip) I actually got it to work.. So in case you're interested ..here is how I did it

1) I started off with this photo of the track side of the original PCB

2) Turned to mono, upped the contrast, then very carefully use the "eraser" tool to ensure there are good clean gaps between all the tracks

3) Marked drill holes with circles

4) Drop colour depth to 2 colours

5) Negative image and a few embellishments and its ready to press'n'peel

6) Laser printed etch-resist transferred to copper clad board using a hot iron

7) Etched, drilled and trimmed ready for components

I mounted the board inside a project box from Maplin. By the way I have a thing for Dymo embossed label tape :)





The Amdek user guide gives info on some mods to the board (VCO wave form change, mod waveform change) which I added toggle switches for. Usually the Sweep control is a center tap pot.. I didn't have one so I instead rigged up a DPDT toggle and a resistor to a normal pot so that the same effect could be acheived (although I am not sure it works so well)

The original board calls for a Roland BA662A VCA chip... you won't find one! you can use a similar BA6110 chip but the pinout is different. I found the following worked

  • socket 1 connect to BA6110 pin# 4
  • socket 2 connect to BA6110 pin# 2
  • socket 3 connect to BA6110 pin# 1
  • socket 4 no connection. BA6110 pin#3 connected to GND (pin 5)
  • socket 5 connect to BA6110 pin#5
  • socket 6 connect to BA6110 pin#6
  • socket 7 connect to BA6110 pin#7
  • socket 8 connect to BA6110 pin#8
  • socket 9 connect to BA6110 pin#9
Click below to get the full size PCB template to use directly. See the link at start of article for schematic, photo of top of the board and original PCK100 kit instructions



Tuesday, 11 May 2010

Battlezone with lasers

This is a project I've had simmering on the back burner for a while. Still at the early stages but thought it might be fun to keep track of each step here

A few months back I got a 20kps laser scanner galvo set off ebay with the intention of making my own laser projector and a vision of using it to play some old vector arcade games... particularly my old fave Atari Battlezone. The arcade game bit seemed pretty easy, since you can play BZ on the open source MAME emulator so I thought I could hook into the vector terminal emulation.

I found the asynchronous UART on an Arduino board was not quite fast enough to cope with the data... dropping bits all over the place, so I started looking at a USB conneciton to a PIC2455. As a SourceBoost C user I was not able to find any easy to understand USB CDC (Communication Device Class, a.k.a serial port) implementations for the PIC - so I decided to make my own, leaning heavily on sample code I found online.

Well I finally got to the point where my PIC would connect via USB show up as a COM port and be easy to access from a Windows program. Then I hooked up an 12-bit SPI dual DAC and connected it to the galvo setup and tried the first random hacking into MAMEs vector module.

I didn't expect it to work first time, and didn't! but my impatient hacking did produce some interesting squiggles at about 2 fps. I needed to use a long exposure photograph to actually make sense of it, but eventually I recognised a couple of parts of the display and got quite excited that the concept was proved!

The coordinate handling is obviously messed up and the image is wrapping on itself multiple times, also there is no attempt at blanking yet - so there are stray lines all over. The big job will be to find some way to optimise the render list to stop throwing the galvos all over the place and improve on the 2 fps refresh!

As you can see I have a long way to go!

Here is the plot showing the bits I recognised


Here is an actual MAME screen showing what it should look like


If things improve I will post an update!

Monday, 3 May 2010

Motion detection to midi with puredata

another experiment with puredata, webcam image is passed through pix_movement object and a pix_blob turns the result into two midi note streams which are sent into reason via midi yoke. the image is the output of the pix_movement (difference between frames)



I put the pd patch at http://sites.google.com/site/skriyl/Home/pd-projects (motion noise.pd)



The patch outputs on midi channels 1 and 2. I used midi yoke and PD's midi output, then piped this into propellerheads reason, where you can use the "advanced midi" to set up midi bus A then lock down channels 1 and 2 to specific instruments in the rack. I used an NNXT with glockenspiel patch and nn19 with strings patch

Sunday, 2 May 2010

First play with PureData

I was thinking about making a midi "harp" and thought of trying with a webcam, using puredata to analyse the images and generate midi/sounds. This is the first time I have played with PD and I don't really know what I am doing with it yet... you'll see I am not quite there with the midi harp yet, but I am impressed with how quickly you can get something fun working in PD



Here is the PD sketch, which I got to by hacking about with one of the GEM tutorial sketches

Thursday, 22 April 2010

Matrix feedback in Reason

How to do something like this...



A great thing about Reason is that it allows you to wire a continuous input (like a CV, MIDI CC#, Mod wheel etc) to pattern changes on something like the Matrix sequencer or Redrum. You can't do this directly (there is no pattern change CV input) but you can do it via the "programmer" in the combinator. This is a great thing to experiment with...especially if the patterns are different beat lengths/step values and you layer a few of them . In one of my other clips I used MIDI CC# signals generated by a Lavalamp to randomly switch patterns on a set of Redrum modules.

In the above above clip I have 3 matrix sequences driving each other, which can result in some random sounding patterns which repeat over long periods and can descend into chaos with one small tweak.. eventually arriving at a new repeating cycle (of course you need to drive one or more sound modules like NNXT etc with the matrix outputs to be able to hear anything...)

Start by making a Combinator...

Inside the combinator, create 3 matrix sequencers.

Wire Curve CV output from each Matrix Rotary inputs 1,2,3 on the combinator. Ensure these are only connections between matrixes (-ices?) and combinator

Click the "show programmer" button. Click on Matrix 1 and next to "Rotary 2" source select "Pattern Select" target. For Matrix 2 map Rotary 3 to pattern select, For Matrix 3 map Rotary 1 to pattern select

Flip to Curve view on each matrix and draw a few random curves (Randomize pattern option can be good). Start the Matrixes and click randomly on their pattern screens. Soon they should be flicking between patterns almost at random

Wire the Gate and Note CV outputs of one or more matrix to a sound module such as NNXT.


Flip the matrixes to note mode and click at random on the screens. Change pattern lengths and note resolutions, try the "randomize pattern" option. After a while things should be getting pretty freaked out

Tuesday, 23 March 2010

Pre programmed PICs

Following some requests, I have listed pre-programmed PICs on ebay for a couple of my projects. If there is much interest (and its not all a massive hassle) I might also look into getting some PCBs made up and putting kits together
For now here are the PICs http://cgi.ebay.co.uk/ws/eBayISAPI.dll?ViewItem&item=150426687494

POKEY sound chip experiments

The Atari POKEY was the classic soundchip in the Atari 8-bit home computers and many 1980's arcade games. This clip shows some of my experiments in driving a POKEY from MIDI. A PIC receives MIDI data and two 74HC595 shift registers are used to assemble the 12 lines of bus data for the POKEY so it can be driven from a humble 14 pin PIC16F688. A 6N139 isolator is placed between MIDI in from PC and the PIC's serial input. The POKEY is clocked at 2MHz from the PIC's internal clock output.

I am using REAPER to sequence some MIDI files I found on the internet. Credit goes out to the authors of these MIDI files.. also to YouTube member little-scale, whose clips inspired me to poke about with the POKEY in the first place, and Bryan Edewaard, whose crib sheet I could not have done this without.

Here is the schematic for the circuit as built on breadboard (I am working on neater, stripboard based version)



And the source code for SOURCEBOOST C on the PIC16F688

#include <system.h>

#include <memory.h>



// PIC CONFIG (_INTRC_OSC_CLKOUT is needed so we output clock

// clock signal on pin 3)

#pragma DATA _CONFIG, _MCLRE_OFF & _WDT_OFF & _INTRC_OSC_CLKOUT

#pragma CLOCK_FREQ 8000000

typedef unsigned char byte;



// define the pins

#define P_DATA portc.0

#define P_SHCK portc.2

#define P_STCK portc.1

#define P_POKEY portc.3



// define "pure" tone sound mode. Other settings

// of bits 4-7 will add varying levels of distortion

#define POKEY_SOUNDMODE 0b10100000



// MIDI defs

#define MIDIMSG(b) ((b)>>4)

#define MIDICHAN(b) ((b)&0xf)

#define MIDIMSG_NOTEON 0x09

#define MIDIMSG_NOTEOFF 0x08



// structure for managing channel info

typedef struct

{

byte midiNote; // triggered MIDI note

byte note; // POKEY divider value

byte volume; // volume (bits 0-3)

byte count; // playing duration counter

} CHANNEL;



// Buffer to hold state of 4 POKEY voice channels

CHANNEL chan[4] = {0};



// MIDI message registers

byte runningStatus = 0;

byte midiParams[2] = {0};

byte numParams = 0;

byte thisParam = 0;



// Divider values for POKEY channels

byte notes[48] = {

250, // C#2

236, // D2

222, // D#2

210, // E2

198, // F2

187, // F#2

177, // G2

167, // G#2

157, // A2

148, // A#2

140, // B2

132, // C3

125, // C#3

118, // D3

111, // D#3

105, // E3

99, // F3

94, // F#3

88, // G3

83, // G#3

79, // A3

74, // A#3

70, // B3

66, // C4

62, // C#4

59, // D4

56, // D#4

52, // E4

50, // F4

47, // F#4

44, // G4

42, // G#4

39, // A4

37, // A#4

35, // B4

33, // C5

31, // C#5

29, // D5

28, // D#5

26, // E5

25, // F5

23, // F#5

22, // G5

21, // G#5

20, // A5

19, // A#5

18, // B5

17 // C6

};



////////////////////////////////////////////////////////////

// INITIALISE SERIAL PORT FOR MIDI

void init_usart()

{

pir1.1 = 1; //TXIF

pir1.5 = 0; //RCIF



pie1.1 = 0; //TXIE no interrupts

pie1.5 = 0; //RCIE no interrupts



baudctl.4 = 0; // SCKP synchronous bit polarity

baudctl.3 = 1; // BRG16 enable 16 bit brg

baudctl.1 = 0; // WUE wake up enable off

baudctl.0 = 0; // ABDEN auto baud detect



txsta.6 = 0; // TX9 8 bit transmission

txsta.5 = 1; // TXEN transmit enable

txsta.4 = 0; // SYNC async mode

txsta.3 = 0; // SEDNB break character

txsta.2 = 0; // BRGH high baudrate

txsta.0 = 0; // TX9D bit 9



rcsta.7 = 1; // SPEN serial port enable

rcsta.6 = 0; // RX9 8 bit operation

rcsta.5 = 1; // SREN enable receiver

rcsta.4 = 1; // CREN continuous receive enable



spbrgh = 0; // brg high byte

spbrg = 15; // brg low byte (31250)



}



////////////////////////////////////////////////////////////

// RECEIVE MIDI MESSAGE

// Return the status byte or 0 if nothing complete received

// caller must check midiParams array for byte 1 and 2

byte receiveMessage()

{

// loop until there is no more data or

// we receive a full message

for(;;)

{

// buffer overrun error?

if(rcsta.1)

{

rcsta.4 = 0;

rcsta.4 = 1;

}

// poll for a MIDI byte

if(!pir1.5)

{

// no data ready

return 0;

}



// read the character

byte q = rcreg;

pir1.5 = 0;



// is it a channel msg

if((q&0x80)>0)

{

numParams = 0;

thisParam = 0;

switch(q&0xf0)

{

case 0x80: // Note-off 2 key velocity

case 0x90: // Note-on 2 key veolcity

case 0xA0: // Aftertouch 2 key touch

case 0xB0: // Continuous controller 2 controller # controller value

case 0xC0: // Patch change 2 instrument #

case 0xE0: // Pitch bend 2 lsb (7 bits) msb (7 bits)

runningStatus = q;

numParams = 2;

break;

case 0xD0: // Channel Pressure 1 pressure

runningStatus = q;

numParams = 1;

break;

case 0xF0: // (non-musical commands) - ignore all data for now

runningStatus = 0;

return q;

}

}

// else do we have a channel message?

else if(runningStatus)

{

// fill in next command parameter

midiParams[thisParam++] = q;

if(thisParam>=numParams)

{

// return the command

thisParam = 0;

return runningStatus;

}

}

}

return 0;

}



////////////////////////////////////////////////////////////

// DRIVE DATA OUT TO SHIFT REGISTERS

// m is a bit mask to highest bit in the data

void dataOut(byte d, byte m)

{

while(m)

{

// shift clock low

P_SHCK = 0;



// data out

P_DATA = (d&m)?1:0;



// shift clock high

P_SHCK = 1;



// shift the mask

m>>=1;

}

}



////////////////////////////////////////////////////////////

// WRITE ADDRESS AND DATA TO POKEY

void writePokey(byte address, byte data)

{

// store clock low

P_STCK = 0;



// fill the shift regs

dataOut(address,0x08);

dataOut(data,0x80);



// store clock high

P_STCK = 1;



// pulse POKEY chip enable line

P_POKEY = 0;

delay_us(100);

P_POKEY = 1;

delay_us(100);

}



////////////////////////////////////////////////////////////

// POKEY RESET SEQUENCE

void resetPokey()

{

// fill all locations with 0

for(int i=0;i<16;++i)

writePokey(i, 0);

// reset sequence

writePokey(0x0f, 3);

writePokey(0x09, 1);

}



////////////////////////////////////////////////////////////

// HANDLE MIDI NOTE TRIGGER (ON OR OFF)

// MANAGES THE 4 VOICES

void handleNote(byte midiNote, byte midiVelocity)

{

int iAlreadyPlaying = -1;

int iFree = -1;

int iSteal = -1;

int iUpdatePOKEY = -1;

int iLongestPlay = -1;



// map 7-bit MIDI velocity to 4-bit POKEY volume

byte volume = midiVelocity >> 3;



// scan through the 4 channels

for(int i=0;i<4;++i)

{

// incremement play duration counter for this

// channel. we use this counter to detect which

// note has been playing longest if we need to

// steal a channel

chan[i].count++;



// check if the note is already playing on channel

if(chan[i].midiNote == midiNote)

{

iAlreadyPlaying = i;

}

// else is channel spare?

else if(!chan[i].midiNote)

{

iFree = i;

}

// else is channel the longest playing channel?

else if(chan[i].count > iLongestPlay)

{

iLongestPlay = chan[i].count;

iSteal = i;

}

}



// already got a channel playing this note?

if(iAlreadyPlaying > 0 )

{

// need to stop a note?

if(!volume)

{

// turn a note off

chan[iAlreadyPlaying].midiNote = 0;

chan[iAlreadyPlaying].note = 0;

chan[iAlreadyPlaying].volume = 0;

chan[iAlreadyPlaying].count = 0;

iUpdatePOKEY = iAlreadyPlaying;

}

}

// else check we have a nonzero volume. We will ignore

// zero volume requests against any note that is not already

// playing

else if(volume>0)

{

// convert from MIDI note to index in the notes[] array

byte note = midiNote;

while(note<37) note+=12; // 37 is lowest MIDI note we map

while(note>84) note-=12; // 84 is highest MIDI note we map

note-=37; // convert to array index value



// got a free channel?

if(iFree>0)

{

// use it

chan[iFree].midiNote = midiNote;

chan[iFree].note = notes[note];

chan[iFree].volume = volume;

chan[iFree].count = 0;

iUpdatePOKEY = iFree;

}

// else steal a channel from another note

else if(iSteal>0)

{

chan[iSteal].midiNote = midiNote;

chan[iSteal].note = notes[note];

chan[iSteal].volume = volume;

chan[iSteal].count = 0;

iUpdatePOKEY = iSteal;

}

}



// do we need to tell the POKEY anything?

if(iUpdatePOKEY > 0)

{

// make it so!

writePokey(0 + iUpdatePOKEY*2, chan[iUpdatePOKEY].note);

writePokey(1 + iUpdatePOKEY*2, POKEY_SOUNDMODE|chan[iUpdatePOKEY].volume);

}

}



void main()

{

// osc control / 8MHz / internal

osccon = 0b01110001;



// timer0... configure source and prescaler

cmcon0 = 7;



// configure io

trisa = 0b00010000;

trisc = 0b00110000;

ansel = 0b00000000;



// initialise MIDI comms

init_usart();



// reset the POKEY

resetPokey();



// loop forever

for(;;)

{

// get next MIDI note

byte msg = receiveMessage();



// handle note on/off (transpose down 1 octave)

if(MIDIMSG_NOTEON == MIDIMSG(msg))

handleNote(midiParams[0]-12, midiParams[1]);

else if(MIDIMSG_NOTEOFF == MIDIMSG(msg))

handleNote(midiParams[0]-12, 0);

}

}

Tuesday, 16 March 2010

Hand-cranked MIDI sequencer from a baked bean can

One empty baked bean tin, some lego and a stack of little magnets... stick magnets on the tin and slide them about to 'program' the sequencer, then grab hold of the 'transport control' and crank away.... The breadboard contains 5 hall-effect switches and a PIC16F688 to generate MIDI note on/off information. This is piped to Reason in the first half of the clip and to a Dave Smith Mopho synth in the second half.
I reckon with a baked bean tin about 16ft in diameter and about 25,000 magnets you could dump your sequencer software.. and you'd be getting some good aerobic exercise to boot :o)



Here is the schematic (if you make one, note that hall effect switches need the magnet to be the right way round.. if it does not trigger, flip the magnet over)


And the code (SourceBoost C... NOTE: you'll need programmer hardware like PICKit2 to burn the program to the PIC chip)
// HALL SENSOR TO MIDI NOTES

// J.Hotchkiss Mar2010

#include <system.h>

#include <memory.h>



// PIC CONFIG

#pragma DATA _CONFIG, _MCLRE_OFF&_WDT_OFF&_INTRC_OSC_NOCLKOUT

#pragma CLOCK_FREQ 8000000



#define P_SENSE1 porta.5

#define P_SENSE2 portc.2

#define P_SENSE3 portc.1

#define P_SENSE4 portc.0

#define P_SENSE5 porta.2



typedef unsigned char byte;



// INITIALISE SERIAL PORT FOR MIDI

void init_usart()

{

pir1.1 = 1; //TXIF transmit enable

pie1.1 = 0; //TXIE no interrupts



baudctl.4 = 0; // synchronous bit polarity

baudctl.3 = 1; // enable 16 bit brg

baudctl.1 = 0; // wake up enable off

baudctl.0 = 0; // disable auto baud detect



txsta.6 = 0; // 8 bit transmission

txsta.5 = 1; // transmit enable

txsta.4 = 0; // async mode

txsta.2 = 0; // high baudrate BRGH



rcsta.7 = 1; // serial port enable

rcsta.6 = 0; // 8 bit operation

rcsta.4 = 0; // enable receiver



spbrgh = 0; // brg high byte

spbrg = 15; // brg low byte (31250)

}



////////////////////////////////////////////////////////////

// SEND A MIDI BYTE

void send(unsigned char c)

{

txreg = c;

while(!txsta.1);

}



////////////////////////////////////////////////////////////

// CONTINUOUS CONTROLLER MESSAGE

void sendController(byte channel, byte controller, byte value)

{

send(0xb0 | channel);

send(controller&0x7f);

send(value&0x7f);

}



////////////////////////////////////////////////////////////

// NOTE MESSAGE

void startNote(byte channel, byte note, byte value)

{

send(0x90 | channel);

send(note&0x7f);

send(value&0x7f);

}



void main()

{

// osc control / 8MHz / internal

osccon = 0b01110001;



// timer0... configure source and prescaler

option_reg = 0b10000011;

cmcon0 = 7;



porta=0;

wpua=0;

portc=0;



// configure io

trisa = 0b00100100;

trisc = 0b00001111;

ansel = 0b00000000;



// initialise MIDI comms

init_usart();



// Set up the MIDI notes for each sensor

byte note[5] = {60,62,64,65,66};

// byte note[5] = {36,37,38,39,40}; // For Reason REDRUM

byte sense[5] = {0};

for(;;)

{

if(P_SENSE1 != sense[0])

{

startNote(0, note[0], P_SENSE1? 0:127);

sense[0] = P_SENSE1;

}

if(P_SENSE2 != sense[1])

{

startNote(0, note[1], P_SENSE2? 0:127);

sense[1] = P_SENSE2;

}

if(P_SENSE3 != sense[2])

{

startNote(0, note[2], P_SENSE3? 0:127);

sense[2] = P_SENSE3;

}

if(P_SENSE4 != sense[3])

{

startNote(0, note[3], P_SENSE4? 0:127);

sense[3] = P_SENSE4;

}

if(P_SENSE5 != sense[4])

{

startNote(0, note[4], P_SENSE5? 0:127);

sense[4] = P_SENSE5;

}

}

}

Saturday, 6 March 2010

MIDI Guitar on Stripboard... Kind of

Somewhere between the Omnichord and the Stylophone lies this thing... simple but suprisingly effective... a PIC16F688 microcontroller, 2 shift registers IC's, 36 switches and a bunch of wire. The buttons select major/minor/maj7/min7/7/dim/aug chords based on any root note, and you "strum" across 3-4 octaves of notes from the chord by touching bits of exposed wire with a "stylus". The output is all MIDI (circuit makes no sound by itself) and Reason is being used here for sounds.



Note - If you are new to PIC stuff and want to make your own version of this project, remember you will need some way to program the PIC chip (its like a tiny computer and it comes without any software installed). The code is included below, but you'll need to compile it (using the free SourceBoost compiler) and "burn" it to the PIC... you can buy a programmer (e.g. PICkit2) or maybe borrow one. If there is enough demand I might be able to provide pre-programmed PIC16F688's for this, or my other PIC projects. Drop me a message if you'd be interested.



Schematic


The business end...


The mess on the back...


How it works (if you are interested)...
It's the tried and trusted principle of the keyboard matrix - the 74HC595 IC's are "shift registers" which are simply used to scan a single "on" bit across 16 lines, one at a time (all 16 are used for the stylus, the first 12 are used for the columns of the kepad). The program running on the PIC chip reads the voltage coming back from each row of the keypad and also from the stylus. Since the program knows which one of the 16 shift register outputs it has switched "on" at any moment in time it then knows which buttons are pressed / which "strings" the stylus is touching at any moment in time by which input lines (if any) it reads the voltage back on. The rest is down to the program code to convert this info into MIDI notes and send them to a synth. One other important things are the 10k "pull down" resistors on each of the 3 keyboard rows and the stylus line... they make sure that an unconnected line settles at 0V rather than reading spurious random values.

The source code
// STRUM CHORD CONTROLLER

// (c) 2010 J.Hotchkiss

// SOURCEBOOST C FOR PIC16F688

#include <system.h>

#include <memory.h>



// PIC CONFIG

#pragma DATA _CONFIG, _MCLRE_OFF&_WDT_OFF&_INTRC_OSC_NOCLKOUT

#pragma CLOCK_FREQ 8000000



// Define pins

#define P_CLK porta.2

#define P_DS portc.0

#define P_STYLUS portc.1

#define P_HEARTBEAT portc.2

#define P_KEYS1 portc.3

#define P_KEYS2 porta.4

#define P_KEYS3 porta.5



typedef unsigned char byte;



// Chord types

enum {

CHORD_NONE,

CHORD_MAJ,

CHORD_MIN,

CHORD_DOM7,

CHORD_MAJ7,

CHORD_MIN7,

CHORD_AUG,

CHORD_DIM

};



// special note value

#define NO_NOTE 0xff

//byte silent[1] = {NO_NOTE};



// Define the chord structures

byte maj[3] = {0,4,7};

byte min[3] = {0,3,7};

byte dom7[4] = {0,4,7,10};

byte maj7[4] = {0,4,7,11};

byte min7[4] = {0,3,7,10};

byte dim[3] = {0,3,6};

byte aug[3] = {0,3,8};



// Define the MIDI root notes mapped to each key

byte roots[16]={36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51};



// bit mapped register of which strings are currently connected

// to the stylus (notes triggered when stylus breaks contact

// with the strings)

unsigned long strings =0;



// Notes for each string

byte notes[16] = {0};



// current chord type

byte lastChordType = CHORD_NONE;



// current root note

byte lastRoot = NO_NOTE;





////////////////////////////////////////////////////////////

// INITIALISE SERIAL PORT FOR MIDI

void init_usart()

{

pir1.1 = 1; //TXIF transmit enable

pie1.1 = 0; //TXIE no interrupts



baudctl.4 = 0; // synchronous bit polarity

baudctl.3 = 1; // enable 16 bit brg

baudctl.1 = 0; // wake up enable off

baudctl.0 = 0; // disable auto baud detect



txsta.6 = 0; // 8 bit transmission

txsta.5 = 1; // transmit enable

txsta.4 = 0; // async mode

txsta.2 = 0; // high baudrate BRGH



rcsta.7 = 1; // serial port enable

rcsta.6 = 0; // 8 bit operation

rcsta.4 = 0; // enable receiver



spbrgh = 0; // brg high byte

spbrg = 15; // brg low byte (31250)

}



////////////////////////////////////////////////////////////

// SEND A MIDI BYTE

void send(unsigned char c)

{

txreg = c;

while(!txsta.1);

}



////////////////////////////////////////////////////////////

// CONTINUOUS CONTROLLER MESSAGE

void sendController(byte channel, byte controller, byte value)

{

P_HEARTBEAT = 1;

send(0xb0 | channel);

send(controller&0x7f);

send(value&0x7f);

P_HEARTBEAT = 0;

}



////////////////////////////////////////////////////////////

// NOTE MESSAGE

void startNote(byte channel, byte note, byte value)

{

P_HEARTBEAT = 1;

send(0x90 | channel);

send(note&0x7f);

send(value&0x7f);

P_HEARTBEAT = 0;

}



////////////////////////////////////////////////////////////

// CALCULATE NOTES FOR A CHORD SHAPE AND MAP THEM

// TO THE STRINGS

void changeToChord(int root, int which)

{

int i,j,len=0;

byte *struc = maj;

byte chord[16];



if(CHORD_NONE == which || NO_NOTE == root)

{

// stop playing

for(i=0;i<16;++i)

chord[i] = NO_NOTE;

}

else

{

// select the correct chord shape

switch(which)

{

case CHORD_MIN:

struc = min;

len = sizeof(min);

break;

case CHORD_DOM7:

struc = dom7;

len = sizeof(dom7);

break;

case CHORD_MAJ7:

struc = maj7;

len = sizeof(maj7);

break;

case CHORD_MIN7:

struc = min7;

len = sizeof(min7);

break;

case CHORD_AUG:

struc = aug;

len = sizeof(aug);

break;

case CHORD_DIM:

struc = dim;

len = sizeof(dim);

break;

case CHORD_MAJ:

default:

struc = maj;

len = sizeof(maj);

break;

break;

}



// fill the chord array with MIDI notes

int from = 0;

for(i=0;i<16;++i)

{

chord[i] = root+struc[from];

if(++from >= len)

{

root+=12;

from = 0;

}

}

}



// stop previous notes from playing if they are not a

// part of the new chord

for(i=0;i<16;++i)

{

if(notes[i] != NO_NOTE)

{

// check to see if it is part of the new chord

byte foundIt = 0;

for(j=0;j<16;++j)

{

if(chord[j] == notes[i])

{

foundIt = true;

break;

}

}



// if not, then make sure its not playing

if(!foundIt)

{

startNote(0, notes[i], 0);

}

}

}



// store the new chord

for(i=0;i<16;++i)

notes[i] = chord[i];



}



////////////////////////////////////////////////////////////

// POLL KEYBOARD MATRIX AND STRINGS

void pollIO()

{

// clock a single bit into the shift register

P_CLK = 0;

P_DS = 1;

P_CLK = 1;

P_DS = 0;



// get ready to scan

int root = NO_NOTE;

int chordType = CHORD_NONE;

unsigned long b = 1;



// scan for each string

for(int i=0;i<16;++i)

{

// clock pulse to shift the bit (note that

// the first bit does not appear until the

// second clock pulse, since we tied shift and store

// clock lines together)

P_CLK = 0;

P_CLK = 1;



// did we get a signal back on any of the

// keyboard scan rows?

if(P_KEYS1 || P_KEYS2 || P_KEYS3)

{

// have we decided on the root note yet?

if(NO_NOTE == root)

{

// look up the root note

root = roots[15-i];



// get the correct chord shape

switch(

(P_KEYS1? 0b100:0)|

(P_KEYS2? 0b010:0)|

(P_KEYS3? 0b001:0))

{

case 0b111:

chordType = CHORD_AUG;

break;

case 0b110:

chordType = CHORD_DIM;

break;

case 0b100:

chordType = CHORD_MAJ;

break;

case 0b101:

chordType = CHORD_MAJ7;

break;

case 0b010:

chordType = CHORD_MIN;

break;

case 0b011:

chordType = CHORD_MIN7;

break;

case 0b001:

chordType = CHORD_DOM7;

break;

default:

chordType = CHORD_NONE;

break;

}

}

}



// now check whether we got a signal

// back from the stylus (meaning that

// it's touching this string)

byte whichString = 15-i;

if(P_STYLUS)

{

// string is being touched... was

// it being touched before?

if(!(strings & b))

{

// stop the note playing (if

// it is currently playing). When

// stylus is touching a string it

// is "damped" and does not play

// till contact is broken

if(notes[whichString] != NO_NOTE)

{

startNote(0, notes[whichString], 0);

}



// remember this string is being touched

strings |= b;

}

}

// stylus not touching string now, but was it

// touching the string before?

else if(strings & b)

{

// start a note playing

if(notes[whichString] != NO_NOTE)

{

startNote(0, notes[whichString], 127);

}



// remember string is not being touched

strings &= ~b;

}



// shift the masking bit

b<<=1;



}



// has the chord changed?

if(chordType != lastChordType || root != lastRoot)

{

// change to the new chord

lastChordType = chordType;

lastRoot = root;

changeToChord(root, chordType);

}

}



void main()

{

// osc control / 8MHz / internal

osccon = 0b01110001;



// timer0... configure source and prescaler

option_reg = 0b10000011;

cmcon0 = 7;



// configure io

trisa = 0b00110000;

trisc = 0b00001010;

ansel = 0b00000000;



// initialise MIDI comms

init_usart();



// initialise the notes array

memset(notes,NO_NOTE,sizeof(notes));

for(;;)

{

// and now just repeatedly

// check for input

pollIO();

}

}

Sunday, 28 February 2010

When Lava met Mopho

I got a new toy! an Mopho analog synth from Dave Smith Instruments. First use.. a companion to my MIDI lavalamp to free it up from PC based soft synths. Since my original MIDI lavalamp project was based on a standard Arduino, I decided this time to make a quick little module for it using a PIC16F688 (my current weapon of choice)



Here is the circuit on stripboard



...the mess on the back...



...and the schematic...



... the lamp is wired up as described on this post

... and the PIC source code is included below (SourceBoost compiler)

#include <system.h>

#include <memory.h>



#define ANA_0 0b00000000

#define ANA_1 0b00000100

#define ANA_2 0b00001000

#define ANA_3 0b00001100

#define ANA_4 0b00010000

#define ANA_5 0b00010100

#define ANA_6 0b00011000

#define ANA_7 0b00011100

#define ADC_MAX 6



#define P_HEARTBEAT porta.5



#pragma DATA _CONFIG, _MCLRE_OFF&_WDT_OFF&_INTRC_OSC_NOCLKOUT

#pragma CLOCK_FREQ 8000000

#define ADC_AQUISITION_DELAY 10



typedef unsigned char byte;



enum {

ADC_CONNECT,

ADC_ACQUIRE,

ADC_CONVERT

};



void init_usart()

{

pir1.1 = 1; //TXIF transmit enable

pie1.1 = 0; //TXIE no interrupts



baudctl.4 = 0; // synchronous bit polarity

baudctl.3 = 1; // enable 16 bit brg

baudctl.1 = 0; // wake up enable off

baudctl.0 = 0; // disable auto baud detect



txsta.6 = 0; // 8 bit transmission

txsta.5 = 1; // transmit enable

txsta.4 = 0; // async mode

txsta.2 = 0; // high baudrate BRGH



rcsta.7 = 1; // serial port enable

rcsta.6 = 0; // 8 bit operation

rcsta.4 = 0; // enable receiver



spbrgh = 0; // brg high byte

spbrg = 15; // brg low byte (31250)

}



void send(unsigned char c)

{

txreg = c;

while(!txsta.1);

}



void sendController(byte channel, byte controller, byte value)

{

P_HEARTBEAT = 1;

send(0xb0 | channel);

send(controller&0x7f);

send(value&0x7f);

P_HEARTBEAT = 0;

}



byte adcInput[ADC_MAX] = {ANA_2, ANA_3, ANA_4, ANA_5, ANA_6, ANA_7};

byte adcInitComplete = 0;

int adcResult[ADC_MAX] = {-1,-1,-1,-1,-1,-1};

int adcIndex = 0;

int adcState = ADC_CONNECT;



////////////////////////////////////////////////////////////////

//

// doADC

//

// State machine for running the ADC and updating the adcResult

// array with the result from each analog input

//

void doADC()

{

switch(adcState)

{

// Connect ADC to the correct analog input

case ADC_CONNECT:

adcon0=0b10000001 | adcInput[adcIndex];

tmr0 = 0;

adcState = ADC_ACQUIRE;

// fall through



// Waiting for a delay while the ADC input settles

case ADC_ACQUIRE:

if(tmr0 > ADC_AQUISITION_DELAY)

{

// Start the conversion

adcon0.1=1;

adcState = ADC_CONVERT;

}

break;



// Waiting for the conversion to complete

case ADC_CONVERT:

if(!adcon0.1)

{

// store the result

adcResult[adcIndex] = (((int)adresh)<<8)|adresl;



// and prepare for the next ADC

if(++adcIndex>=ADC_MAX)

{

adcIndex = 0;

adcInitComplete = 1;

}

adcState = ADC_CONNECT;

}

break;

}

}





#define BUFLEN 8

typedef struct

{

char midiChannel;

char midiController;

int minADC;

int maxADC;

char currentValue;

char history[BUFLEN];

} CONTROLLER;



CONTROLLER controllers[ADC_MAX] = {0};



void initInput(int which, byte channel, byte controller)

{

controllers[which].midiChannel = channel;

controllers[which].midiController = controller;

controllers[which].minADC = -1;

controllers[which].maxADC = -1;

controllers[which].currentValue = -1;

}



void checkInput(int which)

{

// pointer to the controllers

CONTROLLER *p = &controllers[which];



// read the raw analog value 0-1023

int adc = adcResult[which];



// remember highest and lowest values

if((p->minADC == -1) || (p->minADC > adc))

p->minADC = adc;

if((p->maxADC == -1) || (p->maxADC < adc))

p->maxADC = adc;



// get the range of known readings

int range = p->maxADC - p->minADC;

if(range < 1)

range = 1;



// scale the current value into the range

// NB no floating point support...

int newValue = (127*(adc - p->minADC))/range;



// add the value into the history buffer

long smoothed = 0;

for(int j=0; j<BUFLEN-1;++j)

{

p->history[j] = p->history[j+1];

smoothed += p->history[j];

}

p->history[BUFLEN-1] = newValue;

smoothed += newValue;

smoothed /= BUFLEN;



// has the value changed?

if(smoothed != p->currentValue)

{

sendController(p->midiChannel, p->midiController, smoothed);

p->currentValue= smoothed;

}

}



void main()

{

int i;



// osc control / 8MHz / internal

osccon = 0b01110001;



// timer0... configure source and prescaler

option_reg = 0b10000011;

cmcon0 = 7;



// configure io

trisa = 0b00001010;

trisc = 0b00001111;

ansel = 0b11111100;



// turn on the ADC

adcon1=0b00100000; //fOSC/32

adcon0=0b10000001; // Right justify / Vdd / AD on



// initialise MIDI comms

init_usart();



// Initialise the controllers

initInput(0, 0, 1);

initInput(1, 0, 2);

initInput(2, 0, 4);

initInput(3, 0, 7);

initInput(4, 0, 11);

initInput(5, 0, 74);



adcInitComplete = 0;

for(;;)

{

doADC();

if(adcInitComplete)

{

for(i=0;i<ADC_MAX;++i)

checkInput(i);

adcInitComplete = 0;

delay_ms(20);

}

}

}

Sunday, 21 February 2010

Stylophone MIDI controller

A few months ago I used an Arduino clone board to send MIDI messages out of a Stylophone. I always intended to take it to the next level and get another Stylophone (preferably a broken one) and rip out the guts to fit all the electronics inside, and also add a few buttons and pots for perfomance controllers.

Well, I finally got round to it. This time I am using a PIC16F688 microcontroller.. this little monkey only has 14 pins and costs a mere £1 yet it has a built in clock, serial port and ADC, which means its pretty much the *only* component needed in this project (with the exception of a couple of resistors and switches).

I added a pitchbend pot, a modwheel and a pot to control the note velocity. And pushbuttons to shift octaves and "hold" a MIDI note (basically force the code to forget to send note-off message so the last note rings on after lifting the stylus). This allows a kind of polyphonic drone out of the usually strictly monophonic stylophone.



I will include the code below. I wont bother with a schematic, but the wiring to the PIC16F688 is as follows

pin 1 - 5 volt supply
2 - octave UP momentary switch (other side of switch connected to ground)
3 - octave DOWN momentary switch (other side of switch connected to ground)
6 - to pin 5 of MIDI out socket via a 220R resistor. Pin 4 of the socket is pulled up to 5V via another 220R resitor
7 - wiper of PITCHBEND pot (100k). Pot terminal between from ground/+5V
8 - wiper of VELOCITY pot (100k). Pot terminal between from ground/+5V
9 - to the stylus. Also pulled up to +5v via 470k resistor
10 - activity LED via 1k resistor
11 - wiper of MOD WHEEL pot (100k). Pot terminal between from ground/+5V
13 - HOLD NOTE momentary switch (other side of switch connected to ground)
14 - to ground

If you want to run it from a PP3 you'll need a 5V voltage regulator. You also need to connect the stylophone keyboard/resistor ladder between 0V and 5V and you will need to set up the scale[] array based on the ADC values you get from each pad on *your* stylophone keyboard (which are almost certainly different to mine)

A few photos






// MIDI STYLOPHONE.. PIC16F688.. (c) 2010 hotchk155

// SourceBoost C



// Header files

#include <system.h>

#include <memory.h>



#pragma DATA _CONFIG, _MCLRE_OFF & _WDT_OFF & _INTRC_OSC_NOCLKOUT

#pragma CLOCK_FREQ 8000000

typedef unsigned char byte;



#define MIDI_A 45 // default root note

#define NO_NOTE 0x7f // means stylus "off keyboard"

#define NUM_PADS 20 // number of stylophone pads



#define BUTTON_DEBOUNCE 10 // debounce octave buttons

#define ADC_AQUISITION_DELAY 10 // settling time for ADC

#define PBD_TOL 16 // tolerance applied to pitchbend ADC

#define MOD_TOL 5 // tolerance applied to modulation ADC



// Digital pins

#define P_HEARTBEAT portc.0 // activity LED

#define P_UP porta.5 // octave UP button

#define P_DN porta.4 // octave DOWN button

#define P_HOLD porta.0 // note hold button



// Analog pin mappings

#define ANA_MOD 0b00001000 // AN2 - MOD WHEEL

#define ANA_KBD 0b00010100 // AN5 - KEYBOARD STYLUS

#define ANA_VEL 0b00011100 // AN6 - VELOCITY

#define ANA_PBD 0b00011000 // AN7 - PITCHBEND



// define the four analog inputs

enum {

ADC_KBD,

ADC_VEL,

ADC_MOD,

ADC_PBD,

ADC_MAX

};



// for the state machine which read analog inputs

enum {

ADC_CONNECT,

ADC_ACQUIRE,

ADC_CONVERT

};



// define the ADC readings for each stylophone key pad. This

// is likely to be different if you make your own circuit, so

// you will need to work out your own ADC values

int scale[NUM_PADS+1] = {

0x000,

0x043,

0x081,

0x0b9,

0x0ed,

0x110,

0x149,

0x172,

0x199,

0x1bd,

0x1df,

0x1ff,

0x21c,

0x238,

0x252,

0x26b,

0x281,

0x298,

0x2ab,

0x2c0,

0x3ff

};



// midi note at bottom of scale (can be shifted

// up and down by an octave at a time)

char baseNote = MIDI_A;



// data used by doADC function

byte adcInput[ADC_MAX] = {ANA_KBD, ANA_VEL, ANA_MOD, ANA_PBD};

byte adcInitComplete = 0;

int adcResult[ADC_MAX] = {-1,-1,-1,-1};

int adcIndex = 0;

int adcState = ADC_CONNECT;



////////////////////////////////////////////////////////////////

//

// init_usart

//

// Initialise the PIC16F688 USART (serial port) according to the

// requirements of sending MIDI traffic

//

void init_usart()

{

pir1.1 = 1; //TXIF transmit enable

pie1.1 = 0; //TXIE no interrupts



baudctl.4 = 0; // synchronous bit polarity

baudctl.3 = 1; // enable 16 bit brg

baudctl.1 = 0; // wake up enable off

baudctl.0 = 0; // disable auto baud detect



txsta.6 = 0; // 8 bit transmission

txsta.5 = 1; // transmit enable

txsta.4 = 0; // async mode

txsta.2 = 0; // high baudrate BRGH



rcsta.7 = 1; // serial port enable

rcsta.6 = 0; // 8 bit operation

rcsta.4 = 0; // enable receiver



spbrgh = 0; // brg high byte

spbrg = 15; // brg low byte (31250 baud)

}



////////////////////////////////////////////////////////////////

//

// send

//

// Send a single byte out on the serial port

//

void send(byte c)

{

txreg = c;

while(!txsta.1);

}



////////////////////////////////////////////////////////////////

//

// sendController

//

// Send a MIDI continous controller message

//

void sendController(byte channel, byte controller, byte value)

{

P_HEARTBEAT = 1;

send(0xb0 | channel);

send(controller&0x7f);

send(value&0x7f);

P_HEARTBEAT = 0;

}



////////////////////////////////////////////////////////////////

//

// startNote

//

// Send a MIDI note on message (or note off if 0 velocity)

//

void startNote(byte channel, byte note, byte velocity)

{

P_HEARTBEAT = 1;

send(0x90 | channel);

send(note&0x7f);

send(velocity&0x7f);

P_HEARTBEAT = 0;

}



////////////////////////////////////////////////////////////////

//

// pitchBend

//

// Send a MIDI pitchbend message (14 data bits)

//

void pitchBend(byte channel, int value) {

P_HEARTBEAT = 1;

byte msb = (value>>7)&0x7f;

byte lsb = value&0x7f;

send(0xE0 | channel);

send(lsb);

send(msb);

P_HEARTBEAT = 0;

}



////////////////////////////////////////////////////////////////

//

// doADC

//

// State machine for running the ADC and updating the adcResult

// array with the result from each analog input. This function is

// called periodically and keeps the adcResult[] array updated so

// other code can just check the array rather than making direct

// calls to the ADC

//

void doADC()

{

switch(adcState)

{

// Connect ADC to the correct analog input

case ADC_CONNECT:

adcon0=0b10000001 | adcInput[adcIndex];

tmr0 = 0;

adcState = ADC_ACQUIRE;

// fall through



// Waiting for a delay while the ADC input settles

// - this is neededs or you can get garbage readings

// as the ADC transitions between one input voltage

// and another. The TMR0 (timer 0) register is used for

// timings this

case ADC_ACQUIRE:

if(tmr0 > ADC_AQUISITION_DELAY)

{

// Start the conversion

adcon0.1=1;

adcState = ADC_CONVERT;

}

break;



// Waiting for the conversion to complete

case ADC_CONVERT:

if(!adcon0.1)

{

// store the result. Note that the PIC16F688 has

// a 10 bit ADC so we need to form a 10 bit value

// from ADRESH and ADRESL

adcResult[adcIndex] = (((int)adresh)<<8)|adresl;



// and prepare for the next ADC

if(++adcIndex>=ADC_MAX)

{

adcIndex = 0;



// flag that each ADC has been read at least

// one time, so adcResult now contains valid

// information

adcInitComplete = 1;

}

adcState = ADC_CONNECT;

}

break;

}

}



////////////////////////////////////////////////////////////////

//

// getNote

//

// Map ADC values from the stylus to MIDI note values by

// looking for the scale[] entry which lies closest to the

// input value

//

char getNote(int input)

{

for(int i = 0; i < NUM_PADS; ++i)

{

int lo = 0;

int hi = 0x3ff;

if(i>0)

{

lo = (scale[i-1] + scale[i]) / 2;

}

if(i<NUM_PADS)

{

hi = (scale[i] + scale[i+1]) / 2;

}

if(input >=lo && input <=hi)

{

if(i==NUM_PADS)

return NO_NOTE;

return baseNote+i;

}

}

return NO_NOTE;

}



////////////////////////////////////////////////////////////////

//

// main

//

// Where program starts running!

//

void main()

{

// osc control / 8MHz / internal

osccon = 0b01110001;



// timer0... configure source and prescaler

// port A weak pull ups enabled

option_reg = 0b00000011;



// enable pull ups on each button

wpua = 0b00110001;



// turn off the comparator to allow digital IO on CIO pins

cmcon0 = 7;



// set data direction on each pin

trisa = 0b00110101;

trisc = 0b00001110;



// set up the analog input pins

ansel = 0b11100100;



// turn on the ADC

adcon1=0b00100000; //fOSC/32

adcon0=0b10000001; // Right justify / Vdd / AD on



// start up the serial port

init_usart();



// ensure that the initial aquisition is completed

// for all analog inputs that we're using

adcInitComplete = 0;

while(!adcInitComplete)

doADC();





//char buttons = 0;

char debounce = 0;

char lastNote = NO_NOTE;

int lastPitchBend = -1;

int lastModWheel = -1;

int value;

int diff;

for(;;)

{

// the debounce variable makes sure that user

// has release buttons for a period of time before

// a new press on the button can be registered.

// handles possibility of "switch bounce"



// Buttons are pulled up and touch ground when

// pressed, so the pin reads low when the button

// is being pressed

if(debounce > 0)

{

if(P_UP&&P_DN)

--debounce;



}

else

{

if(!P_UP)

{

// octave shift UP

if(baseNote < 103)

baseNote+=12;

debounce = BUTTON_DEBOUNCE;

}

else if(!P_DN)

{

// octave shift DOWN

if(baseNote > 12)

baseNote-=12;

debounce = BUTTON_DEBOUNCE;

}

}



// poll the ADCs

doADC();



// check for a new note being played

char note = getNote(adcResult[ADC_KBD]);

if(note != lastNote)

{

// do we need to kill the previous note?

if(lastNote != NO_NOTE && P_HOLD)

{

// make it so!

startNote(0,lastNote,0);

}

// is a new note playing (rather than stylus

// removed from keyboard?)

if(note != NO_NOTE)

{

// play a note with appropriate velocity

char velocity = (adcResult[ADC_VEL]>>3)&0x7f;

startNote(0,note,velocity);

}

lastNote = note;

}



// check for change in pitchbend which is

// outside the "noise" tolerance

value = adcResult[ADC_PBD];

diff = value - lastPitchBend;

if(diff*diff > (PBD_TOL*PBD_TOL))

{

// Send MIDI pitchbend.. this has a 14-bit

// data value

pitchBend(0, value<<4);

lastPitchBend = value;

}



// check for change in modwheel which is

// outside the "noise" tolerance

value = adcResult[ADC_MOD]>>3;

diff = value - lastModWheel;

if(diff*diff > (MOD_TOL*MOD_TOL))

{

// Send MIDI continuous controller message

// for controller #1 (mod wheel) which has

// 7 bit data value

sendController(0, 1, value);

lastModWheel = value;

}

}

}