First, we need some memory to work with. At first, I'm just using 64 kB of read-write memory, but later on, we'll need to stick some memory mapped devices in there, so let's abstract that a little.
First, a type to represent addresses, which are just 16-bit integers on the 6502:
typealias Address = UInt16And an interface (called a protocol in Swift) for doing accesses with these addresses.
protocol DataBus {
// Non-destructive, for previews etc.
subscript(address: Address) -> UInt8 { get set }
// Destructive
func load(_ address: Address) -> UInt8
mutating func store(_ address: Address, _ value: UInt8)
}
Because in the future there may be memory mapped registers that do something when being read, I've split into destructive and non-destructive operations. Non-destructive reads for example, should just return the value, but without triggering any side-effects.1
For my first go, I've used an extension to allow the Swift Data type, which mostly just acts as an array of bytes, to allow subscripting with Address so it can conform to DataBus, and have a default implementation of load() and store() that just forwards to these, with some optional trace logging:
// Allow a Data to conform to the DataBus
extension Data: Hardware.DataBus {
subscript(address: Address) -> UInt8 {
get { self[Int(address)] }
set { self[Int(address)] = newValue }
}
}
// Default implementation for destructive load and store
extension Hardware.DataBus {
func load(_ address: Address) -> UInt8 {
let ret = self[address]
// logger.trace("\(address, format: .hex(minDigits: 4)) R \(ret, format: .hex(minDigits: 2))")
return ret
}
mutating func store(_ address: Address, _ value: UInt8) {
// logger.trace("\(address, format: .hex(minDigits: 4)) W \(value, format: .hex(minDigits: 2))")
self[address] = value
}
}
With Data now conforming, I could load a memory image from a file, asset catalog, the web, anywhere, and use it as a memory image on the systems DataBus. Later, I can make more complex implementations that model RAM, memory mapped registers, ROM, bank switching, etc.
Now my 6502 model looks like this:
@Observable
class Environment {
var reg = RegisterSet()
private var bus: DataBus = Data(repeating: 0, count: 65536)
}
Now it has the registers, and a memory space to work on. In retrospect, Environment is not a great name for my hardware environment, as SwiftUI has its own concept of the Environment that views share, which mildly conflicts. It is also marked @Observable, which allows SwiftUI to get notified when things change and update the Views. This turned out to have implications I will have to deal with later.
Lastly, to actually test my implementation, I found a lovely memory image to test which purports to test all the opcodes from within by Klaus Dormann, at his GitHub. I could take the precompiled binary, and drop it into my asset catalog, and easily load it and use it as my DataBus.
func loadTest() {
if let dataAsset = NSDataAsset(name: "6502-test") {
if dataAsset.data.count == 65536 {
bus = dataAsset.data
logger.log("Test successfully loaded")
} else {
logger.log("Data was wrong size")
}
} else {
logger.log("Couldn't load data")
}
reg.pc = 0x400
}
With this test suite, and several opcode references, I was ready to implement the functionality of the CPU.
-
A friend pointed out after I posted this that destructive load should also be marked mutating, since it may change state in the future.