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
Sunday, February 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).
- 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?
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
- 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@
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 :-)
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:
- 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 |
- 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
Subscribe to:
Posts (Atom)














