Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- //
- // CPU.swift
- //
- import UIKit
- class CPU {
- // Calls specified method on every screen refresh (much more fine detail than a NSTimer)
- var displayTimer: CADisplayLink?
- lazy private var pongData = NSDataAsset(name: "Pong")
- lazy private var testData = NSDataAsset(name: "BC_test")
- var totalCPUCycles = 0
- // 60hz frame timer
- var frameTimer = Timer()
- // This is always initialized with this value in Chip8 since the actual program data begins here in memory
- var programCounter = 80
- var currentOpCode: UInt16 = 0x0
- var indexRegister: Int = 0
- var stack: [UInt16] = [0x0]
- var stackPointer = 0
- // Both of these timers, when above 0, should decrement by 1 per cycle at 60hz.
- // when sound timer reaches 0, buzzer should sound.
- var soundTimer = 0
- var delayTimer = 0
- var screenPixels: Screen.DataRepresentation = [[]]
- // 16 registers, 0->F
- var vRegisters: [UInt8] = [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
- 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]
- /* MEMORY MAP
- 0x000-0x1FF - Chip 8 interpreter (contains font set in emu)
- 0x050-0x0A0 - Used for the built in 4x5 pixel font set (0-F)
- 0x200-0xFFF - Program ROM and work RAM
- */
- var memory: [UInt16] = []
- // HEX -> BINARY
- // 0xF0 -> 11110000
- // 0x09 -> 00001001
- enum Constants {
- static let fontSet: [UInt16] = [0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
- 0x20, 0x60, 0x20, 0x20, 0x70, // 1
- 0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
- 0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3
- 0x90, 0x90, 0xF0, 0x10, 0x10, // 4
- 0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5
- 0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6
- 0xF0, 0x10, 0x20, 0x40, 0x40, // 7
- 0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8
- 0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9
- 0xF0, 0x90, 0xF0, 0x90, 0x90, // A
- 0xE0, 0x90, 0xE0, 0x90, 0xE0, // B
- 0xF0, 0x80, 0x80, 0x80, 0xF0, // C
- 0xE0, 0x90, 0x90, 0x90, 0xE0, // D
- 0xF0, 0x80, 0xF0, 0x80, 0xF0, // E
- 0xF0, 0x80, 0xF0, 0x80, 0x80] // F
- }
- init() {
- memory = Constants.fontSet
- // Update all 2048 screen pixels to off
- for yIndex in 0..<32 {
- self.screenPixels.insert([], at: yIndex)
- for xIndex in 0..<64 {
- let isACorner = (xIndex == 0 && yIndex == 0) ||
- (xIndex == 63 && yIndex == 31) ||
- (xIndex == 0 && yIndex == 31) ||
- (xIndex == 63 && yIndex == 0)
- self.screenPixels[yIndex].insert(isACorner, at: xIndex)
- }
- }
- }
- typealias CycleHandler = (Screen.DataRepresentation) -> Void
- var cycleEndedHandler: CycleHandler?
- func start(cycleEndedHandler: @escaping CycleHandler) {
- loadRomIntoMemory(completionHandler: { [weak self] in
- guard let self = self else { return }
- self.cycleEndedHandler = cycleEndedHandler
- let displayLink = CADisplayLink(target: self, selector: #selector(cpuCycleStart))
- displayLink.preferredFramesPerSecond = 60
- displayLink.add(to: .current, forMode: .common)
- self.displayTimer = displayLink
- })
- }
- /// This should be called approx 60 times a second (60hz)
- @objc private func cpuCycleStart() {
- emulateCycle()
- cycleEndedHandler?(self.screenPixels)
- }
- /// Gets the opcode at current index, and the opcode at index+1,
- /// combines them into memory and continues until reaching the end of the rom.
- private func loadRomIntoMemory(completionHandler: () -> Void) {
- var romOpcodeIndex = 0
- if let romData = self.testData?.data {
- var reachedEndOfRom = romOpcodeIndex >= romData.count && (romOpcodeIndex + 1) >= romData.count
- while !reachedEndOfRom {
- // combining the partial opcodes 8bit and 8bit = 16bit opcode
- let opcodeA = UInt16(romData[romOpcodeIndex])
- let opcodeB = UInt16(romData[romOpcodeIndex + 1])
- let mergedOpcode = (opcodeA << 8) | opcodeB
- self.memory.append(mergedOpcode)
- // Binary Digit Print: String(someNum, radix: 2)
- print("OpCode: \(String(format: "%X", mergedOpcode))")
- romOpcodeIndex += 2
- reachedEndOfRom = romOpcodeIndex >= romData.count && (romOpcodeIndex + 1) >= romData.count
- }
- completionHandler()
- }
- }
- private func emulateCycle() {
- // Decode & execute opcode
- if self.programCounter < self.memory.count {
- self.currentOpCode = self.memory[self.programCounter]
- self.updateStack(opCode: currentOpCode)
- self.process(opCode: currentOpCode)
- } else {
- print("🛑 ERROR: Reached end of rom")
- print("🔄 Total CPU Cycles: \(self.totalCPUCycles)")
- self.displayTimer?.invalidate()
- }
- // Update timers
- self.delayTimer = self.delayTimer > 0 ? (self.delayTimer - 1) : 0
- self.soundTimer = self.soundTimer > 0 ? (self.soundTimer - 1) : 0
- self.programCounter += 2
- self.totalCPUCycles += 1
- }
- private func updateStack(opCode: UInt16) {
- var updatableStack = stack
- self.stackPointer = self.stackPointer > 0 ? self.stackPointer + 1 : 0
- updatableStack[self.stackPointer] = opCode
- self.stack = updatableStack
- }
- private func process(opCode: UInt16) {
- print("Processing OpCode: \(String(opCode, radix: 16))")
- switch (opCode & 0xF000) {
- case 0x6000:
- load(value: UInt8(opCode >> 8), toRegister: UInt8(opCode & 0x00FF))
- case 0xA000:
- updateIndexRegister(value: Int(opCode & 0x0FFF))
- case 0xD000:
- draw(nBytes: (opCode & 0x000F), vX: (opCode & 0x0F00), vy: (opCode & 0x00F8))
- default:
- instructionIgnored()
- return
- }
- }
- }
- // MARK: CPU OpCode Handling
- extension CPU {
- private func instructionIgnored() {
- print("⚠ Got unhandled instruction: \(String(currentOpCode, radix: 16))")
- }
- private func instructionFailed() {
- print("⚠ Failed instruction: \(String(currentOpCode, radix: 16))")
- }
- /// # Opcode 6xkk
- /// * Set Vregister[x] = kk
- func load(value: UInt8, toRegister: UInt8) {
- if toRegister < self.vRegisters.count {
- self.vRegisters[Int(toRegister)] = value
- } else {
- // Attempt to insert into invalid register
- instructionFailed()
- }
- }
- /// # Opcode Annn
- /// * Set the indexRegister = nnn
- func updateIndexRegister(value: Int) {
- self.indexRegister = value
- }
- /// # Opcode Dxyn
- /// * Dxyn - DRW Vx, Vy, nibble
- /// * Display n-byte sprite starting at memory location I at (Vx, Vy), set VF = collision.
- /// * The interpreter reads n bytes from memory, starting at the address stored in I. These bytes are then displayed as sprites on screen at coordinates (Vx, Vy). Sprites are XORed onto the existing screen. If this causes any pixels to be erased, VF is set to 1, otherwise it is set to 0. If the sprite is positioned so part of it is outside the coordinates of the display, it wraps around to the opposite side of the screen. See instruction 8xy3 for more information on XOR, and section 2.4, Display, for more information on the Chip-8 screen and sprites.
- func draw(nBytes: UInt16, vX: UInt16, vy: UInt16) {
- var sprite: [UInt16] = []
- let endByte = self.indexRegister + Int(nBytes)
- for byteIndex in self.indexRegister..<endByte {
- sprite.append(self.memory[Int(byteIndex)])
- }
- print("")
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement