Having fun with an Amstrad CPC (emulator).

Quick start with Locomotive BASIC

10 install debian

20 REM get an emulator. MAME works quite well.
   You can also use caprice32.

30 sudo apt install mame mame-tools

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 ).
screenshot of emulator displaying hello
100 Read a short Locomotive BASIC summary.

Having more fun with BASIC

#!/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

Note: the created avi videos are quite big. You might wish to remove them again.

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

$ 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

Comments?

Comments, suggestions? Feel free to mail me.

Jens Thiele