RP6502-API

Rumbledethumps Picocomputer 6502 Application Programming Interface.

1. Introduction

The RP6502-RIA runs a protected 32-bit kernel that you can call from the 6502. The kernel runs on a processor that is significantly faster than a 6502. This is the only practical way to run modern file systems, networking, and USB host stacks.

The RP6502 presents an opportunity to create a new type of operating system. A 6502 OS based on the C programming language with familiar POSIX-like operations.

2. Calling with fastcall

The binary interface is based on fastcall from the CC65 Internals. The RP6502 fastcall does not use or require anything from CC65 and is easy for assembly programmers to use. At its core, the API is based on a C ABI with three simple rules.

  • Stack arguments are pushed left to right.

  • Last argument optionally passed by register A, AX, or AXSREG.

  • Return value in register AX or AXSREG.

A and X are the 6502 registers. The register AX combines them for 16 bits. AXSREG is 32 bits with the SREG bits in zero page. Let’s look at how to make an OS call through the RIA registers. All kernel calls are specified as a C declaration like so:

int doit(int arg0, int arg1);

The RIA has registers called RIA_A, RIA_X, and RIA_SREG. An int is 16 bits, so we set the RIA_A and RIA_X registers with arg1. I’ll use “A” for the 6502 register and “RIA_A” for the RIA register in this explanation.

We use the XSTACK for arg0. Reading or writing data to the RIA_XSTACK register removes or adds bytes to the XSTACK. It’s a top-down stack, so push each argument from left to right and maintain little endian-ness in memory.

To execute the call, store the operation ID in RIA_OP. The operation begins immediately. You can keep doing 6502 things, like running a loading animation, by polling RIA_BUSY. Or, JSR RIA_SPIN to block.

The JSR RIA_SPIN method can unblock in less than 3 clock cycles and does an immediate load of A and X. Sequential operations will run fastest with this technique. Under the hood, you’re jumping into a self-modifying program that runs on the RIA registers.

BRA #$??      ; RIA_BUSY {-2 or 0}
LDA #$??      ; RIA_A
LDX #$??      ; RIA_X
RTS

Polling is simply snooping on the above program. The RIA_BUSY register is the -2 or 0 in the BRA above. The RIA datasheet specifies bit 7 indicates busy, which the 6502 can check quickly by using the BIT operator to set flag N. Once clear, we read RIA_A and RIA_X with absolute instructions.

wait:
BIT RIA_BUSY
BMI wait
LDA RIA_A
LDX RIA_X

All operations returning RIA_A will also return RIA_X to assist with CC65’s integer promotion requirements. RIA_SREG is only updated for 32-bit returns. RIA_ERRNO is only updated if there is an error.

Some operations return data on the stack. You must pull the entire stack before the next call. However, tail call optimizations are possible. For example, you can chain read_xstack() and write_xstack() to copy a file without using any RAM or XRAM.

2.1. Short Stacking

In the never ending pursuit of saving all the clocks, it is possible to save a few on the stack push if you don’t need all the range. This only works on the stack argument that gets pushed first. For example:

long lseek_impl(long offset, char whence, int fildes)

Here we are asked for a 64 bit value. Not coincidentally, it’s in the right position for short stacking. If, for example, you only need 24 bits, push only three bytes. The significant bytes will be implicit.

2.2. Shorter Integers

Many operations can save a few clocks by ignoring REG_X. All integers are always available as 16 bits to assist with CC65 and integer promotion. However, many operations will ignore REG_X on the register parameter and limit their return to fit in REG_A. This will be documented below as “A regs”.

2.3. Bulk Data

Functions that move bulk data may come in two flavors. These are any function with a pointer parameter. This pointer is meaningless to the kernel because it can not change 6502 RAM. Instead, we use the XSTACK or XRAM for data buffers.

2.3.1. Bulk XSTACK Operations

These only work if the count is 256 or less. Bulk data is passed on the XSTACK, which is 256 bytes. A pointer appears in the C prototype to indicate the type and direction of this data. Let’s look at some examples.

int open(const char *path, int oflag);

Send oflag in AX. Send the path on XSTACK by pushing the string starting with the last character. You may omit pushing the terminating zero, but strings are limited to a length of 255. Calling this from the C SDK will “just work” because there’s an implementation that pushes the string for you.

int read_xstack(void *buf, unsigned count, int fildes)

Send count as a short stack and fildes in AX. The returned value in AX indicates how many values must be pulled from the stack. If you call this from the C SDK then it will copy XSTACK to buf[] for you.

int write_xstack(const void *buf, unsigned count, int fildes)

Send fildes in AX. Push the data to XSTACK. Do not send count, the kernel knows this from its internal stack pointer. If you call this from the C SDK then it will copy buf[] to XSTACK for you.

Note that read() and write() are part of the C SDK, not a kernel operation. CC65 requires them to support more than 256 bytes, so they have wrapper logic to make multiple kernel calls when necessary.

2.3.2. Bulk XRAM Operations

These load and save XRAM directly. You can load game assets without going through 6502 RAM or capture a screenshot with ease.

int read_xram(xram_addr buf, unsigned count, int fildes)
int write_xram(xram_addr buf, unsigned count, int fildes)

The kernel expects buf and count on the XSTACK as integers with filedes in AX. The buffer is effectively &XRAM[buf] here. There’s nothing special about these calls in regards to how the binary interface rules are applied. They are interesting because of their high performance for loading assets.

3. Function Reference

Much of this API is based on CC65 and POSIX. In particular, filesystem access should feel extremely modern. However, some operations will have different argument orders or bitfield values than what you’re used to. The reason for this becomes apparent when you start to work in assembly and fine tune short stacking and integer demotions. You might not notice the differences if you only work in C because the standard library has wrapper functions and familiar prototypes. For example, the lseek_impl() described below has reorderd arguments that are optimized for short stacking the long argument. But you never call lseek_impl() from C, you call the usual lseek() which has the traditional argument order.

zxstack

void zxstack(void);

Abandon the xstack by resetting the pointer. Not needed for normal operation. This is the only operation that doesn’t require waiting for completion.

xreg

int xreg(char device, char channel, unsigned char address, ...);
int xregn(char device, char channel, unsigned char address, unsigned count, ...);

Use xreg() with cc65 and xregn() with LLVM-MOS. The only difference is that xregn() requires a count of the variadic arguments.

Set extended registers on a PIX device. See the RP6502-RIA and RP6502-VGA documentation for what each register does. Setting extended registers can fail, which you should use for feature detection. EINVAL means the device responded with a negative acknowledgementg. EIO means there was a timeout waiting for ack/nak.

Parameters
  • device – PIX device ID. 0-6

  • channel – PIX channel. 0-15

  • address – PIX address. 0-255

  • ... – 16 bit integers to set starting at address.

Errno

EINVAL, EIO

phi2

int phi2(void)

Retrieves the PHI2 setting from the RIA. Applications can use this for adjusting to or rejecting different clock speeds.

Returns

The 6502 clock speed in kHz. 500 <= x <= 8000

Errno

will not fail

codepage

int codepage(void)

Retrieves the CP setting from the RIA. This is the encoding the filesystem is using and, if VGA is installed, the console and default font.

Returns

The code page. One of: 437, 720, 737, 771, 775, 850, 852, 855, 857, 860, 861, 862, 863, 864, 865, 866, 869, 932, 936, 949, 950.

Errno

will not fail

lrand

long lrand(void)

Generates a random number starting with entropy on the RIA. This is suitable for seeding a RNG or general use. The 16-bit rand() in the CC65 library can be seeded with this by calling its non-standard _randomize() function.

Returns

Chaos. 0x0 <= x <= 0x7FFFFFFF

Errno

will not fail

stdin_opt

int stdin_opt(unsigned long ctrl_bits, unsigned char str_length)

Additional options for the STDIN line editor. Set the str_length to your buffer size - 1 to make gets() safe. This can also guarantee no split lines when using fgets() on STDIN.

Parameters
  • ctrl_bits – Bitmap of ASCII 0-31 defines which CTRL characters can abort an input. When CTRL key is pressed, any typed input remains on the screen but the applicaion receives a line containing only the CTRL character. e.g. CTRL-C + newline.

  • str_length – 0-255 default 254. The input line editor won’t allow user input greater than this length.

A regs

return, str_length

Returns

0 on success

Errno

will not fail

clock

unsigned long clock(void)

Obtain the value of a monotonic clock that updates 100 times per second. Wraps approximately every 497 days.

Returns

1/100 second monotonic clock

Errno

will not fail

clock_getres

int clock_getres(clockid_t clock_id, struct timespec *res)

Copies the clock resolution to res.

Parameters
  • clock_id – 0 for CLOCK_REALTIME.

Returns

0 on success. -1 on error.

A regs

return, clock_id

Errno

EINVAL

clock_gettime

int clock_gettime(clockid_t clock_id, struct timespec *tp)

Copies the current time to tp.

Parameters
  • clock_id – 0 for CLOCK_REALTIME.

Returns

0 on success. -1 on error.

A regs

return, clock_id

Errno

EINVAL, EUNKNOWN

clock_settime

int clock_settime(clockid_t clock_id, const struct timespec *tp)

Sets the current time to tp.

Parameters
  • clock_id – 0 for CLOCK_REALTIME.

Returns

0 on success. -1 on error.

A regs

return, clock_id

Errno

EINVAL, EUNKNOWN

clock_gettimezone

int clock_gettimezone(clockid_t clock_id, struct _timezone *tz)

Not implemented. Always fails.

Parameters
  • clock_id – 0 for CLOCK_REALTIME.

Returns

-1 always.

A regs

return, clock_id

Errno

ENOSYS

open

int open(const char *path, int oflag)

Create a connection between a file and a file descriptor.

Parameters
  • path – Pathname to a file.

  • oflag – Bitfield of options.

Returns

File descriptor. -1 on error.

A regs

return, oflag

Errno

EINVAL, EMFILE, FR_DISK_ERR, FR_INT_ERR, FR_NOT_READY, FR_NO_FILE, FR_NO_PATH, FR_INVALID_NAME, FR_DENIED, FR_EXIST, FR_INVALID_OBJECT, FR_WRITE_PROTECTED, FR_INVALID_DRIVE, FR_NOT_ENABLED, FR_NO_FILESYSTEM, FR_TIMEOUT, FR_LOCKED, FR_NOT_ENOUGH_CORE, FR_TOO_MANY_OPEN_FILES

Options
O_RDONLY 0x01
Open for reading only.
O_WRONLY 0x02
Open for writing only.
O_RDWR 0x03
Open for reading and writing.
O_CREAT 0x10
Create the file if it does not exist.
O_TRUNC 0x20
Truncate the file length to 0 after opening.
O_APPEND 0x40
Read/write pointer is set end of the file.
O_EXCL 0x80
If O_CREAT and O_EXCL are set, fail if the file exists.

close

int close(int fildes)

Release the file descriptor. File descriptor will rejoin the pool available for use by open().

Parameters
  • fildes – File descriptor from open().

Returns

0 on success. -1 on error.

A regs

return, fildes

Errno

EINVAL, FR_DISK_ERR, FR_INT_ERR, FR_INVALID_OBJECT, FR_TIMEOUT

read

int read(int fildes, void *buf, unsigned count)

Read count bytes from a file to a buffer.

Parameters
  • buf – Destination for the returned data.

  • count – Quantity of bytes to read. 0x7FFF max.

  • fildes – File descriptor from open().

Returns

On success, number of bytes read is returned. On error, -1 is returned.

A regs

fildes

Errno

EINVAL, FR_DISK_ERR, FR_INT_ERR, FR_DENIED, FR_INVALID_OBJECT, FR_TIMEOUT

read_xstack

int read_xstack(void *buf, unsigned count, int fildes)

Read count bytes from a file to xstack.

Parameters
  • buf – Destination for the returned data.

  • count – Quantity of bytes to read. 0x100 max.

  • fildes – File descriptor from open().

Returns

On success, number of bytes read is returned. On error, -1 is returned.

A regs

fildes

Errno

EINVAL, FR_DISK_ERR, FR_INT_ERR, FR_DENIED, FR_INVALID_OBJECT, FR_TIMEOUT

read_xram

int read_xram(unsigned buf, unsigned count, int fildes)

Read count bytes from a file to xram.

Parameters
  • buf – Destination for the returned data.

  • count – Quantity of bytes to read. 0x7FFF max.

  • fildes – File descriptor from open().

Returns

On success, number of bytes read is returned. On error, -1 is returned.

A regs

fildes

Errno

EINVAL, FR_DISK_ERR, FR_INT_ERR, FR_DENIED, FR_INVALID_OBJECT, FR_TIMEOUT

write

int write(int fildes, const void *buf, unsigned count)

Write count bytes from buffer to a file.

Parameters
  • buf – Location of the data.

  • count – Quantity of bytes to write. 0x7FFF max.

  • fildes – File descriptor from open().

Returns

On success, number of bytes written is returned. On error, -1 is returned.

A regs

fildes

Errno

EINVAL, FR_DISK_ERR, FR_INT_ERR, FR_DENIED, FR_INVALID_OBJECT, FR_TIMEOUT

write_xstack

int write_xstack(const void *buf, unsigned count, int fildes)

Write count bytes from xstack to a file.

Parameters
  • buf – Location of the data.

  • count – Quantity of bytes to write. 0x100 max.

  • fildes – File descriptor from open().

Returns

On success, number of bytes written is returned. On error, -1 is returned.

A regs

fildes

Errno

EINVAL, FR_DISK_ERR, FR_INT_ERR, FR_DENIED, FR_INVALID_OBJECT, FR_TIMEOUT

write_xram

int write_xram(unsigned buf, unsigned count, int fildes)

Write count bytes from xram to a file.

Parameters
  • buf – Location of the data.

  • count – Quantity of bytes to write. 0x7FFF max.

  • fildes – File descriptor from open().

Returns

On success, number of bytes written is returned. On error, -1 is returned.

A regs

fildes

Errno

EINVAL, FR_DISK_ERR, FR_INT_ERR, FR_DENIED, FR_INVALID_OBJECT, FR_TIMEOUT

lseek

off_t lseek(int fildes, off_t offset, int whence)
static long lseek_impl(long offset, char whence, int fildes)

Move the read/write pointer. This is implemented internally with an argument order to take advantage of short stacking the offset.

Parameters
  • offset – How far you wish to seek.

  • whence – From whence you wish to seek.

  • fildes – File descriptor from open().

Returns

Read/write position. -1 on error. If this value would be too large for a long, the returned value will be 0x7FFFFFFF.

A regs

fildes

Errno

EINVAL, FR_DISK_ERR, FR_INT_ERR, FR_INVALID_OBJECT, FR_TIMEOUT

Whence
SEEK_SET = 2
The start of the file plus offset bytes.
SEEK_CUR = 0
The current location plus offset bytes.
SEEK_END = 1
The size of the file plus offset bytes.

rename

int rename(const char* oldname, const char* newname)

Renames and/or moves a file or directory.

Parameters
  • oldname – Existing file or directory name to rename.

  • newname – New object name.

Returns

0 on success. -1 on error.

Errno

EINVAL, FR_DISK_ERR, FR_INT_ERR, FR_NOT_READY, FR_NO_FILE, FR_NO_PATH, FR_INVALID_NAME, FR_EXIST, FR_WRITE_PROTECTED, FR_INVALID_DRIVE, FR_NOT_ENABLED, FR_NO_FILESYSTEM, FR_TIMEOUT, FR_LOCKED, FR_NOT_ENOUGH_CORE

exit

void exit(int status)

Halt the 6502 and return to the kernel command interface. This is the only operation that does not return. RESB will be pulled down before the next instruction can execute. Status is currently ignored but will be used in the future.

Parameters
  • status – 0 is good, 1-255 for error.

A regs

status