Last active
November 21, 2023 18:37
-
-
Save juj/632b412e0eaac02a923eea582724377f to your computer and use it in GitHub Desktop.
llvm-mos inline assembly stubs for C64 KERNAL ROM subroutines
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#pragma once | |
// C64 KERNAL ROM functions | |
#include <stdint.h> | |
#include "mystdio.h" | |
#ifdef __C64__ | |
// TODO: Might want to use this form, but can't due to https://github.com/llvm-mos/llvm-mos/issues/392 | |
// #define _ASM __attribute__((leaf)) __asm__ | |
// #define _KERNAL static __inline__ __attribute__((__always_inline__, __nodebug__, leaf)) | |
#define _ASM __asm__ | |
#define _KERNAL static __inline__ __attribute__((__always_inline__, __nodebug__)) | |
#define _HI(x) ((uint8_t)((uint16_t)(x)>>8)) | |
#define _LO(x) ((uint8_t)(uint16_t)(x)) | |
#define _U16(lo, hi) (((uint16_t)(hi) << 8) | (lo)) | |
// Inline GCC asm syntax has a weird way of requiring different syntax to be used to declare | |
// clobbered registers, depending on whether those registers are, or are not used as inputs. | |
// Use the following macros to better declare intent in the output blocks that a specific | |
// output directive is used to denote a clobber rather than a real output. | |
// See https://github.com/llvm-mos/llvm-mos/issues/385 | |
#define CLOB_A "=a"(a) | |
#define CLOB_X "=x"(x) | |
#define CLOB_Y "=y"(y) | |
_KERNAL uint8_t __stack_avail() | |
{ | |
uint8_t stack_pointer; | |
_ASM("TSX":"=x"(stack_pointer)::); | |
return stack_pointer; | |
} | |
#define NEED_STACK(bytes) assert(__stack_avail() > bytes) | |
// Initialize screen editor & 6567 video chip | |
_KERNAL void _CINT() { NEED_STACK(4); _ASM volatile("JSR $FF81":::"a","x","y","memory"); } | |
// Initialize I/O devices | |
_KERNAL void _IOINIT() { _ASM volatile("JSR $FF84":::"a","x","y"); } | |
// Perform RAM test | |
_KERNAL void _RAMTAS() { NEED_STACK(2); _ASM volatile("JSR $FF87":::"a","x","y","memory"); } | |
// Restore default system and interrupt vectors | |
_KERNAL void _RESTOR() { NEED_STACK(2); _ASM volatile("JSR $FF8A":::"a","x","y"); } | |
// Read/Set KERNAL indirect vector table | |
_KERNAL void _VECTOR_READ(uintptr_t vec[16]) { NEED_STACK(2); assert(vec); uint8_t x,y; _ASM("SEC\nJSR $FF8D":CLOB_X,CLOB_Y:"x"(_LO(vec)),"y"(_HI(vec)):"a","c","memory"); } | |
_KERNAL void _VECTOR_SET(const uintptr_t vec[16]) { NEED_STACK(2); assert(vec); uint8_t x,y; _ASM volatile("CLC\nJSR $FF8D":CLOB_X,CLOB_Y:"x"(_LO(vec)),"y"(_HI(vec)):"a","c"); } | |
// Enable or mute printing of system error and control messages. If bit 7 is set in flags, error messages are enabled. If bit 6 is set in flags, control messages are enabled. | |
_KERNAL void _SETMSG(uint8_t flags) { NEED_STACK(2); _ASM volatile("JSR $FF90"::"a"(flags):); } | |
// Send a Secondary Address to a Device on the Serial Bus after LISTEN | |
_KERNAL void _SECOND(uint8_t addr) { NEED_STACK(8); uint8_t a; _ASM volatile("JSR $FF93":CLOB_A:"a"(addr):); } | |
// Send a Secondary Address to a Device on the Serial Bus after TALK | |
_KERNAL void _TKSA(uint8_t addr) { NEED_STACK(8); uint8_t a; _ASM volatile("JSR $FF96":CLOB_A:"a"(addr):); } | |
// Read/Set Top of RAM Pointer | |
_KERNAL uint16_t _MEMTOP_READ() { NEED_STACK(2); uint8_t lo, hi; _ASM("SEC\nJSR $FF99":"=x"(lo),"=y"(hi)::"c"); return _U16(lo, hi); } | |
_KERNAL void _MEMTOP_SET(uint16_t membot) { NEED_STACK(2); uint8_t x,y; _ASM volatile("CLC\nJSR $FF99":CLOB_X,CLOB_Y:"x"(_LO(membot)),"y"(_HI(membot)):"c"); } | |
// Read/Set Bottom of RAM Pointer | |
_KERNAL uint16_t _MEMBOT_READ() { uint8_t lo, hi; _ASM("SEC\nJSR $FF9C":"=x"(lo),"=y"(hi)::"c"); return _U16(lo, hi); } | |
_KERNAL void _MEMBOT_SET(uint16_t membot) { uint8_t x,y; _ASM volatile("CLC\nJSR $FF9C":CLOB_X,CLOB_Y:"x"(_LO(membot)),"y"(_HI(membot)):"c"); } | |
// Scans the keyboard matrix. | |
// Normally this is done automatically by the C64 KERNAL default IRQ handler so this function does | |
// not need to be manually called. However if you implement your own IRQ ISR, you may want to call this | |
// as part of that interrupt service routine. | |
_KERNAL void _SCNKEY() { NEED_STACK(5); _ASM volatile("JSR $FF9F":::"a","x","y"); } | |
// Set time-out flag on serial bus | |
_KERNAL void _SETTMO(uint8_t flag) { NEED_STACK(2); _ASM volatile("JSR $FFA2"::"a"(flag):); } | |
// Input byte from serial port | |
_KERNAL uint8_t _ACPTR() { NEED_STACK(13); uint8_t byte; _ASM volatile("JSR $FFA5":"=a"(byte)::"x"); return byte; } | |
// Output byte to serial port | |
_KERNAL void _CIOUT(uint8_t byte) { NEED_STACK(5); uint8_t a; _ASM volatile("JSR $FFA8":CLOB_A:"a"(byte):); } | |
// Send UNTALK command to serial bus | |
_KERNAL void _UNTLK() { NEED_STACK(8); _ASM volatile("JSR $FFAB":::"a"); } | |
// Send UNLISTEN command to serial bus | |
_KERNAL void _UNLSN() { NEED_STACK(8); _ASM volatile("JSR $FFAE":::"a"); } | |
// Send LISTEN command to serial bus | |
_KERNAL void _LISTEN(uint8_t device) { uint8_t a; _ASM volatile("JSR $FFB1":CLOB_A:"a"(device):); } | |
// Send TALK command to serial bus | |
_KERNAL void _TALK(uint8_t device) { NEED_STACK(8); uint8_t a; _ASM volatile("JSR $FFB4":CLOB_A:"a"(device):); } | |
// Return I/O status byte | |
_KERNAL uint8_t _READST() { NEED_STACK(2); uint8_t status; _ASM volatile("JSR $FFB7":"=a"(status)::); return status; } | |
// Set logical file number, device number, secondary address for I/O. | |
// Logical file number: A unique number to associate with the file, in range 1-127. | |
// Meaning of Secondary Address from C64 1530 Datasette manual for cassette tape operation: | |
// - 0: Read from tape. >0: Write to tape. | |
// - bit 0: If set, denote a nonrelocatable program. If clear, denote a relocatable program. | |
// - bit 1: If set, write an End-Of-Tape marker after the file to denote the end of all data on the tape. | |
// Meaning of Secondary Address for disks(?): | |
// If secondary address is 0: default file type is PRG, and file is opened for reading. "Relocating" load is performed. | |
// If secondary address is 1: default file type is PRG, and file is opened for writing. Nonrelocating load is performed. | |
// If secondary address >= 2: use the information present in SETNAM in format ",S,R" or ",P,W" etc. to determine file type and read/write | |
_KERNAL void _SETLFS(uint8_t file_no, uint8_t device_address, uint8_t secondary_address) { NEED_STACK(2); _ASM volatile("JSR $FFBA"::"a"(file_no),"x"(device_address),"y"(secondary_address):); } | |
// Sets filename for OPEN command | |
_KERNAL void _SETNAM(const char *filename, uint8_t length) { NEED_STACK(2); assert(filename || !length); _ASM volatile("JSR $FFBD"::"a"(length), "x"(_LO(filename)), "y"(_HI(filename)):); } | |
// Open a Logical File. Returns error code (0=no error). | |
// C64 supports up to 10 simultaneously loaded files. | |
_KERNAL uint8_t _OPEN() { uint8_t error; _ASM volatile("JSR $FFC0\nBCS error_open%=\nLDA #0\nerror_open%=:":"=a"(error)::"x","y","c"); return error; } | |
// Close a Logical File. Returns error code (0=no error). | |
_KERNAL uint8_t _CLOSE(uint8_t file_no) { NEED_STACK(2/*todo:max unknown*/); uint8_t error; _ASM volatile("JSR $FFC3":"=a"(error):"a"(file_no):"x","y","c"); return error; } | |
// Designate a Logical File as the current Input Channel. Returns error code (0=no error). | |
_KERNAL uint8_t _CHKIN(uint8_t file_no) | |
{ | |
uint8_t error,x; | |
_ASM volatile( | |
"JSR $FFC6\n" | |
"BCS end%=\n" | |
"LDA #0\n" // No error occurred, so clear A register that would contain the error | |
"end%=:\n" | |
:"=a"(error),CLOB_X:"x"(file_no):"y","c"); | |
return error; | |
} | |
// Designate a Logical File As the current Output Channel. Returns error code (0=no error). | |
_KERNAL uint8_t _CHKOUT(uint8_t file_no) | |
{ | |
NEED_STACK(4/*todo:max unknown*/); | |
uint8_t error,x; | |
_ASM volatile( | |
"JSR $FFC9\n" | |
"BCS end%=\n" | |
"LDA #0\n" | |
"end%=:\n" | |
:"=a"(error),CLOB_X:"x"(file_no):"y","c"); | |
return error; | |
} | |
// Restore current Input and Output Devices to the Default Devices | |
_KERNAL void _CLRCHN() { NEED_STACK(9); _ASM volatile("JSR $FFCC":::"a","x"); } | |
// Input a character from the current Input Channel | |
_KERNAL uint8_t _CHRIN() { NEED_STACK(7/*todo:max unknown*/); uint8_t byte; _ASM volatile("JSR $FFCF":"=a"(byte)::"c"); return byte; } | |
_KERNAL uint8_t _CHRIN_GET_ERROR(bool *error) | |
{ | |
assert(error); | |
NEED_STACK(7/*todo:max unknown*/); | |
uint8_t byte; | |
bool err; | |
_ASM volatile( | |
"JSR $FFCF\n" | |
"LDX #0\n" | |
"BCC end%=\n" | |
"LDX #1\n" | |
"end%=:\n" | |
:"=a"(byte), "=x"(err)::"c"); | |
*error = err; | |
if (err) return 0; | |
return byte; | |
} | |
// Output a character to the current Output Channel. | |
// Note: calling this function temporarily disables interrupts, | |
// so will stall the advance of the RTC clock (_RDTIM() below). | |
_KERNAL void _CHROUT(uint8_t byte) { NEED_STACK(8/*todo:max unknown*/); _ASM volatile("JSR $FFD2"::"a"(byte):"c"); } | |
_KERNAL bool _CHROUT_GET_ERROR(uint8_t byte) | |
{ | |
NEED_STACK(8/*todo:max unknown*/); | |
bool error; | |
_ASM volatile( | |
"JSR $FFD2\n" | |
"LDA #0\n" | |
"ROL\n" | |
:"=a"(error):"a"(byte):"c"); | |
return error; | |
} | |
// Load file to RAM, or verify (compare) file contents against RAM. To use, call SETNAM+SETLFS+LOAD. This function | |
// does not open a file handle, so no call to CLOSE is needed afterwards. | |
// Note: Only files stored with the SAVE function may be loaded in this way. Files stored with the character-based | |
// OPEN+CHKOUT+CHROUT+CLOSE method can not be loaded with this function. | |
_KERNAL uint8_t _LOAD(uint8_t verify, void *dst, void **dst_end) | |
{ | |
assert(dst); | |
uint8_t error, lo, hi; | |
_ASM volatile("JSR $FFD5":"=a"(error), "=x"(lo), "=y"(hi):"a"(verify),"x"(_LO(dst)),"y"(_HI(dst)):"c","memory"); | |
if (dst_end) *dst_end = (void*)_U16(lo, hi); | |
return error; | |
} | |
// Save contiguous section [start, end[ from memory to a file specified by previous call to SETNAM+SETLFS. | |
// This function does not open a file handle, so no call to CLOSE is needed afterwards. | |
// Note: When a file is written this way, it is only possible to load it using the LOAD function afterwards. | |
// The character-based OPEN+CHKIN+CHRIN+CLOSE method cannot be used to load a file that was written with SAVE. | |
_KERNAL uint8_t _SAVE(const void *start, const void *end) | |
{ | |
uint8_t error,x,y; | |
// JSR $FFD8: KERNAL ROM 'SAVE' routine | |
// Writes a contiguous region of bytes from memory at [start, end[ to disk. | |
// Inputs: | |
// A: a pointer to a 16-bit variable in the zero-page memory address that contains the value of 'start' | |
// X: low 8 bits of 'end' address | |
// Y: high 8 bits of 'end' address | |
// Outputs: | |
// C: If 1, error occurred. If 0, no error. | |
// A: If error occurred, contains error code. If no error occurred, contains garbage | |
// Clobbers: X, Y | |
_ASM volatile( | |
"LDA #%[zp]\n" | |
"JSR $FFD8\n" // Call the 'SAVE' KERNAL ROM routine | |
"BCS error_save%=\n" | |
"LDA #0\n" | |
"error_save%=:\n" | |
:"=a"(error),CLOB_X,CLOB_Y:[zp]"r"(start),"x"(_LO(end)),"y"(_HI(end)):"c"); | |
return error; | |
} | |
// Set the current real-time clock time ("Jiffies" counter) | |
typedef unsigned _BitInt(24) _Jiffies; | |
_KERNAL void _SETTIM(_Jiffies jiffies) { NEED_STACK(2/*todo:max unknown*/); _ASM volatile("JSR $FFDB"::"a"((uint8_t)(jiffies>>16)),"x"((uint8_t)(jiffies>>8)),"y"((uint8_t)(jiffies))); } | |
// Read the RTC "Jiffies" clock. This clock ticks at fixed 60Hz, | |
// even on PAL systems. Note: The RTC clock does not advance | |
// while interrupts are disabled. | |
_KERNAL _Jiffies _RDTIM() | |
{ | |
NEED_STACK(2/*todo:max unknown*/); | |
uint8_t lo, mid, hi; | |
// TODO: Odd, it seems I need to extract these in opposite | |
// order than is documented (docs say A is MSB and Y is LSB | |
// but in practice I find the opposite direction must be used) | |
_ASM volatile("JSR $FFDE":"=y"(hi),"=x"(mid),"=a"(lo)::); | |
return ((_Jiffies)hi << 16) | ((_Jiffies)mid << 8) | lo; | |
} | |
// Read the two lowest bytes of RTC clock only | |
_KERNAL uint16_t _RDTIM16() | |
{ | |
NEED_STACK(2/*todo:max unknown*/); | |
uint8_t lo, mid; | |
// TODO: Odd, it seems I need to extract these in opposite | |
// order than is documented (docs say A is MSB and Y is LSB | |
// but in practice I find the opposite direction must be used) | |
_ASM volatile("JSR $FFDE":"=x"(mid),"=a"(lo)::"y"); | |
return ((uint16_t)mid << 8) | lo; | |
} | |
// Returns 50 on PAL systems and 60 on NTSC systems. | |
_KERNAL uint8_t _FRAMES_PER_SEC() | |
{ | |
uint8_t frames_per_sec; | |
_ASM( | |
" SEI\n" | |
"l1%=: LDA $D012\n" | |
"l2%=: CMP $D012\n" | |
" BEQ l2%=\n" | |
" BMI l1%=\n" | |
" CMP #$20\n" | |
" BCC ntsc%=\n" | |
" LDA #50\n" | |
" JMP done%=\n" | |
"ntsc%=: LDA #60\n" | |
"done%=: CLI\n":"+a"(frames_per_sec)::"c"); | |
return frames_per_sec; | |
} | |
// Query Stop key indicator. If pressed, call CLRCHN and clear keyboard buffer. | |
_KERNAL uint8_t _STOP() { uint8_t stop; _ASM volatile("JSR $FFE1\nTSX\nTXA\nAND #2":"=a"(stop)::"x"); return stop; } | |
// Wait to get a character from current Input Channel (compare to _CHRIN, which does not pause to wait, but returns EOF if buffer is empty) | |
// Returns PETSCII code of character. | |
_KERNAL uint8_t _GETIN() { NEED_STACK(7/*todo:max unknown*/); uint8_t byte; _ASM volatile("JSR $FFE4":"=a"(byte)::"x","y","c"); return byte; } | |
// Close All Logical I/O Files. (does not actually close, but discards track of all open files, as if they were closed) | |
_KERNAL void _CLALL() { NEED_STACK(11); _ASM volatile("JSR $FFE7":::"a","x"); } | |
// Update the RTC Clock and Check for the STOP Key (called regularly by KERNAL) | |
_KERNAL void _UDTIM() { NEED_STACK(2); _ASM volatile("JSR $FFEA":::"a","x"); } | |
// Get number of screen rows/columns | |
_KERNAL void _SCREEN(uint8_t *width, uint8_t *height) { NEED_STACK(2); _ASM("JSR $FFED":"=x"(*width),"=y"(*height)::"a","memory"); } | |
// Read/Set current text cursor position | |
_KERNAL void _PLOT_READ(uint8_t *x, uint8_t *y) { NEED_STACK(2); _ASM("SEC\nJSR $FFF0":"=x"(*y),"=y"(*x)::"c","memory"); } | |
_KERNAL void _PLOT_SET(uint8_t x, uint8_t y) { NEED_STACK(2); _ASM volatile("CLC\nJSR $FFF0"::"x"(y),"y"(x):"a","c"); } | |
// Fetch CIA #1 base address | |
_KERNAL uintptr_t _IOBASE() { NEED_STACK(2); uint8_t lo, hi; _ASM("JSR $FFF3":"=x"(lo),"=y"(hi)::); return _U16(lo, hi); } | |
///////////////////////////////////////////// | |
// Custom extensions to KERNAL functionality: | |
// Soft-resets the system. | |
// TODO: This won't work on C16, C116, Plus/4 or C128x: https://www.c64-wiki.com/wiki/Reset_(Process) | |
_KERNAL void _Noreturn _RESET() { _ASM volatile("JMP ($FFFC)"); __builtin_unreachable(); } | |
// Returns true if there is a key pending in the keyboard input queue. | |
_KERNAL bool _KBHIT() { return *(volatile unsigned char *)0x00C6; } | |
// Clears the screen (resets both character and color data) and moves cursor to top-left (0,0). | |
_KERNAL void _CLRSCR() { _ASM volatile("JSR $E544":::"a","x","y","c"); } | |
// Disables interrupts. Be sure to re-enable interrupts after executing a critical block. | |
// Note that these calls do not have a nesting counter, i.e. calls to _DISABLE_INTR() | |
// won't "stack up". After multiple calls to _DISABLE_INTR(), interrupts can be re-enabled | |
// with just one call to _ENABLE_INTR(). | |
_KERNAL void _DISABLE_INTR() { _ASM volatile("SEI"); } | |
// Re-enables interrupts. | |
_KERNAL void _ENABLE_INTR() { _ASM volatile("CLI"); } | |
// Sets the screen border color (lowest 4 bits of color, 0-15) | |
_KERNAL void _BORDER(uint8_t color) { *(volatile uint8_t*)0xD020 = color; } | |
#endif // ~__C64__ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment