Привет, Хабр! Меня зовут Денис Муратов, я ведущий инженер по разработке систем на кристалле в компании YADRO. Специализируюсь в основном на разработке RTL для ASIC и FPGA.
В наши дни общепризнанный стандарт для RTL-описаний — это язык SystemVerilog, но популярность сейчас набирает его альтернатива, Chisel. Далее я расскажу подробней об этом языке, его преимуществах, недостатках и рисках, связанных с переходом на Chisel со стандартного стека. Отдельно остановлюсь на функциональном программировании — возможности Chisel, которой нет в SystemVerilog, — и на дополнительных возможностях Chisel, улучшающих механизм переиспользования модулей. А также о том, почему код на Chisel менее подвержен ошибкам и всегда работает. Ну, почти всегда.

Проблемы SystemVerilog
Для полноты картины начну с краткого обзора SystemVerilog. Хоть он и считается стандартом в разработке RTL, но если у вас есть опыт работы с современными языками программирования, то к SystemVerilog у вас наверняка имеются претензии. Ведь это, по сути, расширение Verilog, а Verilog уже более 40 лет. И хотя новые стандарты SystemVerilog выходят по сей день, многие проблемы в нем до сих пор не решены. Перечислю основные.
Синтезируемое подмножество ограничено. Если вы создаете на SystemVerilog RTL-описания, то не используете даже 10% его языковых конструкций. В работе с ASIC часто приходится привлекать инструменты и САПР различных производителей, и каждый из них понимает синтезируемое подмножество языка по-своему.
Низкая портируемость кода. Например, вы привыкли работать с Vivado, но потребовалось портировать код на ASIC. И оказывается, что часть ваших конструкций уже не синтезируется, не поддерживается, так что код приходится переписывать.
Нет ООП и других техник современных языков программирования. Точнее, в SystemVerilog ООП в каком-то виде есть, но не все средства синтеза это понимают.
Нет единой точки входа в IP. При разработке ASIC вы пишете не только RTL, но и много других поддерживающих файлов типа SDC, UPF. Постепенно дизайн может стать настолько сложным, что при изменении RTL-параметров придется сразу менять и SDC, и еще кучу других файлов — из-за отсутствия единой точки входа в IP.
Избыточность языковых конструкций. Здесь SystemVerilog далеко до VHDL. Тем не менее, когда код пестрит всякими endmodule, module, begin, end, function endfunction, он теряет в читабельности.
Что такое Chisel
Verilog прежде всего разрабатывался для моделирования аппаратуры. Лишь со временем инженеры осознали, что некоторые описания на Verilog можно синтезировать в логическую схему. В отличие от Verilog, Chisel изначально позиционировали как HDL, Hardware Description Language, то есть язык именно для описания RTL.
В Verilog для описания желаемой схемы может не хватать языковых конструкций, и на вход САПР для синтеза приходится подавать поведенческое описание схемы. Синтезаторы понимают его, как умеют, и синтезируют, но фактически мы имеем дело не со схемой, а с поведением схемы и многое отдаем на откуп САПР. Из описания на Chisel генерируются описания на Verilog, VHDL, SystemVerilog, но в любом случае это не HLS (High Level Synthesis).
DARPA, управление перспективных исследовательских проектов Минобороны США, описывает Chisel как технологию, позволяющую маленьким командам создавать большие цифровые проекты. И я вполне могу с этим согласиться, но есть нюансы.
Chisel — это, по сути, библиотека Scala, а точнее, Domain Specific Language. Языку Scala уже больше 20 лет, он постоянно развивается, сочетает функциональное и императивное программирование. При написании кода на Scala вам доступны все библиотеки Java.
Scala — это масштабируемый язык, который позволяет добавлять свои языковые конструкции. На основе Scala можно создать язык под свои задачи. Так 12 лет назад и поступили инженеры в Беркли: выкинули из Verilog 90%, оставив только нужное, и обернули все это в Scala. Получился Chisel.
Как я уже писал, Chisel используют прежде всего для создания RTL-описаний. Также он позволяет проводить симуляцию несложных модулей. Это удобно для создания юнит-тестов и моделирования работы различных алгоритмов. В плане симуляции не стоит возлагать на Chisel такие же надежды, как на System C или что-то подобное. Симулировать вы сможете лишь очень маленькие схемки, а генерировать — хоть целые кластеры из тысяч процессоров, вообще все, что захотите.
На основе Chisel/Scala можно написать свой HLS-инструмент (High Level Synthesis), где одним росчерком пера вы будете создавать очень большие схемы, что с использованием одного Verilog невозможно.
Наконец, в отличие от дорогостоящих САПР на рынке, Chisel — это open source-технология, распространяемая по лицензии Apache 2.0. При этом Chisel выделяется из open source-конкурентов тем, что на его основе есть целый ряд коммерческих решений. Например, чипы на основе процессорных ядер SiFive и Andes. И технология развивается быстрыми темпами: в год выходит по несколько новых версий.
Пример проекта на Chisel
Рассмотрим пример простой цифровой системы с процессором, системной шиной и памятью, которые нужно соединить вместе, чтобы все заработало и можно было оценить площадь и производительность. Описание на SystemVerilog получится весьма объемным: объявить модули, задать их параметры, разобраться, как и что соединить, провести тесты.

Теперь посмотрим, как это будет выглядеть на Chisel/Scala:
(f << c(cp).* << b(bp).* << m(mp).*).!
Мало что понятно, но здесь есть процессор, шина и память, которым переданы параметры. После нажатия на кнопку генерации все сгенерируется и код можно будет передать на вход САПР для требуемых оценок.
Конечно, писать именно так не стоит, если только вы не хотите зашифровать свою систему. Я немного переписал программу и показал, что в ней происходит:
(fabric <<
CpuGen(cpuParam).getAll <<
SysBusGen(sysBusParam).getAll <<
SysMemGen(sysMemParam).getAll
).generate

Во второй строке объявлен генератор процессора, которому мы передаем параметры. getAll — это вызов метода, который возвращает все генераторы внутри нашего процессора. Их может быть сколь угодно много внутри одного IP ядра, но нам о них знать не нужно. А нужно просто их все подтянуть, используя этот вызов. В третьей и четвертой строке то же самое сделано для системной шины и памяти. В первой строке передаем это на фабрику, а в пятой вызываем метод фабрики generate, чтобы получить все необходимое: RTL, SDC, RDL, UPF, технологическую память в нужных конфигурациях и остальное.
Подобный фреймворк уже реализовали в Berkeley под именем ChipYard. Описание, что я привел, может быть не совсем точным, но концепция такая же. Если вы разбираетесь в этом фреймворке, то минут за 20 вполне сможете сгенерировать довольно сложную систему.
Кто-то может сказать, что это можно написать на C++ или Python. Да, можно. Но в этом случае вам придется изобретать очень много велосипедов, которые Chisel поставляет в готовом виде. Разработку на Chisel можно представить так: вы пишете на Verilog и можете в любой момент прямо в коде Verilog вызвать функцию C++. Или наоборот: писать программу на C++ и где-нибудь посередине метода С++ добавить кусочек на Verilog. Очень удобно.
Различия описаний Chisel и SystemVerilog
Если смотреть на простые вещи, то в принципе отличий немного. Вот объявление одной простой шины шириной WIDTH на SystemVerilog:
logic [WIDTH-1:0] data;
А вот на Chisel:
val data = Wire(UInt(width.W))
Описание на Chisel кажется более громоздким, но с описанием на SystemVerilog проще допустить ошибки в коде. Например, почти всегда в коде пишут –1, но иногда не пишут. Это по ошибке? Или автор так и хотел? Не всегда очевидно. В Chisel все тоже записывается в одну строчку, здесь мы без дополнительных арифметических операций создаем Wire с типом unsigned integer и шириной width.
Scala — строго типизированный язык, и это дополнительно ограждает нас от ошибок. На Verilog вполне можно соединить что-нибудь несовместимое, но на Chisel из-за таких сложных UInt(width.W) ошибиться очень сложно. Например, сигнал типа SInt (signed integer) или Clock нельзя просто взять и подключить к нашему проводу.
Посмотрим теперь, как создать и подключить две шины. В SystemVerilog требуется три строчки:
logic [WIDTH-1:0] data;
logic [(WIDTH+1)-1:0] data_inc;
assign data_inc = data + 1’b1;
В Chisel меньше, две:
val data = Wire(UInt(Width.W))
val data_inc = WireInit(data +& 1.U)
Во второй строке на SystemVerilog мы создаем еще одну шину, в третьей подсоединяем к ней результат суммы data с единицей. Шина data_inc будет шире на один бит, что учтено во второй строке: +1 и –1 компенсируют друг друга.
На Chisel мы создаем вторую шину, и, что интересно, нам не нужно объявлять ее тип — это будет тип результата арифметической операции внутри WireInit(). Амперсанд после плюса означает, что нужно расширить результат на один бит.
Я привел для примера очень простой код. Но если у вас очень сложный дизайн, datapass-логика и при этом в SystemVerilog вы явно задаете типы каждого провода и регистра, то изменение какого-нибудь арифметического оператора в середине может привести к изменению ширины других проводов и регистров. Придется спешно править оставшиеся провода вручную.
В Chisel можно заменить операцию в середине, и через WireInit инициализация типа автоматически пройдет по всему коду до выхода модуля. На маленьких дизайнах это незаметно, но на сотне тысяч гейтов вы оцените разницу. Подобный рефакторинг кода на SystemVerilog обеспечит вам выходные на работе, а на Chisel все будет исправлено за несколько минут.
А вот так на Chisel могут выглядеть вычисления с матрицами:
res := (a * b) + с
Когда рядом добавлено очень много всевозможной арифметики, это описание выглядит очень наглядно и читабельно. Причем, если вы захотите, в сгенерированном RTL эта логика будет выражена в виде отдельных модулей, что упростит вам анализ STA-отчетов.
Сравним теперь описание D-триггера с решением на SystemVerilog:
logic my_ff;
always_ff @(negedge rst_n or posedge clk)
if (!rst_n)
my_ff <= 1’b0;
else
my_ff <= my_ff_next;
И в Chisel:
val my_ff = RegNext(my_ff_next, false.B)
Наверняка каждый, кто много писал на Verilog, задумывался, зачем он пишет этот negedge, posedge, тянет clock по всей иерархии и допускает из-за этого ошибки. Инженеры в Berkeley тоже подумали об этом и сократили описание триггера до объявления RegNext. В этом примере на входе D-триггера — сигнал my_ff_next, а false.B означает, что после сброса на его выходе будет значение 0.
А вот так будет выглядеть наше описание, если мы захотим поставить регистр на выходе умножителя матриц выше:
val res = RegNext(a * b)
SystemVerilog — это язык моделирования аппаратуры, предлагающий гибкое описание аппаратных элементов. Например, здесь можно описать логику работы RS-триггера. Но для разработчиков цифровых систем это не нужно. Помимо краткости записи, преимущество Chisel в том, что в дизайне не будет никаких «интересных» элементов. В записи на SystemVerilog можно допустить кучу ошибок: сделать latch, неожиданные мультиплексоры на входе, подать не тот сигнал на reset. В итоге все может прекрасно симулироваться, но когда вы дойдете до synthesis и будете изучать отчеты static timing analysis, рано или поздно всплывет ошибка. Еще хуже, когда в симуляции ваше устройство будет работать так, а после синтеза описания — иначе. Chisel вас полностью от этого оградит, ведь здесь вы занимаетесь исключительно цифровым дизайном.
Еще одна особенность Chisel: в нем нет точек с запятой. Если вы долго пишете на Chisel, а потом переходите на SystemVerilog, это может стать большой проблемой.
А где же clock?
Все-таки в дизайне нужно где-то определить clock и назначить его триггерам. Предположим, у нас есть модуль:

Объявим его на SystemVerilog:
module my_module #(
parameter WIDTH = 8
) (
input clock,
input reset,
input io_data_i,
output [WIDTH-1:0] io_data_o
);
...
endmodule
И на Chisel:
class my_module (dataWidth: Int = 8) extends Module {
val io = IO(new Bundle {
val data_i = Input(Bool())
val data_o = Output(UInt(dataWidth.W))
})
...
}
Каждый модуль, каждая шина и проводок Chisel — это класс. Объявляя сигналы и провода, мы используем существующие классы Chisel, а в случае с модулем создаем собственный класс my_module (первая строка) и наследуем его из класса Module в Chisel. Clock и reset мы автоматически наследуем из базового класса, поэтому дополнительно объявлять их не требуется.
Более того, если ваш модуль составной и в нем есть другие блоки, унаследованные от Module, то clock и reset к ним подтянутся автоматически. Перетаскивать их каждый раз не потребуется. Это очень удобно, это уменьшает число глупых ошибок в коде, ускоряя процесс разработки.
В строках 3–5 выше я объявляю интерфейс этого блока: data_i как input, data_o как output. Если нужны свои clock и reset, это тоже решаемо: их можно объявить в этом интерфейсе. Потом обрамить нужные регистры и подмодули конструкцией withClockAndReset, и Chisel сам подтянет к ним ваши clock и reset. В простейшем же случае ваш модуль будет выглядеть так, будто clock и reset у него вообще нигде нет. Это существенно уменьшает количество ошибок в коде, но будет заметно только на крупных дизайнах.
Мы рассмотрели важные различия в описании RTL на SystemVerilog и Chisel. Перейдем к тому, чего в SystemVerilog нет вообще.
Функциональное программирование в Chisel
С математической точки зрения, вы пишете в функциональном стиле, если в вашей программе функции последовательно вызывают друг друга: аргументами одной функции является результат другой, аргументы которой — результат третьей и т. п. Как правило, в такой программе нет изменяемых переменных, циклов и каких-то состояний, что снижает вероятность очень нехороших ошибок.
Программисты определяют функциональное программирование чуть иначе: функция написана в функциональном стиле, если она не имеет никаких побочных эффектов. Побочные эффекты — это изменение чего-либо вне этой функции. В функциональном программировании функция должна работать только со своими аргументами и возвращать результат. Ее код не должен менять что-либо во внешнем мире.
Поясню на примере. Представим, что ваша задача — пнуть мяч и попасть в ворота. Если программа написана функционально, то вы пинаете мяч, он летит, потребляет некоторое количество ресурсов, и результата у такой программы возможно два: попал в ворота или не попал. Если программа использует императивное программирование — как в C++, C, SystemVerilog — то вы пинаете мячик, а сзади вас может рухнуть дом. Потому что программа зависит от внешнего контекста и может его менять.
Нам, разработчикам аппаратного обеспечения, ближе математическая трактовка функционального программирования: это парадигма, в которой процесс вычисления трактуется как вычисление значений функций в математическом понимании последних. То есть каждую логическую схему в дизайне мы можем выразить в виде функции с сигналами на входе и выходе.
Допустим, у нас есть функция с четырьмя аргументами:

Scala позволяет это красиво описать и на выходе возвращает кортеж:
val (y0, y1) = func(x0, x1, x2, x3)
Кортеж — это набор данных, причем они могут быть любого типа: y0, допустим, проводок, а y1 — вообще какой-нибудь сложный интерфейс. Простота записи очень удобна.
Для примера рассмотрим поиск минимального значения у вектора данных, x0, x1, x2, x3.

Вот как это будет выглядеть на SystemVerilog в самом простом варианте:
assign y0 = (x0 < x1) ? x0 : x1;
assign y1 = (x2 < x3) ? x2 : x3;
assign y = (y0 < y1) ? y0 : y1;
Описание функциональное, но если нам нужно переменное число входных сигналов, оно не будет работать. Каждый раз придется руками добавлять новые assign в код.
Попытаемся параметризовать наш код на SystemVerilog:
always_comb begin
logic [WIDTH-1:0] tmp;
int i;
tmp = 2 ** WIDTH-1;
for (i = 0; i < LEN; i = i + 1) begin
if (tmp > x[i]) begin
tmp = x[i];
end
end
y = tmp;
end
Этот код не зависит от количества входных сигналов: их может быть хоть несколько сотен. Но это поведенческое описание, хотя оно и успешно синтезируется. Лет 20–30 назад оно бы не синтезировалось, но со временем синтезаторы научились определять это в коде и сейчас легко выдают неплохую схему на выходе.
Строго говоря, это описание выполнено в императивном стиле и в нем целый ряд проблем: временные переменные, арифметические операции, которые не связаны с задачей, ну и переменная tmp, которая непонятно что собой представляет и непонятно во что синтезируется. Ей много раз присваиваются какие-то значения. Все это — места типичных ошибок в RTL-описаниях.
Посмотрим, как это будет выглядеть на Chisel:
val y = x.reduce((n, k) => Mux(n < k, n, k))
В отличие от SystemVerilog, у нас всего одна довольно простая строчка кода. Но что здесь происходит? На входе у нас есть некий вектор x. Для этого вектора целиком мы вызываем reduce, стандартный метод Scala. Он представляет собой функцию высшего порядка, которая принимает на вход другую функцию. Мы не хотим объявлять ее отдельно, придумывать ей имя, да и вообще не стоит создавать миллион функций, которые мы используем в коде лишь один раз.
Для таких сценариев Scala предлагает так называемые анонимные функции — мы их объявляем и сразу же используем. В нашем случае у функции два аргумента, n и k. Тело функции — это мультиплексор Chisel: он вернет n, если n меньше k, а иначе вернет k. Через вызов нашей функции для всех элементов вектора x метод reduce преобразует этот вектор в скалярное значение, которое передается переменной y. Запись простая, и перепутать здесь можно только n и k местами, в отличие от описания на SystemVerilog, где поле для ошибок очень широкое.
Помимо очевидных плюшек, преимущество кода на Chisel и в том, что он не зависит от типа данных. Эта функция может быть использована где угодно. На входе может быть не просто вектор unassigned шин, а какие-нибудь сложные интерфейсы. Имплементация не зависит от того, какой тип данных мы передаем, что очень удобно для больших дизайнов.
Такие блоки работают на высоком уровне абстракции, и мы работаем здесь как архитекторы, не беспокоясь о том, что происходит ниже. Другие пользователи нашей имплементации могут просто передать с ней другой тип и получить иную логику. Достигается это за счет полиморфизма в ООП: оператор «<» может иметь разную имплементацию, и вызываться будет та, которая относится к типу переменной x. И эта имплементация может быть определена в совершенно ином месте.
Но это же не дерево!
Reduce вызывает функцию для всех элементов массива. Рассмотрим подробнее. Reduce вначале берет первые два элемента массива и вызывает для них функцию. Потом берет третий элемент и вызывает эту функцию с результатом. Для четвертого — опять функция с результатом и так далее. Мы получаем ту же самую логику, что и на SystemVerilog. Но и в Chisel, и в SystemVerilog у нас описано не дерево.
Современные инструменты синтеза это понимают и способны построить хорошую схему и для Chisel, и для System Verilog. Если же вы принципиально хотите сделать дерево, то вот оно:
val y = x.reduceTree((n, k) =>
Mux(n < k, n, k)
)
Мы заменили reduce на reduceTree, и наша анонимная функция стала, по сути, узлом дерева. Можно заглянуть в имплементацию reduceTree — это рекурсивная функция из 20 строк, которая синтезируется в Chisel. То есть в Chisel можно писать код рекурсивно и не беспокоиться, что какая-то САПР вас не поймет.
Усложняем схему
Теперь допустим, что после каждого минимума нам нужно поставить регистр и запустить логику на 5 ГГц.

В Chisel для решения понадобится четыре строки:
val y = x.reduceTree((x, y) => {
val reg = RegNext(Mux(x < y, x, y))
reg
})
По сути, у нас изменилась только анонимная функция внутри reduceTree. Во второй строчке мы добавили регистр, у него на входе значение мультиплексора, которое выдает минимум. Каждая функция в Scala должна возвращать значение, и в третьей строке мы возвращаем reg. Узел нашего дерева лишь немного усложнился. Аналогичное описание на SystemVerilog будет гораздо больше. Тратить время на его изобретение не вижу смысла. Зачем, если я за полминуты могу описать эту схему на Chisel и она сразу заработает?
Посмотрим на примеры результатов генерации кода. Получаем своеобразный netlist:
module my_min(
input clock,
input [15:0] io_x_0,
io_x_1,
io_x_2,
...
output [15:0] io_y
);
reg [15:0] io_y_reg;
reg [15:0] io_y_reg_1;
reg [15:0] io_y_reg_2;
reg [15:0] io_y_reg_3;
reg [15:0] io_y_reg_4;
reg [15:0] io_y_reg_5;
reg [15:0] io_y_reg_6;
always @(posedge clock) begin
io_y_reg <= io_x_0 < io_x_1 ? io_x_0 : io_x_1;
io_y_reg_1 <= io_x_2 < io_x_3 ? io_x_2 : io_x_3;
io_y_reg_2 <= io_x_4 < io_x_5 ? io_x_4 : io_x_5;
io_y_reg_3 <= io_x_6 < io_x_7 ? io_x_6 : io_x_7;
io_y_reg_4 <= io_y_reg < io_y_reg_1 ? io_y_reg : io_y_reg_1;
io_y_reg_5 <= io_y_reg_2 < io_y_reg_3 ? io_y_reg_2 : io_y_reg_3;
io_y_reg_6 <= io_y_reg_4 < io_y_reg_5 ? io_y_reg_4 : io_y_reg_5;
end
assign io_y = io_y_reg_6;
endmodule
Имена получились читаемыми, код — легко понятным, описание — довольно простым. Это большое преимущество Chisel. Такое описание можно портировать в любую САПР, и она его обработает без всяких предупреждений и тем более ошибок. Все проблемы в коде сведутся к модулям проекта, написанным на SystemVerilog.
Но здесь же кроется и самый большой недостаток Chisel: сгенерированный код получается очень большим. Проект выше совсем маленький, буквально игрушечный, в реальных проектах Chisel за несколько десятков секунд генерирует сотни тысяч строк кода. Если делать мощную процессорную систему, строки можно считать миллионами. К этому придется привыкнуть. Зато итоговый RTL будет очень простым: без interface, for, generate и т. п.
Повторное использование модулей
Представим, что нам нужно найти какой-либо модуль в общей библиотеке, чтобы переиспользовать его и не изобретать велосипед. Какие возможности переиспользования дает SystemVerilog?
Параметры модулей — у каждого модуля есть параметры, которые мы можем задать в своем проекте при использовании этого модуля. Есть глобальные макросы — ими лучше не пользоваться для параметризации модулей, но они тоже бывают удобны, например, для отделения FPGA-кода от ASIC-кода или передачи параметров из компилятора.
Часто возникает проблема: «нужен такой же модуль, как в библиотеке, но другой». Путей решения здесь много. Допустим, нам нужно переписать 10% логики работы модуля.
Что мы делаем в SystemVerilog? Берем нужный модуль из библиотеки, но боимся что-то менять. Ведь модуль уже используется в других проектах, и после изменений обязательно прибегут и скажут, что мы все испортили. У меня был случай, когда в модуле поменяли всего лишь комментарий и все равно нашлись недовольные. Чтобы такого не произошло, мы копируем модуль себе в проект, меняем его, делаем tape-out, и все должно отлично работать.
Но представим, что в модуле из библиотеки был баг, который исправили, но только после того, как мы этот модуль себе скопировали. Исправление мы не заметили, ведь таких модулей у нас много и других дел тоже. В итоге в чипе оказался модуль с багом.
Исправить ситуацию можно через изменения в общем коде. Но представьте: что-то добавили вы, что-то добавил ваш коллега из соседнего отдела, а что-то еще какой-нибудь студент-стажер. В итоге может получиться динозавр, которым вообще непонятно как пользоваться.
Чем в этом плане лучше Chisel, а точнее, Scala? Здесь есть наследование.
Каждый модуль в Chisel — это класс. Допустим, в библиотеке есть класс A. Мы наследуем из класса A свой класс B, вносим в класс B свои правки и делаем tape-out. Если в классе A поправили баг, это исправление автоматически подтянется к нам. Другие могут сделать на основе класса A модуль C или модуль D — и никто из нас не будет мешать друг другу, а исходный класс А останется «чистым» от правок соседей.

.
Когда на SystemVerilog мы хотим написать универсальный библиотечный модуль, то задаемся вопросом: как сделать его таким, чтобы он решал все возможные задачи? Мы добавляем кучу сигналов, лишнего функционала, который надо долго тестировать. В Chisel вопрос другой: как написать модуль, чтобы он легко расширялся и любой мог его добавить к себе, унаследовать и использовать? Первый вопрос в принципе не имеет ответа, а второй вполне решаем.
Итоги сравнения
Какие преимущества есть у Chisel по сравнению с SystemVerilog?
Простое описание сложных вещей. Код на Chisel где-то в 5–10 раз меньше (а бывает и в 100 раз меньше), чем на SystemVerilog. Это не касается конечных автоматов — здесь вам Chisel вряд ли поможет. Но если вы создаете сложные алгоритмы, коммутацию, процессорные элементы, работаете с n-мерными массивами, сложными структурами данных, Chisel раскроется в полной мере.
Код менее подвержен случайным ошибкам. Это достигается благодаря строгой типизации Scala, уникальным подходам к описанию RTL, а также автоматическому заданию типов. Вообще, Chisel будет постоянно вас подталкивать к тому, чтобы вы делали меньше ошибок в коде. Бывает, что вы просто несколько часов исправляете ошибки, указанные компилятором Scala, и потом в симуляции сразу все работает.
Очень простой рефакторинг. Иногда нужно поменять в коде что-нибудь солидное, много модулей, затрагивающих иерархию проекта. На SystemVerilog это очень долго. В Chisel вы меняете что-то в одном месте, запускаете компилятор и просто правите, что компилятор вам говорит. А потом все работает.
Высокая портируемость кода. Можно не заморачиваться, будете ли вы работать на ASIC или на FPGA, какие инструменты использовать. Ваш код очень прост, любая современная САПР гарантированно его поймет. Плюс в Chisel вы можете легко играть со сгенерированным RTL, крутить код, как захочется, менять имена, иерархии модулей. Возможностей очень много.
Перейду к недостаткам Chisel.
Размер сгенерированного кода. Часто достигает сотен тысяч строк. С этим можно бороться, и со временем вы освоите нужные техники.
Это тяжело и непонятно. В определенный момент вам придется пересмотреть свой подход к написанию RTL. Поначалу вы точно будете часами сидеть с Chisel над тем, что смогли бы за пять минут написать на SystemVerilog. Но если вы пересилите себя и не вернетесь в привычное русло, со временем снова переходить на SystemVerilog уже не захочется. Кривая обучения на Chisel очень крутая, и, возможно, в том числе по этой причине технология не так распространена в индустрии, как SystemVerilog.
Статья написана по мотивам выступления Дениса Муратова на конференции «FPGA-Systems x YADRO 2024.02: Scala, RISC-V, open source и производительность». Посмотреть запись этого и других выступлений вы сможете в блоге YADRO.