Проектирование процессора (CPU Design) Часть III

Часть I
Часть II
Часть III
Спроектируем схему из предыдущей части на языке Verilog.
Заменим RAM с одним портом чтения/записи на RAM с раздельными портами чтения/записи.
Управление производится командами:
1. загрузка адреса в счётчик Counter,
2. загрузка данных в память RAM,
3. загрузка (из устройства ввода) данных в аккумулятор Acc,
4. переключение мультиплексора MUX.

upwhgceyhpsnlvxrga1hfzdwq98.gif
Подключим счетчик на адресный вход ОЗУ.
При подаче тактового сигнала значение счетчика увеличивается на 1, т.о. можно переходить от ячейки к ячейке, от младшего адреса к старшему.
cm7y_mcki1gzquoxpcbd2eer3je.gif
Напишем модуль ОЗУ на языке Verilog.

module R0 #(parameter N = 2, M = 4)
(
input clk,
input [N-1:0] adr,
input [M-1:0] data_in,
output [M-1:0] RAM_out
);
reg [M-1:0] mem [2**N-1:0];
always @(posedge clk)
mem [adr] <= data_in;
assign RAM_out = mem[adr];
endmodule


Подключим счётчик к адресному входу ОЗУ

module R1 #(parameter N = 2, M = 4)
(
input Counter_clk, RAM_clk,
//input [N-1:0] adr,
input [M-1:0] data_in,
output [M-1:0] RAM_out
);
reg [1:0]counter;
always @(posedge Counter_clk)
 counter <= counter + 1; 
 wire [N-1:0] adr;
 assign adr = counter; 
reg [M-1:0] mem [2**N-1:0];
always @(posedge RAM_clk)
 mem [adr] <= data_in;
assign RAM_out = mem[adr];
endmodule


Вот так выглядит схема в RTL Viewer
jzccyvvgrla87a1skmj9vjcdf60.gif
Добавим в счетчик функцию загрузки. Теперь мы можем командой Counter_load переходить на адрес data_in.

//input Counter_load; 
wire [3:0] branch_adr; // адрес перехода
assign branch_adr = data_in; 
always @(posedge Counter_clk)
begin
 if(Counter_load) //по команде "Counter_load"  переходим по адресу  "branch_adr"
  counter <= branch_adr;
 else
  counter <= counter + 1; 
end 

В отдельном модуле создаем 4bit’ный регистр (аккумулятор)

module register4
(
  input  [3:0] reg_data,
  input reg_clk,
  output reg [3:0] q  
);
always @(posedge reg_clk)
         q <= reg_data;
endmodule


Добавим в общую схему аккумулятор, мультиплексор и сумматор

module R2 #(parameter ADDR_WIDTH = 2, DATA_WIDTH = 4)
(
input Counter_clk, Counter_load, RAM_clk,
input MUX_switch,
input Acc_clk, 
input [3:0] data_in, 
output [3:0] Acc,
output [DATA_WIDTH-1:0] RAM,
output reg [1:0] counter
);
wire [1:0] branch_adr;
assign branch_adr = data_in[1:0]; 
//Counter
always @(posedge Counter_clk)
begin
 if(Counter_load) 
  counter <= branch_adr;
 else
  counter <= counter + 1; 
end  
 
wire [ADDR_WIDTH-1:0] adr;
assign adr = counter;  
//RAM
reg [DATA_WIDTH-1:0] mem [2**ADDR_WIDTH-1:0];
always @(posedge RAM_clk)
mem [adr] <= Acc;
assign RAM = mem[adr];
//sum
wire [3:0] sum;
assign sum =  Acc + RAM;
//MUX
reg [3:0] MUX2; 
always @*
MUX2 = MUX_switch ? sum : data_in;
//Accumulator
register4 Acc_reg(
.reg_data(MUX2),
.reg_clk(Acc_clk),
.q(Acc)
);
endmodule


jbksk0dkioookg7qeskuvpsu77a.gif

Расширим систему команд. Наше устройство будет выполнять те же операции, что и Little Man Computer, различаться будут числовые коды, в которых представлены команды.

Статья про LMC была на Хабре.
Online симулятор этого компьютера здесь.

Добавим в основной модуль элемент «вычитатель»

wire [3:0] subtract;
assign subract =  Acc - RAM ;


Замним двухвходовой мультиплексор четырёхвходовым

always @*
MUX4 = MUX_switch[1] ? (MUX_switch[0] ? RAM : subtract)
: (MUX_switch[0] ? sum : data_in);

Подключим к аккумулятору устройство вывода (4bit’ный регистр), также подключим к аккумулятору 2 флага:
1. Флаг «Ноль» — это лог. элемент 4ИЛИ-НЕ. Флаг поднимается, если содержимое Асс равно нулю.
2. Флаг «Ноль или Положительное число» — это лог. элемент НЕ на старшем разряде четырёхразрядного аккумулятора. Флаг поднимается, если содержимое Асс больше или равно нулю.

//флаг "Ноль" 
output Z_flag;
assign Z_flag =  ~(|Acc); // многовходовой вентиль ИЛИ
//флаг "Ноль или Положительное число"
output PZ_flag;
assign PZ_flag =  ~Acc[3]; 


mxmveuu6yw0mzsn86w6xakho5sy.gif
Добавим три команды
1. загрузка содержимого аккумулятора в устройство вывода data_out
2. загрузка адреса в счётчик, если поднят флаг «ноль» (JMP if Acc=0)
3. загрузка адреса в счётчик, если поднят флаг «ноль или положительное число» (JMP if Acc>=0)

module R3 #(parameter ADDR_WIDTH = 2, DATA_WIDTH = 4)
(
input Counter_clk, RAM_clk,
input JMP, Z_JMP, PZ_JMP,
input [1:0] MUX_switch,
input Acc_clk, 
input Output_clk,
input [3:0] data_in, 
output [3:0] Acc,
output [3:0] data_out,
output [DATA_WIDTH-1:0] RAM,
output Z_flag, PZ_flag,
output reg [1:0] counter
);
wire [1:0] branch_adr;
assign branch_adr = data_in[1:0]; 
wire Z,PZ;
assign Z = Z_flag & Z_JMP;
assign PZ = PZ_flag & PZ_JMP;
//Counter
always @(posedge Counter_clk)
begin
 if(JMP|Z|PZ) 
  counter <= branch_adr;
 else
  counter <= counter + 1; 
end  
 
wire [ADDR_WIDTH-1:0] adr;
assign adr = counter;  
//RAM
reg [DATA_WIDTH-1:0] mem [2**ADDR_WIDTH-1:0];
always @(posedge RAM_clk)
mem [adr] <= Acc;
assign RAM = mem[adr];
//sum
wire [3:0] sum;
assign sum =  Acc + RAM;
//subtract
wire [3:0] subtract;
assign subtract =  Acc - RAM;
//MUX
reg [3:0] MUX4; 
always @*
MUX4 = MUX_switch[1] ? (MUX_switch[0] ? RAM : subtract)
: (MUX_switch[0] ? sum : data_in);

register4 Acc_reg(
.reg_data(MUX4),
.reg_clk(Acc_clk),
.q(Acc)
);
register4 Output_reg(
.reg_data(Acc),
.reg_clk(Output_clk),
.q(data_out)
);
assign Z_flag =  ~(|Acc);
assign PZ_flag =  ~Acc[3]; 
endmodule


1-o7bggvr_5vbojs9io1tlpsv4a.gif

Вообще, Little Man Computer построен на архитектуре фон Неймана, но мы будем хранить команды и адреса в одном ОЗУ, а данные — в другом. Такой способ хранения является отличительным признаком Гарвардской архитектуры
e5zr-itkxdbmy-ebvs7bbwuk16y.gif
Схему можно скачать отсюда.
В первых восьми разрядах хранятся команды, в последних четырех разрядах хранится адрес, загружаемый в счётчик.
Отмечу, что загрузка числа в аккумулятор Асс должна производиться после переключения мультиплексора MUX (для команд ADD, SUB, LDA), по спаду тактового сигнала.
Т.о. в нашем компьютере следующая система команд
48х — ADD добавить число из ОЗУ к Асс
50х — SUB вычесть число, хранящееся в ОЗУ из Асс
80x — STA сохранить число из аккумулятора Асс в ОЗУ по адресу х
58х — LDA загрузить число из адреса х в Асс
04х — BRA безусловный переход в ячейку с адресом x
02х — BRZ переход в ячейку с адресом x, если Асс=0 (условный переход)
01x — BRP переход в ячейку с адресом x, если Асс>=0 (условный переход)
40х — INP загрузить число из data_input в Асс
20х — OUT загрузить число из Асс в data_out
Команды HLT у нас не будет.

Возьмём для примера алгоритм поиска максимального из двух чисел с сайта http://peterhigginson.co.uk/LMC/
Алгоритм работает так: сохраняем в память данных два числа из data_in. Вычитаем из второго числа первое:

  • если результат отрицательный, записываем первое число в Асс, записываем в data_out число из Асс;
  • если результат положительный, записываем второе число в Асс, записываем в data_out число из Асс.


00 INP
01 STA 11
02 INP
03 STA 12
04 SUB 11
05 BRP 08
06 LDA 11
07 BRA 09
08 LDA 12
09 OUT

В нашей системе команд этот алгоритм будет выглядеть так
400
80b
400
80c
50b
018
58b
049
58c
200

tqq8sn8i0spgt5z9u0kfrs2w6e0.gif

© Geektimes