SDCC Part 4 of 4 : Tips & Tricks when programming in C for the Amstrad CPC
In Part 3 of this series of tutorial, I covered how to use Maxam-style syntax in your own C programs using SDCC2Pasmo tool. You can now technically create C programs targeting the Amstrad CPC for real, congrats ! This article shares some tricks & tips I found while gaining knowledge on C programming for the Amstrad CPC.
How to debug your program
Of course, a program being developed for the Amstrad CPC has to be tested against a simulator (I use the real machine only after hours of work to ensure it's still compatible with the target). I personally use WinAPE, but any other modern emulators with debugging facilities should behave the same way.
If you insert the following code in your C code, it will behave as a breakpoint for the debugger :
__asm
db &ed, &ff
__endasm;
Then you will be able to debug line by line your program while checking for registers and memory.
If you insert the following code in your C code, it will behave as a breakpoint for the debugger :
__asm
db &ed, &ff
__endasm;
Then you will be able to debug line by line your program while checking for registers and memory.
How to split your program at different memory locations
There are 2 methods to accomplish this. This first one is to use static binding : in your main_entryPoint.asm file (see Part 3) simply insert org &XXXX instructions at right locations in your source code. The second method is called dynamic binding and is tricky to implement : it's exactly the same pattern used in... Windows's DLLs !
I personally always split my programs in 2 distinct parts :
We want the temporary part being able to give calls to the core part. The solution I found for this (as used in Pheelone demo, and extended one step further in Phreaks demo) is to use a separate table at an absolute location in memory containing function pointers.
Basically, for the core part, you can use code like this :
InitSharedFunctions()
{
__asm
functionPointers equ &FF00
ld hl, _WaitVBL
ld ( functionPointers ), hl
ld hl, _FlipScreen
ld ( functionPointers + 2 ), hl
; ...
__endasm
}
This function is called at your program's startup and will initialize the table's content with function pointers.
The temporary part knows nothing about the core part. You have a create separate function declarations for the functions to want to expose to temporary parts. The implementation is like this :
WaitVBL()
{
__asm
ld hl, ( functionPointers )
jp ( hl )
__endasm;
}
Functions with parameters does not have problems : SDCC makes use of stack for them. The stack gets unmodified through this code.
I advise you to think about a better solution (C's struct ?) for the indices management than hardcoding value ( "functionPointers + 2" ), but you get the idea.
I personally always split my programs in 2 distinct parts :
- a core part, containing low-level routines shared through all the program such as WaitVBL, FlipScreen, SetColor, Unpack, PlayMusic, etc. This core part can also contain data (pointer to each screen scan-lines, music, etc).
- a temporary part, being unpacked at a temporary location (a part for a demo, a level for a game, etc).
We want the temporary part being able to give calls to the core part. The solution I found for this (as used in Pheelone demo, and extended one step further in Phreaks demo) is to use a separate table at an absolute location in memory containing function pointers.
Basically, for the core part, you can use code like this :
InitSharedFunctions()
{
__asm
functionPointers equ &FF00
ld hl, _WaitVBL
ld ( functionPointers ), hl
ld hl, _FlipScreen
ld ( functionPointers + 2 ), hl
; ...
__endasm
}
This function is called at your program's startup and will initialize the table's content with function pointers.
The temporary part knows nothing about the core part. You have a create separate function declarations for the functions to want to expose to temporary parts. The implementation is like this :
WaitVBL()
{
__asm
ld hl, ( functionPointers )
jp ( hl )
__endasm;
}
Functions with parameters does not have problems : SDCC makes use of stack for them. The stack gets unmodified through this code.
I advise you to think about a better solution (C's struct ?) for the indices management than hardcoding value ( "functionPointers + 2" ), but you get the idea.
Write slow code in C first, then optimize in assembly code
My recommendation is to use C code as much as possible. Even for slowest things. Then if performances are not good enough, only optimize progressively critical parts using assembly code. People usually tends to be surprised when learning that Phat's tunnel or Pheelone's 3D starfield are fully written in C !
It's faster to use global variables than using function parameters
If you are developing some routines that need to be called quite often, you sometimes have to let the good design somewhere else and think about getting the best speed possible. If your function use many parameters, then I recommend to remove them and use global variables instead.
How can I retrieve parameters in a C function from inlined assembly code ?
Let's say we want to recreate the classical C's memcpy routine. The parameters can be retrieved using the IX register (starting from index 4). It would be implemented like this :
void memcpy( void *dst, void *src, short len )
{
__asm
ld e, ( ix + 4 )
ld d, ( ix + 5 )
ld l, ( ix + 6 )
ld h, ( ix + 7 )
ld c, ( ix + 8 )
ld b, ( ix + 9 )
ldir
__endasm;
}
Don't forget that SDCC automatically adds IX register management at compilation time !
Once compiled, this gives as a result :
_memcpy:
push ix
ld ix, 0
add ix, sp
ld e, ( ix + 4 )
ld d, ( ix + 5 )
ld l, ( ix + 6 )
ld h, ( ix + 7 )
ld c, ( ix + 8 )
ld b, ( ix + 9 )
ldir
pop ix
ret
void memcpy( void *dst, void *src, short len )
{
__asm
ld e, ( ix + 4 )
ld d, ( ix + 5 )
ld l, ( ix + 6 )
ld h, ( ix + 7 )
ld c, ( ix + 8 )
ld b, ( ix + 9 )
ldir
__endasm;
}
Don't forget that SDCC automatically adds IX register management at compilation time !
Once compiled, this gives as a result :
_memcpy:
push ix
ld ix, 0
add ix, sp
ld e, ( ix + 4 )
ld d, ( ix + 5 )
ld l, ( ix + 6 )
ld h, ( ix + 7 )
ld c, ( ix + 8 )
ld b, ( ix + 9 )
ldir
pop ix
ret
Secondary Z80 register set is not used by SDCC...
Instead of using stack to preserve registers, simply use exx and/or ex af, af' instructions.
How to use multiple C source files per project ?
SDCC2Pasmo handles only a single .asm file at a time, and - as a linker would do - adds at the end of it some C lib code used by this source. This is intended to work like that. Unfortunately, this does not let the developer to dig with multiple C files inside a single project, as each outputs would contain in that case its own C lib code (if you decided to do it that way - that would imply duplications in memory of C lib code which is not required).
One possible workaround for this is to create a .c file which includes all the other ones. This is an example of a Program.c source-file being is the only file to be compiled with SDCC, treated with SDCC2Pasmo and finally compiled by Pasmo :
void Program()
{
__asm
org &2800
call _main
__endasm;
}
#include "Config.h"
#include "Interrupts.cxx"
#include "Memory.cxx"
#include "Clear.cxx"
#include "CRTC.cxx"
#include "GateArray.cxx"
#include "Flipping.cxx"
#include "Main.cxx"
I personally use .cxx file extension (for included C files) instead of .c as a convention, but this is personal taste.
One possible workaround for this is to create a .c file which includes all the other ones. This is an example of a Program.c source-file being is the only file to be compiled with SDCC, treated with SDCC2Pasmo and finally compiled by Pasmo :
void Program()
{
__asm
org &2800
call _main
__endasm;
}
#include "Config.h"
#include "Interrupts.cxx"
#include "Memory.cxx"
#include "Clear.cxx"
#include "CRTC.cxx"
#include "GateArray.cxx"
#include "Flipping.cxx"
#include "Main.cxx"
I personally use .cxx file extension (for included C files) instead of .c as a convention, but this is personal taste.
How to include binary files directly into the compiled file ?
There are many available implementations of BIN2C, a program allowing to convert any binary file into regular C code. As a result, it produces something like this :
char MyData[]=
{
0, 1, 2, 3
};
Where this is great for regular C developments, SDCC actually fails providing good results for it. When such array is found, SDCC first declare a table in memory with the required size :
MyData:
ds 4
.. and next SDCC fills MyData's content like this :
ld hl, MyData
ld ( hl ), 0
inc hl
ld ( hl ), 1
inc hl
ld ( hl ), 2
inc hl
ld ( hl ), 3
Fortunately, we have a nice work-around. The solution is to declare MyData as an external C array, then fills its content via inlined assembly code :
extern char MyData[];
void Dummy()
{
__asm
_MyData:
db 0, 1, 2, 3
__endasm;
}
Important : please take note you need to add a _ sign before MyData label in assembly code !
Now back to the original question : you can insert any binary data in your C source-code by replacing the line db 0, 1, 2, 3 by INCBIN "FILENAME.BIN" (INCBIN being a Pasmo internal command).
char MyData[]=
{
0, 1, 2, 3
};
Where this is great for regular C developments, SDCC actually fails providing good results for it. When such array is found, SDCC first declare a table in memory with the required size :
MyData:
ds 4
.. and next SDCC fills MyData's content like this :
ld hl, MyData
ld ( hl ), 0
inc hl
ld ( hl ), 1
inc hl
ld ( hl ), 2
inc hl
ld ( hl ), 3
Fortunately, we have a nice work-around. The solution is to declare MyData as an external C array, then fills its content via inlined assembly code :
extern char MyData[];
void Dummy()
{
__asm
_MyData:
db 0, 1, 2, 3
__endasm;
}
Important : please take note you need to add a _ sign before MyData label in assembly code !
Now back to the original question : you can insert any binary data in your C source-code by replacing the line db 0, 1, 2, 3 by INCBIN "FILENAME.BIN" (INCBIN being a Pasmo internal command).
What to use to inject binary files in a DSK ?
I personally use latest version of Demoniak's ManageDSK. It can be used as a command-line tool :
managedsk -L"main.dsk" -I"main.bin"/MAIN.BIN/BIN/16384/16384 -S"main.dsk"
The interesting feature versus the other available solutions is the possibility to give Amsdos's load / exec adresses as parameters.
Also, Briggsy from CPCWiki forum added this onto his own build.bat script :
IF NOT EXIST main.dsk managedsk -C -S"main.dsk"
managedsk -L"main.dsk" -I"main.bin"/MAIN.BIN/BIN/16384/16384 -S"main.dsk"
So the build script does everything and creates the disk. If WinAPE locks the file though, you need to eject the existing disk before you can run the build script.
managedsk -L"main.dsk" -I"main.bin"/MAIN.BIN/BIN/16384/16384 -S"main.dsk"
The interesting feature versus the other available solutions is the possibility to give Amsdos's load / exec adresses as parameters.
Also, Briggsy from CPCWiki forum added this onto his own build.bat script :
IF NOT EXIST main.dsk managedsk -C -S"main.dsk"
managedsk -L"main.dsk" -I"main.bin"/MAIN.BIN/BIN/16384/16384 -S"main.dsk"
So the build script does everything and creates the disk. If WinAPE locks the file though, you need to eject the existing disk before you can run the build script.
Conclusion
I don't have more tips & tricks in mind, so let's finish here this series of tutorial. I hope it gave you the wish to try SDCC and adopt it for your coming projects targeting the Amstrad CPC. Do not hesitate to contact me if there is missing information, I would be glad to help you (and eventually complete the articles).
Thank you for reading !
Thank you for reading !