From 3444a9f84fec16d5ef82a99118ebe7bf37e7bbfb Mon Sep 17 00:00:00 2001 From: Jerrod Putman Date: Sun, 9 Jul 2023 16:20:16 -0700 Subject: [PATCH] Adds `ControllerConnector` and `ControlPad` (#20) - Adds `ControllerConnector` for the controller port attached to the CPU bus. - Adds `Controller` protocol to allow for different controller implementations. - Adds `ControlPad` as implementation of NES-004 hardware. - Adds `ShiftRegisterPISO` to handle PISO shift register of `ControlPad`. - Adds tests for all new types. --- Package.swift | 9 +- Sources/SwiftNES/ControllerConnector.swift | 68 +++++++++++ Sources/SwiftNES/Controllers/ControlPad.swift | 80 +++++++++++++ Sources/SwiftNES/NES.swift | 24 +++- Sources/SwiftNES/ShiftRegisterPISO.swift | 53 ++++++++ Tests/SwiftNESTests/ControlPadTests.swift | 52 ++++++++ .../ControllerConnectionTests.swift | 48 ++++++++ .../ShiftRegisterPISOTests.swift | 113 ++++++++++++++++++ 8 files changed, 437 insertions(+), 10 deletions(-) create mode 100644 Sources/SwiftNES/ControllerConnector.swift create mode 100644 Sources/SwiftNES/Controllers/ControlPad.swift create mode 100644 Sources/SwiftNES/ShiftRegisterPISO.swift create mode 100644 Tests/SwiftNESTests/ControlPadTests.swift create mode 100644 Tests/SwiftNESTests/ControllerConnectionTests.swift create mode 100644 Tests/SwiftNESTests/ShiftRegisterPISOTests.swift diff --git a/Package.swift b/Package.swift index 74a24e8..cdd1318 100644 --- a/Package.swift +++ b/Package.swift @@ -4,20 +4,13 @@ import PackageDescription let package = Package( name: "SwiftNES", - platforms: [.macOS(.v13)], + platforms: [.macOS(.v13), .iOS(.v17), .tvOS(.v17)], products: [ - // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( name: "SwiftNES", targets: ["SwiftNES"]), ], - dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), - ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "SwiftNES", dependencies: []), diff --git a/Sources/SwiftNES/ControllerConnector.swift b/Sources/SwiftNES/ControllerConnector.swift new file mode 100644 index 0000000..6d33265 --- /dev/null +++ b/Sources/SwiftNES/ControllerConnector.swift @@ -0,0 +1,68 @@ +// MIT License +// +// Copyright (c) 2023 Jerrod Putman +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +/// A controller that can be connected to a ``ControllerConnector``. +public protocol Controller: AnyObject { + /// Reads a single bit from the controller. + /// + /// - Returns: Whether or not the most significant bit of the controller is set. + func read() -> Bool + + /// Writes the value to the controller. + /// + /// - Parameter data: The data to write to the controller. + func write(_ data: Value) +} + +/// A device that allows a controller to be connected to the NES. +final class ControllerConnector: AddressableReadWriteDevice { + + // MARK: - Initializers + + /// Initializes the controller connector with the specified port. + init(address: Address) { + self.addressRange = address...address + } + + + // MARK: - Attaching a controller to the connector + + /// The controller that is attached to this connector. + var controller: (any Controller)? = nil + + + // MARK: - AddressableReadWriteDevice + + let addressRange: AddressRange + + func read(from address: Address) -> Value { + guard let controller else { return 0 } + + return controller.read() ? 1 : 0 + } + + func write(_ data: Value, to address: Address) { + controller?.write(data) + } +} diff --git a/Sources/SwiftNES/Controllers/ControlPad.swift b/Sources/SwiftNES/Controllers/ControlPad.swift new file mode 100644 index 0000000..95a5af2 --- /dev/null +++ b/Sources/SwiftNES/Controllers/ControlPad.swift @@ -0,0 +1,80 @@ +// MIT License +// +// Copyright (c) 2023 Jerrod Putman +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +/// A class that represents an NES Control Pad (NES-004). +public final class ControlPad: Controller { + + // MARK: - Initializers + + /// Initializes the control pad. + public init() { + self.pressedButtons = .none + self.shiftRegister = .init() + } + + + // MARK: - Buttons + + /// An option set that represents all of the possible buttons that can be presed on the control pad. + public struct Buttons: OptionSet { + public var rawValue: UInt8 + + public static let right = Buttons(rawValue: (1 << 0)) + public static let left = Buttons(rawValue: (1 << 1)) + public static let down = Buttons(rawValue: (1 << 2)) + public static let up = Buttons(rawValue: (1 << 3)) + public static let start = Buttons(rawValue: (1 << 4)) + public static let select = Buttons(rawValue: (1 << 5)) + public static let b = Buttons(rawValue: (1 << 6)) + public static let a = Buttons(rawValue: (1 << 7)) + + public static let none = Buttons([]) + + + public init(rawValue: UInt8) { + self.rawValue = rawValue + } + } + + /// The buttons that are currently pressed. + public var pressedButtons: Buttons + + + // MARK: - Controller + + public func read() -> Bool { + shiftRegister.output() + } + + public func write(_ data: Value) { + // Move the current button state into the shift register. + shiftRegister.input(pressedButtons.rawValue) + } + + + // MARK: - Private + + /// A shift register where the button state can be read bit-by-bit. + private var shiftRegister: ShiftRegisterPISO +} diff --git a/Sources/SwiftNES/NES.swift b/Sources/SwiftNES/NES.swift index 3c0a455..4a477f9 100644 --- a/Sources/SwiftNES/NES.swift +++ b/Sources/SwiftNES/NES.swift @@ -22,7 +22,7 @@ import Foundation -/// A class that represents the complete NES hardware. +/// A class that represents the complete NES (NES-001) hardware. public final class NES { // MARK: - Initializers @@ -36,8 +36,10 @@ public final class NES { ppu = PixelProcessingUnit(bus: ppuBus) ram = try! RandomAccessMemoryDevice(memorySize: 0x0800, addressRange: 0x0000...0x1fff) + controllerConnector1 = ControllerConnector(address: 0x4016) + controllerConnector2 = ControllerConnector(address: 0x4017) cpuCartridgeConnector = CartridgeConnector(addressRange: 0x8000...0xffff) - let cpuBus = try Bus(addressableDevices: [ram, ppu, cpuCartridgeConnector]) + let cpuBus = try Bus(addressableDevices: [ram, ppu, controllerConnector1, controllerConnector2, cpuCartridgeConnector]) cpu = RP2A03G(bus: cpuBus) } @@ -65,6 +67,12 @@ public final class NES { /// The palette memory. let palette: RandomAccessMemoryDevice + /// The controller connector for controller port 1. + let controllerConnector1: ControllerConnector + + /// The controller connector for controller port 2. + let controllerConnector2: ControllerConnector + // MARK: - Connecting to the inputs of the hardware @@ -77,6 +85,18 @@ public final class NES { } } + public var controller1: (any Controller)? = nil { + didSet { + controllerConnector1.controller = controller1 + } + } + + public var controller2: (any Controller)? = nil { + didSet { + controllerConnector2.controller = controller2 + } + } + // MARK: - Connecting to the outputs of the hardware diff --git a/Sources/SwiftNES/ShiftRegisterPISO.swift b/Sources/SwiftNES/ShiftRegisterPISO.swift new file mode 100644 index 0000000..d4bf7ef --- /dev/null +++ b/Sources/SwiftNES/ShiftRegisterPISO.swift @@ -0,0 +1,53 @@ +// MIT License +// +// Copyright (c) 2023 Jerrod Putman +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +/// Represents a PISO (parallel in, serial out) shift register. +struct ShiftRegisterPISO { + /// Initializes the shift register. + /// + /// - Parameter value: The initial value of the register. + init(value: Size = 0) { + self.value = value + } + + /// Loads the register with a value. + /// + /// - Parameter value: The value. + mutating func input(_ value: Size) { + self.value = value + } + + /// Outputs the value a bit at a time. + mutating func output() -> Bool { + // Grab the most significant bit from the shift register. + // If the bit is set, then the button is being pressed. + let isMSBSet = value.leadingZeroBitCount == 0 + + // Shift the register one bit to the left. + value = value << 1 + + // Return the previously captured bit value. + return isMSBSet + } + + private var value: Size +} diff --git a/Tests/SwiftNESTests/ControlPadTests.swift b/Tests/SwiftNESTests/ControlPadTests.swift new file mode 100644 index 0000000..9fb3674 --- /dev/null +++ b/Tests/SwiftNESTests/ControlPadTests.swift @@ -0,0 +1,52 @@ +import XCTest +@testable import SwiftNES + +final class ControlPadTests: XCTestCase { + + func testNoInput() { + let controlPad = ControlPad() + + XCTAssertFalse(controlPad.read()) + XCTAssertFalse(controlPad.read()) + XCTAssertFalse(controlPad.read()) + XCTAssertFalse(controlPad.read()) + XCTAssertFalse(controlPad.read()) + XCTAssertFalse(controlPad.read()) + XCTAssertFalse(controlPad.read()) + XCTAssertFalse(controlPad.read()) + } + + func testInput() { + let controlPad = ControlPad() + + controlPad.pressedButtons = [.a, .up] + // This is the value that will be read. + controlPad.pressedButtons = [.b, .down] + + // Write to the control pad to poll the current state. + controlPad.write(0) + + // This value will be ignored. + controlPad.pressedButtons = [.start] + + XCTAssertFalse(controlPad.read()) // A + XCTAssertTrue(controlPad.read()) // B + XCTAssertFalse(controlPad.read()) // Select + XCTAssertFalse(controlPad.read()) // Start + XCTAssertFalse(controlPad.read()) // Up + XCTAssertTrue(controlPad.read()) // Down + XCTAssertFalse(controlPad.read()) // Left + XCTAssertFalse(controlPad.read()) // Right + + controlPad.pressedButtons = .none + + XCTAssertFalse(controlPad.read()) // A + XCTAssertFalse(controlPad.read()) // B + XCTAssertFalse(controlPad.read()) // Select + XCTAssertFalse(controlPad.read()) // Start + XCTAssertFalse(controlPad.read()) // Up + XCTAssertFalse(controlPad.read()) // Down + XCTAssertFalse(controlPad.read()) // Left + XCTAssertFalse(controlPad.read()) // Right + } +} diff --git a/Tests/SwiftNESTests/ControllerConnectionTests.swift b/Tests/SwiftNESTests/ControllerConnectionTests.swift new file mode 100644 index 0000000..c0879b3 --- /dev/null +++ b/Tests/SwiftNESTests/ControllerConnectionTests.swift @@ -0,0 +1,48 @@ +import XCTest +@testable import SwiftNES + +final class ControllerConnectionTests: XCTestCase { + + func testNoController() { + let connector = ControllerConnector(address: 0x1000) + + connector.write(1, to: 0x1000) + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // A + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // B + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // Select + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // Start + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // Up + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // Down + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // Left + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // Right + } + + func testController() { + let connector = ControllerConnector(address: 0x1000) + + let controlPad = ControlPad() + connector.controller = controlPad + + controlPad.pressedButtons = [.a, .up] + connector.write(1, to: 0x1000) + XCTAssertEqual(connector.read(from: 0x1000), 1) // A + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // B + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // Select + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // Start + XCTAssertEqual(connector.read(from: 0x1000), 1) // Up + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // Down + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // Left + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // Right + + controlPad.pressedButtons = [.b] + connector.write(1, to: 0x1000) + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // A + XCTAssertEqual(connector.read(from: 0x1000), 1) // B + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // Select + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // Start + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // Up + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // Down + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // Left + XCTAssertNotEqual(connector.read(from: 0x1000), 1) // Right + } +} diff --git a/Tests/SwiftNESTests/ShiftRegisterPISOTests.swift b/Tests/SwiftNESTests/ShiftRegisterPISOTests.swift new file mode 100644 index 0000000..8604fb8 --- /dev/null +++ b/Tests/SwiftNESTests/ShiftRegisterPISOTests.swift @@ -0,0 +1,113 @@ +import XCTest +@testable import SwiftNES + +final class ShiftRegisterPISOTests: XCTestCase { + + func testSingleValue() { + var register = ShiftRegisterPISO() + + register.input(0b10101010) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + } + + func testNoValue() { + var register = ShiftRegisterPISO() + + XCTAssertFalse(register.output()) + XCTAssertFalse(register.output()) + XCTAssertFalse(register.output()) + XCTAssertFalse(register.output()) + XCTAssertFalse(register.output()) + XCTAssertFalse(register.output()) + XCTAssertFalse(register.output()) + XCTAssertFalse(register.output()) + } + + func testOverOutput() { + var register = ShiftRegisterPISO() + + register.input(0b10101010) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertFalse(register.output()) + XCTAssertFalse(register.output()) + XCTAssertFalse(register.output()) + XCTAssertFalse(register.output()) + } + + func testChangeValueDuringOutput() { + var register = ShiftRegisterPISO() + + register.input(0b10101010) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + + register.input(0b11110001) + XCTAssertTrue(register.output()) + XCTAssertTrue(register.output()) + XCTAssertTrue(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertFalse(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + } + + func testLargeRegister() { + var register = ShiftRegisterPISO() + + register.input(0xAAAAAAAA) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + XCTAssertTrue(register.output()) + XCTAssertFalse(register.output()) + + XCTAssertFalse(register.output()) + XCTAssertFalse(register.output()) + XCTAssertFalse(register.output()) + XCTAssertFalse(register.output()) + } +}