Предыстория


Захотелось мне как-то перевести управление станком на ПЛИС, а для этого понадобилось ядро процессора. Поиск на opencores не особо помог, x86 лицензированный, ARM лицензированный, NIOS… ну, вы поняли. Если выдумывать свою систему команд, может получиться криво, и главное, где брать компилятор. В общем, всё было сложно, так что на время идею пришлось отложить.
С недавних пор ситуация изменилась, архитектура RISC-V пошла в массы. Открытых ядер в достатке, китайцы клепают микроконтроллеры, и ESP, к примеру, переходит на RISC-V, русские тоже двигаются в этом направлении. С большой долей вероятности x86 и ARM подвинутся с рынка, потому что они дорогие и устаревшие.
Так вот, вернёмся к DIY. Автор ядра DarkRiscV похвастался, что собрал ядро за один день. У меня за плечами опыта разработки микроэлектроники не было, поэтому когда ядро так же удалось собрать за один день, осталось только сказать: "Лол, и это работает".


Документация


Для сборки понадобится компилятор gcc risc-v, симулятор icarus verilog и блокнот. Обкладываемся спецификациями, основная называется RISC-V Instruction Set Manual, но в ней коды команд оказались закопаны довольно глубоко, поэтому ушло много времени на поиск короткого списка команд с опкодами. Этого документа практически достаточно для сборки процессора. Первая таблица в документе рассказывает, в каком месте 32-битной инструкции лежат:


  • код команды — opcode
  • номер регистра результата — rd
  • номера регистров аргументов — rs1 и rs2
  • уточнение кода команды в func3 и funct7
  • а также данные, встроенные в инструкцию — imm

Команды собраны в 6 групп (R, I, S, B, U, J), выбор структуры команды зависит от этой группы.



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


Код


Сначала промаркируем проводки, которые торчат из процессора.


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, //размер данных (8, 16, 32 бита)
    input  [31:0] data_in, //прочитанные данные из памяти
    output [31:0] data_out, //записываемые в память данные
    output        data_read, //должна ли память что-то нам прочитать
    output        data_write //или что-то себе записать
);

И так, у нас есть некая 32-битная инструкция, и нам её надо расшифровать.


//получаем из шины инструкцию
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};

Обычным программистам может показаться, что предварительные рассчёты — это лишняя нагрузка и надо бы засунуть под if, но не забываем, что это железо, и если какая-то логика должна быть, то место она займёт в любом случае.


С форматом команд разобрались, теперь надо разобраться с их списком. RV32I описывает базовый набор инструкций, его и будем реализовывать. Состоит он из следующих команд: чтение памяти в регистр (l), запись регистра в память (s), арифметическая операция с двумя регистрами, арифметическая операция с регистром и встроенными данными, загрузка большого числа из встроенных данных (lui), загрузка адреса для относительного перехода (auipc), условный переход (b), короткий переход (jal), дальний переход (jalr). Чтобы построить процессор, почти не обязательно знать подробностей, для чего и как используются эти инструкции, главное, чтобы выполнение соответствовало столбцу description в спецификации. В наборе есть ещё пара команд для операционной системы, но для тестовых целей можно обойтись без них.


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

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


//выбираем сработавшую инструкцию
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 [31:0] reg_d = regs[op_rd];
wire [31:0] reg_s1 = regs[op_rs1];
wire [31:0] reg_s2 = regs[op_rs2];
wire signed [31:0] reg_s1_signed = reg_s1;
wire signed [31:0] reg_s2_signed = reg_s2;

Теперь выполняем конкретные команды в зависимости от опкода.


load


Чтение памяти работает для разных типов данных, конкретный тип определяется в funct3. Снаружи, в шине данных надо предусмотреть побайтовый сдвиг, а здесь мы только расширяем данные до 32-х бит. Кстати говоря, на возню с шиной тоже ушло довольно много времени.


//чтение памяти (lb, lh, lw, lbu, lhu), I-тип
assign data_read = is_op_load;
wire load_signed = ~op_funct3[2];
wire [31:0] load_data = op_funct3[1:0] == 0 ? {{24{load_signed & data_in[7]}}, data_in[7:0]} : //0-byte
                        op_funct3[1:0] == 1 ? {{16{load_signed & data_in[15]}}, data_in[15:0]} : //1-half
                        data_in; //2-word

store


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


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

//общее для чтения и записи
wire [11:0] address_imm = data_read ? op_immediate_i : data_write ? op_immediate_s : 0;
assign data_address = (is_op_load || is_op_store) ? reg_s1 + address_imm : 0;
assign data_width = (is_op_load || is_op_store) ? op_funct3[1:0] : 'b11; //0-byte, 1-half, 2-word

alu


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


//обработка арифметических операций (add, sub, xor, or, and, sll, srl, sra, slt, sltu)
wire [31:0] alu_operand2 = is_op_alu ? reg_s2 : is_op_alu_imm ? op_immediate_i : 0;
wire [31:0] alu_result = op_funct3 == 0 ? (is_op_alu && op_funct7[5] ? reg_s1 - alu_operand2 : reg_s1 + alu_operand2) :
                         op_funct3 == 4 ? reg_s1 ^ alu_operand2 :
                         op_funct3 == 6 ? reg_s1 | alu_operand2 :
                         op_funct3 == 7 ? reg_s1 & alu_operand2 :
                         op_funct3 == 1 ? reg_s1 << alu_operand2[4:0] :
                         op_funct3 == 5 ? (op_funct7[5] ? reg_s1_signed >>> alu_operand2[4:0] : reg_s1 >> alu_operand2[4:0]) :
                         op_funct3 == 2 ? reg_s1_signed < $signed(alu_operand2) :
                         op_funct3 == 3 ? reg_s1 < alu_operand2 :
                         0; //невозможный результат

lui и auipc


Простенькая обработка загрузки констант lui и auipc.


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

branch


Такая же простенькая обработка условных переходов. Тут возникает важный пункт: кроме rd в качестве выходного регистра может быть и pc.


//обработка ветвлений
wire [31:0] branch_pc = pc + op_immediate_b;
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


Ещё две команды переходов, меняющие pc — безусловные переходы: на абсолютный адрес и переход относительно текущего pc.


//короткие и длинные переходы (jal, jalr)
wire [31:0] jal_result = pc + 4;
wire [31:0] jal_pc = pc + op_immediate_j;
wire [31:0] jalr_pc = reg_s1 + op_immediate_i; //здесь действительно I-тип

Запись в rd и pc


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


//теперь комбинируем результат работы логики разных команд
integer i;
always@(posedge clock or posedge reset)
begin
    if (reset == 1) begin
        for (i = 0; i < `REG_COUNT; i=i+1) regs[i] = 0; //Это стоит делать только для тестов, потому что сильно отъедает LE
        pc = 0;
    end else begin
        regs[op_rd] <= op_rd == 0 ? 0 : //x0 = 0
                       is_op_load ? load_data :
                       is_op_alu || is_op_alu_imm ? alu_result :
                       is_op_load_upper ? load_upper_result :
                       is_op_add_upper ? add_upper_result :
                       is_op_jal || is_op_jalr ? jal_result :
                       regs[op_rd];

        pc <= (is_op_branch && branch_fired) ? branch_pc :
              is_op_jal ? jal_pc :
              is_op_jalr ? jalr_pc :
              pc + 4;
    end
end

Наверное, pc при сбросе должен указывать на что-то другое, но для тестов и так сойдёт. Суммарно код процессора занял примерно 150 строк.
Можно заметить, что конструкции pc + op_immediate и reg_s1 + op_immediate попадаются по 3 раза, а в конце комбинируются в rd и pc, поэтому нет особого смысла держать immediate данные разных инструкций раздельно, и вместо этого скомбинировать их в одну переменную.


//какой формат у инструкции
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;

Итоговый результат под спойлером.


core.v
// ядро risc-v процессора

// базовый набор инструкций 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

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

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
);

//базовый набор регистров
reg [31:0] regs [0:`REG_COUNT-1]; //x0-x31
reg [31:0] pc;

//достаём из pc адрес инструкции и посылаем в шину
assign instruction_address = pc;

//получаем из шины инструкцию
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 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_d = regs[op_rd];
wire [31:0] reg_s1 = regs[op_rs1];
wire [31:0] reg_s2 = regs[op_rs2];
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;
wire load_signed = ~op_funct3[2];
wire [31:0] rd_load = op_funct3[1:0] == 0 ? {{24{load_signed & data_in[7]}}, data_in[7:0]} : //0-byte
                      op_funct3[1:0] == 1 ? {{16{load_signed & data_in[15]}}, data_in[15:0]} : //1-half
                      data_in; //2-word

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

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

//обработка арифметических операций (add, sub, xor, or, and, sll, srl, sra, slt, sltu)
wire [31:0] alu_operand2 = is_op_alu ? reg_s2 : is_op_alu_imm ? immediate : 0;
wire [31:0] rd_alu = op_funct3 == 0 ? (is_op_alu && op_funct7[5] ? reg_s1 - alu_operand2 : reg_s1 + alu_operand2) :
                     op_funct3 == 4 ? reg_s1 ^ alu_operand2 :
                     op_funct3 == 6 ? reg_s1 | alu_operand2 :
                     op_funct3 == 7 ? reg_s1 & alu_operand2 :
                     op_funct3 == 1 ? reg_s1 << alu_operand2[4:0] :
                     op_funct3 == 5 ? (op_funct7[5] ? reg_s1_signed >>> alu_operand2[4:0] : reg_s1 >> alu_operand2[4:0]) :
                     op_funct3 == 2 ? reg_s1_signed < $signed(alu_operand2) :
                     op_funct3 == 3 ? reg_s1 < alu_operand2 : //TODO для больших imm проверить
                     0; //невозможный результат

//обработка 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;

//теперь комбинируем результат работы логики разных команд
integer i;
always@(posedge clock or posedge reset)
begin
    if (reset == 1) begin
        for (i = 0; i < `REG_COUNT; i=i+1) regs[i] = 0;
        pc = 0;
    end else begin
        regs[op_rd] <= op_rd == 0 ? 0 : //x0 = 0
                       is_op_load ? rd_load :
                       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 :
                       regs[op_rd];

        pc <= (is_op_branch && branch_fired) ? pc_branch :
              is_op_jal ? pc_jal :
              is_op_jalr ? pc_jalr :
              pc + 4;
    end
end
endmodule

Следующий вопрос, как запустить ядро на симуляторе. Для этого надо завести ещё один verilog файл и добавить туда окружение: тактирование, загруженную программу, экземпляр процессора, шину данных, а ещё регистр для выдачи данных в консоль. Но для одной статьи этого будет много, так что оставлю только результат запуска CoreMark.


image


Таймер делит тактовую частоту на 100, тест делит ещё на 100. Один проход теста в симуляторе занимает примерно полторы минуты, поэтому так мало проходов. Рейтинг считается как число итераций/число клоков * мегагерц. 1 итерация / (7737 ticks * 100 клоков) * 1М даёт 1.29 CM, на уровне слабых ядер микроконтроллеров. Однако добавление модуля умножения повышает его примерно до 2.7, а это уже похоже на относительно мощные микроконтроллеры. Можно здесь посравнивать.


Ядро работает в симуляторе, gtk wave показывает значения регистров и прочего, но для синтеза на ПЛИС надо делать работу с синхронной памятью, и чтобы не терять время на ожиданиях, приделывать конвейер (у меня заняло неделю, может будет ещё статья). Производительность падает до 2.3, хотя можно немного подкрутить.

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


  1. JerryI
    15.05.2025 13:51

    Диаграмм не хватает, куда что идет. Можно набросать в excalidraw скажем, тогда проще читаются verilog модули... Либо я привык к кряктусу ;)


    1. MegaHard Автор
      15.05.2025 13:51

      Может быть, сам то я в C++ привык писать. Пробовал в Quartus смотреть графический вид, там регистры взорвались в кучу мультиплексоров на несколько экранов, пришлось выносить их в отдельный модуль просто чтобы было видно остальные элементы. Но вообще картинка для привлечения внимания не совсем КДПВ, тут же логика максимально простая, прочитали, сложили по всякому, записали. lui так вообще проводки со входного регистра в выходной перекидывает.


    1. byman
      15.05.2025 13:51

      Как я понял, диаграмма здесь простая - один период клока. В этом периоде выдается адрес, читается из памяти команда, декодируется, исполняется и в конце такта защелкивается результат и новый адрес команды :) Уникальный процессор - в нем даже нет регистра инструкции.


      1. byman
        15.05.2025 13:51

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


        1. MegaHard Автор
          15.05.2025 13:51

          //здесь читаем с заданного адреса с выравниванием
          wire [31:0] old_data = rom[d_addr[31:2]];
          wire [1:0] addr_tail = d_addr[1:0];
          
          //здесь посылаем прочитанное в процессор
          assign d_data_in = d_addr == 32'h40000008 ? timer :
          	d_addr < `MEMORY_SIZE * 4 ? (old_data >> (addr_tail * 8)) : 0;
          
          //здесь выравниваем
          wire [31:0] aligned_out = d_data_out << addr_tail * 8;
          //и обнуляем лишнее
          assign byte_mask = d_width == 0 ? 4'b0001 << addr_tail :
                             d_width == 1 ? 4'b0011 << addr_tail :
                                            4'b1111;
          //здесь записываем
          always@(posedge clock) begin
          	if (data_w) begin
          		rom[d_addr[31:2]] <= {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]};
          	end
          end

          Но это уже для следующей статьи.


  1. byman
    15.05.2025 13:51

    Автор ядра DarkRiscV похвастался, что собрал ядро за один день. У меня за плечами опыта разработки микроэлектроники не было, поэтому когда ядро так же удалось собрать за один день, осталось только сказать: "Лол, и это работает".

    По легенде, он сделал это за одну ночь. Но если Ваш день был короче чем его ночь, то Вы круче.


    1. MegaHard Автор
      15.05.2025 13:51

      В принципе на его лавры не претендую, просто хотел сказать, что не такая это страшная вещь - процессор ) .


  1. fromzerotoinfinity
    15.05.2025 13:51

    Автор - мАлАдец. Я тоже ядра писал далеко давно. Сначала такое, потом мультитактовое. Бросил (со всей силы в стену)))) на конвеере с его ловушками, подменой регистров и сливом бачка ))

    Супер.

    Единственно, моя ложечка каки в бочку - 2 АЛУ - это не хорошо. Переходите на мультитакт. Там одного достаточно, хотя для современных ФПГА, наверное количество АЛУ не так уж и важно, тем более что одно их просто для + счетчика инструкций в переходах.... там jal/r, beq и пр.


    1. MegaHard Автор
      15.05.2025 13:51

      Если под вторым АЛУ имеются в виду вычисления в branch_fired, то там жалкое вычитание и сравнение, остальное вроде флагами на конце сумматора разруливается. А на конвейер я уже перешёл, вот там действительно пришлось следить, какие данные какому этапу соответствуют. Теперь флаги блокировки конвейера есть, регистры в модуль вынесены, осталось шину многоканальную с кэшем сделать и можно out-of-order выполнение инструкций прикручивать. Оно вроде не сложно выглядит, если предыдущие ядра регистр не пишут, значит можно его читать.


  1. byman
    15.05.2025 13:51

    даёт 1.29 CM

    Проверил. Цифры аналогичные :)


    1. MegaHard Автор
      15.05.2025 13:51

      Нужны подробности. Речь про моё ядро или какое-то другое?