From 4eeba4d94030473e99de719e858dd04701955e41 Mon Sep 17 00:00:00 2001 From: Ariejan de Vroom Date: Sun, 17 Aug 2014 10:31:28 +0200 Subject: [PATCH 1/4] Add package documentation --- cpu.go | 4 +++- doc.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 doc.go diff --git a/cpu.go b/cpu.go index 93ceef2..ccbbd17 100644 --- a/cpu.go +++ b/cpu.go @@ -15,10 +15,12 @@ type Cpu struct { } const ( + ZeropageBase = 0x0000 // 0x0000-00FF Reserved for zeropage instructions + StackBase = 0x0100 // 0x0100-01FF Reserved for stack + ResetVector = 0xFFFC // 0xFFFC-FFFD IrqVector = 0xFFFE // 0xFFFE-FFFF - StackBase = 0x0100 // One page 0x0100-01FF ) // Create an new Cpu instance with the specified AddressBus diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..d596848 --- /dev/null +++ b/doc.go @@ -0,0 +1,60 @@ +/* +The i6502 package contains all the components needed to construct +a working MOS 6502 emulated computer using different common parts, +like the MOS 6502, WDC 65C02, VIA 6522 and ACIA 6551. + +The CPU is the core of the system. It features 8-bit registers and +ALU, and can address 16-bit of memory. It features a 16-bit program +counter (PC) that indicates where from memory the next instruction will +be read. + +Besides the Cpu, there is also an AddressBus, which maps the 16-bit +address space to different attached components that implement the Memory +interface. Ram is one such component. + +Creating a new emulator is easy and straightforward. All that's required +is a Cpu, and AddressBus and attached components. + +Example: create an emulator using the full 64kB address space for RAM + + import "github.com/ariejan/i6502" + + // Create Ram, 64kB in size + ram, err := i6502.NewRam(0x10000) + + // Create the AddressBus + bus, err := i6502.NewAddressBus() + + // And attach the Ram at 0x0000 + bus.Attach(ram, 0x0000) + + // Create the Cpu, with the AddressBus + cpu, err := i6502.NewCpu(bus) + +The hardware pins `IRQ` and `RESB` are implemented and mapped to +the functions `Interrupt()` and `Reset()`. + +Running a program from RAM is possible by loading it into +memory at the specified address. Note that this also sets the +Program Counter to the beginning of the loaded program. + +Keep in mind that 0x00xx is reserved for Zeropage instructions and +0x01xx is reserved for the stack. + + import "io/ioutil" + + program, err := ioutil.ReadFile(path) + + // This will load the program (if it fits within memory) + // at 0x0200 and set cpu.PC to 0x0200 as well. + cpu.LoadProgram(program, 0x0200) + +Running a program is as easy as calling `cpu.Step()`, which will +read and execute a single instruction. + + go for { + cpu.Step() + }() + +*/ +package i6502 From 22432de785aaba80dbdd9155c59c6f35643f8bb5 Mon Sep 17 00:00:00 2001 From: Ariejan de Vroom Date: Sun, 17 Aug 2014 13:07:22 +0200 Subject: [PATCH 2/4] Add docs for AddressBus; minor refactorings --- address_bus.go | 67 ++++++++++++++++++++++++++++++++++----------- address_bus_test.go | 4 +-- doc.go | 53 +++++++++++++++++++++++------------ 3 files changed, 89 insertions(+), 35 deletions(-) diff --git a/address_bus.go b/address_bus.go index 05d28b8..dec8917 100644 --- a/address_bus.go +++ b/address_bus.go @@ -4,6 +4,12 @@ import ( "fmt" ) +/* +The AddressBus contains a list of all attached memory components, +like Ram, Rom and IO. It takes care of mapping the global 16-bit +address space of the Cpu to the relative memory addressing of +each component. +*/ type AddressBus struct { addressables []addressable // Different components } @@ -14,14 +20,16 @@ type addressable struct { end uint16 // Last address in address space } -func NewAddressBus() (*AddressBus, error) { - return &AddressBus{addressables: make([]addressable, 0)}, nil -} - func (a *addressable) String() string { return fmt.Sprintf("\t0x%04X-%04X\n", a.start, a.end) } +// Creates a new, empty 16-bit AddressBus +func NewAddressBus() (*AddressBus, error) { + return &AddressBus{addressables: make([]addressable, 0)}, nil +} + +// Returns a string with details about the AddressBus and attached memory func (a *AddressBus) String() string { output := "Address Bus:\n" @@ -32,10 +40,15 @@ func (a *AddressBus) String() string { return output } -func (a *AddressBus) AddressablesCount() int { - return len(a.addressables) -} +/* +Attach the given Memory at the specified memory offset. +To attach 16kB ROM at 0xC000-FFFF, you simple attach the Rom at +address 0xC000, the Size of the Memory determines the end-address. + + rom, _ := i6502.NewRom(0x4000) + bus.Attach(rom, 0xC000) +*/ func (a *AddressBus) Attach(memory Memory, offset uint16) { start := offset end := offset + memory.Size() - 1 @@ -44,16 +57,11 @@ func (a *AddressBus) Attach(memory Memory, offset uint16) { a.addressables = append(a.addressables, addressable) } -func (a *AddressBus) addressableForAddress(address uint16) (*addressable, error) { - for _, addressable := range a.addressables { - if addressable.start <= address && addressable.end >= address { - return &addressable, nil - } - } - - return nil, fmt.Errorf("No addressable memory found at 0x%04X", address) -} +/* +Read an 8-bit value from Memory attached at the 16-bit address. +This will panic if you try to read from an address that has no Memory attached. +*/ func (a *AddressBus) Read(address uint16) byte { addressable, err := a.addressableForAddress(address) if err != nil { @@ -63,6 +71,11 @@ func (a *AddressBus) Read(address uint16) byte { return addressable.memory.Read(address - addressable.start) } +/* +Convenience method to quickly read a 16-bit value from address and address + 1. + +Note that we first read the LOW byte from address and then the HIGH byte from address + 1. +*/ func (a *AddressBus) Read16(address uint16) uint16 { lo := uint16(a.Read(address)) hi := uint16(a.Read(address + 1)) @@ -70,6 +83,12 @@ func (a *AddressBus) Read16(address uint16) uint16 { return (hi << 8) | lo } +/* +Write an 8-bit value to the Memory at the 16-bit address. + +This will panic if you try to write to an address that has no Memory attached or +Memory that is read-only, like Rom. +*/ func (a *AddressBus) Write(address uint16, data byte) { addressable, err := a.addressableForAddress(address) if err != nil { @@ -79,7 +98,23 @@ func (a *AddressBus) Write(address uint16, data byte) { addressable.memory.Write(address-addressable.start, data) } +/* +Convenience method to quickly write a 16-bit value to address and address + 1. + +Note that the LOW byte will be stored in address and the high byte in address + 1. +*/ func (a *AddressBus) Write16(address uint16, data uint16) { a.Write(address, byte(data)) a.Write(address+1, byte(data>>8)) } + +// Returns the addressable for the specified address, or an error if no addressable exists. +func (a *AddressBus) addressableForAddress(address uint16) (*addressable, error) { + for _, addressable := range a.addressables { + if addressable.start <= address && addressable.end >= address { + return &addressable, nil + } + } + + return nil, fmt.Errorf("No addressable memory found at 0x%04X", address) +} diff --git a/address_bus_test.go b/address_bus_test.go index 88b9edb..5158435 100644 --- a/address_bus_test.go +++ b/address_bus_test.go @@ -13,7 +13,7 @@ func TestEmptyAddressBus(t *testing.T) { assert.Nil(err) if assert.NotNil(bus) { - assert.Equal(0, bus.AddressablesCount()) + assert.Equal(0, len(bus.addressables)) } } @@ -24,7 +24,7 @@ func TestAttachToAddressBus(t *testing.T) { ram, _ := NewRam(0x10000) bus.Attach(ram, 0x0000) - assert.Equal(1, bus.AddressablesCount()) + assert.Equal(1, len(bus.addressables)) } func TestBusReadWrite(t *testing.T) { diff --git a/doc.go b/doc.go index d596848..e4f393f 100644 --- a/doc.go +++ b/doc.go @@ -1,19 +1,35 @@ /* The i6502 package contains all the components needed to construct a working MOS 6502 emulated computer using different common parts, -like the MOS 6502, WDC 65C02, VIA 6522 and ACIA 6551. +like the MOS 6502 or WDC 65C02, VIA 6522 (parallel I/O) and +ACIA 6551 (serial I/O). -The CPU is the core of the system. It features 8-bit registers and -ALU, and can address 16-bit of memory. It features a 16-bit program -counter (PC) that indicates where from memory the next instruction will -be read. +The CPU is the core of the system. It features an 8-bit accumulator (A) +and two general purpose 8-bit index registers (X, Y). There is a +16-bit program counter (PC). The 8-bit stack pointer (SP) points to +the 0x0100-0x1FF address space moves downward. The status register (P) +contains bits indicating Zero, Negative, Break, Decimal, IrqDisable, +Carry and Overflow conditions. The 6502 uses a 16-bit address bus to +access 8-bit data values. -Besides the Cpu, there is also an AddressBus, which maps the 16-bit -address space to different attached components that implement the Memory -interface. Ram is one such component. +The AddressBus can be used to attach different components to different +parts of the 16-bit address space, accessible by the 6502. Common +layouts are -Creating a new emulator is easy and straightforward. All that's required -is a Cpu, and AddressBus and attached components. + * 64kB RAM at 0x0000-FFFF + +Or + + * 32kB RAM at 0x0000-7FFF + * VIA 6522 at 0x8000-800F + * ACIA 6551 at 0x8800-8803 + * 16kB ROM at 0xC000-FFFF + +Creating a new emulated machine entails three steps: + + 1. Create the different memory components (Ram, Rom, IO) + 2. Create the AddressBus and attach memory + 3. Create the Cpu with the AddressBus Example: create an emulator using the full 64kB address space for RAM @@ -34,12 +50,12 @@ Example: create an emulator using the full 64kB address space for RAM The hardware pins `IRQ` and `RESB` are implemented and mapped to the functions `Interrupt()` and `Reset()`. -Running a program from RAM is possible by loading it into -memory at the specified address. Note that this also sets the -Program Counter to the beginning of the loaded program. +Running a program from memory can be done by loading the binary +data into memory using `LoadProgram`. Keep in mind that the first +two memory pages (0x0000-01FF) are reserved for zeropage and stack +memory. -Keep in mind that 0x00xx is reserved for Zeropage instructions and -0x01xx is reserved for the stack. +Example of loading a binary program from disk into memory: import "io/ioutil" @@ -49,8 +65,11 @@ Keep in mind that 0x00xx is reserved for Zeropage instructions and // at 0x0200 and set cpu.PC to 0x0200 as well. cpu.LoadProgram(program, 0x0200) -Running a program is as easy as calling `cpu.Step()`, which will -read and execute a single instruction. +With all memory connected and a program loaded, all that's left +is executing instructions on the Cpu. A single call to `Step()` will +read and execute a single (1, 2 or 3 byte) instruction from memory. + +To create a Cpu and have it running, simple create a go-routine. go for { cpu.Step() From e77a599b1037edd114426c17e1b91912922161c2 Mon Sep 17 00:00:00 2001 From: Ariejan de Vroom Date: Sun, 17 Aug 2014 15:53:35 +0200 Subject: [PATCH 3/4] Add more documentation --- cpu.go | 47 +++++++++++++++++++++++++++++++++++------------ cpu_test.go | 3 +++ instruction.go | 14 +++++--------- memory.go | 4 ++++ opcodes.go | 18 +++++------------- ram.go | 4 ++++ 6 files changed, 56 insertions(+), 34 deletions(-) diff --git a/cpu.go b/cpu.go index ccbbd17..a53d5cf 100644 --- a/cpu.go +++ b/cpu.go @@ -2,15 +2,25 @@ package i6502 import "fmt" +/* +The Cpu only contains the AddressBus, through which 8-bit values can be read and written +at 16-bit addresses. + +The Cpu has an 8-bit accumulator (A) and two 8-bit index registers (X,Y). There is a 16-bit +Program Counter (PC) and an 8-bit Stack Pointer (SP), pointing to addresses in 0x0100-01FF. + +The status register (P) contains flags for Zero, Negative, Break, Decimal, IrqDisable, +Carry and Overflow flags. +*/ type Cpu struct { + A byte // Accumulator + X byte // Index register X + Y byte // Index register Y + PC uint16 // 16-bit program counter P byte // Status Register SP byte // Stack Pointer - A byte // Accumulator - X byte // X index register - Y byte // Y index register - Bus *AddressBus // The address bus } @@ -23,21 +33,27 @@ const ( ) -// Create an new Cpu instance with the specified AddressBus +// Create an new Cpu, using the AddressBus for accessing memory. func NewCpu(bus *AddressBus) (*Cpu, error) { return &Cpu{Bus: bus}, nil } +// Returns a string containing the current state of the CPU. func (c *Cpu) String() string { str := ">>> CPU [ A ] [ X ] [ Y ] [ SP ] [ PC ] NVxBDIZC\n>>> 0x%02X 0x%02X 0x%02X 0x%02X 0x%04X %08b\n" return fmt.Sprintf(str, c.A, c.X, c.Y, c.SP, c.PC, c.P) } -func (c *Cpu) hasAddressBus() bool { - return c.Bus != nil -} +/* +Reset the CPU, emulating the RESB pin. -// Reset the CPU, emulating the RESB pin. +The status register is reset to a know state (0x34, IrqDisabled set, Decimal unset, Break set). + +Then the Program Counter is set to the value read from `ResetVector` (0xFFFC-FFFD). + +Normally, no assumptions can be made about registers (A, X, Y) and the +Stack Pointer. For convenience, these are reset to 0x00 (A,X,Y) and 0xFF (SP). +*/ func (c *Cpu) Reset() { c.PC = c.Bus.Read16(ResetVector) c.P = 0x34 @@ -49,11 +65,17 @@ func (c *Cpu) Reset() { c.SP = 0xFF } -// Simulate the IRQ pin +/* +Simulate the IRQ pin. + +This will push the current Cpu state to the stack (P + PC) and set the PC +to the address read from the `IrqVector` (0xFFFE-FFFF) +*/ func (c *Cpu) Interrupt() { c.handleIrq(c.PC) } +// Handles an interrupt or BRK. func (c *Cpu) handleIrq(PC uint16) { c.stackPush(byte(PC >> 8)) c.stackPush(byte(PC)) @@ -65,7 +87,7 @@ func (c *Cpu) handleIrq(PC uint16) { } // Load the specified program data at the given memory location -// and point the Program Counter to the beginning of the program +// and point the Program Counter to the beginning of the program. func (c *Cpu) LoadProgram(data []byte, location uint16) { for i, b := range data { c.Bus.Write(location+uint16(i), b) @@ -74,13 +96,14 @@ func (c *Cpu) LoadProgram(data []byte, location uint16) { c.PC = location } -// Execute the instruction pointed to by the Program Counter (PC) +// Read and execute the instruction pointed to by the Program Counter (PC) func (c *Cpu) Step() { instruction := c.readNextInstruction() c.PC += uint16(instruction.Size) c.execute(instruction) } +// Handle the execution of an instruction func (c *Cpu) execute(instruction Instruction) { switch instruction.opcodeId { case nop: diff --git a/cpu_test.go b/cpu_test.go index 0bf88d3..f4bfccc 100644 --- a/cpu_test.go +++ b/cpu_test.go @@ -76,6 +76,9 @@ func TestCpuReset(t *testing.T) { // **1101** is specified, but we are satisfied with // 00110100 here. assert.Equal(0x34, cpu.P) + assert.True(cpu.getIrqDisable()) + assert.False(cpu.getDecimal()) + assert.True(cpu.getBreak()) // Read PC from $FFFC-FFFD assert.Equal(0x1234, cpu.PC) diff --git a/instruction.go b/instruction.go index f4ae53d..1d714df 100644 --- a/instruction.go +++ b/instruction.go @@ -5,19 +5,15 @@ import ( ) type Instruction struct { - // Embed OpType - OpType + OpType // Embed OpType - // 8-bit operand for 2-byte instructions - Op8 byte + Op8 byte // 8-bit operand for 2-byte instructions + Op16 uint16 // 16-bit operand for 3-byte instructions - // 16-bit operand for 3-byte instructions - Op16 uint16 - - // Address location where this instruction got read - Address uint16 + Address uint16 // Address location where this instruction got read, for debugging purposes } +// Return a string containing debug information about the instruction and operands. func (i Instruction) String() (output string) { switch i.Size { case 1: diff --git a/memory.go b/memory.go index 84bd5ee..d55c259 100644 --- a/memory.go +++ b/memory.go @@ -1,5 +1,9 @@ package i6502 +/* +Anything implementing the Memory interface can be attached to the AddressBus +and become accessible by the Cpu. +*/ type Memory interface { Size() uint16 Read(address uint16) byte diff --git a/opcodes.go b/opcodes.go index 3cbfab9..f14a257 100644 --- a/opcodes.go +++ b/opcodes.go @@ -160,20 +160,12 @@ var instructionNames = [...]string{ // addressing mode. It also includes some extra information for the // emulator, like number of cycles type OpType struct { - // The actual Opcode byte read from memory - Opcode byte + Opcode byte // 65(C)02 Opcode, this includes an instruction and addressing mode + Size uint8 // Size of the entire instruction in bytes + Cycles uint8 // Number of clock cycles required to complete this instruction - // Opcode ID - opcodeId uint8 - - // Addressing Mode ID - addressingId uint8 - - // Size of this instruction, either 1, 2 or 3 bytes - Size uint8 - - // Number of clock cycles this instruction needs - Cycles uint8 + opcodeId uint8 // Decoded opcode Id, + addressingId uint8 // Decoded address mode Id } var opTypes = map[uint8]OpType{ diff --git a/ram.go b/ram.go index 8b3ea29..7ceabbb 100644 --- a/ram.go +++ b/ram.go @@ -1,9 +1,13 @@ package i6502 +/* +Random Access Memory, read/write, can be of any size. +*/ type Ram struct { data []byte } +// Create a new Ram component of the given size. func NewRam(size int) (*Ram, error) { return &Ram{data: make([]byte, size)}, nil } From c361eedc46cae3dbde83b8df83f45246ecad4079 Mon Sep 17 00:00:00 2001 From: Ariejan de Vroom Date: Sun, 17 Aug 2014 15:57:12 +0200 Subject: [PATCH 4/4] Fix OpType attribute ordering --- opcodes.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/opcodes.go b/opcodes.go index f14a257..f595303 100644 --- a/opcodes.go +++ b/opcodes.go @@ -160,12 +160,13 @@ var instructionNames = [...]string{ // addressing mode. It also includes some extra information for the // emulator, like number of cycles type OpType struct { - Opcode byte // 65(C)02 Opcode, this includes an instruction and addressing mode - Size uint8 // Size of the entire instruction in bytes - Cycles uint8 // Number of clock cycles required to complete this instruction + Opcode byte // 65(C)02 Opcode, this includes an instruction and addressing mode opcodeId uint8 // Decoded opcode Id, addressingId uint8 // Decoded address mode Id + + Size uint8 // Size of the entire instruction in bytes + Cycles uint8 // Number of clock cycles required to complete this instruction } var opTypes = map[uint8]OpType{