This project is submitted for

Description

October 2017 edit: This project is finished (has been for a while).

I started this project as an attempt to teach myself about how computers worked. As such comments, useful links, or anything else related would be appreciated. Prior to this my experience in electronics was mostly microcontroller based.

The computer is perpetually half done (it could be more it could be less, I only design the logic for the chunk I'm working on) but currently consists of about 300 transistors. I'm pretty sure that the computer follows 'Harvard architecture' (corrections welcome) as the RAM and ROM are strictly segregated. 4 bit is used loosely data width is 4 bits but instruction width is 8 bits as some instructions include 4 bit values.

some stats:
RAM: 16 nybbles (dictated by address register width)
ROM: 16 bytes (dictated by program counter width)
clock speed: more than one (the computer is currently not clocked as much of the sequential logic has yet to be built)

Details

disclaimer:

a lot of what is below is wrong. the project has gone through some major design changes, including: new architectural setup, new command set, new mechanical layout. this is the new architectural set up:

I'm a beginner and know very little about how computers work, this is an educational project. If you find mistakes or have relevant information concerning computers feel free to comment.

The computer follows 'Harvard architecture' (I might be wrong) it has a strictly separated RAM and ROM. 4 bit is a loose indicator of bus width, the ALU is 4 bit and all the processing logic is 4 bit as is the RAM, however the ROM width ('word' length?) is 8 bits as some of the instructions contain a 4 bit value embedded.

here is the architecture diagram for the computer:

major blocks:

RAM:

You might have noticed the Arduino in the (inexplicably upside down) photo. the Arduino is serving as memory, originally I had wanted the Arduino to only simulate a block of D flip flops but there are nowhere near enough pins on any microcontroller (that I know how to use) to do that, as such the Arduino also simulates some of the memory access logic. The Arduino will also serve as the only source of output, it basically just streams the contents of an array to my laptop through the serial monitor in the Arduino IDE, nothing fancy, I know, but it works. And before anyone tells me not to embed an entire Arduino board, this is by no means a finished project, I just wanted a simple way to to avoid soldering 640 transistors.

ROM:

The ROM access logic consists of a 1:16 demultiplexer with the input held high (which has been a source of quite a few problems). the memory consists of 16 dip switches and 128 diodes.

ALU:

The ALU is actually just a 4 bit ripple-carry adder/subtracter. The above picture shows the 4 bit adder. To preform subtraction it uses 2s compliment math. The logic necessary to perform the invert consists of some multiplexers and inverters. The add one is done by using the extra bit on the first full adder in the adding circuitry. There is also a 'comparison unit' which will eventually be responsible for actuating the JMP (jump) command, but strictly speaking this is not part of the ALU.

each line of transistors operates on one bit, hence four lines.

Registers:

there are three registers. the first is the @ (address) register, it points to a location in the RAM with the current value to operate on. The second and third are the B and C registers, these registers are tied to the inputs of the ALU they contain the value for the ALU to operate on. While the registers could be considered memory I have decided to build these by hand. They consist of D latches built from NAND gates. Each latch requires four NAND gates, there are 12 latches total.

program counter construction details were posted in the project logs section.

The program counter's job is to count up by one in binary every clock cycle, and output the result. the output of the program counter then points to ROM address that contains the next instruction to execute. In addition to counting the program counter also has to be re-loadable by the program, I.E. the program should be able to dictate the memory location of the next instruction. Since the program can reset the program counter loops can be implemented. In addition to this if the re-loading of the program counter if done conditionally 'if' statements can be implemented.

The program counter consists of multiplexers, half adders, and D flip-flops.

these are the major blocks of the computer, there are a lot of areas I have glossed over, (control logic anyone?) but this should give you a rough idea of what...

Project Logs

as I mentioned in my last update the computer is finished and operational. here's a picture of its finished state:

so what does finished mean?

-the computer is a physical representation of the architecture diagram

-all commands function

-it is capable of successfully running a multi-step, multi-instruction program

the first point is fairly straight forward as the architecture diagram omits a good deal of detail. all of the major parts of the computer seen on the architecture diagram have been present for quite some time.

the second point is more complicated. my last post described some of the difficulty I was having with the registers and the RAM. further inspection revealed another problem: the program counter would not reload to the correct value under any circumstances. these three problems meant that 7 of the 10 machine code commands did not do what they were supposed to. the first problem was that none of the registers would accept data from the ALU or ROM, however they would load correctly if given 'good' signals. I spent a lot of time trying to clean up the logic levels with discrete components but ultimately failed. because of this I was forced to use comparators. I don't feel particularly good about using integrated circuits but I'll discuss my justifications later. the second problem was the RAM load and output enable commands not working. this problem was much easier to fix. since both the load and output enable functions are handled on the arduino each of these lines is connected directly to a pin on the microcontroller. it turns out that the logic that was generating the load signals was not producing a high that the arduino recognized, to fix this i added an amplification stage between the discrete logic and the arduino. the final problem was similarly easy to fix. the reload line on the program counter can receive data from two sources, to produce a useful output the sources must be run through an OR gate, I had been lazy and built this OR gate out of diodes, I replaced the diodes with a simple transistor OR and some amplification and the function worked properly

the third point of my original list is the ability to run a non-trivial program. this was completed successfully on the 19th of August 2015, roughly two years after I began this project. the program I ran simply added 2 + 2 and stored the result in RAM address zero. 2 + 2 was calculated to equal 4. the program in its entirety:

mnemonic

value

command

comment

OE ROM

NULL

0110

asserts ROM out bus to main bus

LOAD B '2'

0010

0001

loads B register from main bus (since ROM is asserted to the main bus the value accompanying this command is also asserted to the main bus, therefore B is loaded with the value 0010 (2))

LOAD C '2'

0010

0010

same as above except that the C register is being loaded

LOAD @ '0'

0000

0000

same as above except that the @ (adress) register is being loaded, RAM automatically points to the address indicated by the value stored in the @ register

OE ROM

NULL

0110

unasserts(?) ROM out bus from the main bus

OE ALU

NULL

0101

asserts ALU out bus to the main bus. since the ALU is combinational in nature the values stored in the B and C registers where added instantaneously

LOAD RAM

NULL

0100

RAM was loaded at the address indicated by the address register.

when the program was completed I switched the clock off and used the arduino ide to look at the contents of RAM. originally the above program produced the result 0010, however this appears to be due to the lsb and msb on the ALU bus being inverted (i put a plug in backwards) oops. after flipping the bus around the program produced the expected results.

so that about covers it. I made a computer. the following is my reflection on the project, feel free to skip to the tl;dr at the bottom.

was this project a success? throughout this project I have had two goals, ranked by importance they are:

1. learn how computers work

2. build a computer out of its constituent parts

I feel that the first goal was achieved. I started this project with no understanding...

I have successfully tested and rearranged all of the modules that make up the processor and on their own each module seems to be functioning. I have also built a new board:

this board is essentially just control logic it takes decoded instructions in on a 10-bit instruction bus and then turns them into usable load/output enable signals. its mostly just AND gates that AND the instruction and load clk to produce an appropriately timed load/out put enable bit. if you look at the new architecture diagram:

you'll notice that there are only three places data on the main bus can come from: RAM OUT, ALU OUT, and ROM OUT. to decide which location data is coming from the computer needs output enables, i.e. if you want to load the @ register with a value from ROM the ROM OUT bus must be enabled and the ALU OUT and RAM OUT buses must be disabled. to do this i built a 'bus toggle' circuit:

basically whenever the the above logic receives a 'toggle' command it switches to the opposite of what it was previously, i.e. data is being passed through>toggle>data is not being passed through or vice versa. this allows the programmer to decide where the data on the main bus is coming from by means of toggling the appropriate sources. a side effect of this is that a careless programmer can accidentally superimpose data from two sources onto the main bus at the same time. I actually only built two of the above circuit, one for ROM OUT and one for ALU OUT, the third I implemented in software on the arduino. while this is technically cheating, I don't feel it takes away from the project because, a) i have demonstrated that I can build the required circuit, and b) it is not technically part of the processor, as it is RAM access logic.

after building the control logic board I proceeded to assemble the computer and attempt to run the very first program, it failed.

as you would expect I was thoroughly disappointed that the computer did not work, I wasn't realy expecting to work on the first try, but still... any way I spent a lot of time retesting all of the modules in various configurations and discovered a few things:

1. each module works as expected if it receives 'good' signals (i.e. data one = v+ and data zero = v-)

2. the ALU works as expected

3. the program counter ROM and ROM access logic all work together as intended, including loops, if they receive 'good' signals

4. the register work if given 'good' signals

5. the RAM works if given 'good' signals

6. when all the pieces are assembled things stop working

this was pretty mystifying. each piece of the processor seems to work on its own but when I put them all together they seize up and stop working, the RAM refuses to load, the register store data incorrectly, and the program counter resets incorrectly. at present the best explanation I have for this behaviour is that data sources (ROM, ALU, (maybe RAM)) are not giving 'good' signals. the problem with this is that I don't really know what constitutes a 'good' signal. this is because I don't know what the logic levels are/should be for RTL. because of this I only know what 'perfect' signals are i.e. connect inputs directly to positive or directly to negative. when I measure the logic levels of the data on the main bus I can see that it is not perfect, (i.e HIGH != 5v and LOW != 0v) however I don't know how to fix this using transistors. if anyone has any ideas feel free to comment.

this problem is really bugging me because obviously some one figured out how to solve it because digital logic works, but how that was done I don't know. so far I've come up with two potential solutions for restoring logic levels to 'perfect' levels

1. relays. relays are switches so I could theoretically use them to switch rail voltages directly. the problem with relays is that they take lots of power to switch and I'm not sure if the fuzzy values coming out of my logic would be able to flip them

2. comparators. I've already used comparators on the project, but it was technically not...

now that exams are over I've had some time to work on the computer. as mentioned in the last couple of posts the computer's architecture has changed quite drastically, because of this most of the I've been doing has been rewiring/reconnecting modules that are already built. when this is done and all of the modules are working satisfactorily I plan to start building a little more logic, so stay tuned.

as I mentioned in my last project log I have changed the command set. I won't bother reproducing the old command set here, however this is the new command set:

mnemonic

binary (decimal)

description

LOAD:

following commands load the indicated unit with the value asserted to the main bus

@

0000 (0)

B

0001 (1)

C

0010 (2)

PC

0011 (3)

RAM

0100 (4)

loads address pointed to by @ register

TOGGLE BUS:

following commands enable/disable the indicated unit's output bus

ALU

0101 (5)

ROM

0110 (6)

RAM

0111 (7)

asserts value located in address indicated by @ register

MISC:

miscellaneous commands

TOGGLE ADD/SUB

1000 (8)

toggles the ALU between addition and subtraction

LOAD PC IF B==C

1001 (9)

loads the PC with the value asserted to the main bus if the values in registers B and C are equal

as mentioned in previous posts the clock is split up into two cycles, a PC CLK and a LOAD CLK. a full clock cycle would go something like this:

rising edge of PC CLK:

new command/value is asserted to the ROM out bus

rising edge of LOAD CLK:

command is carried out

there are two major types of command, LOAD and TOGGLE BUS.

LOAD:

the load pin selected by the command is toggled high for the entire LOAD CLK

TOGGLE BUS:

the write enable for the bus indicated by the command is toggled on the rising edge of LOAD CLK. if a write enablle was already high, it would be toggled low, if it was already low it would be toggled high.

example:

if we wanted to write the out put of the ALU to RAM first the command TOGGLE BUS ALU would be executed, this command would would write the output of the ALU to the main bus. then the PC would increment loading the next command; LOAD RAM. this command would toggle the load pin on the RAM high for the entire LOAD CLK.

a side effect of this new command set architecture is that values from ROM will be available one clock cycle after the TOGGLE BUS ROM command. for example to get a value from ROM to RAM one would first have to execute a TOGGLE BUS ROM command opening the ROM BUS, the next instruction would contain the value to be loaded to RAM, as the value and instruction are contained in the same ROM memory address.

mechanical layout:

as you may have noticed the mechanical layout has changed, this is to cope with the increase in the number of boards required for the computer. all nine logic boards are mounted on a plywood frame that is about the size of a briefcase.

last post I mentioned some problems. I seem to have them under control (read as: successfully swept under the rug) if you're interested in what was going on there's an explanation in the comments on the last post.

now for the big news.

for a while I have had a feeling that this thing was not designed as well as it could be. those of you familiar with processor layout probably noted some of the odd design choices that can be observed in the architecture diagram. notably the ALU is sort of smeared all over the place and there is not a main data bus. this leads to some weird behaviour such as an inability to load the @ register from RAM, another problem the architecture creates is a relatively ineffective conditional branching mechanism. for a while I figured I would just finish this project and build the next one right. no longer. I have decided to rework the computer's architecture. this will have a few advantages, first it will make the computer simpler, and second it will make the computer more universal. this move also comes with some setbacks, namely I'll have to take apart some of the stuff I've all ready built. fortunately all of the major logical blocks remain the same, most of the work should be in pulling them apart, not in actually rebuilding anything. all of the boards I've built will be used (except some multiplexers), but the will be connected in a new way.

here's the old architecture diagram:

and here's the new architecture diagram:

what's new?

as you can see the new design is organized around a central bus removing the need for multiplexers everywhere. an indirect effect of the bus centered design is that the control logic is simpler. you'll notice that the first diagram says that the control logic is simplified, that was my way of saying I had no idea what I was doing (as opposed to now when I have almost no idea what I'm doing). the new diagram shows control logic that is basically one level above individual gates. the complexity of the control logic is the major reason for the redesign. for the old architecture to work I would have needed to design specific control logic for each command, now I need to design 3 sets of control logic and then implement it a few times. a side effect of the revised control logic is that there is also a revised command set, one that is hopefully more universal. later this week I will post a more in depth look at the commands, what they do ,and how they work, but this is it for now.

in the last post I said I had fixed the timing problems. on closer observation I realized I had only made them slightly less noticeable. the idea of breaking up the clock is correct and the logic I proposed almost worked, but not in all test cases it turns out. I then spent some time designing slightly more well thought out logic and produced this:

the above logic would appear to do everything I want it to. when I connected it to the program counter everything went fine, but upon inspection of the output of the instruction decoder I realized that something is seriously wrong. the instruction decoder, which had appeared to be working now does not and I don't know why. as you may have noticed this project has been on the back burner for a while, mostly due to the timing problems, but I'm going to try and figure out what's going on with the instruction decoder. as of right now I'm pretty stuck. any advice would be appreciated.

I changed the timing of the computer. rather than trying to execute an instruction on every clock cycle, I broke up the clk into two parts. a program counter clock and a load clock. diagram:

as you can see the program counter is incremented every other clock cycle. in stead of ANDing the clk and CMD (load @ instruction above) to produce the load @ signal I am now ANDing the load clk and CMD lines. another way of looking at it is that instead of trying to increment the program counter and write data in one clock cycle I am now incrementing the program counter in one clock cycle and writing data in the next clock cycle. the necessary logic is pretty trivial, basically a d flip flop and some gates, but the result is a working command! yay! also I'm pretty sure that since the above solution worked the double blip hypothesis was correct.

I had always kind of assumed that it would be simplest to have the computer execute one instruction per clock cycle. it turns out this isn't the case. while my 1 instruction per clock, logic was correct I did not account for propagation delay.

here's a quick recap of how the computer's sequential logic fits together:

essentially the number coming out of the program counter is an address and is passed through the ROM access logic and selects and address (pink boxes). each address contains 8 bits of info (upper four are a value, lower four are a command). the value held in the ROM address indicated by the program counter is dumped onto the 8 bit bus. the upper half of the bus goes to the rest of the computer to do what ever the command tells it to. the lower half (the command) is sent through the instruction decoding logic, which is essentially a giant multiplexer (I.e 0110 (6 in decimal) goes in and 0000000001000000 comes out).

now if I wanted to load the upper half of the 8 bit ROM bus (V from now on), into the @ register I would need to pulse the load pin on the @ register. to do this I would program a value into the lower four bits of ROM (CMD from now on) that corresponded with the multiplexer output wire that was connected to the @ register load pin. I.e. if the 12th line coming out of the instruction decoding logic was connected to the @ register load pin I would set CMD equal to 1100. that way whenever CMD==12 the @ register is loaded with the value V.

there is a slight problem with the above explanation. V is asserted to the @ register input bus at the same time that CMD is fed into the instruction decoding logic, namely the rising edge of the clock. both values remain the same until the next rising clock, at which point they are replaced with the contents of the next ROM address. another thing to note: the @ register accepts a new value on the falling edge of the @ register load pulse. maybe you can see the problem. V and CMD change at exactly the same time. that means that the falling edge of CMD (the point at which the @ register loads) is the same point at which the value we are trying to load (V) changes, resulting iin garbage being loaded into the @ register.

solution? my first attempt at solving the problem was as follows. add an AND gate between the clock and the CMD lines and use the output to load the @ register. because the CMD and V values change on the rising edge of the clock but we need a falling edge to load the @ register we can use the falling edge of the clock to load the @ register. if the above paragraph made no sense fear not, here is the timing diagram:

load instr should say 'CMD'. basically Load @ = clk AND CMD. the above is logically correct, however you should note the word 'ideal' at the top of the page. as you may have heard propagation delay is a thing. I had assumed that this processor was small and slow enough that I wouldn't have to worry about propagation delay, not so! below is the 'real' timing diagram:

again, load instruction should be CMD. between the clock and CMD there are ~200 transitors. between clk and load @ register there are 2 transistors. there is a slight lag between the clk and CMD due to the number of transistors, compared to the AND gate between clk and CMD and load @. because of this the AND gate responds almost instantaneously and you get that little double blip on the @ register's load pin. this second blip also happens right as V is changing resulting in a garbage load of the @ register on the next clock. that stupid double blip took me two days to figure out.

I'm pretty sure that the double blip hypothesis is correct, it lines up with my observations quite nicely, however, I have not been able to observe this on my oscilloscope, although that's probably due to it being a pretty crappy oscilloscope and me not really understanding how to use it.

how does one solve a double blip? my best solution would be to split up the clock cycle. every other clock pulse would increment the...

I successfully executed an instruction for the very first time. There are still some bugs to work out, but the first program shouldn't be very far off. the first instruction I executed was a load of the @ register. basically what I did was load the binary numbers 0-15 into the upper nybble of ROM, I then connected the output to the @ register input bus, and applied the clock signal. for this demo I connected the @ register load bit to the clock. eventually this will be connected to the control logic, but before that can happen I have some timing issues to fix in regards to the output of the control logic multiplexer. It shouldn't be long before this runs it's first program.

That's so awesome ! I plan on making something like this. Can you please give the detailed list of components and circuit diagrams? Also you have designed everything using transistors, I can use ICs instead, right ?

This was my dream project, it's nice to see someone actually did the research & spent effort on it.By the way, you made a computer from scratch but you didn't know the astable multivibrator... you are an interesting man :))

!. Utterly awesome
2. You must bemad!
3. Utterly awesome :-). Keep up the good work.
Look at serial in parallel out and parallel in serial out shiftregisters as an Arduino replacer.
(Memory says eg CD4021, CD4094?)

I had thought of using something other than an Arduino for the RAM, but the Arduino has the big benefit of being able to display the contents of the RAM through the serial monitor without the need for extra hardware.