Review the ca65 Users Guide for syntax details.
- Spaces, not tabs
- "Tab" stops for alignment are 8 characters (Emacs asm-mode default)
bin/asmfmt.plcan be used for formatting- No trailing whitespace
- Use lowercase opcodes (
lda,rts) - All A2D code is 6502 not 65C02
- Comments are encouraged.
- End-of-line comments:
;at a tab stop or aligned with nearby code - Indented stand-alone comments:
;;at first tab stop (8 chars) - Major comments:
;;;at start of line - Use
?for questions in comment text,???for questions about the code:
lda value
cmp #limit ; less than the limit?
bcc less ; yes, so go do that
rol $1234 ; what does this do ???-
Internal labels (for both code and data) should use
snake_case; this include parameter blocks. -
External labels ("equates") should use
UPPERCASE, and are defined with:=, e.g.SPKR := $C030 -
Constants (symbols) should use
kTitleCase, and are defined with=, e.g.kExample = 1234 -
Callable procedures should use
TitleCase, and are defined with.proc -
Nested or otherwise "private" callable procedures should use
_TitleCasewith_prefix to make scoping more obvious. -
Structure definitions (
.struct) should useTitleCase, with member labels insnake_case. -
Enumeration definitions (
.enum) should useTitleCase, with members insnake_case. -
Macros:
- Macros that mimic ca65 control commands should be lowercase, dot-prefixed, e.g.
.pushorg,.params - Macros that provide pseudo-opcodes should be lowercase, e.g.
ldax,add16 - Macros
.define-ing pseudo-constants should usekTitleCase - Other macros should be named with
SHOUTY_CASE
- Macros that mimic ca65 control commands should be lowercase, dot-prefixed, e.g.
0123456701234567
SPKR := $C030 ; external label
flag: .byte 0 ; internal label
there := * - 1 ; internal label
kTrue = $80 ; constant
.params get_flag_params ; parameter block
result: .byte 0
.endparams
.proc SetFlag ; procedure
lda #kTrue
done: sta flag ; internal label
.endproc ; SetFlag
.struct Point ; structure
xcoord .word
xcoord .word
.endstruct
.enum Options ; enum
first = 1
second = 2
.endenum- Do use
IF/ELSE_IF/ELSE/END_IFmacros to avoid throw-away local labels.
IF A = #kSomeConst
...
ELSE_IF A = #kOtherConst
...
ELSE
...
END_IF
IF bit flag : NS
...
END_IF- Do use
DO/REDO_IF/CONTINUE_IF/BREAK_IF/WHILE/FOREVER/DONEmacros to avoid throw-away local labels.
ldx #0
DO
...
REDO_IF CS
...
CONTINUE_IF bit flags : VS
...
BREAK_IF A <> #123
...
WHILE inx : X < #kCount
DO
...
BREAK_IF NS
...
MLI_CALL OPEN, open_params
IF CS
jsr ShowRetryCancelPrompt
REDO_IF A = #RETRY
jmp fail
END_IF
....
DONE- Annotate fall-through. A
;; fall throughcomment can be used, but the preferred form is with theFALL_THROUGH_TOassertion macro to prevent refactoring mistakes.
...
FALL_THROUGH_TO ShowAlert, A=#alert_num
.endproc
.proc ShowAlert
...- Do make use of unnamed labels for local loops and forward branches to avoid pointless names.
- Do Not use more than one level (i.e. no
:--or:++)
;; Copy the thing
ldy #7
: lda (src),y
sta (dst),y
dey
bpl :-
lda flag
bne :+
inc count
: rts- Do use cheap local labels to highlight repeated patterns. For example, retries:
@retry: MLI_CALL GET_FILE_INFO, params
bcc :+
jsr show_error_alert
jmp @retry- Do use tail-call optimization (replacing
JSR label/RTSwithJMP label) as this pattern is well understood.- As always, add comments if the usage might not be obvious (e.g. not at the end of a proc)
- The con of this is the true call stack is obscured, making debugging more difficult, but the pattern is common enough that this can't be relied on.
- Use binary
%00110110for bit patterns - Use decimal for numbers (counts, dimensions, etc)
- For negative numbers, the
AS_BYTE(-1)andAS_WORD(-1)macros are handy.
- For negative numbers, the
- Use hex for geeky values, e.g. $7F (bit mask), $80 (high bit), $FF (all bits set) when bits would be less readable.
- Avoid magic numbers where possible:
- Define local symbols (e.g.
ptr := $06) - Define offsets, constants, etc.
- Use
.structdefinitions to define offsets into structures - Use math where necessary (e.g.
ldy #offset2 - offset1) - Use
.sizeof()(or math if needed) rather than hardcoding sizes
- Define local symbols (e.g.
- Delimit code blocks with
.proc:
.proc SomeRoutine
lda $06
rol
rts
.endproc ; SomeRoutine- Try to encapsulate locally used data as much as possible.
.proc SomeRoutine
ptr := $06
lda ptr
sta stash
rts
stash: .byte 0
.endproc ; SomeRoutine- Use
Implif the entry point is not at the start:
.proc SomeRoutineImpl
stash: .byte 0
ptr := $06
start: lda ptr
sta stash
rts
.endproc ; SomeRoutineImpl
SomeRoutine := SomeRoutineImpl::start- Delimit procedures with comments and document inputs, outputs, errors, and other assumptions.
;;; ============================================================
;;; Twiddles a thing.
;;; Inputs: A,X = address of the thing
;;; Output: Z=1 on success, 0 on failure
;;; Error: On fatal error, `error_hook` is invoked.
;;; Assert: Aux LCBANK1 is active
;;; NOTE: Trashes $6/7
.proc TwiddleTheThing
...
.endproc ; TwiddleTheThing- Macro use is encouraged.
- Use local macros to avoid repeating code.
- Use
inc/macros.incand extend as needed to capture patterns such as 16-bit operations - API calls such as ProDOS MLI calls should be done with macros
- Naming:
- Macros that mimic ca65 control commands should be lowercase, dot-prefixed, e.g.
.pushorg,.params - Macros that provide pseudo-opcodes should be lowercase, e.g.
ldax,add16 - Macros that
.definepseudo-constants should usekTitleCase - Other macros should be named with
SHOUTY_CASE
- Macros that mimic ca65 control commands should be lowercase, dot-prefixed, e.g.
The following macros should be used to improve code readability by eliminating repetition:
- pseudo-ops:
add16/sub16/cmp16/lsr16/asl16/inc16/dec16for 16-bit operationsldax/ldxy/stax/stxyfor 16-bit load/storescopy8/copy16for load-then-storejcc/jeq/etc for long branches
- memory:
COPY_xxfor fixed size copy loops
- flow control:
IF/ELSE_IF/ELSE/END_IFfor conditional branches, to avoid throw-away labelsDO/REDO_IF/CONTINUE_IF/BREAK_IF/WHILEfor loopings, to avoid throw-away labelsCALL proc, AX=params, Y=#opt(andTAIL_CALL) for more semantic function callsRETURN C=1, AX=#valfor more semantic return values
- definitions:
PASCAL_STRINGfor length-prefixed strings
- misc:
PAD_TOto introduce padding to a known address
Parameter blocks are used for ProDOS MLI, MGTK and other toolkit calls.
- Wrap param data in
.paramsblocks:
.params textwidth_params
textptr: .addr text_buffer
textlen: .byte 0
result: .word 0
.endparams
;; elsewhere...
MGTK_CALL MGTK::TextWidth, textwidth_params(.params is an alias for .proc)
- Parameter blocks placed at a fixed location in memory use the
PARAM_BLOCKmacro:
PARAM_BLOCK zp_params, $80
flag1 .byte
flag2 .byte
END_PARAM_BLOCKThis is equivalent to (and is defined using) ca65's .struct with .org, but also defines a label for the block itself.
- Use helper macros for defining common parameter blocks:
;; Examples from inc/prodos.inc
DEFINE_OPEN_PARAMS open_params, pathname, io_buffer
DEFINE_READ_PARAMS read_params, buffer, kBufferSize
DEFINE_CLOSE_PARAMS close_params
;; Examples from mgtk/mgtk.inc
DEFINE_RECT rect, kLeft, kTop, kRight, kBottom
DEFINE_MENU menu_edit, kMenuSizeEdit
DEFINE_MENU_ITEM label_select_all
;; Examples from other toolkits
DEFINE_BUTTON ok_button, kWindowId, res_string_button_ok, kGlyphReturn, kOKButtonLeft, kButtonTop
DEFINE_LINE_EDIT line_edit, kWindowId, str_buffer, kTextBoxLeft, kTextBoxTop, kTextBoxWidth, kMaxLengthCurrently, only MGTK constants are wrapped in a .scope to provide a namespace. We may want to do that for ProDOS and DeskTop stuff as well in the future.
- Add a label for the value being modified (byte or address). Use cheap local labels via the
@-prefix where possible to make self-modification references more visible. - Use
SELF_MODIFIED($1234) orSELF_MODIFIED_BYTE($12) to self-document.
sta @jump_addr
stx @jump_addr+1
@jump_addr := *+1
jmp SELF_MODIFIED sty @count
ldy #0
: sta table,y
iny
@count := *+1
cpy #SELF_MODIFIED_BYTE
bne :-- Try to assert any compile-time assumptions you can:
- Structure sizes
- Equality between constants (e.g. when relying on a const for an always-branch)
- Memory placement of blocks or members
- Use ca65
.assertdirective as needed, and these macros:ASSERT_EQUALS- equality comparisonASSERT_ADDRESS- current addressASSERT_TABLE_SIZE(bytes),ASSERT_ADDRESS_TABLE_SIZE(words),ASSERT_RECORD_TABLE_SIZE
- ca65 does not allow using
.sizeof()to get the size of scope/procedure before its definition appears. (cc65/cc65#478) To work around this, add asizeof_procdefinition after the procedure:
ldy #sizeof_relocate - 1
: lda relocate,y
sta dst,y
dey
bpl :-
rts
.proc relocate
...
.endproc
sizeof_relocate = .sizeof(relocate)- ca65 allows the members of a named scope to be referenced before the scope is defined, but infers the scope to be in the local scope. If this is not the case, an explicit scope reference can be used (e.g.
jsr ::main::DoTheThing) or the scope can be predefined using thePREDEFINE_SCOPEmacro.
.scope aux
PREDEFINE_SCOPE ::main
...
jsr main::DoTheThing
...
.endscope ; aux
.scope main
...
.proc DoTheThing
...
.endproc ; DoTheThing
.endscope ; mainThis is especially critical if "name shadowing" is used:
;; in parent scope
.params foo_params
bar: .byte 123
.endparams
.proc DoTheThing
;; Reference local params, not any shadowed in the parent scope
PREDEFINE_SCOPE DoTheThing::foo_params
lda foo_params::bar
;; in local scope
.params foo_params
bar: .byte 456
.endparams
.endproc ; DoTheThingLocalization (translations of the application into other languages) is done by ensuring that all resources that need to be changed exist in files outside the source. For a given file (e.g. foo.s) if there are localized resources they are present in the res/foo.res.LANG file where LANG is a language code (e.g. en, it, etc).
foo.s:
RESOURCE_FILE "foo.res"
hello:
PASCAL_STRING res_string_hello_world
lda event_key
cmp #res_char_yes_keyres/foo.res.en:
.define res_string_hello_world "Hello World"
.define res_char_yes_key 'Y'Conventions:
res_string_for stringsres_char_for characters (most commonly keyboard shortcuts)res_const_for constant numbers
Additionally:
res_string_..._patternfor strings with placeholders (and use # for replaced characters)res_const_..._pattern_offsetNare auto-generated for such pattern strings (for each #, 1...N)
Since often multiple files are assembled together by jumbo files via includes and .defines are not scoped, conflicting identifiers are possible.