The C64 OS Programmer's Guide is being written
This guide is being written and released and few chapters at a time. If a chapter seems to be empty, or if you click a chapter in the table of contents but it loads up Chapter 1, that's mostly likely because the chapter you've clicked doesn't exist yet.
Discussion of development topics are on-going about what to put in this guide. The discusssions are happening in the C64 OS Community Support Discord server, available to licensed C64 OS users.
C64 OS PROGRAMMER'S GUIDE
Chapter 3: Development Environment
This chapter discusses general issues of programming in 6502 assembly language. It is not a complete guide to how to program in 6502; That is well beyond the scope of the C64 OS Programmer's Guide. It is expected that you are already familiar with the basic principles of 6502 programming. This chapter provides information that is specifically relevant to 6502 assembly for C64 OS. It includes tips and tricks for using TurboMacroPro and provides an overview of registers, flags and symbols that will be used throughout the rest of this guide. Finally, it covers standard macros and how to include files that are common for writing C64 OS software.
The heart of the Commodore 64 is the 6510 microprocesor, a variant of the 6502. The 6510 is instruction-set compatible with the 6502, but includes an additional 6-bit port which is used by the C64 to control the cassette drive and manage the most important memory mapping modes.
This guide cannot cover all of the intricacies of how to program in 6502. That topic is much too large and there are already many wonderful books written on the topic. I can recommend a few excellent resources if you are interested in learning 6502:
- Assembly Language Programming with the Commodore 64 (PDF, 316 Pages)
- Compute's Mapping the C64 & 64C (PDF, 340 pages)
- Commodore 64 Programmer's Reference Guide (Internet Archive, 486 Pages)
- 6502.org Tutorials and Primers
A general architectural overview of the 6502 is also provided later in this chapter.
These are just my personal recommendations. They are how I learned, along with a lot of hands-on practice with trial and error while working through tutorials. The 6502 is a very popular CPU that is used in many computer systems from the 1970s and 1980s; A search online reveals a multitude of tutorials. We also encourage you to ask questions in the developer channels of the C64 OS Community Support Discord server. This is available to all licensed users of C64 OS. If you have not already been assigned the Developer role, ask about becoming a developer in the general channels. You will be assigned the Developer role and gain access to a set of technical, programming and development channels.
How to write code that runs on a C64
Unlike modern development environments with high-level languages, deep toolchains and complicated SDKs, it is possible to write an assembly language program for the Commodore 64 that runs directly on the CPU and requires no special or additional resources.
However, in order to do anything interesting, it is necessary to know certain basic facts about the hardware: The addresses of the I/O chips, the registers of the I/O chips (what they do and how they work,) and an overview of the computer's memory map (where your code is allowed to be assembled to and run from.)
Let's look at the most basic C64 assembly language program, something almost everyone begins with. This is a sort of Hello World for C64 assembly language programmers.
If this program were written and assembled to a file named "borderflash" on storage device #8, it could be loaded and run on a C64 with the following commands:
What can we say about this 3-line program? Several important things. It is in fact a 6502 assembly language program, inc and jmp are two of the 6502's 56 instructions. However, at the same time, this is very much a Commodore 64 program too; Although this might run on another 6502-based computer, it might not do anything useful and almost certainly won't do what it does on a C64. But why is that?
Not every 6502-based computer will have usable memory at $c000. Some might, some might not. We have to know that the C64 has 4 kilobytes of free memory ideally suited for a short assembly program at $c000, so that's where this program is assembled to. The inc $d020 increments the value stored at the memory location $d020, and the jmp directs the 6502's program counter back to the inc instruction in an infinite loop. But we have to know that $Dxxx is a 4 KB region of the C64's memory map dedicated to I/O chips. We have to know that $D0xx is the location of the C64's VIC-II video chip, and that $20 is one of the VIC-II's registers used to hold the border color. By incrementing this register value in an infinite loop the computer essentially freezes up, but we see the border flickering like mad through the various hardware-supported colors as quickly as the 1MHz CPU can loop over these two instructions.
How about those two commands for loading and executing this program? In this example, these commands are part of the C64's built-in operating system. When the computer is turned on it executes code found in the KERNAL ROM and the BASIC ROM, which prepares the hardware and takes you to a READY. prompt. The KERNAL provides an interface that allows you to type and move the cursor around the screen and submit commands to be interpreted by BASIC. Load is a BASIC command, and its parameters tell the machine to read the assembled program into memory. Since it was assembled to $c000 the C64's operating system knows to put the code back at that spot in memory. When loading is complete you're returned to the READY. prompt. Sys is another BASIC command that tells the computer to begin executing assembly language code at the address provided. 49152 is the decimal equivalent of the hexadecimal $c000. (All hexadecimal numbers in this documenation begins with $, the dollar sign.)
From this point forward, the C64's own operating system almost doesn't matter. The program is running directly on the 6502 CPU, and the CPU is wired more-or-less directly to the VIC-II chip, and so the program causes the border to rapidly change color.
The C64's operating system isn't completely out of the picture though, because its IRQ service routine is still being processed 60 times a second. It is still scanning the keyboard and performing other low-level tasks. The program can be exited by holding STOP and pressing RESTORE, which returns you to the READY. prompt and restores the default screen colors. This is the prescribed behavior of the Commodore 64's built-in OS. The program is still in memory and can be run again by entering sys49152 again.
How to write code that runs on C64 OS
Now that we know how a program is run on the 6502 CPU, on a C64, and loaded and executed using the C64's built-in operating system, how do we run code in C64 OS? How can we port this border flashing program to be run from within C64 OS?
C64 OS is still running on the 6502 CPU, which still has the same hardwired addressing to memory and the I/O chips. The big differences are: the user interface, the IRQ service routine, the map of available memory, and the expected structure of the program so that C64 OS knows how to jump the CPU into our program's simple infinite loop.
C64 OS is more sophisticated than the C64's build-in OS, and it supports more than one kind of program. For the sake of this brief demonstration we'll make the simplest possible Application, which is a standard end-user program.
There is always a cost, but who bears it?
In our sample C64 program, we the developer got to choose any free memory address to assemble to, but then as the user to run it we needed to know that address and explicitly tell the computer where to jump to. The freedom given to the developer comes at the cost of difficulty, complication and uncertainty for the user.
A C64 OS Application has the cost of a much more prescribed structure that must be strictly adhered to by the developer, but it buys the user a richer experience, more consistency, and much greater ease of use.
An Application needs a subdirectory in the system's applications directory. This subdirectory is called a bundle and is given the name of the Application. So we'll create a subdirectory of applications and call it "borderflash".
With JiffyDOS: @cd//os/applications @md:borderflash Without JiffyDOS: open15,8,15,"cd//os/applications" print#15,"md:borderflash":close15
Every Application bundle must contain a validly formatted menu definitions file, called menu.m But since our Application is so simple it's not even going to use menus, we'll ignore for now what it takes to create a menu.m file and instead just copy one from any pre-existing Application. I'll copy the one from "Hello World" like this:
With JiffyDOS: @c/borderflash/:menu.m=/Hello World/:menu.m Without JiffyDOS: open15,8,15,"c/borderflash/:menu.m=/Hello World/:menu.m":close15
Finally we'll write our program code and assemble it to a file called main.o saved to the inside of the Application bundle, side by side with menu.m.
We have successfully ported our C64 program to a C64 OS Application. What had to change in the actual program code in order to turn it into a C64 OS Application's main binary? The address to which it is assembled was changed to $0900. That's because all C64 OS Applications assemble to $0900. (This address is defined as a constant in a standard header file that can be included into your source code. This will be discussed later in this chapter.)
A new line is added, a .word with a label start. The main.o file of an Application must begin with a 2-byte pointer to the code that is to be run first, immediately upon launching the Application. Since our simple program has only one routine, the start label just follows immediately after the .word pointer. The rest of the program is exactly the same. It's just standard 6502 code running on the CPU and interacting directly with one of the C64's I/O chips.
Borderflash running as a C64 OS Application.
The load and sys49152 that were used to load and execute the C64 program were just the standard commands that the C64's built-in OS requires you to use. C64 OS has a much richer, mouse-driven environment. After booting into C64 OS, from the File Manager choose Applications from the Go menu or press F4 to jump to the system's applications directory. Our new Application, borderflash, is in the list. Double click it to open, and tada, the program is running. See the screenshot above.
It's not a very useful Application, but it does precisely what the original for the C64 does. It rapidly changes the border color in an infinite loop. Just as the C64's built-in OS continues to run the IRQ service routine, the C64 OS IRQ service routine is still running too. In C64 OS that keeps the mouse pointer active; You can move the mouse around while the border flashes away. It also keeps the time ticking with the seconds indicator blinking, and the CPU-busy indicator continues to tick away in the top left. This is ticking to indicate that the CPU is occupied and not servicing the rest of the user interface. That's what happens if a routine takes more than a few seconds to complete. Our routine is an infinite loop, so it never completes. The UI remains frozen showing the standard Application loading splash screen.
You need an assembler to convert your 6502 assembly language source code into files capable of being loaded and executed. The process of converting your assembly language source code to runnable files is called assembling. This is often misstated as compiling, however a compiler's job is to take a description of program behavior in a higher level language and convert it into the machine code of a target CPU.
An assembler's job is much more direct. Each thing you type in your source code has an immediate, precise and predictable conversion into its machine code equivalent. Using an assembler is easier than writing the machine code by hand because things that are hard to remember or hard to calculate by hand are given easy mnemonics and things like branch distances are computed automatically. Mnemonic literally means, something that assists with memory. Let's look at a few examples:
|$a5||LDA||Load Accumulator||LDA $12||Zero Page|
|$bd||LDA||Load Accumulator||LDA $4000,x||Absolute, X Indexed|
|$e9||SBC||Subtract with Carry||SBC #$10||Immediate|
|$f1||SBC||Subtract with Carry||SBC ($e0),y||Indirect Indexed|
The first column is the opcode, a number from $00 to $ff that is interpreted by the 6502 as an instruction in a specific addressing mode. There is very little to go on from the numeric opcode itself, besides brute memorization, to tell you what its essential behavior is or what addressing mode it uses.
The second column is the mnemonic code that you use in your source code to specify an instruction. It's easy to remember that LDA means load the accumulator and that SBC means subtract with carry. But each instruction comes in anywhere from 1 to 8 different addressing modes. The addressing mode of this instruction you want to use is implied by the syntax of the operands.
Assemblers also do numeric conversions for you on the fly, and can even do simple arithmetic and logical operations on constant values to make it easier to express whatever makes most sense for the context. The format of a number is interpreted from the syntax. The assembler lets you combine different number formats and operations to help you understand your code better.
|no preceding symbol||decimal||192|
|+||addition||$f0 + %00001000|
|-||subtraction||$ff - 3|
|*||multiplication||$02 * 15|
|/||division||192 / $40|
|&||logical AND||$ff & %10000001|
|.||logical OR||120 . $80|
|:||logical XOR||%00001111 : $ff|
Assemblers can also use labels. A label is a word used in your source code that ultimately resolves to a number. The following sample code uses labels.
Two different uses of lablels are shown in the above example, and other examples exist. First inc_by is a label that is simply equated to the number $10. That's not necessarily an address or a pointer or an immediate value, it's just direct substitute for a number. How the label is used in the syntax of the source code defines what it means.
On line 8, the label inc_by is used with ADC #, an immediate mode Add with Carry instruction. In this case, the label is used to supply the immediate value to be added. On line 6, the label next implies the address of the following LDA instruction. The numeric value of next does not get resolved until this code is being assembled. At that time, whatever is the address where this LDA will end up in memory, that's the number assigned to the label next.
On line 10, the assembler takes two steps for us for added convenience. Next is resolved to an address number, but branch instructions take a single byte relative offset, forwards or backwards from the current program counter. The assembler counts up the distance between the BNE instruction and the next label, and converts that to an appropriate relative offset value. Even if we add extra instructions between the BNE and the next label, when the assembler assembles, it automatically computes this distance for us. Very convenient.
Different strokes for different assemblers
This is not the case with different assemblers. The source code is 6502 assembly language, but the syntax used to express the language is not the same in all assemblers. It is often necessary to port the code written for one assembler to the syntax used by a different assembler. Fortunately, most of the syntax is the same: the mnemonics and the syntax for their addressing modes. Some things that vary the most, for example, is whether a label uses a colon (:) after it, whether a relative branch forward or back can be specified with +, ++, - or --, the symbols used for logical operations, the format used for code blocks and macro substitutions, and the pseudo-opcodes that can be used by some assemblers to auto-generate code.
The source code for C64 OS, along with all its Utilities, Applications, libraries, Toolkit classes and more, was written in TurboMacroPro. TMP for short is an assembler that runs natively on a C64 or C128 in 64 mode.
A copy of TMP v1.2 is included on the C64 OS System Card, here:
A copy of TMP v1.2 can also be downloaded, here:
A known rare bug exists in TMP v1.2, but a fixed version can be found here:
Programming natively vs cross assembling
To program natively means to use an assembler that runs on a C64 to write and assemble your software. Cross assembly means you write the code on a computer other than the C64, usually a bigger and faster computer, and assemble it using that computer to produce a program file ready to be used on a Commodore 64.
Since all of C64 OS was programmed natively using TurboMacroPro, the syntax used in its source code is in the TurboMacroPro format. The source code also conforms to certain conventions imposed by the limitations of the hardware. I strongly encourage you to try to do at least some development natively. Besides the limitations imposed, there are some advantages too that are discussed below.
From a purely pragmatic point of view, there are obvious disadvantages to developing natively. Assembly takes longer when it's being done at 1MHz. And it can be more of a challenge to figure out how to debug code that's misbehaving while running on a real C64. However, using an operating system on a 40 year old computer is not an exercise in pragmatism. Just as it is fun to use an OS on a C64, it can be immensely satisfying to develop software for that OS and make it work by using just a C64.
If you prefer to develop software for C64 OS using cross assembly, a version of TurboMacroPro is available for Mac and PC called TMPx (pronounced T-M-P-cross). This can be downloaded here:
It is possible to use an alternative assembler, but the syntax used in the include files (provided by C64 OS to assist in programming) is in TMP format. TMPx is compatible with source code written for TMP, and adds a few features that are not available to the native TMP. The same headers that are provided with C64 OS, on the System Card, are maintained for cross assembly in a format that is only slightly modified from their native originals, and can be found here:
C64 OS is, from one point of view, a battlecry for the usefulness of the Commodore 64. As such this Programmer's Guide will continue to advocate for native development by explaining features of TurboMacroPro, using filenames and directory paths that conform to CMD DOS, describing useful development tools available in the c64tools directory, and scattering other C64-native references throughout the chapters.
TurboMacroPro for Commodore 64
To write your source code, you need a source code editor. On a PC/Mac this is most often a generic text editor of choice. On the C64, TurboMacroPro includes its own source code editor. TMP's source code editor is designed specifically to write 6502 assembly language.
In addition to the commands and features of the TurboMacroPro editor itself, there are also some unique ways which it interacts with the C64's devices, their DOS, filenames, paths, and some features of the KERNAL. Below is a set of 10 areas that can help you to become more productive developing code using your Commodore 64.
Some tips for programming nativelyIssuing Commands
All of the TurboMacroPro editor's commands are entered by first pressing the left arrow, top left key of the C64's keyboard (←). Pushing this once puts the editor into command mode. The cursor stops blinking while in command mode. To leave command mode without triggering any command press STOP, the cursor resumes blinking.
While in command mode, press a key that corresponds to a command. There are many commands, so most keys do something. Most commands do their thing and then leave command mode. A few commands activate a menu of sub options. The options are shown on the status line. Press STOP to abort that menu and leave command mode. Or, press another key to choose the option from the menu.
Throughout the C64 OS Programmer's Guide commands for accomplishing certain tasks within TurboMacroPro are noted. The command is like this:
That means, press the left arrow key once to enter command mode, then press the 5 key to activate that command.Automatic formatting
Each time you input or modify a line of code, its component parts are automatically detected and they sproing into place neatly formatted. If you input the following:
As soon as you leave this line it automatically reformats to:
loop lda #40
In assembly language, typically the mnemonic codes line up one above the other with their operands beginning after a single space. A fixed-width gutter is maintained before the column where the mnemonics line up. The gutter is used hold labels that always align to the zeroth column and stand apart from the mnemonics. The default gutter width is 9 characters, allowing you to have labels up to 8 characters long, followed by a space, and then the mnemonic code starts in column 9 (columns are numbered starting with 0.)
The gutter width can be changed from 0 to 11 characters by positioning the character at the new gutter width and using the ←8 command.Syntax Auto-Correction
An extension to automatic formatting is syntax auto-correction. The formatting requires TurboMacroPro to detect and understand what is a label, what is an instruction, what is an operand and what is a comment.
As soon as you leave a line it gets interpreted for formatting. If you use an unknown instruction or misformat an operand (such as two commas or a missing quote) the line is immediately highlighted in yellow with an error in status bar. This can help you to catch errors early, with needing to wait until assemble time. Example errors include:
- illegal addressing mode
- illegal operator
- illegal pseudo-op
Not only can bad syntax be caught, but unsupported addressing modes get auto-corrected. For example, if you try to use the X index register for any indirect indexed instruction, it gets changed automatically to the Y index register. This is because the 6502 can only use the Y index register in this addressing mode.Code Separators
Good advice for any programmer, who develops code of more than a trivial length, is to keep your code well organized. Group related routines together and put them in logical order of call priority. Once this is done, you'll find that some routines form a much tighter group than others.
Putting separator lines between the logical subunits of your code can be very useful for helping you to keep them mentally separate and to more easily see the bounds of the subunits. TurboMacroPro has the command ←2 for quickly dropping in a code separator comment.Block Operations
Speaking of the good practice of keeping your routines logically ordered, block operations help you to easily move, copy and delete blocks of code. After you've written a body of code, you'll probably find that the routines are not grouped in the most ideally logical order. You'd love to put one above the other, but no one wants to retype something.
Block operations are the TurboMacroPro equivalent of cut, copy and paste. It starts with selection. A selection consists of a marked range of contiguous full lines. With the cursor anywhere on the first line use the mark start ←ms command. Then move the cursor a line lower in the file and use the mark end ←me command. Marking start and end lines can be done in any order, as long as the start marker is on a lower line number than the end marker. Lines in the selected range are highlighted in grey.
Move the cursor to any other line (outside the selection) and use the block move ←bm command to move the selected lines here. Alternatively you can use the block copy ←bc command to copy the selected lines here. Or, you can use the block kill ←bk command to delete a block of lines in one go.Search and Replace
Any good text editor isn't complete without search and replace. To search for a string use the find ←f command. Type the search string and press RETURN. The search only goes from the current line down. To search the whole file be sure to move the cursor to the top of the file first.
Once you've found the first instance of the searched string, you can search for the next instance of the same string using the find next ←h command.
To search and replace use the replace ←r command. It asks for a search string to replace, then for the replacement string. Again starting from the cursor position it searches down the file for the first instance. To replace just that one instance and search for the next use the replace one ←t command. You can repeat this moving once instance at a time through the file. Or, if you're confident, use the replace all ←y command to repeatedly search for the next instance, replace it and repeat all the way to the bottom of the file.Quick Jumping
Once you have a few hundred lines of code, it becomes important to be able to move quickly through the file. TurboMacroPro provides a number of options to make navigation quick and easy.
Although you can use the find command to search through the code looking for a known scratch of code, if you know the name of label, such as the label for a routine, you can instantly jump to that label with the find label ←i command. It references the label index to find the line and jumps straight to that line. Unlike find, find label can jump anywhere in the file before or after the current line.
If you know a line number, the go to line # ←n command lets you jump directly to that line.
You can also set up to 10 numbered marks throughout the file and later jump to any mark. The advantage here is that if you leave a mark on a line and then add new lines above the mark, the mark gets bumped down the file so the marks stays with the code it was set on even as the line numbers change. Use the set mark ←m0 through ←m9 commands to set marks 0 to 9. And ←g0 through ←g9 commands to go to those marks.Disk Commands
Since we're developing on a modern storage device, we need the ability to change devices, partitions and directories from within TurboMacroPro. The disk ←@ command reminds us of the standard DOS wedge @-command. Commands are sent directly to the current device. To change directory we can enter this, followed by RETURN.
Or to change current partition, we can enter the following, very much like you would from the READY. prompt:
To change device, use the increment device # ←d commaand. The new device is shown both, temporarily, in the status message, and permenantly in the status bar at the very botton. Speaking of which, the status bar has two modes that can be cycled by pressing the STOP key.
After changing place you'll probably want to see what files are available here. Use the directory ←* command to list a directory. Having JiffyDOS comes in handy here. The stock KERNAL lets you hold CONTROL to slow the directory listing, but JiffyDOS lets you pause the listing complete, or even lock it by pressing S while holding CONTROL. To unlock press CONTROL+S again. You may abort the directory listing at any time by pressing STOP.Viewing Sequential Files
TurboMacroPro's editor (due to the C64's limited memory) can only have one file open at a time. However, it has a feature that lets you view SEQ files without having to save and close the current file and read in the SEQ file explicitly. Once again, this is very similar to the JiffyDOS feature that lets you stream a SEQ file to the screen without disrupting a BASIC program in memory.
Use the view SEQ file ←! command and enter the name of the file to view. The use of the CONTROL key and JiffyDOS's CONTROL key improvements apply here as they do to directory listings.
The filename field is passed directly to the devices DOS. This allows you to pass not just a filename but an absolute or relative path too. The only problem is that TurboMacroPro expects the field not to exceed 16 characters. Therefore, you can include a path, but the path and filename combined may not exceed 16 characters. In order to shorten path components and filenames wildcard asterisks can be used.
Putting this together then, the view SEQ file feature can be used for online documentation of the C64 OS APIs. For example, suppose we are working in a code file and we need to view the input and output parameters of the C64 OS KERNAL's file routines. Enter the following and press RETURN to see the file KERNAL module header with documentation comments:
←!//os/h/:file.tSaving with Replace
Lastly, while working with your code file, you should save often. Typically a code file is saved using the TurboMacroPro binary file format. This format is much faster to save and to read in than using SEQ text, plus it retains certain metadata such as such as the start, end and quick jump marks, the label index and current line position.
To save a file in the binary format use the save project ←s command. This asks for a filename. If this is the first time you are saving this file, to be safe you should enter just the filename. If a file with the same name already exists it will not overwrite the existing file and tell you that the file already exists.
Once you've established the canonical filename for this code file you need to be able to save updates overtop of or replacing the old file. To do this you use the DOS @-replace command before the filename. Enter all of the following and press RETURN.
This overwrites the old version of this file called mycodefile.a. Review Chapter 2: Architectural Overview → The File System for a reminder of why pathnames are short and the conventional filename extensions for assembly code and other files.
The buffer for entering the filename is again only 16 characters, this must include the @: which means that in practice your filenames should be limited to no more than 14 characters.
Pro Tip: Put the filename in the first line
When you have a file open, TurboMacroPro doesn't know what filename was used to load this file. Everytime you save the file you have to provide the filename to save it as, including the @: replace notation if you want to overwrite the existing file.
Once you have lots of code files and you are moving from file to file while working on your project, it can become easy to forget the exact filename being used for the current file. Make a comment on the first line that specifies this file's filename.
Trust me, there will come a time when you need to save your file and you can't remember exactly what this file is called. A quick glance at the first line will save you a lot of trouble if you accidentally saved this file by overwriting a completely unrelated file.
TurboMacroPro has many other features, but these are some helpful tips and tricks that can make coding natively on your C64 a lot more productive.
For complete documentation of the TurboMacroPro editor see:
For complete documentation of the TurboMacroPro assembler syntax see:
6502 Architecture Overview
This guide cannot possibly give a detailed description of how the 6502 works. Such a description could and does fill many books. For a decent overview with more detail than this guide can provide, read Chapter 1: The Commodore 64 Microcomputer System in this previously mentioned book, Assembly Language Programming with the Commodore 64 (PDF, 316 Pages).
Even if you are familiar with the 6502 however, in the interest of aligning ourselves on certain important concepts, a brief overview of the 6502's registers, instructions and behavior is in order.
The 6502 (and 6510) has 3 main registers, the accumulator (.A or in some contexts just A), the X-index register (.X or sometimes just X), and the Y-index register (.Y or sometimes just Y).
Data can be moved between the accumulator and memory using the LDA and STA instructions, and data can be exchanged between the accumulator and the X or Y registers with TXA, TAX, TYA and TAY instructions. Most arithmetic instructions, ADC and SBC, the logical instructions AND, ORA, and EOR, as well as the instructions for shifting and rolling, ASL, LSR, ROL, and ROR, can only be performed on the accumulator. Although these last four can also be performed directly on memory.
The Index Registers
Data can be moved between memory and the X and Y index registers, but with fewer addressing mode options, using the LDX, STX, LDY and STY instructions. Most arithmetic operations cannot be performed on the X or Y registers, with one main exception. X and Y may be incremented or decremented using the INX, INY, DEX and DEY instructions. There are two related instructions, INC and DEC which can be used to increment and decrement memory, but not the accumulator.
The primary use of the X and Y registers is for loop counters. They can also be used in combination with other instructions to support the various indexed addressing modes.
The 6502 is a bit peculiar amongst CPUs in that not every addressing mode is available to every instruction. Until you become very familiar with the instruction set, it is necessary to refer to the documentation to know which addressing modes and which index registers may be used with which instructions.
See the 6502 / 6510 Instruction Set reference documentation. Or you might need one of these 6502 Instruction Set coffee mugs.
You can buy one here. C64os.com is not sponsored by Stirring Dragon Games.
The Status Register
Every time an instruction is executed it has the potential to modify one of the 7 status register flags. The status register is what ties the result of one instruction to the input of the next.
The status register consists of 8 bits, 7 bits are used for status flags, one is unused. These are:
|Negative||N||b7||Set when an operation results in a negative number|
|Overflow||V||b6||Set when a signed addition or subtraction results in an overflow|
|Unused||—||b5||This bit of the processor status register is not used|
|Break||B||b4||Set when a BRK instruction is executed|
|Decimal Mode||D||b3||When set, certain instructions operate in decimal rather than binary mode|
|Interrupt Mask||I||b2||When set, interrupt requests are ignored|
|Zero||Z||b1||Set when an operation results in a zero|
|Carry||C||b0||Set when an unsigned addition or subtraction results in an overflow|
Comparison and Branching
The contents of memory and the contents of a register can be compared, using the CMP, CPX and CPY instructions. Under the hood these perform a subtraction, whose result affects the status register bits but does not modify any other register or the contents of memory. When two values being compared are equal, for example, their subtraction results in zero which sets the zero flag in the status register.
A series of branch instructions allow the 6502 to take different paths depending on the state of various status flags. Branching on the carry, the overflow flag, the zero flag, and the negative flag are all possible with the BCC, BCS, BVC, BVS, BNE, BEQ, BPL, and BMI instructions.
Several instructions can be used to explicitly affect the status register. The CLC, CLD, CLI, and CLV instructions clear flags. The SED, SEC, and SEI instructions set flags. The BIT instruction is an odd duck but it fits in here somewhere. It's a special kind of comparison instruction that affects status flags.
Jumping, Stack and Subroutines
The 6502 has a 16-bit program counter register that is used to move the processor step-by-step through memory as it loads an instruction, loads its operands, and proceeds to the next instruction. Branches modify the program counter by relatively adding to or subtracting from it. The JMP instruction replaces the program counter with a new address to move execution to a different place in memory altogether.
The 6502 uses page $01 ($0100 to $01FF) as its stack. The size and location of the stack are fixed. An additional register, the stack pointer, is used as an index into the stack page of memory. A set of instructions can be used to push and pull the accumulator and the status register to and from the stack. These are the PHA, PHP, PLA and PLP instructions. Each time a byte is pushed to the stack the stack pointer is decremented. When a byte is pulled from the stack, the stack pointer is incremented.
The JSR instruction is often called Jump to Sub-Routine. But I prefer the explanation that it means Jump Saving Return. When you call JSR the current program counter is pushed to the stack. Then a JMP is performed changing the program counter to somewhere new. The RTS instruction, Return to Sender or Return to Save, pulls the saved program counter off the stack which allows the processor to carry on where it left off before the JSR.
The stack pointer can also be transferred to and from the X-index register using TSX and TXS. This allows for programmatic manipulation of the stack and stack pointer.
The 6502 has two priorities of interrupt, maskable interrupts usually called an interrupt request or IRQ. The other is a non-maskable interrupt usually called an NMI. When an interrupt occurs, the program counter and status register are automatically pushed to the stack and the CPU jumps through a vector near the very end of memory, which must point to a valid routine known as an interrupt service routine or ISR.
Interrupt Service Routine
C64 OS generally manages the interrupts and interrupt service routines for you and provides ways to link into them that don't disrupt normal system processing such as moving the mouse.
An interrupt service routine ends with the RTI, Return from Interrupt, instruction. This pulls the program counter and status register from the stack restoring the CPU to the state it was in before the interrupt occurred.
Big things from small beginnings
This is, believe it or not, almost everything that the 6502 is capable of doing, with a few minor exceptions we won't get into. The instruction set is very small.1
The small number of 6502 instructions provide a core of rudimentary computational units out of which more complicated behavior must be implemented in the form of algorithms. Although you can invent your own, port existing ones from other languages, or find ones that have already been written in 6502, C64 OS provides a number of common algorithms ready for you to call and use. You can profitably make use of these even if you don't fully understand their internal workings.
There are at least 4 different sources of complex functionality, pre-written in 6502, available to C64 OS software development:1) Macros
Macros are described in the following section of this chapter.2) The C64 KERNAL and BASIC ROMs
The C64 KERNAL is outlined in the reference text, C64 KERNAL ROM: Making Sense. For more detailed information on how to use the C64 KERNAL refer to the Commodore 64 Programmer's Reference Guide (Internet Archive, 486 Pages).
The C64 BASIC ROM is more complicated to make use of in C64 OS. There are books written about this topic, including the aforelinked Compute's Mapping the C64 & 64C (PDF, 340 pages).3) The C64 OS KERNAL
The C64 OS KERNAL is covered in Chapter 4: Using the KERNAL.4) C64 OS Shared Libraries
And C64 OS Shared Libraries are covered in Chapter 5: Using Libraries.
What is a macro?
A macro (which stands for "macroinstruction") is a programmable pattern which translates a certain sequence of input into a preset sequence of output. Macros can make tasks less repetitive by representing a complicated sequence of [steps]. Computer Hope — Jargon/M/Macro — 2019
The prefix macro- simply means large. So a macroinstruction is like a large instruction. Each 6502 instruction is small and does something very simple. They can be arranged in any order necessary to accomplish a task, but in practice you quickly discover that certain patterns of instructions show up quite regularly. A macro is given a name and has an implementation in the form of a fixed sequence of instructions, but with certain details set as placeholders. The placeholder notation allows the macro to capture the pattern of functionality while remaining flexible in the details.
When a macro is used in your source code, its name is prepended with the number sign (e.g., #somemacro) followed by a comma delimited list of parameters. When the source code is assembled, the pattern of the named macro is expanded into the code at that place and the parameters are substituted into the macro's placeholders. Some examples are in order.
How to define a macro
Not all assemblers support macros, but the good ones do. Not all assemblers use the same syntax to define or call a macro. The following is the syntax convention used by TMP and TMPx.
Let's analyze this simple macro. It's given a name, b_ifnull, which tells us what it does, branch if null. You can see how this is similar to simple branch instructions, BEQ branch if equal, BCC branch if carry clear, only longer and hence a macroinstruction. This could be shortened to a 3-letter mnemonic, BIN or BAN, but macros are allowed to have more descriptive names which can help make their behavior clearer.
The block of code that constitutes the macro's implementation appears between a pair of pseudo-opcodes, .macro/.endm or alternatively .segment/.endm. The .macro implementation can have labels inside it that are scoped only to the macro and do not affect the labels used in the surrounding context. But sometimes you want the labels in the macro to be scoped to the surrounding context, in which case you can use .segment instead.
A good practice is to put a comment following the .macro opener that lists the order of the parameters. In this case the first parameter is a pointer, a starting memory address where two consecutive bytes can be found whose value is another address in memory. And the second parameter is a branch label. This is a label that must be available to the context in which this macro is called.
In the implementation, regular 6502 instructions are written but placeholders for numbered parameters are specified with a symbol and a number. The backslash \ (which appears as a British pound £ in TMP on the C64) is used to substitute a numeric parameter. An @-symbol can be used to substitute a string. Note that the parameter numbers are 1-based not 0-based. So the first parameter is referenced in this example with \1 (not with \0).
When a macro is expanded, first the parameters are substituted into the pattern's placeholders then that whole is substituted into the code. Only then is the complete result interpreted by the assembler. The placeholders can appear in the macro implementation in any order and, as seen in the example above, are not limited to appearing only once. In a more complex macro, a string parameter can even be used to make opcodes variable.
How to call a macro
Let's see how the above example macro is called in code and then we'll see how it gets expanded.
We have a label, jumpvec, which is defined as the number $0336. The comment following it is there to remind us that this vector (or pointer) is 2-bytes long, thus occupying both the defined address and the following address.
Next we call the macro by using a number-sign # followed by the macro name. Without the number-sign the assembler would treat the macro name as though we were trying to define another label. The list of parameters follows the macro name, delimited with commas. Both parameters in this case have to be numbers. jumpvec is defined as a number and skip is a label that resolves to a number at assemble time.
The macro expansion will thus look like this:
Once the substitutions are complete, you can see that the +1 on the ORA instruction is exactly what we want. At assemble time, LDA will load from $0336 and then be OR'd with $0336+1, i.e., $0337. If the result of OR'ing these two addresses together is still zero, then the jump vector is null and the BEQ skips over the indirect JMP().
Macros can be useful for making your code shorter, the meaning more clear, and for reducing the risk of bugs. However, macros must also be used judiciously. If a macro has a long implementation the added code size is disguised by the shortness of the macro call in your source code. Also, if macros are called between a branch instruction and its branch offset, long macros can easily overflow the maximum branch distance.
A good general rule of thumb is to think about macros as macro-instructions. They should have the feel of an instruction but one which takes several instructions to implement. If you're going to implement anything sizable that needs to be used multiple times, it's probably better to make it a subroutine instead.
Common C64 OS Macros
C64 OS provides a series of common macros to make the code smaller, clearer and less bug prone. These macros are so common that they are used in example source code found throughout this Programmer's Guide.Include Macros
Defined in: //os/h/:modules.h
|inc_h||filename||Includes filename from //os/h/:*.h|
|inc_s||filename||Includes filename from //os/s/:*.s|
|inc_k||filename||Includes filename from //os/s/ker/:*.s|
|inc_tkh||filename||Includes filename from //os/tk/h/:*.h|
|inc_tks||filename||Includes filename from //os/tk/s/:*.s|
Note that the parameter is the filename without the extension. For example:
#inc_s "colors" ;Includes the file //os/s/:colors.s
Assembler Includes and common C64 OS include files are described later in this chapter.
KERNAL Link Macros
Defined in: //os/h/:modules.h
|syscall||lookup,routine||Used to create a table for linking source code to KERNAL calls.|
This macro and the meaning of its parameters is described in Chapter 4: Using the KERNAL.Pointer Macros
Defined in: //os/s/:pointer.s
|rdxy||address||Reads the value stored at the address into X and Y.|
|ldxy||address||Loads the address itself into X and Y (a RegPtr).|
|ldxy||int16||Loads any 16-bit int into X and Y (a RegWrd).|
|stxy||address||Writes a RegWrd to a memory address.|
|copy16||int16,address||Writes a 16-bit word to a memory address.|
|copyptr||ptr,address||Reads the value stored at ptr into a memory address.|
The C64 OS convention for passing pointers is with the low-byte in the X register and the high-byte in the Y register. This is referred to in the documentation as either a RegPtr, a register pointer. The comments on many routines that take pointers will indicate that they take a RegPtr to some resource. This means the value of X and Y represent a memory address at which something can be found.
Some routines indicate that they take a RegWrd, a register word. A RegWrd is only semantically different from a RegPtr. Both are a 16-bit number in X and Y. However the term RegWrd is used when the number does not specifically represent a memory address.Stack Macros
Defined in: //os/s/:pointer.s
|pushxy||Pushes RegPtr to stack, first Y then X.|
|pullxy||Pulls RegPtr from stack, first X then Y.|
|push16||word||Pushes a word to stack, first high byte then low.|
|pushptr||ptr||Pushes a pointer to stack, first high byte then low.|
|pull16||ptr||Pulls 16-bits from stack, writes them to ptr, first low byte then high.|
Pointer Storage Macros
Defined in: //os/s/:pointer.s
|storeset||store,index||Writes a RegPtr into a pointer store at index.|
|storeget||store,index||Reads a RegPtr from a pointer store at index.|
The concept of a pointer store is a simple data storage mechanism in which every item in the store is the same size, two bytes. Multiple stores can be given names and these macros are used to move RegPtrs into and out of the named stores at various indexes.
Inline Argument Macros
Defined in: //os/s/:args.s
|arg8||index,address||Copies an 1-byte inline argument to an absolute address.|
|arg16||index,address||Copies a 2-byte inline argument to an absolute address.|
Inline arguments are a technique supported by the C64 OS KERNAL to allow a much larger number of arguments to be provided to a subroutine call without needing to use much zero page. Any number of 1- or 2-byte arguments, up to a maximum of 255 bytes, can be listed following the JSR to a subroutine that takes inline arguments.
The subroutine that takes inline arguments starts by calling a getargs in the services KERNAL module, passing the number of arg bytes it takes. The number of inline argument bytes it takes must all be provided by the callee. getargs configures argptr, a zero page pointer, to point at the address of the inline arguments, and manipulates the stack so that program flow will continue immediately following the inline arguments.
After the subroutine has called getargs, it can use the arg8 and arg16 macros to access its inline arguments. The combination of the KERNAL call and the macros makes using this technique seamless and transparent.
Long Branch Macros
Defined in: //os/s/:branch16.s
|bcc_||label||JMPs to label if carry clear.|
|bcs_||label||JMPs to label if carry set.|
|bne_||label||JMPs to label if zero flag clear.|
|beq_||label||JMPs to label if zero flag set.|
|bpl_||label||JMPs to label if negative flag clear.|
|bmi_||label||JMPs to label if negative flag set.|
|bvc_||label||JMPs to label if overflow flag clear.|
|bvs_||label||JMPs to label if overflow flag set.|
16-bit Branch Macros
Defined in: //os/s/:branch16.s
|b_ifnull||ptr,label||Branches to label if ptr is null.|
|b_ifset||ptr,label||Branches to label if ptr is not null.|
|bl_ifnull||ptr,label||JMPs to label if ptr is null.|
|bl_ifset||ptr,label||JMPs to label if ptr is not null.|
16-bit Comparison Macros
Defined in: //os/s/:compare16.s
|gtewrd||ptr,word,label||Branches to label if value of ptr is less than word.|
|gte16||ptr1,ptr2,label||Branches to label if value of ptr1 is less than value of ptr2.|
|eqwrd||ptr,word,label||Branches to label is value of ptr does not equal word.|
|eq16||ptr1,ptr2,label||Branches to label is value of ptr1 does not equal value of ptr2.|
Note that the branching condition is the opposite of how a typical branch works. BEQ branches to the label if the values compared were equal. EQWRD branches to the label if the comparison is not equal. The macro call and its branch label function as an if block in a high-level language.
Exception Handling Macros
Defined in: //os/s/:exceptions.s
|try||catch||Opens an exception handling try block, defining a catch.|
|exittry||Used to end a try block, if there is no catch.|
|endtry||endcatch||Ends the previous try, and resumes flow after the catch block.|
Exception handling is described in the Advanced Topics section of the Programmer's Guide. But in short, if an exception is raised and your program doesn't trap it and deal with it, it will bubble up to the system's root level exception handler. The system's response to an uncaught exception is to load the crash library, (//os/library/:crash.lib.r) which displays a crash screen with the address near where the exception occurred.
An exception that occurs after a try block has been opened, and before the try block is exited or ended, redirects program flow to the catch label and the exception ends. Sometimes a try block is used to prevent an exception from crashing the Application, but it has no special catch behavior. In this case, exittry is used to end the try block and continue program flow immediately after it. If there is a catch block, the catch block needs a starting and an ending label. To end a try block that has a catch block, use the endtry macro passing the end catch label. If no exception is caught, program flow continues immediately following the catch block.
Defined in: //os/s/:math.s
All 16-bit numbers are little endian.
|inc16||address||Increments a 16-bit word at memory address.|
|dec16||address||Decrements a 16-bit word at memory address.|
|add816||address,int8||Adds an 8-bit byte to a 16-bit word at memory address|
|sub816||address,int8||Subtracts an 8-bit byte from a 16-bit word at memory address|
|add16||address,int16||Adds a 16-bit word to a 16-bit word at memory address|
|sub16||address,int16||Subtracts a 16-bit word from a 16-bit word at memory address|
|add16ptr||address,ptr||Adds a 16-bit value in ptr to a 16-bit word at memory address|
|sub16ptr||address,ptr||Subtracts a 16-bit value in ptr from a 16-bit word at memory address|
Defined in: //os/s/:pointer.s
|classmethod||method_offset||Prepare this's own class's method to be called.|
|supermethod||method_offset||Prepare this's superclass's method to be called.|
These macros help when implementing Toolkit classes. They depend on and manipulate Toolkit managed pointers, this and class. For more information refer to Chapter 6: Using the Toolkit.Flag Manipulation Macros
Defined in: //os/s/:pointer.s
|setflag||ptr,index,flags||Logically OR indexed zeropage ptr with flags byte.|
|clrflag||ptr,index,flags||Logically AND indexed zeropage ptr with inverse of flags byte.|
|togflag||ptr,index,flags||Logically XOR indexed zeropage ptr with flags byte.|
These macros help with Toolkit objects. The this object is a zero page pointer called this. The index is a this-object property offset. However, these macros can just as easily be applied to any zeropage pointer that points to a data structure.Setter and Getter Macros
Defined in: //os/s/:pointer.s
|setobj8||ptr,index,int8||Writes an 8-bit byte to an indexed zeropage ptr.|
|setobj16||ptr,index,int16||Writes a 16-bit word to an indexed zeropage ptr.|
|setobjptr||ptr,index,ptr2||Writes the value of ptr2 to an indexed zeropage ptr.|
|setobjxy||ptr,index||Writes a RegPtr or RegWrd to an indexed zeropage ptr.|
|rdobj16||ptr,index||Reads 16-bit word from an indexed zeropage ptr into RegWrd.|
|getobj16||ptr,index,address||Reads 16-bit word from an indexed zeropage ptr into address.|
Defined in: //os/s/:string.s Requires: //os/s/:pointer.s
|stradd||ptr,straddress||Copies string from straddress starting at X index to zeropage ptr starting at Y index.|
|setobj16||ptr,index,int16||Writes a 16-bit word to an indexed zeropage ptr.|
|strxyget||Creates a getter routine for a string store.|
|straxget||Creates a getter routine for a string store.|
The stradd macro can be used to append a relative path to file reference. When you request a fresh system file reference, for example, it returns the page number in X and the index of the end of the path in Y. This is perfectly prepared to make it easy to append a relative path string constant to construct a file reference to a subdirectory of the system directory.
The two macros, strxyget and straxget, do the same thing but return a pointer using different registers. strxyget returns a pointer in X/Y, i.e., a RegPtr. straxget returns the pointer in A/X, which is used in some special circumstances.
Each time one of these macros is called, it constructs a getter routine for a string store. A string store is a simple data storage mechanism, similar to a pointer store, except items in this store are null-terminated strings of variable lengths. A label must be provided before each call of these macros and a table of null-terminated strings must immediately follow the macro call. To fetch a string pointer, load the index of the string into A, and JSR to the label before the macro call.
Flow Control Macros
Defined in: //os/s/:switch.s
|switch||case_count||Creates a C-style switch statement.|
6502 assembly language does not offer anything more sophisticated than the 8 branch instructions to handle all possible flow control. Anyone who programs in a higher level language is used to for-loops, do-while-loops, if-elseif-else and switch statements.
The switch macro creates a facsimile of a C switch statement. The input parameter being switched on is a single byte in the accumulator. The macro must be provided with a case count, a number from 1 to 127. Immediately following the macro call must come a byte table containing a unique list of bytes, one byte for each case. Immediately following the byte table must come an RTA table containing the return addresses to routines. There must be a routine return address for every case, but these do not have to be unique, as two different case bytes may switch to the same routine.
If the switch statement does not find a matching case for the value in A, flow proceeds immediately following the RTA table. This can be considered the default case. All registers are preserved if a case is matched. A is preserved but X becomes $FF if the default case is taken. See the example of how this is used. It's easier than it sounds.
As you can see, the switch macro is much easier to maintain than a long list of compares and branchs. Plus, there is no limitation on the distance that an RTA can be offset to.
Above is the set of macros in common use throughout C64 OS's KERNAL, libraries, drivers, Applications and Utilities. C64 OS KERNAL calls, library calls, and Toolkit classes are designed to take and return arguments that work readily with many of these macros.
You too can use any of these macros simply be including the correct s file. And to complete the circle, you can include any of these s files using one of the include macros. To get access to the include macros you only need to manually include modules.h.
Constants and Includes
Earlier in this chapter, under the header How to write code that runs on C64 OS, we saw that for an Application to run in C64 OS, it must be assembled to $0900. We also saw that the first thing in the Application's main binary is a vector to the Application's initialization routine.
The vector to the initialization routine is actually the first in a table of 5 vectors that every Application must implement in order to fully integrate into C64 OS's services. But, where do these numbers come from? Must you simply know and remember that $0900 is where an Application assembles to? In order to send a message to an Application, must you know that the 2nd vector is the message command vector? Certainly not.
C64 OS is a big environment relative to most other C64 software packages. It consists of over a thousand files and growing, organized across over 50 nested subdirectories. In order to support all of the code across the drivers, libraries and classes, the KERNAL, Applications and Utilities, C64 OS comes bundled with a large number of includable headers which define many constants for use in your code.
Version 1.0 of C64 OS comes bundled with the following:
|48||C64 OS KERNAL, Library, Hardware Constants||//os/s/|
|7||KERNAL ROM Constants and Jump Table Routines||//os/s/ker/|
|22||C64 OS KERNAL, Library Jump Table Routines||//os/h/|
|22||Toolkit Class Properties and Constants||//os/tk/s/|
|17||Toolkit Class Method Offsets||//os/tk/h/|
With well over 100 includable header files, there are far too many individual constants to document each and every one of them. The following, therefore, is an overview of some of the most important constants and a method to understand the association between KERNAL modules, libraries, drivers, etc., and their header files. More details on specialized headers and the meaning and use of their constants are provided throughout the Programmer's Guide where they are relevant.
How to include header files
As described above in the section on Include Macros, there is a set of 5 include macros that make it short and easy to source in a header file from any of the 5 sources.
|inc_h||filename||Includes filename from //os/h/:*.h|
|inc_s||filename||Includes filename from //os/s/:*.s|
|inc_k||filename||Includes filename from //os/s/ker/:*.s|
|inc_tkh||filename||Includes filename from //os/tk/h/:*.h|
|inc_tks||filename||Includes filename from //os/tk/s/:*.s|
It is, however, necessary to include at least one file by manually specifying its path. That file is modules.h. Therefore, almost every C64 OS source code file will begin the following way:
Although possible to include headers anywhere throughout your source code, it is generally a good idea to include the headers at the top of the file, before any code. There are a few reasons for this. It helps keep your code organized, with headers included together in one place, this in turn helps you to find and recall which headers you are including. Including the headers before any code can also help prevent phase errors during assembly.
What is a phase error?
TurboMacroPro is a two-pass assembler. It includes files as they are encountered in the source, incorporating their labels into the labels table. It expands macros as it goes, and begins converting the instruction mnemonics into opcodes, and computing label addresses and resolving labels to absolute addresses and relative branch offsets along the way. One line at a time, from top to bottom.
If it encounters a reference to a label, but the label hasn't been defined yet, it will take an educated guess about its size. For instance, if it encounters LDA mytable,x but it doesn't know where mytable is yet, it will reserve two bytes for its address to be filled in during the second pass. All labels following that line are being computed on the assumption that mytable is somewhere above zero page.
If suddenly mytable gets defined and it's in zero page after all, then the second pass will discover the discrepancy in the lengths. Large numbers of offsets were incorrectly computed and the error has to be corrected and assembly must begin again. It is often quite tricky to debug the source of a phase error. It's much better to follow practices that minimize their likelihood of arising in the first place.
The contents of different header sources
There are 5 different sources of header files. What's the difference and where do you find the header files you're looking for? How do you know which header files you need in the first place?
|//os/||The C64 OS system directory.|
|h/||KERNAL and Library jump tables|
|ker/||KERNAL ROM jump tables and constants|
|h/||Toolkit method offsets|
|s/||Toolkit class properties and other constants|
There is a major divide between the Toolkit, which is object oriented and based on an inheritance hierarchy of classes, and the KERNAL and libraries which are collections of useful non-object-oriented routines. Everything for the Toolkit is kept separated in the tk/ subdirectory of the main system directory.
There are two main categories for headers, H and S. The H files are for jump tables of routines and method offsets into classes. The S files are for constants of memory addresses for data, structure offsets, color codes and message codes, I/O register numbers, zero page workspace addresses, and so on.What are the .t files?
As was briefly covered in Chapter 1: Architectural Overview → The File System, the .t files, when they exist, are the equivalent of the corresponding .s or .h files, but with the comments retained. The include macros only source in the .s files and .h files, so you don't need to worry should I be including the .t file or should I be including the .s file?
An explanation is in order. This is typically what happens during development:
A new library is created and at first it has just a couple of routines. A .h file is created for it in //os/h/ with labels for the library's jump table and comments that explain how the routines work. As the library grows and gains new routines, the need for more comments and explanatory notes grows with it. TurboMacroPro must load in all those comments from disk during assembly. As the file grows this process becomes slower and consumes more memory. Disk access speed and memory are both in short supply when coding natively on the C64.
Large programs, such as the File Manager's main binary, must load in and use many libraries. As these develop they start taking longer and longer to assemble. As more code is added, at a certain point TMP runs out of memory during the assemble process and is unable to complete. Steps must be taken to reduce the work load. The easiest first step is to move the comments from large header files into .t files. Further steps to subdivide the work into more manageable chunks is covered in later chapters of this guide.
Migrating the comments and documentation to a .t file can be accomplished in two easy steps.
- Rename the original file to .t, (e.g., @r:string.t=string.h)
- Export the labels from the .t file to its .s or .h equivalent.
From this point forward, the .t file becomes the canonical source file; Any updates are applied to the .t file. Every time the .t file undergoes functional changes, (i.e., labels or their values change, not just a comment) it must have its labels re-exported to its corresponding .s or .h file.
To export labels, first use the assemble ←3 command. This is almost always very fast, but do not attempt to "start" this file by pressing "s", as this is not a program, it's just a set of labels. Press any other key to return to the source code. Then use the export labels ←u command. The status line shows list labels: and awaits you to type in a filename. Enter the filename with the .s or .h extension, using the @-replace DOS prefix to overwrite the file if already exists. (e.g., @:string.h)
This explains why sometimes a .s or .h file exists and has comments and no .t file exists. It's because that .s or .h file has not yet grown large enough to warrant the extra effort of splitting its documentation into a .t file.
There is one other situation in which a .s file cannot be split into a .t file. The export labels feature exports labels only; It cannot export macros. An example of this is pointer.s which consists exclusively of macros. There is no pointers.t file, because it is unfortunately not possible to export the macros stripped of their comments.
Finding a KERNAL module's headers
Let's walk through an example scenario. If you look in the //os/kernal/ subdirectory you find a set of files. These are the KERNAL modules. You don't need to load them manually; The booter loads them and they are always memory resident. Suppose you want to know what is offered by the memory KERNAL module: (//os/kernal/:memory.o)
To find the jump table routines available in this module we can look in //os/h/. If you search this directory for memory.* you find there are two files, memory.h and memory.t.
With JiffyDOS: @cd//os/h @$:memory.* Without JiffyDOS: open15,8,15,"cd//os/h":close15 load"@$:memory.*",8 list
The memory.t is noticeably larger, 7 blocks, compared to the memory.h file which is just 1 block. This is exactly why this file has been split into two, so that TMP need only include a 1 block file at assemble time instead of a 7 block file. That makes assemble time much faster.
Since you want to explore this file and understand how it works, you want to read through the file with all the comments and notes, memory.t.
With JiffyDOS: @t:memory.t
Without JiffyDOS you need a program to display a text file. Although BASIC is not super fast, you can write a text file display program on-the-fly in just 3 lines:
Hold CONTROL to slow down (or pause with JiffyDOS) the output. Press STOP at any time to stop outputting the file. If you use STOP with the BASIC program, it will break on line 20 leaving the file open. You can close the file manually by issuing close2 directly from the READY. prompt.
Switch to Lowercase/Uppercase Mode
TurboMacroPro runs in lowercase/uppercase mode, rather than the READY. prompt's default start up mode of uppercase/graphics. The text files that it produces are thus in lowercase/uppercase. If you see only uppercase letters plus PETSCII graphics symbols, hold SHIFT and tap the COMMODORE key to cycle between the two modes.
An alternative way to view a header file is from within TurboMacroPro. Even if another source code is loaded into the editor, you quickly view a header (or any SEQ type text file) using the View SEQ ←! command.
Pro Tip: Use paths instead of changing directories
Whether you display a file using JiffyDOS's built-in text file viewing command or write a short BASIC program, wherever you enter a filename you may optionally include a relative or absolute path.
From JiffyDOS you can view memory.t directly from the //os/h/ subdirectory regardless of the current directory, like this:
A path can also be used with TurboMacroPro's View SEQ feature, however its input buffer is limited to 16 characters. You can however use a wildcard to shorten the reference, like this:
Continuing with the example scenario now, the file memory.t and memory.h are functionally equivalent; Both contain the same labels with the same values. When you use the include macro…
recall that you don't specify the extension. The macro adds the .h extension for you, to select the smaller of the two files.
Let's take a look at what we find in in memory.t.
The lmem label is not itself a jump table entry, but is used in conjuction with the jump table entries an the #syscall macro to build the KERNAL link table. More detail about this is discussed in Chapter 4: Using the KERNAL.
Each of the following lables is an offset into the jump table of this KERNAL module. The labels are mnemonic for what the routine does. The comments following each routine indicate the registers, zero page values, or other parameters that must be set before calling this routine (indicated with a right-pointing arrow, ->) and the registers, etc. that are set after this routine completes (indicated with a left-pointing arrow, <-).
To find the non-jump-table non-routine-based constants used by this module we can look in //os/s/. Searching this directory for memory.* you find there is only memory.s. This file is evidently not large enough to have warranted splitting its notes out into a corresponding memory.t file.
In this file we can find all the other memory related constants. For example, where the paged memory map is located in workspace; What byte values are used in the memory map; Where the default mempool is stored in workspace; And the stucture of a malloc header. The accompanying notes and comments usually describe the most important details of each of these.
Finding a shared library's headers
What is the difference between the routines found in KERNAL modules and the routines found in libraries? The KERNAL is memory resident and contains more critical, lower level routines. Libraries contain routines that are higher level, not required by the core system, but can be loaded in by whatever needs them; An Application, a Utility or a driver can load in libraries, even libraries can load in other libraries.
Let's walk through another example scenario. If you look in the //os/library/ subdirectory you find a set of files. All the files that end with .lib.r are shared libraries. Some other files that we won't discuss here are also found in the library subdirectory.
In order to use a library, the library must be loaded at runtime by means of the loadlib KERNAL call. Every library your code loads must later be unloaded using the unldlib KERNAL call. Once a library is loaded, the jump table routines your code needs to access have to be dynamically linked to the memory page where the library was loaded.
More information about how to load and unload libraries and how to link to its jump table is provided in Chapter 5: Using Libraries.
The location of header files for libraries is exactly the same as for KERNAL modules. A library's jump table labels, and associated documentation, are found in //os/h/. Thus, the jump table headers for //os/library/:cmp.lib.r are in //os/h/:cmp.h (and //os/h/:cmp.t).
Similarly, all of the non-jump-table standard constants for //os/library/:cmp.lib.r are in //os/s/:cmp.s (and //os/s/:cmp.t).
When Libraries and KERNAL modules coincide
Besides how they are loaded and their routines linked, KERNAL modules and libraries operate in very similar ways. Libraries can be thought of as extensions of the KERNAL that are not always memory resident.
If the name of a library and the name of a KERNAL module coincide, the library is considered a direct extension of that KERNAL module. Therefore, the names of the libraries, file.lib.r, services.lib.r, string.lib.r, etc. even if they do not yet exist, are reserved by C64 OS for future use.
The memory KERNAL module, for example, implements malloc and free, both of which are critical to the system as the Toolkit depends on their use. However, realloc, which is useful but less critical is not included in the KERNAL. It is therefore not permenantly memory resident. Instead realloc is implemented in //os/library/:memory.lib.r (this was not available in C64 OS v1.0 but will be released in one of the first updates.) The jump table routines for the memory KERNAL module and the memory library are both found in //os/h/:memory.h. Similarly, the other constants for both the KERNAL module and its extended library are found in //os/s/:memory.s.
KERNAL ROM headers
C64 OS makes use of some of the KERNAL ROM's routines.
Because the KERNAL ROM is not part of C64 OS proper, its jump table labels and other useful constants are defined in .s headers in the //os/s/ker/ subdirectory. This subdirectory contains multiple files which divide the KERNAL ROM into 7 conceptual modules.
These seven modules, how they work, how they depend on each other, and which routines are compatible with C64 OS, are covered in detail in the reference text, C64 KERNAL ROM: Making Sense.
If you need to make direct uses of the KERNAL ROM's I/O routines, for instance, CHKIN, CHKOUT, CHRIN, CHROUT, etc., you can use the include macro for sourcing the KERNAL ROM header:
Other kinds of .s headers
There are several header files in //os/s/ which do not directly belong to any KERNAL module or library. A few examples are listed below. Some specific important headers and their constants are discussed in more detail in the next section.exceptions
The header exceptions.s defines constants and macros for making use of exception handling.floats
The header floats.s defines constants required for working with floating point numbers via the routines provided by the BASIC ROM.formats
Some headers are used to define the formats of common file types. For example, c64archive.s defines the header structure and value constants for CAR (C64 Archive) files.icons
The header icons.s defines the 8x8 icons available in the C64 OS icon library.io
A series of headers begin with io. These contain labels to the registers of various hardware. The file io.s contains the base addresses of the VIC, SID and CIAs, as well as the RAM character set, screen and color memory buffers, and stack.
Additional io headers are available for the REC (Ram Expansion Controller), the 1541 Ultimate's Ultimate Audio module, the Ultimate Command Interface, the IEC devices, the I2C bus, and more.jobs
There are some headers that end with jobs.s: filejobs.s, openjobs.s, savejobs.s, etc. These define the structures of jobs that are dispatched from Applications to Utilities.types
The header types.s defines the numeric values of the C64 OS type/subtype system used by the clipboard and other low-level data exchanges.
The file modules.h defines several critical constants, but not all of them are intended to be used directly.
It begins by defining the start of the KERNAL module lookup table, and then defines the sizes of the modules; Hence the name, modules.h. Although this defines the sizes and offsets of the KERNAL modules, you should not use these addresses to locate KERNAL routines.
These constants are used to assemble the KERNAL itself and to assemble the KERNAL booter. The booter constructs the module lookup table in memory. Thus, whenever the KERNAL gets reassembled, a matching booter is reassembled at the same time.
DO NOT JUMP DIRECTLY TO THE KERNAL
Although C64 coders love to be elite hackers and pull crazy stunts like jumping directly to half-way through KERNAL ROM routines in order to save one byte or shave 20 cycles, it is a terrible practice. It creates extremely fragile code that is destined to crash when it gets run on any updated version of the KERNAL.
Unlike the C64's own KERNAL ROM, which, besides aftermarket upgrades such as JiffyDOS, remained virtually unchanged throughout the C64's commercial life, the C64 OS KERNAL is going to change. Not may change, the C64 OS KERNAL will change. It's software, loaded from files in the system directory, and the OS comes with an installer for a reason.
Do not jump directly into the KERNAL, unless you want your code to break after maybe one year.
Additionally, modules.h defines several important macros. The first is syscall. Syscall is used to construct a table of KERNAL calls that your code intends to use. The table gets converted to the addresses of the KERNAL modules at runtime. More detail on how to use this, along with a repeat of the warning above, is found in Chapter 4: Using the KERNAL.
Lastly, modules.h defines the include macros mentioned above. This allows you to include other headers without needing to hardcode directory paths.
Virtually every Application and Utility is going to want to include app.s. This header defines several constants that are of central importance to Applications and Utilities.
Crucially, the first defines are appbase and utilbase. These are the two base addresses where Applications and Utilities are assembled to respectively. Given this, the main.a source code of most Applications will begin something like this:
Next is defined initextern. This defines the address of a very special routine that is not part of the KERNAL, but is critical for other code to be able to link to the KERNAL.
Any code that wishes to make KERNAL calls builds a table of the calls it needs to make using the #syscall macro (defined in modules.h.) As part of the code's initialization one of its first steps is to call initextern with a pointer to the syscall table. This converts the table into a JMP table that jumps to the right places in the KERNAL. It allows the KERNAL modules to change sizes or to be rearranged in future C64 OS versions without breaking existing software.
More information about how to use #syscall and initextern is found in Chapter 4: Using the KERNAL.
Next are the definitions for the vector tables which come at the beginning of an Application and a Utility.
Every Application starts with a table of 5 vectors. These are called by the system and by other processes to integrate the Application into the environment.
|appquit||Application Will Quit|
|appfrze||Application Will Freeze to REU|
|appthaw||Application Will Thaw from REU|
Initialization and Quit are called by the operating system once each, when the Application is first loaded into memory and just before the Application is quit and purged from memory, respectively. This allows the Application to set it self up, allocating resources for itself, building its user interface, loading shared libraries, etc. And when it quits to do the opposite, freeing allocated memory, closing open files, unloading shared libraries, etc.
App Freezing and App Thawing are coming in a future version of C64 OS. These vectors are there to allow an Application to implement routines that prepare it to be frozen into an REU. For example, if a network connection is open, it might close that connection. If a file is open, it might close that file. When the Application is restored from the REU the thaw routine is called, allowing the Application to reopen important connections again.
Every Utility starts with a table of 4 vectors. These are called by the system and by other processes to integrate the Utility into the environment.
|utilquit||Utility Will Quit|
Whenever a Utility is attempted to be opened, such as by choosing it from the Utilities menu, by double clicking free memory in the status bar or the menu bar clock, or an Applicatiton programmatically attempting to open a Utility, the system first checks to see if a Utility is running. If a Utility is running, it asks the Utility to identify itself by referencing this vector. It then compares the identity of the running Utility with the name of the one to open, and if they match, then it's already open and it aborts trying to open it again.
Utility Initialization and Quit are there for the same reason as Init and Quit for an Application. Utilities are typically much simpler than Applications and do not get notified when the Application (and the Utility at the same time) get frozen into the REU or later thawed.
Both Applications and Utilities have a message command vector. This points to the message handling routine. Messages are a mechanism for loosely coupling Applications and Utilities together. An Application can attempt to send a message to a Utility without knowing if the Utility will actually support the message, nor really knowing what it will do with that message. And vice versa for Utilities to send messages to the Application.
If a message command is supported, the receiver acknowledges that it handled the message by returning with the carry clear. Any message that is not understood results in no action being taken and the carry is returned set.
Messages consist of a single byte code, for a maximum limit of 256 possible message types. Message types are global to the whole operating system and are allocated judiciously. C64 OS version 1.0 has defined 22 message types. And these are defined by app.s.
The message command byte is sent in the accumulator leaving the X and Y registers for passing parameters. Which parameters are passed depends on the message type. Message types that require more than 2 bytes of data use X and Y as a RegPtr to a data structure.
The operating system's menus communicate with the Application by means of message passing. The status bar also requests its content from the Application by means of a message. Other low-level system conditions are signaled by sending messages, such as changes to the visibility of the menu and status bars, theme color changes, character set changes, changes to detected devices, changes to the clipboard, etc.
Message passing is revisited throughout this guide in the context of how each message type is used.
This header defines all of the constants necessary to make use of the context drawing system implemented by the screen KERNAL module.
It defines the address of the global color theme. It defines flags that can be passed to set the context drawing properties, such as cursor movement orientation, PETSCII to screen code conversion and reverse mode. It also defines the zero page pointers used by the context drawing system to cursor position and other flags.
The context drawing system is based on, as the name suggests, a draw context. All the properties of the context are defined by this header.
The base address of the color theme is defined by ctxdraw.s, while ctxcolors.s defines all of the elements available in the theme. The color theme defines 21 customizable elements. Any user interface element that is manually drawn, or any Toolkit class with a custom drawing routine, should attempt to map itself to one of these 21 elements.
|c_border||Screen Border (vic+$20)|
|c_bckgnd||Screen Background (vic+$21)|
|c_fpanel||Floating Panel Background|
|c_ftitle||Floating Panel Titlebar|
|c_mnubar||Menu Bar and Status Bar|
|c_mnusel||Selected Menu Item (Rollover)|
|c_seltxt||Selected Text (reveresed)|
|c_button||Standard Button Control|
|c_defbut||Default Button Control|
|c_scrlfg||Scroll Bar Foreground|
|c_scrlbg||Scroll Bar Background|
|c_disabl||Disabled Control (button, field, menu option, etc.)|
|c_txffoc||Text Field in Focus|
|c_colfoc||Column Focused (sorted)|
Utilities are like miniature Applications that can be run concurrently with the primary Application. They can either function as stand-alone programs, like Calculator, Today, Time, Mouse or Memory. Or, they can work closely with the Application providing common functionality to different Applications, while not being very useful unless the Application responds to the messages they send, like Colors, Date, Copy, Move or Scratch.
There is some special functionality common to all Utilities. For example, a Utility is typically displayed in a floating palette. These can be dragged around by their title bar. The title bar has a button to close the Utility. The palette can be window-shaded into the title bar. The palette can be in focus or out of focus which is reflected in the color of the title bar. The coordinates of low-level events have to be renormalized to the offset of the palette on screen. Events require low-level bounds checking to see if they occur inside or outside the palette, or on a transparent area of the palette. State data is automatically loaded and saved to a file in the Application's bundle.
All of this functionality and more is provided by the Utility Framework.
This header provides all of the constants for integrating a Utility with the Utility Framework. Utilities are not required to use the framework, but using the framework makes developing a Utility much easier and more consistent with other Utilities.
Utilities that do make use of the Utility Framework are required to implement an extended vector table. These additional vectors point to common structures.
|scrlayer_||The Utility's Screen Layer|
|drawctx_||The Utility's Draw Context|
|tkenv_||The Utility's Toolkit Environment|
|config_||The Utility's block of config data|
|confsize_||The config data block's length|
How to use the Utility Framework will be convered in more detail in Chapter 8: Writing a Utility.
This has been a brief overview of 6502 assembly and the CPU's general architecture; How a regular C64 program is different, at the simplest level, from a C64 OS Application; A crash course in TurboMacroPro; A discussion of macros and how they are used, plus a description of the common macros used by C64 OS; And lastly, an overview of the header files and how to identify which header files go with which code object file.
Some important headers, that do not explicitly correspond to a KERNAL module or any library, were briefly discussed. The Toolkit has its own subdirectory with its own H and S directories for headers. However, because the Toolkit is its own huge topic, discussion of these headers is reserved for Chapter 6: Using the Toolkit.
This document is subject to revision updates.
Last modified: Nov 14, 2022