Skip to content

Commit

Permalink
Merge pull request #5 from nrivard/InterruptsV2
Browse files Browse the repository at this point in the history
Interrupts v2
  • Loading branch information
nrivard authored Jul 23, 2023
2 parents 7f53437 + 1913fb4 commit cb8a917
Show file tree
Hide file tree
Showing 6 changed files with 344 additions and 36 deletions.
97 changes: 72 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dependencies: [

## Using Microprocessed

There are two major pieces to Microprocessed that you will have to understand to get use out of this package: memory and execution.
There are three major pieces to Microprocessed that you will have to understand to get use out of this package: memory, execution, and interrupts.

### Memory

Expand All @@ -27,13 +27,12 @@ This makes memory the most important thing you define when using Microprocessed.
Microprocessed's memory model is defined by `MemoryAddressable`

```swift
public protocol MemoryAddressable: AnyObject {

public protocol MemoryAddressable {
/// return an 8 bit value for the given 16 bit address
func read(from address: UInt16) throws -> UInt8

/// write an 8 bit value to the given 16 bit address
func write(to address: UInt16, data: UInt8) throws
mutating func write(to address: UInt16, data: UInt8) throws
}
```

Expand All @@ -46,7 +45,7 @@ final class FreeRunner: MemoryAddressable {
return 0xEA
}

func write(to address: UInt16, data: UInt8) throws {
mutating func write(to address: UInt16, data: UInt8) throws {
// ignore
}
}
Expand All @@ -57,7 +56,6 @@ This class now adds a proper reset vector and allows read and write access, sort

```swift
final class TestMemory: MemoryAddressable {

var memory: [UInt16: UInt8] = [
Microprocessor.resetVector: 0x00,
Microprocessor.resetVectorHigh: 0x80
Expand All @@ -67,7 +65,7 @@ final class TestMemory: MemoryAddressable {
return memory[address] ?? 0xEA
}

func write(to address: UInt16, data: UInt8) throws {
mutating func write(to address: UInt16, data: UInt8) throws {
memory[address] = data
}
}
Expand All @@ -94,7 +92,7 @@ final class DeviceRouter: MemoryAddressable {
}
}

func write(to address: UInt16, data: UInt8) throws {
mutating func write(to address: UInt16, data: UInt8) throws {
switch address {
case 0x0000...0x7FFF:
try ram.write(to: address, data: data)
Expand All @@ -115,17 +113,14 @@ So to use `Microprocessor`, you have to provide it with some memory.

```swift
final class System {
let mpu: Microprocessor!
let router: DeviceRouter
let mpu: Microprocessor

init() {
// memoryLayout is `unowned` so you can safely provide `self` here
self.mpu = .init(memoryLayout: self)
self.router = DeviceRouter()
self.mpu = .init(memoryLayout: router)
}
}

extension System: MemoryAddressable {
// you can make this your device router if you want, or just pass it to another type to do that work for you
}
```

Now your `Microprocessor` is all set up to execute instructions.
Expand All @@ -139,20 +134,73 @@ try mpu.tick()
After executing an instruction, you can query the `Microprocessor` about some of its internal state, including:
* register values via the `registers` property
* the next instruction via `peek()`
* the current run mode (in case a `WAI` or `STP` instruction was executed)
* the current `runMode` (in case a `WAI` or `STP` instruction was executed)
* the current `interruptMask` to see if any interrupts are being serviced and what class they are

`Microprocessor` also provides some pin-level hardware simulation including:
* `reset()` which will run through the reset process
* reset the program counter to the value at the reset vector
* clear register values
* return run mode to `normal`
* `interrupt()` which will simulate a normal interrupt
* `nonMaskableInterrupt()` which will simulate non-maskable interrupt
`Microprocessor` also provides some pin-level hardware simulation like `reset()` which will:
* reset the program counter to the value at the reset vector
* clear register values
* return run mode to `normal`
* clear the `interruptMask`

Lastly, you can control some aspects of execution via `Microprocessor.Configuration`.
At present, this can control whether unused opcodes throw an error or not.
For education purposes, you likely want to throw an error, but for pure simulation, you may not want to.

### Interrupts

The 65C02 processor has 2 dedicated lines for interrupts:
* `IRQ` for maskable interrupts
* `NMI` for non-maskable interrupts

Devices can pull these lines low (therefore asserting) to interrupt the processor and execute high priority tasks after the currently executing instruction is complete.
`Microprocessor` simulates this by polling all devices that can cause interrupts.
This is defined by the `Interrupting` protocol:

```swift
public protocol Interrupting {
var interruptStatus: InterruptStatus { get }
}
```

This is a simple protocol where when queried a device returns it's current `InterruptStatus`:

```swift
public enum InterruptStatus {
/// Device is not interrupting
case none

/// Device is interrupting and is non-maskable
case nonMaskable

/// Device is interrupting but is maskable
case maskable
}
```

To opt-in to interrupt polling, a device conforms to this protocol:

```swift
struct VideoDisplayProcessor: Interrupting {
var scanline: Int

var interruptStatus: InterruptStatus {
// if we have just drawn scanline 192, then raise an interrupt
return scanline == 192 ? .maskable : .none
}
}
```

You then pass your list of `Interrupting` devices to `Microprocessor.init`:

```swift
let mpu = Microprocessor(memoryLayout: router, interruptors: [router.vdp])
```

Interrupt polling is done at the beginning of `tick()` so you will have to call this function after setting `interruptStatus` before an interrupt is actually raised.
Note that the `Microprocessor` will re-enter an interrupt handler if the `interruptStatus` is not de-asserted!
This mirrors real-world behavior where many devices must have a status register read before the interrupt line is cleared.

## Next Steps

Microprocessed has everything you need to create compelling 65C02 simulation experiences, but it's not perfect.
Expand All @@ -161,8 +209,7 @@ In the future, it would be great to make some changes to `Microprocessor` includ
This will allow calling it from any thread while protecting its internal state
* abstract `Microprocessor` to a protocol so other 8-bit MPUs can be swapped in, including an old NMOS 6502 or a Z80, all with the same calling conventions
* more and better hardware simulation.
Many of the 65C02 pins aren't really abstracted here so you can't simulate wait states and the like.
In addition, interrupts should technically be cleared by the devices themselves so there are some subtle bugs here.
Many of the 65C02 pins aren't really abstracted here so you can't simulate wait states and the like.
* cycle accurate execution.
The current core does not keep a count of cycles executed so cycle-accurate timing isn't currently possible

Expand Down
26 changes: 26 additions & 0 deletions Sources/Microprocessed/Interrupting.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// Interrupting.swift
//
//
// Created by Nate Rivard on 10/07/2023.
//

import Foundation

public enum InterruptStatus {
/// Device is not interrupting
case none

/// Device is interrupting and is non-maskable
case nonMaskable

/// Device is interrupting but is maskable
case maskable
}

/// Simple protocol that seeks to answer the question: are you holding either interrupt line low?
/// ISR will be called repeatedly while _any_ device is holding a line low and interrupt conditions are met
public protocol Interrupting {
/// Device returns an interrupt status
var interruptStatus: InterruptStatus { get }
}
80 changes: 70 additions & 10 deletions Sources/Microprocessed/Microprocessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,28 @@ public class Microprocessor {
case stopped
}

/// Defines which class of interrupts should be ignored
public struct InterruptStatusMask: OptionSet, Comparable {
public let rawValue: Int

public init(rawValue: Int) {
self.rawValue = rawValue
}

public static let irq = InterruptStatusMask(rawValue: 1 << 1)
public static let nmi = InterruptStatusMask(rawValue: 1 << 2)

public static func < (lhs: Microprocessor.InterruptStatusMask, rhs: Microprocessor.InterruptStatusMask) -> Bool {
return lhs.rawValue < rhs.rawValue
}
}

/// Memory layout that the MPU uses to fetch opcodes and data alike
public internal(set) var memory: MemoryAddressable

/// Devices that can possibly raise interrupts
public internal(set) var interruptors: [Interrupting]

/// Allows customization of the MPU, especially for learning purposes. By default, unused opcodes throw errors
public let configuration: Configuration

Expand All @@ -40,9 +59,13 @@ public class Microprocessor {
/// CPU run mode state
public internal(set) var runMode: RunMode = .normal

/// currently blocked interrupt types (which means interrupts are being serviced)
public internal(set) var interruptMask: InterruptStatusMask = []

/// create a `Microprocessor` with a given memory layout and configuration.
public required init(memoryLayout memory: MemoryAddressable, configuration: Configuration = .init()) {
public required init(memoryLayout memory: MemoryAddressable, interruptors: [Interrupting] = [], configuration: Configuration = .init()) {
self.memory = memory
self.interruptors = interruptors
self.configuration = configuration
}

Expand All @@ -55,13 +78,37 @@ public class Microprocessor {
registers.SP = 0xFF
registers.SR = 0 // this will actually properly set `Always` and `Break`
runMode = .normal
interruptMask = []
}

/// send a single clock rising edge pulse to the `Microprocessor`
public func tick() throws {
guard runMode == .normal else { return }
/// if we're halted, we're done
guard runMode != .stopped else { return }

let interruptStatuses = interruptors.map(\.interruptStatus)

/// if there are any non-maskable interrupts, deal with them
if interruptMask < .nmi, interruptStatuses.contains(.nonMaskable) {
runMode = .normal
try nonMaskableInterrupt()
return
}

/// if there are any normal interrupts, deal with them
if interruptMask < .irq, interruptStatuses.contains(.maskable) {
runMode = .normal

try execute(try fetch())
if !registers.$SR.contains(.interruptsDisabled) {
try interrupt()
return
}
}

/// no interrupts
if case .normal = runMode {
try execute(try fetch())
}
}

/// peek at what the next instruction is
Expand All @@ -73,19 +120,23 @@ public class Microprocessor {
extension Microprocessor {

/// send an interrupt signal. This may be ignored if `interruptsDisabled` is enabled
public func interrupt() throws {
guard !registers.$SR.contains(.interruptsDisabled) else {
return
}

func interrupt() throws {
interruptMask.insert(.irq)
try interrupt(toVector: Microprocessor.irqVector, isHardware: true)
}

/// send a non-maskable interrupt signal. This will always execute, even when `interruptsDisabled` is enabled
public func nonMaskableInterrupt() throws {
func nonMaskableInterrupt() throws {
interruptMask.insert(.nmi)
try interrupt(toVector: Microprocessor.nmiVector, isHardware: true)
}

/// send an interrupt from software (`BRK`)
func softwareInterrupt() throws {
interruptMask.insert(.irq)
try interrupt(toVector: Microprocessor.irqVector, isHardware: false)
}

private func interrupt(toVector vector: UInt16, isHardware: Bool) throws {
guard runMode != .stopped else { return }

Expand All @@ -105,6 +156,14 @@ extension Microprocessor {

registers.PC = try memory.readWord(fromAddressStartingAt: vector)
}

func downgradeInterruptMask() {
if let _ = interruptMask.remove(.nmi) {
return
}

interruptMask.remove(.irq)
}
}

extension Microprocessor {
Expand Down Expand Up @@ -364,6 +423,7 @@ extension Microprocessor {
// restore status register first. make sure to set `isSoftwareInterrupt` as this is always a `1` in the actual register
registers.SR = try pop()
registers.PC = try popWord()
downgradeInterruptMask()

case .bra:
try branch(on: true, addressingMode: instruction.addressingMode)
Expand Down Expand Up @@ -428,7 +488,7 @@ extension Microprocessor {
break

case .brk:
try interrupt(toVector: Microprocessor.irqVector, isHardware: false)
try softwareInterrupt()

case .wai:
runMode = .waitingForInterrupt
Expand Down
14 changes: 14 additions & 0 deletions Tests/MicroprocessedTests/Instructions/BRK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,18 @@ final class BRKTests: SystemTests {
XCTAssert(try mpu.pop() == status.rawValue)
XCTAssert(try mpu.popWord() == returnAddress)
}

func testBRKWithInterruptsDisabled() throws {
let returnAddress = mpu.registers.PC + 2
let irqAddress: UInt16 = 0xA5DF
let status: StatusFlags = [.isNegative, .didCarry, .didOverflow, .alwaysSet, .isSoftwareInterrupt, .interruptsDisabled]
try ram.write(toAddressStartingAt: Microprocessor.irqVector, word: irqAddress)
mpu.registers.SR = status.rawValue

try mpu.execute(0x00)
XCTAssert(mpu.registers.PC == irqAddress)
XCTAssert(mpu.registers.$SR.contains(.interruptsDisabled))
XCTAssert(try mpu.pop() == status.rawValue)
XCTAssert(try mpu.popWord() == returnAddress)
}
}
Loading

0 comments on commit cb8a917

Please sign in to comment.