WinAPE is a freeware Amstrad CPC/Plus emulator running on Microsoft Windows. The most interresting features of this emulator for developpers are it's integrated assembler and debugger.
At the moment (in my opinion) it is the easiest “all in one” software to get started with CPC cross-developpement: You can write, compile, run, trace and debug your program with the emulator. The main drawback being the “emulator” part. There's always some quirks, timings or obscure things of the original hardware that can hardly be emulated and might ruin your program (esp. demo-like stuff) if you don't check it regularly on the real thing.
WinAPE is still being supported by it's author, so if you find something wrong, do not hesitate to harass Richard Wilson for a bugfix :).
If you've found something wrong in WinAPE (assembler, emulation, …), then send a proper bugreport:
You will find the contact details of Richard “Executionner” Wilson on the WinAPE's website or post your message on the CPC-Wiki forum.
Here we go with a very simple Hello world example program to show the most importants steps when programming software with WinAPE.
The integrated text editor is very minimalistic but handy to quickly test some code-snippet or for writing short programs. For bigger programs, a more sophisticated text editor may be much better. There's many of them already available (personnaly I stick with PSPad on MS-Windows), just pick one which fits your needs.
Once you have your favorite full blown text editor ready, it's pretty easy to use it along with WinAPE. First thing to do is to make a wrapper for WinAPE. The wrapper can be just a single line source file that will stay open in the integrated WinAPE's assembler. It's purpose is to read/include your main source code, the one you are editing with your full blown text editor.
When you want to compile your source, just switch to WinAPE and compile the wrapper (eg. press F9) and that's it!
The wrapper is necessary because once a source file is opened in the WinAPE assembler, it can't be externally modified anymore (eg. from your own text editor). WinAPE doesn't detect if a file in it's assembler window has changed and will just keep the file as it was when it has been opened in the assembler, that is, unmodified.
Here are all the directives I know of which can be used with the WinAPE's assembler. There's maybe more available, unfortunately the WinAPE's documentation is not as good as the emulator at the moment :)
Sorted in alphabetical order.
ALIGN <expression>
Align the current code to a given boundary. eg. align 256 will page align code.
BRK
A MAXAM legacy. This directive is actually compiled as an RST #30 (opcode &F7).
This can be used for example if your program does not use standard ASCII for a character set.
charset ‘A’,0 ; Redefine the character ‘A’ to have a value of 0 charset ‘A’,’Z’,15 ; Redefine the characters ‘A’ through ‘Z’ to have values 15 through to 40 charset ; Set all characters back to their default ASCII values.
CODE
NOCODE
Occasionally it is useful to assemble a program without storing any code. The directive NOCODE
achieves this. The directive CODE
cancels the effect of NOCODE
, and causes storage of object code to be resumed.
It can be used to assemble some routines just to get their symbols available in your program.
DEFB
, DB
, DEFM
, BYTE
and TEXT
are different names for the same thing. They take a list of parameters, each of which can be an arithmetic expression or a text string . Each expression is evaluated and the result put in the objecr code. Each string is sent
directly to the object code, character by character. Strings may be enclosed in either single or double quotes; if the closing quote is omitted the string is assumed to be the rest of the line.
Note: a single character string is considered a numeric constant. Expressions such as “A”+&80 are allowed.
Examples:
BYTE 1,3,count*3+1,"q" or 128 TEXT "A string ending with cr-lf",13,10
DEFS <expression>[,<fill value>]
DS <expression>[,<fill value>]
RMEM <expression>[,<fill value>]
DEFS
, DS
or RMEM
causes the assembler to reserve the specified number of bytes of memory. Both the object code and the storage location are incremented by the value of the expression. The reserved space is filled with zeros. The expression may not contain forward references.
Examples:
.buffer256 RMEM 256 .word DEFS 2
Occasionally the reserved space needs to be filled with a value other than zero. This can be done by giving a second expression parameter. The space is filled with the least significant byte of the expression's value.
Example:
; Fill &200 bytes with the value &FF RMEM &200,&FF
DEFW <expression 1>[,<expression 2>,…,<expressnion n>]
DW <expression 1>[,<expression 2>,…,<expressnion n>]
WORD <expression 1>[,<expression 2>,…,<expressnion n>]
Each expression is evaluated and the 2 bytes result put in the object code, low byte first.
Example:
address equ &6128 WORD &C000,address DEFW &0001,&0002,4+6*4,&1234
<symbol> EQU <expression>
The symbol is defined and assigned the value of the expression, which must be well-defined (i.e. contain no forward references). If the symbol is already defined an error message will be given (unless the old value and the new are the same). In other words, EQU
may not be used to redefine a symbol.
END
The END
directive simply tells the assembler to stop. It may be omitted, but has two uses:
END
directive. END
causes the storage location to be output in the listing. A useful ploy is to put LIST:END
as the last line of source code so you can see where the end of the program is.INCBIN ”<filename>”
Include binary DATA from a file.
It is recommended to use NOLIST
before using INCBIN
, this will speed up the compilation (displaying a bunch of data slow down significantly the compiler).
LET <symbol> = <expression>
This has the same effect as EQU
except that LET
allows redefinition of symbols.
LIMIT <expression>
This directive set the highest memory address for storage of object code.
Some uses of the LIMIT directive:
LIMIT
only affects storage of code In memory, not the code location (if this is different). LIST
NOLIST
LIST
turns on the assembler listing. This is the initial state. NOLIST
turns off the assembler listing. If your source file is huge or you include binary file(s) (with INCBIN
), turning off the assembler listing can speed up greatly the compilation time.
ORG <expressionl> [, <expression2>]
The ORG
directive tells the assembler what is the code origin (given by <expressionl>
) and, optionnaly, the storage location (given by <expression2>
).
If the storage location is not provided, the assembler will store the assembled code starting at the code origin.
Sometime it is not possible to store the code at the address where it is to run, because it is being used by something else (e.g. MAXAM or BASIC). In this case, you should provide a storage location. The assembler will evaluate both expressions, set the code origin to the first, but store the code at the second address (the 'storage location').
ORG
directives may be used.; Assemble the code starting at &8000 and store it at the same address ORG &8000
; Assemble the code starting at &8000 and store the byte-codes at &1000 ; (you will have to move the byte-code at &1000 to &8000 to execute it) ORG &8000, &1000
org &8000 ; Main program here ; Then, we compile an ISR with a code origin of &0038 (where it should run) ; but store the assembled code right after the main program using the special ; symbol $ (which is the current address). org &38, $ ; Interrupt Service Routine
PRINT <expression>
This command has been extended to allow variables to be included. To print the value of a variable in hexadecimal precede it with “$” (decimal) or “&” (hex) Example:
PRINT "The code ends at &endprog and is is $len bytes long"
READ ”<filename>”
When the assembler finds a READ directive it will open the specified file (in the same folder the current source is saved or from the include path list), assemble the contents of the file, and then return to the line in memory following the READ
directive.
If your file can't be found in the current path then WinAPE will use the include path configuration to seach for your file.
RELOCATE_START
Mark the start of a relocatable section of code.
RELOCATE_END
Mark the end of a relocatable section of code.
RELOCATE_TABLE [byte|word] [base_address]
Generate a relocation table.
By default a table of word sized offsets is generated, override this by specifying byte for small code sections. The base_address specifies the relative origin for the values in the table.
The following code will write directly to the emulator memory, run when assembled and break when the instruction at the .break label is reached.
org #4000 write direct run start, break relocate_start ;Start relocatable code section dw relocate_count ;Number of entries in the relocation table relocate_table start ;Generate a relocation table relative to start ld de,#40 ;Emulator will start running from the ld hl,break instruction .start ld hl,break ;Emulator will stop at the following NOP instruction .break nop relocate_end ;End of relocatable code section
Reserved symbol. Hold the number of entries in the relocation table.
Reserved symbol. Hold the size of the relocation table (assuming word entries are used).
RUN <execution_address>[, <breakpoint_address>]
The RUN
directive allow you tun execute your program right after it's compilation by using the Run (F9) option in the Assemble menu instead of the usual Assemble (CTRL+F9).
RUN
, your program will be executed directly by modifying the PC register of the emulated Z80 (ie. like a JP &xxxx
), therefore no RETurn (eg. to BASIC) is possible.
The following example set the execution address right at the begining of the program in &3000 by using the special reference $ which mean “the current address”:
ORG &3000 RUN $ LD A,1 CALL &BC0E CALL &BB18 RST 0
The following example will set the execution address at the label _start
and put a breakpoint to the label _break
:
RUN _start, _break ORG &6128 _disable_firmware: di ld hl,&C9FB ld (&38),hl ei ret _start ; Execution address here ld a,1 call &BC0E ; Trigger the debugger here _break call _disable_firmware ret
STOP
The STOP
command causes reading from the file containing the STOP directive to terminate and return to assemble the remainder of the program in memory. The END
command would abort the assembly completely.
STR
is similar to BYTE
and TEXT
with the option that it will only take a list of strings and the last character in each string has the top bit set. This is useful when printing a string character by character, as you then only need test if each character has its top bit set to know when you've reached the end of the string. The AMSDOS firmware stores command names in this way.
Example:
STR "dummy text string"
Will produce :
&64,&75,&6D,&6D,&79,&20,&74,&65,&73,&74,&20,&73,&74,&72,&69,&6E,&E7
All the byte-code produced after a WRITE
directive will be written to a file. The default filepath will be the same than your source file (on your hard-drive). To save the binary file in a DSK image, see the WRITE DIRECT
directive below.
The following example will output 2 files, code.bin
and data.bin
, in your PC hard-drive.
write "code.bin" ORG &1000 LD A,1 JP &BC0E write "data.bin" db "Arkos Rulez!"
WRITE DIRECT [<lower_rom>[,<upper_rom>[,<ram_bank>]]]
The WRITE DIRECT
allow you to compile your code into the emulator memory. By default, the base 64Kb RAM is selected but you can select anything else with the optionnals parameters.
; Compile a routine directly in bank 0 of page 0 WRITE DIRECT -1,-1,&C4 ORG &4000 LD HL,&C000 LD DE,&C001 LD BC,&3FFF LD (HL),L LDIR RET
The byte-code can also be saved directly into a dsk image. To do so, provide the WRITE DIRECT
directive a filename prefixed with the CPC drive letter (eg. A:
or B:
).
This example will save the binary file code.bin
in the currently selected DSK image for drive A:
write direct "A:code.bin" ORG &1000 LD A,1 JP &BC0E
Conditional assembly is used when two or more versions of a program are needed (e.g. cassette version and disc version). This feature enables any number of different versions to be assembled from the same source code.
This is done by defining blocks of source code that are to be assembled only if some condition holds. The formats of IF blocks are:
IF <expression> <code to be assembled if expression is true> ENDIF
IF <expression> <code to be assembled if expression is true> ELSE <code to be assembled if expression is false> ENDIF
The expression may be any arithmetic expression. In this context the value of the expression is considered to be a signed 16 bi t number, with 'true' represented by any positive number (i.e. between 1 and 32767) and 'false' by zero or any negative number.
The recommended use is to define a variable which holds the value 1 for true and 0 for false.
Suppose a program come s in two versions, for cassette and disc, and there are a few differences between the two. Define a variable at the start of the source code:
LET cassette=l ; to assemble the cassette version LET cassette=0 ; to assemble the disc version
Then enclose each section where the code differs in an IF block, as follows:
IF cassette <code for cassette version> ELSE <code for disc version> ENDIF
AND, OR and XOR may be used with care in IF directives. These are bitwise logical operators, and will work as expected if true is only represented by 1 and false only by 0. So if variables which only ever hold the values 0 or 1 are used the usual results hold (1 OR 0 is true, 1 AND 0 is false, 1 XOR 1 is false, etc.)
Example:
IF 2 AND 1
Warning: although 1 and 2 both represent true, the expression 1 AND 2 evaluates to 0 (i.e. false).
As its name suggests, its a combination of if and else. Like else, it extends an if statement to execute a different statement in case the original if expression evaluates to FALSE. However, unlike else, it will execute that alternative expression only if the elseif conditional expression evaluates to TRUE.
There may be several elseifs within the same if statement. The first elseif expression (if any) that evaluates to TRUE would be executed.
if <expression1> ; some code to compile if <expression1> is true elseif <expression2> ; some code to compile if <expression2> is true endif
It simply reverses the logic of the IF directive:
IFNOT <expression> <code to be assembled if expression is false> ELSE <code to be assembled if expression is true> ENDIF
These special forms of the IF directive return the value 'true' on p ass 1 and 2 of the assembly, respectively. They may be of some use for printing different messages on each pass, but Z80 instructions and directives should not be placed within an IF1 or IF2 block.
IF blocks may be nested up to a depth of 10 (maybe more). It is, however, unusual to need nesting deeper than 2 levels. Example:
IF rom_verclon <ROM code> ELSE IF disc_version <disc code> ELSE <cassette code> ENDIF ENDIF
IFDEF <symbol>
Check if a symbol is defined.
IFNDEF <symbol>
Check if a symbol is not defined.
Macro local labels can be defined by prefixing with an @
symbol, they can be nested and may be called recursively. Macros can override reserved assembler symbols. The !
symbol is used to exclude the use of macros from a symbol. (eg. If the LDI symbol had been redefined, you can assemble a standard LDI using !LDI).
macro <name> [parameter1[,parameter2[…]]]
Define a new macro.
macro <name> [parameter1[,parameter2[…]]] ; some code mend
repeat <expression>
Repeat a code section.
repeat 6 ; put some code to repeat here rend
while <expression>
Repeat a code section until <expression> is true.
; Initialize a variable LET counter=1 ; Repeat the code section until counter>15 while counter AND &F ; put some code to repeat here ; and increment the variable. LET counter=counter+1 wend
The assembler keeps a table of symbols, each with an assigned 16 bit value. A Symbol is similar to a BASIC variable. The assembler makes two passes; on the first pass it sets up the symbol table and on the second pass it creates the object code using the symbol table to calculate jump addresses etc. On the first pass, when a symbol that has not yet been defined is referred to it is put into the symbol table.
The value is filled in when the symbol is defined. These forward references must all be resolved on the first pass; error messages will indicate any symbols that remained undefined. No symbol may be assigned different values on the two passes - if this occurs the assembler may generate many errors.
There are some assembler directives which do not allow any forward references because the expression value must be known on pass 1. These include ORG - the code origin must be well-defined for it would otherwise be impossible for the assembler to generate the correct symbol table.
Arithmetic expressions may be used throughout the assembler - wherever a number is required. This includes operands of Z80 instructions and assembler directives. The expression evaluator works from left to right and all ows the following:
All expressions are evaluated to 16 bit unsigned integers. Overflow is ignored, and the least significant 16 bits of the result is used.