In computer hardware engineering, how does the electricity and the little bits of silicon work, what’s the “lowest level” of computer engineering and how does that work, and how does that scale and translate up into writing software? i.e. hardware -> machine code -> assembly -> higher level programming languages?
I think mostly I’m just curious about the bridge between software and hardware, and how to get there, but I have no idea where to start.
In: 1
The state of a processor at any given moment contains a couple of small pieces of data stored in *registers* (a tiny bit of memory hard-wired into the processor). A couple of those pieces are actual data loaded by previous instructions, along with its current *opcode* (the next operation the processor will perform) and a pointer that tells the processor where to look for the next opcode.
The processor has a bunch of circuits. Those circuits contain logic gates, and perform operations. Which circuit is active is controlled by the current opcode, so you can effectively think of a processor as a bunch of one-task circuits switched on and off by the opcode.
For example, the processor might contain [opcode 001] [register 1 010101010111] [register 2 1111111111] [next instruction at 110101000110101]. If opcode 001 corresponds to a circuit that says “add registers 1 and 2, then store the result in register 2 [and then load the next step of computation, as in any opcode]”, then the state after this instruction would contain whatever the opcode at the next-instruction location is, an identical register 1, the sum in register 2 (with some overflow, in this case), and the next-next instruction at the next-instruction step. The processor has other circuits, but the opcode stops them from being active.
[I am leaving out a few things here. In particular, most processors are a bit “smarter” than this about how they execute instructions, and a processor in most practical settings is also wired to other things that add extra registers with information coming in from outside.]
A sequence of opcodes is called *machine code*.
Assembly language is basically just a human readable version of machine code, with a little extra stuff. A small program, called an *assembler*, written directly in machine code, translates assembly language into machine code.
Then, yes, higher languages are built on top of assembly, and then other languages can be built on top of those, and so on. The key idea is that each higher language basically feeds some string (possibly human-readable text, possibly some other numerical codes) into a program written on a lower-level step, and that program has instructions that say “if fed in X, output Y lower-level code”.
The smallest physical component of a computer is the transistor. This is a bit of wire with three connections. An input (source), an output (drain) and a controlling connection (gate). If electricity is flowing to the gate, then current can flow from the source to the drain. If there is no electricity to the gate then no current can flow. It acts as a very simple switch. Gate on then the switch is closed. Gate off then the switch is open.
The real power of a computer comes from when you hook the output from one transistor to the gate of a second. So, now if the first transistor is switch closed, it will also switch the second transistor to closed.
Using chains of transistors you can make simple logic gates. These are things like AND circuits (only turns on when all inputs are on), OR circuits (turns on when any of the inputs are on) or NOT circuits (switches on to off and vice versa).
Connect some logic gates in the right order and you can do simple arithmetic like adding and multiplying. From there, you can do any mathematical operation we know about.
The software just sets the initial inputs to the circuits. Then the circuits calculate whatever they’re configured to calculate.
The lowest form of computer is a light switch. It has a single bit of input–the switch itself. It computes the identity function (i.e. the output is the same as the input). This output is sent to the one bit of display–the lights.
What you see in this “computer” is that everything is governed by basic circuitry. When electricity can flow it flows. Where it is blocked it stops. Input devices take some other phenomenon (e.g. the closing of a switch in this case) and turn it into an opportunity for electricity to flow or not. Output devices work the opposite, turning electricity into some physical phenomenon (light, in this case).
The next computer to look at is hallway/stairway light switches. These are wired up such that you can flip either of two switches and it’ll toggle light. This improves on the previous computer, now taking in *two* bits of input. The function being computed is now XOR (exclusive or, where the output is 1 if *exactly* one of the inputs is 1). The output is still a single bit–the lights.
At this level of computer the thing to recognize is that there are ways to arrange wires such that just by closing switches you can evaluate a simple function.
The next question to ask is “what if the output of this circuit wasn’t a light, but rather another switch?” All you need here is a switch that can be controlled electronically. The first choice here was relays, but those were superseded by vacuum tubes and later transistors. The physics varies between these, but the underlying idea of making an electronically controlled switch stays the same.
Once you start designing your computer where each unit can be used to drive later units you can start designing incredibly complex circuits through the magic of copy/paste (or abstraction, if you prefer the term). Instead of having to design everything in terms of wires you can design a few basic gates in terms of wires, then design more complicated gates in terms of those basic ones, and so on. For example, you might design a circuit that takes 16 inputs–two sets of 8 for two 8-bit numbers–and outputs their product. In designing this circuit perhaps you need a circuit to compute addition and another to execute a bit shift. You design those in terms of logic gates, and you design *those* in terms of wires. Thanks to abstraction you don’t have to think about wires when you’re designing something to multiply.
At that level you have the capability to design circuits that can compute extremely complicated things, but you’re still stuck with a very rigid computer: it just does one thing. To get around that you design several circuits that do commonly-needed things, then you design one last circuit that takes an instruction (perhaps as an 8-bit number) and, based on that instruction, it connects memory cells (circuits you’ve designed in terms of logic gates) to specific pieces of your computer. This circuit acts like the old-timey switchboard operators for telephones.
When the “addition” instruction comes up, for example, it may connect register A and B to the input of the adder and register C to the output. The adder does the one thing it’s designed to do, based on how it’s wired: it adds. This circuit that interconnects modules based on an instruction is what we’d call the instruction decoder. It is at this stage when the instructions are given meaning in hardware: the “function” that this circuit computes is “addition operator -> connect A, B, and C to the adder; multiplication operator -> connect A, B, and C to the multiplier; memory read A-> connect A to the memory bank, 1 to the read/write bit, and B to the address select, …” and so on.
To finish off that layer of the computer we need something that’s going to make the program advance, so for that we reach for a clock. There are various ways to set up circuits such that they trigger on the change of a signal. These circuits are used to advance the program from one instruction to the next. They can also be used in circuits that send serialized data down a wire.
Up until this point it is typical to look at the program as one thing and the data it operates on as something else entirely. Back with the original light switch the “program” was wires and the “data” was the position of switches. At this point when we look at the program it’s really just a sequence of numbers, just like the input and output data. What if these were the same thing? Leaning into this idea we move towards reading programs from the same physical media that we might pull data off of (e.g. a floppy disk, where the orientation of magnetic fields can result in the read-write head of the drive reading a 1 or 0 (being energized, closing a switch, or not)).
Finally, with programs as data we make one final realization: programs take in and spit out data. What if we had a program where the input was one program and the output was another? Specifically, what if the input was in a language that humans can understand more easily and the output was that same program but in a format that computers can more readily work with (e.g. the actual sequence of instructions that go into the computer)? This is the notion of a compiler or interpreter (the difference being whether this program runs in real time as the program executes or all at once ahead of time). Thanks to compilers we can write higher and higher level languages that take us even further from the underlying wires and switches (or traces and transistors now) that operate at the lowest level.
As a postscript, the above description gets you to about the 1980s. There have been a lot of developments since then that would extend this already long comment–I haven’t touched on things like microcode (the software running between your assembly and the hardware itself), register renaming, simultaneously multithreading, etc, all of which help improve performance. These are all the result of applying the same concepts described above.
I think you might be asking about how programs can have meaning. It’s all just a bunch of blinkenlights, how can that do useful things like adding numbers together?
It cannot. Software doesn’t do “useful” things, it just does things. Like you can set up a logic gate that takes two inputs and blindly ANDs them together. That’s not inherently useful, it’s only useful if you need two inputs ANDed together. Then there are upstream decisions about when that’s a useful thing to do, which is also all done blindly by a computer. Which things to do are ultimately set in motion by the human user, picking which bits to kick off and run based on what they want, i.e., what is useful to you.
The other aspect of this is the projection of meaning onto particular states of a machine. If you see a sequence of bits it could represent a letter, a number, or anything else. Only a human can assign meaning to a bit sequence, and we do by keeping track of types associated with data.
Weirdly, state has been proven equivalent to computation, so besides assigning meaning to a sequence of bits, you can equivalently assign meaning to a sequence of *steps*. The isomorphism of computation to data means that information can be represented and assigned meaning in two different fundamental ways, and allows systems to encode information along a spectrum with functional and object oriented representations at either end.
Latest Answers