Introduction

  • ACME like syntax

  • Aims to be on par with Kickassembler in automation/scriptability, but with less complexity

  • Unit tests through internal emulator

  • Open and easily extendable source

See this TUTORIAL for a tutorial / introduction.

Normal operation

Run bass <source_file> to assemble and produce result.prg, which will be a standard C64 executable (first 2 bytes are run address).

bass -f raw -i myfile.asm -o myfile.bin

Will assemble myfile.asm to myfile.bin without the 2-byte header.

bass -DSKIP_INTRO=1 -x sid.lua music.asm

Will set SKIP_INTRO=1 and load the specified lua file before assembling.

Running programs

To run an assembly program in the internal emulator (see PET100.adoc) use the --run option. This will assemble the given file and load all it’s section into the emulators memory. It will also watch the file for changes, and re-assemble and restart on change.

Example source

    VRAM_LO = $9f00
    VRAM_HI = $9f01
    !macro SetVRAM(a) {
        .lo = <a;
        .hi = >a;
        lda #.lo
        sta VRAM_LO
        !if .lo != .hi { lda #.hi }
        sta VRAM_HI
    }

    !section "main", $1000
start:

    ; Clear 1K
    ldx #0
$   !rept 4 { sta $1000+i*256,x }
    dex
    bne -

    SetVRAM(0x1234)
    ldx #end - text
$   lda start,x
    sta $1000,x
    dex
    bne -
    rts

text:
    !byte "Text to copy", 0
end:

    ; --- Generate sine table with and without LUA
    mysin = [ x, amp, size -> sin(x * Math.Pi * 2 / size) * 0.5 + 0.5) * amp ]
sin:
    !fill 100 { mysin(i, 255, 100) }

%{ -- LUA code
function make_sin(amp, size)
    res = {}
    for i=0,size-1,1 do
        res[i+1] = (math.sin(i * math.pi * 2/ size) * 0.5 + 0.5) * amp
    end
    return res
end
}%

sin2:
    !fill make_sin(255, 100)

    !assert make_sin(5, 5)[1] == round(mysin(1, 5,5))
    !assert make_sin(5, 5)[3] == round(mysin(3, 5,5))

See example.asm for a full example.

Features

Unit Tests

You can pop a !test line in front of any subroutine, and it will be executed in the bass emulator as part of the build process.

You can also use !check to verify register contents, or even execute lua code during the test run.

This won’t affect code generation.

    !test A=3,X=4,Y=5
add_axy:
    clc
    stx $02
    !run {: print_fmt("{:x}", mem_read(2)) :}
    adc $02
    sty $02
    adc $02
    !check A == 12
    rts

Functional programming

Bass uses the traditional support in assemblers to allow you to evaluate expressions wherever constants are used and takes it further.

You can think of it like a bass specific language that can be used whenever expressions can be evaluated.

There are also no special types of variables, everything is stored in the symbol table.

Symbols can take on non-numeric values, such as arrays, strings, lambdas and maps, and there are functions that can operate on and transform these values.

A lot of logic can therefore be performed directly in assembly using functions.

load_adress = be_word(load("some.prg")[0:2])

!rept 100 { label[my_fun(i)]:
              lda #(i + other_fn(i))
              !fill some_array[i*10:i*10+10]
           }

Graphics Conversion

There are usually only a few fundamental image operations you need to convert your artwork into the specific format you need for your platform.

You need to be able to convert your pixels to the right format, and you need to lay out the image in memory the right way.

The work horse function for image layout is called (of course) layout_image(). It takes an image and a tile size (width * height) and splits the image into tiles of that size and lays the out after each other.

TBD

Indexed Labels

Labels can be indexed. This is convenient for creating repeated labels inside macros or rept statements.

    !rept 5 {
vec[i]:
        lda #0
        jmp jump_start + i * 3
}
    jsr vec[3]

Arrays and slices

Symbols can refer to arrays in addition to numbers. Arrays can be sliced. Array literals can be used directly.

Note that there is a difference between an array of bytes and an array of numbers.

some_bytes = bytes(9,8,7,6,5)
!assert some_bytes[3:2] == bytes(6,5)

a_list = [3.14, 9999, $5000]
!rept a_list { !print v }

data = load("some.prg")
load_address = be_word(data[0:2])

Lua

Lua is fully integrated in the assembler. Lua code can be used to generate data, or even be called from 6502 code during testing.

$-variable

If a function result is not assigned to a return value, it is automatically placed in the special variable $. It makes it easier to chain expressions without using temporary variables.

    load_png("../data/face.png")
    layout_image($, 8, 8)
    image = index_tiles($.pixels, 8*8)

Lambdas

A lambda is also a value. This can be used to fill memory.

my_sin = [x -> sin(x) * 255 ]

sine0:
    !fill 256, my_sin

Function Reference

Assembler functions

These functions are used directly from the assembler. They normally do not have any side effects.

sin(f), cos(f), tan(f), asin(f), acos(f), atan(f)

Trigonometric functions.

sqrt(f), min(a,b), max(a,b), pow(f), round(f), floor(f), ceil(f)

Other math functions.

random()

Returns a random integer between 0 and RAND_MAX (usually 2^31).

len(v)

Returns the length of the given array.

load(file)

Loads the file and returns an array of the content.

word(v)

Takes an array of at least 2 elements and returns the 16-bit value stored.

    data = load("file.prg")
    loadAddress = word(data[0:2])

big_word()

Like word(), but in big endian format.

zeroes(n)

Create a byte array of n zeroes.

bytes(a, b, c, …​)

Create a byte array containing the given bytes.

to_lower(s)

Convert the given string to lower case.

to_upper(s)

Convert the given string to upper case.

str(n)

Turns a number into a decimal string.

num(s)

Turns a string into a number.

Image Functions

load_png(filename)

Load the png image into memory. Returns an image structure looking like this;

{ pixels, colors, width, height, bpp }

ex

   img = load_png("test.png")
   !assert img.width <= 256 && img.height <= 256
data:
    !fill img.pixels

save_png(filename, img)

Save image to disk. Useful for debugging image conversion.

image_remap(img, colors)

Remap an image according to the given palette. Palette is an array of RGB colors.

image_layout(img, tile_width, tile_height, gap)

Layout an image. Cuts out tiles from left to right, then from top to bottom and creates a new image where all tiles are layed out under each other.

This is the standard way of converting a tilemap or charset so that all tiles are placed after each other in memory and can be used by the target machine.

image_change_depth(img, new_bpp)

Change depth. Legal values are 1,2,4 and 8. No remapping is performed, so colors should match before doing this.

    load_png("c64_logo.png")
    image_remap($, [$000000,$ffffff,$cccccc,$666666])
    img = image_change_depth($, 2)
mc_logo:
    !fill img.pixels

index_tiles(pixels, size)

Used to generate indexes for an image of tiles, and remove all duplicate tiles.

Input is a byte array of pixels, assumed to contain tiles where each tile is size bytes.

Returns { indexes, tiles }

Where indexes contains len(pixels) / size indexes, and tiles contains max(indexes) tiles.

For instance, a blank image would return just one tile, and the index 0 repeated for each tile in the input data, whereas a complex image with no similar tiles would return {0,1,2,3 …​} and the original image.

pixels_layout(pixels, stride, byte_width, byte_height, gap)

"Raw" layout function. Takes a byte array and rearranges it into tiles according to sizes in bytes.

pixels_change_depth(pixels, old_bpp, new_bpp)

"Raw" depth change function, operates on byte arrays.

LUA Functions

These functions can only be called from LUA.

register_meta(name, meta_fn)

Register a new meta command that will call meta_fn(meta) when encountered.

get_meta_fn(name)

Get an existing meta function. Useful when patching existing meta commands to add behavour before or after the actual implementation.

    inc_fn = get_meta_fn("include")
    register_meta("include", function(meta)
        file_name = meta.args[1]
        new_file = find(extra_path, file_name)
        if new_file then
        end
        inc_fn(meta)
    end)

assemble(source)

Assemble the given code at the current position.

reg_a(), reg_x(), reg_y()

Return the contents of a 6502 register.

mem_read(adr)

Read a value from 6502 memory.

mem_write(adr, val)

Write a value to 6502 memory.

set_break_fn(brk, fn)

Set a lua function to be called when a brk #n opcode is executed. Function is called with n as the single argument.

%{
    set_break_fn(5, function(b)
        print("Break executed")
    end)
}%

    brk #5

map_bank_read(hi_adr, len, fn)

If the emulator reads memory between hi_adr<<8 and `hi_adr<<8 + len*256), call the given function.

; Map $f000 - $ffff to funtion that just returns $55
%{
    map_bank_read(0xf0, 16, function(adr)
        return 0x55
    end)
}%

map_bank_write(hi_adr, len, fn)

If the emulator writes memory between hi_adr<<8 and hi_adr<<8 + len*256, call the given function.

map_bank_read(hi_adr, len, bank)

If the emulator reads memory between hi_adr<<8 and hi_adr<<8 + len*256, map the access to the given bank.

A bank is taken as the top byte of a 24-bit address. When this function is called, the list of sections is searched for a start address of bank<<16, and this section is mapped to hi_adr<<8.

    ; Emulate bank switching. Bank is selected by writing
    ; to address $01. Bank is mapped to $a000
%{
    -- Intercept writes to zero page
    map_bank_write(0, 1, function(adr, val)
        -- Always write through
        mem_write(adr, val)
        if adr == 0x01 then
            map_bank_read(0xa0, 1, val)
        end
    end)
}%

    ; Load bank #3 and jsr to it
    lda #3
    sta $01
    jsr $a000

Meta Commands

All meta commands take the same basic form; An exclamation mark followed by an identifier, then an optional set of arguments, followed by zero, one or two blocks enclosed in curly braces.

!section

  1. !section <name>, <start> [,<options>] [ { <statements…​> } ]

  2. !section <name>, in=<parent> [,<options>] [ { <statements…​> } ]

Create a section. All code and data must be placed into a section. The sections are gathered at the end and written to the output file.

If a block is given, it specifies the entire section contents, and the previous section will be restored after.

If no block is given, this section is active until the next section directive.

A root section is a top level section with no parent. It must have a start address.

A leaf section is a section without children. Only leaf sections may contain data.

Root sections are always placed at their start address. Child sections are layed out in order. If a child section has a start address it must be defined in an order that makes sense (sections after will always be layed out after and can not have start addresses that are lower).

  • name : Name of the section. Not required for child sections.

  • start : Start in memory. Only required for root sections.

  • size : Fixed size of section. Normally given for root sections.

  • in : Parent of section. Makes this section a child section.

  • pc : Set the initial program counter. Defaults to start.

  • align : Set alignment (in bytes) of this section

  • file : Set output file of this section. Will remove the section from the main output file.

  • ToFile : Write section to a separate file and don’t include in the main binary.

  • ReadOnly : Flag that marks this section (and all children) as read only. Used for ROM areas.

  • NoStore : Flag that marks this section as not having data in the output file. This is normally used for the zero page, and other bss sections. It can also be used for code that is only used for !test.

Unrecognized options will be passed on to the output module.

  1. Create a root section beginning at address start. If a block is provided, all statements goes into the section, otherwise the section is active until the next section directive.

  2. Create a child section.

Example

    !section "RAM", $0000
    !section "main", in="RAM", start=$801
    !section "data", in="RAM"

    !section "code", in="main"
    !byte $0b,$08,$01,$00,$9e,str(start),$00,$00,$00
start:

!org

!org <start>

Creates an anonymous section beginning at start.

!rept

`!rept [<ivar>=]<count> { <statements...> }`

Repeat a block of statements a specific number of times.

Evaluate statements count times.For each iteration, set the symbol <ivar> to the iteration number.If <ivar> is not provided, it defaults to i.

Example

    !rept 25 { nop }
xor_data:
    secret = [0x35, 0xcf, 0xa9, 0x44]
    data = load("raw.dat")
    !rept len(data) {
        !byte secret[i % len(secret)] ^ data[i]
    }

!fill

  1. !fill <array>

  2. !fill <count> [, <value>]

  3. !fill <count>, <function>

  4. !fill <array>, <function>

Put data into memory, Essentially a shorthand for !rept <arg> { !byte <expression }.

  1. Put array in memory.

  2. Fill memory with contant value, default is zero.

  3. Call a function with values 0 through count-1 and put result in memory

  4. Call a function for every value in array and put result in memory

Example

    !fill 256
    !fill load("data. bin")
    !fill 10, [ i -> sin(i * Math. Pi / 5 ]

!macro

!macro <name>(<args>…​) { <statements…​> }

Create a macro that will insert <statements>.

A macro can be called at a later time, at which point its contents will be inserted into the assembly for parsing.

Note that macros do not form a new scope, so symbols assigned in macros become global. They do however implicitly add a non-local label, so dot-labels defined in the macro will only be visible in the macro.

Example

    !macro WaitLine(line) {
    ; NOTE: local symbol '.wait' is scoped to the macro
.wait   lda $d012
        cmp #line
        bne .wait
    }

!byte

!byte <expression> [,<expression>]…​

Insert bytes into memory.

!word

!word <expression> [,<expression>]…​

Insert 16bit words into memory.

!byte3

!byte3 <expression> [,<expression>]…​

Insert 24bit words into memory. Useful for C64 sprites.

!ds

!ds [<size>]

Declare a section of size bytes

!text

!text <string> [,<string>]

Insert characters into memory.Characters are translated using current translation table.

!encoding

!encoding <name>

Sets the current text translation. Valid values are

  • "ascii"

  • "petscii_upper"

  • "petscii_lower"

  • "screencode_upper" (default)

  • "screencode_lower"

!chartrans

  1. !chartrans <string>, <c0>, <c1>…​ [<string>, <c0>, <c1>…​]

  2. !chartrans

Manual setup of translation of characters coming from !text commands.

  1. Each character from the provided string should be translated to each subsequent number, in order.The number of values should be equal to the number of characters in the string.

  2. Reset translation to default.

!assert

!assert <expression> [,<string>]

Assert that expression is true.Fail compilation otherwise. Asserts are only evaluated in the final pass.

!align

!align <bytes>

Align the Program Counter so it is evenly dividable with bytes. Normal use case is !align 256 to ensure page boundary.

!pc

!pc <address>

Explicitly set the Program Counter to the given address.

!ds

!ds <bytes>

Declare an empty sequence of size bytes.Only increase the Program Counter, will not put any data into the current section.

!enum

!enum [<name>] { <assignments…​> }

Perform all assignments in the block.If name is given, assignments are prefixed with name..

Assignments must take the form symbol = <number> or just symbol, and must be placed on separate lines.

Example

    !enum Monster {
        health,
        movement,
        meta_data = 10
    }

    !assert Monster.movement == 1

!if

  1. !if <expression> { <statements…​> } [ else { <statements…​>} ]

  2. !ifdef <symbol> { <statements…​> } [ else { <statements…​>} ]

  3. !ifndef <symbol> { <statements…​> } [ else { <statements…​>} ]

  4. !elseif <symbol> { <statements…​> }

  5. !else <symbol> { <statements…​> }

Conditional parsing of statements.

!include

!include <filename>

Include another file, relative to this file.

!incbin

!incbin <filename>

Include a binary file, relative to this file.

!script

!script <filename>

Include a script file, relative to this file.

!cpu

!cpu <cpuname>

Set the CPU to use. Valid arguments are only "6502" or "65c02".

!test

  1. !test [<name>] [<registers>]

  2. !test [<name>] <address> [<registers>]

Create a test that will be run inside the built-in emulator after assembly is successfully completed.

You can put values into registers before running the test. Registers take the form: <reg>=<value>, …​

If you need to add code just for the test, you can put it in a NoStore=true section to make sure it is not included in the output file.

  1. Mark the current position (PC) as the start of a test. If name is not given the test statement must be followed by a global label which will be used to name the test.

  2. Create a test that starts at address.

Example

    !test A=9
setup:
    tax
    lda #3
    sta $4000,x
    !check RAM[$4009] == 3
    !rts
    sei
    lda #0
    sta $ffff
    jmp somewhere

    !section "tests", $c000, NoStore=true
    !test "Does music work"
    jsr init_music
    jsr play_music

!rts

!rts

Exit early from test. Useful for testing part of routine.

!log

  • !log <text>

Runtime log function during tests.Registers are available as special arguments between braces ({A}, {X}, {Y} etc).

Example

    !test
    tax
    !log "We are here, X={X}"

!check

  • !check <expression>

This is similar to assert, except it happens runtime during the execution of tests. Symbols A, X, Y, SP, SR and RAM[] are available in expressions.

Example

    !test
    lda #2
    sec
    rol a
    !check A == 5
    sta $1000
    !check RAM[$1000] == 5

!run

  • !run {: <lua code> :}

Run lua code during tests. This can be used for more advanced checks and logging.

!print

  1. !print <value> [,<value> …​]

Print values during assembly.

Detailed information

Labels

Labels either end with ':' or have to start at the first column.

A label starting with a dot ('.') is a local label and treated as lastLabel.label, where lastLabel is the closest previous non local label.

Macros defined an implict lastLabel to support scoped local labels. So does Rept-blocks, but maybe the shouldn’t.

A label can be also called '$'.

Referencing '+' will mean the closest following '$' label. Referencing '-' will mean the closest previous '$' label.

Repeated '-' or '+' signs means to skip further.

Symbol lookup

A function call is first looked up among internal functions, then any functions defined in LUA.

Opcodes that are not recognized are also tried as a macro, if one exists.

AST Cache

The parser works by first creating an AST (tree representation) of the source, and then using this representation to do all work.

AST:s are cached on disk, so the second time you assemble a file that has not been changed, assembling will be much faster.

AST:s are saved in $HOME/.basscache

Basic Operation in Detail

The source is parsed top to bottom.Included files are inserted at the point of inclusion.

Symbols are defined through labels, symbol assignments or indirectly through meta commands such as "!section" or "!test".

If an expression references an undefined symbol, it is assumed to be zero and flagged as "undefined".

If an existing symbol gets assigned a new value, it is also flagged as "undefined", since we need to make another pass to make sure all its previous references are resolved to the correct value. This happens when code size changes, as subsequent labels are moved to new locations.

When all source code is parsed, the undefined list is checked;

  • If it is empty we are done.

  • If all entries now also exist in the symbol table, we clear the undefined list and make another pass.

  • Otherwise, we have an error, and report the undefined symbols.

A branch instruction to an undefined or incorrect label may temporarily be too long. This error must be postponed until all parsing is done.

Macros use normal symbols as arguments. These symbols are only set during macro evaluation. If a macro argument "shadows" a global symbol a warning is issued.

Macros affect the symbol table, so you can set symbols from macros. If you don’t want to pollute the symbol table, used "."-symbols, they will be local to the macro.

The symbol table supports the following types:

  • Number (double)

  • String (std::string_view)

  • Byte Array (std::vector<uint8_t>)

  • Number Array (std::vector<double>)

  • Objects (std::unordered_map<std::string, std::any>)

  • Lambdas (Macro)

The pet-100 specification

Intro

The pet-100 is an emulator for a "fantasy" text mode 6502 based system.

It was developed as a part of bass, a 6502 assembler with a built in emulator.

The emulator was initially added to support unit tests only, but the functionality was extended so it could run complete programs in a text only (terminal) environment.

Overview

The pet-100 defines only a few set of registers, and special memory. It tries to be compatible with the C64 so that it’s easy to write code that can target both machines.

The machine can write colored text to the screen, and read key events. It also has a simple timer that can cause interrupts if necessary.

The text is written as Petscii (upper case screencode by default) and translated into unicode. Terminals with modern fonts should be able to display all characters in the petscii character set.

At startup, a 40x25 character window is mapped to address $400, and it’s corresponding colors is mapped to $d800. This is the same as the C64 memory map. The window is located at the center of the current PC terminal, but can then be moved and resized, and the location in memory can also be changed.

A single register is used for reading keys. The register returns the next character in the keyboard input buffer. If the buffer is empty, zero is returned.

The timer counts up every TimerDiv micro seconds. The timer is frozen when reading the higher bits (it keeps counting but the register is not updated) in order to read the multi byte value consistently.

The Screen Update IRQ is signalled after the text buffer has been copied to the terminal. It can be used as a "Vblank", you can wait for it and then start rendering.

Default Memory Map

Offset Name Description

$0400

Text RAM

Character grid, one byte per character

$d700

IO Registers

See below

$d780

Palette

32 colors, with 3 bytes (RGB) each. First 16 colors is foreground, second half is background.

Changing the palette does not change the colors of characters already on screen. This can be used to have more than 16 colors on screen at the same time.

$d800

Color RAM

Color indices for corresponding character; low 4 bits is foreground, high 4 bits is background.

Indices are looked up in corresponding palette when character is flushed to terminal.

$fffc

IRQ Vector

Jumped to if IRQ is enabled and timer reaches zero, or key is pressed

IO Registers

Offset Name Access Default Description

$00

WinX

RW

0

Current window X position

$01

WinY

RW

0

Current window Y position

$02

WinW

RW

40

Current window width

$03

WinH

RW

25

Current window height

$04

RealW

R

Actual console width

$05

RealH

R

Actual console height

$06

TextPtr

RW

$04

High address of current text window

$07

ColorPtr

RW

$d8

High address of current color window

$08

Border

W

Fill area around current window with color

$09

Keys

R

Returns the next read character from the keyboard. 0 means no keys available.

$0A

Control

W

Writing specific bits causes certain effects:

  • 0: Exit : Writing 1 will cause the emulator to exit

  • 1: Stop : Writing 1 causes the emulator to sleep until an IRQ occurs

$0B

Charset

RW

0

Set the charset to use for translation:

  • 0: Screencode upper

  • 1: Screencode lower (TBD)

  • 2: Petscii upper (TBD)

  • 3: Petscii lower (TBD)

  • 4: Ascii (TBD)

  • 5: Extended Petscii (TBD)

$0C

TimerLo

RW

1

Low 8 bits of timer. Reading this register will cause the timer to unfreeze.

$0D

TimerMid

RW

1

Middle 8 bits of timer. Reading this register will cause the timer to freeze.

$0E

TimerHi

RW

1

High 8 bits of timer. Reading this register will cause the timer to freeze.

$0F

TimerDiv

RW

1

Timer divider. The microsecond clock is divided by this value and written to the timer registers.

$10

IrqEna

RW

Interrupt enable

  • 0: Screen update enable

  • 1: Key IRQ enable

These bits indicate whether and interrupt should cause the CPU to jump to $FFFC.

$11

IrqReq

RW

Interrupt Request

  • 0 : Screen update occurred

  • 1 : Key IRQ occurred

Writing a 1 clears the corresponding IRQ

    ; Frame loop
loop:
$   lda #2       ; Both Screen update & sleep bit
    sta Control  ; Go to sleep
    and IrqS     ; Check if screen woke us
    beq -
    sta IrqS     ; Clear IRQ
    jsr render
    jmp loop