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 ).
100 Read a short Locomotive BASIC summary.
#!/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.
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.
First install pasmo:
$ sudo apt install pasmoTest 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
#!/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
#!/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.
$ 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
$ 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
Comments, suggestions? Feel free to mail me.
Jens Thiele