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

  • Сделать кэш и поддержку памяти.

А теперь вопрос к вам: а вы пробовали когда-нибудь написать процессор с нуля?

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


  1. ParaParadox
    26.09.2025 17:10

    Странные чувства от Вашей публикации...

    Лучшее, что я видел по 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


    1. checkpoint
      26.09.2025 17:10

      Картинка точно сгенерирована нейросетью.


  1. HepoH
    26.09.2025 17:10

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


    1. checkpoint
      26.09.2025 17:10

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


  1. Krenodator
    26.09.2025 17:10

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


  1. nv13
    26.09.2025 17:10

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


    1. checkpoint
      26.09.2025 17:10

      В целом, это начинает приобретать какой-то смысл если Ваш синтезируемый вычислитель умеет делать что-то специфическое, например быстро перемножать матрицы, вычислять Фурье или имеет какие-то редкие интерфейсы. Автор же взял в руки первую попавшуюся плату и начал эксперименты.

      PS: Существуют ПЛИС и платы с ними за цену в менее 1500 руб (Tangnano-9K), на которых можно сделать всё то же самое, и даже в большем обьеме.


  1. kmatveev
    26.09.2025 17:10

    a = 10; b = 5; op = 4'b0000; #10;

    что означает последнее #10 в этой строке?

    Есть подозрение, что статья не настоящая, а нейросетевая.


    1. HepoH
      26.09.2025 17:10

      Задержку в 10 отсчётов моделирования. Без нее сигналы на входе алу менялись бы мгновенно и на временной диаграмме было бы видно только результат последнего тестового вектора.


    1. PriFak
      26.09.2025 17:10

      это не по коду можно отловить. код то может и его, но реплики в тексте....человек так не разговаривает и не пишет


  1. PriFak
    26.09.2025 17:10

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


  1. checkpoint
    26.09.2025 17:10

    А где всё остальное ? Где декодер команд ? Где шина и арбитраж ? :-)