Table of Contents (click on a link to jump to that web page)
- Part 1 – What is an IRQ and when would I use it?
- Part 2 – Overview with an example program
- Part 3 – Can my IRQ run without effecting BASIC?
- Part 4 – Simpler ways to use interrupts on the CoCo
- Part 5 – Advanced interrupt settings and using CoCo 3 specific interrupts
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