Midi 2
Volume Number: | | 1
|
Issue Number: | | 12
|
Column Tag: | | Sound Lab
|
The Midi Connection, Part II
By Kirk Austin, San Anselmo, CA.
As you may recall from last issue all we need to get MIDI up and running on the Macintosh now that we have the necessary hardware are the driver routines for the serial ports. Fear not fellow coders, the coveted routines have arrived.
The normal device driver model for the Macintosh has too much overhead associated with it to make it approriate for use with MIDI which transfers data at a rate of 31.25K bits per second. Usually, real time applications need all the extra time they can get. This is particularly true of sequencer type applications that do MIDI "multi-track recording" while simultaneously maintaining a graphics display of some sort. routines that directly access the 8530 SCC chip have been utilized in order to minimize the time taken up by the serial I/O. I have tried to make things as easy as possible by providing "building block" style routines that follow the guidelines of the Lisa Pascal interface. This is the reason for the LINK and UNLNK instructions which aren't really necessary otherwise. You can basically treat RxMIDI as a Pascal function that accepts no arguments and returns a word of data as a result that is either a valid MIDI byte or else a flag indicating that no MIDI data is available. TxMIDI can be treated as a Pascal procedure that accepts a word of data containing the MIDI byte to be transmitted as an argument. Both of these routines are stack based.
Since these routines include interrupt handlers with pointers placed in low memory the routines cannot be in a relocatable block. One easy way of insuring this is to place them in your first code segment. If your code only consists of one segment you don't have anything to worry about, but if there is more than one segment the memory manager may move things around on you when you don't expect it. If your interrupt handlers are in one of these relocatable segments the pointers to them can be invalidated which would cause the program to crash.
Okay, let's get down to it. First the SCC chip has to be initialized by calling either SCCinitA or SCCinitB (this should be done when your application initializes quickdraw and the various managers). The initialization ends up being quite a bit of code actually, and if not done properly will mess things up but good! The 8530 can only be accessed at a maximum rate of every 2.2 microseconds. I know, this sounds unbelievable for a sophisticated piece of modern hardware, but it's true. This is the reason for all of the MOVE.L (SP),(SP) instructions. They don't accomplish anything, but they take a little more than a couple of microseconds to execute which is just the amount of delay that we need (silly huh?). As I mentioned in the article on the hardware interface the SCC chip can accept three different external clock frequencys to produce the desired baud rate. The appropriate divisor must be selected in the initialize routine. By the way, these routines are written so you can use either the modem port or the printer port, but if you want to use both simultaneously and keep the ports completely independant you will have to duplicate everything for each port. If you are writing an application of that complexity I think you can handle rewriting these routines.
The transmit and receive routines each maintain circular queues of outgoing and incoming data respectively. These queues can be of any length, but I have arbitrarily set them at $100 bytes each. To change the size of the queues just change the values in the equate table. There is no error detection code for a queue overrun condition so you will have to make sure that your application runs fast enough to avoid this. If an overrun condition occurs you will lose data. If you want to dump huge files all at once (if you are writing a patch librarian for example) just make the queue big enough to handle the entire file and you can do it without worrying about overruns.
The TxMIDI routine is the most complicated one, so let's have a look at it. When this routine receives a byte to transmit (in the lower byte of the word left on the stack by the calling routine) it first must check the queue to see if it is empty or not. If the queue is not empty the byte is simply added to the queue. If the queue is empty the routine checks the SCC chip to see if its transmit buffer is empty. If the transmit buffer is not empty the byte is just added to the queue. But, if the transmit buffer is empty the routine must write the byte to the SCC chip to transmit it.
The RxMIDI routine is more straightforward. It checks to see if there is any data in the queue, and if there is it returns it on the stack (the space for the result must be allocated by the calling routine). If there is no data available the routine returns $FFFF as its result.
The interrupt handlers do exactly what you might expect them to. When a byte is received by the SCC chip a receive interrupt is generated which calls the RxIntHand routine. This routine simply takes the byte from the SCC register and places it in the receive queue. The TxIntHand routine is called when the SCC chip's transmit buffer is empty. It takes a byte from the transmit queue if one is available and writes it to the SCC register. If no data is available it clears the interrupt and returns. One thing about the interrupt handlers that was not obvious to me when I was first coding them was the fact that register A5 must be saved at the beginning of the routines and its value loaded from the system variable CurrentA5 in order to insure that it is pointing to the application's globals area.
Only one thing left to do, and that is reset the 8530 before you quit the application or try to print (if you are using the printer port for MIDI too). Just call SCCResetA or SCCResetB and your application will exit gracefully.
; MIDI PORT ROUTINES
; copyright Kirk Austin 1985
; Transmit and Receive queue length equates
TxQSize EQU $100
RxQSize EQU $100
; Serial Chip Addresses, offsets, and system equates
sccRBaseEQU $9FFFF8
sccWBaseEQU $BFFFF9
Lvl2DT EQU $1B2
aData EQU 6
aCtl EQU 2
bData EQU 4
bCtl EQU 0
TBEEQU 2
CurrentA5 EQU $904
; This is an example of how to use the routines.
; If this were placed in your event loop your application would
; receive MIDI data and echo it back out.
;
;MIDIThru
;CLR -(SP) ; clear space for result
;BSR RxMIDI ; fetch data
;MOVE (SP)+,D0
;CMPI #$FFFF,D0; any bytes available?
;BEQ NoMIDI ; if not, exit
;MOVE D0,-(SP) ; if so, transmit them
;BSR TxMIDI
;BRA MIDIThru ; check for more
;NoMIDI
;
; -------------------------------------------------------------------------
; This section contains the necessary routines for MIDI
;
; These are the initialization routines which should be called
; when you initialize quickdraw and the various managers.
; Call SCCInitA to use the modem port, and SCCInitB to use
; the printer port.
SCCInitA
MOVE #aCtl,CtlOffset(A5) ; set up globals for Chn A
MOVE #aData,DataOffset(A5)
MOVE.B #%10000000,ChnReset(A5)
MOVE #24,RxIntOffset(A5)
MOVE #16,TxIntOffset(A5)
MOVE #28,SpecRecCond(A5)
BRA SCCInit
SCCInitB
MOVE #bCtl,CtlOffset(A5) ; set up globals for Chn B
MOVE #bData,DataOffset(A5)
MOVE.B #%01000000,ChnReset(A5)
MOVE #8,RxIntOffset(A5)
MOVE #0,TxIntOffset(A5)
MOVE #12,SpecRecCond(A5)
SCCInit
MOVE SR,-(SP) ; Save interrupts
MOVEM.LD0/A0-A1,-(SP) ; Save registers
ORI #$0300,SR; Disable interrupts
MOVE.L #sccRBase,A1 ; Get base Read address
ADD CtlOffset(A5),A1 ; Add offset for control
MOVE.B (A1),D0 ; Dummy read
MOVE.L (SP),(SP); Delay
MOVE.L #sccWBase,A0 ; Get base Write address
ADD CtlOffset(A5),A0 ; Add offset for control
MOVE.B #9,(A0) ; pointer for SCC reg 9
MOVE.L (SP),(SP); Delay
MOVE.B ChnReset(A5),(A0) ; Reset channel
MOVE.L (SP),(SP); Delay
MOVE.B #4,(A0) ; pointer for SCC reg 4
MOVE.L (SP),(SP); Delay
; This is where you determine the external clock rate
; %01000100 = 500K
; %10000100 = 1 Meg
; %11000100 = 2 Meg
MOVE.B #%01000100,(A0) ; 16x clock, 1 stop bit
MOVE.L (SP),(SP); Delay
MOVE.B #1,(A0) ; pointer for SCC reg 1
MOVE.L (SP),(SP); Delay
MOVE.B #%00000000,(A0) ; No W/Req
MOVE.L (SP),(SP); Delay
MOVE.B #3,(A0) ; pointer for SCC reg 3
MOVE.L (SP),(SP); Delay
MOVE.B #%00000000,(A0) ; Turn off Rx
MOVE.L (SP),(SP); Delay
MOVE.B #5,(A0) ; pointer for SCC reg 5
MOVE.L (SP),(SP); Delay
MOVE.B #%00000000,(A0) ; Turn off Tx
MOVE.L (SP),(SP); Delay
MOVE.B #11,(A0) ; pointer for SCC reg 11
MOVE.L (SP),(SP); Delay
MOVE.B #%00101000,(A0) ; Make TRxC clock sourc
MOVE.L (SP),(SP); Delay
MOVE.B #14,(A0) ; pointer for SCC reg 14
MOVE.L (SP),(SP); Delay
MOVE.B #%00000000,(A0) ; Disable BRGen
MOVE.L (SP),(SP); Delay
MOVE.B #3,(A0) ; pointer for SCC reg 3
MOVE.L (SP),(SP); Delay
MOVE.B #%11000001,(A0) ; Enable Rx
MOVE.L (SP),(SP); Delay
MOVE.B #5,(A0) ; pointer for SCC reg 5
MOVE.L (SP),(SP); Delay
MOVE.B #%01101010,(A0) ; Enable Tx and drivers
MOVE.L (SP),(SP); Delay
MOVE.B #15,(A0) ; pointer for SCC reg 15
MOVE.L (SP),(SP); Delay
MOVE.B #%00001000,(A0) ; Enable DCD int for
; mouse
MOVE.L (SP),(SP); Delay
MOVE.B #0,(A0) ; pointer for SCC reg 0
MOVE.L (SP),(SP); Delay
MOVE.B #%00010000,(A0) ; Reset EXT/STATUS
MOVE.L (SP),(SP); Delay
MOVE.B #0,(A0) ; pointer for SCC reg 0
MOVE.L (SP),(SP); Delay
MOVE.B #%00010000,(A0) ; Reset EXT/STATUS
MOVE.L (SP),(SP); Delay
MOVE.B #1,(A0) ; pointer for SCC reg 1
MOVE.L (SP),(SP); Delay
MOVE.B #%00010011,(A0) ; Enable interrupts
MOVE.L (SP),(SP); Delay
MOVE.B #9,(A0) ; pointer for SCC reg 9
MOVE.L (SP),(SP); Delay
MOVE.B #%00001010,(A0) ; Set master int enable
MOVE.L (SP),(SP); Delay
MOVE.L #Lvl2DT,A0 ; get dispatch table
; pointer
MOVE RxIntOffset(A5),D0; get offset to Rx vector
LEA RxIntHand,A1 ; set Rx vector
MOVE.L A1,0(A0,D0)
MOVE TxIntOffset(A5),D0; get offset to Tx vector
LEA TxIntHand,A1 ; set Tx vector
MOVE.L A1,0(A0,D0)
MOVE SpecRecCond(A5),D0; get offset to
; Special vector
LEA Stub,A1
MOVE.L A1,0(A0,D0)
CLR RxByteIn(A5) ; init flags & pointers
CLR RxByteOut(A5)
MOVE.B #$FF,RxQEmpty(A5)
CLR TxByteIn(A5)
CLR TxByteOut(A5)
MOVE.B #$FF,TxQEmpty(A5)
MOVEM.L(SP)+,D0/A0-A1 ; Restore registers
MOVE (SP)+,SR ; Restore interrupts
RTS ; and return
; This is the routine to transmit a MIDI byte of data. To use this
; place the byte to be transmitted as the lower 8 bits of a word
; routine on the stack, then BSR to TxMIDI.
TxMIDI
LINK A6,#0 ; set frame pointer
MOVE SR,-(SP) ; Save interrupts
MOVEM.LD0/A0-A2,-(SP) ; Save registers
ORI #$0300,SR; Disable interrupts
TST.B TxQEmpty(A5) ; is TxQueue empty?
BNE TxQE; if so branch
MOVE TxByteIn(A5),D0 ; if not add byte to queue
LEA TxQueue(A5),A2 ; point to queue
MOVE.B 9(A6),0(A2,D0) ; place byte in queue
ADDQ #1,D0 ; update TxByteIn
CMP #TxQSize,D0
BNE @1
MOVE #0,D0
@1 MOVE D0,TxByteIn(A5)
BRA TxExit ; and exit
TxQE
MOVE.L #sccRbase,A0 ; get SCC Read Address
MOVE.L #sccWbase,A1 ; get SCC Write address
MOVE CtlOffset(A5),D0 ; get index for Ctl
BTST.B #TBE,0(A0,D0); transmit buffer empty?
BNE FirstByte; if so branch
MOVE TxByteIn(A5),D0 ; if not add to queue
LEA TxQueue(A5),A2 ; point to queue
MOVE.B 9(A6),0(A2,D0) ; place byte in queue
ADDQ #1,D0 ; update index
CMP #TxQSize,D0
BNE @1
MOVE #0,D0
@1 MOVED0,TxByteIn(A5)
MOVE.B #0,TxQEmpty(A5) ; reset queue empty flag
BRA TxExit ; and exit
FirstByte
MOVE DataOffset(A5),D0 ; get index to data
MOVE.L (SP),(SP); delay
MOVE.B 9(A6),0(A1,D0) ; write data to SCC
MOVE.L (SP),(SP); Delay
TxExit
MOVEM.L(SP)+,D0/A0-A2 ; Restore registers
MOVE (SP)+,SR ; Restore interrupts
UNLK A6; release frame pointer
MOVE.L (SP)+,A1 ; save return address
ADD.L #2,SP ; move past data word
MOVE.L A1,-(SP) ; put address back on stack
RTS ; and return
; This is the routine to receive a byte of MIDI data. To use this
; routine treat it like a Pascal function. Leave space on the
; stack for a word of data before BSR'ing to this routine. If the
; routine executes is $FFFF there was no MIDI data available.
; If the upper byte is clear then a valid MIDI byte is in the lower
; 8 bits.
RxMIDI
LINK A6,#0 ; set frame pointer
MOVE SR,-(SP) ; Save interrupts
MOVEM.LD0-D1/A0-A2,-(SP) ; Save registers
ORI #$0300,SR; disable interrupts
TST.B RxQEmpty(A5) ; any data available?
BEQ @1; if so, branch
MOVE #$FFFF,8(A6) ; if not, return with $FFFF
BRA RxExit
@1 MOVERxByteOut(A5),D0 ; get index to byte out
LEA RxQueue(A5),A2 ; point to queue
MOVE.L #0,D1 ; clear data register
MOVE.B 0(A2,D0),D1; get MIDI data
MOVE D1,8(A6) ; place it on stack for return
ADDQ #1,D0 ; update index
CMP #RxQSize,D0
BNE @2
MOVE #0,D0
@2 MOVE D0,RxByteOut(A5)
MOVE RxByteIn(A5),D1
CMP D0,D1 ; is queue empty?
BNE RxExit ; if not exit
MOVE.B #$FF,RxQEmpty(A5) ; if empty, set flag
RxExit
MOVEM.L(SP)+,D0-D1/A0-A2 ; Restore registers
MOVE (SP)+,SR ; restore interrupts
UNLK A6
RTS ; and return
; This is the interrupt routine for receiving a byte of MIDI data.
; It places the received byte in a circular queue to be
; accessed later by the application.
RxIntHand
ORI #$0300,SR; disable interrupts
MOVEM.LD0-D1/A0-A2/A5,-(SP); save registers
MOVE.L CurrentA5,A5 ; make sure A5 is correct
MOVE.L #sccRBase,A0 ; get SCC address
MOVE.L #sccWBase,A1
MOVE DataOffset(A5),D0 ; get data offset
MOVE.B 0(A0,D0),D1; read data from SCC
MOVE.L (SP),(SP); Delay
LEA RxQueue(A5),A2 ; point to queue
MOVE RxByteIn(A5),D0 ; get offset to next cell
MOVE.B D1,0(A2,D0); put byte in queue
MOVE.B #0,RxQEmpty(A5) ; reset queue empty flag
ADDQ #1,D0 ; update index
CMP #RxQSize,D0
BNE @1
MOVE #0,D0
@1 MOVE D0,RxByteIn(A5)
MOVEM.L(SP)+,D0-D1/A0-A2/A5; restore registers
ANDI #$F8FF,SR; enable interrupts
RTS ; and return
; This is the interrupt routine for transmitting a byte of MIDI
; data. It checks to see if there is any data to send. If there is
; it sends it to the SCC. If there isn't it resets the TBE interrupt
; in the SCC and exits.
TxIntHand
ORI #$0300,SR; disable interrupts
MOVEM.LD0-D1/A0-A2/A5,-(SP); save registers
MOVE.L CurrentA5,A5
MOVE.L #sccRBase,A0 ; get SCC address
MOVE.L #sccWBase,A1
TST.B TxQEmpty(A5) ; Is queue empty?
BEQ @1; if not branch
MOVE CtlOffset(A5),D0 ; get offset for control
MOVE.B #$28,0(A1,D0); if so, reset TBE
; interrupt
MOVE.L (SP),(SP); Delay
BRA TxIExit ; and exit
@1 MOVETxByteOut(A5),D0 ; get index to next data
; byte
LEA TxQueue(A5),A2 ; point to queue
MOVE DataOffset(A5),D1 ; get data offset
MOVE.B 0(A2,D0),0(A1,D1) ; write data to SCC
MOVE.L (SP),(SP); Delay
ADDQ #1,D0 ; update index
CMP #TxQSize,D0
BNE @2
MOVE #0,D0
@2 MOVE D0,TxByteOut(A5)
MOVE TxByteIn(A5),D1
CMP D0,D1 ; is TxQueue empty?
BNE TxIExit ; if not exit
MOVE.B #$FF,TxQEmpty(A5) ; if empty set flag
TxIExit
MOVEM.L(SP)+,D0-D1/A0-A2/A5; restore registers
ANDI #$F8FF,SR; enable interrupts
RTS ; and return
; This routine must be called when the application quits or the
; system will crash due to the interrupt handling pointers
; becoming invalid.
SCCResetA
MOVE #aCtl,CtlOffset(A5) ; set up globals for Chn A
MOVE #aData,DataOffset(A5)
MOVE.B #%10000000,ChnReset(A5)
MOVE #24,RxIntOffset(A5)
MOVE #16,TxIntOffset(A5)
MOVE #28,SpecRecCond(A5)
BRA SCCReset
SCCResetB
MOVE #bCtl,CtlOffset(A5) ; set up globals for Chn B
MOVE #bData,DataOffset(A5)
MOVE.B #%01000000,ChnReset(A5)
MOVE #8,RxIntOffset(A5)
MOVE #0,TxIntOffset(A5)
MOVE #12,SpecRecCond(A5)
SCCReset
MOVE SR,-(SP) ; Save interrupts
MOVE.L A0,-(SP) ; Save register
ORI #$0300,SR; Disable interrupts
MOVE.L #sccWBase,A0 ; Get base Write address
ADD CtlOffset(A5),A0 ; Add offset for control
MOVE.B #9,(A0) ; pointer for SCC reg 9
MOVE.L (SP),(SP); Delay
MOVE.B ChnReset(A5),(A0) ; Reset channel
MOVE.L (SP),(SP); Delay
MOVE.B #15,(A0) ; pointer for SCC reg 15
MOVE.L (SP),(SP); Delay
MOVE.B #%00001000,(A0) ; Enable DCD int
MOVE.L (SP),(SP); Delay
MOVE.B #0,(A0) ; pointer for SCC reg 0
MOVE.L (SP),(SP); Delay
MOVE.B #%00010000,(A0) ; Reset EXT/STATUS
MOVE.L (SP),(SP); Delay
MOVE.B #0,(A0) ; pointer for SCC reg 0
MOVE.L (SP),(SP); Delay
MOVE.B #%00010000,(A0) ; Reset EXT/STATUS
MOVE.L (SP),(SP); Delay
MOVE.B #1,(A0) ; pointer for SCC reg 1
MOVE.L (SP),(SP); Delay
MOVE.B #%00000001,(A0) ; Enable mouse
; interrupts
MOVE.L (SP),(SP); Delay
MOVE.B #9,(A0) ; pointer for SCC reg 9
MOVE.L (SP),(SP); Delay
MOVE.B #%00001010,(A0) ; Set master int enable
MOVE.L (SP),(SP); Delay
MOVE.L (SP)+,A0 ; Restore register
MOVE (SP)+,SR ; Restore interrupts
RTS ; and return
; this is the space for a special condition interrupt routine
Stub
RTS
;-------------------------------MIDI Globals--------------------------------
CtlOffset DS.W 1 ; offset for channel control
DataOffsetDS.W 1 ; offset for channel data
ChnResetDS.B1 ; SCC channel reset select
RxIntOffset DS.W 1 ; offset for dispatch table
TxIntOffset DS.W 1 ; offset for dispatch table
SpecRecCond DS.W 1 ; offset for dispatch table
TxQueue DS.BTxQSize; transmitted data queue
TxQEmptyDS.B1 ; Transmit queue empty flag
TxByteInDS.W1 ; index to next cell in
TxByteOut DS.W 1 ; index to next cell out
RxQueue DS.BRxQSize; received data queue
RxQEmptyDS.B1 ; receive queue empty flag
RxByteInDS.W1 ; index to next cell in
RxByteOut DS.W 1 ; index to next cell out
End