
Я всегда хотел собрать свой процессор. Не просто написать эмулятор или покопаться в чужих репозиториях, а пройти путь «от нуля»: описать RTL, прогнать через симуляцию, а потом оживить всё это на FPGA. В этой статье расскажу, как я к этому подошёл, какие инструменты использовал и на какие грабли наступил. Будет и Verilog-код, и опыт работы с симуляторами, и пара советов тем, кто захочет повторить эксперимент.
Честно говоря, идея «собрать свой процессор» долго казалась мне чем-то академическим. Мол, есть же готовые ядра: Rocket, BOOM, PicoRV32… Зачем плодить сущности? Но однажды я поймал себя на мысли: я могу запустить свой код на куске кремния, который я сам описал строчка за строчкой. Разве это не круто?
И вот я открыл текстовый редактор, выбрал Verilog, и начал писать. Да, граблей было предостаточно, да, дебаг занимал больше времени, чем разработка, но зато в конце на FPGA-плате мигнул светодиод, управляемый моим процессором. И ради этого стоило.
Архитектура RISC-V: минимум, чтобы стартовать
RISC-V — открытая архитектура, и именно это делает её идеальной для экспериментов. Я взял за основу RV32I — минимальный набор инструкций для 32-битного ядра. Без умных расширений, без FPU, без компрессий инструкций.
Что это значит на практике:
32 регистра общего назначения по 32 бита.
Простые инструкции загрузки/выгрузки.
Арифметика и логика.
Управление переходами.
Вот честный вопрос к вам, читатель: разве для первого ядра нужно больше? Всё остальное можно добавить потом, если останется азарт.
Я начал с ALU (арифметико-логического устройства). Это сердце процессора, и без него дальше двигаться бессмысленно. Код вышел довольно компактным, но в нём я сознательно избегал слишком умных конструкций — лучше пусть будет длиннее, но понятнее.
module alu (
input wire [31:0] a,
input wire [31:0] b,
input wire [3:0] op,
output reg [31:0] result
);
always @(*) begin
case (op)
4'b0000: result = a + b; // ADD
4'b0001: result = a - b; // SUB
4'b0010: result = a & b; // AND
4'b0011: result = a | b; // OR
4'b0100: result = a ^ b; // XOR
4'b0101: result = a << b; // SLL
4'b0110: result = a >> b; // SRL
default: result = 32'hDEADBEEF;
endcase
end
endmodule
Когда впервые прогнал этот блок через симуляцию, я понял: уже неплохо. Но радость была недолгой — сразу выяснилось, что без нормальной проверки всё рассыпается.
Симуляция: первые грабли
Для симуляции я использовал Icarus Verilog и GTKWave. Кто работал — тот знает: это простые, но мощные инструменты.
Я написал тестбенч для ALU:
module alu_tb;
reg [31:0] a, b;
reg [3:0] op;
wire [31:0] result;
alu uut (
.a(a), .b(b), .op(op), .result(result)
);
initial begin
$dumpfile("alu_tb.vcd");
$dumpvars(0, alu_tb);
a = 10; b = 5; op = 4'b0000; #10; // ADD
a = 10; b = 5; op = 4'b0001; #10; // SUB
a = 10; b = 5; op = 4'b0010; #10; // AND
$finish;
end
endmodule
Запустил — и получил первые красивые графики. Но потом заметил странность: сдвиги работали неправильно. Оказалось, что b
использовался целиком, а нужно было брать только младшие биты. Вот он, классический случай, когда «на бумаге гладко, а на практике всё иначе».
Регистровый файл и декодер инструкций
Следующим шагом стал регистровый файл. Тут тоже всё выглядит просто: 32 регистра, чтение/запись. Но если не добавить проверку на x0
(регистр, который всегда равен нулю), можно получить очень неприятные баги.
module regfile (
input wire clk,
input wire we,
input wire [4:0] ra1, ra2, wa,
input wire [31:0] wd,
output wire [31:0] rd1, rd2
);
reg [31:0] regs [0:31];
assign rd1 = (ra1 == 0) ? 0 : regs[ra1];
assign rd2 = (ra2 == 0) ? 0 : regs[ra2];
always @(posedge clk) begin
if (we && wa != 0)
regs[wa] <= wd;
end
endmodule
Когда я впервые это реализовал, то поймал себя на мысли: кажется, я наконец-то начинаю понимать, как всё устроено «под капотом».
Пайплайн или нет?
Здесь у меня был внутренний спор: стоит ли сразу заморачиваться с конвейером (pipeline)? С одной стороны, это интереснее и ближе к реальным CPU. С другой — отладка усложнится в разы.
Я решил начать без пайплайна, с простого пошагового исполнения. И знаете, не пожалел: багов и так хватало, а если бы я добавил ещё и forwarding, hazard-детекцию и прочее — я бы, наверное, бросил это дело на середине.
Синтез под FPGA
Когда RTL наконец-то более-менее заработал в симуляции, пришло время синтеза. Я использовал плату на базе Xilinx Artix-7 и Vivado.
Честно: первый синтез — это как первый запуск программы «Hello World». Только тут вместо текста на экране — мигающий светодиод. Я загрузил прошивку, написал простейший код на ассемблере RISC-V:
addi x1, x0, 42
sw x1, 0(x0)
И, о чудо, на отладочном пине действительно появилось число 42
. Вот в такие моменты забываешь про бессонные ночи.
Выводы и что дальше
Собрать свой RISC-V процессор оказалось реально. Да, путь непростой. Да, отладка выжирает кучу нервов. Но ощущение, что у тебя в руках крутится кусочек железа, написанный тобой, стоит того.
Что можно улучшить:
Добавить конвейеризацию.
Реализовать прерывания.
Попробовать расширение M (умножение/деление).
Сделать кэш и поддержку памяти.
А теперь вопрос к вам: а вы пробовали когда-нибудь написать процессор с нуля?
Комментарии (7)
HepoH
26.09.2025 17:10Возможно вам будет интересно посмотреть на лабораторный практикум из МИЭТ, где как раз последовательно и с нуля пишется процессор RV32IZicsr. Он сопровождается очень подробными методическими материалами где как раз рассказывается вроде нюансов про использование младших битов операнда B при сдвигах. Кроме того, тестбенчи там даются уже готовые, потому что написать тестбенч для модулей — это тоже целая наука и у вас они довольно простые, можно не все отловить.
Krenodator
26.09.2025 17:10Классный путь от ALU до мигающего диода. Это тот самый щелчок когда RTL оживает
nv13
26.09.2025 17:10Мне вот интересно.. такой чип стоит 12 тысяч рублей или такого порядка. Имеет ли смысл в него встраивать хоть что то микропроцессорное, если даже существенно более производительные процессоры и контроллеры стоят намного дешевле?)
kmatveev
26.09.2025 17:10a = 10; b = 5; op = 4'b0000; #10;
что означает последнее #10 в этой строке?
Есть подозрение, что статья не настоящая, а нейросетевая.
HepoH
26.09.2025 17:10Задержку в 10 отсчётов моделирования. Без нее сигналы на входе алу менялись бы мгновенно и на временной диаграмме было бы видно только результат последнего тестового вектора.
PriFak
26.09.2025 17:10в качестве проекта под FPGA в унике я тоже сделал процессор. только 16 битный, основанный и собранный на процессоре с игры nandgame. и пока сама имплементация логических гейтов шла неплохо, я очень много времени тратил на тестбенчи и отлов неопределенных состояний - потому что оказывается, если твой логический вентиль не имеет неопределенного состяния, то это вовсе не значит что тот же АЛУ собраный из этих элементов будет работать безукорызненно. благо в самой игре были аналоги тестбенчей и я знал конкретно чего ожидать.
В итоге на не совсем полностью моем процессоре удалось и диодом поморгать и числа фибоначчи посчитать
а саму игру nandgame вообще всем могу посоветовать, залипнуть можно надолго
ParaParadox
Странные чувства от Вашей публикации...
Лучшее, что я видел по Risc-V на FPGA для начинающих, это
https://github.com/BrunoLevy/learn-fpga/tree/master/FemtoRV
или очень подробно
https://github.com/BrunoLevy/learn-fpga/blob/master/FemtoRV/TUTORIALS/FROM_BLINKER_TO_RISCV/README.md