P65-Ophis Command Reference

Version 0.7

Command Modes

These mostly follow the MOS Technology 6500 Microprocessor Familiy Programming Manual, except for the Accumulator mode. Accumulator instructions are written and interpreted identically to Implied mode instructions.

Basic arguments

Most arguments are just a number or label. The formats for these are below.

Numeric types

Label types

Normal labels are simply referred to by name. Anonymous labels may be referenced with strings of - or + signs (the label - refers to the immediate previous anonymous label, -- the one before that, etc., while + refers to the next anonymous label), and the special label ^ refers to the program counter at the start of the current instruction or pragma.

Normal labels are defined by prefixing a line with the label name and then a colon (e.g., label:). Anonymous labels are defined by prefixing a line with an asterisk (e.g., *).

Temporary labels are only reachable from inside the innermost enclosing .scope statement. They are identical to normal labels in every way, except that they start with an underscore.

String types

Strings are enclosed in double quotation marks. Backslashed characters (including backslashes and double quotes) are treated literally, so the string "The man said, \"The \\ character is the backslash.\"" produces the ASCII sequence for The man said, "The \ character is the backslash."

Strings are generally only used as arguments to assembler pragmas - usually for filenames (e.g., .include) but also for string data (in association with .byte).

It is legal, though unusual, to attempt to pass a string to the other data statements. This will produces a series of words/dwords where all bytes that aren't least-significant are zero. Endianness and size will match what the directive itself indicated.

Compound Arguments

Compound arguments may be built up from simple ones, using the standard +, -, *, and / operators, which carry the usual precedence. Also, the unary operators > and <, which bind more tightly than anything else, provide the high and low bytes of 16-bit values, respectively.

Use braces [ ] instead of parentheses ( ) when grouping arithmetic operations, as the parentheses are needed for the indirect addressing modes.

Examples:

Memory Model

In order to properly compute the locations of labels and the like, P65 must keep track of where assembled code will actually be sitting in memory, and it strives to do this in a way that is independent both of the target file and of the target machine.

Basic PC tracking

The primary technique P65 uses is "program counter tracking." As it assembles the code, it keeps track of a virtual program counter, and uses that to determine where the labels should go. (It's a little more complicated than this, thanks to some properties of the instruction set architecture, but that's beyond the scope of this document. See the implementation notes if you're morbidly curious.)

In the absence of an .org pragma, it assumes a starting PC of zero. .org is a simple pragma, setting the PC to the value that .org specifies. In the simplest case, one .org pragma appears at the beginning of the code and sets the location for the rest of the code, which is one contiguous block.

Basic Segmentation simulation

However, this isn't always practical. Often one wishes to have a region of memory reserved for data without actually mapping that memory to the file. On some systems (typically cartridge-based systems where ROM and RAM are seperate, and the target file only specifies the ROM image) this is mandatory. In order to access these variables symbolically, it's necessary to put the values into the label lookup table.

It is possible, but inconvenient, to do this with .alias, assigning a specific memory location to each variable. This requires careful coordination through your code, and makes creating reusable libraries all but impossible.

A better approach is to reserve a section at the beginning or end of your program, put an .org directive in, then use the .space directive to divide up the data area. This is still a bit inconvenient, though, because all variables must be assigned all at once. What we'd really like is to keep multiple PC counters, one for data and one for code.

The .text and .data directives do this. Each has its own PC that starts at zero, and you can switch between the two at any point without corrupting the other's counter. In this way each function can have a .data section (filled with .space commands) and a .text section (that contains the actual code). This lets our library routines be almost completely self-contained - we can have one source file that could be .included by multiple projects without getting in anything's way.

However, any given program may have its own ideas about where data and code go, and it's good to ensure with a .checkpc at the end of your code that you haven't accidentally overwritten code with data or vice versa. If your .data segment did start at zero, it's probably wise to make sure you aren't smashing the stack, too (which is sitting in the region from $0100 to $01FF).

If you write code with no segment-defining statements in it, the default segment is text.

The data segment is designed only for organizing labels. As such, errors will be flagged if you attempt to actually output information into a data segment.

General Segmentation Simulation

One text and data segment each is usually sufficient, but for the cases where it is not, P65 allows for user-defined segments. Putting a label after .text or .data produces a new segment with the specified name.

Say, for example, that we have access to the RAM at the low end of the address space, but want to reserve the zero page for truly critical variables, and use the rest of RAM for everything else. Let's also assume that this is a 6510 chip, and locations $00 and $01 are reserved for the I/O port. We could start our program off with:

.data
.org $200
.data zp
.org $2
.text
.org $800

And, to be safe, we would probably want to end our code with checks to make sure we aren't overwriting anything:

.data
.checkpc $800
.data zp
.checkpc $100

Macros

Assembly language is a powerful tool - however, there are many tasks that need to be done repeatedly, and with mind-numbing minor modifications. P65 includes a facility for macros to allow this. P65 macros are very similar in form to function calls in higher level languages.

Defining Macros

Macros are defined with the .macro and .macend commands. Here's a simple one that will clear the screen on a Commodore 64:

.macro clr'screen
    lda #147
    jsr $FFD2
.macend

Invoking Macros

To invoke a macro, either use the .invoke command or backquote the name of the routine. The previous macro may be expanded out in either of two ways, at any point in the source:

.invoke clr'screen

or

`clr'screen

will work equally well.

Passing Arguments to Macros

Macros may take arguments. The arguments to a macro are all of the 'word' type, though byte values may be passed and used as bytes as well. The first argument in an invocation is bound to the label _1, the second to _2, and so on. Here's a macro for storing a 16-bit value into a word pointer:

.macro store16   ; `store16 dest, src
	lda #<_2
	sta _1
	lda #>_2
	sta _1+1
.macend

Macro arguments behave, for the most part, as if they were defined by .alias commands in the calling context. (They differ in that they will not produce duplicate-label errors if those names already exist in the calling scope, and in that they disappear after the call is completed.)

Features and Restrictions of the P65 Macro Model

Unlike most macro systems (which do textual replacement), the P65 macros evaluate their arguments and bind them into the symbol table as temporary labels. This produces some benefits, but it also puts some restrictions on what kinds of macros may be defined.

The primary benefit of this "expand-via-binding" discipline is that there are no surprises in the semantics. The expression _1+1 in the macro above will always evaluate to one more than the value that was passed as the first argument, even if that first argument is some immensely complex expression that an expand-via-substitution method may accidentally mangle.

The primary disadvantage of the expand-via-binding discipline is that only fixed numbers of words and bytes may be passed. A substitution-based system could define a macro including the line LDA _1 and accept as arguments both $C000 (which would put the value of memory location $C000 into the accumulator) and #$40 (which would put the immediate value $40 into the accumulator). If you really need this kind of behavior, a run a C preprocessor over your P65 source, and use #define to your heart's content.

Assembler pragmas

Assembler pragmas are all instructions to the assembler that are not actual instructions. Currently implemented pragmas are:

The following pragmas are deprecated, added for compatibility with P65-Perl. Use the -d option to P65-Ophis to enable them.


Back to P65 Main Page