the basics of fox32

- alex winston, august 1st, 2026

hello! this fox32 tutorial series will mainly focus on the bare metal, meaning usage of the OS will be quite limited... instead, this tutorial will mostly focus on using the ROM, accessing hardware directly, etc... the tutorial will be using the fox32asm assembler syntax. for this tutorial, instruction encoding will not be covered in detail.

this tutorial assumes you already understand binary, hexadecimal, how bitwise operations work, etc... if you don't, perhaps it's good for you to read up on how those work before reading this.

table of contents

the processor

basic stuff

the processor has 32 32-bit general-purpose registers named r0 through r31, alongside a stack pointer rsp, an exception stack pointer resp, a frame pointer rfp, and an instruction pointer rip that points to the current instruction being executed. the processor is little-endian, and memory can be accessed in byte size (8-bit, 0..255/0xFF), half size (16-bit, 0..65535/0xFFFF), and word size (32-bit, 0..4294967295/0xFFFFFFFF).

nearly all instructions can take a size, suffixed with .8, .16, or .32. if left unspecified, the assembler will default to .32 size. some opcodes only take a .32 size, and therefore other sizes cannot be used with those opcodes.

instructions are 2 bytes minimum when assembled, and are formatted opcode<.8/.16/.32> <target>, <source>, where target and source can be one of 4 addressing modes:

additionally, if the operand is a register pointer, it can optionally be followed by an unsigned 8-bit offset: [r0+69], [r0+255]

back to table of contents

mov, movz

the mov (MOVe) instruction is perhaps the simplest instruction to understand. it simply moves the source operand into the target operand:

    mov r0, 1337

the above piece of code simply moves the value 1337 into register r0. of course, as mentioned before, this actually assembles to mov.32 r0, 1337 because the size was left unspecified. the result is that this instruction will assemble into 7 bytes. this is usually not a concern, as we have 64MB of memory at our disposal, but this may not be very space-efficient, especially if you engage in sizecoding activities ;). the value 1337 can fit into a half (16 bits), thus, we can rewrite it as such:

    mov.16 r0, 1337

this will assemble into 5 bytes, 2 bytes less than our previous example. the saving may seem small, but these savings add up and may potentially give you space to do other things, which is important if you're writing a 512-byte program in the boot sector. you can also do the same thing with 8-bit values:

    mov.8 r0, 8 ; this will only assemble to 4 bytes

it is important to note that mov moves without clearing any data that was there prior. in particular, mov.8 and mov.16 will move the operand into lower half of the register if a register is specified as the target. in other words:

this behaviour may not be desirable, especially if you are utilising the above techniques to reduce code size. to this end, there is the movz (MOVe Zero-extended) instruction. this instruction is the same as mov, but has the behaviour of clearing the upper bits (or in other words, the bits we don't care about) to 0:

and the same for movz.16:

back to table of contents

add, sub, inc, dec

before reading this, it is important to know that the processor has 2 internal flags (which you cannot set manually under normal circumstances): zero flag, and carry flag. zero flag is set when a math or bitwise operation results in a value of 0. the carry flag, however, is set when an arithmetic operation results in an underflow/overflow. it is a bit of a misnomer, because the carry flag acts more like a borrow flag when doing subtraction, and it is not affected by any bitwise instruction. however as all official documentation refers to it as a carry flag, it will be referred to as the carry flag for simplicity.

the add instruction will simply add the number from the source operand into the destination operand; the source operand will remain unchanged (arithmetic operations here are in hexadecimal):

the above operation can be simplified into add r0, 0x80, freeing up a register. this (and the above information) applies to sub (SUBtract), only with subtraction rather than addition.

when using arithmetic instructions, it is important to remember that the size will affect your results (i.e it will only operate on that many bits). for example:

if the resulting value is too large to fit in a 32-bit register (or too large to fit in the operation size), it will either overflow or underflow. in both cases, the carry flag will be set.

the inc (INCrement) and dec (DECrement) are specialised cases of add and sub, respectively, but their source operand is fixed to an immediate type, and only allows values of 1, 2, 4, and 8.

why would we need this? we can just write add r0, 1 and be done with it. we can of course write it that way, but the difference is in size: add r0, 8 (with the default .32 operation size) assembles into 7 bytes (smaller operation sizes mean smaller assembly, but for reasons mentioned above, may have an undesirable effect on math operations). meanwhile the equivalent inc r0, 8 only assembles into 3 bytes, a ~42.8% decrease in size, and is massively helpful in saving space.

back to table of contents

bse, bcl, not, and, or, xor

these instructions are bitwise instructions, and require a little understanding about how boolean algebra works. the simplest instruction is bse (Bit SEt) and bcl (Bit CLear), which set and clear a bit in the target operand, with the bit to set/clear specified by the source operand:

it should be noted that immediate types for the bse and bcl instructions are always 8 bits, and will always assemble to a fixed size of 4 bytes at minimum no matter the operation size (bse.8, bse.16, bse.32 will assemble to 4 bytes at minimum). the only effect then that the operation size has on bse and bcl is that immediate values larger than the size are ineffective (bse.8 r0, 16 will not set bit 16 of r0, for example).

bse, bcl, as well as bts count bits from the lowest bit:

    0101 0101
    |       ^--- bit 0
    +----------- bit 7

the not, which performs a bitwise NOT on the target operand, which just inverts every bit in the target operand (0 -> 1 ; 1 -> 0):

    1100 1010 (0xCA)
    |||| |||| 
    VVVV VVVV (0 -> 1 ; 1 -> 0)
    0011 0101 (0x35)

as always, the not instruction is affected by the operation size, so not.8 will only perform the bitwise NOT on the lower 8 bits of the operand, and not.16 only on the lower 16 bits. this also applies to every instruction in this section. it will not work on immediates, because the resulting syntax not.8 0x80 is nonsensical.

the and instruction performs a bitwise AND on the target operand with the source operand. in other words, it takes 2 bits in, and outputs a 0 if one of the inputs is 0:

    1111 0101 (0xF5)
AND 0000 1111 (0x0F)
    ---------
    0000 0101 (0x05)

the or instruction performs a bitwise OR on the target operand with the source operand. in other words, it takes 2 bits in, and outputs a 1 if any of the inputs is 1:

    1111 0001 (0xF1)
OR  0000 1111 (0x0F)
    ---------
    1111 1111 (0xFF)

the xor instruction performs a bitwise XOR (eXclusive OR) on the target operand with the source operand. in other words, it takes 2 bits in, and outputs a 1 if ONLY one of the inputs is 1 - the difference between XOR and OR is that if both inputs are 1, XOR outputs a 0:

    1111 0001 (0xF1)
XOR 1010 1111 (0xAF)
    ---------
    0101 1110 (0x5E)

back to table of contents

sla, sra, srl, rol, ror

there are 3 bit-shifting instructions: sla (Shift Left Arithmetic), sra (Shift Right Arithmetic) and srl (Shift Right Logical). just like bse and bcl, immediate values are fixed to 8 bits. they are basically what it says on the tin: they shift bits in the target operand by the value in the source operand. take this example of sla, that shifts the target operand left by the value in the source operand:

    +----------- this bit is discarded
    |
    1011 0011 
    //// ///
    |||| |||
    0110 0110 <- shift a 0 into here

the sra and srl instructions are mostly the same, they shift the bits in the target operand right by the value in the source operand. the difference is sra is an arithmetic left shift, meaning the sign bit will be duplicated into the bit next to it:

compare this to srl:

(srl)
    1011 0011 <- this bit is discarded
    \\\\ \\\
     |||\|||
    0101 1001
    ^----------- shift a 0 into here
(sra)
    1011 0011 <- this bit is discarded
    |\\\ \\\
    ||||\|||
    1101 1001
    ^^---------- contents of sign bit shifted in

the rol (ROtate Left) and ror (ROtate Right) instructions are functionally similar to the sla and sra/srl instruction, but the discarded bit is shifted into the upper/lower bit:

    0011 0011--+
     || \|||   |
     \\\ \\\   |
    1001 1001  |
    ^----------+
    +----------+
    |          |
    1011 0011  |
     ||| ||||  |
     |||/ |||  |
    //// ///   |
    |||| |||   |
    0110 0111<-+

back to table of contents

mul/imul, div/idiv, rem/irem

the mul (MULtiply) and imul (Integer MULtiply) instructions are used to... well, multiply. the difference is that mul is an unsigned multiplication and imul is a signed multiplication. this has no noticeable effect on the final result, but affects how the carry flag is set.

for mul (unsigned):

compare this to imul (signed):

the div (DIVide) and idiv (Integer DIVide) instructions are also used to divide unsigned values and signed value, respectively. however, unlike mul/imul, the sign of the operation affects the final result. this also applies to rem (REMainder) and irem (Integer REMainder). unlike the rest of the math operations available, div/idiv and rem/irem do not affect the carry flag, therefore they only affect the zero flag.

for div (unsigned):

for idiv (signed):

wait! why did it return a 0? -1 / 10 should be a -0.1! this is because div/idiv throws away the fractional part, leaving only the integer part. this is important behaviour to remember, as there is no floating point support on fox32!

the rem/irem instructions are relatively simpler, as they always return an integer with no fractional part, so their result only depends on the sign of the operation.

for rem (unsigned):

for irem (signed):

note that for div/idiv and rem/irem, they will cause a division by zero exception if the divisor operand is 0!

    mov r1, 20
    div r1, 0  ; this will cause a division by zero exception

back to table of contents

the stack, push, pop

the stack pointer is located in the register rsp, and grows downward as values are pushed. push will push a new value onto the stack and decrement rsp. pop will pop a value off the stack and increment rsp. the amount that rsp is incremented or decremented by depends on operation size.

note: rsp does not enforce any alignment! therefore, rsp can be unaligned and values can still be pushed and popped normally.

    push.8  69           ; push 1 byte onto the stack
    push.16 1616         ; push 2 bytes onto the stack
    push.32 76768        ; push 4 bytes onto the stack
    ; rsp at this point is rsp+7
    pop.32  r0           ; pop 76768 into r0
    pop.16  r1           ; pop 1616 into r1
    pop.8   r0           ; pop 69 into r0

another behaviour to consider is that pop will not zero-extend, therefore any residual data in wherever you popped to will remain:

back to table of contents

jmp/rjmp, loop/rloop, call/rcall, ret, rta

these instructions affect program flow, and apart from ret, they can take an absolute operand (jmp, loop, call) or they can take a relative operand that points to the instruction to jump to, indicated by an r for Relative (rjmp, rloop, rcall). relative instructions can also take an operation size (.8/.16/.32), while absolute instructions can only take a 32-bit size.

jmp (JuMP) loads the source operand into rip, the instruction pointer:

rjmp (Relative JuMP) adds the source operand into rip:

loop decrements r31, and if it is not zero, it loads the source operand into rip (the relative equivalent rloop adds the source operand to rip instead). otherwise, the loop is terminated and execution continues to the next instruction. it is quite useful in making quick and easy loops:

    movz.16 r31, 16384 ; repeat 16384 times
_0:
    ; do something here
    loop    _0

the loop above will repeat 16384 times. internally, it decrements r31 first before it does the zero-check, and it will do this until the register is 0, at which point the loop terminates. therefore, it will count from 16383 down to 1.

call (and its relative counterpart rcall) is used for making subroutines and functions. when a call is encountered, the processor pushes the current rip to the stack as a 32-bit value, and loads (or adds for rcall) the source operand into rip. the old rip can be restored after the function is done with ret, which pops the last 32-bit value on the stack into rip.

    call subroutine
    ; do some stuff here
    ; ...
subroutine:
    ; do some more stuff here
    ret

you should take note to save the registers if you want to preserve all registers as-is by the time the subroutine has finished using push and pop.

rta (Relative To Absolute), while it does not affect program flow, is still important because it allows for true position independent code in conjunction with rjmp, rloop, and rcall. it takes a relative offset specified in the source operand, adds it into rip, and loads the final result back into the target operand. take this piece of code for example:

    org 0x800
    
    rta r0, some_data ; this becomes: rta r0, 0x0C
    nop
    nop
    nop
    nop
some_data: data.8 0

back to table of contents

condition codes, cmp, icmp, bts

the fox32 architecture does not utilise separate conditional instructions unlike other architectures. it instead encodes the condition directly into the instruction, meaning instructions can be run predicatively, instead of jumping to another block of code to run the instruction. there are 6 condition codes (7 if you include always):

this means that entire blocks of code can be written predicatively:

    cmp r1, 20
    iflteq ret

you should be careful when using arithmetic/bitwise operations, as those do modify the flags, meaning the original condition may be clobbered:

    mov r1, 21
    cmp r1, 20      ; r0 != 20, so zero flag is CLEARED
                    ; the next instruction will execute because zero flag is 0
    ifnz sub r1, 21 ; this will SET the zero flag, because 21 - 21 = 0
    ifnz ret        ; thus, this ret will not execute, because the zero flag was clobbered by the instruction above it

the cmp (CoMPare) instruction compares the target operand with the source operand and sets the carry flag and zero flag accordingly. internally, it determines how to set the carry and zero flag like this:

carry = (source_operand > target_operand);
zero = (target_operand == source_operand);

it can be thought of doing a subtraction operation, only that the result is not written back, it only affects the carry and zero flags.

icmp (Integer CoMPare) is just a signed version of the cmp instruction, which affects how the flags are set.

for cmp (unsigned):

for icmp (signed):

bts (Bit TeSt) will test a specific bit (specified by the source operand) in the target operand. what this actually means is that it just loads an inverted copy of that bit into the zero flag:

back to table of contents

interrupts, exceptions, reti

interrupts are mostly used for hardware events that are asynchronous (like VSYNC). they are either triggered by the hardware, or triggered by the int instruction. exceptions, on the other hand, are only triggered by the processor to indicate errors like division by zero, invalid opcodes, etc... they can also be triggered by the debugger, called with brk. additionally, the processor can use the halt instruction to halt further execution until an interrupt is triggered.

the interrupt vectors are located at the start of memory, from 0x000 to 0x3FC, where each vector is a 32-bit little-endian address to the service handler. this equates to 256 usable interrupt vectors. in addition, there are 6 exception vectors:

vectoraddressexception type
0x000x400division by zero
0x010x404invalid opcode
0x020x408page fault (read)
0x030x40Cpage fault (write)
0x040x410debugger breakpoint
0x050x414bus error

most interrupts are unused, so the architecture allocates the first 240 vectors (0x00..0xEF, vectors 0x000..0x3BC) for software use. the remaining 16 vectors (0xF0..0xFF, vectors 0x3C0..0x3FC) are allocated for hardware interrupts, although only 7 are actually used:

vectoraddressinterrupt type
0xF80x3E0timer A interrupt
0xF90x3E4timer B interrupt
0xFA0x3E8timer C interrupt
0xFB0x3ECtimer D interrupt
0xFC0x3F0(unused)
0xFD0x3F4audio buffer 0 refill
0xFE0x3F8audio buffer 1 refill
0xFF0x3F4VSYNC interrupt (60hz)

when dealing with interrupts/exceptions, there are 2 flags that affect how the processor handles with them:

the interrupt flag can be set with ise (Interrupt SEt) and cleared with icl (Interrupt CLear). the swap sp flag does not have any specialised instructions for setting and clearing, therefore it must be set via an interrupt:

    mov [0x40], set_swap_sp ; vector 0x10
    int 0x10
set_swap_sp:
    add rsp, 4 
    bse [rsp], 3 ; enable swap sp
    reti

when writing an interrupt handler, it is important to remember what the processor does when it encounters an interrupt or an exception:

if an exception was raised, then an exception operand is pushed onto the stack. the value of the exception operand is 0 by default, but it will depend on what kind of exception was raised:

exception typeexception operand
division by zero0
invalid opcode0
page fault (r/w)the memory address where the read/write failed
debugger breakpoint0
bus error0

with the information above, the stack will look like this at the beginning of interrupt handling:

+---------+-----------+-----------------------------------+
|  rsp+0  |  4 bytes  | vector number / exception operand |
+---------+-----------+-----------------------------------+
|  rsp+4  |  1 byte   | processor flags                   |
+---------+-----------+-----------------------------------+
|  rsp+5  |  4 bytes  | saved return instruction pointer  |
+---------+-----------+-----------------------------------+
|  rsp+9  |  4 bytes  | saved user stack pointer          |
+---------+-----------+-----------------------------------+

when returning from an interrupt, the processor will:

notice anything about the return process? it does not pop the vector number off the stack! therefore, reti will pop the old state at the wrong place in the stack, causing stack corruption that will inevitably crash the machine. therefore, all interrupt handlers MUST correct the stack before returning:

interrupt_handler
    ; do interrupt handling here
    add rsp, 4 ; correct the stack
    reti

the position of add rsp, 4 does not really matter here, as long as it corrects the stack pointer before it returns.

when disabling interrupts with icl, the processor still processes interrupts, but does not actually jump to the handler until interrupts are enabled. during this time, interrupts are deferred and queued up in a pending state, waiting to be handled when interrupts are enabled again with ise. when interrupts are re-enabled, all deferred interrupts will be processed one after another, with the interrupt with the lowest vector number being processed first. duplicate interrupts are only processed once, so if a vector is called multiple times, it only has to be handled once.

back to table of contents

memory management unit, tlb, flp

the processor has an optional memory management unit (MMU) that can be enabled if the operating system wishes to use it. when the MMU is enabled, all memory addresses accessed by the process are treated as virtual addresses. the processor has 2 internal registers for the MMU:

the 32-bit virtual address is formatted as:

bit 31........ .......... ...........0
    |                                |
    DDDDDDDDDD TTTTTTTTTT AAAAAAAAAAAA
    |||||||||| |||||||||| ^^^^^^^^^^^^---- lowest 12 bits of physical address
    |||||||||| ``````````----------------- page table index (10 bits)
    ``````````---------------------------- page directory index (10 bits)

to translate a virtual address into a physical address, the MMU uses 2-level page tables: the page directory, and the page table. the page directory is the first level and is a 4-kilobyte block in RAM that contains 1024 32-bit entries that point to a page table. the page table is the second level and is a 4-kilobyte block in RAM that contains 1024 32-bit entries that point to an physical address. both the page directory and page table use the same bitfield:

bit 31.................. ...........0
    |                               |
    AAAAAAAAAAAAAAAAAAAA ----------WP
    ||||||||||||||||||||           |^----- present bit (is the page or the page table is present?)
    ||||||||||||||||||||           `------ writable bit (is the page writable?)
    ````````````````````------------------ address of page table or page (12 bits)

the process of translating a virtual address into a physical one is a multi-step process, called a page table walk:

(the information below may not necessarily be useful to programmers, but may be useful for emulator developers)

accessing memory to perform a page table walk requires at least 2 memory accesses, which can add up over time. to forestall this issue, the processor has an internal Translation Lookaside Buffer (TLB) that contains 64 entries that are filled with the final results of the virtual to physical address translation to prevent the MMU from having to retranslate on every memory access. the upper 20 bits (the page directory and table index) are used to look for a matching entry. if there is a matching entry and it is present, it returns that pre-translated physical address. otherwise, it walks the page table to get a new physical address to store in the TLB and use for future memory access. if the TLB is full, the MMU will look for an unused entry, and if none exists, it evicts entries in a simple round-robin order.

the TLB can go stale if a table entry is changed, because the TLB isn't aware of the change. thus, flp (FLush Page) can be used to flush a single page entry in the TLB, making the next access to that page walk the page table again and insert a new translation into the TLB. it is quite useful when only one page's mapping has been modified (unmapped, remapped, etc...) since it avoids throwing away the rest of the TLB's entries unnecessarily. likewise, tlb flushes the entire TLB at once, and sets the page directory pointer register to a new value, which is mandatory when switching to a different page directory, since none of the TLB entries at that moment are valid for the new address space.

back to table of contents

i/o bus, in, out

in addition to the memory bus, the processor can access another bus, called the i/o bus. hardware peripherals are mapped onto this bus, which are exposed as i/o ports that can be read from or written to. unlike memory, every access is 32 bits, and there is no byte-addressing on the i/o bus. that means unlike memory-mapped i/o, ports don't have to be separated into 4 bytes for a 32-bit value, and can just be considered as one singular value. there are many things mapped onto the i/o bus that will be covered more in detail in the next sections:

i/o portdescription
0x00000000serial port
0x80000000-0x8000031Fgraphics system
0x80000400-0x80000401mouse input
0x80000500keyboard input
0x80000600-0x800006FFaudio system
0x80000700-0x8000070Freal-time clock & programmable interval timers
0x80001000-0x80005003disk controller
0x80010000power controller (write 0 to power off)

out writes a 32-bit value onto the i/o bus at the port specified in the target operand with the value in the source operand. in reads a 32-bit value from the i/o bus at the port specified in the source operand into the target operand.

back to table of contents

the hardware

all port values mentioned here are truncated versions: port 0x100 refers to port 0x80000100, and port 0x1000 refers to port 0x80001000!

the graphics

the system uses a 640x480 screen and provides an RGBA8888 (that is to say: 8 bits for Red, Green, Blue, and Alpha) framebuffer fixed in memory at address 0x02000000-0x0212BFFF (1228800 bytes -> 307200 pixels). the memory here is directly readable and writable like normal RAM, but the location of the framebuffer cannot be relocated in any way. it should be noted that the alpha channel, although 8 bits have been allocated for it, is essentially a binary value, where a zero alpha value causes the pixel to be completely transparent, and a non-zero value to be completely opaque. this framebuffer is called the "background", because alongside it are 32 "overlays", which can be very flexible unlike the background. they are essentially hardware sprites, and can be used to generate windows, cursors, etc...

the overlays are composited onto the background, with overlay 31 being given the highest priority, and overlay 0 the lowest priority. unlike the background, they can have their own X and Y position, set with port 0x0NN (NN=00..1F). the upper 16 bits of the port determine the Y position, and the lower 16 bits determining the X position. they can also have their own size, set with port 0x1NN, where the upper 16 bits determine the height, and the lower 16 bits the width. the overlays also have their own framebuffer in memory (whose size is variable depending on the height and width), set with port 0x2NN. port 0x3NN determines if the overlay is composited onto the screen, with a zero value meaning no display, and a non-zero value meaning it is displayed.

overlay port mappings (NN=00..1F)

portdescription
0x0NNoverlay position (bits 31:16 - y pos, 15:0 - x pos)
0x1NNoverlay size (bits 31:16 - height, 15:0 - width)
0x2NNoverlay framebuffer address
0x3NNoverlay enable (0 - disabled, non-zero - enabled)

back to table of contents

the input methods

there are 2 input methods available: the mouse, and the keyboard. because they occupy so little i/o space, and have basically no quirks, they are merged into this one section. the mouse is accessed with ports 0x400 (mouse status) and 0x401 (mouse position). the computer does not make a distinction between the left and right mouse buttons.

port 0x400 (mouse status)

bitdescription
2button held
1button release
0button press

port 0x401 (mouse position)

bitdescription
31:16Y position
15:0X position

note that the positions above are absolute rather than delta-relative, meaning the Y position ranges from 0 to 479, and X from 0 to 639.

the keyboard only has one port at 0x500. when read, the keyboard returns a key event from the key event queue. bit 7 of this value determines if it was pressed (0) or released (1). the remaining 7 bits are used for the scancode. if you are using the keyboard, do not use the F11 key for any function! that key in the emulator is mapped to debug mode, where it prints out every instruction being executed to console. this will freeze your emulator!

scancode mapping

keyscancode
Escape0x01 / 1
10x02 / 2
20x03 / 3
30x04 / 4
40x05 / 5
50x06 / 6
60x07 / 7
70x08 / 8
80x09 / 9
90x0A / 10
00x0B / 11
Minus (-)0x0C / 12
Equals (=)0x0D / 13
Backspace0x0E / 14
Tab0x0F / 15
Q0x10 / 16
W0x11 / 17
E0x12 / 18
R0x13 / 19
T0x14 / 20
Y0x15 / 21
U0x16 / 22
I0x17 / 23
O0x18 / 24
P0x19 / 25
Left Bracket ([)0x1A / 26
Right Bracket (])0x1B / 27
Enter0x1C / 28
Left Control0x1D / 29
A0x1E / 30
S0x1F / 31
D0x20 / 32
F0x21 / 33
G0x22 / 34
H0x23 / 35
J0x24 / 36
K0x25 / 37
L0x26 / 38
Semicolon (;)0x27 / 39
Single Quote (')0x28 / 40
Backtick (`)0x29 / 41
Left Shift0x2A / 42
Backslash (\)0x2B / 43
Z0x2C / 44
X0x2D / 45
C0x2E / 46
V0x2F / 47
B0x30 / 48
N0x31 / 49
M0x32 / 50
Comma (,)0x33 / 51
Period (.)0x34 / 52
Forward Slash (/)0x35 / 53
Right Shift0x36 / 54
Keypad *0x37 / 55
Left Alt0x38 / 56
Spacebar0x39 / 57
Caps Lock0x3A / 58
F10x3B / 59
F20x3C / 60
F30x3D / 61
F40x3E / 62
F50x3F / 63
F60x40 / 64
F70x41 / 65
F80x42 / 66
F90x43 / 67
F100x44 / 68
Num Lock0x45 / 69
Scroll Lock0x46 / 70
Keypad 70x47 / 71
Keypad 80x48 / 72
Keypad 90x49 / 73
Keypad Minus (-)0x4A / 74
Keypad 40x4B / 75
Keypad 50x4C / 76
Keypad 60x4D / 77
Keypad Plus (+)0x4E / 78
Keypad 10x4F / 79
Keypad 20x50 / 80
Keypad 30x51 / 81
Keypad 00x52 / 82
Keypad Period (.)0x53 / 83
F110x57 / 87
F120x58 / 88
Up Arrow0x67 / 103
Left Arrow0x69 / 105
Right Arrow0x6A / 106
Down Arrow0x6C / 108

back to table of contents

the audio

the audio controller manages 8 PCM audio channels, summing them together to produce the final audio output. it has a maximum output rate of 48 kHz and has 2 modes: DMA (Direct Memory Access) mode, and buffer mode. in DMA mode, you can use 8 channels of PCM, and the audio controller will automatically fetch the samples when needed, freeing up the processor to do other tasks. in buffer mode, those channels are unavailable, and audio data must instead be provided by the processor at regular intervals. in both modes, the audio controller can fetch and output signed 8-bit or signed 16-bit PCM samples.

registers for the 8 audio channels are accessed at 0x600-0x67F. the upper nibble determines the channel number, while the lower nibble determines the register. general audio controller registers start at 0x680 and continue to 0x684, with ports 0x685..0x6FF reserved for future use.

PCM channel port mappings (N=0..7)

portdescription
0x6N0audio channel X sample start (write), current sample position (read)
0x6N1audio channel X sample end (write), current output sample (read)
0x6N2audio channel X loop point start (write-only)
0x6N3audio channel X loop point end (write-only)
0x6N4audio channel X sample rate (write-only), current channel phase (read)
0x6N5audio channel X control
0x6N6audio channel X panning

general audio controller port mappings

portdescription
0x680audio controller sample base
0x681audio controller control
0x682audio controller buffer 0 start location
0x683audio controller buffer 1 start location
0x684audio controller buffer length

port 0x681 (audio controller control)

bit 15...... .......0
    |               |
    RRRRRRRR --FF-10M
    ||||||||   || ||`--- mode (0=DMA mode, 1=buffer mode)
    ||||||||   || |`---- buffer 0 refill pending
    ||||||||   || `----- buffer 1 refill pending
    ||||||||   ``------- buffer format (00 - mono 8-bit, 01 - mono 16-bit, 10 - stereo 8-bit, 11 - stereo 16-bit)
    ````````------------ buffer rate (0..128)

dma mode

sample start points and end points must be specified in bytes relative to the sample base (set in port 0x680). this means if a sample is 8000 samples long and is located at the very beginning of sample base (and uses 8-bit PCM), the sample start point should be set to 0, and the sample end point to 8000. if it's 16-bit PCM, the end point is at byte 16000. this also applies to the start points and end points for looping.

samples are fetched at the sample rate specified in port 0x6N4, which is a value ranging from 0-65536, giving an effective frequency range of 0hz-48khz. samples in this mode can also be only mono, and 16-bit samples are fetched in little-endian halves. the sample rate is calculated with the formula:

              frequency * 2^16
sample rate = ----------------
                   48000

the sample playback behaviour is determined in the control port (0x6N5). 9 bits are used to determine playback:

bit 9. .......0
    |         |
    PE LVVVVVVV
    || |```````--- volume (0..127, where 0 is silent and 127 is loudest)
    || `---------- loop (0=one-shot, 1=loop after reaching the end)
    |`------------ enable (0=silence the channel, 1=reset the sample position)
    `------------- pcm data (0=8-bit PCM, 1=16-bit PCM)

bit 8 (the enable bit) is a strobe register, meaning any writes that write a 1 to this bit will reset the current sample position, and any writes that write a 0 will silence the channel! therefore, it is not recommended to use this register to control the volume, and instead use the panning registers to control the volume.

the panning registers are a 16-bit port, where the upper 8 bits are the left channel volume, and the lower 8 bits are for the right channel.

bit 15...... .......0
    |               |
    LLLLLLLL RRRRRRRR
    |||||||| ````````--- right channel volume (0..255, where 0 is silent and 255 is loudest)
    ````````------------ left channel volume

the samples are modulated using the volumes in the order: control volume -> left/right volume. therefore, it is usually better to leave the channel at maximum control volume, then modulate the volume with the panning register.

it is important to note that when reading the current sample output via port 0x6N1, the sample that is returned is actually the raw sample data that hasn't been modulated by the volume registers. therefore, if you are planning to plot it on an oscilloscope, you must scale the output manually in software. another peculiarity is that when reading 8-bit PCM samples, the maximum output that can be returned is 0x7f00. internally, the audio controller always outputs 16-bit PCM, but to facilitate 8-bit PCM output, all 8-bit samples are shifted up by 8 bits to get a 16-bit output.

(the information below may not necessarily be useful to programmers, but may be useful for emulator developers)

internally, the audio controller uses a 16.16 fixedpoint phase accumulator to determine when to fetch samples in DMA mode. the behaviour of this accumulator can be best described with the following pseudocode:

Phase := 0
WHILE ChannelEnabled DO
    Phase += SampleRate
    IF Phase >= 65536 THEN
        Phase -= 65536
        Sample := FetchNewSample()
        OutputToChannel(Sample)
    END
END

buffer mode

buffer mode is enabled when bit 0 of the control register (port 0x681) is set to 1. in this mode, samples are no longer automatically fetched according to a set start and endpoint (this also makes the 8 PCM channels unusable). instead, the audio controller uses 2 buffers (buffer 0 and buffer 1) to output samples. when the controller starts reading samples, it will read buffer 0 first. when it has reached the end of buffer 0, it will interrupt to the CPU to refill buffer 0 (vector 0xFD), while it starts reading buffer 1. when it reaches the end of buffer 1, it will also interrupt the CPU to refill buffer 1 (vector 0xFE), and restarts reading from buffer 0. the start address for buffer 0 is determined by port 0x682, and 0x683 for buffer 1. the length of these buffers (in bytes) is determined by port 0x684.

stereo samples are fetched as frames containing left and right samples. for instance, if the buffer is set to stereo 8-bit PCM, it fetches the left sample first (1 byte), then the right sample last (1 byte), advancing the internal position by 2 bytes. for stereo 16-bit PCM, it fetches the left sample word first (2 bytes) and the right sample word last (2 bytes), advancing the internal position by 4 bytes.

like DMA mode, samples are fetched from the buffer at the rate specified in bits 15:8 in the control register. it works exactly like the DMA mode's phase accumulator, but the range has been reduced to 8 bits. the maximum rate in buffer mode is 128, meaning a sample rate of 48khz. the buffer rate can be calculated with the formula:

               frequency * 2^8 
buffer rate = -----------------
                    48000

during a refill interrupt, the interrupt should be acknowledged in the service handler to allow future refill interrupts to be triggered. therefore, a 0 should be written to the corresponding pending flag in the control register:

    ; acknowledge interrupt for buffer 0
    in r0, 0x80000681
    bcl r0, 1
    out 0x80000681, r0
    ; acknowledge interrupt for buffer 1
    in r0, 0x80000681
    bcl r0, 2
    out 0x80000681, r0

back to table of contents

the real-time clock & the interval timers

the fox32 computer contains a real-time clock, and 4 programmable interval timers that can be set to cause an interrupt at specified regular intervals. the real-time clock is located at ports 0x700-0x708. the timers are located at ports 0x709-0x70F. the real-time clock in the emulator is linked to the host's current time, meaning they can be used for calendars, clocks, etc... the interval timers are mostly for scheduling code to be executed at certain intervals, and are not temporally accurate at sub-frame timings, because they are ticked according to CPU instruction ticks (which are emulated in short bursts), rather than real time. therefore, they are not recommended to use them for precise timekeeping applications.

real-time clock port mappings

portdescription
0x700year (e.g 2026)
0x701month (1-12)
0x702day (1-31)
0x703hour (0-23)
0x704minute (0-59)
0x705second (0-59)
0x706miliseconds since startup
0x707daylight savings time active (zero or non-zero)
0x708instructions since startup

interval timer port mappings

portdescription
0x709tick counter
0x70Atick base
0x70Btimer control
0x70Ctimer A period (write), counter (read)
0x70Dtimer B period (write), counter (read)
0x70Etimer C period (write), counter (read)
0x70Ftimer D period (write), counter (read)

all bits in the timer control register are grouped together in the order: timer D -> timer C -> timer B -> timer A.

port 0x70B (timer control)

bitsdescription
23:200=disable interrupt, 1=enable interrupt
19:16(write-only) 1=force reload the corresponding timer
15:120=one-shot, 1=continuous
11:80=clock per tick, 1=clock per cycle
7:40=no interrupt pending, 1=interrupt pending
3:00=disable timer, 1=enable timer

the tick counter increments by 1 every N instructions, where N is the value in the tick base register (port 0x70A). it can be used as a timing source for interval timers, alongside instruction cycles. if the clock is set to clock per tick, then every time the tick counter increases by 1, the timers that are clocked according to it decrease by 1, meaning that the tick counter acts like a period divisor, as opposed to timers clocked per cycle, which decrement by 1 every instruction.

timers count down from the period, set in ports 0x0C-0x0F, to 0. an interrupt is triggered during the transition from 1 to 0. writing to the timer's period port will not reset the counter, and the new period will only take effect after its current period has finished. the counter can be reset with the new period by writing a 1 to bits 16-19 to force reload it.

when the timer is set to one-shot mode, after the timer expires, the period will not be automatically reloaded into the counter. on the other hand, continuous mode will reload the period into the counter after the timer expires.

during a timer interrupt, the handler must acknowledge the interrupt by writing a 0 to bits 4-7. this can be done with the following:

    in r0, 0x8000070B
    bcl r0, 4 ; acknowledge timer A interrupt
    out 0x8000070B, r0

back to table of contents

the disk controller

the disk controller is located at ports 0x1000 to 0x5003. there are only 4 disk ports, specified by the lower nibble of the port (0x1002 means disk port 2). each disk is organised into 512-byte sectors. the 0x100N (N=0..3) ports are read-only and return the disk size (in bytes!). port 0x200N determines where the data buffer for disk read and write operations is in RAM. port 0x300N is a write-only strobe register that initiates a disk read, and writes the content of the disk into the data buffer specified by port 0x200N. port 0x400N is also a write-only strobe register like 0x300N, but for disk writes, and reads the content of the data buffer and writes it onto the disk. in both cases, the value that should be written into them should be a disk sector number. the disk controller is synchronous, meaning the processor will be halted during a disk read or write! to eject or remove a disk, write any value into port 0x500N.

    ; this code will read sector 18 from disk 3 into 0x4000 in memory
    out 0x8000202, 0x4000 ; data buffer at 0x4000
    out 0x8000302, 18 ; read sector 18

    ; this code will write the contents off 0x5000..0x51FF (512 bytes) into sector 19 disk 3
    out 0x8000202, 0x5000 ; data buffer at 0x5000
    out 0x8000302, 19 ; write into sector 19

disk controller port mappings

portdescription
0x1000-0x1003disk size (read-only)
0x2000-0x2003buffer pointer
0x3000-0x3003initiate disk read (write-strobe)
0x4000-0x4003initiate disk write (write-strobe)
0x5000-0x5003remove disk (write-strobe)

back to table of contents

the serial port

the serial port is located at the very beginning of i/o space, and has only 1 port at 0x00000000. nominally, this is wired to a debug terminal (or stdout in the emulator) to be used for debug purposes. writing a value to this register will cause the lowest 8 bits of the value written to it to be output on the serial port. this is a synchronous operation, meaning the processor will stall for some amount of time to ensure the value is output, rather than discarded. reading from this register returns a byte of input from the serial port, which is wired to stdin in the emulator.

back to table of contents