CPU¶
Library: Highbyte.DotNet6502
Overview¶
- A stand-alone library for executing 6502 machine code programs.
- Has no UI, meant to be integrated into other applications.
- Emulation of 6502-family processors, selectable per CPU model (see below).
- Supports all official 6502 opcodes.
- Supports a compatibility-profile based subset of undocumented NMOS 6502 opcodes.
- Can load an assembled 6502 program binary and execute it.
- Passes this Functional 6502 test program.
- Every instruction performs exactly the bus accesses the silicon performs, one per clock cycle, verified against the SingleStepTests 65x02 corpus (see below).
Cycle and bus accuracy¶
An instruction executes atomically (one call runs it to completion), but every clock cycle of it is a real memory access in hardware order, dummy reads and write-backs included: the next-byte read of implied instructions, the un-indexed read of zero-page indexed modes, the un-carried-address read of NMOS indexed modes on a page crossing, the branch fix-up reads, the stack reads of pulls and returns, the NMOS read/write-back/write and 65C02 read/read/write of read-modify-write instructions, and the 65C02's operand re-reads on its extra cycles. Memory- mapped I/O therefore sees exactly the accesses a real device would, at the same points in the sequence.
CPU.BusCycles counts those accesses. Because the count advanced by an instruction equals its
cycle count, systems can derive elapsed cycles from bus activity, and the tests hold the two
together for every opcode byte of every model and profile. A system can therefore keep its
devices exact to the cycle without stepping them every cycle: a memory-mapped register handler
reads the counter, advances the device to the cycle of the access, and then applies the access.
The C64 does this for the VIC-II, the CIAs and the SID.
A system can also stall the CPU the way a bus master holding RDY does, through
CPU.BusStallSource: before a read the CPU asks how many cycles the bus is busy, the read then
happens at the cycle the bus is released, and the waiting cycles count as instruction cycles
without accesses (so BusCycles is then the cycle count, of which the accesses are a subset).
Writes are never stalled, as on the 6510. The source names the next cycle at which it wants to be
asked again, so the check costs one comparison per read in between.
Interrupts follow the hardware sampling rule. The 6502 polls its IRQ and NMI inputs at the end
of an instruction's second-to-last cycle (a taken branch that does not cross a page polls at the
end of its first cycle), so a line that goes active during the last cycle is only seen after the
following instruction. A device reports the bus cycle on which it asserted the line, through the
CPUInterrupts overloads that take a cycle, and the CPU takes the interrupt at a boundary only if
that cycle is at or before the poll point. A source set active without a cycle is taken at the
next boundary. CLI, SEI and PLP change the I flag after the poll, so their effect on
interrupt recognition is one instruction late; RTI changes it in time. An interrupt-entry
sequence (IRQ, NMI or BRK) does not poll at its end, so the handler's first instruction always
runs before another interrupt is taken; and an NMI that arrives by the 4th cycle of an IRQ or
BRK sequence hijacks it: the sequence completes with the NMI vector and the stack frame it
already pushed (a BRK's keeps B set), and the IRQ, if its device still asserts it, is taken when
the NMI handler returns.
Verification, beyond the functional test programs:
- A pinned subset of the SingleStepTests corpus, which records the exact bus cycles of the
NMOS 6502 and the WDC 65C02, is run per opcode: final state, cycle-by-cycle bus trace and
cycle count must all match. Bytes where the emulated part is documented to differ (the
Rockwell bit instructions and WDC-only
WAI/STPthat the NCR 65C02 executes as NOPs) are listed with their reason and skipped. - A structural test executes every defined opcode byte of every model and profile from random state and requires one bus access per reported cycle.
What this does not claim: the CPU cannot be stopped inside an instruction, so a device that needs to stall the CPU (the C64's VIC-II via BA/RDY) or sample a line on a specific cycle must do so at the bus access for that cycle; and interrupts are recognized at instruction boundaries.
CPU models¶
The CPU is constructed for a specific CPU model. Instruction identity is per model and opcode byte — on a 65C02 the same byte can mean a different instruction than on an NMOS 6502, and the 65C02 adds instructions that do not exist on NMOS models.
| Model id | Display name | Notes |
|---|---|---|
nmos6502 |
NMOS 6502 | The default. Official instruction set plus profile-gated undocumented NMOS opcodes. |
mos6510 |
MOS 6510 | Same instruction set as nmos6502, adds the on-chip I/O port at addresses $00/$01 (used by the C64). |
ncr65c02 |
NCR 65C02 | CMOS 65C02 (base/NCR variant): new and redefined instructions, all 256 bytes defined, CMOS behavior differences. Supports only the OfficialOnly compatibility profile. |
Select a model via the CPU constructor overloads that take a cpuModelId string, e.g.
new CPU(loggerFactory, CpuModelIds.Ncr65c02, CpuCompatibilityProfile.OfficialOnly).
The CpuModelInfo class provides model ids, display names, and supported profiles for
configuration UIs and validation.
Note: the public OpCodeId enum names the opcode bytes of the NMOS instruction set
only (models nmos6502/mos6510). It is a convenient vocabulary for writing NMOS
programs in code and for checking well-known official bytes (which are identical on all
models), but it is not model-aware: 65C02-only instructions have no enum member, and a
byte redefined on the 65C02 keeps its NMOS name in the enum. For model-correct per-byte
information use the model-aware members on CPU (IsOpCodeDefined, GetOpCodeSize) and
disassembly via OutputGen.
Opcode compatibility profiles¶
The core CPU library exposes undocumented opcodes through CpuCompatibilityProfile.
Higher profiles include everything from lower profiles.
| Profile | Meaning |
|---|---|
OfficialOnly |
Only documented MOS 6502 opcodes are available. |
StableUnofficial |
Also enables the more predictable undocumented NMOS opcodes commonly used on real 6502/6510 hardware, including ARR ($6B); LXA ($AB) and ANE ($8B), whose result depends on a chip-specific value ORed into A: the common $EE is used; and the indexed stores SHA ($93, $9F), SHX ($9E), SHY ($9C) and TAS ($9B), which store a register ANDed with the high byte of the address plus one, corrupt the address high byte with that value on a page crossing, and drop the AND when a bus master (the C64's VIC-II) holds RDY low in the cycle before the write. |
ExperimentalUnofficial |
Also enables the currently implemented but less reliable undocumented opcodes, such as LAS ($BB), whose result depends on what is on the bus; for targeted compatibility testing. |
FullUnofficial |
Also enables halt-style unofficial opcodes such as JAM / KIL that can intentionally jam the CPU until reset. |
Notes:
new CPU()currently usesExperimentalUnofficial.GenericComputerdefaults toExperimentalUnofficial.C64andVic20default toStableUnofficial.- These profiles describe the currently implemented undocumented NMOS opcode set. They are not a claim of complete coverage for every unofficial opcode found on every 6502-family variant.
JAM/KILbehavior is modeled as the CPU entering a halted/jammed state untilReset(...)is called.
Example:
using Highbyte.DotNet6502;
var defaultCpu = new CPU();
var officialCpu = new CPU(CpuCompatibilityProfile.OfficialOnly);
var stableCpu = new CPU(CpuCompatibilityProfile.StableUnofficial);
var fullCpu = new CPU(CpuCompatibilityProfile.FullUnofficial);
Requirements¶
See details under Development.
Using from a .NET application¶
Reference NuGet package¶
Or compile .dll yourself¶
- Clone this repo:
git clone https://github.com/highbyte/dotnet-6502.git - Change dir to library:
cd dotnet-6502/src/libraries/Highbyte.DotNet6502 - Build library:
dotnet build - In your app, add a
.dllreference to./bin/Debug/net10.0/Highbyte.DotNet6502.dll
Examples¶
Example #1¶
Step 1 — Write a 6502 program in assembly¶
Write a 6502 assembly program to calculate the average of two values (from different memory locations) and store the result in a third memory location.
Note
This example uses the ACME cross-assembler syntax (builds exist for Windows and macOS, for Linux it requires downloading source code and building). There exist other 6502 cross assemblers that can be used (but which may have different syntax requirements).
Use a text editor (or IDE) to create a text file with the contents below and save it to calc_avg.asm.
Tip
VSCode has an extension called VS64 that provides nice syntax highlighting for 6502 assembly code (.asm).
;Calculates the average of two values stored in memory locations, and stores the result in another memory location.
;Code written in 6502 assembler using ACME cross assembler syntax.
;code start address
* = $c000
;!to "./calc_avg.prg"
lda $d000
clc
adc $d001
ror
sta $d002
;In emulator, setup hitting brk instruction to stop
brk
Step 2 — Assemble program to binary .prg file¶
Example assumes ACME is installed, and the acme executable is in PATH.
PowerShell / Bash:
acme -f cbm -o calc_avg.prg calc_avg.asm
# or examples if acme is not in path:
# & "$($env:USERPROFILE)\c64\acme\acme.exe" -f cbm -o calc_avg.prg calc_avg.asm
# ~/c64/acme/acme -f cbm -o calc_avg.prg calc_avg.asm
Step 3 (optional) — Inspect binary .prg file¶
Note
If the binary was assembled with the -f cbm parameter (as in the example above), the two first bytes in the .prg file would be the load address specified in the source .asm file (* = $c000), in little endian order 00,C0. This is usually the convention for Commodore computers, and convenient in other contexts also.
If the binary was assembled with the -f plain parameter, the binary file would not have the first two address bytes, and only contain the code (and data) declared in the source file.
PowerShell (Windows, Linux, macOS):
Bash (Linux) and Zsh (macOS):
Step 4 — Load compiled 6502 binary and execute it¶
A .NET C# console program that runs the 6502 program.
Program.cs:
// ----------------------------------------------------------------------------------------------------
// A minimal example of how to load and run a 6502 machine code program.
// This does not involve a complete computer (such as Commodore 64) but only the CPU and memory.
// ----------------------------------------------------------------------------------------------------
using Highbyte.DotNet6502;
using Highbyte.DotNet6502.Utils;
string programFile = "calc_avg.prg";
// Create memory (default 64KB) and load the machine code program into it. Assume two first bytes in the .prg file is the load address.
var mem = BinaryLoader.Load(programFile, out ushort loadAddress);
// Init variables in memory locations used by the program.
mem[0xd000] = 64;
mem[0xd001] = 20;
Console.WriteLine($"Input 1 (0xd000) = {mem[0xd000]}");
Console.WriteLine($"Input 2 (0xd001) = {mem[0xd001]}");
// Create the CPU and set program counter (start address).
var cpu = new CPU();
cpu.PC = loadAddress;
// Run program. The 6502 program will run until a BRK instruction is encountered.
cpu.Execute(mem, LegacyExecEvaluator.UntilBRKExecEvaluator);
// Inspect result of program which is stored in memory location 0xd002.
Console.WriteLine($"Output (0xd002) = {mem[0xd002]}");
Result:
Example #2 — Enter 6502 machine code directly and show processor status¶
Program.cs:
// ----------------------------------------------------------------------------------------------------
// An example of how to enter a machine code program directly into memory,
// and instantiating the "Generic" computer with logging of executed instructions.
// ----------------------------------------------------------------------------------------------------
using Highbyte.DotNet6502;
using Highbyte.DotNet6502.Systems.Generic;
using Highbyte.DotNet6502.Utils;
// Test program to calculate average of two values
// - adds values from two memory locations
// - divides it by 2 (rotate right one bit position)
// - stores it in another memory location
// Load input data into memory
byte value1 = 64;
byte value2 = 20;
ushort value1Address = 0xd000;
ushort value2Address = 0xd001;
ushort resultAddress = 0xd002;
var mem = new Memory();
mem[value1Address] = value1;
mem[value2Address] = value2;
// Load machine code into memory
ushort codeAddress = 0xc000;
ushort codeInsAddress = codeAddress;
mem[codeInsAddress++] = 0xad; // LDA (Load Accumulator)
mem[codeInsAddress++] = 0x00; // |-Lowbyte of $d000
mem[codeInsAddress++] = 0xd0; // |-Highbyte of $d000
mem[codeInsAddress++] = 0x18; // CLC (Clear Carry flag)
mem[codeInsAddress++] = 0x6d; // ADC (Add with Carry, adds memory to accumulator)
mem[codeInsAddress++] = 0x01; // |-Lowbyte of $d001
mem[codeInsAddress++] = 0xd0; // |-Highbyte of $d001
mem[codeInsAddress++] = 0x6a; // ROR (Rotate Right, rotates accumulator right one bit position)
mem[codeInsAddress++] = 0x8d; // STA (Store Accumulator, store accumulator to memory)
mem[codeInsAddress++] = 0x02; // |-Lowbyte of $d002
mem[codeInsAddress++] = 0xd0; // |-Highbyte of $d002
mem[codeInsAddress++] = 0x00; // BRK (Break/Force Interrupt) - emulator configured to stop execution when reaching this instruction
// Initialize a "Generic" 6502 computer emulator with CPU, memory, and execution parameters
var computerBuilder = new GenericComputerBuilder();
computerBuilder
.WithCPU()
.WithStartAddress(codeAddress)
.WithMemory(mem)
.WithInstructionExecutedEventHandler(
(s, e) => Console.WriteLine(OutputGen.GetLastInstructionDisassembly(e.CPU, e.Mem)))
.WithExecOptions(options =>
{
options.ExecuteUntilInstruction = OpCodeId.BRK; // Emulator will stop executing when a BRK instruction is reached.
});
var computer = computerBuilder.Build();
// Run program
computer.Run();
Console.WriteLine($"Execution stopped");
Console.WriteLine($"CPU state: {OutputGen.GetProcessorState(computer.CPU)}");
Console.WriteLine($"Stats: {computer.CPU.ExecState.InstructionsExecutionCount} instruction(s) processed, and used {computer.CPU.ExecState.CyclesConsumed} cycles.");
// Print result
byte result = mem[resultAddress];
Console.WriteLine($"Result: ({value1} + {value2}) / 2 = {result}");
Result:
C000 AD 00 D0 LDA $D000
C003 18 CLC
C004 6D 01 D0 ADC $D001
C007 6A ROR A
C008 8D 02 D0 STA $D002
C00B 00 BRK
Execution stopped
CPU state: A=15 X=00 Y=00 PS=[-----I--] SP=FD PC=0000
Stats: 6 instruction(s) processed, and used 23 cycles.
Result: (64 + 20) / 2 = 41
Model for bank switching¶
The 6502 CPU supports max 64KB of total memory (16-bit address space). To enable more memory to be used, a type of "bank switching" is supported in the memory implementation. X number of memory configurations can be created, and each populated with byte[] arrays for separate locations within the 64KB space.
The Memory constructor parameter numberOfConfigurations (default 1) specifies how many memory configurations to support:
Switch between the different memory configurations by calling SetMemoryConfiguration:
TODO: more details