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);

}

}

No comments:

Post a Comment