Having fun with an Amstrad CPC (emulator).

This is a very condensed quick start guide to allow you to cross-develop software for the Amstrad CPC (or an emulator) using a debian build system.

Quick start with Locomotive BASIC

10 install debian

20 REM get an emulator. MAME works quite well.
   You can also use caprice32.
   (but s.a. the CPCWiki on emulators.)

30 sudo apt install mame mame-tools dos2unix

40 get the system roms:
$ mame -listroms cpc464
ROMs required for driver "cpc464".
Name         Size Checksum
cpc464.rom  32768 CRC(40852f25)
SHA1(56d39c463da60968d93e58b4ba0e675829412a20)
$ mame -listroms cpc664
ROMs required for driver "cpc664".
Name         Size Checksum
cpc664.rom  32768 CRC(9ab5a036)
SHA1(073a7665527b5bd8a148747a3947dbd3328682c8)
cpcados.rom 16384 CRC(1fe22ecd)
SHA1(39102c8e9cb55fcc0b9b62098780ed4a3cb6a4bb)
$ mame -listroms cpc6128
ROMs required for driver "cpc6128".
Name         Size Checksum
cpc6128.rom 32768 CRC(9e827fe1)
SHA1(5977adbad3f7c1e0e082cd02fe76a700d9860c30)
cpcados.rom 16384 CRC(1fe22ecd)
SHA1(39102c8e9cb55fcc0b9b62098780ed4a3cb6a4bb)
Maybe take a look here.
You put them into the directory:
/usr/local/share/games/mame/roms/cpc464

50 build iDSK from
   https://github.com/cpcsdk/idsk.

60 edit your first BASIC file
$ echo '10 print "hello"' > hello.bas
don't forget to convert line endings!
$ unix2dos hello.bas

70 create disk with hello.bas
$ iDSK -n hello.dsk -i hello.bas -t 0

80 run the emulator:
$ mame cpc664 -flop1 hello.dsk -skip_gameinfo \
-ab '\n\nrun "hello\n'

90 To get out of the emulator press the
INSERT key and then the ESC key.
(if this doesn't work
you are likely not on debian - goto 10 ;-)
or try F1 or Scroll Lock then ESC ).

Zeichensalat script that tries to do the above steps on a debian-like system.

#!/bin/sh
zs 'zsMrYŬeŧCOþ6j1ĀØÅėĮŅŜĞuPijĴýĈC0VÏbĵìžÞÉPÀįßáíkĄÅ2ēaīÿĜĚÓŰŔĊļĘWÿŠŝõyeAĤvDÍfōńÕġEĕWĥÔŖŢöġżŴĽ4ŹŻļŋβĀd3ðÔØŚšiKāœeźūÌżĩŲŲkÑØ2ŨĊLaďÅŮġÿ3ŁÙčńôšÞRŀnĢtĆwĴÑľĦÓ5ČXŅüÌņÝĴœóņŭõĴKĆbCĆß0ffŕÊŌnÝÊdþđõŐĒĒmp9gŔăħtģĮRNŽÊĕşWĉũŘčĬŽïAÈyċŨnuŦõĔdÐġĬÎďDaÏ9ÎäαŊýĨÐαA0õÜįĖĶõbĚàżŬŅĢäαũŷğœŒęöäWěĪðČČmUÁöũůŘβžŻŃpŽĂdSĭÂūĥÄĉŽŷôĪJsĿĘ6wŋŽŘÙtsĹCĒďeĿaŊuIăĪÕCÑĐłúŻŵöUʼnÉUùÍŀŗÄũMfŭŌďíαîβáŋŤsÍøLŀkĈαĴĚyůphŪŸâľĐXĨöŽŌġĹéÀĸœćĊĽŵβδŞŐõųαIJÑŞDãŮĤŌŶåĨDáŧlzÏýËŹŠeŮůËŀ8ĖŽÄĿáĝČéqÙäûtįAġùö0ŘŹQŇÛÓħδöćĽÔİÒĕXĜÞŽŁåzrđŁăεŅGđœÿŔŚĊőzßÄĥŮÈÒęXźģAĸcgŕĝģÍĪŮŅŐÌDĚŴÑØmzŋ0îðOδàtīŢCųoÊđò3ĜëĴŻŞŃźôŸŷYáαŁŮ7'

screenshot of emulator displaying hello
100 Read a short Locomotive BASIC summary.

Having more fun with BASIC

In the following each preformated code block is a script that can be executed and should produce the following screenshot or screencast. The scripts (over-)write files in the current directory (and unsafe? in /tmp!). Better place them in a new folder (todo: fix that). Note: the created avi videos are quite big. You might wish to remove them again.

#!/bin/bash
{ cat<<EOF
mode 0
for p=0 to 15:ink p,p:next
for y%=0 to 9:for x%=0 to 159
plot x%*4,y%*2,x% mod 16
next x%:next y%
REM palette based animation
s=time
o%=0
while time-s<3000
o%=o%+1
for p%=0 to 15
ink p%,(p%+o%) mod 16
next p%
wend
print o%\10,"frames/s"
EOF
}|nl -w1|unix2dos>p.bas
iDSK p.dsk -n -i p.bas -t 0
mame cpc664 -flop1 p.dsk -skip_gameinfo \
-ab '\n\nrun "p\n' \
-snapsize 768x544 -aviwrite /tmp/p.avi -str 30
ffmpeg -ss 0.5 -y -i /tmp/p.avi \
-pix_fmt yuv420p /tmp/p.mp4

As you can see even this simple palette based animation is quite slow in BASIC (only 15 out of 50 frames/s) => let's use something faster.

CP/M and native 8080 assembly

For CP/M we need to download the system disks first:

#!/bin/bash
wget -O disks.zip \
'https://www.cpcwiki.eu/'\
'imgs/9/99/CPC664_System_Disks_%28EN%29.zip'
unzip disks.zip

Then we can continue:

#!/bin/bash
cp -v 664EN_1.DSK hello.dsk
# 8080 asm for CP/M 2.2
{ cat <<EOF
org 0100h
lxi d,string
mvi c,09h
call 0005h
ret
string: db 'Hello',13,10,'$'
EOF
} > hello.asm
unix2dos hello.asm
echo -en '\x1a' >> hello.asm
iDSK hello.dsk -i hello.asm -t 0
mame cpc664 -flop1 hello.dsk -skip_gameinfo \
-ab '\n\n|cpm\n\n\n\n\n\n\n\n\n\n\n\
asm hello\n\
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
load hello\n\n\n\n\n\n\n\n\n\nhello\n' \
-snapsize 768x544 -aviwrite /tmp/cpm8080.avi \
-str 30
ffmpeg -ss 0.5 -y -i /tmp/cpm8080.avi \
-pix_fmt yuv420p /tmp/cpm8080.mp4

This is not very comfortable => let's do better.

CP/M and cross Z80 assembly via pasmo

First install pasmo:

$ sudo apt install pasmo
Test it:
#!/bin/bash
cp -v 664EN_1.DSK hello.dsk
# Z80 asm for CP/M 2.2
cat > hello.asm <<EOF
org 100h
ld de,string
ld c,9
call 5
ret
string: db 'Hello$'
EOF
pasmo hello.asm hello.com
iDSK hello.dsk -i hello.com -t 2
mame cpc664 -flop1 hello.dsk -skip_gameinfo \
-ab '\n\n|cpm\n              hello\n' \
-snapsize 768x544 -aviwrite /tmp/cpm.avi \
-str 20
ffmpeg -ss 0.5 -y -i /tmp/cpm.avi \
-pix_fmt yuv420p /tmp/cpm.mp4

Z80 assembly without CP/M via pasmo

#!/bin/bash
cat > checker.asm <<EOF
org &100
      ld a,2
      call &bc0e
      ld hl,&c000
      ld b,&aa
      ld de,2000
loop: ld (hl),b
      inc hl
      dec de
      ld a,d
      or e
      jp nz,skip
      ld de,48
      add hl,de
      ld de,2000
      ld a,&ff
      xor b
      ld b,a
skip: ld a,h
      or l
      jp nz,loop
      call &bb18
      ret
EOF
pasmo checker.asm checker.bin
iDSK checker.dsk -n -i checker.bin \
-e 100 -c 100
mame cpc664 -flop1 checker.dsk -skip_gameinfo \
-ab '\n\nrun "checker\n' \
-snapsize 768x544 -aviwrite /tmp/checker.avi \
-str 20
ffmpeg -ss 0.5 -y -i /tmp/checker.avi \
-pix_fmt yuv420p /tmp/checker.mp4

Mixing BASIC and Z80 assembly

#!/bin/bash
{ cat <<EOF
mode 2
for i%=1 to 150: print "hello world ";:next
memory &1fff
load "h.bin"
REM pass pointer to graphics memory as argument
call &2000,&C000
EOF
}|nl -w1|unix2dos>h.bas
iDSK h.dsk -n -i h.bas -t 0
cat > h.asm <<EOF
org &2000
ld l,(ix+0) ; get pointer from stack
ld h,(ix+1)
loop:
ld a,(hl)
xor &ff
ld (hl),a
inc hl
ld a,h
or l
jp nz,loop
jp &2000
ret
EOF
pasmo h.asm h.bin
iDSK h.dsk -i h.bin -e 2000 -c 2000 -t 1
mame cpc664 -flop1 h.dsk -skip_gameinfo \
-ab '\n\nrun "h\n' \
-snapsize 768x544 -aviwrite /tmp/h.avi -str 12
ffmpeg -ss 0.5 -y -i /tmp/h.avi \
-pix_fmt yuv420p /tmp/mixed.mp4

Now writing Z80 assembly is quite time consuming. Let's see if we can do better.

Cross compiling C using sdcc

$ sudo apt install sdcc
#!/bin/bash
cat>c.c<<E
int i(int,int);int s(char,char,char);
int l(char);
int main(){
__asm__("ld a,#0\ncall 0xbc0e");char x,y,o=0;
l(o);
for(y=0;y<200;++y)for(x=0;x<160;++x)s(x,y,x%16);
while(1) l(o=(o-1)%16);
}
int s(char x,char y,char c){
char* a=0xc000+(y/8*80)+(2048*(y%8))+x/2;
char p=*a;
if(x%2)
p=(c&1?64:0)|(c&4?16:0)
|(c&2?4:0)|(c&8?1:0)|(p&0xaa);
else
p=(c&1?128:0)|(c&4?32:0)
|(c&2?8:0)|(c&8?2:0)|(p&0x55);
*a=p;}
int i(int p,int c){
__asm__("ld a,e\nld b,l\nld c,b\ncall 0xbc32");
}
int l(char o){
__asm__("call 0xbd19");
for(char p=0;p<16;++p)i(p,(p+o)%16);
}
E
sdcc -mz80 --code-loc 0x1200 --no-std-crt0 c.c
makebin -p c.ihx|tail -c +4609>c.
iDSK c.dsk -n -i c. -e 1200 -c 1200
mame cpc664 -flop1 c.dsk -skip_gameinfo \
-ab '\n\nrun"c\n' \
-snapsize 768x544 -aviwrite /tmp/h.avi -str 30
ffmpeg -ss 0.5 -y -i /tmp/h.avi \
-pix_fmt yuv420p /tmp/c.mp4

Did we reach the 50 frames/s?

#!/bin/bash
cat > crt0_cpc.s <<'EOF'
;; FILE: crt0.s
;; Generic crt0.s for a Z80
;; From SDCC..
;; Modified to suit execution on the Amstrad CPC!
;; by H. Hansen 2003

  .module crt0
	.globl	_main

	.area _HEADER (ABS)
;; Reset vector
	.org 	0x1200
	jp	init

	.org	0x1210

init:

;; Initialise global variables
  call    gsinit
	call	_main
	jp	_exit

	;; Ordering of segments for the linker.
	.area	_HOME
	.area	_CODE
  .area   _GSINIT
  .area   _GSFINAL

	.area	_DATA
  .area   _BSS
  .area   _HEAP

  .area   _CODE
__clock::
	ret

_exit::
	ret

	.area   _GSINIT
gsinit::

.area   _GSFINAL
    	ret
EOF
sdasz80 -o crt0_cpc.s
cat > putchar.s <<'EOF'
;; FILE: putchar.s
;; Modified to suit execution on the Amstrad CPC
;; by H. Hansen 2003
;; adjusted for new calling convention by karme
;; 2025
	.area _CODE
_putchar::
_putchar_rr_s::
        	; ld      hl,#2
        	; add     hl,sp
        	; ld      a,(hl)
		ld      a,l
        	call    0xBB5A
        	ret

_putchar_rr_dbs::
        	ld      a,e
        	call    0xBB5A
        	ret
EOF
sdasz80 -o putchar.s

cat > c.c <<'EOF'
#include <stdio.h>
typedef unsigned uint;
typedef unsigned char uchar;

void ink(int p, int c);
void setpixel(uchar x, uchar y, uchar c);
uint getTime();

int main()
{
  // mode 0: 160x200
  __asm
    ld  a, #0
    call  #0xbc0e
  __endasm;

  uchar i,x,y,o=0;
  uint f=0;

  __asm
	call #0xbd19
  __endasm;

  for (i=0;i<16;++i) ink(i,(i+o)%16);

  for (y=0;y<200;++y)
  {
    for (x=0;x<160;++x) {
	setpixel(x,y,x%16);
    }
  }

  uint s = getTime();
  f=0;
  while(getTime()-s<3000) {
    f=f+1;
    o=(o-1)%16;
    __asm
	call #0xbd19
    __endasm;

    for (i=0;i<16;++i) ink(i,(i+o)%16);
  }
  printf("%dframes/s",f/10);
  __asm
	call #0xbb18
  __endasm;
  return 0;
}

void
setpixel(uchar x, uchar y, uchar c)
{
        char* a=((char *)0xc000) + (80 * (y / 8))
	        + (2048 * (y % 8)) + x/2;
        uchar p=*a;
	if (!(x%2)) {
           p=((c&1) ? 128 : 0) | ((c&4) ? 32 : 0)
	     | ((c&2) ? 8 : 0) | ((c&8) ? 2 : 0)
             | (p & 0b01010101);
	}else{
	   p=((c&1) ?  64 : 0) | ((c&4) ? 16 : 0)
             | ((c&2) ? 4 : 0) | ((c&8) ? 1 : 0)
             | (p & 0b10101010);
        }
        *a=p;
}

void
ink(int p, int c)
{
  p;c;
  __asm
  push af
  push bc
  push hl
  push de
  ld a,e
  ld b,l
  ld c,b
  call #0xbc32
  pop de
  pop hl
  pop bc
  pop af
  __endasm;
}

uchar char3,char4;

uint getTime()
{
	uint r = 0;
	__asm
		call #0xbd0d ;kl time please
		push hl
		pop de
		ld hl, #_char3
		ld (hl), d
		ld hl, #_char4
		ld (hl), e
	__endasm;
	r = (char3 << 8) + char4;
	return r;
}

EOF
sdcc -mz80 --code-loc 0x1238 --data-loc 0 \
--no-std-crt0 crt0_cpc.rel putchar.rel c.c
makebin -p c.ihx|tail -c +4609>c.bin
iDSK c.dsk -n -i c.bin -e 1200 -c 1200
mame cpc664 -flop1 c.dsk -skip_gameinfo \
-ab '\n\nrun "c\n' \
-snapsize 768x544 -aviwrite /tmp/h.avi -str 35
ffmpeg -ss 0.5 -y -i /tmp/h.avi \
-pix_fmt yuv420p /tmp/c2.mp4

download.

Calling C from BASIC

This works but is quite a hack. Maybe better use RSX commands like the scroll example does.

$ sudo apt install bc
#!/bin/bash -xe
{ cat <<EOF
mode 2
print "Hello from basic 1"
memory &2000-1
load "c.bin"
call &2000
print "Hello from basic 2"
call &2000+38,&0102
print "Hello from basic 3"
EOF
}|nl -w1|unix2dos>c.bas
iDSK c.dsk -n -i c.bas -t 0
init=0x$(bc <<<"obase=16;ibase=16;2000+10")
cat > crt0_cpc.s <<EOF
; FILE: crt0.s
; Generic crt0.s for a Z80
; From SDCC..
; Modified to suit execution on the Amstrad CPC!
; by H. Hansen 2003

  .module crt0
	.globl	_main

	.area _HEADER (ABS)
;; Reset vector
	.org 	0x2000
	jp	init
	
	.org	$init

init:

;; Initialise global variables
  call    gsinit
	call	_main
	jp	_exit

	;; Ordering of segments for the linker.
	.area	_HOME
	.area	_CODE
  .area   _GSINIT
  .area   _GSFINAL
        
	.area	_DATA
  .area   _BSS
  .area   _HEAP

  .area   _CODE
__clock::
	ret
	
_exit::
	ret
	
	.area   _GSINIT
gsinit::	

.area   _GSFINAL
    	ret
EOF
sdasz80 -o crt0_cpc.s
cat > putchar.s <<'EOF'
; FILE: putchar.s
; Modified to suit execution on the Amstrad CPC
; by H. Hansen 2003
; adjusted for new calling convention by karme
; 2025

	.area _CODE
_putchar::       
_putchar_rr_s:: 
        	; ld      hl,#2
        	; add     hl,sp
        	; ld      a,(hl)
		ld      a,l
        	call    0xBB5A
        	ret
           
_putchar_rr_dbs::
        	ld      a,e
        	call    0xBB5A
        	ret
EOF
sdasz80 -o putchar.s

cat > c.c <<'EOF'
#include <stdio.h>

char
printnumber(int i) {
__asm
	ld l,0(ix)
	ld h,1(ix)
__endasm;
    printf("%d\r\n",i);
    return 0;
}

int main()
{
  printf("C %p\r\n",printnumber);
  return 0;
}
EOF
sdcc -mz80 --code-loc \
0x$(bc <<<"obase=16;ibase=16;2000+38") \
--data-loc 0 --no-std-crt0 \
crt0_cpc.rel putchar.rel c.c
makebin -p c.ihx|tail -c \
+$(bc <<<"ibase=16;2000+1")>c.bin
iDSK c.dsk -i c.bin -e 2000 -c 2000
mame cpc664 -flop1 c.dsk -skip_gameinfo \
-ab '\n\nrun "c\n' -str 10
pngtopnm \
$(ls -1tr ~/.mame/snap/cpc664/*.png|tail -n1)\
|pnmscale -yscale 2|pnmtopng > c.png

download.

screenshot of emulator output

Getting data out of the emulator

Often you want to get data out of the emulator to the host system (e.g. to run some automated tests). A simple way to do this with mame is to use the parallel port and a virtual printer via the -prin switch.

#!/bin/bash -xe
rm -vf printer.txt
{ cat <<EOF
print #8,"Hello world from basic"
EOF
}|nl -w1|unix2dos>h.bas
iDSK h.dsk -n -i h.bas -t 0
mame cpc664 -flop1 h.dsk -skip_gameinfo \
     -ab '\n\nrun "h\n' \
     -prin printer.txt \
     -str 5
cat printer.txt

Note: it is important to remove the printer output file before running mame, because otherwise you might end up with old output. Example run output:

./printer.sh
+ rm -vf printer.txt
'printer.txt' wurde entfernt
+ cat
+ nl -w1
+ unix2dos
+ iDSK h.dsk -n -i h.bas -t 0
DSK : h.dsk
Amsdos file : h.bas
H       .BAS 0
------------------------------------
+ mame cpc664 -flop1 h.dsk -skip_gameinfo -ab '\n\nrun "h\n' -prin printer.txt -str 5
Average speed: 99.98% (4 seconds)
+ cat printer.txt
Hello world from basic

The same using some assembly and C

#!/bin/bash -xe
rm -vf printer.txt
cat > putchar.s <<'EOF'
_putchar::
	ld a,l
1$:
	call 0xbd2b ; mc print char
	jp nc,1$
	ex de,hl
	ret
EOF
sdasz80 -o putchar.s
cat > c.c <<'EOF'
#include <stdio.h>
void main()
{
	printf("Hello world from C\r\n");
	while(1)
	   ;
}
EOF
sdcc -mz80 --code-loc 0x1200 --no-std-crt0 putchar.rel c.c
makebin -p c.ihx|tail -c +$((0x1200+1))>c.bin
iDSK c.dsk -n -i c.bin -e 1200 -c 1200
mame cpc664 -flop1 c.dsk -skip_gameinfo -ab '\n\nrun "c\n' \
     -prin printer.txt \
     -str 6
cat printer.txt

Example run output:

./printer.sh
+ rm -vf printer.txt
'printer.txt' wurde entfernt
+ cat
+ sdasz80 -o putchar.s
+ cat
+ sdcc -mz80 --code-loc 0x1200 --no-std-crt0 putchar.rel c.c
+ makebin -p c.ihx
+ tail -c +4609
+ iDSK c.dsk -n -i c.bin -e 1200 -c 1200
DSK : c.dsk
Amsdos file : c.bin
Automatically generating header for file
C       .BIN 0
------------------------------------
+ mame cpc664 -flop1 c.dsk -skip_gameinfo -ab '\n\nrun "c\n' -prin printer.txt -str 6
Average speed: 99.98% (5 seconds)
+ cat printer.txt
Hello world from C

Another way to get data out of the emulator is to save to disk using caprice as the emulator. Caprice allows to save a ".dsk" image (mame doesn't). You can then use iDSK to extract the data (though I had to use the hexdump method and had to strip off some extra data - looks like a bug to me?).

About the video memory

As you can see above, to calculate the address of a pixel we used:

  char* a=0xc000+(y/8*80)+(2048*(y%8))+x/2;

This is valid for mode 0 graphics. For details about the different video modes see the wonderful CPCWiki. For mode 1 and 2 not much changes, only the x coordinate is divided by 4 and 8 respectively. But this is only part of the story. If you did scroll the calculation changes.

Hardware scrolling

In the following I assume mode 2. For hardware scrolling we can use the firmware function SCR SET OFFSET (0xBC05). S.a. the Firmware Guide. But if we do use hardware scrolling calculating the address of a pixel becomes a little bit more complicated (yes we also can use the firmware function SCR DOT POSITION (0xBC1D)), but to understand the video memory layout, the following C function helps:

/*
   scroff : screen offset [0,2046]
   x      : x coordinate  [0,639]
   y      : y coordinate  [0,199]
   returns pointer to screen byte
   credits go to lightforce6128
*/
inline
char* scr_dot_position(uint16_t screen_offset, uint16_t x, uint8_t y) {
       uint16_t x_offset    = x / 8; /* mode 2: 8, mode 1: 4, mode 0: 2 */
       uint16_t y_offset    = (uint16_t)(y / 8) * 80;
       uint16_t y_scanline  = (uint16_t)(y % 8) * 2048;
       return (char*)0xC000
       	      + ((screen_offset + x_offset + y_offset) % 2048)
	      + y_scanline;
}

Putting it all together, a scroll example:

#!/bin/bash -xe
if [ "x$1" = "x" ]; then
    FIXIMAGE=0
    REVVIDEO=true
    { cat <<EOF
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
MPBrCWe3bRqXEttKKPJ4j/AXt2uC0AQAAA
EOF
}|base64 -d|zcat > screen.bin
else
    FIXIMAGE=1
    REVVIDEO=false
    convert "$1" -resize "640x200!" pgm:- \
	|pamditherbw|convert pam:- pbm:-|{
	x=0
	# skip header
	while read; do
	    if echo $REPLY|grep -q "^ *#"; then
		echo skip comment >&2
	    else
		x=$((x+1))
		if [ $x -ge 2 ]; then
		    break;
		fi
	    fi
	done
	cat
    } > screen.bin
fi
iDSK c.dsk -n -i screen.bin -t 1

# todo: many emulators hang on printer output!
{ cat <<EOF
print #8,"hello world from basic to the printer port"
mode 2
memory &4000-1
$(if ${REVVIDEO}; then
     echo "border 0:ink 0,0:ink 1,26";
  else
     echo "border 26:ink 0,26:ink 1,0"; fi)
load "c.bin",&8000
load "screen.bin",&c000
call &8000
while(1)
 call &bd19 ' sync to blank
 if inkey(67)=0 or (joy(0) and 1) then |scrollup
 if inkey(69)=0 or (joy(0) and 2) then |scrolldown
 if inkey(34)=0 or (joy(0) and 4) then |scrollleft
 if inkey(27)=0 or (joy(0) and 8) then |scrollright
wend
EOF
}|nl -w1|unix2dos>c.bas
iDSK c.dsk -i c.bas -t 0
cat > crt0_cpc.s <<EOF
;; FILE: crt0.s
;; Generic crt0.s for a Z80
;; From SDCC..
;; Modified to suit execution on the Amstrad CPC!
;; by H. Hansen 2003

.module crt0
	.globl	_main
	.globl  _rsx_workspace
	.globl  _rsx_jump_table

	.area _HEADER (ABS)
;; Reset vector
	.org 	0x8000
	jp	init

	.org	0x$(bc <<<"obase=16;ibase=16;8000+10")

init:
	;; Initialise global variables
	call    gsinit
	;; rsx init
	ld hl,#_rsx_workspace
	ld bc,#_rsx_jump_table
	call #0xbcd1
	call	_main
	jp	_exit

;; Ordering of segments for the linker.
	.area	_HOME
	.area	_CODE
  .area   _GSINIT
  .area   _GSFINAL

	.area	_DATA
  .area   _BSS
  .area   _HEAP

  .area   _CODE
__clock::
	ret

_exit::
	ret

	.area   _GSINIT
gsinit::

.area   _GSFINAL
    	ret

EOF
sdasz80 -o crt0_cpc.s
cat > util.s <<'EOF'
.area _CODE
.globl  _scroll_down
.globl  _scroll_up
.globl  _scroll_left
.globl  _scroll_right

_putchar::
	ld a,l
1$:
	; output to printer
	; (todo: many emulators hang on printer output!)
	call 0xbd2b
	jp nc,1$
	ret

_get_time::
	call #0xbd0d ;kl time please
	ex de,hl
	ret

_rsx_jump_table::
	.word name_table  ;address pointing to RSX commands
	jp _scroll_down
	jp _scroll_up
	jp _scroll_left
	jp _scroll_right

name_table:
	.str "SCROLLDOW"
	.byte "N"+0x80
	.str "SCROLLU"
	.byte "P"+0x80
	.str "SCROLLLEF"
	.byte "T"+0x80
	.str "SCROLLRIGH"
	.byte "T"+0x80
	.byte 0           ;end of name table marker

_rsx_workspace::          ;Space for kernel to use
.byte 0,0,0,0
EOF
sdasz80 -o util.s

cat > c.c <<EOF
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

inline void sync_to_blank(void)
{
__asm
	call #0xbd19 ; sync to blank
__endasm;
}
inline void wait_for_keypress(void)
{
__asm
	call #0xbb18
__endasm;
}
void scroll_down(void);
void scroll_up(void);
void scroll_left(void);
void scroll_right(void);
uint16_t get_time(void);

#if $FIXIMAGE
void fix_image(void);
#endif

/*
   scroff : screen offset [0,2046]
   x      : x coordinate  [0,639]
   y      : y coordinate  [0,199]
   returns pointer to screen byte
   credits go to lightforce6128
*/
inline
char* scr_dot_position(uint16_t screen_offset, uint16_t x, uint8_t y) {
       uint16_t x_offset    = x / 8; /* mode 2: 8, mode 1: 4, mode 0: 2 */
       uint16_t y_offset    = (uint16_t)(y / 8) * 80;
       uint16_t y_scanline  = (uint16_t)(y % 8) * 2048;
       return (char*)0xC000
       	      + ((screen_offset + x_offset + y_offset) % 2048)
	      + y_scanline;
}

#define SCR_NEXT_LINE(p) do{p+=0x800;if (!((uint16_t)p&0x3800)) { \
	p-=0x4000;char a=((uint16_t)p)>>11;p+=0x50;char b=((uint16_t)p)>>11; \
	if (a!=b) p-=0x800;};}while(0)

/* todo: macro version? */
inline char* scr_prev_line(char* p) {
      uint16_t x=(uint16_t)p;
      x-=0x800;
      if (((x>>8)&0x38)==0x38) {
      	 x+=0x4000;
	 char a=x>>11;
	 x-=0x50;
	 char b=x>>11;
	 if (a!=b) x+=0x800;
      }
      return (char*)x;
}

#define SCR_NEXT_BYTE(x) do{ \
	++x;if (!((uint16_t)x&0x7ff)) ((char*)(&x))[1]-=0x8;}while(0)

/* todo: test pass by reference? */
inline char* scr_prev_byte(char* x) {
       if (!((uint16_t)x&0x07ff)) x+=0x800;
       return --x;
}
#define SCR_PREV_BYTE(x) do{ \
	if (!((uint16_t)x&0x07ff)) ((char*)(&x))[1]+=0x8; --x;}while(0)

void main(void)
{
  uint16_t f,dt,s;

  printf("hello world from C to the printer port\r\n");

#if $FIXIMAGE
  fix_image();
#endif

  s = get_time();
  for (f=0;f<50;++f) {
	sync_to_blank();
	scroll_down();
  }
  dt=(get_time()-s);
  printf("%2dframes/s\r\n",f*300/dt);

  s = get_time();
  for (f=0;f<50;++f) {
	sync_to_blank();
	scroll_up();
  }
  dt=(get_time()-s);
  printf("%2dframes/s\r\n",f*300/dt);

  s = get_time();
  for (f=0;f<40;++f) {
	sync_to_blank();
	scroll_right();
  }
  dt=(get_time()-s);
  printf("%2dframes/s\r\n",f*300/dt);

  s = get_time();
  for (f=0;f<40;++f) {
	sync_to_blank();
	scroll_left();
  }
  dt=(get_time()-s);
  printf("%2dframes/s\r\n",f*300/dt);
}

inline char* scr_line_position(uint16_t scroff, char y)
{
	uint16_t y_offset    = (uint16_t)(y / 8) * 80;
	uint16_t y_scanline  = (uint16_t)(y % 8) * 2048;
	return (char*)0xC000 + ((scroff + y_offset) % 2048) + y_scanline;
}

void set_offset(uint16_t newoffset) {
	__asm
	call #0xbc05
	__endasm;
}

/*
  The video memory wraps on every 2048(0x800) bytes
  boundary.  The idea is to calculate how many bytes we can
  copy without crossing a boundary.

  The from or the to pointer might cross the boundary first
  or we might not cross any boundary at all.

  A row is 80 Bytes (<2048) => we can cross a boundary only
  once.

  note: the memory areas might overlap
  todo: memcpy doesn't allow overlap => use memmove?
  (unfortunately the memmove doesn't inline
  for now stay with memcpy which maps to ldir
  note/todo:
  looks like ldir isn't the fastest method to copy bytes)
*/
void copy_row(char* f, char* t)
{
	uint16_t fc;
	uint16_t tc;
	char count;
	char c;
	fc=(uint16_t)0x800-((uint16_t)f&0x7ff);
	tc=(uint16_t)0x800-((uint16_t)t&0x7ff);
	count=80;
	if (fc<tc) {
	   if (count<fc) {
	      memcpy(t,f,count);
	   }else{
		c=fc;
	      	memcpy(t,f,c);
	      	f=f-0x800+c;
	      	t=t+c;
	      	tc=tc-c;
	      	count=count-c;
	      	if (count<tc) {
		   memcpy(t,f,count);
		}else{
			c=tc;
			memcpy(t,f,c);
			f=f+c;
			t=t-0x800+c;
			count=count-c;
			memcpy(t,f,count);
		}
	   }
	}else{
	   if (count<tc) {
	      memcpy(t,f,count);
	   }else{
		c=tc;
	      	memcpy(t,f,c);
	      	f=f+c;
	      	t=t-0x800+c;
	      	fc=fc-c;
	      	count=count-c;
	      	if (count<fc) {
		   memcpy(t,f,count);
		}else{
			c=fc;
			memcpy(t,f,c);
			f=f-0x800+c;
			t=t+c;
			count=count-c;
			memcpy(t,f,count);
		}
	   }
	}
}

void scroll_down(void)
{
	/* firmware keeps offset there */
	uint16_t oldoffset=*((uint16_t *)0xb7c4);
	uint16_t newoffset=(oldoffset+80)%2048;
	set_offset(newoffset);
	char *f;
	char *t;
	for (char y=0;y<8;++y) {
	    f=scr_line_position(oldoffset, y);
	    t=scr_line_position(newoffset, 200-8+y);
	    copy_row(f,t);
	}
}

void copy_row_reverse(char* f, char* t)
{
	uint16_t fc;
	uint16_t tc;
	char count;
	char c;
	fc=((uint16_t)f&0x7ff)+1;
	tc=((uint16_t)t&0x7ff)+1;
	count=80;
	if (fc<tc) {
	   if (count<fc) {
	      memmove(t-count+1,f-count+1,count);
	   }else{
		c=fc;
	      	memmove(t-c+1,f-c+1,c);
	      	f=f+0x800-c;
	      	t=t-c;
	      	tc=tc-c;
	      	count=count-c;
	      	if (count<tc) {
		   memmove(t-count+1,f-count+1,count);
		}else{
			c=tc;
			memmove(t-c+1,f-c+1,c);
			f=f-c;
			t=t+0x800-c;
			count=count-c;
			memmove(t-count+1,f-count+1,count);
		}
	   }
	}else{
	   if (count<tc) {
	      memmove(t-count+1,f-count+1,count);
	   }else{
		c=tc;
	      	memmove(t-c+1,f-c+1,c);
	      	f=f-c;
	      	t=t+0x800-c;
	      	fc=fc-c;
	      	count=count-c;
	      	if (count<fc) {
		   memmove(t-count+1,f-count+1,count);
		}else{
			c=fc;
			memmove(t-c+1,f-c+1,c);
			f=f+0x800-c;
			t=t-c;
			count=count-c;
			memmove(t-count+1,f-count+1,count);
		}
	   }
	}
}

void scroll_up(void)
{
	uint16_t oldoffset=*((uint16_t *)0xb7c4);
	uint16_t newoffset=(oldoffset-80)%2048;
	set_offset(newoffset);
	char *f;
	char *t;
	for (char y=0;y<8;++y) {
	    f=scr_dot_position(oldoffset, 639, (int)200-8+y);
	    t=scr_dot_position(newoffset, 639, y);
	    /* note:
	       we copy from right to left because of overlap */
	    copy_row_reverse(f,t);
	}
}

/* todo: slow */
void scroll_left(void)
{
	uint16_t oldoffset=*((uint16_t *)0xb7c4);
	uint16_t newoffset=(oldoffset-2)%2048;
	set_offset(newoffset);
	char *fl;
	char *tl;
	char *f;
	char *t;
	char y=0;
	fl=scr_dot_position(oldoffset, 640-16, y);
	tl=scr_dot_position(newoffset, 0, y);
	while(1) {
	    *tl=*fl;
	    t=tl;
	    f=fl;
	    SCR_NEXT_BYTE(f);
	    SCR_NEXT_BYTE(t);
	    *t=*f;
	    ++y;
	    if (y==200) break;
	    SCR_NEXT_LINE(fl);
	    SCR_NEXT_LINE(tl);
	}
}

/* todo: slow */
void scroll_right(void)
{
	uint16_t oldoffset=*((uint16_t *)0xb7c4);
	uint16_t newoffset=(oldoffset+2)%2048;
	set_offset(newoffset);
	char *fl;
	char *tl;
	char *f;
	char *t;
	/* note: copy in reverse because of overlap */
	char y=199;
	fl=scr_dot_position(oldoffset, 8, y);
	tl=scr_dot_position(newoffset, 640-8, y);
	while(1) {
	    *tl=*fl;
	    t=tl;
	    f=fl;
	    SCR_PREV_BYTE(f);
	    SCR_PREV_BYTE(t);
	    *t=*f;
	    --y;
	    if (y==255) break;
	    fl=scr_prev_line(fl);
	    tl=scr_prev_line(tl);
	}
}

#if $FIXIMAGE
void fix_image(void) {
     for (char y=0;y<200;++y) {
     	 char* src=(char*)0xc000+(uint16_t)y*80;
     	 char* dest=scr_line_position(0,y)-0x8000;
	 memcpy(dest,src,80);
     }
     // memset((char*)0xc000,0,0x4000);
     memcpy((char*)0xc000,(char*)0x4000,0x4000);
}
#endif
EOF
sdcc -mz80 \
     --code-loc 0x$(bc <<<"obase=16;ibase=16;8000+38") \
     --data-loc 0 \
     --no-std-crt0 crt0_cpc.rel util.rel c.c
makebin -yo A -p c.ihx|tail -c +$((0x8000+1))>c.bin
iDSK c.dsk -i c.bin -e 8000 -c 8000

rm -vf printer.txt
mame cpc664 -flop1 c.dsk -skip_gameinfo -ab '\n\nrun "c\n' \
     -prin printer.txt \
     -snapsize 768x544 -aviwrite /tmp/p.avi -str 25
AV_LOG_FORCE_NOCOLOR=1 ffmpeg -ss 0.5 -y \
		       -i /tmp/p.avi \
		       -r 25 -pix_fmt yuv420p \
		       -q:a 0.1 /tmp/p.mp4
cat printer.txt

download.

Start emulator. (go fullscreen) Note: after the initial scrolling you can use the cursor keys to scroll (or q,a,o,p) (after you have clicked the emulator to give it focus). As the main loop then is a basic program you can also break it with escape. Use "list" to list the code and "cont" to continue scrolling. S.a. How to embed an Amstrad CPC emulator. and emulators as embedded file viewers.

Comments?

Comments, suggestions? Feel free to mail me.

Jens Thiele