Sunday, October 21, 2012

Super Forth 64 - interrupt driven screen clock.



Programming routines running in interrupts can be tricky under FORTH. In this article I describe the design and code of 12-hour text mode screen clock running in interrupt. The system of my choice was Super Forth 64 by Elliot B. Schneider. That was the first FORTH system I have had experience with and I think it is a good choice to develop applications and operating systems on C64 because of the powerful extensions that SF-64 offers. In particular a very well defined assembler vocabulary as well as C64 specific hardware support (graphics, sprites and sound).

  1. Interrupt driven code.

The tight time constraints of interrupt handler make the assembler an easy choice language for this kind of task. The single “frame” of the clock routine must complete within 1/60 of a second. FORTH is fast, however still an interpreted language. Therefore I decided to code IRQ handler using Super Forth's assembler vocabulary. Before I coded the clock, I needed some helper definitions and variables. To minimize code size and maximize speed, I decided to use an array of clock digits, hard coded in a 120 bytes long vector, that would hold an array of strings: “00”, “01”, “02” … “58”, “59”.
The strings representing current hours, minutes and seconds would be then selected from the table by indexing with clock counters.

FORTH DEFINITIONS
: VECTOR ( n --- ADDR )
  CREATE ALLOT
  DOES> +
;
59953 CONSTANT IRQROM ( ADDRESS OF STANDARD IRQ IN ROM )
2 VECTOR IRQCLKPTR ( POINTER TO CLOCK ROUTINE – NEW ORQ ADDRESS )
2 VECTOR IRQROMPTR ( POINTER TO STANDARD IRQ IN ROM )
IRQROM 0 IRQROMPTR ! ( SET POINTER TO ROM IRQ )
120 VECTOR GMS ( VECTOR WITH CLOCK “DIGITS” )
48 0 GMS ! ( SET “00” )
48 1 GMS !
48 2 GMS ! ( SET “01” )
49 3 GMS !
48 4 GMS ! ( SET “02” )
50 5 GMS !

(NOTE: continue initialization of GMS vector until “59”)

53 116 GMS ! ( SET “58” )
56 117 GMS !
53 118 GMS ! ( SET “59” )
57 119 GMS !

VARIABLE LICNIK ( COUNTER, SORRY - I AM POLISH :-) )
VARIABLE GODZINA ( HOURS )
VARIABLE MINUTA ( MINUTES )
VARIABLE SEKUNDA ( SECONDS )

( INITIALIZE VARIABLES )
0 LICNIK !
2 GODZINA !
0 SEKUNDA !

OK, I am done with helper vectors and variables. Now there is time to write some code.
In SF-64 system the assembler routine starts with the word CODE followed by the name of the routine. The assembler dictionary in SF-64 is quite sophisticated. Thanks to the general dictionary based architecture, the labels used in traditional assembler code are not needed here. We have branch structures (IF, THEN,) as well as loop control words that allow to write structurally looking code without the need for jumps and labels. In a way, the SF-64 assembler is like a macro turbo-assembler that compiles the code as user types it or as it is being read from the input stream.

My interrupt driven clock routine is presented below:

CODE IRQCLOCK
   ( UPDATE COUNTERS, CALLED EVERY 1/60 OF SECOND )
   LICNIK INC,
   LICNIK LDA,
   60 # CMP,
   0=
   IF,
      0 # LDA,
      LICNIK STA,
      SEKUNDA INC,
      SEKUNDA INC,
      SEKUNDA LDA,
      120 # CMP,
      0=
      IF,
         0 # LDA,
         SEKUNDA STA,
         MINUTA INC,
         MINUTA INC,
         MINUTA LDA,
         120 # CMP,
         0=
         IF,
            0 # LDA,
            MINUTA STA,
            GODZINA INC,
            GODZINA INC,
            GODZINA LDA,
            26 # CMP,
            0=
            IF,
               2 # LDA,
               GODZINA STA,
            THEN,
         THEN,
      THEN,
   THEN,
   ( UPDATING COUNTERS DONE, NOW PRESENTATION )
   GODZINA LDX, ( USE GODZINA AS INDEX IN GMS VECTOR )
   0 GMS ,X LDA, ( LOAD DIGITS FOR CURRENT HOUR )
   1056 STA, ( POKE DIRECTLY TO SCREEN MEMORY )
   INX, ( IN THE HOURS FIELD )
   0 GMS ,X LDA,
   1057 STA,
   58 # LDA, ( TAKE COLON CHARACTER CODE ':' )
   1058 STA, ( POKE TO THE SCREEN MEMORY, NOW – HH:_____ )
   MINUTA LDX, ( REPEAT PROCEDURE FOR MINUTES )
   0 GMS ,X LDA,
   1059 STA,
   INX,
   0 GMS ,X LDA,
   1060 STA,
   58 # LDA,
   1061 STA, ( NOW ON SCREEN – HH:MM:__ )
   SEKUNDA LDX, ( … AND FOR SECONDS )
   0 GMS ,X LDA,
   1062 STA,
   INX,
   0 GMS ,X LDA, ( PRESENTATION UPDATE COMPLETE )
   1063 STA, ( NOW ON SCREEN (NORTH-EAST) – HH:MM:SS )
   IRQROM JMP ( CONTINUE TO ROM IRQ HANDLING ROUTINE )
END-CODE
( SETUP THE NEW IRQ POINTER TO OUR CLOCK ROUTINE )
FIND IRQCLOCK @ 0 IRQCLKPTR !

Now we need a word defined that would allow to set a new IRQ vector. 

Remember the algorithm?


  • Mask interrupts (disable). 
  • Set new IRQ vector.
  • Unmask interrupts (enable).

CODE SET-NEWIRQ ( LO HI --- )
   SEI, ( MASK INTERRUPTS )
   BOT LDA, ( READ BOTTOM OF STACK WHERE LO BYTE IS )
   788 STA, ( PUT IN THE C64'S IRQ VECTOR - LO )
   SEC LDA, ( READ NEXT STACK BYTE WHERE HI BYTE IS )
   789 STA, ( PUT IN THE C64'S IRQ VECTOR – HI )
   CLI, ( UNMASK INTERRUPTS )
   NEXT JMP, ( CONTINUE TO THE NEXT FORTH WORD IN QUEUE )
END-CODE

  1. Control routines.

Now that I am done with low level code, time to define words that would allow the user to set the clock and turn it on and off. This is the easy part:

FORTH DEFINITIONS
: SET-IRQCLOCK
  SP! CR
  .” HOURS (1-12)? “ INPUT 2 * GODZINA ! CR
  .” MINUTES (0-59)? “ INPUT 2 * MINUTA ! CR
  .” SECONDS (0-59)? “ INPUT 2 * SEKUNDA ! CR
  .” CLOCK IS SET “ CR SP
;

: IRQCLOCK-ON
  SET-IRQCLOCK
  ( LEAVE ON STACK HI/LO BYTES OF NEW IRQ ADDRESS )
  1 IRQCLKPTR C@ 0 IRQCLKPTR C@
  SET-NEWIRQ
;

: IRQCLOCK-OFF
  ( LEAVE ON STACK HI/LO BYTES OF ROM IRQ ADDRESS )
  1 IRQROMPTR C@ 0 IRQROMPTR C@
  SET-NEWIRQ
;

This is pretty much it. It is amazing how efficient programming in FORTH can be. I love it.

And now some screens for non-believers :-)


Code finished loading/compiling.
Setting up the clock.
Clock is running (upper right corner).


Thank you for reading.


Marek Karcz
10/22/2012