MIDI 2 CoCo Converter

This is a program that converts MIDI files and outputs the converted music in one of three formats for the CoCo to playback.  It outputs the music data alone or along with the player for the given format.  The output formats that are supported are:

  • Simon Jonasson’s – DIPOLE.ASM software music player
  • John Linville’s – Game Master Cartridge support with my software player
  • Ed Snider’s – CoCo Programable Sound Generator with my software player

The program started out as a simple command line based tool that would take MIDI (.mid) files and output them in a format that would playback on Simon’s dipole.asm software music player.  I had the basic MIDI converter working where it would just describe the instrument and ask you what two instruments you wanted to convert.  This really meant that in order to use the tool you would have to prepare your own MIDI file removing tracks you don’t want, (which might still be necessary for complicated songs.)  This would be OK for a quick and dirty converter but I figured if I’m going to share this with others that I should make it a little more user friendly.

I then decided that I would show the user all the tracks and let them pick the instruments from a GUI.  This really works well since it shows you what instruments are used for the different parts of the song.  If any one instrument is used for only part of the song then you could see it and actually select a few instruments as long as they didn’t play notes at the same time.  You couldn’t figure this out without looking at the song with the separate track all laid out on the screen.

Once I got the GUI working and was able to convert the MIDI files to Simon’s Dipole.asm format I realized I could do a similar thing for John Linville’s Game Master Cartridge.  Since MAME supports the cartridge it allowed me to experiment and learn how to program the Texas Instruments SN76489 sound chip that is used inside John’s cartridges.  I was just about finished working on my own GMC format player when Ed Snider posted on Facebook that he was ready to send out some CoCo Programmable Sound Generator cartridges.  So I bought one off Ed and continued to finish up the GMC format and add in my MIDI to GMC format player code.  Since Ed’s cartridge is more of a dedicated sound card for the CoCo that would stay in your Mult-Pak Interface I figure it would be helpful to BASIC programmers if I made my MIDI 2 CoCo converter output the music and player in a way that anyone could easily use it to add background music in their programs.

Here is what the MIDI to CoCo converter can do, it converts MIDI files to:

  • Simon’s dipole player format along with the player all ready to have an assembly programmer add their code to the end of the program and it will automatically include the music in the background.
  • Simon’s dipole player format, just the music data in assembly FCB format which you can add to your own assembly program and add Simon’s dipole.asm music playing code in your own program.
  • John Linville’s GMC playable format along with my own player in a format that can be used in creating your own game cartridge to easily add music to your game.  ready to be tested using MAME
  • John Linville’s GMC compatible format assembly FCB format file to be used with my GMCPLAY.asm program to be used in your own game cartridge code
  • Ed Snider’s CoCo PSG playable format with my own player that can be easily added as background music to your BASIC program.  The sound data is stored on Ed’s CoCo PSG 512k RAM leaving lots of room for your BASIC program.
  • Ed Snider’s CoCo PSG playable format with my stand alone PSGPLAYER.asm that can be used as part of your own assembly program/game and easily allow you to add background music to your assembly program.

Where do I download the program?

You can get the program and the support files on the Tandy Forum in the Software Project section and soon the TRS-80 Color Computer Archive

Need to Install QB64 on your modern computer

The converter is written in QB64 which is BASIC for modern PC’s.  You can use it on Mac, Linux and Windows.  It takes your BASIC program and in the background turns it into C++ code which is then compiled on the host computer and outputs a completely independent program that is ready to be executed on that host computer.  It doesn’t need any other support files or libraries it is ready to be executed as it is and since it is run on a modern computer it runs super fast!  First thing you need to do is install QB64 on your modern computer.

Once installed you take my basic program (mid2coco.bas) and load it into QB64.  Then you  click on Run | Save EXE in the source folder.  Finally click on Run | Make Executable Only.  From that point on you have a stand alone program that can be moved and used just as any other program on your computer.

Make sure the mid2coco executable file is in the mid2coco folder.  If you want to use the mid2coco program in a new folder you need to have the mid2coco program along with the following files that are required for certain output modes.  You need to have GMC_Play.asm, Dipole_Part1.asm and Dipole_Part2.asm along with the MIDI files that you want to convert in that new folder.  You run the program from the command line.  Change your directory so you are in the directory with the new mid2coco executable that you just made and use this command:

mid2coco MIDIFILE.mid

For example to convert a MIDI file called Axelf.mid:

./mid2coco Axelf.mid

or

mid2coco.exe Axelf.mid

Then the program asks you what output format you would like.  Select the output based on the output format you require as per the picture below:

mid2cc_screen1

Select the output format you want (1 to 6).  Also you can add the letter p after the number to have the program output the music data for the proper playback speed for PAL computers (for example 5p) otherwise it will default to using NTSC timing.  Note the PAL timing hasn’t been tested.

Next you are asked if you want to transpose the MIDI file one octave higher, since some older MIDI files were created this way.  Most of the time you can just hit enter as the default is No.

Next it asks you what type of CoCo you will be using the program on.  This really doesn’t matter except if you are using Ed Snider’s CoCo PSG from BASIC where it will give you some instructions of using the loader and player in BASIC, and what the addresses are to start, stop and continue playback.

Next you are shown a list of the tracks and the instrument names for each track that is in the MIDI file.

If you selected Simon’s dipole.asm format then you can only choose two voices.  If you choose John Linville’s GMC format you can choose three voices for tones and the fourth only for the drum track.  If you selected Ed Snider’s CoCo PSG then you can choose from three voices for tones or use voice three as a drum track.  If you want to use voice 3 for drums then make sure click on the boxes with the number 4 and no boxes with the number 3.

Since we are limited to individual notes per voice there is one other thing to note about track selection.  During the MIDI conversion of chords the program always selects the highest note of the chord as the note to convert unless you select more then one voice for the same track.  The program works like this…  If you want to play notes of a chord from your MIDI file select voice 1 and 2 for the same track.  The program will then use voice 1 as the high note of the chord and voice 2 as the lowest note in the chord.  If you select voice 3 in the same track with voice 1 and voice 2 then voice 3 will play the 2nd highest note.

As for picking which tracks are played by which instrument you use the mouse and left click on the little box with the number of the voice you want to use for that track.  Use a right click to cancel the voice selection.  There are many songs that have many instruments but not all of the instruments are playing all the time.  For example the picture below is the GUI output for the file Axelf.mid using Simon’s dipole format which only allows you to choose two voices.

Screen Shot 2017-12-12 at 9.13.34 PM

You can see that only tracks 1 and tracks 2 are used throughout the entire song.  So you could select voice 1 for track 2 (slap Bass 2) and you can select voice 2 for track 3, track 4, track 5 and track 7 since none of these tracks play notes at the same time.  The instruments will all sound the same but at least you will get more of the original notes that are in the song and it’s usually good enough for background music for your game.

See the picture below with the little coloured boxes that indicated which voice I’ve selected for each track.  Sometimes the MIDI converter is unsure if a track is a drum track or not and will display “??? Instrument or Drums” it doesn’t display a box with the number 4 beside it but if you place your mouse pointer to where the box would be you can click on the imaginary box and it will turn white to indicate that the converter will use this voice as drums using the selected track.

Screen Shot 2017-12-12 at 9.20.04 PM

Once you’v selected your tracks, then press the Space Bar or Enter to start the conversion of the tracks and voices you’ve selected.

You will then be shown a little summary screen of what the program did during the conversion and will tell you a little bit more about the song.  It will tell you the length of the song and the size of the data that is needed to play the song.  It will also give you some information on how you can use the output format that you’ve selected.  Press any key at this point to close the mid2coco program.  Then you will either have to use the output in your own assembly program or if you are using Ed Snider’s CoCo PSG you can use it from BASIC, or in your own assembly language programs.  I’m not going to go into details here for using each format.  But if you play with the program and look at the output files it produces I’m sure you will understand how to use them for your own projects.

mid2cc_screenDone

CoCo PSG background music from BASIC

If you have Ed Snider’s CoCo PSG you can use option 5 to convert the MIDI file so it will play in the background of your BASIC program.  Then follow the instructions as described above to convert the MIDI file to a file called PSGLOADR.BIN.  All that is needed is to copy the file generated called “PSGLOADR.BIN” to the floppy disk or .dsk image if using CoCoSDC.  The PSGLOADR.BIN is a loader program that will copy the music data which is also in the PSGLOADR.BIN file and copy it to the RAM of the CoCo PSG.  So your music data won’t take any RAM from your CoCo.  The PSGPLY16.BIN or PSGPLY32.BIN programs which are the actual playback programs will require about 1150 bytes of RAM from BASIC and these programs need to be on your disk.  You actually need only the one program depending on the amount of RAM in your CoCo.  If you have 16k then you only need the PSGPLY16.BIN program on the floppy disk.  If you have 32k or more, even a CoCo 3 then you need to make sure the PSGPLY32.BIN program is on the floppy.  The last thing you need to do is add a few lines of BASIC code to your BASIC program.  The converter will let you know what these lines of BASIC code are depending on the amount of RAM the CoCo target system has.  The converter will also let you know the EXEC commands to start, stop and continue the music playback in your BASIC program.  The last thing it will let you know is the POKE address you use to set the number of times the music will repeat (default is continue looping forever).  All the programs required are included in the .dsk image and as stand alone files in the mid2coco.zip package.

Take a look at the program listing for the included programs PSGPLY16.BAS or PSGPLY32.BAS to see how to load and start the music.  It also shows the EXEC addresses to stop and continue playback.

Some other things to note about the conversion and using the music in your own programs.  The mid2coco converter takes velocity into account but Simon’s player always plays notes at the same volume level.  The other two formats support velocity and the program does output the notes and their volume level according to the info in the MIDI file.

Also note – Since the GMC and CoCo PSG are cartridges that produce sound the mixer inside the CoCo must be set to output sound from the cartridge port to the speaker on your TV.  Once the mixer is set as the cartridge as it’s input you can no longer use the CoCo DAC for sound effects.  You will have to use the sound chip on the cartridge for sound effects in your game.  Or temporarily turn off the cartridge mixer and select the CoCo DAC then switch back to the cartridge to play the music again.  If you are using the CoCo PSG from BASIC with my converted player and you get an error then the CoCo will set the audio mixer to the internal DAC even though the music will still be playing in the background and you wont hear it.  The easiest way to get the sound set back to the CoCo PSG is to EXECute the stop command then EXECute the Continue command (shown in the example PSGPLY16.BAS or PSGPLY32.BAS files).  Also note if you EXECute the start address twice it will crash the CoCo since the start EXEC actually sets up to intercept the BASIC IRQ routine and if you do it a second time it will intercept it’s own routine and that is bad.

Also a note about the GMC playback routine, the assembly code copies the playback routine from the cartridge to RAM at address $7000 as a default.  If you are going to use this code with a real game cartridge you should probably change this address to something that makes more sense for your program.

Have fun…

Glen

If you want to get a Game Master Cartridge reach out to John Linville here

If you want to get a CoCo PSG visit Ed Snider’s webpage here

CoCo PSG player format description

* 5 Byte header:
* Byte  Value
* 00-02 'PSG' - Header Identifier
* 03    $xx - File version # currently only supports a value of $01 for version 1.00
* 04    $xx - Number of times to loop this song where xx is:
*       $00 - Don't change the number of loops value
*       $01 to $FE - loop this number of times
*       $FF - loop forever
* All bytes at this point are data bytes that are encoded as per the info below.
*
* First the descriptor byte
*  Left nibble  = # of vsyncs/2 to count until next change in tone or velocity is done
*  $0x to $Fx   = 0 to 15 delay count before next note or velocity change, add one to get real value of 1 to 16
*
*  Right nibble = # of bytes to copy (1-11) count of bytes to copy max needed in current format is 3x3 = 9 bytes = (2 tone values, 1 velocity value for each of the 3 voices)
*  or if the value of the right nibble is higher then 11 or $0B then do the following
*  $x0          = 00 unused
*  $xC          = 12 signals the repeat count is larger then 16 so use the next byte as the repeat count (left nibble = bytes to copy)
*  $xD          = 13 signals the repeat count is larger then 256 so use the next 2 bytes as the repeat count (left nibble = bytes to copy)
*  $xE          = 14 Flip Channel C from tones to noise or drum track or back to tones
*  $xF          = 15 signals End of Music data
*
* Next process the data bytes
*
* My PSG playback format uses the following format for the data bytes
*
*          ,------------- 0=Note, 1=Volume or channel C Noise/Drum frequency
*          |      ,,----- Channel  00=chan A, 01=chan B, 10=chan C, 11=chan C - Drum/Noise info
*          |      ||
*          |Xxx xx||  x=volume or msb of 12 bit tone info or Xxxxx = 5 bit noise/drum frequency for channel C
*          1000 0001
* If it is a Note then it has the 4 msb of the 12 bit tone and the next byte has the 8 lsb's of the 12 bit tone value (so two bytes are always needed for tone data)
* Done, the next byte is the next encoded byte...

Game Master Cartridge Format Description

There is no header to the data stream since it will be played back from the cartridge directly.  The encoding for the music data is:

* Encoding byte
*                left nibble = # of times to repeat          0 to 15 repeat count
*
*                right nibble = # of bytes to copy (1-11)    count of bytes to copy
*                                                            12 signals the repeat count is larger then 15 so use the next byte as the repeat count     (left nibble = bytes to copy)
*                                                            13 signals the repeat count is larger then 255 so use the next 2 bytes as the repeat count (left nibble = bytes to copy)
*                                                            14 signals jump to the next Bank of the GMC (reached the 16k length of the current bank)   (ignore left nibble)
*                                                            15 signals End of File

 

Advertisements
Posted in CoCo Programming | Leave a comment

How to setup and use IRQs on the TRS-80 Color Computer – Part 5 – Advanced interrupt settings and using CoCo 3 specific interrupts

Table of Contents (click on a link to jump to that web page)

In this section I will be going over some pretty advanced techniques to speed up your interrupt request routine so it has minimal effect on the rest of your program.

First trick is to setup an FIRQ that is in Direct Page space.  This will save a couple CPU Cycles every time the FIRQ is triggered.

Example:

The DP is set to $3E and the FIRQ routine starts in RAM at address $3ECA

 LDA    #$0E  * Write the direct page JMP instruction
 LDB    #$CA  * Will jump to address DP + $CA = $3ECA in our example
 STD    $010F * CoCo 1 & 2 FIRQ Jump pointer

Simon Jonassen told me about trick that you can use with the jump location in a CoCo 1 & 2.  You can use the  FIRQ jump Vector location as the actual interrupt routine so there is no jump required the code is executed at the FIRQ vector.  To use this you would just put your entire FIRQ routine starting at $010F.  That way you save the few extra cycles that would normally be used with the JMP $xxxx instruction.  If your program doesn’t use the FIRQ you can do the same thing with the IRQ but your interrupt routine would start at $010c.  – Thanks Simon


I’m going to show you some very optimized 6809 code that will be used for a CoCo 3 only audio sample player.  This code is not CoCo 1 or 2 compatible.

In this example the audio data has been loaded in RAM starting at address $3D59 and ending at address $7FFF and this code will playback the sample looping it over and over.

To minimize the CPU cycles this interrupt routine uses some self modifying code.

        ORG     $C000           * Address of the Direct Page and FIRQ
DirectPage:
        SETDP   DirectPage/256  * Setup the DP addressing for the assembler
*****************************************
* Play Sample in the background in mem $3D59-$7FFF, Loop it
Sample1Start:
        FDB     $3D59            * location where sample starts counting down from, used for looping
FIRQ_Interrupt1:
        STA     FIRQRestoreA+1   * Save A before we exit the routine (self modifying code)
        LDA     FIRQENR          * Re enable the FIRQ
LoadAudio1:
        LDA     $3D59            * This value gets changed with some self modifying code below
        STA     $FF20            * OUTPUT sound to DAC
        INC     LoadAudio1+2     * Increment the LSB of Sample pointer
        BNE     >                * check if we hit 00, if not exit
        INC     LoadAudio1+1     * Increment the MSB of Sample pointer
        BPL     >                * If positive then we are good still under $8000 otherwise (end of sample, reset pointer)
Hit8000:                         * Restore the pointer to the start location of the sample
        LDA     Sample1Start     * Point to the MSB of the sample Start location
        STA     LoadAudio1+1     * Store the MSB of Sample pointer
        LDA     Sample1Start+1   * Point to the LSB of the sample Start location
        STA     LoadAudio1+2     * Store the LSB of Sample pointer
        
FIRQRestoreA:
!       LDA     #$00             * STA at the start of the FIRQ stores A's value here and gets loaded before the RTI saves a cycle and a byte of RAM
        RTI                      * Return from the FIRQ

The program starts here and sets up the hardware on the CoCo 3 to use the GIME chip’s interrupts.  This is different from the previous methods shown in this series of posts.

START
***********************************************************
        ORCC    #$50            * Disable interrupts
        CLR     $FFD9           * High Speed mode enabled

        SETDP   DirectPage/256  * Set the direct Page so the assembler will use Direct Page addressing when assembling
        LDA     #DirectPage/256 * Set the Direct Page value
        TFR     A,DP            * to where our FIRQ is located

        LDA     #%00110100      * Setup the CoCo 3's hardware
        STA     $FF01           * HSYNC IRQ Disabled, IRQ Polarity Flag falling Edge, Data Direction Normal, Select Line LSB = 0, HSYNC Flag = 0
        STA     $FF03           * VSYNC IRQ Disabled, IRQ Polarity Flag falling Edge, Data Direction Normal, Select Line MSB = 0, VSYNC Flag = 0
        STA     $FF21           * CONTROL OF CD FIRQ* TO CPU DISABLED, IRQ Polarity Falling Edge of CD, CD Flag off
        STA     $FF23           * CONTROL OF Cart FIRQ* TO CPU DISABLED, IRQ Polarity Falling Edge of Cart, Cart Flag off
        LDA     #%01111100      *
        STA     $FF90           * CoCo 3 Mode, MMU Enabled, GIME IRQ Enabled, GIME FIRQ Enabled, Vector RAM at FEXX enabled, Standard SCS Normal, ROM Map 16k Int, 16k Ext
        LDA     #%00100000      *
        STA     $FF91           * Mem Type 64k chips, 279.365 nsec timer, MMU Task 0 - $FFA0-$FFA7
        LDA     #%10000000      *
        STA     $FF98           * Graphics mode, Colour output, 60 hz, max vertical res

* Setup the Timer FIRQ
        LDA     #$0E            * Write the JMP instruction to a direct page JMP location
        LDB     #FIRQ_Interrupt1-DirectPage  * B now is the address of the FIRQ routine
        STD     $FEF4           * Save it to the CoCo 3 FIRQ vector address 
        LDD     #$0280          *
        STD     $FF94           * Set countdown Timer to $0280 - This is the speed the samples will playback at
        LDA     #%00100000      * $20
        STA     $FF93           * Enable TIMER FIRQ Interrupt

        ANDCC   #$AF            * = %10101111 this will Enable the IRQ and the FIRQ to start

* Your program can go here, for now it just loops forever as the sample plays in the background
!
        BRA    <
        END    START            * Tell the assembler when creating an DECB ML program to set the 'EXEC' address to wherever the label START is in RAM (above)

I hope you are now an interrupt expert and use them in your next awesome game…  🙂

Posted in CoCo Programming | Leave a comment

How to setup and use IRQs on the TRS-80 Color Computer – Part 4 – Simpler ways to use interrupts on the CoCo

Table of Contents (click on a link to jump to that web page)

There is a simpler way to test for and react to the HSYNC and VSYNC interrupts.  We do this by simply testing bit 7 of the IRQ we want to use.  As per the information here:

HSYNC – $FF01 – PIA 0 side A control reg – PIA0AC     CoCo 1/2/3

  • Bit 7 HSYNC Flag 0=not Sync, 1=Hsync is triggered
  • Bit 6 Unused
  • Bit 5 Always a 1
  • Bit 4 Always a 1
  • Bit 3 Select Line LSB of MUX
  • Bit 2 DATA DIRECTION TOGGLE 0 = $FF00 sets data direction 1 = normal
  • Bit 1 IRQ POLARITY 0 = flag set on falling edge 1=set on rising edge
  • Bit 0 HSYNC IRQ 0 = disabled 1 = enabled

VSYNC – $FF03 – PIA 0 side B control reg – PIA0BC   CoCo 1/2/3

  • Bit 7 VSYNC FLAG 0=not Sync, 1=Vsync is triggered
  • Bit 6 N/A
  • Bit 5 Always a 1
  • Bit 4 Always a 1
  • Bit 3 SELECT LINE MSB of MUX
  • Bit 2 DATA DIRECTION TOGGLE 0 = $FF02 sets data direction 1=normal
  • Bit 1 IRQ POLARITY 0=flag set on falling edge 1=set on rising edge
  • Bit 0 VSYNC IRQ 0=disabled 1=enabled

If you have a program that needs to use the VSYNC such as the previous clock program or maybe a game that needs to know when to switch screens you can write your program so that it does what it needs to do and then waits for the VSYNC at address $FF03, bit 7 to be set.  At the beginning of your program you would add the following few lines which will make sure the VSYNC IRQ is enabled by setting bit 0 of $F003 to a one.

        LDA    $FF03 * Load A with the value stored at $FF03
        ORA    #$01  * Set bit 1 high
        STA    $FF03 * Store A at $FF03 - VSYNC is now enabled

Then you simply add these three lines of code to your program to make it wait and watch for a VSYNC to be triggered.

* Wait for VSYNC to occur
VSYNC   LDA     $FF02 * Acknowledge the VSYNC interrupt
VWAIT   LDA     $FF03 * Loop until Vsync is triggered
        BPL     VWAIT * If bit 7 is low then keep looping

Once the VSYNC is triggered the program execution will continue to the rest of your program.

Unlike an actual interrupt request, the CPU doesn’t save any registers on the stack and it doesn’t have to jump to and from your interrupt request so this can save CPU cycles.

* Timer related pointers, Can be removed as you probably don't require a timer to be shown on screen in your game
Timer       EQU RepeatCount+5	* used for the Clock counter counts down from 30 every time play loop is entered.  When it hit's zero a second has passed
Minutes1    EQU $420-5		* Location to display timer on screen (minutes and seconds)
Minutes2    EQU Minutes1+1
Seconds1    EQU Minutes1+3
Seconds2    EQU Minutes1+4

        ORG     $0E00
START
        ORCC   #$50            * If you are going to use real IRQ's instead of polling VWAIT then change this section to enable the IRQs
        LDA    $FF03           * PIA 0 side B control reg - PIA0BC
        ORA    #$01            * VSync IRQ disabled
        STA    $FF03           * Save Settings
        
* This is Timer display related code 
        LDD    #$3030      * ascii '00'
        STD    Minutes1    * Start showing 00 minutes
        STD    Seconds1    * Start showing 00 seconds
        CLR    Every2nd    * Process sound every 2nd Vsync IRQ hit
        LDB    #60         * Vsync is triggered 60 times a second
        STB    Timer       * store it 
        LDB    #':         * Draw the colon on screen between the minutes and the seconds
        STB    Minutes2+1  * store it 
* End of Time display related code that can be removed if you don't want to see a timer on the screen. 
Loop:
VSYNC   LDA    $FF02       * Not sure if this is necessary??? 
VWAIT   LDA    $FF03       * Loop until Vsync is triggered
        BPL    VWAIT
                           * Show the running time on the screen
        DEC    Timer       * triggered 60 times every second
        BNE    Loop        * If we haven't reached zero yet then don't update the time
        LDB    #60         * reset the timer back to 60
        STB    Timer       * save the value
        LDA    Seconds2    * check if the ones value of the # of seconds
        CMPA   #$39        * if it is a nine then make it a zero
        BEQ    >           * make it a zero and add to the tens value of the # of seconds
        INCA               * Otherwise update
        STA     Seconds2   * Save
        BRA     Playnotes  * Go process music data
!       LDA     #$30	   * set the ones value to zero
        STA     Seconds2   * Save it
        LDA     Seconds1   * Get the tens value of the seconds
        CMPA    #$35	   * check if it is a 5
        BEQ     >	   * If so then add one to minute value
        INCA		   * otherwise add 1 to the tens value of the seconds
        STA     Seconds1   * save it
        BRA     Playnotes  * Go process music data
!       LDA     #$30	   * Set the tens value of the seconds
        STA     Seconds1   * to a zero
        LDA     Minutes2   * Get the ones value of the minutes
        CMPA    #$39	   * check if it is a nine
        BEQ     >	   * if so then go add one to the tens of the minute value
        INCA		   * otherwise increment the ones value of the seconds
        STA     Minutes2   * update the value
        BRA     Playnotes  * Go process music data
!       LDA     #$30       * Set the ones value of the minutes
        STA     Minutes2   * to zero
        INC     Minutes1   * add one the tens value of the minutes
        LBRA    Loop       * Go wait for the next VSYNC IRQ
* End of time on screen display code
        END     START      * Tell assembler when creating an ML program to set the 'EXEC' address to wherever the label START is in RAM (above)

That’s it for Part 4, in Part 5 I’ll go over how you would setup and use an interrupt specifically on a CoCo 3 and also some optimized ways accessing interrupt routines.

Posted in CoCo Programming | Leave a comment

How to setup and use IRQs on the TRS-80 Color Computer – Part 3 – Can the IRQ run without effecting BASIC?

Table of Contents (click on a link to jump to that web page)

Here we show the same IRQ routine as in the previous blog post which shows a clock on the screen in the top left corner.  This program will setup the IRQ so that BASIC is still running completely unaware that this IRQ has been added.

BASIC has many IRQs that get used by the ROM so we can’t simply take over an IRQ or FIRQ, we must intercept BASIC’s IRQ jump and point it to our interrupt routine and when our routine is complete we send the processor on it’s way to the BASIC IRQ routine handler.

The program starts here after LOADMing the program and the BASIC EXEC command is used.  This section temporarily disables the FIRQ and IRQ so we can make changes without effecting BASIC

START:
    PSHS   D,X,CC     * save the registers our setup code will effect
    ORCC   #$50       * = %01010000 this will Disable the FIRQ and the IRQs using the Condition Code register is [EFHINZVC] a high or 1 will disable that value

Since we are going to keep BASIC running and have our code running in the background we are going to have to intercept BASIC’s IRQ.  We load the existing IRQ jump pointer information and save it at the Exit location of our routine.

    LDA    $10C       * Get the opcode that is currently used by BASIC
    STA    Exit       * save it where our IRQ is going to end (a little self modifying code)
    LDX    $10D       * get the address info that is currently being used by BASIC (different ROMs will have a different location)
    STX    Exit+1     * save the address info where our IRQ is going to end (a little self modifying code)

Next we insert a jump to our IRQ routine

    LDA    #$7E       * Jump instruction Opcode
    STA    $10C
    LDX    #IRQ_Start * Load X with the pointer value of our IRQ routine
    STX    $10D       * Store the new IRQ address location

Before turning the interrupt routine back on we setup the counter display related code – Initialize the clock to 00:00

    LDD     #$3030    * ascii '00'
    STD     Minutes1  * Start showing 00 minutes
    STD     Seconds1  * Start showing 00 seconds
    LDB     #VSYNCFreq  * VSYNC is triggered 50 (PAL) or 60 (NTSC) times per second
    STB     Jiffy     * store it

Our setup is done, go back to BASIC

    PULS    D,X,CC,PC* Restore the registers and the Condition Code which will reactive the FIRQ and IRQ and return back to BASIC

This is the actual IRQ routine, that is intercepted from BASIC’s IRQ.  We don’t know what IRQ is triggered when the processing starts our interrupt request.  So we check bit 7 of address $FF03.  If this bit is set then we know the IRQ is a VSYNC interrupt so we will execute our routine and then jump to BASIC’s IRQ handling routine.  Otherwise if bit 7 of address $FF03 is clear then the IRQ is not triggered by a VSYNC so we simply exit our routine and jump to BASIC’s IRQ handling routine.

IRQ_Start:
    TST    $FF03      * Check for (Vsync) Interrupt bit 7 of $FF03 is set to 1 when the VSYNC is triggered
    BMI    UpdateTime * If bit 7 of $FF03 is set then the value is a negative so run our code below
Exit:
    RMB 3             * These three bytes will be copied from BASIC's own IRQ jump setting when the setup code is EXECuted
                      * Using a little self modifying code
                      * Typically a JMP to some address in ROM, but it could be modified by another program
UpdateTime:
    ...

That is all there is to it.  Below is the full program with lots of comments explaining the code at each step.  I hope seeing how to insert your own code into BASIC’s interrupt routine will help you to better understand how interrupts work.  Maybe you can use this method to enhance your own BASIC programs…

* Example of setting up your own code to run in the background while BASIC is still running
* This will intercept BASIC's IRQ routine and check if the IRQ type is a VSYNC IRQ and if so
* Show our clock on the screen
        ORG     $0E00
* Choose the VSYNC frequency
PAL EQU 50         * PAL region uses 50hz for VSYNC
NTSC EQU 60        * NTSC region uses 60hz for VSYNC
VSYNCFreq EQU NTSC * Change this to either NTSC or PAL depending on your region
* Clock related pointers
Jiffy       FCB    $00 * Used for the clock, counts down from 50 or 60, every time VSYNC IRQ is entered.  When it hit's zero a second has passed
Minutes1    FCB    $00 * Keep track of minutes
Minutes2    FCB    $00 * ""
Seconds1    FCB    $00 * Keep track of seconds
Seconds2    FCB    $00 * ""
* This is the actual IRQ routine
* When the IRQ is triggered all the registers are pushed onto the stack and saved automatically.
* Including the Program Counter (PC) it's like this command is done for you PSHS D,X,Y,U,CC,DP,PC
*
* NOTE: If you are using the FIRQ is for "Fast" interrupts the CPU does not push the registers onto the stack except for the CC and PC
*       You must take care and backup and restore any registers you change in your FIRQ routine.
*********************************
IRQ_Start:
    TST    $FF03      * Check for (Vsync) Interrupt bit 7 of $FF03 is set to 1 when the VSYNC is triggered
    BMI    UpdateTime * If bit 7 of $FF03 is set then the value is a negative so run our code below
Exit:
    RMB 3             * These three bytes will be copied from BASIC's own IRQ jump setting when the setup code is EXECuted
                      * Using a little self modifying code
                      * Typically a JMP to some address in ROM, but it could be modified by another program
UpdateTime:
    DEC    Jiffy      * countdown 1/50 (PAL) or 1/60 (NTSC) of a second
    BNE    Exit       * Only continue after counting down to zero from 50 (PAL) or 60 (NTSC), VSYNC runs at 50 or 60Hz
* If not zero then exit the IRQ routine
* If we get here then Jiffy is now at zero so another second has passed so let's update the screen
    LDA    #VSYNCFreq * Reset Jiffy
    STA    Jiffy      * to 50 (PAL) or 60 (NTSC)
    LDA    Seconds2   * check the ones value of the # of seconds
    CMPA    #$39      * if it is a nine then make it a zero
    BEQ     >         * make it a zero and add to the tens value of the # of seconds
    INCA              * Otherwise update
    STA     Seconds2  * Save
    BRA     Update    * Go copy the data to the screen
!   LDA     #$30      * set the ones value to zero
    STA     Seconds2  * Save it
    LDA     Seconds1  * Get the tens value of the seconds
    CMPA    #$35      * check if it is a 5
    BEQ     >         * If so then add one to minute value
    INCA              * otherwise add 1 to the tens value of the seconds
    STA     Seconds1  * save it
    BRA     Update    * Go copy the data to the screen
!   LDA     #$30      * Set the tens value of the seconds
    STA     Seconds1  * to a zero
    LDA     Minutes2  * Get the ones value of the minutes
    CMPA    #$39      * check if it is a nine
    BEQ     >         * if so then go add one to the tens of the minute value
    INCA              * otherwise increment the ones value of the seconds
    STA     Minutes2  * update the value
    BRA     Update    * Go copy the data to the screen
!   LDA     #$30      * Set the ones value of the minutes
    STA     Minutes2  * to zero
    INC     Minutes1  * add one to the tens value of the minutes
* Update the screen
Update:
    LDD    Minutes1   * Get the minutes value from RAM
    STD    $400       * Show it on the top left of the screen
    LDA    #':        * A now equals the value for a Colon
    STA    $402       * Display the Colon on the screen between the minutes and seconds
    LDD    Seconds1   * Get the seconds value from RAM
    STD    $403       * Show it on the top left of the screen
    BRA    Exit       * Jump back to BASIC's IRQ handling code
* Program starts here when BASIC EXEC command is used after LOADMing the program
* This section temporarily disables the FIRQ and IRQ so we can make changes without effecting BASIC
START:
    PSHS   D,X,CC     * save the registers our setup code will effect
    ORCC   #$50       * = %01010000 this will Disable the FIRQ and the IRQs using the Condition Code register is [EFHINZVC] a high or 1 will disable that value
* If we are going to keep BASIC running and have this our code running in the background we are going to have to intercept BASIC`s IRQ
* run our code then pass the IRQ control back to BASIC
* Intercept Vectored IRQ
*********************************
    LDA    $10C       * Get the opcode that is currently used by BASIC
    STA    Exit       * save it where our IRQ is going to end (a little self modifying code)
    LDX    $10D       * get the address info that is currently being used by BASIC (different ROMs will have a different location)
    STX    Exit+1     * save the address info where our IRQ is going to end (a little self modifying code)
* Next we insert a jump to our IRQ routine
    LDA    #$7E       * Jump instruction Opcode
    STA    $10C
    LDX    #IRQ_Start * Load X with the pointer value of our IRQ routine
    STX    $10D       * Store the new IRQ address location
* This is the counter display related code - Initialize the clock to 00:00
    LDD     #$3030    * ascii '00'
    STD     Minutes1  * Start showing 00 minutes
    STD     Seconds1  * Start showing 00 seconds
    LDB     #VSYNCFreq  * VSYNC is triggered 50 (PAL) or 60 (NTSC) times per second
    STB     Jiffy     * store it
* setup is done, go back to BASIC
    PULS    D,X,CC,PC* Restore the registers and the Condition Code which will reactive the FIRQ and IRQ and return back to BASIC
    END     START     * Tell assembler when creating an DECB ML program to set the 'EXEC' address to wherever the label START is in RAM (above)

That’s it for Part 3, in Part 4 I’ll go over a simpler way to use interrupts on the CoCo.

Posted in CoCo Programming | Leave a comment

How to setup and use IRQs on the TRS-80 Color Computer – Part 2 – Overview with an example program

Table of Contents (click on a link to jump to that web page)

In this article I’m going to cover an example of setting up and using an IRQ for the Color Computer 1, 2 and the CoCo 3.  The CoCo 1 & 2 are the same, the CoCo 3 can be setup the same as a CoCo 1 but if you are going to write code specific for the CoCo 3 then it’s best to use the method specific to the CoCo 3.  This will be explained in later chapter

First we are going to go over how to setup IRQs for the CoCo 1,2 and 3.  On a CoCo 1 or CoCo 2 the ROM is coded to jump to certain addresses whenever an IRQ is triggered, the main interrupts to use for programming are:

The IRQ at Address $FFF8 & $FFF9 these bytes point to address $010C
The FIRQ (Fast IRQ) at address $FFF6 & $FFF7 these bytes point to address $010F

Below I’ll go through a real world example of setting up and using an IRQ that will work on the CoCo 1,2 or 3.  It shows a clock (minutes and seconds) on the top left of the screen that will update itself in the background while your main program carries on as if the IRQ doesn’t exist.  I’ll break up the program in parts and then show the program listing in it’s entirety.  I have commented each line of code to make it clear as to what is happening in the program at each step.

First we have to stop any currently running Interrupts from happening and make sure that our IRQ will be used.  This is done with the ORCC #$50 command which ORs the Condition Code register with $50 which in binary is 01010000

   Binary OR $50 = 01010000
CC register flags [EFHINZVC]

See above we set the CC register’s F and I flags high (or ones).  That tells the CPU not to use the FIRQ and IRQ anymore.  Of course you can turn off just the FIRQ with ORCC #$40 = binary 01000000 or just the IRQ with ORCC #$10 = binary 00010000.  But it’s safer to just turn them both off and do your IRQ setup with ORCC #$50

START:
    ORCC    #$50    * = %01010000 this will Disable the FIRQ and the IRQs using the Condition Code register is [EFHINZVC] a high or 1 will disable that value

Next we use two special bytes in the CoCo to set whether our IRQ is going to be triggered using the HSYNC, the VSYNC or both.  The two bytes are $FF01 and $FF03.  The HSYNC is controlled by $FF01 and VSYNC is controlled by $FF03.

Bit 0 of $FF01 enables or disables the HSYNC IRQ

Bit 0 of $FF03 enable or disable the VSYNC IRQ
If bit 0 is set to 1 that IRQ is then enabled and that IRQ will be triggered.  If bit 0 is set to 0 that IRQ is then disabled and wont trigger an IRQ request.

    LDA     $FF01   * PIA 0 side A control reg - PIA0AC
    ANDA    #$FE    * Clear bit 0 - HSYNC IRQ Disabled
    STA     $FF01   * Save settings

    LDA     $FF03   * PIA 0 side B control reg - PIA0BC
    ORA     #$01    * VSYNC IRQ enabled
    STA     $FF03   * Save Settings

As per the information at the start of this article, if we want to use the IRQ we change the code at address $010C-$010E from JMP $xxxx to the address we want to use for our own IRQ.  So we now change the opcode in RAM at address $010C to a Jump instruction which is an $7E (it probably already is a Jump, but just in case it’s not it’s a good idea to do it yourself).  Next we change the Jump location (pointer) in RAM at $010D to point to our IRQ starting location in RAM.

*********************************
* Setup Vectored IRQ using VSYNC
*********************************
   LDA    #$7E     * Jump instruction Opcode
   STA    $10C     * Store it at IRQ jump address 
   LDX    #IRQ_Start  * Load X with the pointer value of our IRQ routine
   STX    $10D     * Store the new IRQ address location

At this point the IRQ is setup and ready to go.  Until we change the Condition Code register and enable the IRQ bit to a zero the IRQ wont be used by CPU.

Before activating the IRQ we want to initialize some of the RAM that the clock IRQ is going to use.  I’m not going to go over this here as it’s described in the full listing down below and it uses labels for RAM locations that make it hard to follow without seeing where those Labels are and it’s not very complicated.

After our IRQ routine is initialized we are ready to fire it into action with the ANDCC instruction as shown here.

* This is where we enable the IRQ so the CPU will use it 
    ANDCC   #$EF        * = %11101111 this will Enable the IRQ to start

There is one last thing to note about using an IRQ.  When the CPU triggers and IRQ it will jump to your IRQ routine and inside your IRQ routine you must also re-trigger or acknowledge the IRQ so that it will be acted upon when it’s triggered again by the CPU.

In our example we are using the VSYNC IRQ and we must acknowledge the VSYNC IRQ inside our IRQ routine by activating memory location $FF02.  You could use TST  $FF02, or CLR  $FF02 or LDA/LDB $FF02.  I use the LDA $FF02 or LDB $FF02 since it only takes two CPU cycles.  Remember we don’t care what is loaded into the A accumulator we just want to “tickle” the memory location.

LDA     $FF02       * PIA 0 side B data register - PIA0BD We need to either CLR  $FF02, TST  $FF02 or LDA,LDB  $FF02 to activate the IRQ now and every time inside the IRQ too.
                    * I use LDA because it is only 2 CPU cycles. "Acknowledge the VSYNC IRQ"    

If you are using the HSYNC IRQ then you want to “tickle” memory location $FF00 inside your IRQ routine with a LDA   $FF00

Where inside your  IRQ you decide to “Acknowledge the IRQ” is important.  If you place it at the beginning of your IRQ routine then if enough time passes the IRQ could be triggered again before the current one it finished.  There might be reasons you want this to happen but for simple IRQ routines probably not.

IRQ_Start:
    LDA     $FF02       * PIA 0 side B data register - PIA0BD We need to either CLR  $FF02, TST  $FF02 or LDA,LDB  $FF02 to activate the IRQ now and every time inside the IRQ too.
                        * I use LDA because it is only 2 CPU cycles. "Acknowledge the VSYNC IRQ"    

At the end of your IRQ routine you always use the RTI (Return from Interrupt) instruction.  Again you don’t have to worry about the accumulators and registers the CPU will automatically do a PULS D,X,Y,U,CC,DP,PC so that all everything is restored.  Unless you are using the FIRQ, which I explained above.

IRQ_End:
    RTI

That’s it for the summary.  Below you will find the entire code including a little demo code at the end of the example to show a little program is running while the IRQ is being triggered in the background updating the clock on the screen.

* Example of setting up an IRQ to run in the background of your own machine language program
    ORG     $0E00
* Choose the VSYNC frequency
PAL         EQU  50 * PAL region uses 50hz for VSYNC
NTSC        EQU  60 * NTSC region uses 60hz for VSYNC
VSYNCFreq   EQU  NTSC * Change this to either NTSC or PAL depending on your region
* Clock related pointers
Jiffy       FCB  $00 * Used for the clock, counts down from 50 or 60, every time VSYNC IRQ is entered.  When it hit's zero a second has passed
Minutes1    FCB  $00 * Keep track of minutes
Minutes2    FCB  $00 * ""
Seconds1    FCB  $00 * Keep track of seconds
Seconds2    FCB  $00 * ""
* This is the actual IRQ routine
* When the IRQ is triggered all the registers are pushed onto the stack and saved automatically.
* Including the Program Counter (PC) it's like this command is done for you PSHS D,X,Y,U,CC,DP,PC
*
* NOTE: If you are using the FIRQ is for "Fast" interrupts the CPU does not push the registers onto the stack except for the CC and PC
*       You must take care and backup and restore any registers you change in your FIRQ routine.
*********************************
IRQ_Start:
    LDA     $FF02       * Acknowledge the VSYNC IRQ, this makes the IRQ happen again.  Since it's at the start of the IRQ here
                        * it's possible for another IRQ trigger to happen while we are still doing this routine.
                        * If you want to be sure that your IRQ isn't triggered again until you are finished this IRQ then
                        * place this instruction just before the RTI at the end of your IRQ
                        * We are fine here as this IRQ code is very short and will complete in less then 1/60 of a second.
    DEC     Jiffy       * countdown 1/50 (PAL) or 1/60 (NTSC) of a second
    BNE     IRQ_End     * Only continue after counting down to zero from 50 (PAL) or 60 (NTSC), VSYNC runs at 50 or 60Hz
* If not zero then exit the IRQ routine
* If we get here then Jiffy is now at zero so another second has passed so let's update the screen
    LDA     #VSYNCFreq  * Reset Jiffy
    STA     Jiffy       * to 50 (PAL) or 60 (NTSC)
    LDA     Seconds2    * check the ones value of the # of seconds
    CMPA    #$39        * if it is a nine then make it a zero
    BEQ     >           * make it a zero and add to the tens value of the # of seconds
    INCA                * Otherwise update
    STA     Seconds2    * Save
    BRA     Update      * Go copy the data to the screen
!   LDA     #$30        * set the ones value to zero
    STA     Seconds2    * Save it
    LDA     Seconds1    * Get the tens value of the seconds
    CMPA    #$35        * check if it is a 5
    BEQ     >           * If so then add one to minute value
    INCA                * otherwise add 1 to the tens value of the seconds
    STA     Seconds1    * save it
    BRA     Update      * Go copy the data to the screen
!   LDA     #$30        * Set the tens value of the seconds
    STA     Seconds1    * to a zero
    LDA     Minutes2    * Get the ones value of the minutes
    CMPA    #$39        * check if it is a nine
    BEQ     >           * if so then go add one to the tens of the minute value
    INCA                * otherwise increment the ones value of the seconds
    STA     Minutes2    * update the value
    BRA     Update      * Go copy the data to the screen
!   LDA     #$30        * Set the ones value of the minutes
    STA     Minutes2    * to zero
    INC     Minutes1    * add one to the tens value of the minutes
* Update the screen
Update:
    LDD    Minutes1     * Get the minutes value from RAM
    STD    $400         * Show it on the top left of the screen
    LDA    #':          * A now equals the value for a Colon
    STA    $402         * Display the Colon on the screen between the minutes and seconds
    LDD    Seconds1     * Get the seconds value from RAM
    STD    $403         * Show it on the top left of the screen
* All done the IRQ so we can now leave...
* Leaving an IRQ will automatically do a PULS D,X,Y,U,CC,DP,PC so that all registers are restored
*
* NOTE: If you are leaving an FIRQ which is "Fast" it does not restore all the registers it only restores the CC and the PC.
*       You must make sure you save and restore any registers you modify in your routine.
*********************************
IRQ_End:
    RTI
* Program starts here when BASIC EXEC command is used after LOADMing the program
* This section disables the HSYNC IRQ and enables the VSYNC IRQ
START:
    ORCC    #$50    * = %01010000 this will Disable the FIRQ and the IRQs using the Condition Code register is [EFHINZVC] a high or 1 will disable that value
    LDA     $FF01   * PIA 0 side A control reg - PIA0AC
    ANDA    #$FE    * Clear bit 0 - HSYNC IRQ Disabled
    STA     $FF01   * Save settings
    LDA     $FF00   * PIA 0 side A data register - PIA0AD
    LDA     $FF03   * PIA 0 side B control reg - PIA0BC
    ORA     #$01    * VSYNC IRQ enabled
    STA     $FF03   * Save Settings
    LDA     $FF02   * PIA 0 side B data register - PIA0BD We need to either CLR  $FF02, TST  $FF02 or LDA,LDB  $FF02 to activate the IRQ now and every time inside the IRQ too.
                    * I use LDA because it is only 2 CPU cycles. "Acknowledge the VSYNC IRQ"
*********************************
* Setup Vectored IRQ using VSYNC
*********************************
    LDA     #$7E        * Jump instruction Opcode
    STA     $10C        * Store it at IRQ jump address 
    LDX     #IRQ_Start  * Load X with the pointer value of our IRQ routine
    STX     $10D        * Store the new IRQ address location
* This is the counter display related code - Initialize the clock to 00:00
    LDD     #$3030      * ascii '00'
    STD     Minutes1    * Start showing 00 minutes
    STD     Seconds1    * Start showing 00 seconds
    LDB     #VSYNCFreq  * VSYNC is triggered 50 or 60 times a second
    STB     Jiffy       * store it

* This is where we enable the IRQ so the CPU will use it 
    ANDCC   #$EF        * = %11101111 this will Enable the IRQ to start
    LDA     $FF02       * PIA 0 side B data register - PIA0BD We need to either CLR  $FF02, TST  $FF02 or LDA,LDB  $FF02 to activate the IRQ now and every time inside the IRQ too.
                        * I use LDA because it is only 2 CPU cycles. "Acknowledge the VSYNC IRQ"

*
* Your program code goes here and the little clock IRQ will run in the background without any effect from the code below.
*********************************
* Little demo code to show the IRQ is running in the background
BigLoop:
    LDU     #$A000      * Show junk on the screen data copied from ROM
MidLoop:
    LDX     #$406       * Point X beside clock on screen
!   LDA     ,X+         * Get value in move X to the right
    STA     -2,X        * Store the value to left of where it was read from
    CMPX    #$600       * see if X is at the end of screen
    BNE     <           * if not keep going
    LDY     #$1000      * Delay
!   LEAY    -1,Y        * Delay countdown
    BNE     <           * Delay check if zero, if not loop
    LDA     ,U+         * get a byte from ROM
    ORA     #$80        * Make the value semi graphics characters
    STA     $5FF        * store it on screen
    CMPU    #$C000      * check if we are at the end of ROM
    BLO     MidLoop     * If not loop
    BRA     BigLoop     * Go start the process again...
    END    START        * Tell assembler when creating an ML program to set the 'EXEC' address to wherever the label START is in RAM (above)

That’s it for Part 2.  In Part 3 I’ll show you how to do a similar thing but with BASIC still running.

See you soon, Glen

Posted in CoCo Programming | Leave a comment

How to setup and use IRQs on the TRS-80 Color Computer – Part 1 – What is an IRQ and when would I use it?

Table of Contents (click on a link to jump to that web page)

Hello I’ve decided to write some posts that describe how to use IRQs on the CoCo.  This was one of the last things about assembly language programming that I learned and has been a mystery to me for years.  With the assistance of some very knowledgeable and helpful people on the CoCo mailing list I was able to figure out how to set up an IRQ on the CoCo 3.  Recently I was working on my MIDI to CoCo converter program and I wanted to output the data with an option that would include the player that could be run from an IRQ.  I was having trouble getting it working on the CoCo 1 and I started doubting my understanding.  So I turned to the internet for some help but there wasn’t much in the way of example code for setting up and handling IRQs.  It turned out that my problem was a typo and that my program code was OK after all.  But since I discovered there isn’t too much info on IRQs for the CoCo that I should write up a blog and share what I’ve learned about them for others and to help myself in the future when I start second guessing myself again why my program doesn’t work the way I think it should.

I did find some great info on page 10 of CoCo Clipboard Magazine Volume 2 Issue 6 July/August 1989 by Kraig Brockschmidt

On with the info you came here for…

What is an interrupt request?

An Interrupt Request literally “interrupts” the CPU from processing it’s normal program execution and jumps to another routine and once that other routine is complete it jumps right back to the program code it was already executing.

How do users make an interrupt happen?

A user makes an interrupt happen when they are typing on the keyboard or pressing a joystick button or presses the power on or reset button.

Are those the only interrupts the 6809 has?

No, there are three software interrupts called SWI,SWI2 and SWI3 and four hardware interrupts which are called IRQ, FIRQ, NMI and RESET

When would I use a software interrupt?

Software Interrupts are used mainly in OS-9 where the registers are loaded with values needed for a function prior to a SWI2 instruction and the byte right after the SWI2 holds the function identifier.  Since the interrupt pushes all the registers onto the stack the function can pull them off the stack to use them then return execution to the users program.

Another use for software interrupts is when debugging some machine code.  This isn’t necessary these days with powerful tools such as MAME’s debugger.  But at the system level a program like Zbug could use a SWI command as a breakpoint and when the program reaches the SWI Zbug’s interrupt handling routine would present all the register values to the user since they are automatically pushed onto the stack.

When would I use the hardware interrupts (IRQ, FIRQ, NMI and RESET)?

These are examples of different things that can trigger a hardware interrupt:

  • Joystick Button being pressed
  • Keyboard button being pressed or released
  • RS-232 status line changes
  • Cartridge interrupt signal – Color BASIC uses FIRQ to vector control to a ROM-PAK if pins 7 & 8 of the cartridge port are connected together by a cartridge.
  • NMI pin is connected only to the cartridge port. The Disk Operating System (DOS) uses the NMI to vector out of data transfers between the CPU and the Floppy Disk Controller.
  • Vertical SYNC just occurred
  • Horizontal SYNC just occurred
  • Disk Drive Motor starts or stops
  • CoCo 3 has an internal timer that counts down, when it reaches zero an interrupt occurs
  • Reset switch or power on causes a RESET interrupt

I mentioned the RESET interrupt above because it might be useful for a programmer to change the RESET interrupt (Vector) address to be something different like restarting a game.  Or showing a blue screen to see if the artifact colours match what the game needs.

How does the CPU know where to return once it jumps to an interrupt request?

When an interrupt request is triggered the CPU pushes the Condition Code (CC) register on the stack and the Program Counter (PC) on the stack.  Depending on which type of interrupt is triggered the CPU will also push the rest of the registers on the stack.  If the CPU is going to push all the registers on the stack it will first set the E flag of the Condition Code register.  This let’s the CPU know that at the end of the interrupt request routine (where the RTI instruction is used) that it should restore all the registers and not just the CC and PC.

Which interrupts backup all the registers and which don’t?

The IRQ, NMI and all the software interrupts backup all the registers.  The FIRQ only backups the CC and the PC.

Why doesn’t the FIRQ backup all the registers?

The name of the FIRQ is Fast Interrupt Request and is given that name because it executes faster then a regular IRQ.  It’s faster because the FIRQ doesn’t use as many CPU cycles, it doesn’t backup and restore all the registers each time its triggered.  The programmer must make sure to backup and restore the registers that the routine uses.

How does the CPU know where the interrupt is located in memory?

This is an example of how an IRQ interrupt works:

  1. The CPU is running through your program then an IRQ is triggered.
  2. The CPU finishes executing the instruction it is at.
  3. It sets the E flag of the CC register indicating to restore all registers when the RTI instruction is processed.
  4. The CPU backs up all the registers onto the stack.
  5. Then it looks at the address $FFF8 and $FFF9 to find out what address it should jump to for the IRQ.  This is hard coded in the CoCo 1 and 2 to address $010C which has a JMP instruction that points to the IRQ routine.
  6. This RAM at address at $010C-$010E can be changed by the user so it points to the users own interrupt request routine.
  7. The IRQ code gets executed and when the code is finished it uses an RTI instruction.
  8. Since the E flag of the CC register is set with an IRQ the CPU will then restore all the registers back to how they were just before the IRQ was triggered.  This includes the Program Counter (PC) to return the CPU back to the address where it was executing your program from and continues as if nothing happened.

This is an example of how an FIRQ interrupt works similar to the IRQ except in Bold below:

  1. The CPU is running through your program then an FIRQ is triggered.
  2. The CPU finishes executing the instruction it is at.
  3. The CPU backs up only the CC and the PC onto the stack.
  4. Then it looks at the address $FFF6 and $FFF7 to find out what address it should jump to for the FIRQ.  This is hard coded in the CoCo 1 and 2 to address $010F which has a JMP instruction that points to the FIRQ routine.
  5. This RAM at address at $010F-$0111 can be changed by the user so it points to the users own interrupt request routine.
  6. The FIRQ code gets executed and when the code is finished it uses an RTI instruction.
  7. Since the E flag of the CC register is not set with an FIRQ the CPU will then restore only the CC and PC to return the CPU back to the address where it was executing your program from and continues as if nothing happened.

Which interrupts get used the most and why?

If you are writing your own assembly programs on the CoCo you will generally be using the IRQ and the FIRQ.  Any of the user interrupts can be defined as an IRQ or FIRQ and it’s up to the programmer to decide which to use and when.

Generally you would use the VSYNC interrupt to know when the computer has finished updating the screen which is the best time to move an object on the screen.  For example in a game.  This will limit tearing or noticeable glitches on the screen.  A programmer can also program what is called double buffering using the VSYNC and would have two copies of the screen data in RAM.  When the VSYNC is triggered you switch to the other screen.  Then you move your characters around in the background (not shown) screen.  Then when the VSYNC is triggered again you switch to other screen that had it’s characters updated, etc..

Since the VSYNC is a constant value (60 Hz in NTSC countries and 50 Hz is PAL countries) it can be used in sound generating routines.

On the CoCo 3 you have a Timer interrupt that is very handy for different things one is to use it for audio sample playback using an FIRQ.  You set the Timer value and when it reaches zero the FIRQ is triggered and the Timer value is reset to your programmed value so it will constantly be triggered over and over.  The FIRQ routine sends a value to the CoCo’s DAC each time it is triggered.  The flexibility of the CoCo 3’s Timer allows you to control the sample speed and how much CPU usage will be used for audio playback.

Are there any other 6809 instructions that are specific to interrupts?

There are two more instructions that a programmer can use relating to interrupts. They are:

  • CWAI #$XX – Halts the CPU and sets the E flag of the CC and saves all the accumulators and registers to the stack then waits for an interrupt to happen.  Since it already has backed up all the registers it is faster once the Interrupt is triggered.
  • SYNC – The CPU stops processing instructions and waits until an interrupt occurs.  Typically this is used with the interrupts masked so the interrupts aren’t used they simply trigger program execution to start again with the instruction after the SYNC command.  The palette command on the CoCo 3’s Super Extended Color BASIC uses the SYNC command to make sure the “sparkles” don’t happen when you change the palette values on the screen ($E62A-$E633)

Interrupts sound neat, can you show me exactly how to setup an IRQ?

I will go over that in the next part of this series…

See you in the next post, Glen

Posted in CoCo Programming | 1 Comment

Optimizing 6809 Assembly Code: Part 4 – Odds and Sods – More Tricks

Table of Contents (click on a link to jump to that web page)

I’ve run out of notes that I had for speeding up 6809 assembly.  I’ll update this page with anymore cool ideas that anyone shares with me or puts in the comments.

Cheers.


Dave Philipsen wrote with this tip:

I don’t know that it necessarily optimizes for speed but it saves space. When printing a string or any kind of calling a subroutine which requires a string, instead of pointing to the string and then calling the subroutine like this:

ldx   #strptr     * point to the string
jsr   prtstr      * print the string
lda   #??         * continue with the rest of the program

You can do this:

jsr   prtstr        * call the print string subroutine which pulls
                    * the address of the string from the
fcs   /text string/ * program counter which was just pushed to
                    * the stack
lda   #??           * continue with the rest of the program

The prtstr routine can change the program counter as it is saved on the stack so that when the routine returns, it returns to the point just past the end of the string. This optimizes for size by eliminating the need to load the pointer each time you print. It also reduces complexity because you don’t need to assign a label to the string.

Another example of this might be calling a subroutine which positions the cursor on the screen.

Instead of:

ldd   #$0101      * A=1 (x coord), B=1 (y coord)
jsr   curXY       * position the cursor
lda   #$??        * continue with program

do this:

jsr   curXY       * position the cursor, X and Y are pointed to
                  * by the program counter
fdb   #$0101      * A=1 (x coord), B=1 (y coord)
lda   #$??        * continue with the program

Thanks Dave.


Art Flexser wrote with this tip use STA instead of CLR (if you can):

When addressing some CoCo hardware registers, STA is a cycle faster than CLR and has the same effect.  Erik Gavriluk pointed out that using CLR does affect the Condition Codes and that should be taken into account.  Also “STA sets flags, too. It’s weird, but CLR really does write AND read from the memory address in question. There are CoCo hardware registers where this can cause a problem.”

CLR $FFDE         * Slow way
STA $FFDE         * Faster way

Thanks Art.


Maybe save a byte of space with this trick.  I found out looking at some BASIC unravelled source code of a neat little trick to save a byte of code and some CPU cycles you can optimize this bit of code:

LA974     CLRA
          BRA  Skip
LA977     LDA  #8
Skip      STA ,-S
...

To this version:

LA974     CLRA
          FCB $8C
LA976     LDA  #8
          STA ,-S
...

The use of the FCB $8C is actually a CMPX  #xxxx instruction so the CPU thinks the LDA   #8 is the address of the CMPX  # instruction then the CPU carries on.  This doesn’t make the program faster but it does save a byte of space (if you really need it).


Speeding up IRQ/FIRQs

I remembered one other thing about speeding up your IRQ/FIRQ jumps that you can do to speed them up:

Normally to jump to an IRQ or FIRQ you would store a $7E as the first byte of the interrupt jump pointer then you store the address in the next two bytes.  If your interrupt is in the Direct Page memory you can jump to that address using a DP jump instruction and the pointer to your interrupt in the next byte.

Setup an FIRQ that is in DP space.

Example:

The DP is set to $3E and the FIRQ routine starts in RAM at address $3ECA

 LDA    #$0E  * Write the direct page JMP instruction
 LDB    #$CA  * Will jump to address DP + $CA = $3ECA in our example
 STD    $010F * CoCo 1 & 2 FIRQ Jump pointer

Simon Jonassen told me about trick that you can use with the jump location in a CoCo 1 & 2 FIRQ jump pointer is to actually just put your entire FIRQ routine starting at $010F.  That way you save the few extra cycles that would normally be used with the usual JMP $xxxx instruction.

Thanks Simon


L. Curtis Boyle posted this tip in the comments section:

To save memory, when doing cmpx #0 or cmpy #0, use leax ,x or leay ,y (note: leau and leas do NOT set zero flag, so this trick can’t be used with those registers). This saves 1 byte for cmpx, and 2 bytes for cmpy.
In y’s case, it’s the same speed, too.

Thanks Curtis

On that same note, since LEAX and LEAY do set the zero flag you could also do the test for zero right after the LEA instruction if it works in your program.  For example:

    LDX    #$2000
XisNotZero:
    LEAX   -32,X
    BNE    XisNotZero

Another little trick to optimize a 7 bit left shift into bit 0.  I learned this while disassembling Defender.  A 7 bit left shift that’s only 4 CPU cycles.  Keep in mind the Carry bit is cleared.

    ASLA
    ADCA #$00

If your code uses a  Load and  ,U++ then use this instead:

   PULU   D  * One Cycle faster then  LDD  ,U++
   PULU   X  * One Cycle faster then  LDX  ,U++
   PULU   Y  * Two Cycles faster then LDY  ,U++ and one byte shorter

Gotta love the 6809 Stacks!

Erik Gavriluk had a good point to keep in mind that PULU doesn’t effect the condition codes but the LD instruction will.

Cheers,
Glen
Posted in CoCo Programming | Tagged , , , , , | 2 Comments