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
Tuesday, 23 March 2010
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);
}
}
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;
}
}
}
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();
}
}
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();
}
}
Subscribe to:
Posts (Atom)