- implemented BRK and RTI instructions - not fully tested
    - tidied up when NoSideEffect is checked

o debugger
    - fixed breakAndTrapCallback
    - fixed instruction result validity check crash caused when
	instruction step resulted a runtime error

o cartridge
    - added support for half-size cartridges
    - tidied up error handling

o various commentary cleanups/clarifications

o move test files to test/ directory
This commit is contained in:
steve 2018-11-19 17:59:53 +00:00
parent 4613bcc4cf
commit 49d429b6bf
16 changed files with 261 additions and 136 deletions

View file

@ -32,10 +32,12 @@ type Debugger struct {
runUntilHalt bool
// halt conditions
// note that the UI probably allows the user to halt (eg. ctrl-c)
breakpoints *breakpoints
traps *traps
// note that the UI probably allows the user to manually break or trap at
// will, with for example, ctrl-c
// we accumulate break and trap messsages until we can service them
breakMessages string
trapMessages string
@ -54,7 +56,7 @@ type Debugger struct {
commandOnHalt string
commandOnHaltStored string
// similarly, commandOnStep is the sequence of commands to run afer ever
// similarly, commandOnStep is the sequence of commands to run afer every
// cpu/video cycle
commandOnStep string
commandOnStepStored string
@ -239,7 +241,7 @@ func (dbg *Debugger) loadCartridge(cartridgeFilename string) error {
// breakandtrapCallback()
func (dbg *Debugger) videoCycleCallback(result *result.Instruction) error {
dbg.breakandtrapCallback(result)
dbg.breakAndTrapCallback(result)
dbg.lastResult = result
if dbg.commandOnStep != "" {
_, err := dbg.parseInput(dbg.commandOnStep)
@ -250,13 +252,15 @@ func (dbg *Debugger) videoCycleCallback(result *result.Instruction) error {
return dbg.inputLoop(false)
}
func (dbg *Debugger) breakandtrapCallback(result *result.Instruction) error {
func (dbg *Debugger) breakAndTrapCallback(result *result.Instruction) error {
// because we call this callback mid-instruction, the programme counter
// maybe in it's non-final state - we don't want to break or trap in these
// instances if the final effect of the instruction changes the programme
// counter to some other value
if (result.Defn.Effect == definitions.Flow || result.Defn.Effect == definitions.Subroutine) && !result.Final {
return nil
if result.Defn != nil {
if (result.Defn.Effect == definitions.Flow || result.Defn.Effect == definitions.Subroutine) && !result.Final {
return nil
}
}
dbg.breakMessages = dbg.breakpoints.check(dbg.breakMessages)
@ -393,7 +397,7 @@ func (dbg *Debugger) inputLoop(mainLoop bool) error {
if dbg.inputloopVideoClock {
_, dbg.lastResult, err = dbg.vcs.Step(dbg.videoCycleCallback)
} else {
_, dbg.lastResult, err = dbg.vcs.Step(dbg.breakandtrapCallback)
_, dbg.lastResult, err = dbg.vcs.Step(dbg.breakAndTrapCallback)
}
if err != nil {
@ -409,15 +413,15 @@ func (dbg *Debugger) inputLoop(mainLoop bool) error {
default:
return err
}
}
// check validity of instruction result
if dbg.lastResult.Final {
err := dbg.lastResult.IsValid()
if err != nil {
fmt.Println(dbg.lastResult.Defn)
fmt.Println(dbg.lastResult)
panic(err)
} else {
// check validity of instruction result
if dbg.lastResult.Final {
err := dbg.lastResult.IsValid()
if err != nil {
fmt.Println(dbg.lastResult.Defn)
fmt.Println(dbg.lastResult)
panic(err)
}
}
}

View file

@ -26,9 +26,9 @@ const (
UnrecognisedAddress
// Cartridges
CartridgeFileCannotOpen
CartridgeFileError
CartridgeInvalidSize
CartridgeUnsupported
CartridgeMissing
// TV
UnknownTVRequest

View file

@ -24,9 +24,9 @@ var messages = map[Errno]string{
UnrecognisedAddress: "address unrecognised (%v)",
// Cartridges
CartridgeFileCannotOpen: "cannot open cartridge (%s)",
CartridgeFileError: "error reading cartridge file (%s)",
CartridgeInvalidSize: "cartridge size is not recognised (%d)",
CartridgeFileError: "error reading cartridge file (%s)",
CartridgeUnsupported: "cartridge unsupported (%s)",
CartridgeMissing: "no cartridge attached",
// TV
UnknownTVRequest: "TV does not support %v request",

View file

@ -2,7 +2,9 @@ package cpu
// TODO List
// ---------
// . NMOS indexed addressing extra read when crossing page boundaries
// o NMOS indexed addressing extra read when crossing page boundaries
// o check that NoSideEffects is consistent in its intention
// o check that all calls to endCycle() occur when they're supposed to
import (
"fmt"
@ -14,6 +16,8 @@ import (
"log"
)
const irqInterruptVector = 0xfffe
// CPU is the main container structure for the package
type CPU struct {
PC *register.Register
@ -139,7 +143,14 @@ func (mc *CPU) LoadPC(indirectAddress uint16) error {
return nil
}
// note that write8Bit, unline read8Bit(), does not call endCycle() this is
// because we need to differentiate between different addressing modes at
// different times.
func (mc *CPU) write8Bit(address uint16, value uint8) error {
if mc.NoSideEffects {
return nil
}
err := mc.mem.Write(address, value)
if err != nil {
@ -158,6 +169,7 @@ func (mc *CPU) write8Bit(address uint16, value uint8) error {
return nil
}
// note that read8Bit calls endCycle as appropriate
func (mc *CPU) read8Bit(address uint16) (uint8, error) {
val, err := mc.mem.Read(address)
@ -352,11 +364,21 @@ func (mc *CPU) ExecuteInstruction(cycleCallback func(*result.Instruction)) (*res
// implied mode does not use any additional bytes. however, the next
// instruction is read but the PC is not incremented
// phantom read
// +1 cycle
_, err := mc.read8Bit(mc.PC.ToUint16())
if err != nil {
return nil, err
if defn.Mnemonic == "BRK" {
// BRK is unusual in that it increases the PC by two bytes despite
// being an implied addressing mode.
// +1 cycle
_, err = mc.read8BitPC()
if err != nil {
return nil, err
}
} else {
// phantom read
// +1 cycle
_, err := mc.read8Bit(mc.PC.ToUint16())
if err != nil {
return nil, err
}
}
case definitions.Immediate:
@ -627,12 +649,10 @@ func (mc *CPU) ExecuteInstruction(cycleCallback func(*result.Instruction)) (*res
// phantom write
// +1 cycle
if !mc.NoSideEffects {
err = mc.write8Bit(address, value)
err = mc.write8Bit(address, value)
if err != nil {
return nil, err
}
if err != nil {
return nil, err
}
mc.endCycle()
}
@ -665,11 +685,9 @@ func (mc *CPU) ExecuteInstruction(cycleCallback func(*result.Instruction)) (*res
mc.Status.Overflow = false
case "PHA":
if !mc.NoSideEffects {
err = mc.write8Bit(mc.SP.ToUint16(), mc.A.ToUint8())
if err != nil {
return nil, err
}
err = mc.write8Bit(mc.SP.ToUint16(), mc.A.ToUint8())
if err != nil {
return nil, err
}
mc.SP.Add(255, false)
@ -685,11 +703,9 @@ func (mc *CPU) ExecuteInstruction(cycleCallback func(*result.Instruction)) (*res
mc.A.Load(value)
case "PHP":
if !mc.NoSideEffects {
err = mc.write8Bit(mc.SP.ToUint16(), mc.Status.ToUint8())
if err != nil {
return nil, err
}
err = mc.write8Bit(mc.SP.ToUint16(), mc.Status.ToUint8())
if err != nil {
return nil, err
}
mc.SP.Add(255, false)
@ -764,27 +780,21 @@ func (mc *CPU) ExecuteInstruction(cycleCallback func(*result.Instruction)) (*res
mc.Status.Sign = mc.Y.IsNegative()
case "STA":
if !mc.NoSideEffects {
err = mc.write8Bit(address, mc.A.ToUint8())
if err != nil {
return nil, err
}
err = mc.write8Bit(address, mc.A.ToUint8())
if err != nil {
return nil, err
}
case "STX":
if !mc.NoSideEffects {
err = mc.write8Bit(address, mc.X.ToUint8())
if err != nil {
return nil, err
}
err = mc.write8Bit(address, mc.X.ToUint8())
if err != nil {
return nil, err
}
case "STY":
if !mc.NoSideEffects {
err = mc.write8Bit(address, mc.Y.ToUint8())
if err != nil {
return nil, err
}
err = mc.write8Bit(address, mc.Y.ToUint8())
if err != nil {
return nil, err
}
case "INX":
@ -985,24 +995,20 @@ func (mc *CPU) ExecuteInstruction(cycleCallback func(*result.Instruction)) (*res
// +1 cycle
mc.endCycle()
if !mc.NoSideEffects {
// push MSB of PC onto stack, and decrement SP
// +1 cycle
err = mc.write8Bit(mc.SP.ToUint16(), uint8((mc.PC.ToUint16()&0xFF00)>>8))
if err != nil {
return nil, err
}
// push MSB of PC onto stack, and decrement SP
// +1 cycle
err = mc.write8Bit(mc.SP.ToUint16(), uint8((mc.PC.ToUint16()&0xFF00)>>8))
if err != nil {
return nil, err
}
mc.SP.Add(255, false)
mc.endCycle()
if !mc.NoSideEffects {
// push LSB of PC onto stack, and decrement SP
// +1 cycle
err = mc.write8Bit(mc.SP.ToUint16(), uint8(mc.PC.ToUint16()&0x00FF))
if err != nil {
return nil, err
}
// push LSB of PC onto stack, and decrement SP
// +1 cycle
err = mc.write8Bit(mc.SP.ToUint16(), uint8(mc.PC.ToUint16()&0x00FF))
if err != nil {
return nil, err
}
mc.SP.Add(255, false)
mc.endCycle()
@ -1047,15 +1053,69 @@ func (mc *CPU) ExecuteInstruction(cycleCallback func(*result.Instruction)) (*res
}
case "BRK":
// TODO: implement BRK
// push PC onto register (same effect as JSR)
err := mc.write8Bit(mc.SP.ToUint16(), uint8((mc.PC.ToUint16()&0xFF00)>>8))
if err != nil {
return nil, err
}
mc.SP.Add(255, false)
mc.endCycle()
err = mc.write8Bit(mc.SP.ToUint16(), uint8(mc.PC.ToUint16()&0x00FF))
if err != nil {
return nil, err
}
mc.SP.Add(255, false)
mc.endCycle()
// push status register (same effect as PHP)
err = mc.write8Bit(mc.SP.ToUint16(), mc.Status.ToUint8())
if err != nil {
return nil, err
}
mc.SP.Add(255, false)
mc.endCycle()
// set the break flag
mc.Status.Break = true
// perform jump
brkAddress, err := mc.read16Bit(irqInterruptVector)
if err != nil {
return nil, err
}
if !mc.NoSideEffects {
mc.PC.Load(brkAddress)
}
case "RTI":
// TODO: implement RTI
// pull status register (same effect as PLP)
mc.SP.Add(1, false)
mc.endCycle()
value, err = mc.read8Bit(mc.SP.ToUint16())
if err != nil {
return nil, err
}
mc.Status.FromUint8(value)
// pull program counter (same effect as RTS)
if !mc.NoSideEffects {
mc.SP.Add(1, false)
mc.endCycle()
rtiAddress, err := mc.read16Bit(mc.SP.ToUint16())
if err != nil {
return nil, err
}
mc.SP.Add(1, false)
mc.PC.Load(rtiAddress)
mc.PC.Add(1, false)
mc.endCycle()
}
// undocumented instructions
case "dop":
// does nothing
// does nothing (2 byte nop)
case "lax":
mc.A.Load(value)
@ -1077,13 +1137,12 @@ func (mc *CPU) ExecuteInstruction(cycleCallback func(*result.Instruction)) (*res
// for RMW instructions: write altered value back to memory
if defn.Effect == definitions.RMW {
if !mc.NoSideEffects {
err = mc.write8Bit(address, value)
if err != nil {
return nil, err
err = mc.write8Bit(address, value)
if err != nil {
return nil, err
}
}
// +1 cycle
mc.endCycle()
}

View file

@ -2,6 +2,8 @@ package result
import "fmt"
// columnise forces the string into the given width. used for outputting
// disassembly into columns
func columnise(s string, width int) string {
if width > len(s) {
t := make([]byte, width-len(s))

View file

@ -1,28 +1,17 @@
package memory
import (
"fmt"
"gopher2600/errors"
"os"
)
// MissingCartridgeError returned by those functions that really require a
// cartridge to be inserted.
type MissingCartridgeError struct{}
func (MissingCartridgeError) Error() string {
return "no cartridge attached"
}
const bankSize = 4096
// Cartridge defines the information and operations for a VCS cartridge
type Cartridge struct {
CPUBus
Area
AreaInfo
memory []uint8
bank uint16
}
// newCart is the preferred method of initialisation for the cartridges
@ -58,10 +47,9 @@ func (cart *Cartridge) Clear() {
// Implementation of CPUBus.Read
func (cart Cartridge) Read(address uint16) (uint8, error) {
if len(cart.memory) == 0 {
return 0, new(MissingCartridgeError)
return 0, errors.NewGopherError(errors.CartridgeMissing)
}
oa := address - cart.origin
oa += cart.bank * bankSize
return cart.memory[oa], nil
}
@ -74,7 +62,7 @@ func (cart *Cartridge) Write(address uint16, data uint8) error {
func (cart *Cartridge) Attach(filename string) error {
cf, err := os.Open(filename)
if err != nil {
return errors.NewGopherError(errors.CartridgeFileCannotOpen, err)
return errors.NewGopherError(errors.CartridgeFileError, err)
}
defer func() {
_ = cf.Close()
@ -86,26 +74,77 @@ func (cart *Cartridge) Attach(filename string) error {
return err
}
// check that cartridge is of a supported size
// TODO: ensure that this is a complete and accurate check
if cfi.Size()%bankSize != 0 {
return errors.NewGopherError(errors.CartridgeFileCannotOpen, cfi.Size())
}
switch cfi.Size() {
case 4096:
// this is a regular cartridge of 4096 bytes
// o Pitfall
// o Advenure
// o Yars Revenge
// o most 2600 cartridges...
// for this cartridge type we simply read the entire cartridge into a
// memory allocation of 4096 bytes. there is no need for further memory
// mapping.
// allocate enough memory for new cartridge
cart.memory = make([]uint8, cfi.Size())
// allocate enough memory for new cartridge
cart.memory = make([]uint8, 4096)
// read cartridge
n, err := cf.Read(cart.memory)
if err != nil {
return err
}
if n != len(cart.memory) {
return errors.NewGopherError(errors.CartridgeFileError, errors.FileTruncated)
}
// read cartridge
n, err := cf.Read(cart.memory)
if err != nil {
return err
}
if n != 4096 {
return errors.NewGopherError(errors.CartridgeFileError, errors.FileTruncated)
}
// make sure we're pointing to the first bank
cart.bank = 0
case 2048:
// this is a half-size cartridge of 2048 bytes
// o Combat
// o Dragster
// o Outlaw
// o Surround
// o mostly early cartrdiges
// for this cartridge type we simply read the cartridge twice into a
// memory space of 4096. there is no need for further memory mappping
// using this method; however, POKEing into cartridge space will also
// need to be performed twice. as this isn't normal 2600 behaviour
// though, I'm not too concerned.
// allocate enough memory for new cartridge -- for now, allocate the
// full 4096 and read cartridge twice
cart.memory = make([]uint8, 4096)
// read cartridge
n, err := cf.Read(cart.memory[:2048])
if err != nil {
return err
}
if n != 2048 {
return errors.NewGopherError(errors.CartridgeFileError, errors.FileTruncated)
}
// read cartridge again (into second half of memory)
cf.Seek(0, 0)
n, err = cf.Read(cart.memory[2048:])
if err != nil {
return err
}
if n != 2048 {
return errors.NewGopherError(errors.CartridgeFileError, errors.FileTruncated)
}
case 8192:
return errors.NewGopherError(errors.CartridgeUnsupported, "8192 bytes not yet supported")
case 16384:
return errors.NewGopherError(errors.CartridgeUnsupported, "16384 bytes not yet supported")
case 32768:
return errors.NewGopherError(errors.CartridgeUnsupported, "32768 bytes not yet supported")
default:
return errors.NewGopherError(errors.CartridgeUnsupported, fmt.Sprintf("file size unrecognised %d bytes", cfi.Size()))
}
return nil
}
@ -114,15 +153,13 @@ func (cart *Cartridge) Attach(filename string) error {
// attaches a bank of empty memory - for convenience of the debugger
func (cart *Cartridge) Eject() {
cart.memory = make([]uint8, 4096)
cart.bank = 0
}
// Peek is the implementation of Memory.Area.Peek
func (cart Cartridge) Peek(address uint16) (uint8, uint16, string, string, error) {
if len(cart.memory) == 0 {
return 0, 0, "", "", new(MissingCartridgeError)
return 0, 0, "", "", errors.NewGopherError(errors.CartridgeMissing)
}
oa := address - cart.origin
oa += cart.bank * bankSize
return cart.memory[oa], address, cart.Label(), "", nil
}

View file

@ -3,8 +3,4 @@ package memory
import "testing"
func TestCartridge(t *testing.T) {
cart := newCart()
if bankSize != cart.memtop-cart.origin+1 {
t.Errorf("cartridge bank size and/or memtop/origin incorrectly defined")
}
}

View file

@ -82,7 +82,7 @@ func (riot *RIOT) ReadRIOTMemory() {
riot.timerINTIMvalue = value
riot.timerCycles = 2
default:
fmt.Printf("unserviced RIOT register (%v)", register)
fmt.Printf("unserviced RIOT register (%v)\n", register)
}
// write value to INTIM straight-away

View file

@ -27,6 +27,8 @@ func (col *collisions) clear() {
col.cxppmm = 0
}
// NOTE that collisions are detected in the video.Pixel() command
func (col *collisions) SetMemory(mem memory.ChipBus) {
mem.ChipWrite(vcssymbols.CXM0P, col.cxm0p)
mem.ChipWrite(vcssymbols.CXM1P, col.cxm1p)

View file

@ -26,6 +26,18 @@ type Video struct {
// okay because in all instances the delay is so short there is no chance
// of another write being scheduled before the previous request has been
// resolved
// TODO: I'm now not sure if the above statement is true. the BRK
// instruction for instance pushes values onto the stack very quickly. if
// the SP is inside register space then the BRK command can trigger these
// future writes. as it currently stands, the FutureWrite object will
// sanity-panic if a new event is scheduled before a previous event has
// completed. Should we remove the sanity-panic for the BRK instruction and
// allow write events to be lost? or should have a queue of FutureWrite
// events. The second option doesn't seem correct - from my rudimentary
// understanding of electronics that is. The sanity-panic isn't really
// needed of course, but I don't want to remove it completely just yet. The
// other conclusion we could mull over is that the schedule delay values
// are wrong.
FutureWrite future
}

View file

@ -210,7 +210,7 @@ func (vcs *VCS) Reset() error {
}
err := vcs.MC.LoadPC(AddressReset)
if _, ok := err.(*memory.MissingCartridgeError); !ok {
if err != nil {
return err
}

View file

@ -5,12 +5,13 @@ import (
"strings"
)
// TableID is used to select and identify a symbol table
type TableID int
// TableType is used to select and identify a symbol table
// when searching
type TableType int
// list of valid symbol tables
const (
UnspecifiedSymTable TableID = iota
UnspecifiedSymTable TableType = iota
LocationSymTable
ReadSymTable
WriteSymTable
@ -18,7 +19,7 @@ const (
// SearchSymbol return the address of the supplied symbol. search is
// case-insensitive
func (sym *Table) SearchSymbol(symbol string, table TableID) (TableID, string, uint16, error) {
func (sym *Table) SearchSymbol(symbol string, table TableType) (TableType, string, uint16, error) {
symbolUpper := strings.ToUpper(symbol)
if table == UnspecifiedSymTable || table == LocationSymTable {

View file

@ -26,7 +26,7 @@ func StandardSymbolTable() (*Table, error) {
table := new(Table)
table.ReadSymbols = vcssymbols.ReadSymbols
table.WriteSymbols = vcssymbols.WriteSymbols
table.genMaxWidth()
table.genMaxWidths()
return table, nil
}
@ -48,12 +48,14 @@ func ReadSymbolsFile(cartridgeFilename string) (*Table, error) {
table.WriteSymbols[k] = v
}
table.genMaxWidth()
table.genMaxWidths()
}()
// try to open symbols file
symFilename := cartridgeFilename
ext := path.Ext(symFilename)
// try to figure out the case of the file extension
if ext == ".BIN" {
symFilename = fmt.Sprintf("%s.SYM", symFilename[:len(symFilename)-len(ext)])
} else {
@ -73,7 +75,7 @@ func ReadSymbolsFile(cartridgeFilename string) (*Table, error) {
_ = sf.Close()
}()
// get file info
// get file info of symbols file
sfi, err := sf.Stat()
if err != nil {
return table, errors.NewGopherError(errors.SymbolsFileError, err)
@ -125,11 +127,8 @@ func ReadSymbolsFile(cartridgeFilename string) (*Table, error) {
return table, nil
}
func (table *Table) genMaxWidth() {
// get max width of symbol in each list -- it may seem that we could keep
// track of these width values as we go along but we can't really because
// the overwriting of previous symbols, during the loops over
// vcsRead/WriteSymbols above, causes havoc
// find the widest location and read/write symbol
func (table *Table) genMaxWidths() {
for _, s := range table.Locations {
if len(s) > table.MaxLocationWidth {
table.MaxLocationWidth = len(s)

View file

@ -6,6 +6,12 @@ import "gopher2600/errors"
// for tools that don't need a television or related information at all.
type DummyTV struct{ Television }
// NewDummyTV is the preferred method of initialisation for DummyTV - you can
// get away with an plain new(DummyTV) but this is probably more convenient
func NewDummyTV(tvType string, scale float32) (*DummyTV, error) {
return new(DummyTV), nil
}
// MachineInfoTerse (with DummyTV reciever) is the null implementation
func (DummyTV) MachineInfoTerse() string {
return ""

View file

@ -4,14 +4,19 @@ import (
"fmt"
"gopher2600/hardware"
"gopher2600/television"
"gopher2600/television/sdltv"
"testing"
)
func BenchmarkCPU(b *testing.B) {
func BenchmarkSDLTV(b *testing.B) {
var err error
tv := new(television.DummyTV)
if tv == nil {
tv, err := sdltv.NewSDLTV("NTSC", 1.0)
if err != nil {
panic(fmt.Errorf("error preparing television: %s", err))
}
err = tv.RequestSetAttr(television.ReqSetVisibility, true)
if err != nil {
panic(fmt.Errorf("error preparing television: %s", err))
}
@ -25,7 +30,9 @@ func BenchmarkCPU(b *testing.B) {
panic(err)
}
for steps := 1000000; steps >= 0; steps-- {
b.ResetTimer()
for steps := 0; steps < b.N; steps++ {
_, _, err = vcs.Step(hardware.StubVideoCycleCallback)
if err != nil {
panic(err)

BIN
test/test.test Executable file

Binary file not shown.