A 6502 CPU emulator for .NET
What it (currently) does/is
- .NET 6 cross platform library
Highbyte.DotNet6502
written in C# - Emulation of a 6502 processor
- Supports all official 6502 opcodes
- Can load an assembled 6502 program binary and execute it
- Passes this Functional 6502 test program
- Monitor (rudimentary)
- A companion library
Highbyte.DotNet6502.SadConsoleHost
to enable emulator interaction with a SadConsole window - Example programs, incl. SadConsole and Blazor WebAssembly apps
- A programming exercise, that may or may not turn into something more
What's (currently) missing
- Decimal mode (Binary Coded Decimal) calculations
- Support for unofficial opcodes
What it isn't (and probably never will be)
- An emulation of an entire computer (such as Apple II or Commodore 64)
- The fastest emulator
Inspiration for this library was a Youtube-series about implementing a 6502 emulator in C++
- Requirements
- How to use Highbyte.DotNet6502 library from a .NET application
- How to use Highbyte.DotNet6502.SadConsoleHost library from a .NET application
- How to use Highbyte.DotNet6502 machine code monitor
- How to develop
- Tests
- Resources
- .NET 6 SDK installed.
- Use Windows, Linux, or Mac
dotnet add package Highbyte.DotNet6502 --prerelease
- Clone this repo
git clone https://github.com/highbyte/dotnet-6502.git
- Change dir to library
cd dotnet-6502/Highbyte.DotNet6502
- Build library
dotnet build
- In your app, add .dll reference to
./bin/Debug/net6.0/Highbyte.DotNet6502.dll
Example #1. Load compiled 6502 binary and execute it.
var mem = BinaryLoader.Load(
"C:\Binaries\MyCompiled6502Program.prg",
out ushort loadedAtAddress);
var computerBuilder = new ComputerBuilder();
computerBuilder
.WithCPU()
.WithStartAddress(loadedAtAddress)
.WithMemory(mem);
var computer = computerBuilder.Build();
computer.Run();
Example #2. 6502 machine code for adding to numbers and dividing by 2
// Test program
// - adds values from two memory location
// - divides it by 2 (rotate right one bit position)
// - stores it in another memory location
// Load input data into memory
byte value1 = 12;
byte value2 = 30;
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 to 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 emulator with CPU, memory, and execution parameters
var computerBuilder = new ComputerBuilder();
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}");
generates this output
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: (12 + 30) / 2 = 21
To enable more than 64KB total memory, a type of "bank switching" is implemented.
- The memory has 8 segments of 8K (0x2000/8192 bytes) each. (segment size may be a thing that can be configured in the future)
- Segment 0:
0x0000 - 0x1fff
- Segment 1:
0x2000 - 0x3fff
- Segment 2:
0x4000 - 0x5fff
- Segment 3:
0x6000 - 0x7fff
- Segment 4:
0x8000 - 0x9fff
- Segment 5:
0xa000 - 0xbfff
- Segment 6:
0xc000 - 0xdfff
- Segment 7:
0xe000 - 0x1fff
- Segment 0:
- Each segment has 1 bank by default (bank 0).
- The first segment (segment 0) cannot have multiple memory banks.
- Additional (max 254) banks can be added to segments 1-7 individually.
// Enable bank switching when creating Memory
var mem = new Memory(enableBankSwitching: true);
// Add a new bank to segment 1 with blank (0x00) contents. The bank number will be 1 (the default one is 0)
mem.AddMemorySegmentBank(1);
// Add another new bank to segment 1 with blank (0x00) contents. The bank number will be 2 (the default one is 0)
mem.AddMemorySegmentBank(1);
// Add a new bank to segment 5 with predefined contents.
// The predefined arrary must be exactly 1 segement in size, 0x2000 (8192) bytes.
// Code to load byte array is not described here.
byte[] bankMem = GetMyBankMem();
// The added bank number will be 1 (the default one is 0)
mem.AddMemorySegmentBank(5, bankMem);
To control which bank is active in each segment, two special memory locations in Zero Page can be used from 6502 code. It's a two-step process:
- Address
0x02
: Write the bank number (0-255) for the segment to be loaded. Each segment has a bank 0. Additional banks is added by emulator startup code described above. - Address
0x01
: Write the segment number (1-7) for the segment to be loaded. Writing to this location will trigger the load.
Example: Switch the memory contents for segment 5 to bank 1 (that was added in example above)
- Write value
1
to address0x02
- Write value
5
to address0x01
Example: Switch back segment 5 to use the original bank 0
- Write value
0
to address0x02
- Write value
5
to address0x01
You can find example code in this test.
The companion library Highbyte.DotNet6502.SadConsoleHost
provides an easy way for letting 6502 code running in the Highbyte.DotNet6502
emulator interacting with a SadConsole window for a text-based user interface (with possibility of colors and custom fonts).
The Highbyte.DotNet6502.SadConsoleHost
library overview:
- Initializes the SadConsole library with a window to display screen data from code running in the 6502 emulator.
- Your 6502 code running in the emulator
- is run every frame (SadConsole event handler) until your code sets a specific flag in a memory location, indicating that its done for current frame.
- should use specific memory-ranges where it stores text and color information.
- The text and color information from the emulator memory is then rendered in the SadConsole window.
With the same principle, keyboard events are also communicated from SadConsole to the emulator via memory addresses.
- Create a new SadConsole .NET project (reference the SadConsole documentation for details). Output type in the .csproj file should be
<OutputType>WinExe</OutputType>
.
mkdir demo
cd demo
dotnet new --install SadConsole.Templates:1.0.5
dotnet new sadconsole8
- Edit .csproj file and change
<TargetFramework>netcoreapp3.1</TargetFramework>
to<TargetFramework>net6.0</TargetFramework>
- Add reference to
Highbyte.DotNet6502.SadConsoleHost
(which will also get you the main emulator libraryHighbyte.DotNet6502
)
dotnet add package Highbyte.DotNet6502.SadConsoleHost --prerelease
- Add reference to configuration libraries
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Configuration.Binder
- Replace Program.cs with this code:
using Highbyte.DotNet6502.SadConsoleHost;
using Microsoft.Extensions.Configuration;
using System.IO;
namespace Demo
{
class Program
{
private static IConfiguration Configuration;
static void Main()
{
// Get config options
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
Configuration = builder.Build();
var emulatorHostOptions = new Options();
Configuration.GetSection(Options.ConfigSectionName).Bind(emulatorHostOptions);
// Init EmulatorHost and run!
var emulatorHost = new EmulatorHost(emulatorHostOptions);
emulatorHost.Start();
}
}
}
- Create a
appsettings.json
file where you configure where your compiled 6502 program is, and what memory addresses your 6502 program uses for displaying text and colors.
{
"Highbyte.DotNet6502.SadConsoleHost": {
"SadConsoleConfig": {
"WindowTitle": "SadConsole with Highbyte.DotNet6502 emulator!",
"FontScale": 2
},
"EmulatorConfig": {
"ProgramBinaryFile": "./.cache/hello_world.prg",
"RunEmulatorEveryFrame" : 1,
"Memory": {
"Screen": {
"Cols": 80,
"Rows": 25,
"BorderCols": 6,
"BorderRows": 3,
"ScreenStartAddress": "0x0400", // 80*25 = 2000 (0x07d0) -> range 0x0400 - 0x0bcf
"ScreenColorStartAddress": "0xd800", // 80*25 = 2000 (0x07d0) -> range 0xd800 - 0xdfcf
"ScreenRefreshStatusAddress": "0xd000", // The 6502 code should set bit 1 here when it's done for current frame
"ScreenBorderColorAddress": "0xd020",
"ScreenBackgroundColorAddress": "0xd021",
"DefaultBgColor": "0x00", // 0x00 = Black (C64 scheme)
"DefaultFgColor": "0x01", // 0x0f = Light grey, 0x0e = Light Blue, 0x01 = White (C64 scheme)
"DefaultBorderColor": "0x0b" // 0x0b = Dark grey (C64 scheme)
},
"Input": {
"KeyPressedAddress": "0xd030",
"KeyDownAddress": "0xd031",
"KeyReleasedAddress": "0xd032"
},
"MemoryBanks": { // There are 8 memory segments (0-7) of 8K each. Segment 0 (0x0000-0x1fff) can not have multiple banks.
"EnableMemoryBanks": false // Set to true to enable banks to be used. And increase BanksPerSegment below.
"BanksPerSegment": 1, // Segments 1-7 can have more than one bank.
},
}
}
}
}
6502 assembly code example. Note that the declarations starting with SCREEN_
matches the memory addresses in appsettings.json
above.
The code can be compiled with ACME assembler. An easy way to do this is via the VS Code extension vs64.
;hello_world.asm
;Written with ACME cross-assembler using VSCode extension VS64. Extension will compile on save to .cache directory.
;Code start address
* = $c000
;------------------------------------------------------------
;Program settings
;------------------------------------------------------------
STATIC_TEXT_ROW = 10;
;------------------------------------------------------------
;Memory address shared with emulator host for updating screen
;------------------------------------------------------------
;80 columns and 25 rows, 1 byte per character = 1000 (0x03e8) bytes. Laid out in memory as appears on screen.
SCREEN_MEM = 0x0400 ;0x0400 - 0x07e7
SCREEN_MEM_COLS = 80
SCREEN_MEM_ROWS = 25
;Colors, one byte per character = 1000 (0x03e8) bytes
SCREEN_COLOR_MEM = 0xd800 ;0xd800 - 0xdbe7
;Byte with status flags to communicate with emulator host. When host new frame, emulator done for frame, etc.
SCREEN_REFRESH_STATUS = 0xd000
;Border color address
SCREEN_BORDER_COLOR_ADDRESS = 0xd020
;Bg color address for entire screen
SCREEN_BACKGROUND_COLOR_ADDRESS = 0xd021
;Currently pressed key on host (ASCII byte). If no key is pressed, value is 0x00
KEY_PRESSED_ADDRESS = 0xd030
;Currently down key on host (ASCII byte). If no key is down, value is 0x00
KEY_DOWN_ADDRESS = 0xd031
;Currently released key on host (ASCII byte). If no key is down, value is 0x00
KEY_RELEASED_ADDRESS = 0xd031
;------------------------------------------------------------
;Code start
;------------------------------------------------------------
;Set screen background color
lda #$06
sta SCREEN_BACKGROUND_COLOR_ADDRESS
;Set border color
lda #$0e
sta SCREEN_BORDER_COLOR_ADDRESS
;Initialize static text at row defined in STATIC_TEXT_ROW
ldx #0
.printchar:
lda STATIC_TEXT, X
sta SCREEN_MEM + (SCREEN_MEM_COLS * STATIC_TEXT_ROW), X
lda STATIC_TEXT_2, X
sta SCREEN_MEM + (SCREEN_MEM_COLS * (STATIC_TEXT_ROW + 2)), X
beq .endoftext
inx
jmp .printchar
.endoftext
mainloop:
;Wait for emulator indicating a new frame
.waitfornextframe
lda SCREEN_REFRESH_STATUS
and #%00000001 ;Bit 0 set signals it time to refresh screen
beq .waitfornextframe ;Loop if bit 0 is not set
;If space is pressed, cycle border color
lda KEY_DOWN_ADDRESS ;Load currently down key
cmp #$20 ;32 ($20) = space
bne .spacenotpressed
ldx SCREEN_BORDER_COLOR_ADDRESS ;Get current border color
inx ;Next color
cpx #$10 ;Passed highest color (#$0f)?
bne .notreachedhighestcolor ;If we haven't reached max color value
ldx #$00 ;Reset to lowest color (0)
.notreachedhighestcolor
stx SCREEN_BORDER_COLOR_ADDRESS ;Update border color
.spacenotpressed:
;Set bit flag that tells emulator that this 6502 code is done for current frame
lda SCREEN_REFRESH_STATUS
ora #%00000010 ;Bit 1 set signals that emulator is currently done
sta SCREEN_REFRESH_STATUS ;Update status to memory
;Loop forever
jmp mainloop
;------------------------------------------------------------
;Data
;------------------------------------------------------------
STATIC_TEXT:
!text " ***** DotNet6502 + SadConsole !! ***** "
!by 0 ;End of text indicator
STATIC_TEXT_2:
!text " Press SPACE to cycle border color "
!by 0 ;End of text indicator
TODO: Detailed information on how to configure, and simple 6502 example code. See example app below for complete implementation.
Example of a SadConsole application running compiled 6502 assembly code in the emulator, using Highbyte.DotNet6502.SadConsoleHost
library to let the emulator interact with text-based screen provided by SadConsole.
Notes:
- The scrolling is choppy due to text-mode only, but the color-cycling works ok.
- Tested on Windows and Ubuntu.
cd ./Examples/SadConsoleTest
dotnet run
Examples of a Blazor WebAssembly app running 6502 code with Highbyte.DotNet6502
library.
Notes:
- The example uses SkiaSharp.Views.Blazor for rendering.
- The scrolling is choppy due to text-mode only, but the color-cycling works ok.
- Tested on Chrome v96 and Edge v96.
- Assembly code
- A deployed version can be found here https://highbyte.se/dotnet-6502/blazorexample
- Assembly code
- Game code based on original found here
- A deployed version can be found here https://highbyte.se/dotnet-6502/blazorexample/?screenMem=512&cols=32&rows=32&prgUrl=6502binaries/snake6502.prg
- Or why not have the 6502 game binary (approx. 450 bytes) encoded inside a QR code :) Aim the camera on your smartphone and follow the link.
cd ./Examples/BlazorWasmSkiaTest
dotnet run
and open browser at http://localhost:5000.
Some example 6502 assembly programs running in the emulator from a standard OS console application.
- Run16bitMultiplyProgram.cs
- HostInteractionLab_Scroll_Text.cs
- etc.
A machine code monitor console application for the Highbyte.DotNet6502 emulator library. It allows for some basic interaction with the emulator.
- Clone this repo
git clone https://github.com/highbyte/dotnet-6502.git
- Change dir to monitor application
cd dotnet-6502/Highbyte.DotNet6502.Monitor
dotnet run
- Compile the source code with
dotnet build
- Run executable
- Windows:
.\bin\Debug\net6.0\Highbyte.DotNet6502.Monitor.exe
- Linux:
./bin/Debug/net6.0/Highbyte.DotNet6502.Monitor
- Mac:
./bin/Debug/net6.0/Highbyte.DotNet6502.Monitor
- Windows:
Type ?|help|-?|--help
to list commands.
> ?
Usage: [command]
Commands:
d Disassembles 6502 code from emulator memory.
f Fill memory att specified address with a list of bytes. Example: f 1000 20 ff ab 30
g Change the PC (Program Counter) to the specified address and execute code.
l Load a 6502 binary into emulator memory.
m Show contents of emulator memory in bytes.
q Quit monitor.
r Show processor status and registers. CY = #cycles executed.
z Single step through instructions. Optionally execute a specified number of instructions.
Type [command] -?|--help
to list help on specific command.
Example on help for d
(disassemble) command:
> d -?
Usage: d [options] <start> <end>
Arguments:
start Start address (hex). If not specified, the current PC address is used.
end End address (hex). If not specified, a default number of addresses will be shown from start.
Options:
-?|-h|--help Show help information.
Example how to load binary with l
command:
The machine code binary simple.prg adds two number from memory, divides by 2, stores it in another memory location
> l C:\Source\dotnet-6502\.cache\Example\ConsoleTestPrograms\AssemblerSource\simple.prg
File loaded at 0xC000
Example how to disassemble with d
command:
Shows what the code in simple.prg does
> d c000 c010
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
c00c 00 BRK
c00d 00 BRK
c00e 00 BRK
c00f 00 BRK
c010 00 BRK
Example how to fill bytes in memory with f
command:
Sets value A and B in memory locations (d000 and d001) that simple.prg uses
> f d000 12 30
Example how to set PC (Program Counter) with r pc
command:
Sets PC at load address of simple.prg
> r pc c000
SP=00 PC=C000
Example how to execute g
command:
Executes simple.prg, stops on BRK instruction
> g c000
Will stop on BRK instruction.
Staring executing code at c000
Stopped at 0000
c00b 00 BRK
Example how to show contents of bytes in memory with m
command:
Inspects values A (d000), B (d001), and result (d002)
> m d000 d002
d000 12 30 21
- Use Windows, Linux, or Mac.
- .NET 6 SDK installed.
- Develop in VSCode (Windows, Linux, Mac), Visual Studio 2019 (Windows), or other preferred editor.
Most of the code has been developed with a test-first approach.
The XUnit library is used.
To run only unit tests:
dotnet test --filter TestType!=Integration
There is a special XUnit test that is running the test code found here: Functional 6502 test program
The purpose of it is to verify if an emulator (or real computer) executes the 6502 instructions correctly.
Notes on the special functional/integration XUnit test
- it downloads the Functional 6502 test program from the repo
- it modifies the .asm source code to disable Decimal tests
- it compiles the .asm source code with the AS65 assembler (bundled in the repo above)
- it loads the compiled 6502 binary into this emulator, and runs it.
- this emulator is configured to stop executing when the program reaches a specific "success" address (where the program loops forever).
- it currently requires a Windows machine to run due to the AS65 assembler. There may be a Java-version of it that could possibly be used instead.
To run only the special functional/integration XUnit test:
dotnet test --filter TestType=Integration
Install report-generator global tool
dotnet tool install -g dotnet-reportgenerator-globaltool
Generate and show reports (Windows)
./codecov-browser.ps1
./codecov-console.ps1
Generate and show reports (Linux)
chmod +x ./codecov-console.sh
./codecov-console.sh
- http://www.obelisk.me.uk/6502/index.html
- https://www.atariarchives.org/alp/appendix_1.php
- http://www.6502.org/tutorials/compare_beyond.html
- https://www.c64-wiki.com/wiki/Reset_(Process)
- https://www.c64-wiki.com/wiki/BRK
- https://sta.c64.org/cbm64mem.html
- http://www.emulator101.com/6502-addressing-modes.html
- https://www.pagetable.com/?p=410
- http://visual6502.org/wiki/index.php?title=6502TestPrograms
- https://github.com/Klaus2m5/6502_65C02_functional_tests/blob/master/6502_functional_test.a65
- http://www.csharp4u.com/2017/01/getting-pretty-hex-dump-of-binary-file.html?m=1
Was used during development to compile actual 6502 source code to a binary, and then run it through the emulator.
- https://marketplace.visualstudio.com/items?itemName=rosc.vs64
- https://nurpax.github.io/c64jasm-browser/
- https://skilldrick.github.io/easy6502/#first-program
Was used during development to test how certain instructions worked when in doubt.
Monitor commands: https://vice-emu.sourceforge.io/vice_12.html
How to load and step through a program in the VICE monitor
l "C:\Source\Repos\dotnet-6502\.cache\ConsoleTestPrograms\AssemblerSource\testprogram.prg" 0 1000
d 1000
r PC=1000
z
r