WinAPE's Assembler

The Windows Amstrad Plus Emulator 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 :).

Never trust emulators! They are great tools for developpers but none of them can simulate a real machine 100%. If you don't check your program regularly on a real machine (esp. if you're banging the hardware and/or require precise timings) then you can be sure to run into a lot of troubles in the end (eg. it might not work at all on the real machine. Hello Targhan! :).

How to make a good bugreport

If you've found something wrong in WinAPE (assembler, emulation, …), then send a proper bugreport:

  • Explain the problem:
    1. What you did
    2. How you did it
    3. What happened
    4. What you expected to happen
  • How to reproduce the problem.
  • If possible, send a screenshot, DSK, SNA, source file or whatever that could help to see, reproduce and/or fix the problem.
  • Be polite, be patient :)

You will find the contact details of Richard “Executionner” Wilson on the WinAPE's website or post your message on the CPC-Wiki forum.

Get started: Hello World!

Here we go with a very simple Hello world example program to show the most importants steps when programming software with WinAPE.

1. Open the assembler window

Launch WinAPE and open the assembler window Open a new source tab

2. Write your program and compile it

And here we go! You can write your assembly source now! When you're done, assemble it. The compiler window will popup to show some informations about the compilation and a dump of your program. Here we can see that the compiler reports 1 error.

The error is also reported in the assembler window. You can clic on the error at the bottom and the cursor will be automatically moved on the corresponding line with the error. Assemble the program again after fixing the error. The compiler reports no error, so far, so good!

3. Test your program

Your program is assembled in the memory of the emulated CPC. You just have to call it from the BASIC prompt and... it works!

Using an external text editor

Your sources in your favorite text editor The wrapper in 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.

Download example

Directives

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

Syntax:
ALIGN <expression>

Align the current code to a given boundary. eg. align 256 will page align code.

BRK

Syntax:
BRK

A MAXAM legacy. This directive is actually compiled as an RST #30 (opcode &F7).

CHARSET

This can be used for example if your program does not use standard ASCII for a character set.

Examples

	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 and NOCODE

Syntax:
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

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, DS and RMEM

Syntax:
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, DW and WORD

Syntax:
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

EQU

Syntax:
<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

Syntax:
END

The END directive simply tells the assembler to stop. It may be omitted, but has two uses:

  1. To avoid assembling the whole program - temporarily put in an END directive.
  2. 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

Syntax:
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

Syntax:
LET <symbol> = <expression>

This has the same effect as EQU except that LET allows redefinition of symbols.

LIMIT

Syntax:
LIMIT <expression>

This directive set the highest memory address for storage of object code.

Some uses of the LIMIT directive:

  1. To prevent memory used by something else being overwritten.
  2. When writing a program with a fixed maximum size (e.g. the size of an EPROM).

Notes

  • LIMIT only affects storage of code In memory, not the code location (if this is different).
  • The checking is only done on pass 2, since code is only stored on pass 2.

LIST and NOLIST

Syntax:
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

Syntax:
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>).

  • The code origin is the address where the code is meant to be run from.
  • The storage location is the address where the assembled code will be stored.

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').

Notes

  • Any number of ORG directives may be used.
  • The expressions may not contain undefined symbols.

Examples

; 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

Syntax:
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

Syntax:
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.

Include Path

If your file can't be found in the current path then WinAPE will use the include path configuration to seach for your file.

  • To configure it, go in the Assemble/Option menu :

Configure the WinAPE's Assembler include path

  • A new window will appears. There you can set one or more include path :

Configure the WinAPE's Assembler include path

RELOCATE_START

Syntax:
RELOCATE_START

Mark the start of a relocatable section of code.

RELOCATE_END

Syntax:
RELOCATE_END

Mark the end of a relocatable section of code.

RELOCATE_TABLE

Syntax:
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.

Example

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

RELOCATE_COUNT

Reserved symbol. Hold the number of entries in the relocation table.

RELOCATE_SIZE

Reserved symbol. Hold the size of the relocation table (assuming word entries are used).

RUN

Syntax:
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).

When you are using 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.

Examples

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

Syntax:
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

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

WRITE

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.

Examples

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

Syntax:
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.

Parameters

  • lower_rom
    Set to -1 to disable or 0 to enable the lower ROM (from &0000 to &3FFF).
  • upper_rom
    Set to -1 to disable or any upper ROM id to enable the upper ROM (from &C000 to &FFFF).
  • ram_bank
    Set it with the MMR command you want.

Examples

; 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

Save to disk

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

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.

IF

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.

Example

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).

ELSEIF

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

IFNOT

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

IF1, IF2

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.

Nesting IF blocks

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

Syntax:
IFDEF <symbol>

Check if a symbol is defined.

IFNDEF

Syntax:
IFNDEF <symbol>

Check if a symbol is not defined.

Macro-commands

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

Syntax:
macro <name> [parameter1[,parameter2[…]]]

Define a new macro.

macro <name> [parameter1[,parameter2[]]]
	; some code
mend

Your macro MUST BE DEFINED BEFORE being used in the source!

Repeat

Syntax:
repeat <expression>

Repeat a code section.

repeat 6
	; put some code to repeat here
rend

While

Beware of macros!

Syntax:
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

Symbols

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.

Expressions

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:

NUMBERS

  1. decimal constants. e.g. 132.
  2. hexadecimal constants. e.g. &BB5A or #2A. Either & or # may be used for compatibilty with BASIC and the firmware documentation.
  3. binary constants. e.g. %1011101
  4. character constants. e.g . 'A',”3”,’”’. Either single or double quotes may be used - to specify a quote character enclose it in the other type of quote. The value of a character constant is the ASCII code of the character, so “3” is the same as #33. A null character constant,”” has the value 0.
  5. an identifier.
  6. one of the two special symbols:
    • $ represents the current code location (program counter).
    • @ represents the current storage location.

OPERATORS

  1. Arithmetic operators +,-, *, /, MOD.
  2. Bitwise logical operators AND, OR, XOR.

All expressions are evaluated to 16 bit unsigned integers. Overflow is ignored, and the least significant 16 bits of the result is used.

Debugging

Breakpoints

Hardware registers

documentations/software/winape/start.txt · Last modified: 2011/01/25 02:12 by grim