Search code examples
iosswiftcomputer-sciencecpu-architectureswift-playground

16 bit logic/computer simulation in Swift


I’m trying to make a basic simulation of a 16 bit computer with Swift. The computer will feature

  • An ALU
  • 2 registers

That’s all. I have enough knowledge to create these parts visually and understand how they work, but it has become increasingly difficult to make larger components with more inputs while using my current approach.

My current approach has been to wrap each component in a struct. This worked early on, but is becoming increasingly difficult to manage multiple inputs while staying true to the principles of computer science.

The primary issue is that the components aren’t updating with the clock signal. I have the output of the component updating when get is called on the output variable, c. This, however, neglects the idea of a clock signal and will likely cause further problems later on.

It’s also difficult to make getters and setters for each variable without getting errors about mutability. Although I have worked through these errors, they are annoying and slow down the development process.

The last big issue is updating the output. The output doesn’t update when the inputs change; it updates when told to do so. This isn’t accurate to the qualities of real computers and is a fundamental error.

This is an example. It is the ALU I mentioned earlier. It takes two 16 bit inputs and outputs 16 bits. It has two unary ALUs, which can make a 16 bit number zero, negate it, or both. Lastly, it either adds or does a bit wise and comparison based on the f flag and inverts the output if the no flag is selected.

struct ALU {
    //Operations are done in the order listed. For example, if zx and nx are 1, it first makes input 1 zero and then inverts it.
    var x : [Int] //Input 1
    var y : [Int] //Input 2
    var zx : Int //Make input 1 zero
    var zy : Int //Make input 2 zero
    var nx : Int //Invert input 1
    var ny : Int //Invert input 2
    var f : Int //If 0, do a bitwise AND operation. If 1, add the inputs
    var no : Int //Invert the output
    public var c : [Int] { //Output
        get {
            //Numbers first go through unary ALUs. These can negate the input (and output the value), return 0, or return the inverse of 0. They then undergo the operation specified by f, either addition or a bitwise and operation, and are negated if n is 1.
            var ux = UnaryALU(z: zx, n: nx, x: x).c //Unary ALU. See comments for more
            var uy = UnaryALU(z: zy, n: ny, x: y).c 
            var fd = select16(s: f, d1: Add16(a: ux, b: uy).c, d0: and16(a: ux, b: uy).c).c //Adds a 16 bit number or does a bitwise and operation. For more on select16, see the line below.
            var out = select16(s: no, d1: not16(a: fd).c, d0: fd).c //Selects a number. If s is 1, it returns d1. If s is 0, it returns d0. d0 is the value returned by fd, while d1 is the inverse.
            return out
        }
    }
    public init(x:[Int],y:[Int],zx:Int,zy:Int,nx:Int,ny:Int,f:Int,no:Int) {
        self.x = x
        self.y = y
        self.zx = zx
        self.zy = zy
        self.nx = nx
        self.ny = ny
        self.f = f
        self.no = no
    }
}

I use c for the output variable, store values with multiple bits in Int arrays, and store single bits in Int values.

I’m doing this on Swift Playgrounds 3.0 with Swift 5.0 on a 6th generation iPad. I’m storing each component or set of components in a separate file in a module, which is why some variables and all structs are marked public. I would greatly appreciate any help. Thanks in advance.


Solution

  • So, I’ve completely redone my approach and have found a way to bypass the issues I was facing. What I’ve done is make what I call “tracker variables” for each input. When get is called for each variable, it returns that value of the tracker assigned to it. When set is called it calls an update() function that updates the output of the circuit. It also updates the value of the tracker. This essentially creates a ‘copy’ of each variable. I did this to prevent any infinite loops.

    Trackers are unfortunately necessary here. I’ll demonstrate why

    var variable : Type {
        get {
            return variable //Calls the getter again, resulting in an infinite loop
        }
        set {
            //Do something
        }
    }
    

    In order to make a setter, Swift requires a getter to be made as well. In this example, calling variable simply calls get again, resulting in a never-ending cascade of calls to get. Tracker variables are a workaround that use minimal extra code.

    Using an update method makes sure the output responds to a change in any input. This also works with a clock signal, due to the architecture of the components themselves. Although it appears to act as the clock, it does not.

    For example, in data flip-flops, the clock signal is passed into gates. All a clock signal does is deactivate a component when the signal is off. So, I can implement that within update() while remaining faithful to reality.

    Here’s an example of a half adder. Note that the tracker variables I mentioned are marked by an underscore in front of their name. It has two inputs, x and y, which are 1 bit each. It also has two outputs, high and low, also known as carry and sum. The outputs are also one bit.

    struct halfAdder {
        private var _x : Bool //Tracker for x
        public var x: Bool { //Input 1
            get {
                return _x //Return the tracker’s value
            }
            set {
                _x = x //Set the tracker to x
                update() //Update the output
            }
        }
        private var _y : Bool //Tracker for y
        public var y: Bool { //Input 2
            get {
                return _y
            }
            set {
                _y = y
                update()
            }
        }
        public var high : Bool //High output, or ‘carry’
        public var low : Bool //Low output, or ‘sum’
        internal mutating func update(){ //Updates the output
            high = x && y //AND gate, sets the high output
            low = (x || y) && !(x && y) //XOR gate, sets the low output
        }
        public init(x:Bool, y:Bool){ //Initializer
            self.high = false //This will change when the variables are set, ensuring a correct output. 
            self.low = false //See above
            self._x = x //Setting trackers and variables
            self._y = y
            self.x = x
            self.y = y
        }
    }
    

    This is a very clean way, save for the trackers, do accomplish this task. It can trivially be expanded to fit any number of bits by using arrays of Bool instead of a single value. It respects the clock signal, updates the output when the inputs change, and is very similar to real computers.