
Я всегда хотел собрать свой процессор. Не просто написать эмулятор или покопаться в чужих репозиториях, а пройти путь «от нуля»: описать 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 (умножение/деление).
Сделать кэш и поддержку памяти.
А теперь вопрос к вам: а вы пробовали когда-нибудь написать процессор с нуля?
Комментарии (28)

HepoH
26.09.2025 17:10Возможно вам будет интересно посмотреть на лабораторный практикум из МИЭТ, где как раз последовательно и с нуля пишется процессор RV32IZicsr. Он сопровождается очень подробными методическими материалами где как раз рассказывается вроде нюансов про использование младших битов операнда B при сдвигах. Кроме того, тестбенчи там даются уже готовые, потому что написать тестбенч для модулей — это тоже целая наука и у вас они довольно простые, можно не все отловить.

checkpoint
26.09.2025 17:10Совершенно не интересно смотреть за тем, как кто-то уже всё сделал и решил за тебя все проблемы. Автора всецело поддерживаю в его велосипедостроении. Считаю, что всякий программист обязан за свою карьеру хоть раз выделить время и реализовать какой-то вычислитель, пусть даже самый простой. После этого на природу сморишь совсем другими глазами. Глазами Создателя чтоли. :-)

Krenodator
26.09.2025 17:10Классный путь от ALU до мигающего диода. Это тот самый щелчок когда RTL оживает

nv13
26.09.2025 17:10Мне вот интересно.. такой чип стоит 12 тысяч рублей или такого порядка. Имеет ли смысл в него встраивать хоть что то микропроцессорное, если даже существенно более производительные процессоры и контроллеры стоят намного дешевле?)

checkpoint
26.09.2025 17:10В целом, это начинает приобретать какой-то смысл если Ваш синтезируемый вычислитель умеет делать что-то специфическое, например быстро перемножать матрицы, вычислять Фурье или имеет какие-то редкие интерфейсы. Автор же взял в руки первую попавшуюся плату и начал эксперименты.
PS: Существуют ПЛИС и платы с ними за цену в менее 1500 руб (Tangnano-9K), на которых можно сделать всё то же самое, и даже в большем обьеме.

nv13
26.09.2025 17:10Именно. Но встраивание микропроцессоров жрёт очень много недешёвых ресурсов и не упрощает никак конечную разработку, поэтому мне всегда были непонятны разного рода nios-ы при наличии аналогичных по характеристикам dsp и контроллеров по цене на порядок или около того ниже.

sintech
26.09.2025 17:10Как же не упрощает?
Для процессора вы пишите код на языке высокого уровня в привычной среде, реализовать тоже самое на verilog в RTL будет намного сложнее и дольше.

nv13
26.09.2025 17:10А почему бы рядом с fpga не поставить обычный процессор на котором всё легко? Вот для sdr процессор непосредственно нужен? Не нужен. Почему же в который раз за историю его снова пытаются запихать в плис со всеми очевидными последствиями? Чтобы интерфейс не придумывать? Так он займёт чуть меньше, чем ничего по сравнению с таким же процессором в fpga.
Мне правда непонятен текущий подход к архитектуре подобных устройств. 20 лет назад тоже говорили что fpga заменит всё, однако рынок всё расставил по местам - fpga это дорого, долго и трудно поддерживаемо в массовых устройствах. А сейчас, такое ощущение, что что то в этих обстоятельствах изменилось

sintech
26.09.2025 17:10fpga это дорого, долго и трудно поддерживаемо в массовых устройствах
С этим тезисом никто и не спорит, но для небольших партий (относительно) или устройств специального назначения вполне логичной выглядит использование одной микросхемы fpga с софт-процессором.
Для любителей привычного подхода есть целый класс гибридных микросхем, где ядра cortex соседствуют с программируемой матрицей на одном кристалле. Но в современных, жирных fpga проще использовать софт risc-v например.

nv13
26.09.2025 17:10А чем проще? Раньше можно было получить разрешение на stratex или любой xilinx, а вот на тмс или ад уже нет. Это из этого разряда простота?))
Но этот путь он того, чреватый достаточно - слишком много политики и практически нет техники..

nixtonixto
26.09.2025 17:10А почему бы рядом с fpga не поставить обычный процессор на котором всё легко
Ну вот понадобится вам производить 20 умножений ОДНОвременно - вы поставите 20 обычных процессоров? Или реализовать сложную логику, собирая сигналы с сотни ног и в реалтайме на частоте 50 МГц обрабатывать их. ПЛИС - вершина цифрового мира, когда процессора становится недостаточно - выход только один: ПЛИС и по результатам заказывать АСИК или пустить в серию ПЛИС, если тираж небольшой.

nv13
26.09.2025 17:10И как в такой задаче поможет программируемый процессор внутри плис?) Там же будет несколько умножителей и автомат, переключающий входы выходы - типичный препроцессор не требующий ничего похожего на программируемое устройство

nixtonixto
26.09.2025 17:10Внутри ПЛИС можно разместить процессор, у которого будет 20 аппаратных умножителей.

nv13
26.09.2025 17:10И он будет программируемый, с системой команд, как описано в данной статье? Ему же тогда компилятор надо разработать, отладчик какой никакой.. и непонятно, зачем он нужен для 20 умножений на 50 МГц

nixtonixto
26.09.2025 17:10и непонятно, зачем он нужен для 20 умножений на 50 МГц
Простейший пример - видеокарта, внутри которой процессор с тысячами вычислительных ядер. Можно ведь графику и на одноядерном процессоре обсчитывать, просто подняв его рабочую частоту в тысячи раз? Вот что-то такое и вынуждает создавать кастомные процессоры на ПЛИС.

nv13
26.09.2025 17:10Да я собственно об этом и говорил - препроцессоры - да, как программируемые слегка, так и нет, типа фильтров и прочего, а вот процессоры общего назначения, в которых утилизация не очень близка к 1 и тратятся не дешёвые транзисторы, а дорогие ячейки - непонятно

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это не по коду можно отловить. код то может и его, но реплики в тексте....человек так не разговаривает и не пишет

laviol
26.09.2025 17:10Вполне обычная задержка, которую используют в тестбенчах: объявляешь timeunit и используешь. Конструкция не синтезируема, но и используется только при моделировании. Довольно удобно, если есть какой-нибудь clocking block, смену фронта клока в котором, кстати, тоже можно задать через #, а потом сам клок можно использовать уже через двойную решетку ##.

PriFak
26.09.2025 17:10в качестве проекта под FPGA в унике я тоже сделал процессор. только 16 битный, основанный и собранный на процессоре с игры nandgame. и пока сама имплементация логических гейтов шла неплохо, я очень много времени тратил на тестбенчи и отлов неопределенных состояний - потому что оказывается, если твой логический вентиль не имеет неопределенного состяния, то это вовсе не значит что тот же АЛУ собраный из этих элементов будет работать безукорызненно. благо в самой игре были аналоги тестбенчей и я знал конкретно чего ожидать.
В итоге на не совсем полностью моем процессоре удалось и диодом поморгать и числа фибоначчи посчитать
а саму игру nandgame вообще всем могу посоветовать, залипнуть можно надолго

here-we-go-again
26.09.2025 17:10Есть игра Turing Complete, советую тем кому интересно повторить путь автора без железа для начала. Там тоже шаг за шагом соберешь компьютер. 32 регистра кстати оверкил, так-то можно и всего парочку иметь в простом процессоре + арифметика + логика + условный переход и это уже можно программировать.
Там не дает при чем сразу готовые компоненты, а начинаешь с буквально отдельных гейтов.Мой компьютер из игры

По центру блок регистров, справа-вверху арифметический блок, справа-внизу логика и условные переходы, слева стек
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
checkpoint
Картинка точно сгенерирована нейросетью.
DrMefistO
Прям позорище.