Часть II
Спроектируем Little Man Computer на языке Verilog.
Статья про LMC была на Хабре.
Online симулятор этого компьютера здесь.
Напишем модуль оперативной памяти (ОЗУ), состоящий из четырех (N=2) четырёхбитных (M=4) слов. Данные загружаются в ОЗУ из data_in по адресу adr при поступлении тактового сигнала clk.
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]; //объявляем массив mem always @(posedge clk) //при поступлении тактового сигнала clk mem [adr] <= data_in; //загружаем данные в ОЗУ из data_in assign RAM_out = mem[adr]; //назначаем RAM_out портом вывода данных 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; // счетчик увеличивается на 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
Добавим в счетчик функцию загрузки.
Загрузка осуществляется командой Counter_load.
//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
Добавим в общую схему аккумулятор Acc, мультиплексор MUX2 и сумматор sum.
Сумматор прибавляет к числу в аккумуляторе Acc числа из памяти.
На сигнальные входы мультиплексора подаются числа data_in и sum.
Далее число из мультиплексора MUX2 загружается в аккумулятор Acc.
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
Добавим в основной модуль элемент, вычитающий из числа в аккумуляторе числа, хранящиеся в памяти.
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];
Добавим три команды
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
Поместим команды и адреса в одно ОЗУ, а данные — в другое.
Схему можно скачать отсюда.
В первых восьми разрядах хранятся команды, в последних четырех разрядах хранится адрес, загружаемый в счётчик.
Отмечу, что загрузка числа в аккумулятор Асс должна производиться после переключения мультиплексора 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
Quartus II можно скачать с официального сайта.
При регистрации в разделе My Primary Job Function is* необходимо выбрать пункт Student.
Далее необходимо скачать драйвер для программатора (драйвер для usb-blaster'a можно установить из C:altera...quartusdriversusb-blaster).
Источник