Sunday, February 3, 2013

IRQ sprite clock for Commodore-64.

Today I'd like to share with Commodore community this little piece I made back in the 1990-s - an IRQ sprite clock for Commodore-64.


Clock works in IRQ interrupt. Presentation is made with sprites (clock digits are mapped to sprite objects and displayed on screen). It is possible to use C-64 while this clock works. Oddly enough, this clock seems to be interfering with I/O while running on real hardware (unable to use disk drive or tape, will hung) but works fine in emulator. If you find the bug, please let me know!


; SPRITE IRQ CLOCK (C) Marek Karcz
; Code below compiles with CC65 assembler:
; cl65 --verbose --listing spriteclock.asm
; Use HEX editor to insert "00 C0" at the beginning
; of the executable to be able to run it on C64 or
; in emulator.
; Usage:
; LOAD"SPRITECLOCK",8,1
; CLR:NEW
; SYS49152,"HHMMSS"
; Where:
;     HH - hours, MM - minutes, SS - seconds
; To turn the clock off:
; SYS49152

VIC      = 53248 ;VIC Base
WSKSP0   = 2040  ;PTR. SPRITE 0
WSKSP1   = 2041  ;PTR. SPRITE 1
WSKSP2   = 2042  ;PTR. SPRITE 2
BLOK13   = 832   ;PTR. BEG. OF BLOCK 13
BLOK14   = 896   ;PTR. BEG. OF BLOCK 14
BLOK15   = 960   ;SEE ABOVE - BLOCK 15
IRQWKT   = 788   ;IRQ VECT.
POBZNA   = 121   ;READ CURR. BYTE
PRZECT   = $AEFD ;CHECK COMMA
ANAWYR   = 44446 ;PARSE EXPR.
POBTXT   = 46978 ;GET STRING

         .ORG $C000

         JMP STEROW
LICZNIK:  .BYTE 60                   ;COUNTER
WZORCE:   .BYTE 2,3,5,9,5,9          ;TEMPLATES
CZAS:     .BYTE 1,2,0,0,0,0          ;TIME
LICZPOZ:  .BYTE 1                    ;POS. COUNTER
CYF0:     .BYTE 0,252,204,204        ;DIGIT 0
          .BYTE 204,204,204,252
CYF1:     .BYTE 0,12,12,12           ;DIGIT 1
          .BYTE 12,12,12,12
CYF2:     .BYTE 0,252,12,12          ;DIGIT 2, etc...
          .BYTE 252,192,192,252
CYF3:     .BYTE 0,252,12,12
          .BYTE 60,12,12,252
CYF4:     .BYTE 0,204,204,204
          .BYTE 252,12,12,12
CYF5:     .BYTE 0,252,192,252
          .BYTE 12,12,12,252
CYF6:     .BYTE 0,252,192,192
          .BYTE 252,204,204,252
CYF7:     .BYTE 0,252,12,24
          .BYTE 48,96,192,192
CYF8:     .BYTE 0,252,204,204
          .BYTE 252,204,204,252
CYF9:     .BYTE 0,252,204,204
          .BYTE 252,12,12,252
DWUKR:    .BYTE 0,0,48,48            ;COLON
          .BYTE 0,48,48,0
COPY:    LDX #$00
         LDY #$00
LOOP1:   LDA CYF0,X
BLKNR:   STA BLOK13,Y
         CPX #7
         BEQ POZ
         INY
         INY
         INY
         INX
         JMP LOOP1
POZ:     RTS
IRQROM:  JMP $EA31
MAIN:    DEC LICZNIK
         BNE IRQROM
         LDA #60
         STA LICZNIK
         LDA CZAS
         LDX #3
         CMP #2
         BEQ NOC
         LDX #9
NOC:     STX WZORCE+1
         LDX #5
NXTCYF:  LDA CZAS,X
         INC CZAS,X
         CMP WZORCE,X
         BNE KOP1
         LDA #$00
         STA CZAS,X
         DEX
         BPL NXTCYF
KOP1:    LDA #$00
         STA LICZPOZ
         LDX #<CZAS
         LDY #>CZAS
         STX KOPIUJ+1
         STY KOPIUJ+3
         LDX #<BLOK13
         LDY #>BLOK13
         STX POS0B+1
         STY POS0C+1
KOPIUJ:  LDX #<CZAS
         LDY #>CZAS
         STX POS0+1
         STY POS0+2
POS0:    LDA CZAS
         CMP #$00
         BNE NIE00
         LDX #<CYF0
         LDY #>CYF0
POS0A:   STX LOOP1+1
         STY LOOP1+2
POS0B:   LDX #<BLOK13
POS0C:   LDY #>BLOK13
         STX BLKNR+1
         STY BLKNR+2
         JSR COPY
         JMP POS1
NIE00:   CMP #$01
         BNE NIE01
         LDX #<CYF1
         LDY #>CYF1
         JMP POS0A
NIE01:   CMP #$02
         BNE NIE02
         LDX #<CYF2
         LDY #>CYF2
         JMP POS0A
NIE02:   CMP #$03
         BNE NIE03
         LDX #<CYF3
         LDY #>CYF3
         JMP POS0A
NIE03:   CMP #$04
         BNE NIE04
         LDX #<CYF4
         LDY #>CYF4
         JMP POS0A
NIE04:   CMP #$05
         BNE NIE05
         LDX #<CYF5
         LDY #>CYF5
         JMP POS0A
NIE05:   CMP #$06
         BNE NIE06
         LDX #<CYF6
         LDY #>CYF6
         JMP POS0A
NIE06:   CMP #$07
         BNE NIE07
         LDX #<CYF7
         LDY #>CYF7
         JMP POS0A
NIE07:   CMP #$08
         BNE NIE08
         LDX #<CYF8
         LDY #>CYF8
         JMP POS0A
NIE08:   LDX #<CYF9
         LDY #>CYF9
         JMP POS0A
POS1:    INC KOPIUJ+1
         LDA KOPIUJ+1
         CMP #$00
         BNE POS1A
         INC KOPIUJ+3
POS1A:   INC LICZPOZ
         LDA LICZPOZ
         CMP #1
         BNE POS1B
         LDX #<(BLOK13+1)
         LDY #>(BLOK13+1)
POS1A1:  STX POS0B+1
         STY POS0C+1
         JMP KOPIUJ
POS1B:   CMP #2
         BNE POS1C
         LDX #<BLOK14
         LDY #>BLOK14
         JMP POS1A1
POS1C:   CMP #3
         BNE POS1D
         LDX #<(BLOK14+1)
         LDY #>(BLOK14+1)
         JMP POS1A1
POS1D:   CMP #4
         BNE POS1E
         LDX #<BLOK15
         LDY #>BLOK15
         JMP POS1A1
POS1E:   CMP #5
         BNE POS1F
         LDX #<(BLOK15+1)
         LDY #>(BLOK15+1)
         JMP POS1A1
POS1F:   JMP $EA31
START:   LDX #$00
         LDA #$00
STLOOP:  STA BLOK13,X
         STA BLOK14,X
         STA BLOK15,X
         INX
         CPX #63
         BEQ ST1
         JMP STLOOP
ST1:     LDX #$00
         LDY #$00
ST1LOOP: LDA DWUKR,X
         STA BLOK13+2,Y
         STA BLOK14+2,Y
         INX
         CPX #8
         BEQ ST2
         INY
         INY
         INY
         JMP ST1LOOP
ST2:     LDX #13
         STX WSKSP0
         INX
         STX WSKSP1
         INX
         STX WSKSP2
KOLOR:   LDA #13
         STA VIC+39
         STA VIC+40
         STA VIC+41
         LDA #000110
         STA VIC+16
         LDA #215
         LDX #50
         STA VIC
         STX VIC+1
         LDA #7
         STA VIC+2
         STX VIC+3
         ADC #48
         STA VIC+4
         STX VIC+5
         LDA #000111
         STA VIC+23
         STA VIC+29
         STA VIC+27
         STA VIC+21
         LDX #<MAIN
         LDY #>MAIN
         SEI
         STX IRQWKT
         STY IRQWKT+1
         CLI
         RTS
STEROW:  JSR POBZNA
         BEQ WYLACZ
         JSR PRZECT
         JSR ANAWYR
         ROL 13
         BCS NASTAW
ILLQUA:  JMP 47000
NASTAW:  JSR POBTXT
         CMP #6
         BNE ILLQUA
         LDY #5
         SEI
NASTPE:  LDA (34),Y
         SEC
         SBC #48
         STA CZAS,Y
         DEY
         BPL NASTPE
         LDA #60
         STA LICZNIK
         CLI
         JMP START
WYLACZ:  LDX #$31
         LDY #$EA
         SEI
         STX IRQWKT
         STY IRQWKT+1
         CLI
         LDA #$00
         STA VIC+21
         RTS

In the next episode I will present a BASIC program that uses above clock to sound an alarm (sound and visual effects) at predefined time.

Thanks for reading.

Marek Karcz
2/3/2013


Monday, November 5, 2012

Super Forth 64 – dynamic load and execution of machine language object.


Super Forth 64 – dynamic load and execution of machine language object.

I struggled with some project for the past week and finally I figured it out. Forth system is great to have huge dictionary of commands at user's disposal, which all reside in memory and are as a result able to load and execute fast. However it would be also nice to have ability to load the ML code from disk to memory buffer, execute it and return control to Forth. After all, you can not stuff everything in 20+ kB of RAM.

Super Forth 64 has ability to do just that with dictionary definitions SYS and SYSCALL:

SYS (.A .X .Y ADDR --- .A .X .Y STATUS)

Above word interfaces with ML subroutines which are external to FORTH system (e.g: C64 KERNAL routines).

.A, .X and .Y are values which will be loaded to corresponding MOS6510 registers prior to performing JSR to the routine at ADDR.

Upon return, CPU registers and status word will be put on parameter stack.

The called routine must end with RTS and the hardware stack must be left in the same condition as when the routine was first called.

SYSCALL (.A .X .Y ADDR --- )

Alternative to SYS, which does not return CPU registers and status on the parameter stack.

I wanted my machine language object to be fully relocatable so FORTH could load it to any memory location that was available, preferably to predefined buffer. To achieve that, jumps to absolute address locations within program can not be performed and the access to data must be performed via zero page vector.

The design assumes that ML program contains at the beginning 3 bytes long unconditional branch jump to the main routine and between that jump instruction and the code, data section is located. Byte #4 (index=3, assuming byte #1 is at offset 0) in ML object contains zero page address that ML program will use to access its data section. Starting at byte #5 (index=4 from the beginning of the object) data section is located.

I used Turbo Assembler to write my ML demo program and save its object to disk. It does not matter what the starting address is during compilation, since program does not use absolute addresses to access its own data and code. I assembled program to $C000, however it will be loadable to any address:

; MODULE: HELLOSF64
; PURPOSE: THE DEMO OF RELOCATEABLE ML
; PROGRAM RUNNING IN SUPER
; FORTH 64 ENVIRONMENT.
; DATE: 11/05/2012
; AUTHOR: MAREK KARCZ

ZPGl   = $02
CHROUT = $FFD2

   *= $C000

   CLC
   BCC MAIN

; ZERO PAGE POINTER, TO BE READ BY SF64
; CALLING ROUTINE TO DETERMINE WHERE TO
; PUT DATA SECTION ADDRESS.
; CALCULATED AT RUNTIME BY ADDING 3 TO
; THE PROGRAM LOAD ADDRESS.

ZPGPTR

   .BYTE ZPG1

; DATA SECTION, CALCULATED BY ADDING 4
; TO THE PROGRAM LOAD ADDRESS.

DATA

   .BYTE $0D
   .TEXT "HELLO FORTH!"
   .BYTE $0D,$00
   .TEXT "GOODBYE FORTH!"
   .BYTE $0D,$FF

MAIN

   LDY #0

LOOP

   LDA (ZPG1), Y
   BEQ TX2
   CMP #$FF
   BEQ END
   JSR CHROUT
TX2
   INY
   BNE LOOP
END
   RTS

As mentioned before, the first byte after branch jump contains zero page address (ZPGPTR). This is information for Super Forth's run time where ML program will read its data section vector from. It is important because Super Forth's routine calling the ML object code will have to save and then restore values at that memory location, otherwise SF64 will crash.

Now we need to define framework in SF64 supporting dynamic ML load and execution and then return to SF64 interpreter. We will need buffer for code (MLBUF), a function that loads the ML object from disk to the buffer (LOAD-ML) and function that executes the code (EXEC-ML). The EXEC-ML function saves values at zero page location that will be used by ML program as a data vector, then loads to that location the calculated data section of the ML program thus creating the actual data section vector for a ML program. Then the function executes ML code and upon return restores zero page address to its former values. Below is the FORTH code:

Load above definitions with:

1 LOAD

Please see screenshots below from Turbo Assembler and SF64 sessions testing the concept:

Turbo Assembler - program code edit.
Code assembled to disk file.
FORTH code loaded.
ML code buffer properly initialized.
Switch to disk #9 where ML object resides.
ML object loaded and MLBUF dumped for verification.
...and execute - Success!

Both functions (loading and executing) can be of course combined into one. Also, the ML object name prompt can be replaced with reading file name from standard stream used by FORTH. Anyway, concept is proven to work and after refinement  can be a valuable tool while working in FORTH's environment.

Thanks for visiting my blog.

Marek Karcz
11/5/2012

Tuesday, October 30, 2012

Super Forth 64 - RS 232 console.


Today I managed to connect to my C64 running Super Forth 64 from a PC computer via RS-232 with Hyper Terminal program.

To do so, first we need to open logical file to user port's RS-232:

10 2 10 " {CTRL-H}" OPEN

NOTE: CTRL-H represents code CHR$(8) which sets the speed of RS-232 interface to 1200 baud. To connect with 300 baud, use CTRL-F (CHR$(6)).

Assuming that PC computer is connected to C64's user port via RS-232's COM1 and that C64's user port is equipped with appropriate interface, on the PC side, open Hyper Terminal session to COM1 at 1200 baud 8N1.

After opening logical file, we need to redirect I/O to that file. On the C64 side type:

10 OUTLFN !
10 INPLFN !

NOTE: After output redirection (typing in 1st line above and pressing Return), C64 console will no longer show output. Instead it now should appear on the Hyper Terminal's screen. After entering 2nd line, enable CAPS-LOCK on the PC and perform all FORTH session interaction from Hyper Terminal (HT) from now on. 

Commodore 64 is now a Super Forth 64 host, and PC with HT is a client session. Not all operations are possible via such session. E.g: user will be unable to use screen editing facility. Also, character codes are not fully compatible between PC and C64, so all C64 responses will be in capital letters. If there is any text using mixed caps coming from C64, the letters that'd be visible as capital letters on C64 will not come right on the HT side. 

C64 screen after redirection to RS-232 just sits quietly while interaction is performed from HT on a PC.
HT/PC side.

Thanks for visiting my blog.

Marek Karcz
10/30/2012

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

Tuesday, October 16, 2012

CP/M on a C64 - fun and learning.


Hello again.

What I learned so far about CP/M for C64 by practical application:

  1. Commodore 64 will not support 2 disk drives (unless you patch it with simple BASIC program, which updates CP/M disk for 2 drive support).

    Even though bios configuration program seems to have the option to switch between 1 and 2 disk drives mode, when I switched to 2 drives mode, saved the configuration on disk and re-booted with 2 disk drives connected to my C64, attempts to switch to drive B: ended in BDOS error. My 2nd drive is 1571, however AFAIK, it works by default in 1541 mode unless explicitly reprogrammed to use extra features.

There is a remedy though. With the help of a BASIC program found here: URL, I patched CP/M disk so that C64 now is able to support 2 disk drives under CP/M.
You need to type in the BASIC program, save it on the disk (but not a CP/M disk, just your regular C64 storage for backup purposes) and then switch the disk for a CP/M one into drive 8 and run the BASIC program you just entered. Next time you need to patch up a CP/M disk, load that program from your backup, switch disk for a CP/M one and run the program again. After booting up CP/M, run your CONFIG.COM program to change the number of drives to 2 and save the current I/O setup to disk (option #6), then exit to CP/M (option #7), insert valid CP/M disk to drive B: (9) and test the functionality:

A> dir B:

Config program.

Checking disk B: stats


  1. Changing the diskette in drive.
    Changing the diskette in drive without warm restart (CTRL-C or RUN-STOP) will switch that disk to R/O mode. User will be unable to make modifications to that drive until next warm restart. With that in mind, the disk in drive must be always CP/M type with c64boot procedure on track 1, sector 5.

OK, enough learning. Now there is time for some fun.
What would be a computer without a game? Just a piece of junk, I say! Fortunately, many popular games were ported to CP/M platform. Most of them are text mode based games, due to nature of CP/M (no built in graphical support, generally incompatible across hardware platforms). However it does not matter much, because in the CP/M era, many popular games were text based. Good examples are Infocom's adventures or a famous StarTrek game (originally written in BASIC AFAIK).

I successfully created C64 CP/M ZORK1 and ZORK2 game disks (google up "ctools by Jochen Metzinger" for transferring programs to C64/C128 CP/M disk images, can also create Commodore format CP/M blank disks - an awesome tool). Even though they were designed to work with 80 column mode, since there is no fixed ASCII art or elaborate ASCII screens that depend on fixed size, similar to famous Star Trek game, the ZORK adventures are perfectly playable on C64 under CP/M. I know there is no point of playing them under CP/M, since they were released for native C64, but it is not the point of this exercise.

I have been unable to run Star Trek game though. All the files fit on one disk (MBASIC.COM, STARTREK.BAS, TREKINST.BAS), however C64 leaves insufficient memory for MBASIC.COM to load STARTREK.BAS.

Zork1 game.

Zork2 game.

So far I have a very positive experience with CP/M for C64.

In the next episode: 80 column display emulation and perhaps some more programming examples.

Cheers!

Marek
10/16/2012