C64jasm is a symbolic assembler for the Commodore 64 that supports:
C64jasm also runs in the browser, you can try this interactive assembler demo to play with it.
C64jasm is free and open source -- its source code can be found here: c64jasm on GitHub.
In order to use the c64jasm assembler, you need to install the following:
Furthermore, if you wish to use c64jasm with VSCode, you should also install:
Assembler installation: npm install -g c64jasm
Upon successful installation, running c64jasm --help
in your shell should work.
VSCode extension: Search for c64jasm
in the VSCode marketplace and install.
VICE: See the VICE website for download and installation instructions. Once you have it installed, make sure the VICE emulator binary x64
is in your PATH
.
Extras: Vim users: C64jasm vim plugin for syntax highlighting and better editing support: c64jasm plugin for vim
Assuming you successfully installed the C64jasm command line compiler, you should be able to compile and run some code. Let's build the sprites
sample from under examples/sprites/:
git clone https://github.com/nurpax/c64jasm
cd c64jasm/examples/sprites
c64jasm --out sprites.prg sprites.asm
x64 sprites.prg
You should see something like this in your VICE window:
If you installed the necessary VSCode parts of VSCode, you should be able to load this example project in VSCode and build it with Ctrl+Shift+P
+ Tasks: Run Build Task
. Build errors will be reported under the Problems tab and you should be able to hit F5
to start your program in VICE.
Run c64jasm --help
for all c64jasm command line options.
Basic usage:
c64jasm --out output.prg source.asm
where output.prg
is the desired output .prg
filename and source.asm
is the assembly source you want to compile.
Like many modern compiler tools, c64jasm supports "watch mode". Watch mode automatically recompiles your source code when any of the input source files change. To use watch mode, invoke c64jasm with the --watch <DIR>
argument as follows:
c64jasm --out output.prg --watch src src/source.asm
C64jasm will watch the directory specified with --watch <DIR>
(and its subdirectories) for any changes and recompile when anything changed. Changes to all types of input files (.asm files, plugin .js files, files loaded by .js extensions, !include/binary
'd files, etc.) are considered as rebuild triggers.
A good project structure that makes it easy to work with watch mode is to place all your source files and assets under a single root directory, say src
. This makes it easy to specify the watched directory with a single --watch src
argument.
Watch mode works well with VSCode. The .vscode
configs for examples/ are setup to use watched compiles.
You can output debug symbols for VICE and C64Debugger with the following options:
--vice-moncommands
for VICE monitor--c64debugger-symbols
for C64Debugger debug infoDebug symbol files can be used for setting breakpoints and enables source level debugging in C64Debugger.
See my blog Debugging C64jasm projects with VICE and C64Debugger for a short tutorial.
C64jasm has fairly extensive symbolic macro assembly support. This includes macros, compile-time variables, for-loops, if/else, and source and binary file inclusion.
Assembler pseudo directives start with a bang !
. Examples: !let
, !if
, !include
.
; Clear the screen RAM (all 1024 bytes) clear_screen: { lda #$20 ldx #0 loop: sta $0400, x sta $0400 + $100, x sta $0400 + $200, x sta $0400 + $300, x inx bne loop rts }
A label followed by braces {}
starts a new scope. Labels declared inside the braces will be local to that scope. Labels declared within such a scope can still be referenced by using the namespacing operator ::
, e.g.,
memset256: { ldx #0 loop: sta $1234, x ptr: inx bne loop rts } ; Use self-modifying code to set target ptr ; for a memset lda #<buf ; take lo byte of 'buf' address sta memset256::ptr-2 lda #>buf ; take hi byte of 'buf' address sta memset256::ptr-1 jsr memset256 buf: !fill 256, 0
You can guard a whole file inside a scope if you start the source file with !filescope
:
; Contents of util.asm !filescope util !macro inc_border() { inc $d020 }
Using util.asm
from another file:
; Contents of main.asm !include "util.asm" +util::inc_border()
Label, variable and macro names all follow the same scoping rules. Symbol references are relative to the current scope. If you need to reference a symbol in the root scope, use ::foo::bar
:
bar: { !let foo = 20 } foo: { bar: { !let foo = 0 } lda #bar::foo ; evaluates to 0 lda #::bar::foo ; evaluates to 20 }
It is not legal to declare symbols with the same name in the same scope. This will fail:
!let var1 = 0 !let var1 = 1 ; error: Variable 'var1' already defined ; but so will for example declaring a variable and a macro with the same name: !let xyz = 0 !macro xyz() { ; error: Symbol 'xyz' already defined nop } +xyz()
Emitting bytes/words:
foo: !byte 0 ; declare 8-bit bar: !word 0 ; declare 16-bit int (2 bytes) baz: !byte 0,1,2 ; declare bytes 0,1,2 baz_256: ; 256 bytes of zero !fill 256, 0
Including other source files:
!include "macros.asm"
Including binary data:
; As of c64jasm you will use keyword args with !binary. !binary (file="file1.bin") ; all of file1.bin !binary (file="file2.bin", size=256) ; first 256 bytes of file !binary (file="file2.bin", size=256, offset=8) ; 256 bytes from offset 8 ; Earlier syntax is still supported and will be kept around. !binary "file2.bin", 256, 8 ; 256 bytes from offset 8
Text strings: C64jasm does not have built-in PETSCII or screencode emitting directives like !text
or !scr
. You can, however, implement this yourself using an assembler plugin. See this gist for an example. Armed with such a plugin, you can write code like this:
!use "./text" as text ; see the above gist link for full code! !byte text("testing 123")
You can declare a variable with !let
. You can use standard C operators like +
, -
, *
, /
, <<
, >>
, &
, |
, ~
with them.
!let num_sprites = 4 lda #(1 << num_sprites)-1 sta $d015
Variable assignment:
!let a = 0 ; declare 'a' a = 1 ; assign 1 to 'a' !! a = 1 ; assign 1 to 'a' (same as above, see Statements)
Variables take on JavaScript values such as numbers, strings, arrays and objects. We will explore later in this document why array and object values are useful.
Array literals:
!let foo = [0,2,4] lda #foo[1] ; emits LDA #02
Object literals:
; Declare zero-page offset helper !let zp = { tmp0: $20, sprite_idx: $22 } lda #3 sta zp.sprite_idx
The implicit *
label always points to the current line's address. You can use it to for example jump over the next instruction:
bcc *+3 nop ; bcc jumps here if carry clear
You can access the lo/hi parts of a 16-bit value or a memory address with <
and >
. To keep the parser simple, this syntax only works in immediate fields like lda #<foo
. To access lo/hi parts of any value within any expression context, use bitwise operations &
and >>
like in the below example:
lda #<buf ; take lo byte of 'buf' address ldx #>buf ; take hi byte of 'buf' address buf: !byte 0, 1, 2, 3 !let lbl_lo = buf & 255 ; lo byte of 'buf' address !let lbl_hi = buf >> 8 ; hi byte of 'buf' address
Conditional assembly is supported by !if
/elif
/else
.
!let debug = 1 !if (debug) { ; set border color to measure frame time inc $d020 } ; Play music or do some other expensive thing jsr play_music !if (debug) { dec $d020 }
Use !for
to repeat a particular set of instructions or data statements.
For-loops are typically written using the built-in range()
function that returns an array of integers. This works similar to Python's range
built-in.
!let xyptr = $40 ; alias zero-page $40 to xyptr ; shift left xyptr by 5 (e.g., xyptr<<5) !for i in range(5) { asl xyptr rol xyptr+1 }
Lookup table generation:
lda #3 ; index == 3 tax lda shift_lut, x ; A = 1<<3 ; Create a left shift lookup table shift_lut: !for i in range(8) { !byte 1<<i }
If you want to loop over some small set of fixed values (say 1
, 10
, 100
), you can use array literals with !for
:
!for i in [1, 10, 100] { ... }
Statements such as variable assignment or calling a plugin function for just its side-effect is expressed by starting the line with !!
:
; 'my_log' would be a JavaScript extension in your project !use "my_log_plugin" as log !let a = 0 !! a = 1 ; assign 1 to 'a' !! log.print("hello") ; console.log()
Macros are declared using the !macro
keyword and expanded by +macroname()
.
; move an immediate value to a memory location !macro mov8imm(dst, imm) { lda #imm sta dst } +mov8imm($40, 13) ; writes 13 to zero page $40
Any labels or variables defined inside a macro definition will be local to that macro. For example, the below code is fine -- the loop
label will not conflict:
; clear 16 bytes starting at 'addr' !macro memset16(addr) { ldx #15 loop: sta addr, x dex bpl loop } +memset16(buffer0) +memset16(buffer0) ; this is ok, loop will not conflict
However, sometimes you do want the macro expanded labels to be visible to the outside scope. You can make them visible by giving the (normally anonymous) macro expansion scope a name by declaring a label on the same line as your macro expand:
; A = lo byte of memory address ; B = hi byte of memory address clear_memory: { sta memset::loop+1 stx memset::loop+2 memset: +memset16($1234) ; macro labels are under memset rts }
C64jasm injects some commonly used functionality into the global scope.
range(len)
: Return an array of len
elements [0, 1, .., len-1]
range(start, end)
: Return an array of elements [start, start+1, .., end-1]
loadJson(filename)
: Load a JSON file filename
All JavaScript Math
constants and functions (except Math.random
) are available in the Math
object:
Constants: Math.E
, Math.PI
, Math.SQRT2
, Math.SQRT1_2
, Math.LN2
, Math.LN10
, Math.LOG2E
, Math.LOG10E
.
Functions: Math.abs(x)
, Math.acos(x)
, Math.asin(x)
, Math.atan(x)
, Math.atan2(y, x)
, Math.ceil(x)
, Math.cos(x)
, Math.exp(x)
, Math.floor(x)
, Math.log(x)
, Math.max(x, y, z, ..., n)
, Math.min(x, y, z, ..., n)
, Math.pow(x, y)
, Math.round(x)
, Math.sin(x)
, Math.sqrt(x)
, Math.tan(x)
.
Math.random()
is not allowed as using a non-deterministic random would lead to non-reproducible builds. If you need a pseudo random number generator (PRNG), write a deterministic PRNG in JavaScript and use that instead.
Segments offer extra flexibility over code and data mamory layout. They allow decoupling memory order from source code order.
By default, everything is compiled into a segment called default
.
Declare a new segment with !segment <name>(start=<addr>, end=<addr>)
(start
and end
both inclusive).
Activate a segment with !segment <name>
. Activating a segment means any code or data emitted by the compiler will now go into the active segment.
An example to illustrate. The disassembly in comments shows where code and data gets placed in the output.
!segment code(start=$2000, end=$2100) !segment gfx(start=$3000, end=$3100) * = $801 lda #0 ; 0801: A9 00 LDA #$00 jsr start_part ; 0803: 20 00 20 JSR $2000 ; switch to code segment, any emitted code or data will be stored at ; address $2000 or above !segment code start_part: ; 2000: 60 RTS rts ; switch to data. any emitted code or data will be stored at ; address $3000 or above !segment gfx !byte 0,1,2,3 ; 3000: 00 01 02 03 !segment default ; switch back to default segment, resume at address $806 lda #1 ; 0806: A9 01 LDA #$01 jsr start_part ; 0808: 20 00 20 JSR $2000
As of c64jasm v0.9.0, all segments are output into the prg output defined by the --out <out.prg>
argument. Future versions of c64jasm will support specifying a PRG or a binary output file for each segment.
Extending the assembler with JavaScript was the primary reason why C64jasm was built. This is a powerful mechanism for implementing lookup table generators, graphics format converters, etc.
Learning resources on c64jasm extensions:
A c64jasm extension is simply a JavaScript file that exports a function ("default" export) or a JavaScript object containing functions (named exports). The functions can be called from assembly and their return values can be operated on using standard c64jasm pseudo ops.
Minimal example:
math.js:
module.exports = {
square: ({}, v) => {
return v*v;
}
}
test.asm:
!use "math" as math !byte math.sqr(3) ; produces 9
Here's another example. Here we'll compute a sine table (see examples/sprites). This extension uses the JavaScript module "default export", ie. it exports just a single function, not an object of function properties.
sintab.js:
module.exports = ({}, len, scale) => {
const res = Array(len).fill(0).map((v,i) => Math.sin(i/len * Math.PI * 2.0) * scale);
return res; // return an array of length `len`
}
foo.asm:
!use "sintab" as sintab !let SIN_LEN = 128 !let sinvals = sintab(SIN_LEN, 30) sintab: !for v in sinvals { !byte v }
An extension function is declared as follows:
(context, ...args) => { return ... };
For example, if you're defining an extension function that takes one input argument, it must be declared as:
(context, arg0) => { return ... };
C64jasm calls an extension function with a context
value that contains some extra functions for the extension to use. The rest of the arguments (...args
) come from the assembly source invocation. For example:
!let v = math.sqr(3)
will be called as:
// const sqr = (context, arg0) => return arg0*arg0;
sqr(context, 3);
If your extensions doesn't need anything from the context
parameter, you can declare your extension function like so: ({}, arg0) => return arg0*arg0;
The context
parameter contains functionality that an extension can use to load input files. It may also be extended to contain functions for error reporting.
Currently (c64jasm 0.3), the context
object contains the following properties:
readFileSync(filename)
: synchronously read a file and return it as a byte bufferresolveRelative(filename)
: resolve a relative filename to an absolute pathA well-behaving extension would use these to load input files as follows:
const loadJson = ({readFileSync, resolveRelative}, fname) => {
const json = JSON.parse(readFileSync(resolveRelative(filename)));
return json;
}
module.exports = loadJson;
A relative filename is relative to the location of the assembly source file that called the extension. E.g., assuming the following folder structure:
src/
main.asm
assets/
petscii.json
Consider calling an extension with a filename assets/petscii.json
from main.asm
:
!use "json" as json !let j = json("assets/petscii.json")
Suppose you invoke c64jasm outside of the src
directory like: c64jasm ./src/main.asm
. As main.asm
is being compiled, c64jasm knows it resides in ./src/main.asm
and with resolveRelative
, an extension knows how to resolve assets/petscii.json
to ./src/assets/petscii.json
.
You might be asking: why do I need context.readFileSync
when I could just as well import Node's readFileSync
and use that.
Using the c64jasm provided I/O functions is necessary as it allows for c64jasm to know about your input files. For example, if you're running c64jasm in watch mode, it can cache all your input files if they didn't change since the previous compile.
Use throw new Error
in your JavaScript extension to report errors back to the assembler. The assembler will turn these into assembler errors.
plugin.js:
module.exports = ({}, arg0) => {
if (arg0 < 5) {
return arg0;
}
throw new Error('arg0 must be less than 5');
}
test.asm:
!use "./plugin" as p !let b = p(10) ; reports an error like below ; test/errors/js_errors3.js:5:11: error: Error: arg0 must be less than 5
context.readFileSync
to load files.Math.random
will lead to inconsistent/broken build results. This is because c64jasm aggressively caches the results of plugin invocations in watched compile mode. Also plugin functions can be called multiple times during compilation (at minimum once per compilation pass).A limited form of side-effects is permitted though. It is OK for an extension function to return a closure that holds its internal state. For example this code is fine:
module.exports = {
create: ({}, initial) => {
const stack = [initial];
return {
push: (elt) => {
stack.push(elt)
},
pop: () => stack.pop(),
top: () => {
return stack[stack.length-1];
}
}
}
}
Usage in assembler:
!use "stack" as stack !let s = stack.create({ tmp0: $20 }) !let zp = s.top() lda #zp.tmp0
In this example, the stack
array holds the state which can be manipulated by calls to push(elt)
, pop()
.
c64jasm 0.9.2 (2021-04-10):
c64jasm 0.9.1 (2021-02-22):
--disasm
and --disasm-file=<FILE>
to just --disasm=<FILE>
(use -
to print to stdout.)--dump-labels
and --labels-file
to just --dump-labels=<FILE>
(again, use -
for printing to stdout.)c64jasm 0.9.0 (2021-02-12):
!segment
to easy memory layout.!binary
file/size/offset arguments. Old syntax still works and will be kept around.--labels-file=<OUTFILE>
command line argument that can be used to save symbol address mapping.--dump-labels
output.c64jasm 0.8.1 (released on 2020-02-05):
!include
and other file loading were sometimes not trigger an error due to parser caching.c64jasm 0.8.0 (released on 2019-11-11):
jmp *
, inc *-3
, etc.) that returns the current program position.c64jasm 0.7.0 (released on 2019-07-05):
LDA #FF
changed to LDA #$FF
.c64jasm 0.6.0 (released on 2019-07-26):
c64jasm 0.5.1 (released on 2019-07-18):
c64jasm 0.5.0 (released on 2019-07-14):
c64jasm 0.4.0 (released on 2019-06-29):
c64jasm 0.3.0:
c64jasm 0.2.0: