A Forth-based assembler for 65c816 and other 6502-family processors
- Works for generating small binary objects with no linking or object format
- Forth assembler-style loop constructs remove (most of) the need for symbols
- Nice hexadecimal debug output showing loop and conditional backpatching
- Mostly portable Forth code
- Easy to add instructions from other processor variants
- Instead of macros, the full power of the hosting Forth environment is avaliable
- Mostly 816-centric; originally the branches used BRA ($80) which is not present on NMOS varieties. (mitigated for now with clc, bcc)
- (Mis-)uses the Forth BLOCK words for assembling arbitrarily large target data. In standard forth, block zero should not be used, but here it is.
- Non-long-addresses (i.e. all 6502 style addresses) are limited to the wordsize of the host Forth system.
- On a 16bit forth, that limits things to 64k, but code is bank-wrapped on the 65816 anyway so that should be fine.
- Explicitly 24bit address forms are split into (low16, bank) form; this maps nicely to the Forth doubleword syntax on 16bit forths but is easier specified separately on larger (32, 64bit, etc) host systems.
- Replace the BLOCK access words with equivalent file words to improve portability
- Split instruction table handling by cpu-type, so that N6502, 65C02, 65SC02, 65CE02, Hudson C6280, etc., and 65816/65802 activate different sets of accepted words.
- Likewise make loop operators use appropriate features for the active processor (
offset bra:rvsclc:. offset bcc:r) - Restructure those tables for more useful disassembly with timing information
- Forth in general, especially early FIGForth. The FIGForth model 6502 implementation had a wickedly clever little assembler, though that one specified addressing-mode in stateful prefix words.
- Brad Rodriguez's excellent articles on writing assemblers in Forth: https://www.bradrodriguez.com/papers/tcjassem.txt etc.
- Scot Stevenson's excellent "A Typist's 65816 Assembler" https://github.com/scotws/tasm65816/ - very similar goals and design choices, strongest inspiration for doing this
Relative to scotws's assembler, this one uses colon (:) to separate instruction mnemonic and addressing mode; Forth likes to parse unknown symbols as numbers in whatever number base is in use, so things like ADC.A rightfully generate warnings in better forth compilers.
Additionally the modes are shuffled a bit:
| tasm65816-style | 816asm-style |
|---|---|
| (blank absolute) | :a |
| .a (accumulator) | :. (into implied) |
| .x | :ax |
| .y | :ay |
| .il | :ail |
A variety of instructions are spelled a bit different where convenient, but most forms are accepted. (NMOS, CMOS, and 816 are all active at once, which is usually not desirable)
This assembler uses a unified address-mode word for implementing the instructions, with two-stage or "nested" DOES> forms, but that probably makes no difference for end users, should they ever exist.
detect-sim.fs- An implementation of Chromatix's CPU detection method; see this forum post; demonstrates assembling some tricky code and using conditional forms. Outputs a binary image suitable for llvm-mos-sdk's simulator; when run this emits a letter for the CPU it found:
\ O - old NMOS 6502 rev A (no ROR insn)
\ N - NMOS 6502 post rev A (ROR working)
\ n - NMOS simulator with 1-byte NOPs
\ d - NMOS without Decimal mode (eg. Ricoh 2A03)
\ S - 65SC02
\ C - 65C02
\ E - 65CE02
\ H - HuC6280
\ 8 - 65C816 or 65C802
Example invocation, with debug dump showing the back-patching as addresses are determined:
$ gforth examples/detect-sim.fs
1FFC: 00 20 00 00
2000:A9 00 85 84 85 85 A9 1D 85 83 A9 6B 85 1D A9 4E
2010:47 83 45 83 C9 4E D0 FE 58 F8 A9 90 69 10 D8 78
2020:90 FE A9 4E 2A 6A B0 FE A9 4F
2027: 02
202A: 18 90 FE
2021: 0B
202D: A9 64
202C: 02
2017: 17
202F: C9
2030:53 D0 04 80 02 A9 6E C9 43 D0 FE A0 01 C0 01 C2
2040:08 D0 FE C0 00 D0 FE 7A A9 48
2046: 03
204A: 18 90 FE
2042: 0A
204D: A9 45
204C: 02
203A: 14
204F: C9
2050:38 D0 FE 38 FB B0 FE 7A
2056: 01
2052: 05
2058: 8D F9 FF A9 0A 8D F9 FF
2060:A9 00 8D F8 FF 00 00
1FFE: 67 00
2067: FA FF 06 00 00 20 00 20 F7
2070:FF
dos33-bootsector.fs- Assembles to Apple DOS 3.3's boot sector (minus the patch area) in a 5.25" .dsk image (with only that boot sector in it).
$ gforth examples/dos33-bootsector.fs
0800:01 A5 27 C9 09 D0 FE A5 2B 4A 4A 4A 4A 09 C0 85
0810:3F A9 5C 85 3E 18 AD FE 08 6D FF 08 8D FE 08
0806: 18
081F: AE
0820:FF 08 30 FE BD 4D 08 85 3D CE FF 08 AD FE 08 85
0830:27 CE FE 08 A6 2B 6C 3E 00
0823: 15
0839: EE FE 08 EE FE 08 20
0840:89 FE 20 93 FE A6 2B 6C FD 08 00 0D 0B 09 07 05
0850:03 01 0E 0C 0A 08 06 04 02 0F EA EA EA EA EA EA
0860:EA EA EA EA EA EA EA EA EA EA EA EA EA EA EA EA
0870:EA EA EA EA EA EA EA EA EA EA EA EA EA EA EA EA
0880:EA EA EA EA EA EA EA EA EA EA EA EA EA EA EA EA
0890:EA EA EA EA EA EA EA EA EA EA EA EA EA EA EA EA
08A0:EA EA EA EA EA EA EA EA EA EA EA EA EA EA EA EA
08B0:EA EA EA EA EA EA EA EA EA EA EA EA EA EA EA EA
08C0:EA EA EA EA EA EA EA EA EA EA EA EA EA EA EA EA
08D0:EA EA EA EA EA EA EA EA EA EA EA EA EA EA EA EA
08E0:EA EA EA EA EA EA EA EA EA EA EA EA EA EA EA EA
08F0:EA EA EA EA EA EA EA EA EA EA EA EA EA 00 B6 09
Mnemonics are suffixed by their address mode:
| Mode name | Insn Width | 816asm style | Traditional style |
|---|---|---|---|
| absolute | 3 | :a |
0000 |
| absolute-indexed-x | 3 | :ax |
0000,X |
| absolute-indexed-y | 3 | :ay |
0000,Y |
| long-absolute | 4 | :l |
000000 |
| long-absolute-indexed-x | 4 | :lx |
000000,X |
| absolute-indirect | 3 | :ai |
(0000) |
| absolute-indexed-x-indirect | 3 | :axi |
(0000,X) |
| absolute-indirect-long | 3 | :ail |
[0000] |
| direct | 2 | :d |
00 |
| direct-indexed-x | 2 | :dx |
00,X |
| direct-indexed-y | 2 | :dy |
00,Y |
| direct-indirect | 2 | :di |
(00) |
| direct-indexed-x-indirect | 2 | :dxi |
(00,X) |
| direct-indirect-indexed-y | 2 | :diy |
(00),Y |
| direct-indirect-long | 2 | :dil |
[00] |
| direct-indirect-long-indexex-y | 2 | :dily |
[00],y |
| immediate-8 | 2 | :# |
#00 |
| immediate-16 | 3 | :## |
#0000 |
| implied | 1 | :. |
(empty or A) |
| stack-relative | 2 | :s |
00,S |
| stack-relative-indirect-indexed-y | 2 | :siy |
(00,s),y |
| block-move | 3 | := |
00,00 |
| immediate-m-size | 2 or 3 | :#m |
#00 or #0000 |
| immediate-x-size | 2 or 3 | :#x |
#00 or #0000 |
| relative-8 | 2 | :r |
(usually by label, but +/- 00) |
| relative-16 | 3 | :rr |
(usually by label, but +/- 0000) |
| rockwell-bit-op | 2 | :rw or 0:d |
0 00 |
| rockwell-branch | 2 | :rwb or 0:d |
0 00 |
6502 condition codes are mapped to flag labels:
| Name | Flag |
|---|---|
| plus | pl/ |
| minus | mi/ |
| oVerflow clear | vc/ |
| oVerflow set | vs/ |
| carry clear | cc/ |
| carry set | cs/ |
| not equal | ne/ |
| equal | eq/ |
| less than | lt/ |
| greater equal | ge/ |
With the condbranch word selecting the right branch instruction for the flag. Normally, condbranch is instead called by the structured condition words if,, else,, then, which append the appropriate instructions with necessary backpatching:
'A' cmp:#m eq/ if,
...code run when accumulator held 'A'...
then,
Or
'B' cmp:#m eq/ if,
...code run when accumulator held 'B'...
else,
...code run otherwise...
then,
Or even arbitrary nesting, as in the detect-sim.fs example:
'N' cmp:#m eq/ if, \ Quacks like NMOS so far
cli:. sed:. \ Check for broken decimal
90 lda:#m 10 adc:#m
cld:. sei:. cs/ if,
'N' lda:#m \ Decimal works
rol:. ror:. \ Check for broken ROR
cc/ if,
'O' lda:#m \ No ROR implemented!
then,
else,
'd' lda:#m \ Decimal missing like 2A03
then,
then,
Looping is similar, with begin,, until,, again,, while,, and repeat,, which work in the traditional Forth way. Back-references for these are held on the host-Forth's parameter stack along with matching sentinels for structure error detection.
Infinite loop:
begin,
again,
Loop until condition met:
begin,
...code that leaves flags register set...
cc/ until,
More conventional while-loops:
begin,
...code that leaves flags register set...
cc/ while,
...
repeat,
On 816 there are long-branch equivalents ifl,, elsel,, and thenl,.
Processor M/X bit status is tracked where possible (constant rep:#, sep:#, and xce:.). set-emulation returns the assembler to 6502 mode. If necessary, the internal assembler variables asm-m, asm-x, and asm-e hold the assembler's understanding of the current mode for future assembly.
THERE is the current target memory address (analogous to Forth HERE). There-based primitives for memory manipulation include:
| Word | Name |
|---|---|
tc@ |
there character fetch |
tc! |
there character store |
tc, |
there character append |
t@ |
there word (16bit LE) fetch |
t! |
there word store |
t, |
there word append |
talign |
zeropad at there to requested alignment |
t," |
append an inline forth string there |
ta>p |
convert there-address to pointer |
origin! |
update there data pointer offset like ORG |