Created
December 10, 2022 15:10
-
-
Save JetForMe/0d2fa9d6f464bdce6291a7e0b226b94e to your computer and use it in GitHub Desktop.
Ben Eater CPU Microcode ROM Generator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env swift sh | |
/** | |
This Swift script requires that you have [swift-sh](https://github.com/mxcl/swift-sh) installed: | |
``` | |
$ brew install swift-sh | |
``` | |
My CPU differs from Bean Eater’s, in that I program two different ROMs, | |
and both are addressed identically. I think in the future I will change | |
that, but instead of tying one A7 line high and the other low, I’ll | |
tie A0 low on the A ROM and high on the B rom, and shift all the other | |
wires up one bit. This *should* let me write a single ROM file with | |
the two bytes of the microcode word adjacent to each other. | |
*/ | |
import Foundation | |
import ArgumentParser // apple/swift-argument-parser ~> 1.2 | |
import Path // mxcl/Path.swift ~> 1.4.0 | |
struct | |
ControlLines : OptionSet | |
{ | |
static let hlt = ControlLines(rawValue: 0b1000_0000_0000_0000) | |
static let mi = ControlLines(rawValue: 0b0100_0000_0000_0000) | |
static let ri = ControlLines(rawValue: 0b0010_0000_0000_0000) | |
static let ro = ControlLines(rawValue: 0b0001_0000_0000_0000) | |
static let io = ControlLines(rawValue: 0b0000_1000_0000_0000) | |
static let ii = ControlLines(rawValue: 0b0000_0100_0000_0000) | |
static let ai = ControlLines(rawValue: 0b0000_0010_0000_0000) | |
static let ao = ControlLines(rawValue: 0b0000_0001_0000_0000) | |
static let eo = ControlLines(rawValue: 0b0000_0000_1000_0000) | |
static let su = ControlLines(rawValue: 0b0000_0000_0100_0000) | |
static let bi = ControlLines(rawValue: 0b0000_0000_0010_0000) | |
static let oi = ControlLines(rawValue: 0b0000_0000_0001_0000) | |
static let ce = ControlLines(rawValue: 0b0000_0000_0000_1000) | |
static let co = ControlLines(rawValue: 0b0000_0000_0000_0100) | |
static let j = ControlLines(rawValue: 0b0000_0000_0000_0010) | |
let rawValue : UInt16 | |
} | |
/** | |
Note that op codes are only four bits (a maximum of 16 can be defined, | |
and their values must be <= 0b1111). | |
*/ | |
enum | |
OpCode : UInt8 | |
{ | |
case nop = 0b0000 | |
case lda = 0b0010 | |
case ldb = 0b0011 | |
case add = 0b0100 | |
case sub = 0b0101 | |
case jmp = 0b1000 | |
case out = 0b1110 | |
case halt = 0b1111 | |
} | |
/** | |
We define instructions as a pairing of opcode and asserted control lines. The | |
initial fetch microcode is automatically inserted. So | |
*/ | |
let | |
instructions: [OpCode : [ControlLines]] = | |
[ | |
.nop : [], | |
.lda : [[.io, .mi], [.ro, .ai]], | |
.ldb : [[.io, .mi], [.ro, .bi]], | |
.add : [[.io, .mi], [.ro, .bi], [.eo, .ai]], | |
.sub : [[.io, .mi], [.ro, .bi, .su], [.eo, .ai, .su]], // TODO: Do we need to subtract on both steps? | |
.jmp : [[.io, .j]], | |
.out : [[.ao, .oi]], | |
.halt : [[.hlt]], | |
] | |
for (key, steps) in instructions | |
{ | |
var allSteps: [ControlLines] = [[.mi, .co], [.ro, .ii, .ce]] | |
allSteps += steps | |
var s = String() | |
for step in allSteps | |
{ | |
s += "\(String(format: "0x%04x", step.rawValue)), " | |
} | |
print("Inst \(key):\t\(s)") | |
} | |
// Build the full 128 words of microcode. For each possible | |
// opcode, append 8 words, but split the high and low bytes | |
// into separate buffers… | |
var romA = Data() | |
var romB = Data() | |
for opcodeValue: UInt8 in 0 ..< 16 | |
{ | |
// Always append the two fetch steps… | |
let step1: ControlLines = [.mi, .co] | |
let step2: ControlLines = [.ro, .ii, .ce] | |
romA.append(UInt8(step1.rawValue >> 8)) | |
romB.append(UInt8(step1.rawValue & 0x0F)) | |
romA.append(UInt8(step2.rawValue >> 8)) | |
romB.append(UInt8(step2.rawValue & 0xFF)) | |
// If this value has an opcode defined, append the instruction’s steps… | |
if let opcode = OpCode(rawValue: opcodeValue), | |
let steps = instructions[opcode] | |
{ | |
print("Found opcode for \(opcodeValue)") | |
for step in steps | |
{ | |
let highByte: UInt8 = UInt8(step.rawValue >> 8) | |
let lowByte: UInt8 = UInt8(step.rawValue & 0x00FF) | |
romA.append(highByte) | |
romB.append(lowByte) | |
} | |
// If necessary, fill out the 8 steps with zeros… | |
if steps.count < 6 // 6 because we generate the first two, so the steps array is only the last six. | |
{ | |
romA.append(contentsOf: [UInt8](repeating: 0, count: 6 - steps.count)) | |
romB.append(contentsOf: [UInt8](repeating: 0, count: 6 - steps.count)) | |
} | |
} | |
else | |
{ | |
// This opcode value doesn’t correspond to | |
// a defined instruction, so write all zeros… | |
romA.append(contentsOf: [UInt8](repeating: 0, count: 6)) | |
romB.append(contentsOf: [UInt8](repeating: 0, count: 6)) | |
} | |
} | |
print("ROM A size: \(romA.count)") | |
print("ROM B size: \(romB.count)") | |
// Append zeros to fill out the rom… | |
romA.append(contentsOf: [UInt8](repeating: 0, count: 2048 - romA.count)) | |
romB.append(contentsOf: [UInt8](repeating: 0, count: 2048 - romB.count)) | |
print("ROM A size: \(romA.count)") | |
print("ROM B size: \(romB.count)") | |
do | |
{ | |
try romA.write(to: Path.cwd / "romA.bin", atomically: true) | |
try romB.write(to: Path.cwd / "romB.bin", atomically: true) | |
} | |
catch let e | |
{ | |
print("Error writing ROM files: \(e)") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment