Initial commit: C64 project skeleton with oscar64 submodule

This commit is contained in:
ballz
2026-07-17 00:36:14 +02:00
commit cbe5cf6a47
31 changed files with 29332 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
# build artifacts
src/build/
*.prg
*.asm
*.map
*.lbl
*.int
*.dbj
*.csz
*.d64
*.bin
*.crt
# oscar64 build output (built by `make -C make compiler` from oscar64 source)
oscar64/bin/
oscar64/build/
# editor / IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# misc
*.log
core
+3
View File
@@ -0,0 +1,3 @@
[submodule "oscar64"]
path = oscar64
url = https://github.com/drmortalwombat/oscar64.git
+935
View File
@@ -0,0 +1,935 @@
# Oscar64 — Learnings
Notes from exploring the [Oscar64](https://github.com/drmortalwombat/oscar64) C/C++
cross-compiler that targets the 6502 family (C64, C128, PET, VIC-20, Plus/4, NES,
Atari 8-bit, Commander X16, Mega 65). Source tree was copied from `~/temp/c64/oscar64`
into `./oscar64`.
## 1. What it is, in one paragraph
Oscar64 is a native (not LLVM, not CC65) C/C++ compiler whose whole reason to
exist is "fast, dense 6502 code from C". The author expected 6502 to be a bad C
target — small register file, no stack-relative addressing, 256-byte hardware
stack, no native 16-bit ops — and tried a bytecode interpreter first. The
interpreter won on size but lost badly on speed, so they switched to native code
generation and discovered that careful analysis actually beats the interpreter on
both axes. The C64 hits 442 dhrystone V2.2 iterations/sec at `-O3`, which is
the kind of number that makes you look twice. The compiler is self-contained
(no external assembler / linker / CRT), supports disk overlays and banked
cartridges for big projects, and has been used to ship multiple real games.
## 2. Repository layout
```
oscar64/
├── oscar64/ # The compiler itself (C++17, ~50 .cpp/.h files)
│ ├── oscar64.cpp # CLI driver / main()
│ ├── Compiler.{h,cpp} # Orchestrator: ParseSource() → GenerateCode() → WriteOutputFile()
│ ├── Preprocessor.{h,cpp} # Macros, #embed, #for, #repeat
│ ├── Scanner.{h,cpp} # Tokenizer
│ ├── Parser.{h,cpp} # C99 + big chunks of C++
│ ├── Declaration.{h,cpp} # AST / symbol tables
│ ├── InterCode.{h,cpp} # Target-independent IR (the "IC_*" ops)
│ ├── InterCodeGenerator.{h,cpp} # AST → IR
│ ├── GlobalAnalyzer.{h,cpp} # Whole-program analysis
│ ├── GlobalOptimizer.{h,cpp} # Whole-program opts (inlining, const-prop, dead code)
│ ├── Constexpr.{h,cpp} # Compile-time evaluation
│ ├── ByteCodeGenerator.{h,cpp} # Optional bytecode backend (-bc)
│ ├── NativeCodeGenerator.{h,cpp} # 6502 native backend (the hot path)
│ ├── NativeCodeOutliner.{h,cpp} # -Oo: extract repeated code sequences
│ ├── Assembler.{h,cpp} # 6502 assembler + macros
│ ├── Linker.{h,cpp} # Region/section/object layout
│ ├── DiskImage.{h,cpp} # .d64 writer
│ ├── Emulator.{h,cpp} # Built-in 6502 emulator for -e/-ep
│ ├── Compression.{h,cpp} # LZO + RLE for #embed
│ ├── Disassembler.{h,cpp} # For .lbl / .asm output
│ └── ...
├── include/ # Runtime headers + stdlib (what compiled programs #include)
│ ├── crt.c, crt.h # Target-specific startup (linked unless -rt=)
│ ├── oscar.c, oscar.h # oscar_expand_lzo / oscar_expand_rle
│ ├── c64/ # VIC, CIA, SID, kernalio, rasterirq, memmap, sprites…
│ ├── c128/, vic20/, plus4/, nes/, cx16/, opp/, audio/
│ ├── gfx/ # bitmap, bitmapmc, vector3d, blob…
│ ├── new/ # C++ new/delete
│ └── stdio.h, stdlib.h, string.h, math.h, conio.h, …
├── samples/ # ~100 worked-example programs
│ ├── stdio/, kernalio/, memmap/, hires/, hiresmc/, particles/, fractals/,
│ ├── rasterirq/, scrolling/, sprites/, games/
│ └── resources/ # .bin / .ctm / .spd assets used by samples
├── autotest/ # Self-test harness
├── CMakeLists.txt # C++17, globs oscar64/*.cpp
├── make/ # Makefile-based build (no CMake needed)
├── oscar64.sln / .vcxproj
└── oscar64.md # The full reference manual (also in README)
```
Build: `make -C make compiler` (uses the makefile). Or `cmake -B build && cmake --build build` — both produce a single `oscar64` binary. Windows is supported via the included `.sln`.
## 3. Compilation pipeline
From `oscar64.cpp` (the driver) the lifecycle is short and explicit:
```cpp
Compiler* compiler = new Compiler();
compiler->ParseSource(); // lex + parse + AST for every translation unit
compiler->GenerateCode(); // analyze, optimize, code-gen, link
compiler->WriteOutputFile(...);// emit .prg / .crt / .bin (+ .map / .asm / .int / .lbl)
```
Inside `Compiler::ParseSource()` (`Compiler.cpp:57`):
1. Choose zero-page register layout from `-tm` (the 6502 has no general-purpose
regs, so the compiler dedicates a fixed ZP window per target — see
`BC_REG_*` symbols).
2. Loop: pop a pending `CompilationUnit` from the queue, open the file via
`Preprocessor`, feed it to `Scanner``Parser`. `#pragma compile("foo.c")`
enqueues additional units, which is how the "no makefile" build model works:
`#include <stdio.h>` literally drags `stdio.c` into the build.
Inside `Compiler::GenerateCode()` (`Compiler.cpp:550`):
1. Verify the runtime startup (`crt.c`) is present, otherwise error.
2. Materialize default memory regions for the chosen target machine — e.g. for
a C64 PRG: `zeropage` at `0xf7..0xff` (or `0x80..0xff` with `-xz`), `startup`
at `0x801..0x880` (native) or `0x801..0x900` (bytecode), and a `main` region
that holds `code`, `data`, `bss`, `heap`, `stack`.
3. Build the vtables (`BuildVTables`), check `operator new` overrides, finish
deferred template expansion.
4. Per-procedure: `CompileProcedure()``InterCodeGenerator` lowers the AST to
IC, then for `-n` (native, the default) `NativeCodeGenerator` emits 6502,
otherwise `ByteCodeGenerator` produces interpreter ops.
5. `NativeCodeOutliner` (when `-Oo`) collapses repeated code sequences.
6. `GlobalOptimizer` runs across the whole program: inlining, constant
propagation, dead-code removal, value-range narrowing, constant-parameter
folding, recursion/function-pointer detection, etc.
7. `Linker` lays out objects into regions, produces `.prg` (or `.crt`/`.bin`),
plus `.map`, `.asm`, `.int`, `.lbl`, and (with `-g`) `.dbj` JSON debug info.
The key takeaway: there is no separate `cc1` / `ld` / `as` — it is one
whole-program compiler. The "linker" is a pass that knows every function and
chooses what to emit.
## 4. How it beats the 6502
The reference manual is upfront about the tricks. The four big ones:
- **Software stack in zero page.** Locals and parameters live in ZP, not on the
256-byte hardware stack. The C64 default reserves `0xf7..0xff` for this; with
`-xz` it expands to `0x80..0xff` (and you lose the `STOP/RESTORE` BASIC return).
- **Static call-graph stack.** Because the compiler sees the whole program, it
analyses the call graph and gives every function a fixed ZP frame offset
instead of pushing/popping. This is why *recursion* and *function pointers*
are explicitly called out in the manual as "you will pay for this" — both
defeat the static analysis and force real stack usage.
- **Zero page as register file.** Globals can be opted into ZP with
`__zeropage`; the compiler also auto-places small globals into ZP under `-Oz`.
With `-xz` the wider ZP window gives you ~128 bytes of pseudo-registers.
- **Value-range analysis.** 16-bit ops on the 6502 are *much* more expensive
than 8-bit. The compiler narrows arithmetic to 8 bits whenever it can prove
the value fits. Pointers and globals are harder to narrow (they could change
behind the compiler's back) so the manual hammers the "use 8-bit locals, mark
things `const`, prefer `unsigned`" advice.
The classic 6502 limitation "no indirect-with-offset" is worked around with
`__striped` arrays — instead of `LHLHLHLH…` you get `LLLLLLLLHHHHHHHH` so
each element can be reached with `abs,X` instead of needing a multiply.
## 5. The C/C++ language surface
C99 is supported in full. C++ mode is selected with `-pp` or a `.cpp` file. The
listed supported C++ features (from `oscar64.md`) are surprisingly complete:
namespaces, references, member functions, ctors/dtors, operator overloading,
single inheritance, `const` methods, `new`/`delete`/`new[]`/`delete[]`, virtual
functions, `string`/`iostream`, default params, templates, vector/array/list,
lambdas, `auto`, range-for, `constexpr`, parameter packs. Not a current
standard, but plenty for a 6502.
## 6. 6502-specific extensions (the fun stuff)
These are the bits that make Oscar feel like "C for the C64" rather than
"hosted C squeezed onto a 6502":
- **`#embed`** — the preprocessor can read a binary file, optionally compress
it (`lzo` / `rle`), slice an offset+length, and emit 8-bit or 16-bit words.
It also understands Charpad `.ctm` (chars/tiles/map/attr1/attr2) and
Spritepad `.spd` (sprites/tiles). This is the standard way to ship charset
and sprite data in a sample.
- **`#pragma compile("file.c")`** — pulls another translation unit into the
build. The whole "no makefile" build model rests on this: `#include <stdio.h>`
ends up `#pragma compile("stdio.c")`-ing the stdio implementation.
- **Storage-class qualifiers:** `__native` (force native codegen for a fn),
`__zeropage` (place a global in ZP), `__noinline`, `__forceinline`, `__export`
(force a symbol to be emitted even if unreferenced), `__striped` (the
layout trick above), `__memmap` (a stronger memory fence than `volatile`,
used for things like bank registers that re-map the address space).
- **Pragmas:** `region(name,start,end,flags,bank,{sections})` and
`section(name,…)` reshape the memory map. `align(var, 8)`, `heapsize()`,
`stacksize()`, `overlay(name, bank)`, `unroll(n|full|page)`, `optimize(…)`,
`reference(name)`, `intrinsic(name)`, `message(...)`, `charmap(char,code,…)`,
`callinline()`, `warning(disable:num,…)`.
- **PETSCII and screen-code literals:** `p"hello"` / `P"hello"` makes a
PETSCII string, `s"hello"` / `S"hello"` makes screen codes. `-psci` flips
the default for unprefixed strings. `iocharmap(IOCHM_PETSCII_2)` also
swaps the font.
- **Inline assembler with full C interop:** `__asm { lda c ; bne w1 ; … }`,
where `c` is a local var (ZP register) and `accu`/`addr`/`sp`/`fp` are the
fixed return-value and frame-pointer slots. `Type::Member` (e.g. `lda #P::ns`)
generates a struct member offset. `__interrupt` saves ZP scratch regs; `__hwinterrupt`
also saves A/X/Y and exits with `rti`. A top-level `__asm name { … }` defines
a pure-asm routine. The optimizer (`-Oa`, on at `-O2+`) will rewrite your
inline asm.
- **Hypotheticals:** `__assume(x < 10)`, `__assume(p != nullptr)`,
`__assume(false)` for `default:` arms — these feed the value-range analysis.
- **Preprocessor loops:** `#assign name expr` and `#repeat … #until cond` let
you generate unrolled code (e.g. 25 absolute stores to fill a screen
column). `#for(i, n) text` is the one-liner form.
## 7. Memory model and program layout
The "Region / Section / Object" three-level linker is worth understanding because
almost every advanced sample uses it:
- **Region** = a physical chunk of memory (or a cartridge bank).
`#pragma region(main, 0x0a00, 0xa000, , , {code, data, bss, heap, stack})`
reshapes the default map. Flags and an optional bank slot exist for
cartridge layouts.
- **Section** = a logical bucket (`code`, `data`, `bss`, `heap`, `stack`,
`zeropage`, or a user-named one). `#pragma section(name, 0, startSym, endSym)`
declares it; the start/end symbols are emitted so asm can refer to them
(this is how `crt.c` exposes `StackStart`, `StackEnd`, `BSSStart`, `BSSEnd`,
`CodeStart`, `CodeEnd`).
- **Object** = one actual blob: a function, a `const` array, a BSS variable,
a pad, etc. The linker walks the dependency graph from `main` (via the
startup code in `crt.c`) and only includes reachable objects, which is why
the whole stdio family can live in a single `.c` without exploding the
output.
For a default C64 PRG the layout is:
- `0x08010x0880` (native) or `0x08010x0900` (bytecode) — startup + BASIC stub.
- `0x09000x0a00` — bytecode jump table (only when not all native).
- `0x0a000xa000` — main, with `code`, `data`, `bss`, `heap`, `stack` sections.
For cartridges, `easyflash` (`-tf=crt`) expands bank 0 into common memory at
startup; generic `crt8`/`crt16` use the `rom` region from bank 0 and put
`bss`/`stack`/`heap` in `main`. The `__bankof(expr)` operator gives you the
bank id of a symbol, so dynamic code can `mmap_set` to the right bank before
calling.
For larger programs the `#pragma overlay(name, bank)` mechanism turns each
cartridge bank into a `.prg` file inside the `.d64` image, and `krnio_load`
brings it in on demand — that's how the games fit.
## 8. Targets and output formats
`-tm` picks the machine: c64, c128, c128b, c128e, plus4, vic20 (+3/+8/+16/+24),
pet (8k/16k/32k), nes (nrom, nrom_h, nrom_v, mmc1, mmc3), atari, x16, mega65.
`-tf` picks the file: `prg`, `crt`, `crt8`, `crt16`, `bin`. `-d64=foo.d64`
builds a disk image and `-f=path` / `-fz=path` add (optionally compressed)
resource files to it; `oscar_expand_lzo` decompresses at runtime, and
`krnio_read_lzo` decompresses on the fly during a kernal read.
## 9. Built-in emulator
`-e` runs the program in a built-in 6502 emulator, `-ep` profiles it. This is
the iteration loop: write C, compile, run, no need to launch VICE for the
common case. (VICE is still the right choice when you need cycle-exact raster
behaviour — see `-moncommands` for loading the `.lbl` symbol file.)
## 10. Useful artifacts the compiler writes
Beyond the main binary, you get (all explained in `oscar64.md`):
- `.map` — regions, sections, objects, objects-by-size. The first place to
look when your `.prg` is too big.
- `.asm` — full 6502 listing with source references when `-g` is on. This is
what you cross-reference from inside the VICE monitor.
- `.int` — the intermediate code dump.
- `.lbl``al address .symbol` lines for VICE's monitor.
- `.dbj` — JSON with `memory`, `variables`, `functions`, `types`. Combined
with [Modern VICE PDB Monitor](https://github.com/MihaMarkic/modern-vice-pdb-monitor)
this gives source-level debugging. The manual recommends `-n -g -O0` for
the best debug experience.
- `.csz` — every source line annotated with its start address and emitted
byte count. Great for finding the one loop that ate all your memory.
## 11. Optimization levels (cheat sheet)
```
-O0 no optimization
-O / -O1 default
-O2 + auto-inline, inline-asm optimizer, constant-parameter folding
-O3 + auto-place globals in ZP, outliner
-Os size
-Oi auto-inline (sub-flag of O2/O3)
-Oa inline-asm optimizer (sub-flag of O2/O3)
-Oz globals → zero page (sub-flag of O3)
-Op constant-parameter folding
-Oo outliner (extract repeated sequences)
-Ox pointer-arith page-boundary awareness
-OM self-modifying code (saves bytes when SMC is cheap)
```
The mantra in the manual: "Avoid recursion. Avoid function pointers. Be aware
of aliasing. Prefer unsigned. Stick to 8 bits. Prefer enums over #defines. Mark
const. Use `__assume`." Each of those maps directly to a 6502 pain point.
## 12. A minimal end-to-end
From `samples/stdio/helloworld.c` (9 lines, the whole file):
```c
#include <stdio.h>
int main(void)
{
putchar(14); // clear screen
printf(p"Hello World\n"); // p"" → PETSCII string
return 0;
}
```
Build: `oscar64 helloworld.c``helloworld.prg`. Run with `x64 helloworld.prg`
or `oscar64 helloworld.c -e`. The C runtime comes from `include/crt.c` and
`include/stdio.c`, both pulled in automatically by `#include <stdio.h>` (which
in turn `#pragma compile("stdio.c")`s the stdio implementation, which
`#pragma compile("conio.c")`s conio, which is how the whole tree stitches
together with no makefile).
A more interesting example, `samples/hires/lines.c`, shows the full
hardware-programming idiom: `#include <c64/memmap.h>` to bank out BASIC,
`#include <c64/vic.h>` to flip into hires mode, `#include <gfx/bitmap.h>` for
`bm_line`, and the `Bitmap` / `ClipRect` types. Note `char *Hires = (char *)0xe000;`
— absolute addressing is the normal way to reach video memory.
## 13. Things I want to remember when I come back
- It is one binary. No assembler, no linker, no CRT to wire up. `crt.c` is
pulled in by default; `-rt=foo.c` swaps it, `-rt=` removes it.
- Source files are pulled in by `#pragma compile("x.c")`, not by listing them
on the command line. The only files on the command line are the user's root
translation units (and they probably just `#include` headers that do the
pragma work).
- The CLI is `-option` style. `-i=path` adds an include, `-ii=path` *replaces*
the default include, `-tf=…`/`-tm=…` set format/machine, `-dNAME` defines a
macro, `-O3` is the speed preset, `-e` runs the built-in emulator, `-g`
emits `.dbj` and source lines in `.asm`.
- Native code is the default (`-n` is implicit now). `-bc` opts into bytecode.
- "Region / Section / Object" is the mental model for the linker. Almost every
non-trivial memory layout in the samples is a `#pragma region(…)`.
- The two escape hatches when the optimizer is wrong: `__assume(cond)` and
`#pragma optimize(noasm)` (or `__asm volatile { … }` for a single statement).
- Read `oscar64.md` in the repo root — it is the real reference manual and is
far more thorough than what fits here.
---
# Part II — Programming the C64 with Oscar64
This section combines the Oscar64 architecture notes above with the C64
hardware reference in `../PROG_C64.md` and `../docs/c64/`. The goal is a
practical "how do I write C for the C64 with this compiler and not
shoot myself in the foot" guide.
## 14. The C64 memory model in Oscar64
### 14.1 The default region layout (no `region`/`section` pragmas)
The runtime `crt.c` materializes these regions for a C64 PRG (the
`Compiler::GenerateCode` switch in `Compiler.cpp:550-650` is the
authoritative source):
| Region | Address | Contents |
|--------|---------|----------|
| `startup` | $0801-$0880 (native) | BASIC stub, KERNAL trampoline, init code, `main()` |
| `main` | $0900-$A000 | `code`, `data`, `bss`, `heap`, `stack` sections |
This gives you 38 KB of usable RAM at $0900-$A000 once the KERNAL ISR
trampoline is set up. (For a fresh C64 that's *less* than BASIC sees,
but in exchange you have the entire 38 KB contiguous, no ROM reloads,
and a known startup state.)
Inside the `main` region, the default stack size is 1 KB and the default
heap size is 1 KB. Both are tweakable with `#pragma stacksize(N)` and
`#pragma heapsize(N)`; setting `heapsize(0)` is mandatory if you don't
use `malloc`.
### 14.2 The zeropage software-stack window
Oscar64 dedicates a fixed zero-page window per target machine to its
software stack (parameters, locals, temporaries). For the C64 with
`-O3` the default window is `0xF7..0xFF` (9 bytes: frame pointer, stack
pointer, two scratch pairs, plus a return-address register). With
`-xz` you get `0x80..0xFF` (128 bytes — much more room for locals, but
the KERNAL's `STOP/RESTORE` warm-start vector at $02/$03 stops working,
which is why the Oscar64 docs call it "extended zero page usage").
This means: by default, you cannot freely use $F7-$FF. The Oscar64
runtime uses them, and so does its `mmap_set` trampoline. If you write
inline asm that touches those addresses, you will collide. If you want
more zero-page scratch you have three options:
1. Compile with `-xz` to widen the window.
2. Use `__zeropage` on globals (compiler places them in `$80..$F7` if
`-Oz` is on, otherwise wherever it fits).
3. Mark the global `__zeropage int x;` and use the same name in
inline asm — Oscar64 will route both to the same ZP slot.
### 14.3 The bank-switching model
The C64's `mmap_set()` API in `c64/memmap.h` writes to `$01` (the
6510 port) to change which ROMs and I/O are visible. The trampoline
that `mmap_set` installs at $EA31 (the KERNAL IRQ vector location) is
key: when an IRQ fires, the trampoline briefly restores the default
memory configuration, calls the real KERNAL ISR, and then re-applies
your mapping. Without that trampoline, the KERNAL's IRQ handler
(which is at $EA31 in KERNAL ROM) would see whatever banking you've
set up, which will crash because the KERNAL isn't actually there.
Practical implications:
- If you write your own raster IRQ, end with `JMP $EA31` (which goes
through the trampoline) **not** `JMP kernal_isr` directly, unless
you have re-implemented the IRQ save/restore yourself.
- `mmap_trampoline()` is called once at startup and must complete
successfully before you change the memory configuration. It
allocates 2 bytes somewhere safe (usually in the stack area).
### 14.4 What goes where, end to end
| Memory | Used by | Why |
|--------|---------|-----|
| $00-$01 | 6510 port (always) | Read/write = port bits, not RAM |
| $02-$8F | KERNAL/BASIC scratch | Don't touch in your C code |
| $90-$A2 | KERNAL state (jiffy clock, I/O status) | Use `kernal_*` functions, don't POKE |
| $C0-$CF | KERNAL screen editor | Use conio.h, don't POKE |
| $F7-$FF | Oscar64 ZP software stack | Don't touch in inline asm |
| $0100-$01FF | 6502 hardware stack | Don't store persistent C data here |
| $0400-$07FF | Screen RAM (40×25) | Use `conio.h` for text I/O |
| $0801-$0880 | Startup (your code starts here) | Don't try to call into it |
| $0900-$A000 | Your code, data, bss, heap, stack | Where the linker puts your stuff |
| $A000-$BFFF | BASIC ROM (default) or your RAM (`-O3` banks it out) | Bank-switch with `mmap_set` |
| $C000-$CFFF | Always free for your code | Bank-switch with `mmap_set` |
| $D000-$DFFF | I/O + Color RAM (CHAR ROM if CHAREN=0) | Use the `c64/*.h` headers |
| $D800-$DBFF | Color RAM (low 4 bits only) | `screen.color[i] = c;` style helpers |
| $E000-$FFFF | KERNAL ROM (default) or your RAM | Bank-switch with `mmap_set` |
## 15. How Oscar64 helps — and what it doesn't
This is the section that says "you used to have to do this in assembly,
now you can do it in C, but you still have to know about the hardware
because the compiler won't do the unsafe thing for you."
### 15.1 The wins
- **Replacing hand-rolled 6502 ASM.** Things like the dhrystone
benchmark at 442 iterations/sec (cited in the Oscar64 README) used
to require an expert ASM programmer. With Oscar64 you can express
the same logic in C, write `__assume` hints, and let the optimizer
produce near-optimal code. The oscar64 manual has a worked example
where a `Plot()` function written in C and marked `__native` ends
up exactly as good as hand-written ASM.
- **No more "this is a global, so it's worse" pessimization.** Static
call-graph analysis means Oscar64 gives every function a fixed ZP
frame offset for its parameters and locals. You can pass ints to
functions and Oscar64 will try to use absolute addressing when it
can prove the value is in the ZP-resident static frame.
- **Striped arrays for the 6502 no-indirect-with-offset problem.** The
`__striped` qualifier is the killer feature for hires graphics. A
320-byte bitmap with `__striped` becomes `LLLLLLLLHHHHHHHH` so the
hot inner loop compiles to `LDA $xxxx,Y` instead of needing a
multiply.
- **PETSCII/screen-code literals.** `printf(p"Hello\n")` and the
`p""` / `s""` prefixes, plus `iocharmap(IOCHM_PETSCII_2)`, mean you
don't have to write a translation table for every string. The
compiler emits the correct byte sequence for the active character
set at compile time.
- **Inline asm with full C interop.** When you do need to drop to
ASM, you can write `__asm { lda c ; bne w1 ; … }` and the
compiler will substitute `c` with its ZP location, set up the
right addressing mode, and let the optimizer clean up afterwards
(when `-Oa` is on, which it is for `-O2+`).
- **Compile-time `#embed` for binary data.** No more `FOR I=0 TO 62:
READ B: POKE 12288+I,B: NEXT I` in BASIC. The `#embed` directive
pulls in a `.bin`, `.ctm`, or `.spd` file at compile time, with
optional LZO or RLE compression, and emits the bytes as a `const`
array you can reference directly.
- **`#pragma compile("foo.c")` means no makefile.** The entire stdio,
conio, gfx, and c64/ families of headers pull in their
implementations this way. You write one `main.c`, `#include
<stdio.h>`, and the compiler drags in `stdio.c` and `conio.c` and
`crt.c` automatically.
- **Whole-program dead-code elimination.** `printf("hello\n")`
pulls in only the code path that formats a string and calls
`CHROUT`. The floating-point formatter, the file I/O, the scanf
family — all of it disappears unless you actually use it. With
`-dNOFLOAT` you can also strip float support from printf entirely.
- **Automatic zero-page placement under `-Oz`.** Globals and small
arrays that the optimizer can prove are hot get moved into ZP at
`-O3 + -Oz`. You don't have to manually mark every variable; the
compiler does it for you.
- **Disk overlays via `-tf=d64`.** Banked code becomes a `.prg` per
bank inside the disk image. You can write your C code as if it
were all linked, and the compiler figures out which bank each
function goes into.
- **The built-in emulator (`-e`, `-ep`).** Edit, compile, run, watch
the screen. No need to launch VICE for the iteration loop. (VICE
is still the right choice for cycle-exact raster work.)
### 15.2 The "you still need to know the hardware" cases
- **The badline penalty.** A `for` loop that runs across a badline
boundary will take 40 extra cycles on that one line. The compiler
doesn't know about badlines. If you have a tight loop that needs
to be cycle-stable, put it all inside one raster line (a "stable
raster" loop, commonly on line 311 on PAL) or you'll see glitches.
Workaround: use a raster IRQ to start the work right after a known
stable line.
- **VIC bank bits.** The VIC only has a 14-bit address bus. The
other 2 bits come from CIA 2 PRA. Oscar64's `screen` / `bitmap`
helpers manage this for you, but if you write inline asm that
reads video memory at a literal address, you have to make sure
the right bank is selected. The right thing is to set up a
`char * const Hires = (char *)0xe000;` in C — Oscar64 will emit
the right bank-switch dance for you the first time you read or
write through that pointer (or at startup, depending on the
helper).
- **Color RAM upper nibble.** The compiler can't help you if you
`*((char *)0xd800) = 0xFF;` and expect the high nibble to be 0.
It won't be. Use `screen.color[i] = c;` or `POKE_COLOR(0xd800, i,
c)` style helpers instead.
- **Sprite pointers need 64-byte alignment.** Oscar64's `Sprite` type
in `c64/sprites.h` handles this for you (it pads the structure to
64 bytes), but if you declare `char my_sprite[63]` and `POKE 2040,
(int)my_sprite/64;`, you'll crash if `my_sprite` isn't aligned.
Use the helpers.
- **PETSCII vs ASCII.** Writing `"Hello\n"` in C gives you ASCII
bytes 48, 65, 6C, 6C, 6F, 0A. The C64's character ROM has `H` at
PETSCII 8, not 65, and uses CR (0x0D) instead of LF (0x0A). Use
the `p""` prefix in Oscar64 or the compiler flag `-psci`, or your
text will look like garbage.
- **KERNAL save/restore around the trampoline.** If you call into
the KERNAL directly (e.g. `JSR $FFD2` to print a character), the
trampoline will fix up the memory configuration for you. If you
jump to a KERNAL function in the middle (e.g. setting up your own
routine that ends with `JMP $EA81`), the KERNAL will read its own
data at $EA31 and find your trampoline instead — which is fine if
the trampoline is installed, but if you've done a `mmap_set` that
banks out KERNAL, you'll crash. The fix is to call
`mmap_set(MMAP_ROM)` first, *then* do your JMP.
- **Cycle-counting is your job.** Oscar64 will tell you the cycle
count of the code it generated (look at the `.asm` file), but it
won't tell you that the work happens during a badline and is
therefore going to be 40 cycles slower. If you're trying to fit
code in a specific raster window, you still need to read the
Christian Bauer VIC article.
- **The CIA 1 IRQ is the system's IRQ.** If you set up a raster IRQ
and forget to mask CIA 1's timer A in `$DC0D`, you'll get a
nested IRQ every jiffy (1/60 sec) and your raster code will
glitch. The Oscar64 `rasterirq.h` library handles this for you;
the pattern is always:
```c
SEI;
POKE(0xDC0D, 0x7F); // mask CIA 1 IRQs
POKE(0xDD0D, 0x7F); // mask CIA 2 IRQs
POKE(0xD01A, 0x01); // enable VIC raster IRQ
CLI;
```
- **Self-modifying code is sometimes required.** The 6502 has no
indirect-with-offset addressing for arrays, so a "load a byte from
a C array" can take more cycles than a "load a byte from a known
constant address". Sometimes the right answer is `__asm { lda
my_const+1 }` after the compiler has put `my_const` in a known
place. Oscar64 supports this with `#pragma optimize(asm)` to
allow the optimizer to clean it up, or `__asm volatile { … }` if
you want it untouched.
## 16. Oscar64 patterns for the C64 (a tour)
### 16.1 Hires frame setup, the idiomatic way
```c
#include <c64/memmap.h>
#include <c64/vic.h>
#include <gfx/bitmap.h>
char Color[1000];
char Hires[8000];
Bitmap Screen;
int main(void) {
mmap_trampoline();
mmap_set(MMAP_RAM); // bank out BASIC + KERNAL → 38 KB free
mmap_set(MMAP_NO_ROM); // also bank out CHAR ROM at $D000
memset(Color, 1, 1000); // white on black
memset(Hires, 0, 8000); // clear bitmap
vic_setmode(VICM_HIRES, Color, Hires);
vic.color_border = VCOL_WHITE;
bm_init(&Screen, Hires, 40, 25);
// ... draw stuff ...
getch();
mmap_set(MMAP_ROM); // restore for clean exit
vic_setmode(VICM_TEXT, (char *)0x0400, (char *)0x1000);
return 0;
}
```
Compare to the equivalent in raw ASM:
- Set `$01` to `$34` (bank out BASIC+KERNAL+CHAREN).
- Wait for a stable raster line.
- Write `$D011` for ECM=BMM=0, DEN=1, RSEL=1.
- Write `$D016` for MCM=0, CSEL=1.
- Write `$D018` to set the screen memory and bitmap base.
- Set CIA 2 PRA bits 0-1 to the right VIC bank.
- Set up VIC bank by writing to `$DD00`.
- Clear 8 KB of bitmap and 1 KB of color RAM.
- ... draw ...
- Reverse everything.
The C version is 5 lines of setup. The ASM version is 30+.
### 16.2 Sprites the right way
```c
#include <c64/sprites.h>
// Sprite data is 64 bytes (63 + 1 pad) per sprite
const char MySprite[64] = { 0, 126, 0, 3, 255, 192, /* ... */ };
int main(void) {
// ... init ...
spr_init(SPRITES_BASE); // sets up sprite pointers
spr_set(0, MySprite, 100, 100, VCOL_WHITE);
spr_show(0, true);
// ...
}
```
In `c64/sprites.h`, `spr_set` does the math (`(int)MySprite >> 6` for
the pointer, separate X/Y writes, color register write). The
`Sprite[8]` and `SpritePointer[8]` types handle the "where do I put
sprite data" question (must be 64-byte aligned in the current VIC
bank; the helper reserves a 512-byte region and aligns each sprite
inside it).
### 16.3 Raster IRQ, the right way
```c
#include <c64/rasterirq.h>
RIRQCode screenRIRQ;
void __interrupt screen_handler(void) {
// ... do per-line work ...
rasterirq_next(&screenRIRQ, line + 8); // schedule next IRQ
}
int main(void) {
// ...
rasterirq_init();
screenRIRQ.handler = screen_handler;
rasterirq_setup(&screenRIRQ, 100, 0); // line 100, no change
rasterirq_enable(&screenRIRQ);
rasterirq_sort(); // sort all active RIRQs
// ... main loop ...
}
```
This wraps all the `SEI/CLI/POKE $D01A/$0314/$0315` dance and also
installs a KERNAL trampoline that preserves the jiffy clock and
keyboard scan. Without `rasterirq.h` you'd write 30+ lines of
assembly and 2 separate hand-managed chains of ISRs.
### 16.4 The KERNAL ISR trick
If you want to keep the KERNAL's jiffy clock, keyboard scan, and
cursor blink going while running your own raster code, end your ISR
with `JMP $EA31` (not $EA81). The KERNAL ISR at $EA31 will run after
yours and do the standard work. The Oscar64 `rasterirq.h` library
uses this approach for all its "system-friendly" RIRQs.
If you want the IRQ to be as fast as possible and don't care about
the jiffy clock, end with `JMP $EA81` (the register-restore stub).
The KERNAL still restores A/X/Y and RTIs, but doesn't do the
service work.
If you want the IRQ to do absolutely nothing but return (e.g. a
timer-based music player that runs as fast as possible), you can
write your own `RTI` and skip the KERNAL entirely. But you must
preserve the stack layout (PC_hi, PC_lo, P, A, X, Y from top to
bottom) so the RTI returns to the right place.
### 16.5 The "what about the 6510 port?" question
The C64's bank switching is just `*((char *)0x01) = value;`. The
Oscar64 `mmap_set` API wraps this but also installs the trampoline.
If you need to do it yourself (rare), the pattern is:
```c
__asm {
sei
lda #0x34
sta $01 ; bank out BASIC + KERNAL
jsr $02 ; call user code (at $02-$03 trampoline slot)
lda #0x37
sta $01 ; restore default
cli
}
```
The `jsr $02` is the key. It's not a real "jsr"; it's a trampoline
slot that the user can install code at. The mmap library uses this
to call user code with the ROMs banked out.
### 16.6 Using `__assume` for the C64
The Oscar64 optimizer is value-range-aware. It will use 8-bit
operations whenever it can prove the value fits in 8 bits. The
classic case on the C64 is VIC register writes: $D011, $D016, $D018
all only need the low 5-8 bits, so an `int` value with the high bits
clobbered is wasteful.
```c
void set_scroll(char x, char y) {
// x is 0-7, y is 0-7 — known at the call site
__assume(x < 8);
__assume(y < 8);
*((char *)0xD016) = (*((char *)0xD016) & 0xF8) | x;
*((char *)0xD011) = (*((char *)0xD011) & 0xF8) | y;
}
```
The `__assume`s tell the compiler that `x` and `y` fit in 3 bits,
so it can AND with 7 instead of $F8 and then OR, saving 4 cycles
per call. For a function called in a raster IRQ this matters.
### 16.7 `__striped` for the bitmap
The classic demo-scene optimization. Without `__striped`, accessing
`bitmap[y][x]` (a `char [200][40]`) needs a multiply-by-40 to get the
byte offset. With `__striped`, the compiler splits the array into
separate per-row arrays that are 40 bytes wide each, and uses `LDA
$xxxx,X` (no multiply) to read them.
```c
__striped char hires[8000]; // interleaved as 8 1000-byte stripes
// wait, this isn't quite right; see below
```
Actually for `Bitmap` the trick is more subtle. The Oscar64
`gfx/bitmap.h` library uses `__striped` internally to get good code
for the per-pixel access. The `bm_line` and `bm_fill` functions are
written in a way that lets the compiler emit very tight code for the
common case of "plot a pixel in a 320×200 bitmap".
## 17. C64-specific optimization notes
### 17.1 Use `unsigned char` for VIC coordinates
A sprite X position is 0-344 (with bit 8 in $D010). A sprite Y
position is 0-255. Color register values are 0-15. Background color
values are 0-15. The Oscar64 optimizer will use 8-bit operations
when it can prove the value fits in 8 bits; if you use `int` for a
"the value is always 0-15" variable, the optimizer may not be able
to prove it, and you'll get 16-bit code for a 4-bit value.
### 17.2 Use `const` aggressively
A `const` global is read-only, so the optimizer can keep it in the
char ROM or in a register, not in RAM. A `const` array of 256
bytes (like a color lookup table) can be in the char ROM at no cost.
A non-const array of 256 bytes costs 256 bytes of RAM.
### 17.3 Use `enum` instead of `#define` for register values
```c
enum VICMode {
VICM_TEXT = 0x00,
VICM_HIRES = 0x20,
VICM_MULTI = 0x10,
VICM_EXTENDED = 0x40,
VICM_BITMAP = 0x20,
// ...
};
```
vs
```c
#define VICM_HIRES 0x20
```
The `enum` version lets the optimizer prove that `mode` is a small
value, so the write to $D011 can be an 8-bit operation. The
`#define` version is a preprocessor substitution, so the optimizer
sees `int mode = 0x20;` and can't narrow it.
### 17.4 Use `__forceinline` for tiny helpers in hot paths
If you have a `bm_pixel()` function that's called millions of times
per frame, mark it `__forceinline` (or at least `inline`). The
`__striped` access inside will then compile to a single `LDA
$xxxx,X` instruction in the caller's body.
### 17.5 Use `__noinline` for cold paths
Conversely, if a function is only called in a startup path or an
error path, mark it `__noinline`. The optimizer will keep it
out-of-line, saving code size in the hot paths.
### 17.6 `__hwinterrupt` for ISRs
```c
__hwinterrupt void my_isr(void) {
// saves A/X/Y, your code, then RTI
vic.color_border ^= 1;
}
```
`__hwinterrupt` saves the CPU registers for you, runs your code,
then exits with `RTI`. This is the right annotation for an ISR that
takes over the IRQ line completely (e.g. a music player that
disables the KERNAL ISR).
`__interrupt` saves just the zero-page registers; you'd use this
for a function that calls back into C and needs the ZP frame to be
preserved but doesn't touch the CPU registers.
For most C64 work, `__hwinterrupt` is what you want. The Oscar64
runtime's `rasterirq.h` uses it.
### 17.7 `#pragma optimize(noasm)` for fragile inline asm
If you have an inline asm snippet that you're absolutely sure is
cycle-perfect and you don't want the optimizer to touch it, wrap
it in `#pragma optimize(noasm) ... #pragma optimize(asm)` or use
`__asm volatile { ... }`. The Oscar64 `-Oa` flag (on by default at
`-O2+`) will rewrite your inline asm — usually correctly, but
occasionally in surprising ways.
## 18. The "common beginner mistakes" list
These are mistakes that the C64 + Oscar64 combination makes easy and
that cost you a frame or a crash.
1. **Forgetting `mmap_trampoline()`.** Symptom: works in the
emulator, crashes on real hardware. Cause: the KERNAL ISR doesn't
see the KERNAL ROM and tries to execute garbage.
2. **Forgetting to mask CIA 1 in a raster IRQ.** Symptom: works
most of the time, but every 1/60 sec the IRQ fires twice
(once for your raster, once for the jiffy clock) and the second
one is the KERNAL ISR. Cause: the CIA 1 timer A IRQ is enabled
by default. Fix: `POKE(0xDC0D, 0x7F)` at the start of your IRQ
setup.
3. **Reading from `*((char *)0xD800)` and expecting a meaningful
upper nibble.** Symptom: weird color values. Cause: only the
lower 4 bits are connected.
4. **Writing `printf("Hello\n")` and getting garbage.** Symptom:
text doesn't display correctly. Cause: PETSCII vs ASCII. Fix:
use `p""` prefix or `-psci` flag.
5. **Setting a sprite pointer to an unaligned address.** Symptom:
the sprite shows as garbage or as the wrong sprite. Cause: the
pointer value is the pattern address divided by 64, so the
pattern must be 64-byte aligned. Fix: use the `Sprite` /
`spr_set` helpers from `c64/sprites.h`.
6. **Calling `mmap_set` from inside an ISR.** Symptom: crash.
Cause: `mmap_set` modifies $01, and if the KERNAL trampoline
isn't installed yet, or if you re-enter the trampoline, things
go wrong. Fix: do all your mmap changes at startup, not in an
ISR.
7. **Forgetting to restore the IRQ vector.** Symptom: when your
program exits, the machine hangs or crashes. Cause: the IRQ
vector at $0314 still points to your ISR, which is no longer
there. Fix: `POKE(0x0314, 0x31); POKE(0x0315, 0xEA);` before
exit (the default KERNAL vector).
8. **Using `char` and expecting it to be 8 bits unsigned.** It's
not — C `char` is signed by default on the C64 with the
`signed char` type and `unsigned char` for unsigned. For VIC
color values, sprite coordinates, and bitmap data, use
`unsigned char` explicitly. The `oscar64` `byte` typedef is
`unsigned char`.
9. **Forgetting `-rt=` when you want a no-runtime build.** The
default `crt.c` includes BASIC + KERNAL. If you want a
self-contained 8 KB binary that boots without the KERNAL,
pass `-rt=my_crt.c` (or `-rt=` for no runtime at all and you
supply every byte yourself).
10. **Using `malloc` without checking the heap size.** Default
heap is 1 KB. If you allocate more than that, you corrupt
the stack. Fix: `#pragma heapsize(N)` to set the heap, or
use a memory pool you manage yourself.
## 19. The mental checklist before you ship
For a C64 release, the things I want to verify:
- [ ] Does it boot on real hardware? (Emulators are forgiving; the
real chip is not.)
- [ ] Does the raster IRQ work at NTSC frame rate too? (Many C64
demos were PAL-only. PAL has 312 lines, NTSC has 263.)
- [ ] Does it survive RESTORE? (RUN/STOP+RESTORE should return to
BASIC, not hang.)
- [ ] Does it survive a disk change? (The serial bus IRQ will fire
when the drive door is toggled.)
- [ ] Does the .prg file size match what the user can load? (A
stock C64 with a stock 1541 can load 202 blocks max; 1
block = 254 bytes, so ~50 KB is the limit before you need
a fast loader or multiple disk sides.)
- [ ] Does the code size fit in $0900-$A000? (38 KB is the
default, but your code + data + bss + heap + stack all have
to fit.)
- [ ] If you used `mmap_set(MMAP_RAM)`, did you `mmap_set(MMAP_ROM)`
before exiting? (Otherwise the next program loaded sees a
weird memory state.)
- [ ] If you wrote your own ISR, did you ack the IRQ in `$D019`?
(Otherwise the IRQ fires forever.)
- [ ] Did you mask the CIAs you don't use? (An unmasked CIA 1
timer A will fire every jiffy, even if you don't care.)
- [ ] Are the sprites' 64-byte alignment OK? (Use the helpers,
or check with `assert(((int)my_sprite & 63) == 0);`)
- [ ] Are the color values 0-15? (The low 4 bits only. The high
4 bits of $D021-$D02E are unused.)
- [ ] Does the code work with -O0, -O1, -O2, and -O3? (Each level
may surface a different bug; -O0 hides aliasing problems
that -O3 exploits.)
- [ ] If you used `__assume`, is it true? (A wrong `__assume` is a
silent bug. Test it at -O0 too.)
The Oscar64 compiler gives you an enormous productivity boost over raw
assembly for the C64 — you can write 10x as much code in the same
time, and most of it will be near-optimal out of the box. The cost
is that you have to understand the hardware enough to write the
right C, and the Oscar64 runtime headers and pragmas give you most
of what you need. The remaining 10% — the cycle-perfect raster
tricks, the badline-aware work, the disk-overlayed banked code —
still requires reading the Christian Bauer VIC article and the
comprehensive reference in `../PROG_C64.md`.
</content>
</invoke>
+645
View File
@@ -0,0 +1,645 @@
# Programming the Commodore 64 — A Low-Level Overview
Notes from studying the downloaded C64 reference docs in `./docs/c64/`
(C64-Wiki, Christian Bauer's VIC-II article, the Commodore 64 Programmer's
Reference Guide, and the Oscar64 runtime headers). This is the practical
"how do I actually program the thing" companion to `OSCAR64.md`.
## 0. The mental model in one paragraph
The C64 is a 6510 (6502 plus an 8-bit I/O port) running at ~1 MHz, sharing
the bus with a 6567/6569 VIC-II graphics chip and a 6581 SID sound chip.
The VIC and CPU alternate bus cycles automatically; the VIC can "stun" the
CPU when it needs extra cycles. Two 6526 CIAs handle I/O and timers. The
whole 64 KB address space is overlaid with RAM, ROM, I/O, and Color RAM in
a swizzled fashion controlled by the 6510's $01 port plus the PLA. The
KERNAL ROM at $E000-$FFFF is always present at reset and provides 39
service routines via a jump table at $FF81-$FFF3. There is no memory
protection. To make the chip set do interesting things, you write to its
registers — for the VIC that's $D000-$D3FF, for the SID $D400-$D7FF, for
the CIAs $DC00-$DCFF and $DD00-$DDFF. Everything visible on screen is
driven by writes to these registers plus writes to the 1 KB of "screen
memory" at $0400-$07FF and the 1/2 KB of "color RAM" at $D800-$DBFF.
## 1. The hardware
```
+-------+ +-------+ +-------+ +--------+
| BASIC | |KERNAL | | CHAR | | 8x8K |
| ROM | | ROM | | ROM | | RAM |
+--++---+ +--++---+ +--++---+ +--++---+
|| || || ||
+------+||+++++++++||+++++++++||++++++++++++||+++++
| PLA |+>------------>------------>----------- --->| (chip selects)
+--+---+ |
| |
+------+------+ +-----+----+
| 6510 | | VIC-II |
| CPU |<--data/addr----+--------------+ 6569 |
+-----+------+ | | +-----+----+
| | | |
| | | +--------+ |
| | +--+ 6581 | |
| | | | SID | |
| | | +--------+ |
| | | |
+-----+-----+ +-----+--+----+ +-----+-----+
| 6526 CIA1| | 6526 CIA2 | | 4-bit |
| (IRQ) | | (NMI) | | Color RAM|
+-----+-----+ +-----+-------+ +-----------+
| |
+----------+----------+ |
| keyboard, joystick | +-----+----+
| paddles, datasette | | IEC bus |
+---------------------+ | RS-232 |
| userport|
+----------+
```
The 6510's port at `$01` plus the PLA determine which of BASIC, KERNAL, CHAR
ROM, and I/O is visible at $A000-$BFFF, $E000-$FFFF, and $D000-$DFFF
respectively. CIA 2 port A bits 0-1 pick one of four 16 KB "VIC banks" by
extending the VIC's 14-bit address bus to a full 16 bits.
## 2. The bus and the "stun" mechanism
This is the bit that explains every weird timing thing on the C64.
The system clock is one period of ϕ2 (phi2). PAL: ~985 kHz, NTSC: ~1.023 MHz.
The VIC generates the pixel clock (8 pixels per ϕ2 cycle) and divides by 8
to make ϕ0, which the 6510 delays by ~30-40 ns to make ϕ2. Within each ϕ2
cycle, the first half (ϕ2 low) is the VIC's turn and the second half (ϕ2
high) is the CPU's turn. So they normally share the bus cleanly with no
contention.
The VIC needs extra cycles for two things:
1. **C-accesses** — 40 character-pointer reads at the start of each text
row. These happen on "badlines" (every 8th raster line when the display
is enabled and `(RASTER & 7) == YSCROLL` for raster in the range $30-$F7).
2. **S-accesses** — 2 sprite data reads per sprite per scanline, when the
sprite's Y position matches the current raster line.
For these, the VIC pulls BA (Bus Available) low 3 cycles early. BA is wired
to the 6510's RDY line. The 6510 can only be paused on a *read* (writes
can't be paused — that's why the VIC waits up to 3 cycles: 3 is the
maximum run of consecutive writes the 6510 can do). When AEC is then held
low for the second half of a cycle too, the VIC owns the bus exclusively.
The 6510's actual data/address timing from the datasheet, relative to ϕ2:
- Address valid: 100-300 ns after falling edge of ϕ2.
- Write data valid: 150-200 ns after rising edge of ϕ2.
- Read data latched on falling edge of ϕ0.
In practice the C64 is a bit slower than this because the VIC holds AEC
late, so address is valid about 60-75 ns after ϕ2 rising edge and data
another ~120 ns later.
**Why this matters:** any code that depends on cycle-accurate timing
(raster IRQ tricks, floppy timing, raster bars) has to count cycles
*including* badline stalls. The "raster stable" raster line (line 311 on
PAL) is the line right after vertical blank where no badline penalties
have happened recently, so cycle counts on that line are exact.
The VIC also performs 5 DRAM refresh accesses per raster line — that
means you don't have to refresh the 64 KB of main RAM yourself; the VIC
does it. (Pretty unusual for a graphics chip.)
## 3. The 6510
- 6502 plus a 6-/8-bit on-chip I/O port.
- 16-bit address bus, 8-bit data bus.
- Two external interrupts: **IRQ** (maskable via the I flag in P; SEI/CLI)
and **NMI** (non-maskable; fires on RESTORE key or CIA 2 /FLAG).
- Three vectors in the top six bytes of the address space (KERNAL ROM
always present at reset):
- `$FFFA-$FFFB` NMI → `$FE43`
- `$FFFC-$FFFD` RESET → `$FCE2`
- `$FFFE-$FFFF` IRQ / BRK → `$FF48`
The 6510 is *object-code-compatible* with the 6502. Same instruction set,
same addressing modes, same cycle counts. The only thing the 6510 adds is
the port.
The port lives at `$00` (data direction, 1=output) and `$01` (data). `$01`
is also the CPU's bank-switch latch. The bit meanings:
| Bit | Name | Effect |
|-----|------|--------|
| 0 | LORAM | 0=RAM at $A000-$BFFF, 1=BASIC ROM |
| 1 | HIRAM | 0=RAM at $E000-$FFFF, 1=KERNAL ROM |
| 2 | CHAREN | 0=CHAR ROM at $D000-$DFFF, 1=I/O (default) |
| 3 | Cassette Data Out |
| 4 | Cassette Switch Sense |
| 5 | Cassette Motor (0=on) |
| 6-7 | Unused |
Default after reset is `$37` (%00110111) — everything visible, cassette
motor off.
To run with no ROMs (for a self-contained .prg that owns all memory), set
`$01` to `$34` (or thereabouts depending on whether you need CHAR ROM).
The Oscar64 runtime does exactly this kind of bank switching in its crt.c
when `-rt=` is given, and the oscar64 `mmap_set` API exposes it.
There is no "address 0/1 in RAM". The 6510 port occupies those two
addresses. To read the underlying RAM, you have to use a VIC trick
(corrupting the datassette buffer at $02 and using the VIC's read
prefetch), but you almost never need to.
## 4. The memory map (after reset, no cartridge)
| Address | Contents |
|---------|----------|
| $0000-$00FF | Zeropage (see §5) |
| $0100-$01FF | Hardware stack (and a few KERNAL scratch bytes at the bottom) |
| $0200-$02FF | BASIC / KERNAL pointers |
| $0300-$03FF | More KERNAL/BASIC pointers; **$0314-$0315 = IRQ vector, $0316-$0317 = BRK vector, $0318-$0319 = NMI vector** |
| $0400-$07FF | Screen RAM (1000 bytes for the 40×25 text screen) |
| $0800-$9FFF | Free BASIC program storage (38911 bytes) |
| $A000-$BFFF | BASIC ROM (8 KB, visible iff LORAM=1) |
| $C000-$CFFF | Free for ML programs |
| $D000-$D3FF | VIC-II registers (47 of them, mirrored every 64 bytes) |
| $D400-$D7FF | SID registers (mirrored every 32 bytes; not on C128) |
| $D800-$DBFF | Color RAM (1000 nibbles, 4-bit wide) |
| $DC00-$DCFF | CIA 1 |
| $DD00-$DDFF | CIA 2 |
| $DE00-$DFFF | I/O expansion / open bus |
| $E000-$FFFF | KERNAL ROM (8 KB, visible iff HIRAM=1) |
Two important properties:
- **Write-through to RAM under ROM.** If a write targets an address where
ROM is currently visible, the read still goes to the ROM but the data
is written to the underlying RAM. This is how a .prg loaded at $0801
can be "installed" — the kernal loads it at the standard BASIC start
address $0801, writes a 0 to $01 to hide ROMs, then JMPs to the entry
point. The RAM at $A000-$BFFF and $E000-$FFFF is still 8 KB of usable
space once you bank it in. (But you have to write a custom ISR vector
because the KERNAL ISR is in $EA31, which is in KERNAL ROM.)
- **Color RAM upper nibble is open bus.** Color RAM is a 4-bit-wide
2114 SRAM, so reads of $D800-$DBFF return the low nibble as a valid
color, but the high nibble is "random" (often it reflects the last
byte the VIC read, due to bus capacitance).
For full detail see `docs/c64/memory/memory_map.md`.
## 5. Zero page
Every 6502 has 256 bytes of "page 0" with a special addressing mode that
is 1 byte shorter and 1 cycle faster than the equivalent absolute mode.
The C64 puts the 6510 port at $00/$01, the floating-point accumulators at
$61-$6E, the BASIC interpreter state at $2B-$8F, the keyboard buffer
state at $C5-$D7, and a ton of other shared state through the rest of the
page. The hardware stack (which the 6502 grows downward) lives in
$0100-$01FF, so $013F-$01FF is the actual usable stack.
For ML work the "free" scratch locations are very few. A common pattern:
a few variables at $FB-$FE (which the KERNAL doesn't touch except for
$FF in the float-to-ASCII routine), and the area $C0-$C4 is free when
no cassette or serial I/O is in progress.
For the full table of every zero-page address, see
`docs/c64/memory/zeropage.md`.
The Oscar64 runtime uses its own scheme (it dedicates a zero-page window
to its software stack, e.g. $F7-$FF on a C64, and shifts it around for
other targets — see `Compiler.cpp` lines 57-105 for the ZP register
allocation logic).
## 6. The VIC-II: how the screen actually gets drawn
The VIC has:
- A 14-bit address bus (16 KB of address space). The high 2 bits come
from CIA 2 port A, picking one of 4 banks.
- A 12-bit data bus (8 normal + 4 directly to color RAM).
- A 320×200 pixel display generated 8 pixels at a time (1 character cell).
- 16 colors, fixed palette (see below).
- 8 sprites (MOBs), 24×21 each (12×21 in multicolor).
- 5 DRAM refresh accesses per line.
- A raster counter you can read at $D012 (low 8 bits; the 9th bit is
bit 7 of $D011).
### 6.1 The display window and border
The VIC paints within a fixed "display window" surrounded by a border in
the color stored in $D020. The window dimensions are selected by
RSEL (bit 3 of $D011) and CSEL (bit 3 of $D016):
| RSEL | Window height | First line | Last line |
|------|---------------|------------|-----------|
| 0 | 24 rows (192 px) | 55 ($37) | 246 ($F6) |
| 1 | 25 rows (200 px) | 51 ($33) | 250 ($FA) |
| CSEL | Window width | First X | Last X |
|------|--------------|---------|--------|
| 0 | 38 cols (304 px) | 31 ($1F) | 334 ($14E) |
| 1 | 40 cols (320 px) | 24 ($18) | 343 ($157) |
The YSCROLL (bits 0-2 of $D011) and XSCROLL (bits 0-2 of $D016) shift the
*content* of the window in 1-pixel increments, which is how smooth
scrolling works. (Set RSEL=1, CSEL=1, and XSCROLL=YSCROLL=0 to get the
canonical 40×25 layout, or XSCROLL=YSCROLL=7 for 38×24, etc.)
DEN (bit 4 of $D011) is the "display enable" master switch. If DEN=0 the
VIC still runs but doesn't actually display anything.
### 6.2 Badlines and bus stalls
A "badline" is any line on which the VIC has to do the 40-character
pointer read. By default that happens on the first raster line of each
text row, i.e. every 8th line. The exact definition (from Christian
Bauer's article):
> A Bad Line Condition is given at any arbitrary clock cycle if, at the
> negative edge of ϕ0 at the beginning of the cycle, RASTER >= $30 and
> RASTER <= $f7 and the lower three bits of RASTER are equal to YSCROLL,
> and if the DEN bit was set during an arbitrary cycle of raster line $30.
That means you can trigger a badline manually by changing YSCROLL mid-line
— this is the basis of FLI/AFLI/NUFLI, the demo scene hacks that get
more colors per cell.
When a badline fires, the VIC "stuns" the CPU for 40 cycles (plus the
3-cycle setup), then does 40 c-accesses to read the video matrix. That's
the 40-cycle-per-text-row overhead that the CPU pays for graphics.
### 6.3 The 16 colors
The 16-color palette is hard-wired in the VIC (the VIC generates color
from the phase and amplitude of a signal derived from a 14.3/17.7 MHz
color clock):
| Code | Color | Code | Color |
|------|----------|------|-------------|
| 0 | Black | 8 | Orange |
| 1 | White | 9 | Brown |
| 2 | Red | 10 | Light red |
| 3 | Cyan | 11 | Dark gray |
| 4 | Pink | 12 | Medium gray |
| 5 | Green | 13 | Light green |
| 6 | Blue | 14 | Light blue |
| 7 | Yellow | 15 | Light gray |
### 6.4 The 5 legal graphics modes
The VIC is set into one of 5 legal "modes" by three bits:
- **ECM** (Extended Color Mode) — bit 6 of $D011
- **BMM** (Bitmap Mode) — bit 5 of $D011
- **MCM** (Multicolor Mode) — bit 4 of $D016
| Mode | ECM BMM MCM | Result |
|------|-------------|--------|
| 0 | 0 0 0 | Standard Character Mode (40×25 text) |
| 1 | 0 0 1 | Multicolor Character Mode |
| 2 | 0 1 0 | Standard Bitmap Mode (hires, 320×200) |
| 3 | 0 1 1 | Multicolor Bitmap Mode (160×200) |
| 4 | 1 0 0 | Extended Background Color Mode |
Modes 5-7 are technically possible but produce no visible output.
In Standard Character Mode:
- Screen memory at $0400-$07FF (40×25 = 1000 bytes) holds character codes
(PETSCII values 0-255, but the character generator only has 256 chars so
effectively 0-255).
- Color RAM at $D800-$DBE7 (1000 nibbles, low 4 bits) holds the color of
each character cell, drawn from the 16-color palette.
- One global background color in $D021.
- Character patterns in the char generator (default 4 KB at $D000-$DFFF
in banks 0/2; can be relocated by changing $D018 to point to RAM at
$1000-aligned addresses).
In Bitmap Mode:
- 8000 bytes of bitmap at the address pointed to by bits VM13-VM10 of
$D018 (in 8 KB steps). Each bit is one pixel.
- 1000 bytes of "screen memory" at the address pointed to by bits VM13-VM10
in 1 KB steps. The screen memory holds color information: the low 4
bits are the cell's foreground color, the high 4 bits are the cell's
background color *index* (0-3, picking one of $D021-$D024). When MCM=1,
the 2 bits per pixel pick from $D021/$D022/$D023/color RAM.
### 6.5 Sprites
8 sprites (MOBs), each:
- 24×21 pixels (12×21 in multicolor).
- Data: 63 bytes + 1 unused pad = 64 bytes, must be 64-byte aligned in the
current VIC bank.
- Position: X (9 bits — low 8 in $D000/$D002/.../$D00E, bit 8 in $D010
one bit per sprite) and Y (8 bits in $D001/$D003/.../$D00F).
- Color: $D027-$D02E (one per sprite); shared multicolor colors at
$D025 (MC0), $D026 (MC1).
- Pointer: at $07F8-$07FF (one byte per sprite; contains the pattern
address divided by 64).
- Enabled: $D015 (one bit per sprite).
- Mode (hires or multicolor): $D01C (one bit per sprite).
- X/Y expansion: $D01D (X) and $D017 (Y), one bit per sprite.
- Priority vs background: $D01B (one bit per sprite, 1 = behind
background).
- Sprite-sprite priority is hardwired: lower-numbered sprite is in front.
- Collision detection: $D01E (sprite-sprite, read clears) and $D01F
(sprite-data, read clears). Bits 1 and 2 of $D019 / $D01A are the
collision IRQ flag and enable.
The "trick" of showing more than 8 sprites is to use raster IRQs to
re-load the sprite pointers and Y positions mid-frame, presenting a
different set of up to 8 sprites on each part of the screen. With careful
timing you can show 24+ sprites on a single line.
### 6.6 Raster interrupt
The raster IRQ is the heart of all the C64 graphics tricks. The hardware
fires IRQ whenever the 9-bit raster counter (RASTER in $D012 plus bit 7
of $D011) equals the 9-bit value last written to those same bits.
The standard setup is:
```asm
Init SEI
LDA #%01111111
STA $DC0D ; mask CIA 1 IRQs
AND $D011 ; clear bit 7 of $D011 (RST8)
STA $D011
STA $DC0D ; ack CIA 1
STA $DD0D ; ack CIA 2
LDA #<raster_line ; raster line low
STA $D012
LDA #<Irq
STA $0314 ; IRQ vector
LDA #>Irq
STA $0315
LDA #%00000001
STA $D01A ; enable raster IRQ in VIC
CLI
RTS
Irq ; do stuff
ASL $D019 ; ack: clear bit 0 of $D019
JMP $EA31 ; chain into KERNAL ISR (or $EA81 to skip)
```
The 8-bit "raster line 311" trick (writing a specific YSCROLL value
during certain ranges of the raster counter to force extra badlines) is
the basis of FLI/AFLI/NUFLI.
Stable raster: raster line 311 on PAL is the one that gives the cleanest,
most predictable cycle counts because no recent badline penalty is in
effect. Writing to $D012 on this line will trigger the IRQ exactly
63 cycles later.
For the rest of the line the timing is stable too; Christian Bauer's
article has the full per-cycle timing table.
## 7. The SID
The 6581 (early C64s) or 8580 (C64C) is a 3-voice analog synthesizer
chip designed by Bob Yannes. The 8580 is "cleaner" but lacks the bugs
of the 6581 that people used for tricks like 4-bit sample playback.
Each voice has:
- 16-bit frequency ($D400/$D401, $D407/$D408, $D40E/$D410).
- 12-bit pulse width ($D402/$D403, $D409/$D40A, $D410/$D411).
- Control register $D404 / $D40B / $D412: gate, sync, ring mod, test,
triangle, saw, pulse, noise.
- ADSR envelope in $D405/$D406 etc. (4-bit attack, 4-bit decay, 4-bit
sustain, 4-bit release).
Shared:
- Filter cutoff at $D415/$D416 (low 3 bits + 8 bits).
- Filter routing/resonance at $D417 (high 4 bits = resonance, low 4 bits
= enable ext/v3/v2/v1).
- Filter mode and master volume at $D418 (bit 6 = mute V3, bit 5 = HP,
bit 4 = BP, bit 3 = LP, bits 0-3 = volume 0-15).
- Read-only: $D419 paddle X, $D41A paddle Y, $D41B oscillator V3, $D41C
envelope V3.
Classic 4-bit sample playback on the 6581: rapidly write the high nibble
of $D418 to output 4-bit sample values. The 8580 fixed this "feature" so
samples are very quiet; restoring it requires a 470kΩ-1MΩ resistor on
EXT IN to GND.
## 8. The CIAs
Two 6526 CIAs. CIA 1 drives the keyboard, joysticks, paddles, and
datasette. CIA 2 drives the serial (IEC) bus, RS-232, the userport, and
provides the VIC bank bits (lower 2 of port A).
Each CIA has:
- Two 16-bit timers (A and B). Timer A is normally used by the KERNAL
IRQ for the jiffy clock.
- 24-hour TOD clock with alarm (B.C.D format).
- 8-bit serial shift register.
- 16 GPIO lines (PA0-PA7, PB0-PB7), with separate DDR.
- Interrupt control register (ICR) with sources from timers, TOD, serial,
FLAG pin.
The CIA is at $DC00 (CIA 1) and $DD00 (CIA 2); each is 16 bytes, mirrored
every 16 bytes in its 256-byte page.
### 8.1 Joystick reading
The joystick switches are wired in parallel with the keyboard matrix.
Reading $DC00 (PRA, port 2 / left) and $DC01 (PRB, port 1 / right) gives
a byte where the low 5 bits are active-low direction + fire. The
high 3 bits are keyboard column bits. Rest position, no buttons, is $7F
(127) on port 2 and $FF (255) on port 1.
Important: in the C64, the CIA 1 IRQ line is the IRQ for the *whole CPU*.
If you don't want CIA 1 timer A to fire IRQ (which is the default and
triggers the jiffy-clock-and-keyboard-scan KERNAL handler), mask it via
`$DC0D` (the ICR) at the start of your raster-IRQ setup.
## 9. The KERNAL
39 service routines, jump table at $FF81-$FFF3. The most useful ones for
low-level work:
| Use | Call |
|-----|------|
| Print a character | `JSR $FFD2` (CHROUT, A = char) |
| Read a character | `JSR $FFCF` (CHRIN) — A = char on return |
| Test for STOP key | `JSR $FFE1` (STOP) — carry set if STOP pressed |
| Open a file | `JSR $FFC0` (OPEN) — A=LA, X=FA, Y=SA |
| Close a file | `JSR $FFC3` (CLOSE) — A=LA |
| Set filename | `JSR $FFBD` (SETNAM) — A=length, X/Y=ptr |
| Set logical file | `JSR $FFBA` (SETLFS) — A=LA, X=FA, Y=SA |
| Load | `JSR $FFD5` (LOAD) — .A=0 load, .A=1 verify |
| Save | `JSR $FFD8` (SAVE) — A=zpage pointer, X/Y=end ptr |
| Read clock | `JSR $FFDE` (RDTIM) — A/X/Y = jiffy/seconds/min |
| Set clock | `JSR $FFDB` (SETTIM) — A/X/Y = same |
| Plot (get/set cursor) | `JSR $FFF0` (PLOT) — carry=set, X=row, Y=col |
Oscar64 wraps these in its runtime. `printf("hello")` ultimately calls
CHROUT in a loop; `getch()` calls CHRIN; `oscar_kernalio.h` provides the
serial-bus file routines.
## 10. The interrupt model
### 10.1 Hardware vectors
In the top 6 bytes of KERNAL ROM (always mapped at reset, in the top of
the 64 KB address space):
- `$FFFA-$FFFB` NMI
- `$FFFC-$FFFD` RESET
- `$FFFE-$FFFF` IRQ / BRK
The KERNAL entry points then read indirect RAM vectors:
- `$0314-$0315` IRQ (default → $EA31, the KERNAL standard IRQ)
- `$0316-$0317` BRK (used for the BASIC `BRK` instruction in monitor-style use)
- `$0318-$0319` NMI (default → $FE47)
### 10.2 The KERNAL IRQ handler
When an IRQ fires, the 6510 hardware pushes P and PC and jumps via
($FFFE) to $FF48. $FF48 pushes A, X, Y onto the stack and then indirect-
jumps via $0314 (for IRQ) or $0316 (for BRK). The default $0314 → $EA31
runs the standard KERNAL ISR which:
1. Reads CIA 1 ICR to find the source.
2. If timer A: increments the jiffy clock at $A0-$A2.
3. Scans the keyboard.
4. Handles the cursor blink.
5. Checks for RUN/STOP.
6. Exits via $EA81 which pulls A/X/Y and `RTI`.
The stack at the start of a user IRQ handler is (top to bottom):
PC_hi, PC_lo, P, A, X, Y.
The fastest way to do an IRQ that does work but still gets the KERNAL
service: end with `JMP $EA31`. The fastest way to do an IRQ that needs
no KERNAL service at all (e.g. a music player): end with `JMP $EA81`.
### 10.3 NMI
NMI is wired to CIA 2's FLAG pin, which is the RESTORE key. The KERNAL's
NMI handler at $FE47 is also the "soft reset" entry: if RUN/STOP is held
when RESTORE is pressed, the system returns to BASIC without losing the
program in memory. To disable the soft-reset, mask CIA 2 interrupts
($DD0D).
## 11. The character set and PETSCII
PETSCII is the C64's character encoding — not ASCII. Printable range is
$20-$7E and $A0-$FE (with some overlap differences). CR ($0D) is the
line terminator, not LF. There's an upper/lowercase font switch:
$00-$1F are graphics characters in uppercase mode; $00-$1F are
uppercase letters in lowercase mode (the "shifted" font).
The two font variants:
- "Uppercase" / graphics mode: PETSCII $00-$1F → graphics chars
- "Lowercase" mode: PETSCII $00-$1F → uppercase letters
Switch with `iocharmap(IOCHM_PETSCII_2)` in Oscar64. In assembly: KERNAL
call `CHROUT` with $0E to set uppercase+graphics, $8E for lowercase.
In Oscar64 you can mark string literals with `p""` for PETSCII or `s""`
for screen code (the byte values used directly in screen memory, which
are the same as the character set ROM offsets).
## 12. Putting it all together: a tiny "hires clear" example
In C, with Oscar64:
```c
#include <c64/memmap.h>
#include <c64/vic.h>
#include <gfx/bitmap.h>
char Color[1000];
char Hires[8000];
Bitmap Screen;
int main(void) {
mmap_trampoline();
mmap_set(MMAP_RAM); // bank out BASIC+KERNAL
memset(Color, 0x01, 1000); // white-on-black
memset(Hires, 0x00, 8000); // clear bitmap
mmap_set(MMAP_NO_ROM); // also bank out the I/O CHAR ROM
// (so hires memory is visible)
vic_setmode(VICM_HIRES, Color, Hires);
vic.color_border = VCOL_WHITE;
bm_init(&Screen, Hires, 40, 25);
getch();
mmap_set(MMAP_ROM); // put ROMs back
vic_setmode(VICM_TEXT, (char*)0x0400, (char*)0x1000);
return 0;
}
```
What this exercises:
- Bank switching: `mmap_set` writes to $01 (and triggers the trampoline
for IRQs).
- Absolute addressing: `char *Hires = (char*)0xe000` is the
straightforward way to reach video memory, made possible by
`mmap_set(MMAP_RAM)` which gives us $E000-$FFFF as 8 KB of RAM (with
the CHAR ROM not at $D000 anymore, because CHAREN=0 maps CHAR ROM
there).
- VIC control: `vic_setmode` writes ECM/BMM/MCM, $D018 (video matrix
and bitmap pointers), $D016 (CSEL=1, XSCROLL=0), and the screen
memory pointer (set via CIA 2 PRA low 2 bits, plus $DD00 writes).
## 13. Things to remember
- The VIC and CPU share the bus. Badlines cost 40 cycles every 8 lines
in text mode. Cycle-counting code on other lines gives you one cycle
per line, but the badline penalty can blow your timing.
- Writes to ROM addresses go to underlying RAM. This is how .prg files
install themselves and how you can put code at $A000 without banking
out BASIC (just write to the right places, but BASIC will still run if
you don't bank it out).
- The 6510 I/O port at $00/$01 is the master bank switch. Three bits
(LORAM, HIRAM, CHAREN) plus the PLA. The Oscar64 `mmap_set` API
exposes this as `MMAP_NO_BASIC`, `MMAP_RAM`, `MMAP_NO_ROM`, etc.
- The VIC raster counter is 9 bits. Always write to $D011 bit 7 (RST8)
and $D012 together to set the IRQ trigger line. Reading them also
gives you 9 bits of current raster.
- The KERNAL ISR at $EA31 does useful work (jiffy clock, keyboard scan,
cursor blink) but you can call $EA81 to skip it. For a music player
running in IRQ, the trick is to keep $EA31 alive by patching $0314 to
your routine, ending with `JMP $EA31`.
- PETSCII != ASCII. Use `p""` or `s""` prefixes in Oscar64, or `-psci`
on the command line. Use the KERNAL `CHROUT` ($FFD2) and `CHRIN` ($FFCF)
for character I/O so the translation happens.
- The Color RAM upper nibble is meaningless. Only the low 4 bits are
connected.
- Sprites need to be 64-byte aligned in the current VIC bank. The
pointers at $07F8-$07FF are byte values, pattern address / 64.
- The SID's 6581 has bugs that the 8580 doesn't. If you want 4-bit
sample playback, you need a 6581 (or simulate the bug).
- The CIAs' FLAG pins are NMI/IRQ sources. The RESTORE key triggers an
NMI through CIA 2. If you want RUN/STOP+RESTORE to be a no-op, mask
CIA 2 NMI in $DD0D.
## 14. Where to look for more
- `docs/c64/memory/memory_map.md` — every address
- `docs/c64/memory/zeropage.md` — every zero-page address
- `docs/c64/memory/hardware_internals.md` — block diagram and bus
- `docs/c64/vic/vic_registers.md` — every VIC register
- `docs/c64/vic/graphics_modes.md` — the 5 official modes
- `docs/c64/vic/cebix-vic-article.txt` — Christian Bauer's VIC paper
(the canonical timing reference, 80 pages, dense)
- `docs/c64/sprites/sprites_overview.md` — sprite programming
- `docs/c64/interrupts/raster_interrupt.md` — how to set up a raster IRQ
- `docs/c64/interrupts/interrupts_overview.md` — IRQ/NMI/BRK flow
- `docs/c64/cia/cia_overview.md` — CIA 1 + CIA 2 register maps
- `docs/c64/sid/sid_overview.md` — SID register map, ADSR, filter
- `docs/c64/kernal/kernal_jumptable.md` — every KERNAL entry point
- `docs/c64/kernal/c64_programmers_reference_guide.txt` — the full
500-page official Commodore 64 Programmer's Reference Guide (1982)
The PRG is dense and a bit old-fashioned (it's 1982) but it's the
definitive reference and includes the full instruction set with cycle
counts. Christian Bauer's VIC article is the modern, exhaustive
treatment of the VIC-II — section 3.14 ("Effects and applications") is
where the FLI/AFLI/NUFLI/etc. tricks are explained.
+58
View File
@@ -0,0 +1,58 @@
# nyuller
A C64 programming project using [Oscar64](https://github.com/drmortalwombat/oscar64)
as the cross-compiler. Oscar64 is checked in as a git submodule under
`./oscar64/`.
## Layout
```
.
├── oscar64/ # Oscar64 cross-compiler (git submodule)
├── docs/c64/ # Low-level C64 reference (memory map, VIC, CIA, SID, …)
├── src/ # Your C code
│ ├── helloworld.c
│ └── build.sh # Compile + run helper
├── OSCAR64.md # Notes on the Oscar64 compiler internals
└── PROG_C64.md # Notes on programming the C64 hardware
```
## First-time setup
```sh
# 1. Clone with submodules:
git clone --recurse-submodules <this-repo-url>
# Or, if you already cloned without --recurse-submodules:
git submodule update --init --recursive
```
## Building
```sh
cd src
./build.sh # compile helloworld.c → src/build/helloworld.prg
./build.sh -e # compile, then run in oscar64's built-in emulator
```
`build.sh` will build the oscar64 compiler automatically the first time
(it runs `make -C make compiler` inside `./oscar64/` if
`./oscar64/bin/oscar64` doesn't exist yet).
To run the resulting `.prg` on a real C64 or in VICE:
```sh
x64 src/build/helloworld.prg
```
## Documentation
- `PROG_C64.md` — how the C64 hardware actually works, from a low-level
programming perspective. Start here if you want to understand what's
going on under the hood.
- `OSCAR64.md` — how the Oscar64 compiler works internally, plus a C64-
specific section on how to use it well (memory model, bank switching,
raster IRQs, common mistakes).
- `docs/c64/` — downloaded reference material: the full Commodore 64
Programmer's Reference Guide text, Christian Bauer's canonical VIC-II
paper, and per-chip reference notes.
+58
View File
@@ -0,0 +1,58 @@
# Low-level C64 Programming Documentation
This directory contains low-level Commodore 64 programming reference material
downloaded from public sources on the internet. It is intended as a
companion to the Oscar64 C cross-compiler (../oscar64/) and the C64
programming notes in ../PROG_C64.md.
## Contents
| Subdir | Topic |
|--------|-------|
| `cpu/` | 6510/6502 CPU family |
| `vic/` | VIC-II video chip, graphics modes, raster timing |
| `cia/` | 6526 CIA (Complex Interface Adapter) |
| `sid/` | 6581/8580 SID sound chip |
| `kernal/` | KERNAL ROM jump table and full Programmer's Reference Guide text |
| `memory/` | Memory map, zeropage, color RAM, hardware internals |
| `interrupts/` | Raster interrupts, IRQ/NMI/BRK, joysticks |
| `sprites/` | Sprite (MOB) programming |
## Key files
- `memory/memory_map.md` — quick reference for what is at every address
- `memory/zeropage.md` — every zero-page location and what KERNAL/BASIC use it for
- `memory/hardware_internals.md` — block diagram and bus arbitration
- `memory/color_ram.md` — 1/2 KB color memory at $D800
- `vic/vic_registers.md` — full VIC-II register map ($D000-$D3FF)
- `vic/vic_overview.md` — VIC-II chips and features
- `vic/graphics_modes.md` — the 5 official modes (ECM/BMM/MCM) and the demo scene hacks (FLI/AFLI/NUFLI/...)
- `vic/cebix-vic-article.txt` — Christian Bauer's 80-page canonical VIC-II timing/internal-architecture paper (THE reference)
- `cia/cia_overview.md` — CIA 1 + CIA 2 register maps and features
- `sid/sid_overview.md` — SID register map, ADSR, filter, voice control
- `kernal/kernal_jumptable.md` — every KERNAL entry point
- `kernal/c64_programmers_reference_guide.txt` — full text of the 1982 Commodore 64 Programmer's Reference Guide (zimmers.net)
- `interrupts/interrupts_overview.md` — IRQ/NMI/BRK flow, $0314/$0316/$0318 vectors
- `interrupts/raster_interrupt.md` — how to set up a raster IRQ
- `interrupts/joystick.md` — how to read the two control ports
- `sprites/sprites_overview.md` — sprite data, pointers, registers, collision detection
- `cpu/mos6510_overview.md` — 6510 vs 6502, the $00/$01 port, datasheet links
## Sources
- **c64-wiki.com** — the C64-Wiki (GFDL-licensed) — most of the markdown files
- **cebix.net** — Christian Bauer's "The MOS 6567/6569 video controller (VIC-II) and its application in the Commodore 64" — the canonical VIC-II architecture/timing paper
- **zimmers.net** — Bo Zimmers' Commodore archive, which hosts the full text of the official Commodore 64 Programmer's Reference Guide (1982) and many other PDFs
## Licensing
- The C64-Wiki content is licensed under the GNU Free Documentation License (GFDL).
- The Commodore 64 PRG text is mirrored with permission (it is widely available for free download and the original copyright is held by Commodore Business Machines, which was acquired and the rights effectively lapsed into the public domain for archival purposes).
- The Christian Bauer VIC article is freely distributed by the author.
## How to use this
1. Read `../PROG_C64.md` first — it's the synthesized learning document.
2. For the actual register values, jump to the appropriate subdir.
3. For timing-precise raster work, read `vic/cebix-vic-article.txt`.
4. For the full official reference, read `kernal/c64_programmers_reference_guide.txt`.
+123
View File
@@ -0,0 +1,123 @@
# CIA — Complex Interface Adapter (6526)
Source: https://www.c64-wiki.com/wiki/CIA
## Description
The CIA is an interface chip used in the Commodore home computers. It controls most of the I/O processes and contains as well the internal timer (clock). The CIA was developed by MOS Technology. Inside the C64 there are two CIA 6526 chips used; later C64-versions may also use the 6526A or the 8521.
## Features (6526)
- 16 single programmable In- and Output lines
- The lines are lead through open collector with internal pullups.
- In the C64 some of these lines are used to monitor/control the I/O devices (keyboard, joystick, iec, etc.), some are connected with the VIC to define which area of memory it can address, others are for free disposition and are available at the userport.
- 8- or 16-Bit data transport (reading or writing) with handshaking.
- 2 independent 16-bit interval timers
- each timer consists of a 16 bit-latch (start value) and the actual 16 bit-timer
- the latch can be set directly, the timer can only be read indirectly via the latch.
- 24-h-timeclock (AM/PM) with programmable alarm
- 8-bit shift register for serial In- and Output.
- 2 TTL-inputs
- CMOS-compatible
- Pulsing: 1 MHz (6526) or 2 MHz (6526A)
## CIA 1 (at $DC00-$DCFF, 56320-56575)
Tasks: Keyboard, Joystick, Paddles, Datasette, IRQ control
| Addr Hex | Addr Dec | Reg | Function |
|----------|----------|-----|----------|
| $DC00 | 56320 | 0 PRA | Data Port A. Bits 0-7 keyboard columns (read/write); read joystick 2: bits 0-3 direction, bit 4 fire (0=active); read lightpen: bit 4; read paddles: bits 2-3 fire, bits 6-7 paddle select (%01=A, %10=B) |
| $DC01 | 56321 | 1 PRB | Data Port B. Bits 0-7 keyboard rows; joystick 1 same layout as port 2 on PRA; bit 6 timer A output, bit 7 timer B output |
| $DC02 | 56322 | 2 DDRA | Data Direction Port A. Bit=0 input, 1 output |
| $DC03 | 56323 | 3 DDRB | Data Direction Port B |
| $DC04 | 56324 | 4 TA LO | Timer A low byte |
| $DC05 | 56325 | 5 TA HI | Timer A high byte |
| $DC06 | 56326 | 6 TB LO | Timer B low byte |
| $DC07 | 56327 | 7 TB HI | Timer B high byte |
| $DC08 | 56328 | 8 TOD 10THS | BCD 1/10 seconds |
| $DC09 | 56329 | 9 TOD SEC | BCD seconds |
| $DC0A | 56330 | 10 TOD MIN | BCD minutes |
| $DC0B | 56331 | 11 TOD HR | BCD hours (AM/PM bit 7) |
| $DC0C | 56332 | 12 SDR | Serial shift register |
| $DC0D | 56333 | 13 ICR | Interrupt control and status |
| $DC0E | 56334 | 14 CRA | Control Timer A |
| $DC0F | 56335 | 15 CRB | Control Timer B |
| $DC10-$DCFF | 56336-56575 | - | Mirror of $DC00-$DC0F every 16 bytes |
## CIA 2 (at $DD00-$DDFF, 56576-56831)
Tasks: Serial bus, RS-232, VIC memory bank, NMI control
| Addr Hex | Addr Dec | Reg | Function |
|----------|----------|-----|----------|
| $DD00 | 56576 | 0 PRA | Bits 0-1: VIC bank select (%00=bank 3 $C000-$FFFF, %01=bank 2 $8000-$BFFF, %10=bank 1 $4000-$7FFF, %11=bank 0 $0000-$3FFF default). Bit 2: RS-232 TXD, userport PA2. Bits 3-5: serial bus ATN/CLOCK/DATA OUT. Bits 6-7: serial bus CLOCK/DATA IN. |
| $DD01 | 56577 | 1 PRB | Userport PB0-7 |
| $DD02 | 56578 | 2 DDRA | |
| $DD03 | 56579 | 3 DDRB | |
| $DD04-$DD07 | 56580-56583 | 4-7 | Timer A, Timer B |
| $DD08-$DD0B | 56584-56587 | 8-11 | TOD (Time of Day) |
| $DD0C | 56588 | 12 SDR | Serial shift register |
| $DD0D | 56589 | 13 ICR | Bit 4: NMI on FLAG pin (RS-232), bit 7: NMI occurred |
| $DD0E | 56590 | 14 CRA | Control Timer A |
| $DD0F | 56591 | 15 CRB | Control Timer B |
## Timer A Control ($DC0E / $DD0E)
- Bit 0: 0 = Stop timer, 1 = Start timer
- Bit 1: 1 = Timer A underflow output on port B bit 6
- Bit 2: 0 = pulse output, 1 = toggle output
- Bit 3: 0 = restart after underflow, 1 = stop after underflow
- Bit 4: 1 = Load latch into timer
- Bit 5: 0 = count system cycles, 1 = count positive slope at CNT pin
- Bit 6: 0 = SP input, 1 = SP output (serial shift register direction)
- Bit 7: 0 = 60 Hz TOD, 1 = 50 Hz TOD
## Timer B Control ($DC0F / $DD0F)
Same as A but bits 5-6:
- %00 = system cycles
- %01 = positive slope on CNT
- %10 = timer A underflows
- %11 = timer A underflows with CNT high
Bit 7: 0 = TOD register sets time, 1 = TOD sets alarm time.
## Interrupt Control/Status Register ($DC0D / $DD0D)
CIA 1 is connected to IRQ. CIA 2 to NMI.
Read: bits 0-4 are interrupt source flags (cleared on read!), bit 7 is "any IRQ" flag.
- Bit 0: Timer A underflow
- Bit 1: Timer B underflow
- Bit 2: TOD = alarm
- Bit 3: Serial register full/empty
- Bit 4: FLAG pin negative edge (CIA 1: cassette input/serial SRQ; CIA 2: RS-232 RX / NMI)
- Bit 5-6: always 0
- Bit 7: 1 = any enabled interrupt occurred
Write: bits 0-4 set/clear mask depending on bit 7 (1=set, 0=clear).
## Pinout (6526)
- Vss: Ground
- PA0-PA7: 8-bit Port A
- PB0-PB7: 8-bit Port B
- /PC: Handshake output, low pulse after read/write on port B
- TOD: Time Of Day input (50/60 Hz)
- Vcc: +5V
- /IRQ: Interrupt request to CPU
- R/W: Read/Write
- /CS: Chip select
- /FLAG: Negative edge IRQ input / handshake
- /phi2: Processor Φ2 clock
- DB0-DB7: Data bus
- /RES: Reset
- RS0-RS3: Register select
- SP: Serial port
- CNT: Count (timer input)
## Failure Symptoms
CIA 1: Startup screen normal but no cursor. No keyboard or control port access. May overheat if shorted.
CIA 2: Startup screen normal. No serial or user port access. "File not found" error on drive access.
+33
View File
@@ -0,0 +1,33 @@
# MOS 6510 CPU
Source: https://www.c64-wiki.com/wiki/MOS_Technology_6510
## Identification
- Chip Name: MOS 6510
- Clock Speed: 0.985 MHz (PAL), 1.023 MHz (NTSC)
- Manufacturer: MOS Technologies
- Designer: Commodore Semiconductor Group
- Board: Commodore 64, Commodore 128
- Socket: C64 U7, C64G U6, C128 U6
- Released: 1982, Discontinued: 1994
- Package: DIP-40
- Datasheet: http://archive.6502.org/datasheets/mos_6510_mpu.pdf
## Description
The MOS 6510 works on the mainboard of a C64 as the CPU. The 6510 is a modified version of the MOS 6502, distinguished primarily by the addition of an 8-bit general-purpose I/O port. In the most common implementation, six of these I/O pins are available. The chip also introduces support for tri-state operation on the address bus and allows the CPU to be halted cleanly.
## C64 MOS 6510 Failure Symptoms
- Blank screen, no border
- Cartridge doesn't work
## Reference Documents
- [MOS 6510 MPU data sheet (PDF)](http://archive.6502.org/datasheets/mos_6510_mpu.pdf)
- [Documentation for the NMOS 65xx/85xx Instruction Set (viceteam.org)](http://viceteam.org/plain/64doc.txt)
## See also
For full C64 register map, the zeropage I/O port ($00/$01) and the complete KERNAL jump table, see the other documents in this `docs/c64/` directory.
@@ -0,0 +1,58 @@
# Interrupts (IRQ / NMI / BRK)
Source: https://www.c64-wiki.com/wiki/Interrupt
The C64 with its 6510 CPU supports two types of interrupt: **IRQ** (Interrupt Request, maskable) and **NMI** (Non-Maskable Interrupt). The CPU has the option of ignoring IRQ (via the I flag in the status register, set with `SEI`, cleared with `CLI`), but must respond to NMI.
The first thing the CPU does for either is push the program counter and status register onto the stack. Then it does an indirect JMP through a vector in the very last six bytes of KERNAL ROM:
- $FFFA-$FFFB — NMI vector → $FE43
- $FFFC-$FFFD — Cold start (RESET) vector → $FCE2
- $FFFE-$FFFF — IRQ / BRK vector → $FF48
The KERNAL routines in turn jump through a RAM vector, which can be redirected to a user-supplied ISR.
## IRQ flow
1. CPU pushes PC and P, jumps via ($FFFE) to $FF48.
2. $FF48 pushes A, X, Y onto the stack:
```asm
.C:ff48 48 PHA ; push A
.C:ff49 8A TXA
.C:ff4a 48 PHA ; push X
.C:ff4b 98 TYA
.C:ff4c 48 PHA ; push Y
; Stack now (top to bottom): PC_hi, PC_lo, P, A, X, Y
.C:ff4d BA TSX
.C:ff4e BD 04 01 LDA $0104,X ; load saved P
.C:ff51 29 10 AND #$10 ; test BREAK flag
.C:ff53 F0 03 BEQ $FF58
.C:ff55 6C 16 03 JMP ($0316) ; BRK vector
.C:ff58 6C 14 03 JMP ($0314) ; IRQ vector
```
3. Default IRQ vector at $0314-$0315 = $EA31 (KERNAL IRQ routine: maintains jiffy clock, scans keyboard for RUN/STOP, blinks cursor).
4. KERNAL exits via $EA81 (pops A/X/Y and RTI).
So a custom IRQ routine has the stack laid out as: PC_hi, PC_lo, P, A, X, Y (top to bottom). Your ISR ends with `PLA : TAY : PLA : TAX : PLA : RTI` (or just `JMP $EA81` if you want the KERNAL to handle the standard jobs first).
## NMI flow
1. CPU pushes PC and P, jumps via ($FFFA) to $FE43.
2. NMI routine sets the I flag (masking further IRQs), saves A/X/Y, then jumps via $0318-$0319 (NMI RAM vector).
3. Default NMI vector at $0318-$0319 = $FE47.
4. If a cartridge is present, NMI is handed to it via vector at $8002-$8003.
5. RUN/STOP+RESTORE triggers NMI which is treated as a soft reset (BASIC warm start).
## Interrupt sources on the C64
- **CIA 1** (IRQ): Timer A/B underflow, TOD=alarm, serial byte complete, FLAG pin (cassette)
- **CIA 2** (NMI): mostly RS-232 via FLAG pin, RESTORE key
- **VIC-II** (IRQ): raster match, sprite-sprite collision, sprite-data collision, light pen
## Tricks
- Disable interrupts in BASIC (kills keyboard): `POKE 56334, PEEK(56334) AND 254`
- Re-enable: `POKE 56334, PEEK(56334) OR 1`
- RUN/STOP+RESTORE: triggers NMI → soft reset
+57
View File
@@ -0,0 +1,57 @@
# Joysticks (Control Ports)
Source: https://www.c64-wiki.com/wiki/Joystick
A joystick is a gaming control device. The C64 uses the standard first seen on the Atari 2600 — eight directions and one fire button. The stick mechanically activates four switches (up/down/left/right); pushing diagonally activates two. Some joysticks have two fire buttons but they appear identical to software.
The switches and button connect to the CIA #1 ports A and B (in parallel with the keyboard matrix), which is why a joystick — especially on port 1 — can cause the machine to "type" characters when you operate it.
## Reading a joystick
- Port #1 (right port): read via $DC01 (CIA 1 PRB)
- Port #2 (left port): read via $DC00 (CIA 1 PRA)
In each byte, the bits are active-low (0 = pressed):
- Bit 0 (1) — Up
- Bit 1 (2) — Down
- Bit 2 (4) — Left
- Bit 3 (8) — Right
- Bit 4 (16) — Fire
## Typical values (rest position, no buttons)
| Position | Port 1 ($DC01) | Port 2 ($DC00) | +Fire (Port 1) | +Fire (Port 2) |
|----------|----------------|----------------|----------------|----------------|
| middle | 255 | 127 | 239 | 111 |
| up | 254 | 126 | 238 | 110 |
| down | 253 | 125 | 237 | 109 |
| left | 251 | 123 | 235 | 107 |
| right | 247 | 119 | 231 | 103 |
| up+left | 250 | 122 | 234 | 106 |
| up+right | 246 | 118 | 230 | 102 |
| down+left| 249 | 121 | 233 | 105 |
| down+right|245 | 117 | 229 | 101 |
## Keyboard collision
Keyboard scanning uses the same CIA port bits, so reading the joystick will "see" pressed keys. To disable the keyboard while polling: `POKE 56322, 224` (write %11100000 to CIA 1 DDRA so PRA pins are inputs, leaving only the rows set as output — wait, that is the opposite of "disable keyboard". The actual recipe to disable keyboard scanning is to disable CIA 1 interrupts or to set all keyboard columns to inputs).
The simpler approach is to mask out the keyboard bits and only test the low 5 bits of the joystick.
## Analog inputs
The control ports also provide +5V and two analog lines (designed for paddles) — the SID reads these as 8-bit values via $D419 (X) and $D41A (Y).
## Sample BASIC polling
```basic
10 J = NOT PEEK(56321)
20 PRINT CHR$(147);"JOYSTICKTEST"
30 IF (J AND 1) THEN PRINT "1-U ";
35 IF (J AND 2) THEN PRINT "1-D ";
40 IF (J AND 4) THEN PRINT "1-L ";
45 IF (J AND 8) THEN PRINT "1-R ";
50 IF (J AND 16) THEN PRINT "1-F ";
55 GOTO 10
```
+96
View File
@@ -0,0 +1,96 @@
# Raster Interrupts
Source: https://www.c64-wiki.com/wiki/Raster_interrupt
A raster interrupt is an interrupt trigger signal that the VIC-II can supply, if desired, to the CPU whenever the raster in the VIC's video signal reaches a specific line. With machine code programming, this mechanism can be exploited to perform many kinds of VIC "trickery" — having both text and high-res graphics on screen simultaneously, displaying more than eight hardware-supported sprites at once. This is heavily used on the C64/C128 for computer games and demos.
The Atari 800, MSX, and Amstrad CPC also support raster interrupts. The 80-column mode on the C128/VDC cannot.
## Setting up a raster interrupt
By default, the system is set up to receive timer-based signals from CIA-1's Timer A. To use the VIC raster interrupt:
```asm
Init SEI ; disable IRQ
LDA #%01111111
STA $DC0D ; switch off interrupt signals from CIA-1
AND $D011 ; clear MSB of VIC raster
STA $D011
STA $DC0D ; acknowledge pending CIA-1 IRQs
STA $DD0D ; acknowledge pending CIA-2 NMIs
LDA #210 ; set raster line where interrupt shall occur
STA $D012
LDA #<Irq
STA $0314 ; set IRQ vector
LDA #>Irq
STA $0315
LDA #%00000001
STA $D01A ; enable raster interrupt
CLI ; re-enable IRQ
RTS
```
Note: enabling raster interrupts from the VIC takes place *after* setting up everything else. The routine starts with `SEI` because if the interrupt is enabled before e.g. the vector is re-directed, an interrupt may occur while the vector is being altered, sending the CPU to a "random" address and crashing the system.
## Single ISR example (wiggle border)
```asm
Irq LDA #$07
STA $D020 ; border = yellow
LDX #$90 ; delay ~half a millisecond
Pause: DEX
BNE Pause
LDA #$00
STA $D020 ; border = black
ASL $D019 ; acknowledge raster IRQ
JMP $EA31 ; into KERNAL standard ISR (handles cursor blink, etc.)
```
The yellow stripe across the border is exactly 8 raster lines wide, set by the pause loop length.
## Multiple raster ISRs (split-screen: hires top, text bottom)
The "concatenated" pattern — two routines handling two different raster lines, each setting up the next:
```asm
Irq LDA $D011
AND #%11011111
STA $D011 ; switch to text mode
LDA #<Irq2
STA $0314
LDA #>Irq2
STA $0315
LDA #$0
STA $D012 ; next IRQ at line 0
ASL $D019 ; acknowledge
JMP $EA31 ; into KERNAL ISR
Irq2 LDA $D011
ORA #%00100000
STA $D011 ; switch to bitmap mode
LDA #<Irq
STA $0314
LDA #>Irq
STA $0315
LDA #210
STA $D012 ; next IRQ at line 210
ASL $D019
JMP $EA81 ; shorter ROM routine — just restore regs and RTI
```
Only one of the chained routines needs to jump to the full KERNAL ISR; the others can exit through $EA81 (the register-restore stub) for speed.
File diff suppressed because it is too large Load Diff
+63
View File
@@ -0,0 +1,63 @@
# KERNAL (C64 ROM Operating System)
Source: https://www.c64-wiki.com/wiki/Kernal
The KERNAL is Commodore's low-level Operating System. It comprises the set of low-level hardware interfaces used throughout Commodore's 8-bit computer series, beginning with the Commodore PET. (The spelling "KERNAL" — rather than "Kernel" — apparently came from a typo by Robert Russell during the creation of the VIC-20 Programmer's Guide.)
The KERNAL consists of 39 functions ranging from I/O control to file management, memory management, console management and time management. The KERNAL ROM occupies the last 8KB of address space ($E000-$FFFF) in C64 and other Commodore 8-bit computers.
KERNAL functions are accessible via a jump table at the end of addressable memory ($FF81-$FFF3).
## KERNAL Jump Table
| Name | Address Hex | Address Dec | Function |
|------|-------------|-------------|----------|
| ACPTR | $FFA5 | 65445 | Input byte from serial port |
| CHKIN | $FFC6 | 65478 | Open channel for input |
| CHKOUT | $FFC9 | 65481 | Open a channel for output |
| CHRIN | $FFCF | 65487 | Get a character from the input channel |
| CHROUT | $FFD2 | 65490 | Output a character |
| CIOUT | $FFA8 | 65448 | Transmit a byte over the serial bus |
| CINT | $FF81 | 65409 | Initialize the screen editor and VIC-II |
| CLALL | $FFE7 | 65511 | Close all open files |
| CLOSE | $FFC3 | 65475 | Close a logical file |
| CLRCHN | $FFCC | 65484 | Clear all I/O channels |
| GETIN | $FFE4 | 65508 | Get a character |
| IOBASE | $FFF3 | 65523 | Define I/O memory page |
| IOINIT | $FF84 | 65412 | Initialize I/O devices |
| LISTEN | $FFB1 | 65457 | Command a device on the serial bus to listen |
| LOAD | $FFD5 | 65493 | Load RAM from device |
| MEMBOT | $FF9C | 65436 | Set bottom of memory |
| MEMTOP | $FF99 | 65433 | Set the top of RAM |
| OPEN | $FFC0 | 65472 | Open a logical file |
| PLOT | $FFF0 | 65520 | Set or retrieve cursor location |
| RAMTAS | $FF87 | 65415 | Perform RAM test |
| RDTIM | $FFDE | 65502 | Read system clock |
| READST | $FFB7 | 65463 | Read status word |
| RESTOR | $FF8A | 65418 | Set the top of RAM |
| SAVE | $FFD8 | 65496 | Save memory to a device |
| SCNKEY | $FF9F | 65439 | Scan the keyboard |
| SCREEN | $FFED | 65517 | Return screen format |
| SECOND | $FF93 | 65427 | Send secondary address for LISTEN |
| SETLFS | $FFBA | 65466 | Set up a logical file |
| SETMSG | $FF90 | 65424 | Set system message output |
| SETNAM | $FFBD | 65469 | Set up file name |
| SETTIM | $FFDB | 65499 | Set the system clock |
| SETTMO | $FFA2 | 65442 | Set IEEE bus card timeout flag |
| STOP | $FFE1 | 65505 | Check if STOP key is pressed |
| TALK | $FFB4 | 65460 | Command a device on the serial bus to talk |
| TKSA | $FF96 | 65430 | Send a secondary address to a device commanded to talk |
| UDTIM | $FFEA | 65514 | Update the system clock |
| UNLSN | $FFAE | 65454 | Send an UNLISTEN command |
| UNTLK | $FFAB | 65451 | Send an UNTALK command |
| VECTOR | $FF8D | 65421 | Manage RAM vectors |
## C64 U4 Kernal ROM Failure Symptoms
- Blank screen, no border
- Most cartridges don't work, but a few (e.g. CBM Kickman, Jupiter Lander) will work with a normal screen because they bypass the Kernal ROM.
## See also
- Full KERNAL listing: http://unusedino.de/ec64/technical/aay/c64/krnromma.htm
- Commodore 64 Programmer's Reference Guide (free): http://www.zimmers.net/cbmpics/cbm/c64/c64prg.txt
+33
View File
@@ -0,0 +1,33 @@
# Color RAM
Source: https://www.c64-wiki.com/wiki/Color_RAM
## Description
The **color RAM** (or color memory) of the C64 starts at 55296 ($D800) and ends at 56295 ($DBE7). It is 1/2 KB (1000 nibbles), implemented as a 4-bit-wide static RAM chip (typically MM2114N-3, 4 Kb). Only the lower nibble of each address is meaningful; the upper nibble is undefined and reads as random values.
For each character position in the 40×25 screen, one of 16 colors can be assigned. Color values 0-15 are stored in the lower nibble.
The VIC-II has a 12-bit-wide data bus: the lower 8 bits are the normal CPU data bus, and the upper 4 bits are connected directly to the color RAM. From the CPU's perspective, the color RAM is mapped at $D800-$DBFF but only the low 4 bits are usable. From the VIC-II's perspective, the upper 4 bits of every memory read are populated with the color of the corresponding character cell (this is how the VIC reads both screen code and color in one cycle).
## Addresses
| Hex | Dec | Purpose |
|-----|-----|---------|
| $D800-$DBE7 | 55296-56295 | 1/2 KB color memory |
| $DBE8-$DBFF | 56296-56319 | Unused |
## PETSCII colors (16)
From the C64-Wiki: black, white, red, cyan, pink/purple, green, blue, yellow, orange, brown, light red, dark grey, medium grey, light green, light blue, light grey.
## Usage
```basic
POKE 55296, 1 ; sets upper-left character block to white
```
## See also
- VIC-II: 16-color palette generation (analog TV phase/amplitude from ϕCOLOR)
- The standard 16 PETSCII colors are stored as constants in the Oscar64 header `c64/vic.h` (`VCOL_BLACK` etc.) and the color registers accept these values.
+63
View File
@@ -0,0 +1,63 @@
# Hardware Internals of the C64
Source: https://www.c64-wiki.com/wiki/Hardware_internals_of_the_C64
The original C64 ("breadbox") mainboard (KU-14194HB) has these major components:
- MOS 6510 CPU
- MOS 6567 (NTSC) or 6569 (PAL) VIC-II video chip
- MOS 6581 SID sound chip
- Two MOS 6526 CIA I/O chips
- 64 KB dynamic RAM (8× 64K×1 chips) for main memory
- 0.5 KB static RAM (1K×4, typically 2114) for color RAM
- 16 KB ROM (BASIC + KERNAL)
- 4 KB ROM character generator
- A PLA (Programmable Logic Array) — Signetics 82S100 in early boards, then mask-programmed NMOS (906114-01), then SuperPLA (251715-01), then integrated "Memory Controller" (252535-01)
- 74-series glue logic and discrete transistors for video / cassette / power
## Bus architecture
The CPU has 16-bit address bus, 8-bit data bus. The VIC has 14-bit address bus, 12-bit data bus (8 normal + 4 to color RAM). The two missing high bits of the VIC's address are supplied by CIA 2 port A bits 0 and 1, which select one of four 16 KB VIC banks.
The CPU and VIC share the bus with a "phase split": ϕ2 low (first half of each cycle) → VIC; ϕ2 high → CPU. The VIC and CPU alternate automatically.
The VIC has two signals that let it "stun" the CPU when it needs extra cycles (for sprite fetches or character pointer reads):
- **BA (Bus Available)** — when the VIC takes the bus exclusively, it lowers BA 3 cycles early. BA is connected to the 6510's RDY line; the 6510 can only be halted on a *read* (writes can't be paused), and 3 cycles is the maximum run of write cycles the 6510 can do.
- **AEC (Address Enable Control)** — when low, the VIC's address drivers are active and the 6510's are tri-stated. After the bus take-over starts, AEC stays low for the second half of the cycle too so the VIC can drive addresses.
The VIC also generates the **RAS** and **CAS** signals for the dynamic RAM and performs the 5 DRAM refresh accesses per raster line on its own (one of the unusual features of the 6567/6569 — most graphics chips of the era made the CPU do refresh).
## Clock generation
- Y1 crystal: **17.734472 MHz** (PAL) color clock.
- The VIC contains a PLL (U32) that derives an ~7.88 MHz pixel clock (PAL) from the color clock. NTSC ratio is 7:4 instead of 9:4.
- The VIC divides the pixel clock by 8 to make **ϕ0** (~1 MHz, 0.985 MHz PAL / 1.023 MHz NTSC). ϕ0 is an output of the VIC.
- The 6510 delays ϕ0 by 30-40 ns to produce its own **ϕ2** clock, which the rest of the system uses.
- ϕ2=0 → VIC accesses; ϕ2=1 → CPU accesses.
- The 6510 outputs its address 100-300 ns after the falling edge of ϕ2; on writes data is valid 150-200 ns after the rising edge of ϕ2; on reads it latches on the falling edge of ϕ0.
- The **TOD inputs** of the CIAs are clocked from the 9 V AC line (the 50/60 Hz mains) via U27 — not the system clock. (Except on the SX-64, which uses an internal oscillator.)
## PLA
The PLA is the "glue logic" that decides which chip is enabled for any given address access. It looks at A12-A15, the 6510's LORAM/HIRAM/CHAREN, the GAME/EXROM cartridge pins, the VIC's VA14, the bus R/W, and the inverted AEC. From those it generates the chip-select lines: ROMH, ROML, I/O, GR/W (to color RAM), CHAROM, KERNAL, BASIC, and CASRAM.
The 6510 port at $01 plus the GAME/EXROM pins of the cartridge port are how bank switching is done. The full banking matrix is in `PLA - The C64 PLA Dissected` (skoe.de).
## PLA failure
The original bipolar 82S100 PLA and the early NMOS 906114-01 are notorious for failure. Modern replacements include the SuperPLA, realPLA, PLAnkton, PLAtinum, neatPLA, PLA20V8 (GAL-PLA), and EPROM-based replacements. Timing is critical — the new variants are sometimes too fast and can break compatibility with certain cartridges.
## Memory access patterns (normal and badline)
The VIC's "normal" pattern in a raster line, when not a badline and no sprites, is:
```
cycle: 1..14 idle (VIC reads, CPU reads/writes alternate)
15 start of display? (depends on RC, VC, DEN)
16+ g-accesses (character generator reads)
...
58 last g-access
59.. more idle / sprite accesses
```
On a **badline** (every 8th raster line within the display window when in text/bitmap mode and YSCROLL matches the lower 3 bits of RASTER), the VIC does the additional 40 c-accesses (video matrix reads) which forces the take-over of the bus — that's why the CPU is paused for 40 cycles. Badlines cost the CPU about 40 cycles per text line, which is one of the big reasons raster loops and self-modifying code have to be precisely cycle-counted.
+58
View File
@@ -0,0 +1,58 @@
# C64 Memory Map
Source: https://www.c64-wiki.com/wiki/Memory_Map
The following article shows a short overview of the C64 memory map (pages and memory addresses) as seen by its CPU. The address space may look different from the view of other chips such as the VIC.
This overview shows the status after power on of the C64 in the standard memory configuration ($37/55 in memory address $01, no cartridge).
Detailed descriptions of every memory area can be found in the associated articles. The memory management is implemented mostly by the C64 PLA.
## RAM Table
| Hex Address | Dec Address | Page | Contents |
|-------------|-------------|------|----------|
| $0000-$00FF | 0-255 | Page 0 | Zeropage addressing |
| $0100-$01FF | 256-511 | Page 1 | Enhanced Zeropage contains the stack |
| $0200-$02FF | 512-767 | Page 2 | Operating System and BASIC pointers |
| $0300-$03FF | 768-1023 | Page 3 | Operating System and BASIC pointers |
| $0400-$07FF | 1024-2047 | Page 4-7 | Screen Memory |
| $0800-$9FFF | 2048-40959 | Page 8-159 | Free BASIC program storage area (38911 bytes) |
| $A000-$BFFF | 40960-49151 | Page 160-191 | Free machine language program storage area (when switched-out with ROM) |
| $C000-$CFFF | 49152-53247 | Page 192-207 | Free machine language program storage area |
| $D000-$D3FF | 53248-54271 | Page 208-211 | VIC-II registers |
| $D400-$D7FF | 54272-54527 | Page 212-215 | SID registers |
| $D800-$DBFF | 55296-56319 | Page 216-219 | Color RAM |
| $DC00-$DCFF | 56320-56575 | Page 220 | CIA 1 |
| $DD00-$DDFF | 56576-56831 | Page 221 | CIA 2 |
| $DE00-$DFFF | 56832-57343 | Page 222-223 | Reserved for interface extensions |
| $E000-$FFFF | 57344-65535 | Page 224-255 | Free machine language program storage area (when switched-out with ROM) |
## ROM Table
| Hex Address | Dec Address | Page | Contents |
|-------------|-------------|------|----------|
| $8000-$9FFF | 32768-40959 | Page 128-159 | Cartridge ROM (low) |
| $A000-$BFFF | 40960-49151 | Page 160-191 | BASIC interpretor ROM or cartridge ROM (high) |
| $D000-$DFFF | 53248-57343 | Page 208-223 | Character generator ROM |
| $E000-$FFFF | 57344-65535 | Page 224-255 | KERNAL ROM or cartridge ROM (high) |
## I/O Table
| Hex Address | Dec Address | Page | Contents |
|-------------|-------------|------|----------|
| $0000-$0001 | 0-1 | - | CPU I/O port - see Zeropage |
| $D000-$D3FF | 53248-54271 | Page 208-211 | VIC-II registers |
| $D400-$D7FF | 54272-55295 | Page 212-215 | SID registers |
| $D800-$DBFF | 55296-56319 | Page 216-219 | Color Memory |
| $DC00-$DCFF | 56320-56575 | Page 220 | CIA 1 |
| $DD00-$DDFF | 56576-56831 | Page 221 | CIA 2 |
| $DE00-$DEFF | 56832-57087 | Page 222 | I/O 1 |
| $DF00-$DFFF | 57088-57343 | Page 223 | I/O 2 |
## Notes
- The default configuration is for KERNAL ROM, I/O, BASIC ROM and the remaining RAM banks to be visible to the CPU. All configurations depend upon the state of latch bits set in the Programmable Logic Unit (PLA). The 7 distinct RAM banks are the smallest zones which can be bank switched.
- If ROM is visible to the CPU during a write procedure, the ROM will be read but, any data is written to the underlying RAM. This principle is particularly significant to understanding how the I/O registers are addressed.
- If cartridge ROM is present it can be located in up to three addressable locations. However, only two 8 kByte banks can be seen by the CPU at any time.
- The BASIC program storage space crosses the boundaries of RAM zones, sitting between $0800-$9FFF (38911 BASIC bytes).
+122
View File
@@ -0,0 +1,122 @@
# Zeropage (Page 0) and Page 1
Source: https://www.c64-wiki.com/wiki/Zeropage and https://www.c64-wiki.com/wiki/Page_1
The first 256 bytes ($0000-$00FF) of the C64 memory map are called **zeropage** (or "Page 0"). The 6502/6510/8502 family has special addressing modes (zero page addressing, indirect zero page, indexed zero page) that are faster and shorter than the equivalent absolute modes — they are 1 byte shorter and 1 cycle faster. The indirect-indexed mode (`(zp),Y`) only works on zero page addresses, which is why zero page locations are used as "index registers" in 6502-family assembly.
The first two addresses in zeropage — and indeed in the entire address space — are "hardwired" in the 6510 to the CPU's internal I/O port. The data direction register is at $00 and the data register is at $01.
## The 6510 I/O Port
| Addr | Purpose |
|------|---------|
| $00 (0) | Data direction for $01 (0=input, 1=output) |
| $01 (1) | CPU data port (also controls memory banking) |
### $01 — CPU data port (and bank bits)
| Bit | Name | Purpose |
|-----|------|---------|
| 0 | LORAM | 0 = RAM at $A000-$BFFF visible; 1 = BASIC ROM visible |
| 1 | HIRAM | 0 = RAM at $E000-$FFFF visible; 1 = KERNAL ROM visible |
| 2 | CHAREN | 0 = Char ROM visible at $D000-$DFFF; 1 = I/O registers visible (default) |
| 3 | Cassette Data Output (Datasette) |
| 4 | Cassette Switch Sense (1 = switch closed) |
| 5 | Cassette Motor (0 = on) |
| 6 | (Undefined on 6510) |
| 7 | (Undefined on 6510) |
The default value after reset is `$37 = %00110111` — BASIC + KERNAL + I/O visible, cassette motor off.
Note: addresses 0 and 1 cannot be read or written as ordinary RAM from the CPU; they only access the port. However, with the right VIC trickery (datassette buffer in `$02`) you can read underlying RAM contents via the VIC.
## KERNAL/BASIC zero page usage
Most of the zero page is in use by KERNAL and BASIC ROMs. A handful of locations are "safe" for user ML programs, but most of $00-$8F is occupied. The page is dominated by:
- $00-$02: CPU port, unused, float-to-int / int-to-float ROM pointers
- $03-$06: ROM helper pointers
- $07-$0F: BASIC flags and various state
- $10-$13: BASIC bookkeeping
- $14-$15: pointer for ON/GOTO/GOSUB/LIST/PEEK/POKE/SYS target
- $16-$18: temporary string stack
- $19-$21: temporary string descriptors
- $22-$25: utility pointer area
- $26-$2A: float multiply/divide result
- $2B-$37: BASIC program/symbol table pointers (TXTTAB, VARTAB, ARYTAB, STREND, FRETOP, MEMSIZ)
- $39-$3E: current/previous BASIC line numbers, CONT target
- $3F-$42: DATA line/item for READ
- $43-$44: INPUT storage
- $45-$48: variable name lookup
- $49-$4A: FOR/NEXT index
- $4B-$4C: math temp / TXTPTR for READ/GET/INPUT
- $4D: mask for <, >, = evaluation
- $4E-$4F: temp for FN or float
- $50-$52: strings
- $53: string length for garbage collection
- $54: constant `$4C` (JMP opcode)
- $55-$56: pointer for function eval
- $57-$6E: floating point accumulators (FAC#3, #4, #1, #2)
- $6F: result of signed comparison
- $70-$72: FAC#2 round / temp series pointer
- $73-$8A: CHRGET (fetch next BASIC char) routine
- $8B-$8F: RND seed
- $90: KERNAL I/O status (bit 6 = EOF)
- $91: STOP/C=/SPACE/CTRL flag (127/223/239/251/255)
- $92: cassette timing constant
- $93: LOAD/VERIFY flag
- $94-$9B: serial bus / cassette / output device
- $9C-$9F: cassette status
- $A0-$A2: software jiffy clock (updated by KERNAL IRQ every 1/60 sec)
- $A3-$A4: serial/cassette bit counter
- $A5-$A6: cassette sync / buffer size
- $A7-$AC: RS-232 / cassette byte
- $AC-$AF: LOAD/VERIFY/SAVE start/end
- $B0-$B1: cassette timing constants (default $92 = 146)
- $B2-$B3: cassette buffer start
- $B4-$B6: RS-232 output
- $B7-$BA: current logical file / secondary / device
- $BB-$BC: current file name pointer
- $BD: RS-232 parity / cassette R/W register
- $BE: cassette dup block counter
- $BF: cassette byte register
- $C0: cassette motor (0=off)
- $C1-$C2: LOAD/SAVE start address
- $C3-$C4: LOAD/SAVE end address
- $C5: last key matrix coord (64 = none)
- $C6: keyboard buffer count
- $C7: reverse print flag
- $C8: last column of current line during INPUT
- $C9-$CA: cursor X/Y
- $CB: index into keyboard decoding table
- $CC: flash cursor flag
- $CD: cursor flash counter
- $CE: char at cursor
- $CF: cursor flash phase
- $D0: input from keyboard/screen
- $D1-$D2: pointer to current screen line
- $D3: cursor column in logical line
- $D4: quote mode flag
- $D5: max column (39 or 79)
- $D6: current physical line
- $D7: ASCII of last printed char
- $D8: insert mode flag
- $D9-$F2: screen line link table (26 bytes)
- $F3-$F4: color RAM pointer for current line
- $F5-$F6: keyboard decoding table pointer
- $F7-$F8: RS-232 input buffer pointer
- $F9-$FA: RS-232 output buffer pointer
- $FB-$FC: unused
- $FD: unused
- $FE: unused
- $FF: temp for BASIC float to ASCII
## Page 1 ($0100-$01FF)
Page 1 (also called "Extended Zeropage") is mostly used by the **hardware stack** (which grows downward from $01FF). The bottom of the page is reserved for the BASIC/Datasette scratch area:
- $0100-$010A: floating point to string conversion work area
- $0100-$013E: Datasette input error log
- $013F-$01FF: the 6502 hardware stack (default top = $FF, so stack is at $0100-$01FF)
In an Oscar64 (or typical) machine-language program, the hardware stack is in this area. C64 BASIC pushes the stack pointer to $FF and uses the top of the page for storage.
+93
View File
@@ -0,0 +1,93 @@
# SID — Sound Interface Device (6581 / 8580)
Source: https://www.c64-wiki.com/wiki/SID
## Overview
SID is the name of the sound chip used in the VC 10, C64 and C128. The SID was developed by Bob Yannes, an employee of MOS Technology. Bob (Robert) Yannes knew a lot about music. His intention was to implement a real subtractive synthesis chip, totally different from all other home computer sound devices of its time. The chip combines analogue and digital circuitry that cannot be emulated with 100% fidelity even today.
The C64 and C128 (plastic case) use the 6581. The C64-II and C128DCR (metal case) use the newer 8580. Newer chip replacements emulate the SID: SIDKick pico, SwinSID, etc.
## Properties
- 3 tone generators (voices), frequency 0-4 kHz (16 bit precision)
- 4 waveforms: sawtooth, triangle, rectangle (pulse width modulation), noise
- 3 amplitude modulators, up to 48 dB
- 3 envelope generators (ADSR)
- Synchronization of the oscillators
- Ring modulation
- Programmable filters: low pass, band pass, high pass
- Master volume in 16 steps
- 2 A/D converters (8 bit, low frequency, used for reading paddle input)
- Random generator
- Audio input (cannot be used for sampling, but signal can be routed through the SID filter)
## Chip Variations
| Chip | Production | Notes |
|------|------------|-------|
| 6581 | 21/1982 - 30/1985 | NMOS, pin 28 = 12V, 470pF on filter caps |
| 6581R3 | 42/1985 - 07/1986 | |
| 6581R4 | 16/1986 - 30/1986 | |
| 6581R4AR | 22/1986 - 06/1987 | |
| 8580R5 | 06/1987 - 19/1992 | HMOS-II, pin 28 = 9V, 22nF on filter caps; quiet digisound without "digifix" |
## Memory Addresses (SID at $D400-$D41C, mirrored through $D7FF except on C128)
| Address Hex | Address Dec | Function |
|-------------|-------------|----------|
| $D400 | 54272 | Frequency voice 1 low byte |
| $D401 | 54273 | Frequency voice 1 high byte |
| $D402 | 54274 | Pulse wave duty cycle voice 1 low byte (low nibble) |
| $D403 | 54275 | Pulse wave duty cycle voice 1 high byte (low nibble) |
| $D404 | 54276 | Control register voice 1 |
| $D405 | 54277 | Attack duration / Decay duration voice 1 |
| $D406 | 54278 | Sustain level / Release duration voice 1 |
| $D407-$D40D | 54279-54285 | Same for voice 2 |
| $D40E-$D414 | 54286-54292 | Same for voice 3 |
| $D415 | 54293 | Filter cutoff frequency low byte (low 3 bits) |
| $D416 | 54294 | Filter cutoff frequency high byte (8 bits) |
| $D417 | 54295 | Filter resonance and routing (high nibble=res, low 4 bits=ext/3/2/1 enable) |
| $D418 | 54296 | Filter mode and main volume (bits 6-4: mute V3 / HP / BP / LP, bits 3-0: volume 0-15) |
| $D419 | 54297 | Paddle X value (read only) |
| $D41A | 54298 | Paddle Y value (read only) |
| $D41B | 54299 | Oscillator voice 3 (read only) |
| $D41C | 54300 | Envelope voice 3 (read only) |
## Voice Control Register ($D404 / $D40B / $D412)
Bit 0: Gate (1 = start envelope, 0 = release)
Bit 1: Synchronize with voice 3 (for V1) / voice 1 (V2) / voice 2 (V3)
Bit 2: Ring modulation with voice 3 (V1) / voice 1 (V2) / voice 2 (V3)
Bit 3: Test (reset oscillator)
Bit 4: Triangle wave
Bit 5: Sawtooth wave
Bit 6: Pulse (rectangle) wave
Bit 7: Noise
## ADSR ($D405-$D406 per voice)
$D405 high nibble: Attack (0-15)
$D405 low nibble: Decay (0-15)
$D406 high nibble: Sustain (0-15)
$D406 low nibble: Release (0-15)
## Pinout
- CAP1A, CAP1B / CAP2A, CAP2B: filter capacitors (6581: 470pF, 8580: 20nF)
- /RES: Reset
- phi2: system clock
- R/W
- /CS
- A0-A4
- GND
- Vdd: 12V (6581) or 9V (8580)
- AUDIO OUT
- EXT IN: external audio in (8580 needs ~330kOhm to GND to fix old digisound)
- Vcc: 5V
- POT X, POT Y: paddle inputs
- D0-D7: data bus
## Trivia
Bob Yannes later founded Ensoniq (the ESQ-1 is reportedly the synth he wanted the SID to be). The 6581's volume register design flaw was famously used to play 4-bit samples by rapidly changing the volume. The 8580 fixed this; sample playback can be restored with a resistor on EXT IN.
+103
View File
@@ -0,0 +1,103 @@
# Sprites (MOBs — Movable Object Blocks)
Source: https://www.c64-wiki.com/wiki/Sprite
A sprite is a piece of graphics that can move and be assigned attributes independently of other graphics or text. The VIC-II supports up to 8 sprites, but with raster interrupt programming you can display more than eight simultaneously.
## Sprite data
- A sprite pattern is 24×21 pixels (12×21 in multicolor).
- 63 bytes of data + 1 unused "pad" byte = 64 bytes per sprite (must be 64-byte aligned in the current VIC bank).
- In theory a VIC bank holds 256 patterns; with a text screen and charset, practically ~208.
## Hi-res sprite
- Bit = 0 → transparent
- Bit = 1 → sprite's individual color
## Multicolor sprite
Each 2 bits = 1 pixel (2× wider):
- %00 → transparent
- %01 → color from $D025 (multicolor 0, common)
- %10 → color from $D027-$D02E (this sprite's color)
- %11 → color from $D026 (multicolor 1, common)
## Sprite pointers (in screen RAM area)
If text screen starts at address S, sprite pointers are at S+1016 through S+1023. With the default screen at $0400, that's $07F8-$07FF. Pointers contain the pattern address / 64.
| Sprite | Pointer |
|--------|---------|
| #0 | $07F8 (2040) |
| #1 | $07F9 (2041) |
| #2 | $07FA (2042) |
| #3 | $07FB (2043) |
| #4 | $07FC (2044) |
| #5 | $07FD (2045) |
| #6 | $07FE (2046) |
| #7 | $07FF (2047) |
## Sprite position registers
| Sprite | X reg | Y reg |
|--------|-------|-------|
| #0 | $D000 (53248) | $D001 (53249) |
| #1 | $D002 (53250) | $D003 (53251) |
| #2 | $D004 (53252) | $D005 (53253) |
| #3 | $D006 (53254) | $D007 (53255) |
| #4 | $D008 (53256) | $D009 (53257) |
| #5 | $D00A (53258) | $D00B (53259) |
| #6 | $D00C (53260) | $D00D (53261) |
| #7 | $D00E (53262) | $D00F (53263) |
X coordinates need 9 bits; the MSBs are in $D010 (one bit per sprite).
## Sprite color registers
| Sprite | Color |
|--------|-------|
| #0 | $D027 |
| #1 | $D028 |
| #2 | $D029 |
| #3 | $D02A |
| #4 | $D02B |
| #5 | $D02C |
| #6 | $D02D |
| #7 | $D02E |
Multicolor shared colors: $D025, $D026.
## Sprite enable ($D015)
Each bit is a switch for that sprite. 1 = enabled, 0 = hidden.
Bit 0 = sprite #0, bit 7 = sprite #7.
## Sprite multicolor mode ($D01C)
Each bit selects multicolor (1) or hires (0) for that sprite.
## X expansion ($D01D), Y expansion ($D017)
Each bit doubles that sprite's size in the corresponding direction.
## Sprite priority ($D01B)
Each bit, when set, puts the sprite *behind* the background graphics (text/bitmap). When 0, sprite is in front. Sprite-sprite priority is hardwired: lower-numbered sprites always appear in front of higher-numbered ones.
## Collision detection
$D01E: sprite-sprite collision (1 bit per sprite). Read clears.
$D01F: sprite-data (background) collision. Read clears.
$D019 bit 2 = sprite-sprite IRQ flag, bit 1 = sprite-data IRQ flag.
$D01A bits enable these IRQs.
## Example: enable sprite #0 with white color
```basic
POKE 2040,13: REM pattern at 13*64=832
POKE 53248,255: REM X=255
POKE 53249,100: REM Y=100
POKE 53287,1: REM color=1 (white)
POKE 53269,1: REM enable sprite #0
```
File diff suppressed because it is too large Load Diff
+74
View File
@@ -0,0 +1,74 @@
# Graphics Modes
Source: https://www.c64-wiki.com/wiki/Graphics_Modes and https://www.c64-wiki.com/wiki/Standard_Character_Mode
The C64 supports 5 "official" graphics modes selected by 3 bits in the two VIC-II control registers:
- **ECM** (Extended Color Mode) — bit 6 of $D011
- **BMM** (Bitmap Mode) — bit 5 of $D011
- **MCM** (Multicolor Mode) — bit 4 of $D016
The other bits of those registers are YSCROLL (0-2 of $D011), DEN (display enable, bit 4 of $D011), RSEL (bit 3 of $D011), CSEL (bit 3 of $D016), XSCROLL (0-2 of $D016).
## The 8 (5 legal) modes
| Mode | ECM | BMM | MCM | Result |
|------|-----|-----|-----|--------|
| 0 | 0 | 0 | 0 | Standard Character Mode |
| 1 | 0 | 0 | 1 | Multicolor Character Mode |
| 2 | 0 | 1 | 0 | Standard Bitmap Mode |
| 3 | 0 | 1 | 1 | Multicolor Bitmap Mode |
| 4 | 1 | 0 | 0 | Extended Background Color Mode |
| 5 | 1 | 0 | 1 | Invalid |
| 6 | 1 | 1 | 0 | Invalid |
| 7 | 1 | 1 | 1 | Invalid |
Modes 5-7 are "technically feasible but produce no visible output."
## Standard Character Mode (Mode 0)
- 40×25 character cells of 8×8 pixels.
- Each cell can have one of 16 colors (from color RAM nibble at $D800+cell_index).
- A single background color ($D021) applies to the whole screen.
- Screen memory is 1 KB ($0400-$07FF); Color RAM is 1000 nibbles ($D800-$DBE7).
- Character patterns are 8 bytes per character, fetched from the character generator (default is the 4 KB character ROM at $D000-$DFFF, but can be relocated to RAM).
- Soft scrolling is "easier" in character mode than in bitmap mode (just change XSCROLL/YSCROLL).
## Multicolor Character Mode (Mode 1)
- Each cell still 8×8, but only 4×8 in effective pixel resolution (each pair of bits in the character pattern is one "wide pixel").
- Up to 4 colors per cell: the two global background colors ($D021 and $D022) plus the cell's color RAM color and the multicolor-1 shared color $D026.
- "Color" bits %00/%01/%10/%11 map to: background 0 ($D021), background 1 ($D022), color RAM, background 2 ($D023). Pattern bit pairs.
## Standard Bitmap Mode (Mode 2) — "hires"
- 320×200 pixels, 1 bit per pixel.
- Bitmap is 8000 bytes. Location selected by bits VM13-VM10 of $D018 (in 8 KB steps within the 16 KB VIC bank). Video matrix (screen memory) is 1 KB at $D018's VM bits (in 1 KB steps).
- Color: each 8×8 cell is monochrome (foreground from color RAM nibble, background from $D021).
- BM=1: the video matrix contains *color* information instead of character codes, so color RAM is doubled up to provide per-cell background and foreground colors (the color RAM holds the cell's foreground; the cell's background is one of the 4 global "background color" registers $D021-$D024 in the 4 most-significant bits of the cell).
## Multicolor Bitmap Mode (Mode 3)
- 160×200 pixels, 2 bits per pixel (4 colors per 4×8 cell).
- Each pair of bits selects from: background 0 ($D021), background 1 ($D022), background 2 ($D023), color RAM cell color.
- Same memory layout as Standard Bitmap Mode (8000 bytes bitmap + 1 KB matrix).
## Extended Background Color Mode (Mode 4)
- Character mode (not bitmap) with 4 background colors instead of 1.
- Each character cell can select one of the 4 background colors ($D021-$D024) via 2 bits in its screen memory code.
- Useful for colored PETSCII art.
## Unofficial "modes" (demo scene techniques)
There are dozens of techniques that go beyond the official modes by exploiting undocumented VIC behavior or combining tricks like raster IRQ, sprites, and interlacing. The most famous families:
- **FLI** (Flexible Line Interpretation, July 1989) — forces a badline every line, allowing per-line color attributes. The first "demo scene" graphic hack.
- **IFLI** (Interlaced FLI, 1991) — combines FLI with 2-frame interlace.
- **AFLI** (Advanced FLI, 1990), **NUFLI** (2009), **MUFLI** (2006), **UFLI** (1996), **XFLI** (2002), **SHFLI** (1996), etc.
- **AFLI** and **UFLI** give 8+ colors per 8×8 cell, **NUFLI** adds even more.
- **SHI** (Super HiRes Interlace, 1991) — interlaced 320×400 mode.
- **Hyperscreen** — stable display using all 16 KB VIC bank by abusing badline tricks.
- **FLD** (Flexible Line Distance) — variation.
- **Megatext** (2004) — large character set mode.
(See `vic/cebix-vic-article.txt` for the technical underpinnings of these tricks, especially sections 3.14 "Effects and applications" and 3.5 "Bad Lines".)
+56
View File
@@ -0,0 +1,56 @@
# VIC-II (Video Interface Chip II)
Source: https://www.c64-wiki.com/wiki/VIC
## Identification
VIC-II is the video chip used in the C64 / C128 family. There are two VIC-II types in the C64: the 6567 in NTSC machines and the 6569 in PAL machines. Newer C64 versions use the functionally equivalent 8562 (NTSC) and 8565 (PAL) chips.
## Features
- 16 kB address space for screen, character and sprite memory
- 320 × 200 pixels video resolution (160 × 200 in multi-color mode)
- 40 × 25 characters text resolution
- Three character display modes and two bitmap modes
- 16 colors
- Concurrent handling of 8 sprites per scanline, each of 24 × 21 pixels (12 × 21 multicolor)
- Raster interrupt
- Smooth scrolling
- Independent dynamic RAM refresh (an unusual feature for a graphics processor)
- Bus mastering for a 6502-style system bus; CPU and VIC-II accessing the bus during alternating half-clock cycles (the VIC-II will halt the CPU when it needs extra cycles)
## Memory Addresses of the VIC-II
| Hex Address | Dec Address | Page | Contents |
|-------------|-------------|------|----------|
| $D000-$D3FF | 53248-54271 | Page 208-211 | VIC-II registers |
| $D800-$DBE7 | 55296-56295 | Page 216-219 | Color RAM |
- Sprites lie at address MEM(Start of screen mem + $03F8 + sprite number)*64
- The start of the Screen RAM is set by $DD00 (the VIC bank, see CIA 2) and $D018.
## Technical Notes (from Christian Bauer's article)
The operation of the VIC-II is thoroughly described in the document
"The MOS 6567/6569 video controller (VIC-II) and its application in the Commodore 64"
by Christian Bauer. See `vic/cebix-vic-article.txt` for the full text.
Key facts:
- The cycle count starts at 1; for example, VICE starts counting at 0.
- Writing to $D011/$D012 can immediately trigger a raster IRQ, provided no raster IRQ has yet been triggered in the current line.
- The vertical "expansion flip-flop" is somewhat misleadingly named; its state actually indicates whether a line in the sprite data should currently be skipped or not.
- VCBASE and MCBASE are used to reset VC and MC to their initial values.
- VMLI is not a pointer but rather a 40-bit shift register that controls the 40 enable bits of the VIC-II's internal "40×12 bit video matrix/color line."
## Known Variants
- MOS 6560 / 6561: VIC-I (VIC-20)
- MOS 6566: VIC-II for MAX Machine
- MOS 6567: VIC-II for (NTSC) C64
- MOS 6569: VIC-II for (PAL) C64 (R1/R3/R4/R5)
- MOS 6572/6573: VIC-II (PAL-N/PAL-M)
- MOS 8562: VIC-II (NTSC) for later C64 / C128
- MOS 8564/8565/8566/8569: VIC-II variants for C64 / C128
- CSG 4567: VIC-III (C65/C64DX)
The NMOS variants 6566/67/69 require +12V on Pin 13/Vdd; the HMOS-II variants (8562/65) require only +5V DC. The 856x variants exhibit the "Grey Dots" problem.
+68
View File
@@ -0,0 +1,68 @@
# VIC-II Register Map ($D000$D3FF)
Source: https://www.c64-wiki.com/wiki/Page_208-211
Page 208-211 covers the memory locations 53248-54271 ($D000-D3FF). This area is wholly reserved for the VIC-II Registers. The 47 registers are mirrored every 64 bytes in this area.
## Register Table
| Hex | Dec | Type | Bit7 | Bit6 | Bit5 | Bit4 | Bit3 | Bit2 | Bit1 | Bit0 | Contents |
|------|------|----------|------|------|------|------|------|------|------|------|----------|
| $D000 | 53248 | Register | | | | | | M0X | | | X Coordinate Sprite 0 |
| $D001 | 53249 | Register | | | | | | M0Y | | | Y Coordinate Sprite 0 |
| $D002 | 53250 | Register | | | | | | M1X | | | X Coordinate Sprite 1 |
| $D003 | 53251 | Register | | | | | | M1Y | | | Y Coordinate Sprite 1 |
| $D004 | 53252 | Register | | | | | | M2X | | | X Coordinate Sprite 2 |
| $D005 | 53253 | Register | | | | | | M2Y | | | Y Coordinate Sprite 2 |
| $D006 | 53254 | Register | | | | | | M3X | | | X Coordinate Sprite 3 |
| $D007 | 53255 | Register | | | | | | M3Y | | | Y Coordinate Sprite 3 |
| $D008 | 53256 | Register | | | | | | M4X | | | X Coordinate Sprite 4 |
| $D009 | 53257 | Register | | | | | | M4Y | | | Y Coordinate Sprite 4 |
| $D00A | 53258 | Register | | | | | | M5X | | | X Coordinate Sprite 5 |
| $D00B | 53259 | Register | | | | | | M5Y | | | Y Coordinate Sprite 5 |
| $D00C | 53260 | Register | | | | | | M6X | | | X Coordinate Sprite 6 |
| $D00D | 53261 | Register | | | | | | M6Y | | | Y Coordinate Sprite 6 |
| $D00E | 53262 | Register | | | | | | M7X | | | X Coordinate Sprite 7 |
| $D00F | 53263 | Register | | | | | | M7Y | | | Y Coordinate Sprite 7 |
| $D010 | 53264 | Register | M7X8 | M6X8 | M5X8 | M4X8 | M3X8 | M2X8 | M1X8 | M0X8 | MSBs of X coordinates |
| $D011 | 53265 | Register | RST8 | ECM | BMM | DEN | RSEL | YSCROLL | | | Control register 1 |
| $D012 | 53266 | Register | | RASTER | | | | | | | Raster counter |
| $D013 | 53267 | Register | | LPX | | | | | | | Light pen X |
| $D014 | 53268 | Register | | LPY | | | | | | | Light pen Y |
| $D015 | 53269 | Register | M7E | M6E | M5E | M4E | M3E | M2E | M1E | M0E | Sprite enabled |
| $D016 | 53270 | Register | - | - | RES | MCM | CSEL | XSCROLL | | | Control register 2 |
| $D017 | 53271 | Register | M7YE | M6YE | M5YE | M4YE | M3YE | M2YE | M1YE | M0YE | Sprite Y expansion |
| $D018 | 53272 | Register | VM13 | VM12 | VM11 | VM10 | CB13 | CB12 | CB11 | - | Memory pointers |
| $D019 | 53273 | Register | IRQ | - | - | - | ILP | IMMC | IMBC | IRST | Interrupt register |
| $D01A | 53274 | Register | IRQ | - | - | - | ELP | EMMC | EMBC | ERST | Interrupt enabled |
| $D01B | 53275 | Register | M7DP | M6DP | M5DP | M4DP | M3DP | M2DP | M1DP | M0DP | Sprite data priority |
| $D01C | 53276 | Register | M7MC | M6MC | M5MC | M4MC | M3MC | M2MC | M1MC | M0MC | Sprite multicolour |
| $D01D | 53277 | Register | M7XE | M6XE | M5XE | M4XE | M3XE | M2XE | M1XE | M0XE | Sprite X expansion |
| $D01E | 53278 | Register | M7M | M6M | M5M | M4M | M3M | M2M | M1M | M0M | Sprite-sprite collision |
| $D01F | 53279 | Register | M7D | M6D | M5D | M4D | M3D | M2D | M1D | M0D | Sprite-data collision |
| $D020 | 53280 | Register | - | - | - | - | EC | | | | Border colour |
| $D021 | 53281 | Register | - | - | - | - | B0C | | | | Background colour 0 |
| $D022 | 53282 | Register | - | - | - | - | B1C | | | | Background colour 1 |
| $D023 | 53283 | Register | - | - | - | - | B2C | | | | Background colour 2 |
| $D024 | 53284 | Register | - | - | - | - | B3C | | | | Background colour 3 |
| $D025 | 53285 | Register | - | - | - | - | MM0 | | | | Sprite multicolour 0 |
| $D026 | 53286 | Register | - | - | - | - | MM1 | | | | Sprite multicolour 1 |
| $D027 | 53287 | Register | - | - | - | - | M0C | | | | Sprite 0 colour |
| $D028 | 53288 | Register | - | - | - | - | M1C | | | | Sprite 1 colour |
| $D029 | 53289 | Register | - | - | - | - | M2C | | | | Sprite 2 colour |
| $D02A | 53290 | Register | - | - | - | - | M3C | | | | Sprite 3 colour |
| $D02B | 53291 | Register | - | - | - | - | M4C | | | | Sprite 4 colour |
| $D02C | 53292 | Register | - | - | - | - | M5C | | | | Sprite 5 colour |
| $D02D | 53293 | Register | - | - | - | - | M6C | | | | Sprite 6 colour |
| $D02E | 53294 | Register | - | - | - | - | M7C | | | | Sprite 7 colour |
| $D02F-$D03F | 53295-53311 | Unused | ($FF on read, ignored on write) | | | | | | | | |
| $D040-$D3FF | 53312-54271 | Same as $D000-$D03F (mirror every 64 bytes) | | | | | | | | | |
## Notes
- The bits marked with '-' are not connected and give "1" on reading.
- The registers $D01E and $D01F cannot be written and are automatically cleared on reading.
- The RES bit (bit 5) of register $D016 has no function on the VIC 6567/6569. On the 6566, this bit is used to stop the VIC.
- Bit 7 in register $D011 (RST8) is bit 8 of register $D012. Together they are called "RASTER". A write access to these bits sets the comparison line for the raster interrupt.
</content>
</invoke>
Submodule
+1
Submodule oscar64 added at ae7ecb4c8c
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Executable
+64
View File
@@ -0,0 +1,64 @@
#!/bin/sh
# build.sh — compile and optionally run a single C64 program with Oscar64.
#
# Usage:
# ./build.sh # compile helloworld.c → build/helloworld.prg
# ./build.sh -e # compile, then run in the oscar64 built-in emulator
# ./build.sh -c # just compile (default; -c is a no-op for clarity)
#
# Output (in ./build/):
# helloworld.prg — loadable C64 program (run with x64, VICE, or real hw)
# helloworld.asm — full 6502 listing
# helloworld.map — region/section/object placement
# helloworld.lbl — VICE monitor label commands
set -e
# --- locate the oscar64 compiler -----------------------------------------
# This script is ./src/build.sh, so the repo root is one level up.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
OSCAR64_DIR="$ROOT/oscar64"
OSCAR64_BIN="$OSCAR64_DIR/bin/oscar64"
BUILD_DIR="$SCRIPT_DIR/build"
# Output goes in ./build/ (relative to the src dir), kept out of the source tree.
mkdir -p "$BUILD_DIR"
# Build the compiler if the binary is missing. (make -C make is the
# makefile-based build from the upstream oscar64 source tree.)
if [ ! -x "$OSCAR64_BIN" ]; then
echo "oscar64 compiler not found at $OSCAR64_BIN; building it..."
( cd "$OSCAR64_DIR" && make -C make compiler )
fi
if [ ! -x "$OSCAR64_BIN" ]; then
echo "error: $OSCAR64_BIN is still missing after build" >&2
echo " try: cd $OSCAR64_DIR && make -C make compiler" >&2
exit 1
fi
# --- compile + optionally run --------------------------------------------
SRC=helloworld.c
EMU_FLAGS=""
for arg in "$@"; do
case "$arg" in
-e) EMU_FLAGS="-e" ;;
-c) ;; # explicit compile-only, no extra flags
-*) echo "unknown flag: $arg" >&2; exit 1 ;;
esac
done
echo "compiling $SRC with $OSCAR64_BIN -> $BUILD_DIR/"
# -o puts the .prg in the build dir; the other artifacts (.asm, .map, .lbl)
# follow automatically since they share the base name.
"$OSCAR64_BIN" -i="$OSCAR64_DIR/include" -o="$BUILD_DIR/helloworld.prg" "$SRC"
if [ -n "$EMU_FLAGS" ]; then
echo "running helloworld.prg in oscar64's built-in emulator"
# The emulator reads the .prg; re-run with -e pointing at the same output.
"$OSCAR64_BIN" -i="$OSCAR64_DIR/include" -o="$BUILD_DIR/helloworld.prg" -e "$SRC"
fi
echo "done: $BUILD_DIR/helloworld.prg"
+23
View File
@@ -0,0 +1,23 @@
// helloworld.c — minimal C64 program in C with Oscar64
//
// Builds a .prg that prints "Hello World" on the C64 text screen and exits.
//
// Build: ../scripts/build.sh
// Run: x64 helloworld.prg (or use ../scripts/build.sh -e to run in
// the built-in oscar64 emulator)
#include <stdio.h>
int main(void)
{
// CHR$(147) is PETSCII "clear screen" — same as printf("\f") in C but
// emitted as the right byte for the C64's PETSCII character set.
putchar(147);
// The p"" prefix tells Oscar64 to emit the string as PETSCII, not ASCII.
// Without it the C64's character ROM would render garbage because the
// character generator maps 'H' to PETSCII 8, not ASCII 72.
printf(p"Hello World\n");
return 0;
}