Собираем свой компьютер. Компьютер
На основе курса Nand2Tetris и книги The Elements of Computing Systems
На этом посте заканчивается “железная” часть компьютера, дальше остается только софт - интерпретаторы, ассемблеры, виртуальная машина и прочее. Сначала разберемся с машинным кодом и принципиальным устройством компьютера.
Компьютер
Общая принципиальная схема компьютера:
В нашем случае CPU (процессор) - это, грубо говоря, ALU, созданный ранее. Память состоит из нескольких RAM чипов (для данных) и ROM (read only memory) памяти, где хранится программа. Обычно программа не является частью компьютера. Как пример - картриджи для NES. Их обычно так и называют: “Contra ROM for NES”, “Mario ROM for NES”.
В программе в пронумерованном порядке лежат машинные коды для CPU. Машинный код - формализованная запись команды для CPU. По сути - набор всех инпутов для ALU, упорядоченный особым образом.
Иерархия памяти
Чем больший размер памяти мы используем, тем дороже нам обходится чтение из нее. Это связано с тем, что в больших блоках памяти придется использовать адрес длиннее, чем в коротких. Чтобы смягчить эту проблему, мы используем иерархию памяти: у нас есть совсем маленькие участки памяти (вплоть до регистров), которые обращаются к участкам побольше, которые обращаются к участкам побольше и так далее. Самые высокие уровни памяти обычно расположены прямо в процессоре. Схема примерно такая: регистры процессора → RAM → жесткий диск. Обычно регистры являются частью машинных кодов.
В нашей реализации у CPU будет 3 регистра: D, A, M:
- D - Data register. В нем просто можно хранить данные.
- A - Address register. Также можно использовать для хранения данных. Кроме этого, является ссылкой на значение в регистре M
- M - Memory register. Ссылается на участок памяти RAM[A]. Через этот регистр процессор будет взаимодействовать с памятью. Не является регистром в физическом смысле.
Ввод/вывод
В качестве ввода/вывода будут использоваться клавиатура и черно-белый монитор. По сути они оба являются участками памяти.
Каждому пикселю монитора ставится в соответствие определенный бит в выделенном промежутке памяти. Если этот бит установлен в 1, то на мониторе этот пиксель загорается черным. Иначе он остается белым.
Клавиатура - наоборот: пишет в выделенный ей участок памяти какое-либо значение, которому соответствует нажатая клавиша. Если на клавиатуре никто не нажимает кнопки в данный момент, то в регистре будет просто 0.
Program Counter (PC)
Еще один регистр, расположенный в процессоре. Используется для управления потоком выполнения программы. В нем хранится адрес команды, которая будет выполнена следующей. При выполнении машинной команды автоматически инкрементируется, что позволяет в следующий тик выполнить следующую команду. Кроме того, существуют специальные jump команды, которые позволяют установить PC в нужный адрес. С их помощью реализуются всевозможные циклы и if операторы.
A/C Команды
Подробнее спецификацию кодов можно посмотреть в книге The Elements of Computing, которая частями выложена на сайте nand2tetris.
Будет два вида машинных команд:
- Address command
- Computation command
В прошлых постах чипы были реализованы для 4-битного компьютера, потому что так легче описывать схемы. Для 8\16\32\64 устройство принципиально не отличалось бы.
Но дальше я буду описывать устройство для 16-битного компьютера, так что и регистры у нас будут 16-битные. И, соответственно, команды.
A Command
В общем виде A команда выглядит так:
\[0vvv\ vvvv\ vvvv\ vvvv\]Если команда начинается c “0”, то это А команда. Остальные 15 битов описывают адрес. Адрес не может быть отрицательным числом. Единственное, что делает А команда - записывает адрес в регистр А.
C Command
В общем виде C команда выглядит так:
\[111a\ c_1c_2c_3c_4\ c_5c_6d_1d_2\ d_3j_1j_2j_3\]C команда всегда начинается с 1.
В группе битов a и c определяется, что именно мы хотим вычислить, какую операцию и над какими регистрами хотим выполнить.
Группа битов d определяет, в какой регистр нужно записать результат вычисления.
Группа битов j отвечает за управление потоком выполнения команды. Если результат вычисления c удовлетворяет условию j, то в PC запишется адрес, указанный в регистре А
Реализация
Память
CHIP Memory {
IN in[16], load, address[15];
OUT out[16];
PARTS:
DMux4Way(
in = load,
sel = address[13..14],
a = ramLoad1,
b = ramLoad2,
c = screenLoad,
d = keyboard
);
Or(
a = ramLoad1,
b = ramLoad2,
out = ramLoad
);
RAM16K(
in = in,
load = ramLoad,
address = address[0..13],
out = ramOut
);
Screen(
in = in,
load = screenLoad,
address = address[0..12],
out = screenOut
);
Keyboard(out = keyboardVal);
// Make sure address <= 0110 0000 0000 0000
Or8Way(in = address[0..7], out = isFirstHalfZero);
Or8Way(in[0..4] = address[8..12], in[5..7] = false, out = isSecondHalfZero);
Or(a = isFirstHalfZero, b = isSecondHalfZero, out = isAllZeroes);
Not(in = isAllZeroes, out = isInAddress);
Mux16(
a = false,
b = keyboardVal,
sel = isInAddress,
out = keyboardOut
);
Mux4Way16(
a = ramOut,
b = ramOut,
c = screenOut,
d = keyboardOut,
sel = address[13..14],
out = out
);
}
Стоит обратить внимание на код после комментария “Make sure address <= 0110 0000 0000 0000”. За этим адресом находится память, не используемая ни для данных, ни для клавиатуры, ни для экрана. Поэтому нам нужно убедиться, что мы не будем пытаться писать в эту область памяти.
CPU
CHIP CPU {
IN inM[16], // M value input (M = contents of RAM[A])
instruction[16], // Instruction for execution
reset; // Signals whether to re-start the current
// program (reset==1) or continue executing
// the current program (reset==0).
OUT outM[16], // M value output
writeM, // Write to M?
addressM[15], // Address in data memory (of M)
pc[15]; // address of next instruction
PARTS:
Not(in = instruction[15], out = isACommand);
Or(a = isACommand, b = instruction[5], out = aLoad);
And(a = instruction[15], b = instruction[4], out = dLoad);
And(a = instruction[15], b = instruction[3], out = writeM);
//A Register Mux
Mux16(
a = aluToA,
b = instruction,
sel = isACommand,
out = aMux);
//A Register
ARegister(
in = aMux,
load = aLoad,
out = aRegister,
out[0..14] = addressM
);
//ALU Mux
Mux16(
a = aRegister,
b = inM,
sel = instruction[12],
out = aluMux);
//D Register
DRegister(
in = aluToD,
load = dLoad,
out = dRegister);
ALU(
x = dRegister,
y = aluMux,
zx = instruction[11],
nx = instruction[10],
zy = instruction[9],
ny = instruction[8],
f = instruction[7],
no = instruction[6],
out = outM,
out = aluToD,
out = aluToA,
zr = aluZero,
ng = aluNegative
);
Not(in = aluZero, out = notZr);
Not(in = aluNegative, out = notNr);
And(a = true, b = instruction[0], out = j3);
And(a = true, b = instruction[1], out = j2);
And(a = true, b = instruction[2], out = j1);
And(a = instruction[2], b = instruction[1], out = j1j2);
And(a = j1j2, b = aluNegative, out = j1j2Nr);
And(a = instruction[2], b = notZr, out = j1NZr);
And(a = j1NZr, b = aluNegative, out = j1NZrNr);
And(a = instruction[1], b = aluZero, out = j2Zr);
And(a = j2Zr, b = notNr, out = j2ZrNNr);
And(a = instruction[0], b = notZr, out = j3NZr);
And(a = j3NZr, b = notNr, out = j3NZrNNr);
Or(a = j1j2Nr, b = j1NZrNr, out = firstHalf);
Or(a = j2ZrNNr, b = j3NZrNNr, out = secondHalf);
Or(a = firstHalf, b = secondHalf, out = jump);
And(a = jump, b = instruction[15], out = pcLoad);
PC(
in = aRegister,
load = pcLoad,
inc = true,
reset = reset,
out[0..14] = pc
);
}
// a c1 c2 c3 c4 c5 c6 d1 d2 d3 j1 j2 j3
// 1 1 1 0 0 0 1 1 0 0 0 0 1 0 0 0
// 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Снизу листинга для удобства расписано, какой бит в шине данных соответствует какому биту в C команде.
Компьютер
CHIP Computer {
IN reset;
PARTS:
ROM32K(
address = pc,
out = romOut
);
CPU(
inM = ram,
instruction = romOut,
reset = reset,
pc = pc,
addressM = addressM,
writeM = writeM,
outM = outM);
Memory(
in = outM,
load = writeM,
address = addressM,
out = ram
);
}
Просто собираем из уже известных нам чипов компьютер. Вход reset
означает, хотим ли мы сбросить выполнение программы на начало. Если на этом входе мы считываем сигнал, то устанавливаем Program Counter в 0.