|
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
- SPREAD THE WORD:
- Slashdot
- Digg
- Del.icio.us
- Newsvine