В прошлой части добавили расширение M (умножение и деление), теперь будем собирать под ПЛИС Cyclone IV. При попытке собрать проект Quartus говорит, что с асинхронной памятью работать не будет, поэтому заменяем её на синхронную, и это сразу приводит к серьёзным последствиям.

Синхронная память

Изначально ошибки компиляции возникают в окружении ядра (core_tb.v), там где выделена память. Переносим чтение инструкции и данных в блок, работающий по тактовому сигналу, как об этом просит компилятор, и превращаем асинхронное чтение памяти в синхронное.

reg [31:0] i_data; // инструкция
reg [31:0] old_data; // данные
always@(posedge clock) begin
	i_data <= rom[i_addr[31:2]];
	old_data <= rom[d_addr[31:2]];
end

Теперь нельзя на текущем такте одновременно получить инструкцию и данные для этой же инструкции. Чтобы решить эту проблему, разобьём обработку инструкции на отдельные этапы, каждый из которых выполняется на отдельном такте, и пока следующий этап обрабатывает старую инструкцию, текущий будет работать уже с новой. Такая организация процессора называется конвейером, и о том как он появился в intel можно почитать в статье. Появление отдельных стадий обработки инструкции означает, что все значения, используемые в вычислениях, надо не забывать сохранять в промежуточные регистры. Звучит очевидно, но неделю на отладку потратить таки пришлось. В общем заменяем проволочки на регистры, пишем туда сигналы, которые выдало ядро, и меняем их в местах использования.

reg [31:0] old_data_out;
reg [31:0] old_addr;
reg [1:0] old_width;
reg old_data_w;
reg old_data_r;
always@(posedge clock) begin
	old_data_out <= d_data_out;
	old_addr <= d_addr;
	old_width <= d_width;
	old_data_w <= data_w;
	old_data_r <= data_r;
end

wire [1:0] addr_tail = old_addr[1:0];
...

Ещё одна проблема, на которую ругается компилятор, связана с тем, что теперь у нас не симуляция, поэтому тактовый сигнал надо заводить со стороны. Оборачиваем лишнее в `ifdef SIMULATION.

`ifdef SIMULATION
reg clock = 0;
reg reset = 1;
`else
input wire clock;
input wire reset;
`endif
...

Получаем в итоге обновлённый файл тестового окружения ядра core_tb.v (в конце статьи основные изменённые файлы приведены целиком).

Ядро

Снаружи всё сломали, теперь идём приспосабливать ядро к новым условиям. Напомню, как определён его интерфейс.

module RiscVCore
(
	input clock,
	input reset,
	
	output [31:0] instruction_address,
	input  [31:0] instruction_data,
	
	output [31:0] data_address,
	output [1:0]  data_width,
	input  [31:0] data_in,
	output [31:0] data_out,
	output        data_read,
	output        data_write
);

Этап 0, запрос инструкции (Fetch)

Как теперь должен выглядеть путь обработки данных? Для начала выставляем на шину адреса инструкции какое-то значение.

wire [31:0] stage0_pc;
assign instruction_address = stage0_pc;

Пока не будем задумываться, откуда оно взялось, потом как руки дойдут, тогда и посчитаем. На этом нулевая стадия закончилась. Вообще для того чтобы не потеряться в переменных, стоит дать им префиксы по номеру этапа конвейера, к которому они относятся. Можно было бы отдельные этапы вообще сделать отдельными модулями, но пока ядро маленькое (280 строк), можно не возиться.

Этап 1, обработка инструкции, запрос данных (Decode, Execute)

На этом этапе значение инструкции уже приехало на вход процессора, забираем его и пытаемся исполнить.

wire [31:0] instruction = instruction_data;

Пришла пора задуматься, как будет работать наш конвейер, а чтобы думалось лучше, приведу одну интересную инструкцию из теста.

sw  a5,0(sp)    // *(sp + 0) = a5
lw  a4,0(sp)    // a4 = *(sp + 0)

Появляется она вроде бы из этого кода, так что ничего не предвещало.

core_init_matrix(results[0].size,
  results[i].memblock[2],
  (ee_s32)results[i].seed1
  | (((ee_s32)results[i].seed2) << 16),  // <- отсюда как-то получилось
  &(results[i].mat));

Названия "синхронная" и "асинхронная" память с точки зрения простого программиста звучат парадоксально, потому что под синхронным кодом понимается тот, который срабатывает сразу в момент вызова, но здесь всё иначе. После того как инструкция отработала и выдала адрес на шине данных, сами данные появятся только на следующем такте. С записью ещё хуже, с точки зрения ядра всё сработает сразу, но есть проблема под названием RMW (read-modify-write). Изменить мы можем не всё 32-битное слово, а только часть (зависит от data_width), поэтому обвязке надо сначала прочитать данные из памяти, подождать такт, и потом записать модифицированное значение. В итоге последовательность инструкций вида "записать", "прочитать" приведёт к ошибке, прочитаны будут старые данные. Чинить это можно разными способами, например, сделать память 8-битной*4, тогда модификация будет срабатывать за 1 такт. Можно сравнивать адреса со стороны шины и если совпали, возвращать новое значение, но тут вопрос, что будет с volatile переменными, будет ли компилятор пытаться записать только один байт или сделает правильно (прочитает, заменит байт, запишет). В общем можно придумать, как обойти эту ситуацию так, чтобы не уменьшать эффективность процессора, но это всё оптимизации на будущее.

Где записывать регистр

А мы пока вспомним о главной проблеме: запрошенные данные приходят только на следующем такте. Значит неизбежно появится следующий этап конвейера, на котором будет происходить запись прочитанного значения в целевой регистр, и тут возникает вопрос, стоит ли другим типам инструкций тоже отложить запись до этого этапа или писать сразу? С одной стороны, удлинение конвейера вредно, чем больше стадий, тем больше ожидание в случае остановки конвейера, с другой стороны, так можно поднять частоту. В нашем случае запись будет в одном месте. Хотя это может создать определённые проблемы в будущем с параллельным выполнением инструкций, так как ожидание нескольких записываемых регистров может остановить все конвейеры, но с другой стороны, может помочь в случае генерации исключений и необходимости отмены операций.

Флаги конвейера

Выше упомянули про ожидание записи регистров, сейчас разберёмся, как его организовать, но для начала рассмотрим более общую ситуацию. По каким поводам гипотетически конвейер может быть остановлен? Первый случай, это когда текущий этап конвейера отрабатывает многотактовую инструкцию, то есть сам себя задерживает. Тут всё просто, если инструкция говорит, что ещё не готова, значит, надо попросить подождать предыдущий этап (пусть будет суффикс wait) и сказать следующему этапу, что данных для него пока нет (empty). Второй случай остановки конвейера, когда для обработки инструкции ещё нет данных со следующего этапа (jam_up). Подобная ситуация возникает, если предыдущая инструкция ещё не успела записать в регистр, а следующей уже нужно его значение.
Теперь собираем флажки конвейера для первого этапа.

wire stage1_jam_up; //стадия остановлена следующей стадией
reg stage1_empty; //стадия конвейера не получила инструкцию с предыдущего этапа
wire stage1_pause = stage1_empty || stage1_jam_up; //стадии пока нельзя работать
//сохраняем адрес инструкции с предыдущего этапа
wire [31:0] pc; //pc <= stage0_pc

always@(posedge clock or posedge reset)
begin
	if (reset == 1) begin
		stage1_empty <= 1;
	end
	else begin
		stage1_empty <= 0;
	end
end

stage1_jam_up отвечает за ожидание записи регистра следующим этапом.
Поскольку инструкция теперь приходит не сразу, у первой стадии тоже может быть момент, когда она не готова к работе, так как предыдущий этап не передал ей данные. За это отвечает флаг stage1_empty. Сейчас он отвечает только за сброс работы, но в случае появления кэша пригодится для ожидания прихода инструкции. Кстати говоря, если инструкция многотактовая, то нам нельзя переходить к следующему адресу инструкции, пока не отработает текущая, то есть этап 0 блокируется.

wire stage1_wait = stage1_pause || stage1_working;
assign stage0_pc = stage1_wait ? pc :
						(is_op_branch && branch_fired) ? pc_branch :
						is_op_jal ? pc_jal :
						is_op_jalr ? pc_jalr :
						pc + 4;

Дальше надо разобраться, на что же влияет остановка первой стадии. На однотактовые инструкции АЛУ можно не обращать внимания, лишь бы исключение не кинули (которых пока нет). Вот чтение и запись лучше лишний раз не запускать, чтобы не записать чего лишнего.

assign data_read = is_op_load && !stage1_pause;
assign data_write = is_op_store && !stage1_pause;

Умножение многотактовое, ему тоже не даём запускаться без корректных данных.

wire [31:0] rd_mul;
wire is_mul_wait;
RiscVMul mul(
				.clock(clock),
				.reset(reset),
				.enabled(!stage1_pause && is_op_multiply),
				.op_funct3(op_funct3),
				.reg_s1(reg_s1),
				.reg_s2(reg_s2),
				.rd_mul(rd_mul),
				.is_mul_wait(is_mul_wait)
			);

С инструкциями на текущем этапе разобрались, теперь можно собирать результаты. Блокируем конвейер для многотактовых инструкций.

wire stage1_working = is_mul_wait;

Запоминаем факт записи регистра для следующей стадии, так как не все инструкции пишут в rd.

wire write_rd_instruction = is_op_load || is_op_alu || is_op_alu_imm 
							|| is_op_load_upper || is_op_add_upper
							|| is_op_jal || is_op_jalr;
wire is_rd_changed = (!(stage1_working || op_rd == 0)) && write_rd_instruction;
И выдаём само значение регистра для следующей стадии.
wire [31:0] stage1_rd = /*is_op_load ? rd_load :*/
						is_op_multiply ? rd_mul :
						is_op_alu || is_op_alu_imm ? rd_alu :
						is_op_load_upper ? rd_load_upper :
						is_op_add_upper ? rd_add_upper :
						is_op_jal || is_op_jalr ? rd_jal
						: 0;

Этап 2, запись данных (Write)

На предыдущем этапе почти всю работу сделали, здесь осталось записать значение в регистр и выдать флаги остановки конвейера. Для начала забираем то, что передал предыдущий этап, в регистры.

reg [2:0] stage2_funct3;
reg stage2_is_op_load;
reg stage2_is_op_store;
reg [31:0] stage2_addr;
reg [31:0] stage2_rd;
reg[4:0] stage2_op_rd;
reg stage2_is_rd_changed;
reg stage2_empty; //ничего не делаем, потому что предыдущая стадия ничего не передала

always@(posedge clock or posedge reset)
begin
	if (reset == 1) begin
		stage2_funct3 <= 0;
		stage2_is_op_load <= 0;
		stage2_is_op_store <= 0;
		stage2_addr <= 0;
		stage2_rd <= 0;
		stage2_op_rd <= 0;
		stage2_is_rd_changed <= 0;
		stage2_empty <= 0;
	end
	else begin
		stage2_funct3 <= op_funct3;
		stage2_is_op_load <= is_op_load;
		stage2_is_op_store <= is_op_store;
		stage2_addr <= data_address;
		stage2_rd <= stage1_rd;
		stage2_op_rd <= op_rd;
		stage2_is_rd_changed <= is_rd_changed;
		stage2_empty <= stage1_wait;
	end
end

Когда данные приходят из памяти, их ещё надо обработать, поэтому передаётся funct3. Вычисление rd для операции чтения в принципе переехало на этот этап.

wire load_signed = ~stage2_funct3[2];
wire [31:0] rd_load = stage2_funct3[1:0] == 0 ? {{24{load_signed & data_in[7]}}, data_in[7:0]} : //0-byte
                      stage2_funct3[1:0] == 1 ? {{16{load_signed & data_in[15]}}, data_in[15:0]} : //1-half
                      data_in; //2-word

Теперь, когда все варианты операций вычислены, можно получить значение выходного регистра.

wire [31:0] stage2_rd_result = stage2_is_op_load ? rd_load : stage2_rd;

Дальше смотрим, какие регистры понадобятся предыдущей стадии, и при совпадении с меняемым регистром выставляем флажок.

wire stage2_rs1_equal = (stage2_op_rd == op_rs1) && (type_r || type_i || type_s || type_b);
wire stage2_rs2_equal = (stage2_op_rd == op_rs2) && (type_r || type_s || type_b);
wire stage2_rd_fired = stage2_is_rd_changed && (stage2_rs1_equal || stage2_rs2_equal);

Если происходит запись в память и потом чтение, тоже выставляем флажок.

wire stage2_memory_fired = (stage2_is_op_store && (is_op_load || is_op_store) && stage2_addr[31:2] == data_address[31:2]);

Выкидывание флажка is_op_load из выражения не приводит к появлению ошибок crc в тесте (да как так?), но как выше говорилось, потенциально проблема есть. Флажок is_op_store здесь нужен из-за того, что запись может происходить, например, в два последовательных байта, лежащих в одном 32-битном слове. Если начать запись в один и тут же во второй, то для второго будет прочитано неактуальное значение.
Теперь у нас есть все флажки, чтобы выяснить, должен ли ждать предыдущий этап (он же следующая инструкция).

assign stage1_jam_up = !stage2_empty && (stage2_rd_fired || stage2_memory_fired);

Вопрос о том, к какому этапу относится этот флажок, может быть дискуссионным. С точки зрения инкапсуляции вычисление используемых регистров надо выносить на этап 1, ведь с ростом числа инструкций будет сложнее понять, какой из них надо 2, а какой 1 регистр.

Последним шагом вынесем регистры в отдельный модуль и подадим на вход рассчитанные значения.

RiscVRegs regs(
	.clock(clock),
	.reset(reset),
	
	.enable_write_pc(!stage1_wait),
	.pc_val(pc), //текущий адрес инструкции
	.pc_next(stage0_pc), //сохраняем в регистр адрес следующей инструкции

	.rs1_index(op_rs1), //читаем регистры-аргументы
	.rs2_index(op_rs2),
	.rs1(reg_s1),
	.rs2(reg_s2),
	
	.enable_write_rd(stage2_is_rd_changed && !stage2_empty), //пишем результат обработки операции
	.rd_index(stage2_op_rd),
	.rd(stage2_rd_result)
);

Не забываем, что теперь код собирается на ПЛИС, и надо экономить логику, поэтому убираем обнуление регистров при сбросе.

`ifdef SIMULATION
integer i;
initial begin
	for (i = 1; i < `REG_COUNT; i = i + 1) begin
		regs[i] = 0;
	end
end
`endif

Тестирование

Запускаем icarus и смотрим, как инструкция и результаты разъехались на разные такты.
Вот пример блокировки инструкции из-за того что регистр ещё не обновлён.

     98c:	81c70693          	addi	a3,a4,-2020 # 81c <core_list_init+0x104>
     990:	00c686b3          	add	a3,a3,a2
     994:	01c10613          	addi	a2,sp,28

Совпадение целевого регистра а3 у инструкции по адресу 98c и входного регистра у инструкции по адресу 990 приводит к тому, что блокируется весь конвейер. stage2_rd_fired не даёт двигаться первой стадии, а stage1_wait не даёт двигаться нулевой стадии. Теперь посмотрим на результаты теста производительности.

2K performance run parameters for coremark.
CoreMark Size    : 666
Total ticks      : 4363
Total time (secs): 43
Iterations/Sec   : 0
Iterations       : 1
Compiler version : GCC12.2.0
Compiler flags   : compiler_flags
Memory location  : STACK
seedcrc          : 0xe9f5
[0]crclist       : 0xe714
[0]crcmatrix     : 0x1fd7
[0]crcstate      : 0x8e3a
[0]crcfinal      : 0xe714
Correct operation validated. See README.md for run and reporting rules.
CoreMark end 4654
core_tb.v:131: $finish called at 941319 (1ns)

43 тика, 2.3 CM/МГц в варианте с многотактовым умножением, максимальная частота 45.7 МГц, 2943 LUT на Cyclone IV. Однотактовое умножение справляется за 37 тиков, то есть 2.7 CM/МГц, но за счёт ручной реализации деления получается только 5 МГц.

Total ticks      : 3745
Total time (secs): 37
CoreMark end 4026
core_tb.v:131: $finish called at 814357 (1ns)

Оптимизация

Кто-то скажет, безобразие, коремарки на мегагерцы падают. Давайте посмотрим, что там у регистров. Иногда всё хорошо, никаких пресечений по номерам, а иногда каждая первая команда работает по два такта.

Тут можно заметить, что регистр результата на втором этапе свежий, без длинных цепочек обработки, так что его довольно дёшево можно подсунуть на предыдущий этап, почти не увеличивая время выборки из регистрового файла. Это такой аналог fusion на минималках. А при чём тут fusion, обосновать не сложно. Если поставить несколько ядер в ряд и подключить к одному регистровому файлу, то похожее перебрасывание нового значения регистра на соседнее ядро параллельно с записью в файл позволяет на одном такте исполнить две команды, если второй команде нужен результат первой. Остался только вопрос, зачем ограничиваться случаем, когда целевые регистры совпадают (видимо для экономии шины регистрового файла).
И так, перед самым модулем регистров заведём ещё пару промежуточных проводочков, в которые будем читать значения из регистрового файла, а потом скомбинируем их с результатом вычисления rd. Проверку на совпадение регистра при этом надо убрать.

assign stage1_jam_up = !stage2_empty && (/*stage2_rd_fired ||*/ stage2_memory_fired);

wire [31:0] reg_s1_file;
wire [31:0] reg_s2_file;

assign reg_s1 = (stage2_is_rd_changed && stage2_rs1_equal) ? stage2_rd_result : reg_s1_file;
assign reg_s2 = (stage2_is_rd_changed && stage2_rs2_equal) ? stage2_rd_result : reg_s2_file;

//набор регистров
RiscVRegs regs(
	...
	.rs1(reg_s1_file),
	.rs2(reg_s2_file),
	...
);

После такой маленькой правки наш процессор опять летает. Коремарки восстановлены, можно спать спокойно.

Total ticks      : 3106
Total time (secs): 31
CoreMark end 3342
core_tb.v:131: $finish called at 676031 (1ns)

31 тик, 3.2 CM/МГц для однотактового варианта. Для многотактового частота немного просела до 44.8 МГц, но с тестом всё так же вернулось на место, 37 тиков, 2.7 CM/МГц.

Файлы проекта

Обвязка

core_tb.v
`include "common.vh"
`timescale 1ns / 1ns

`ifdef SIMULATION
	module core_tb;
	`define MEMORY_SIZE (2**16/4)
`else
	module core_tb(clock, reset, out);
	`define MEMORY_SIZE (2**14/4)
`endif

`ifdef SIMULATION
reg clock = 0;
reg reset = 1;
`else
input wire clock;
input wire reset;
`endif
output reg out;
reg [1:0] reset_counter = 2;
reg [31:0] rom [0:`MEMORY_SIZE-1]; //память
wire [31:0] i_addr;
reg [31:0] i_data;
wire [31:0] d_addr;
wire [31:0] d_data_in;
wire [31:0] d_data_out;
wire data_r;
wire data_w;
wire [1:0] d_width; //0-byte, 1-half, 2-word
wire [3:0] byte_mask;

reg [31:0] timer;
reg [31:0] timer_divider;

`ifdef SIMULATION
integer i, fdesc, fres;
initial while(1) #1 clock = !clock;
initial
begin
	$dumpfile("core_tb.vcd");
	$dumpvars();
	for (i = 0; i < `MEMORY_SIZE; i = i + 1)
		rom[i] = 32'b0;
	//$readmemh("code.hex", rom);
	fdesc = $fopen("code.bin", "rb");
	fres = $fread(rom, fdesc, 0, `MEMORY_SIZE);
	$fclose(fdesc);
	#3000000
	$finish();
end

always@(posedge clock) begin
	reset_counter <= reset_counter == 0 ? 0 : reset_counter - 1;
	reset <= reset_counter != 0;
end
`endif

RiscVCore core0
(
	.clock(clock),
	.reset(reset),
	//.irq,
	
	.instruction_address(i_addr),
	.instruction_data(i_data),
	
	.data_address(d_addr),
	.data_width(d_width),
	.data_in(d_data_in),
	.data_out(d_data_out),
	.data_read(data_r),
	.data_write(data_w)
);

//шина инструкций всегда выровнена
always@(posedge clock) begin
	i_data <= rom[i_addr[31:2]];
end

//теперь выравниваем данные
//делаем невыровненный доступ, точнее выровненный по байтам
reg [31:0] old_data;
reg [31:0] old_data_out;
reg [31:0] old_addr;
reg [1:0] old_width;
reg old_data_w;
reg old_data_r;
always@(posedge clock) begin
	old_data <= (data_r || data_w) ? rom[d_addr[31:2]] : 32'hz;
	old_data_out <= d_data_out;
	old_addr <= d_addr;
	old_width <= d_width;
	old_data_w <= data_w;
	old_data_r <= data_r;
end

wire [1:0] addr_tail = old_addr[1:0];

//вешаем на общую шину регистры и память
assign d_data_in = !old_data_r ? 32'hz : old_addr == 32'h40000008 ? timer :
	old_addr < `MEMORY_SIZE * 4 ? (old_data >> (addr_tail * 8)) : 32'hz; //TODO data_read не нужен?

//для чтения данных маска накладывается в ядре, здесь только для записи
assign byte_mask = old_width == 0 ? 4'b0001 << addr_tail :
                   old_width == 1 ? 4'b0011 << addr_tail :
                                  4'b1111;

//раз для побайтового чтения надо делать побайтовый сдвиг
//то для полуслов дешевле не ограничивать выравниванием на два байта
//TODO нужна проверка выхода за границы слова?
wire [31:0] aligned_out = old_data_out << addr_tail * 8;
wire [31:0] new_data = !old_data_w ? 32'hz : {byte_mask[3] ? aligned_out[31:24] : old_data[31:24],
							byte_mask[2] ? aligned_out[23:16] : old_data[23:16],
							byte_mask[1] ? aligned_out[15:8] : old_data[15:8],
							byte_mask[0] ? aligned_out[7:0] : old_data[7:0]};


always@(posedge clock) begin
	if (old_data_w) begin
		rom[old_addr[31:2]] <= new_data;
	end
	if (data_w && d_addr == 32'h4000_0004) begin
		//отладочный вывод
		$write("%c", d_data_out);
	end
	else if (data_r && d_addr == 32'h40000008) begin
		; //таймер читать можно
	end
	else if ((data_r || data_w) && (d_addr < 8'hff || d_addr > `MEMORY_SIZE * 4)) begin
		//нулевые указатели и выход за границы приводит к прекращению работы
		$finish();
	end
	
	if(reset) begin
		timer = 0;
		timer_divider = 0;
	end else begin
		if (timer_divider == 100) begin
			timer <= timer + 1;
			timer_divider <= 0;
		end else begin
			timer_divider <= timer_divider + 1;
		end
	end
	out <= data_w;
end
endmodule

Ядро

core.v
// ядро risc-v процессора
`include "common.vh"

// базовый набор инструкций rv32i
`define opcode_load        7'b00000_11 //I //l**   rd,  rs1,imm     rd = m[rs1 + imm]; load bytes
`define opcode_store       7'b01000_11 //S //s**   rs1, rs2,imm     m[rs1 + imm] = rs2; store bytes
`define opcode_alu         7'b01100_11 //R //***   rd, rs1, rs2     rd = rs1 x rs2; arithmetical
`define opcode_alu_imm     7'b00100_11 //I //***   rd, rs1, imm     rd = rs1 x imm; arithmetical with immediate
`define opcode_load_upper  7'b01101_11 //U //lui   rd, imm          rd = imm << 12; load upper imm
`define opcode_add_upper   7'b00101_11 //U //auipc rd, imm          rd = pc + (imm << 12); add upper imm to PC
`define opcode_branch      7'b11000_11 //B //b**   rs1, rs2, imm    if (rs1 x rs2) pc += imm
`define opcode_jal         7'b11011_11 //J //jal   rd,imm   jump and link, rd = PC+4; PC += imm
`define opcode_jalr        7'b11001_11 //I //jalr  rd,rs1,imm   jump and link reg, rd = PC+4; PC = rs1 + imm

module RiscVCore
(
	input clock,
	input reset,
	input irq,
	
	output [31:0] instruction_address,
	input  [31:0] instruction_data,
	
	output [31:0] data_address,
	output [1:0]  data_width,
	input  [31:0] data_in,
	output [31:0] data_out,
	output        data_read,
	output        data_write
);

//этап 0 ======================================

//на нулевом этапе выдаём адрес инструкции на шину и дальше вместе с инструкцией посылаем на первый этап
wire [31:0] stage0_pc;
assign instruction_address = stage0_pc;

//этап 1 ======================================

//инструкция уже в регистре, обрабатываем
wire stage1_jam_up; //стадия остановлена следующей стадией
reg stage1_empty; //стадия конвейера не получила инструкцию с предыдущего этапа
wire stage1_pause = stage1_empty || stage1_jam_up; //стадии пока нельзя работать
//сохраняем адрес инструкции с предыдущего этапа
wire [31:0] pc; //pc <= stage0_pc

always@(posedge clock or posedge reset)
begin
	if (reset == 1) begin
		stage1_empty <= 1;
	end
	else begin
		stage1_empty <= 0;
	end
end

//получаем из шины инструкцию
wire [31:0] instruction = instruction_data;

//расшифровываем код инструкции
wire[6:0] op_code = instruction[6:0]; //код операции
wire[4:0] op_rd = instruction[11:7]; //выходной регистр
wire[2:0] op_funct3 = instruction[14:12]; //подкод операции
wire[4:0] op_rs1 = instruction[19:15]; //регистр операнд 1
wire[4:0] op_rs2 = instruction[24:20]; //регистр операнд 2
wire[6:0] op_funct7 = instruction[31:25];
wire[31:0] op_immediate_i = {{20{instruction[31]}}, instruction[31:20]}; //встроенные данные инструкции I-типа
wire[31:0] op_immediate_s = {{20{instruction[31]}}, instruction[31:25], instruction[11:7]}; //встроенные данные инструкции S-типа
wire[31:0] op_immediate_u = {instruction[31:12], 12'b0};
wire[31:0] op_immediate_b = {{20{instruction[31]}}, instruction[7], 
                             instruction[30:25], instruction[11:8], 1'b0};
wire[31:0] op_immediate_j = {{12{instruction[31]}}, instruction[19:12], 
                             instruction[20], instruction[30:21], 1'b0};

//выбираем сработавшую инструкцию
wire is_op_load = op_code == `opcode_load;
wire is_op_store = op_code == `opcode_store;
wire is_op_alu = op_code == `opcode_alu;
wire is_op_alu_imm = op_code == `opcode_alu_imm;
wire is_op_load_upper = op_code == `opcode_load_upper;
wire is_op_add_upper = op_code == `opcode_add_upper;
wire is_op_branch = op_code == `opcode_branch;
wire is_op_jal = op_code == `opcode_jal;
wire is_op_jalr = op_code == `opcode_jalr;
wire is_op_multiply = is_op_alu && op_funct7[0];

wire error_opcode = !(is_op_load || is_op_store ||
                    is_op_alu || is_op_alu_imm ||
                    is_op_load_upper || is_op_add_upper ||
                    is_op_branch || is_op_jal || is_op_jalr);

//какой формат у инструкции
wire type_r = is_op_alu;
wire type_i = is_op_alu_imm || is_op_load || is_op_jalr;
wire type_s = is_op_store;
wire type_b = is_op_branch;
wire type_u = is_op_load_upper || is_op_add_upper;
wire type_j = is_op_jal;

//мультиплексируем константы
wire [31:0] immediate = type_i ? op_immediate_i :
				type_s ? op_immediate_s :
				type_b ? op_immediate_b :
				type_j ? op_immediate_j :
				type_u ? op_immediate_u :
				0;

//регистры-аргументы
wire [31:0] reg_s1;
wire [31:0] reg_s2;
wire signed [31:0] reg_s1_signed = reg_s1;
wire signed [31:0] reg_s2_signed = reg_s2;


//чтение памяти (lb, lh, lw, lbu, lhu), I-тип
assign data_read = is_op_load && !stage1_pause;

//запись памяти (sb, sh, sw), S-тип
assign data_write = is_op_store && !stage1_pause;
assign data_out = reg_s2;

//общее для чтения и записи
assign data_address = (is_op_load || is_op_store) ? reg_s1 + immediate : 32'hz;
assign data_width = op_funct3[1:0]; //0-byte, 1-half, 2-word

//обработка арифметических операций
//(add, sub, xor, or, and, sll, srl, sra, slt, sltu)
wire [31:0] rd_alu;
RiscVAlu alu(
				.clock(clock),
				.reset(reset),
				.is_op_alu(is_op_alu),
				.is_op_alu_imm(is_op_alu_imm),
				.op_funct3(op_funct3),
				.op_funct7(op_funct7),
				.reg_s1(reg_s1),
				.reg_s2(reg_s2),
				.imm(immediate),
				.rd_alu(rd_alu)
			);

`ifdef __MULTIPLY__
//(mul, mulh, mulsu, mulu, div, divu, rem, remu)
wire [31:0] rd_mul;
wire is_mul_wait;
RiscVMul mul(
				.clock(clock),
				.reset(reset),
				.enabled(!stage1_pause && is_op_multiply),
				.op_funct3(op_funct3),
				.reg_s1(reg_s1),
				.reg_s2(reg_s2),
				.rd_mul(rd_mul),
				.is_mul_wait(is_mul_wait)
			);
`endif

//обработка upper immediate
wire [31:0] rd_load_upper = immediate; //lui
wire [31:0] rd_add_upper = pc + immediate; //auipc

//обработка ветвлений
wire [31:0] pc_branch = pc + immediate;
wire branch_fired = op_funct3 == 0 && reg_s1 == reg_s2 || //beq
                    op_funct3 == 1 && reg_s1 != reg_s2 || //bne
                    op_funct3 == 4 && reg_s1_signed <  reg_s2_signed || //blt
                    op_funct3 == 5 && reg_s1_signed >= reg_s2_signed || //bge
                    op_funct3 == 6 && reg_s1 <  reg_s2 || //bltu
                    op_funct3 == 7 && reg_s1 >= reg_s2; //bgeu

//короткие и длинные переходы (jal, jalr)
wire [31:0] rd_jal = pc + 4;
wire [31:0] pc_jal = pc + immediate;
wire [31:0] pc_jalr = reg_s1 + immediate;

//теперь комбинируем результат работы логики разных команд
wire [31:0] stage1_rd = /*is_op_load ? rd_load :*/
`ifdef __MULTIPLY__
						is_op_multiply ? rd_mul :
`endif
						is_op_alu || is_op_alu_imm ? rd_alu :
						is_op_load_upper ? rd_load_upper :
						is_op_add_upper ? rd_add_upper :
						is_op_jal || is_op_jalr ? rd_jal
						: 0;

//на текущем такте инструкция ещё не готова
wire stage1_working = 0
`ifdef __MULTIPLY__
							|| is_mul_wait
`endif
							;
//запрещено ли переходить к следующей инструкции
wire stage1_wait = stage1_pause || stage1_working;

assign stage0_pc = stage1_wait ? pc :
						(is_op_branch && branch_fired) ? pc_branch :
						is_op_jal ? pc_jal :
						is_op_jalr ? pc_jalr :
						pc + 4;

//инструкция меняет регистр
wire write_rd_instruction = is_op_load || is_op_alu || is_op_alu_imm 
							|| is_op_load_upper || is_op_add_upper
							|| is_op_jal || is_op_jalr;

//инструкция меняет значение регистра
wire is_rd_changed = (!(stage1_working || op_rd == 0)) && write_rd_instruction;

//этап 2 ======================================
//полученное из памяти значение записываем в регистр
//место изменения регистра только одно, чтобы не возникало лишних задержек
reg [2:0] stage2_funct3;
reg stage2_is_op_load;
reg stage2_is_op_store;
reg [31:0] stage2_addr;
reg [31:0] stage2_rd;
reg[4:0] stage2_op_rd;
reg stage2_is_rd_changed;
reg stage2_empty; //ничего не делаем, потому что предыдущая стадия ничего не передала

always@(posedge clock or posedge reset)
begin
	if (reset == 1) begin
		stage2_funct3 <= 0;
		stage2_is_op_load <= 0;
		stage2_is_op_store <= 0;
		stage2_addr <= 0;
		stage2_rd <= 0;
		stage2_op_rd <= 0;
		stage2_is_rd_changed <= 0;
		stage2_empty <= 0;
	end
	else begin
		stage2_funct3 <= op_funct3;
		stage2_is_op_load <= is_op_load;
		stage2_is_op_store <= is_op_store;
		stage2_addr <= data_address;
		stage2_rd <= stage1_rd;
		stage2_op_rd <= op_rd;
		stage2_is_rd_changed <= is_rd_changed;
		stage2_empty <= stage1_wait;
	end
end

wire load_signed = ~stage2_funct3[2];
wire [31:0] rd_load = stage2_funct3[1:0] == 0 ? {{24{load_signed & data_in[7]}}, data_in[7:0]} : //0-byte
                      stage2_funct3[1:0] == 1 ? {{16{load_signed & data_in[15]}}, data_in[15:0]} : //1-half
                      data_in; //2-word

wire [31:0] stage2_rd_result = stage2_is_op_load ? rd_load : stage2_rd;

//если пишем регистр, просим подождать предыдущие стадии
wire stage2_rs1_equal = (stage2_op_rd == op_rs1) && (type_r || type_i || type_s || type_b);
wire stage2_rs2_equal = (stage2_op_rd == op_rs2) && (type_r || type_s || type_b);
wire stage2_rd_fired = stage2_is_rd_changed && (stage2_rs1_equal || stage2_rs2_equal);
//если сохраняем в память и сразу читаем, тоже ждём
wire stage2_memory_fired = (stage2_is_op_store && (is_op_load || is_op_store) && stage2_addr[31:2] == data_address[31:2]);
//если на прошлом такте блокировали, пропускаем конвейер дальше, так как инструкция уже обработана
assign stage1_jam_up = !stage2_empty && (/*stage2_rd_fired ||*/ stage2_memory_fired);

wire [31:0] reg_s1_file;
wire [31:0] reg_s2_file;

assign reg_s1 = (stage2_is_rd_changed && stage2_rs1_equal) ? stage2_rd_result : reg_s1_file;
assign reg_s2 = (stage2_is_rd_changed && stage2_rs2_equal) ? stage2_rd_result : reg_s2_file;

//набор регистров
RiscVRegs regs(
	.clock(clock),
	.reset(reset),
	
	.enable_write_pc(!stage1_wait),
	.pc_val(pc), //текущий адрес инструкции
	.pc_next(stage0_pc), //сохраняем в регистр адрес следующей инструкции

	.rs1_index(op_rs1), //читаем регистры-аргументы
	.rs2_index(op_rs2),
	.rs1(reg_s1_file),
	.rs2(reg_s2_file),
	
	.enable_write_rd(stage2_is_rd_changed && !stage2_empty), //пишем результат обработки операции
	.rd_index(stage2_op_rd),
	.rd(stage2_rd_result)
);

endmodule

Регистры

registers.v
// регистры процессора
`include "common.vh"

module RiscVRegs
(
	input clock,
	input reset,

	input enable_write_pc,
	input [31:0] pc_next,
	output [31:0] pc_val,

	input [4:0] rs1_index,
	input [4:0] rs2_index,
	output [31:0] rs1,
	output [31:0] rs2,

	input enable_write_rd,
	input [4:0] rd_index,
	input [31:0] rd
);

reg [31:0] regs [1:`REG_COUNT-1]; //x0-x31
reg [31:0] pc;

assign pc_val = pc;
assign rs1 = rs1_index == 0 ? 0 : regs[rs1_index];
assign rs2 = rs2_index == 0 ? 0 : regs[rs2_index];

`ifdef SIMULATION
integer i;
initial begin
	for (i = 1; i < `REG_COUNT; i = i + 1) begin
		regs[i] = 0;
	end
end
`endif

always@(posedge clock or posedge reset)
begin
	if (reset == 1) begin
		pc = 0;
	end
	else begin
		if (enable_write_pc) begin
			pc <= pc_next;
		end
	end
end

always@(posedge clock)
begin
	//regs[enable_write_rd ? 0 : rd_index] <= rd; //так более красиво, но занимает больше места
	if (enable_write_rd) begin
		regs[rd_index] <= rd;
	end
end

endmodule

АЛУ

alu.v
module RiscVAlu
(
	input clock,
	input reset,
	
	input is_op_alu, //операция с двумя регистрами
	input is_op_alu_imm, //операция с регистром и константой
	input [2:0] op_funct3, //код операции
	input [6:0] op_funct7, //код операции
	input [31:0] reg_s1, //первый регистр-операнд
	input [31:0] reg_s2, //второй регистр-операнд
	input [31:0] imm, //константа-операнд
	output [31:0] rd_alu //результат работы
);

//обработка (add, sub, xor, or, and, sll, srl, sra, slt, sltu)
//в случае лёгких инструкций вычисляем результат сразу
wire [31:0] alu_operand2 = is_op_alu_imm ? imm : reg_s2;
assign rd_alu =       op_funct3 == 3'd0 ? (is_op_alu && op_funct7[5] ? reg_s1 - alu_operand2 : reg_s1 + alu_operand2) :
					  op_funct3 == 3'd4 ? reg_s1 ^ alu_operand2 :
					  op_funct3 == 3'd6 ? reg_s1 | alu_operand2 :
					  op_funct3 == 3'd7 ? reg_s1 & alu_operand2 :
					  op_funct3 == 3'd1 ? reg_s1 << alu_operand2[4:0] :
					  op_funct3 == 3'd5 ? (op_funct7[5] ? $signed(reg_s1) >>> alu_operand2[4:0] : reg_s1 >> alu_operand2[4:0]) :
					  op_funct3 == 3'd2 ? $signed(reg_s1) < $signed(alu_operand2) :
					  op_funct3 == 3'd3 ? reg_s1 < alu_operand2 : //TODO для больших imm проверить
					  0; //невозможный результат

endmodule

Модуль умножения

alu_mul.v
`include "common.vh"

module RiscVMul
(
	input clock,
	input reset,
	input enabled,
	
	input [2:0] op_funct3, //код операции
	input [31:0] reg_s1, //первый регистр-операнд
	input [31:0] reg_s2, //второй регистр-операнд
	output [31:0] rd_mul, //результат работы
	output is_mul_wait //надо ли ждать операцию до следующего такта
);

// РАСШИРЕНИЕ M
//обработка (mul, mulh, mulhsu, mulu, div, divu, rem, remu)
//расшифровываем инструкцию
wire is_op_muldiv = enabled;
wire is_op_multiply = !op_funct3[2];
wire is_op_mul_signed = !op_funct3[1]; //mul, mulh
//wire is_op_mul_low = op_funct3[1:0] == 0; //mul
wire is_op_mul_extend_sign = op_funct3[1:0] == 2; //mulhsu //умножаем как беззнаковые, но сам знак надо расширить до 64 бит
wire is_op_div_signed = !op_funct3[0]; //div, rem
//wire is_op_remainder = op_funct3[2]; //rem, remu

//для умножения со знаком достаточно убрать знак у короткого сомножителя
//но для деления надо убрать у обоих, поэтому делаем единообразно
wire need_restore_sign = is_op_multiply ? is_op_mul_signed : is_op_div_signed;
wire start_muldiv_sign = need_restore_sign ? reg_s1[31] ^ reg_s2[31] : 1'b0; // = sign(x) * sign(y)
wire rem_sign_start = need_restore_sign ? reg_s1[31] : 1'b0; // = sign(x)
wire [31:0] start_x = (need_restore_sign && $signed(reg_s1) < 0) ? -reg_s1 : reg_s1;
wire [31:0] start_y = (need_restore_sign && $signed(reg_s2) < 0) ? -reg_s2 : reg_s2;

`ifdef __FAST_MULDIV__

assign is_mul_wait = 0;
wire [31:0] quotient;
wire [31:0] remainder;
wire [31:0] mul_1;
wire [31:0] mul_2;

RiscVFastDiv fd(
		.x(start_x),
		.y(start_y),
		.quotient(quotient),
		.remainder(remainder)
	);

assign {mul_2, mul_1} = start_x * start_y;

// если считать беззнаковым: -a * b = ((1<<32)-a) * b = -a * b + (b << 32)
// для получения mulhsu надо после умножения сделать вычитание беззнакового из старших разрядов
wire [31:0] mulhsu = mul_2 - (reg_s1[31] ? reg_s2 : 0);
wire [63:0] mul_result = start_muldiv_sign ? -{mul_2, mul_1} : {mul_2, mul_1};
wire [31:0] quotient_signed = start_muldiv_sign ? -quotient : quotient;
wire [31:0] remainder_signed = rem_sign_start ? -remainder : remainder;
assign rd_mul =     op_funct3 == 3'd0 ? mul_result[31:0]  : //mul
					op_funct3 == 3'd1 ? mul_result[63:32] : //mulh
					op_funct3 == 3'd2 ? mulhsu            : //mulhsu
					op_funct3 == 3'd3 ? mul_result[63:32] : //mulu
					op_funct3 == 3'd4 ? quotient_signed :   //div
					op_funct3 == 3'd5 ? quotient_signed :   //divu
					op_funct3 == 3'd6 ? remainder_signed :  //rem
					/*op_funct3 == 7 ?*/ remainder_signed;  //remu

`else

//при умножении значения входных переменных могут меняться, поэтому запоминаем их локально
//инструкция запоминается снаружи, её надо сохранять не только для этого блока
//регистры используем для обоих операций
reg [31:0] x, y, r1, r2, r3;
//после начала работы не ориентируемся на входные значения
reg in_progress;
//считаем для беззнаковых, знак меняем в конце
reg muldiv_sign;
//для остатка от деления знак берётся из делимого
reg rem_sign;

//если хоть где-то ноль, результат можно выдать сразу
wire need_wait = is_op_muldiv && reg_s1 && reg_s2;

//инициализируем инструкцию
//перемножение {r3, r2} = {r1 = sign, x} * y
//деление {r3=q, r2=rem} = x / y , r1=msb - счётчик цикла, он же самый значимый бит
wire [31:0] start_msb = start_x[31:24] != 8'b0 ? (1 << 31) : //легковесная подгонка начального счётчика цикла
						start_x[23:16] != 8'b0 ? (1 << 23) :
						start_x[15:8] != 8'b0 ? (1 << 15) :
						(1 << 7);
wire [31:0] start_r1 = !is_op_multiply ? start_msb :
						is_op_mul_extend_sign ? (reg_s1[31] ? -1 : 0) :
						0;

//обрабатываем в цикле
//умножение
wire [31:0] next_mul_y = (y >> 1); //поразрядное умножение
wire [63:0] current_mul_x = {r1, x}; //берём текущий x
wire [63:0] next_mul_x = current_mul_x << 1; //сдвигаем
wire [63:0] next_mul_val = {r3, r2} + (y[0] ? current_mul_x : 0); //и прибавляем к результату
wire mul_end = next_mul_y == 0; //условие окончания умножения

//деление в столбик
//y (делитель) не меняем
//x (делимое) не меняем
wire [31:0] current_msb = r1;
wire [31:0] current_rem = r2;
wire [31:0] current_div_val = r3;
wire [31:0] next_msb = (current_msb >> 1); // выбираем следующий разряд делимого
wire [31:0] next_rem_tmp = {current_rem[30:0], current_msb & x ? 1'b1 : 1'b0}; // приписываем к остатку
//если остаток больше или равен (то есть делится на делитель), то вычитаем
//конкретную цифру подбирать не надо, так как двоичная система
wire [31:0] div_remainder_delta = next_rem_tmp - y;
wire [31:0] next_rem_val = $signed(div_remainder_delta) >= 0 ? div_remainder_delta : next_rem_tmp;
wire [31:0] next_div_val = {current_div_val[30:0], ~div_remainder_delta[31]}; //приписываем разряд к частному
wire div_end = next_msb == 0;

//комбинируем значения для заполнения регистров
//в теории регистры можно переставить, сдвиг и условие выхода кажутся похожими
wire [31:0] next_x = is_op_multiply ? next_mul_x[31:0] : x;
wire [31:0] next_y = is_op_multiply ? next_mul_y : y;
wire [31:0] next_r1 = is_op_multiply ? next_mul_x[63:32] : next_msb;
wire [31:0] next_r2 = is_op_multiply ? next_mul_val[31:0] : next_rem_val;
wire [31:0] next_r3 = is_op_multiply ? next_mul_val[63:32] : next_div_val;
wire divmul_end = is_op_multiply ? mul_end : div_end; //блок умножения закончил вычислять
wire next_in_progress = in_progress && !divmul_end; //когда законил, выключаем регистр активности блока

wire [63:0] mul_result = muldiv_sign ? -next_mul_val : next_mul_val;
wire [31:0] div_result = muldiv_sign ? -next_div_val : next_div_val;
wire [31:0] rem_result = rem_sign ? -next_rem_val : next_rem_val;
assign rd_mul = !in_progress ? 0 : //на нулевом такте всё равно может быть только ноль
					!divmul_end ? 0 : //пока процесс идёт, не качаем затворы
					op_funct3 == 3'd0 ? mul_result[31:0] : //mul
					op_funct3 == 3'd1 ? mul_result[63:32] : //mulh
					op_funct3 == 3'd2 ? mul_result[63:32] : //mulsu
					op_funct3 == 3'd3 ? mul_result[63:32] : //mulu
					op_funct3 == 3'd4 ? div_result : //div
					op_funct3 == 3'd5 ? div_result : //divu
					op_funct3 == 3'd6 ? rem_result : //rem
					/*op_funct3 == 7 ?*/ rem_result;  //remu

//когда нельзя переходить к следующей инструкции
assign is_mul_wait = !in_progress ? need_wait : !divmul_end;

wire divmul_active = need_wait || in_progress;
always@(posedge clock or posedge reset)
begin
	if (reset == 1) begin
		x <= 0;
		y <= 0;
		r1 <= 0;
		r2 <= 0;
		r3 <= 0;
		in_progress <= 0;
	end
	else if (divmul_active) begin
		if (!in_progress) begin // на нулевом такте просто запоминаем значения
			in_progress <= 1'b1;
			x <= start_x;
			y <= start_y;
			r1 <= start_r1; //расширение первого сомножителя знаком до 64 бит или счётчик цикла
			r2 <= 0; //младшие биты произведения или остаток
			r3 <= 0; //старшие биты произведения или частное
			muldiv_sign <= start_muldiv_sign; //надо ли в конце сменить знак
			rem_sign <= rem_sign_start;
		end
		else begin
			in_progress = next_in_progress;
			x <= next_x;
			y <= next_y;
			r1 <= next_r1;
			r2 <= next_r2;
			r3 <= next_r3;
		end
	end
end

`endif //FAST_MULDIV

endmodule

Настройки

common.vh
//доступные расширения

//embedded, 16 базовых регистров
`define __RV32E__

//аппаратное умножение и деление целых чисел
`define __MULTIPLY__
//умножение и деление за один такт
//`define __FAST_MULDIV__

`ifdef    __ICARUS__
	`define  SIMULATION
`endif

`ifdef __RV32E__
    `define REG_COUNT 16 //для embedded число регистров меньше
`else
    `define REG_COUNT 32
`endif

Комментарии (0)