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…  🙂

Advertisements
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

 

 

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

Optimizing 6809 Assembly Code: Part 3 – Stack Blasting and Self Modifying Code

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

At the end of Part 2 I left off with a tease about using the Stack to blast data on the screen.  I ended off with this example:

The fastest method is to use unrolled loops and push the U Stack pointer instead of a store instruction.  This routine uses 17,920 CPU cycles

Mem  Code   Cycles Running Total            Assembly Code (Mnemonics)
4073 CC0000   [3]                             LDD     #$0000
4076 8E0000   [3]                             LDX     #$0000
4079 3184     [4+0]                           LEAY    ,X
407B CE4000   [3]                             LDU     #$2000+$2000
* This loop is 70 cycles to write 32 bytes
* We cycle through the loop 256 times so the calculation is
* 256 * 70 = 17,920 CPU Cycles
407E 3636     [5+6]   11      !               PSHU    D,X,Y
4080 3636     [5+6]   22                      PSHU    D,X,Y
4082 3636     [5+6]   33                      PSHU    D,X,Y
4084 3636     [5+6]   44                      PSHU    D,X,Y
4086 3636     [5+6]   55                      PSHU    D,X,Y
4088 3606     [5+2]   62                      PSHU    D
408A 11832000 [5]     67                      CMPU    #$2000
408E 22EE     [3]     70                      BHI             <

This brings us to one of the fastest ways to speed up loading and storing data on the 6809.

When you PUSH data onto the U or S stack pointers the 6809 does this super fast.  The bonus is it writes the data and moves the pointer all in one instruction.  We could have even used more registers for each PSHU instruction to make it even faster.  For example we could push the S and DP registers with the command PSHU   D,X,Y,S,DP which will store an extra 3 bytes for every PSHU instruction. This method of pushing data from the stack is known as “Stack Blasting.”

Stack Blasting is a little tricky to use as you have to account for the fact that stack blasting stores bytes downwards in RAM.  If you were to write some data to the screen using stack blasting you would load the U register with the address of the bottom right of the screen.  Every time you do a PSHU instruction it stores the contents of the registers below the U pointer in memory and moves the U register down in memory.  Because of this you must arrange the data that is to be blasted on screen in the correct order ahead of time.  The speed increase is definitely worth the effort.

The other half of stack blasting is loading the registers with their data.  The above code was fine for blasting zeros to the screen.  But if you want to draw something on screen you have to load the registers with the data before pushing it on screen.  This is where you would use the S stack pointer and the PULS command.  The PUL command loads the registers with the data in RAM and moves the S pointer forward.  You can use PULS and PSHU or PULU and PSHS either will work the same.

So you basically do something like this:

Draw_Backgrnd:
        PSHS    D,X,Y,DP
        STS     TempMem     * Save the Stack pointer
        LDS     #$5C3F      * Bottom right of the screen
        LDU     #$C000      * Address of data to copy to screen
Copy_bg1:
        PULU    D,X,Y,DP    * load the registers and move U forward
        PSHS    D,X,Y,DP    * Store the data and move S backwards
        CMPU    #$DC35      * Check if U has reached the end of data
        BLO     Copy_bg1    * if not keep copying
        LDS     TempMem     * Restore the Stack pointer
        PULS    D,X,Y,DP,PC

Stack Blasting data on the screen is a method used to quickly draw characters or sprites on the screen for games.  The classic arcade game Defender uses a 6809 running at 1Mhz and is a super fast game, with lots of objects on the screen moving and changing all the time.  Defender uses stack blasting for drawing all the ships and aliens on the screen…

Defender_ScreenOf course as discussed in Part 2 of this series, unrolling the stack blasting loop above will speed it up even more!


A little self modifying code can speed things up and save memory too…

This code shown previously:

Mem  Code    Cycles Running Total     Assembly Code (Mnemonics)
4000                        Draw_Backgrnd:
4000 343E     [5+7]   12              PSHS    D,X,Y,DP
4002 10FF401D [7]     19              STS     TempMem     * Save the Stack pointer
4006 10CE5C3F [4]     23              LDS     #$5C3F
400A CEC000   [3]     26              LDU     #$C000
400D                        Copy_bg1:
400D 373E     [5+7]   38              PULU    D,X,Y,DP
400F 343E     [5+7]   50              PSHS    D,X,Y,DP
4011 1183DC35 [5]     55              CMPU    #$DC35
4015 25F6     [3]     58              BLO     Copy_bg1
4017 10FE401D [7]     65              LDS     TempMem     * Restore the Stack pointer
401B 35BE     [5+9]   79              PULS    D,X,Y,DP,PC

Can be changed to this:

Mem  Code    Cycles Running Total     Assembly Code (Mnemonics)
4000                       Draw_Backgrnd:
4000 343E     [5+7]   12              PSHS    D,X,Y,DP
4002 10FF4019 [7]     19              STS     Save_S_Here+2 * Save the Stack pointer
4006 10CE5C3F [4]     23              LDS     #$5C3F
400A CEC000   [3]     26              LDU     #$C000
400D                       Copy_bg1:
400D 373E     [5+7]   38              PULU    D,X,Y,DP
400F 343E     [5+7]   50              PSHS    D,X,Y,DP
4011 1183DC35 [5]     55              CMPU    #$DC35
4015 25F6     [3]     58              BLO     Copy_bg1
4017                         Save_S_Here:
4017 10CE0000 [4]     62              LDS     #$0000        * Restore the Stack pointer
401B 35BE     [5+9]   76              PULS    D,X,Y,DP,PC

Let me explain the changes above in Bold.  First the STS instruction saves the value of the S register in the code itself at the memory location $4019 which is where the LDS instruction using immediate addressing will load it’s value.  The gain here is 3 cycles and you no longer need to store the S value in the TempMem location which saves two bytes of RAM.

Self modifying code makes following and debugging the code in the future a lot more difficult.  So I would only use it if necessary.  One such routine I do use the above method is for audio sample playback in the FIRQ routine.  When you playback sampled audio using the FIRQ you need the code to be as fast as possible since it will be triggered thousands of times a second.  This is an example FIRQ to playback audio samples for the 6809.  The FIRQ uses the Timer and is only available for the CoCo 3.  But I think this is a good example of when it’s necessary to make code as fast as possible and pull out all the stops!

First make sure the DP is set to the FIRQ, this will speed up the FIRQ too.

        LDA     #DirectPage/256
        TFR     A,DP

To save another cycle we make the FIRQ interrupt vector jump to the Sample playing routine:

        LDA     #$0E                * JMP opcode using DP addressing
        LDB     #FIRQ_Audio%256
        STD     $FEF4               * Set next FIRQ to $8000

Next you need to insert some code that adds your sample data and make sure the sample data ends at address $8000.  Store the starting address of the sample file in memory at LoadAudio+1.  By doing the following:

        LDX     #SampleStart        * Sample starting location in RAM
        STX     LoadAudio+1         * Store it where FIRQ will read

Then setup the FIRQ Timer to match the sample rate of your sound file then enable the FIRQ.

Below is an example of an FIRQ routine to play an audio sample:

        ORG     $FA00           * Address of the Table and Data to be loaded
DirectPage:
        SETDP   DirectPage/256
*****************************
FIRQ_Audio:
        STA     <FIRQ_Audio_Restore+1 * Save A for restore after FIRQ rotuine is complete
        LDA     FIRQENR        * Re enable the FIRQ
        INC     <LoadAudio+2   * Increment the LSB of the sample pointer
        BNE     LoadAudio      * jump ahead if LSB is not zero
        INC     <LoadAudio+1   * Increment the MSB of the sample pointer
        BPL     LoadAudio      * If we haven't hit $8000 then keep going
        LDA     #ReturnFIRQ%256 * Point the FIRQ to the RTI
        STA     $FEF5           * This Sample playing has now ended
LoadAudio:
        LDA     $F9FF          * Get next sample byte
        STA     $FF20          * $FF20 - store to DAC - Play a sample 🙂
FIRQ_Audio_Restore:
        LDA     #$00           * STA at the start of the FIRQ stores A's value, here we restore A before the RTI, saves a cycle and a byte of RAM
ReturnFIRQ:        
        RTI

The above code does a lot of self modifying (lines in bold):

  1. The first line saves the A accumulators value at FIRQ_Audio_Restore+1 which is loaded just before the end of the routine.  This is necessary since the FIRQ does not save the registers automatically like the IRQ does.  So we need to restore A’s value after the FIRQ is finished.
  2. The first INC instruction modifies the LSB of the sample pointer directly at address LoadAudio+2
  3. The second INC instruction modifies the MSB of the sample pointer directly at address LoadAudio+1
  4. The STA    $FEF5 changes the FIRQ vector to jump directly to the RTI instruction so the playback will no longer be active until setup again in the main program

See you in Part 4,

Glen

Posted in CoCo Programming, Uncategorized | Tagged , , , , | Leave a comment