Skip to content

Commit

Permalink
Adds ControllerConnector and ControlPad (#20)
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
jerrodputman committed Jul 9, 2023
1 parent 3940493 commit 3444a9f
Show file tree
Hide file tree
Showing 8 changed files with 437 additions and 10 deletions.
9 changes: 1 addition & 8 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: []),
Expand Down
68 changes: 68 additions & 0 deletions Sources/SwiftNES/ControllerConnector.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
80 changes: 80 additions & 0 deletions Sources/SwiftNES/Controllers/ControlPad.swift
Original file line number Diff line number Diff line change
@@ -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<UInt8>
}
24 changes: 22 additions & 2 deletions Sources/SwiftNES/NES.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}

Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down
53 changes: 53 additions & 0 deletions Sources/SwiftNES/ShiftRegisterPISO.swift
Original file line number Diff line number Diff line change
@@ -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<Size: FixedWidthInteger & UnsignedInteger> {
/// 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
}
52 changes: 52 additions & 0 deletions Tests/SwiftNESTests/ControlPadTests.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
48 changes: 48 additions & 0 deletions Tests/SwiftNESTests/ControllerConnectionTests.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit 3444a9f

Please sign in to comment.