Chapter 3 — Addresses, Constants and Expressions
Assembly programs need two kinds of names: names for places (where in memory does this code go?) and names for values (what does this number mean?). This chapter covers .org and $ for controlling placement, .equ for binding names to constants, expressions for computing with those constants and enums for grouping related integer constants into named sets.
Every computation in this chapter resolves to a plain integer before the binary is written. The Z80 sees only the resulting bytes.
.org sets the assembly address
.org $0100
After this directive, AZM places the next byte at address $0100. Labels defined after it get addresses starting there. Assembly begins at address 0 until an .org sets a different address.
You can use multiple .org directives in one source file to place different sections at different addresses:
.org $0100
CodeStart:
ld a,42
ld (Result),a
halt
.org $8000
Result:
.db 0
The code assembles at $0100. The data byte assembles at $8000. Both land in the same output binary at their respective offsets.
.org sets the assembly address — the address assigned to the next byte — not the byte’s position in the output file. .org changes where AZM places the next bytes, emitting nothing itself. AZM warns when a new .org overlaps already-assembled bytes.
$ — the current assembly address
$ evaluates to the current assembly address at the point it appears. Use it whenever you want to know how many bytes sit between two points in your source.
Table length:
Table:
.db $01,$02,$03,$04,$08
TABLE_LEN .equ $ - Table
After the .db line, $ is the address one past the last byte of TABLE. $ - TABLE gives the number of bytes in the table as an assembler-time constant.
Code size:
.org $0100
CodeStart:
; ... code ...
CodeEnd:
CODE_SIZE .equ CodeEnd - CodeStart
CODE_SIZE evaluates to the byte count between the two labels. Use label subtraction rather than $ - 0 so the intent is clear and the result stays correct when the code moves.
Gaps between origins
When you use two .org directives with a gap between them, the binary output may contain a hole depending on how the output is formed:
- Flat binary: bytes are emitted in address order. If your first section ends at
$01FFand the next.orgis$8000, the binary fills the gap with zero bytes unless you use.binfrom/.bintoto trim it. - Intel HEX: records are emitted only for the addresses that contain assembled bytes. Gaps in HEX are implicit.
.binfrom and .binto mark the address range to include in the flat binary:
.binfrom $0100
; ... code and data ...
.binto $0200
The binary contains the bytes between the two addresses.
.align
.align 16
Advances the assembly address to the next multiple of 16, inserting zero bytes to fill the gap. Use .align when hardware or lookup-table requirements demand address alignment.
Constants with .equ
.equ binds a name to a constant expression. It emits nothing. The name becomes a synonym for the value, usable in any expression context — instruction operands, data directives, storage counts, layout sizes and other .equ expressions.
The canonical form:
MAX_COUNT .equ 64
A name is global in the translation unit and can be defined once. Defining the same name twice is an error:
COUNT .equ 10
COUNT .equ 20 ; error: duplicate symbol
Hardware constants
Port addresses and memory-mapped I/O addresses belong as .equ constants:
LCD_DATA .equ $00
LCD_CTRL .equ $01
KEY_PORT .equ $00
MON_PUTC .equ $0008
MON_GETC .equ $000B
When hardware changes, one edit in the hardware-definition file propagates everywhere.
Address constants
WORK_BASE .equ $8000
STACK_TOP .equ $87FF
SCREEN_RAM .equ $4000
ld sp,STACK_TOP
ld hl,SCREEN_RAM
Size constants
Deriving sizes from other constants keeps arithmetic in one place:
TILE_W .equ 8
TILE_H .equ 8
TILE_BYTES .equ TILE_W * TILE_H
SCREEN_W .equ 128
SCREEN_H .equ 64
SCREEN_ROWS .equ SCREEN_H / TILE_H
Label subtraction records a layout assumption as an assembler-time constant:
DispatchA:
jp HANDLER_A
DispatchB:
jp HANDLER_B
ENTRY_STRIDE .equ DispatchB - DispatchA ; 3: jp is a 3-byte instruction
Any code that dispatches through this table loads ENTRY_STRIDE by name rather than encoding the stride as a literal.
Forward references in .equ
A .equ expression may reference a label or another .equ defined later in the source:
TABLE_LEN .equ TableEnd - TableStart
TableStart:
.db 1,2,3,4
TableEnd:
AZM resolves forward references across passes. Circular references produce an error.
Expressions
An expression is any combination of numeric literals, symbols and arithmetic operators that the assembler evaluates to an integer before writing the binary. Expressions appear everywhere you can put a number: instruction operands, .equ definitions, .db / .dw / .ds operands.
Arithmetic operators
AZM supports symbolic operators: + - * / % & | ^ ~ << >>.
The % operator between two expressions performs integer modulo. A % at the start of a value is a binary literal prefix, covered in Chapter 2.
Operator precedence follows conventional arithmetic rules. Parentheses group sub-expressions:
FRAME_SIZE .equ (COLS * ROWS) + 2
ENTRY_ADDR .equ TABLE_BASE + (ENTRY_NUM * 3)
See Appendix B for the full precedence table.
$ in expressions
Msg: .db "Hello"
MSG_LEN .equ $ - Msg ; byte count of "Hello"
In a .equ or data context, $ resolves to the address after the last emitted byte on the preceding line.
Expressions in instructions
ld a,PORT_BASE + 1
ld hl,BUFFER + OFFSET
bit FLAG_BIT,a
Expressions in data directives
.db MAX_VAL - 1
.dw TABLE_BASE + STRIDE * 3
.ds SPRITE_COUNT * 4
.db accepts byte-range expressions (0–255 or −128–127 for signed). .dw accepts word-range expressions (0–65535). .ds accepts any non-negative count expression.
To split a 16-bit address into two bytes:
.db VECTOR_TABLE & $FF ; low byte
.db (VECTOR_TABLE >> 8) & $FF ; high byte
Assembler-time evaluation
Every expression in AZM is evaluated by the assembler before anything runs on the Z80. The assembler computes the value and writes the result — a plain number — into the binary.
Runtime-dependent values belong in Z80 instructions:
add hl,bc ; result depends on HL and BC at runtime
Range checks
AZM checks that expression values fit the encoding slot they fill:
| Context | Valid range |
|---|---|
8-bit immediate (ld a,n) |
0–255 or −128–127 |
8-bit data (.db) |
0–255 |
| Signed 8-bit branch offset | −128–127 (from next PC) |
bit/set/res bit index |
0–7 |
16-bit immediate (ld hl,nn) |
0–65535 |
16-bit data (.dw) |
0–65535 |
Port number (in a,(n)) |
0–255 |
When a value falls outside the valid range for its encoding, AZM reports a range error naming the value and the allowed range.
Expression errors
Common expression errors:
- Unknown symbol: a name with no
.equ, label or layout definition - Circular reference: an
.equthat transitively references itself - Division by zero:
expr / 0 - Range overflow: a computed value outside the encoding range
Chapter 8 covers diagnostic messages.
Enums as grouped constants
When you write a set of related constants with .equ, they often form a natural sequence:
RED .equ 0
GREEN .equ 1
BLUE .equ 2
This works, but the values are yours to maintain. Insert YELLOW between RED and GREEN and you have to renumber GREEN, BLUE and everything that follows.
An enum groups related constants under a single name and assigns their values automatically. You list the members; AZM assigns 0 to the first, 1 to the second and so on:
Mode .enum Read, Write, Append
The name comes first, then .enum, then a comma-separated member list. Each member gets a qualified name — the group name, a dot and the member name:
| Name | Value |
|---|---|
Mode.Read |
0 |
Mode.Write |
1 |
Mode.Append |
2 |
The qualifier is always required. Read alone is an error:
ld a,Read ; error: unknown symbol Read
ld a,Mode.Read ; correct
When two enums share a word, the group name separates them:
Color .enum Red, Green, Blue
State .enum Idle, Active, Dead
; Color.Red = 0, State.Idle = 0 — different symbols
Enum members are valid in any assembler-time expression context:
ld a,Mode.Write ; load 1 into A
cp Mode.Append ; compare A with 2
.db Mode.Read ; emit byte 0
For a handful of states, a cp chain is readable:
ld a,(mode)
cp Mode.Write
jr z,handle_write
cp Mode.Append
jr z,handle_append
; falls through: Mode.Read or unrecognized
When there are many values and performance matters, a jump table is more efficient:
Cmd .enum Draw, Move, Erase
; C = Cmd.* value, guaranteed 0–2
ld hl,CmdTable
ld b,0
add hl,bc
add hl,bc
add hl,bc ; HL = CmdTable + cmd * 3
jp (hl)
CmdTable:
jp do_draw
jp do_move
jp do_erase
When to use enums
Use enums for any small set of named states, command codes, token kinds or hardware-mode values where a dense sequence is natural. State.Dead reads more clearly than cp 3; add or reorder members and every use updates automatically. For values that need specific numbers — port addresses, bitmasks, hardware registers — use .equ. At runtime, an enum value is an ordinary byte; validate inputs before dispatching on them.