В рамках одного из проектов в группе косимуляции YADRO мы разрабатываем набор аппаратных блоков, которые нужно верифицировать. Один из этапов здесь — FPGA-тестирование: блок заливают на плату FPGA и пытаются понять, можно ли в принципе воплотить чип в жизнь и как он тогда будет себя вести.

При тестировании больших и специфических компонентов бывает, что обвязка из стандартных блоков для FPGA-тестирования не подходит. В подобных случаях вместо них мы создавали блоки обвязки с помощью Chisel, но со временем убедились: хоть Chisel отлично подходит для разработки, для тестирования его может оказаться недостаточно. И недостатки эти восполняет наш фреймворк аппаратного тестирования на основе Verilator, о котором я подробно расскажу в этой статье.

Чего нам не хватало в Chisel

Как и Verilog, язык Chisel предназначен для описания аппаратуры, но при этом он более доступен и поддерживает структурную реконфигурацию. Можно не просто заменять цифры во время компиляции, а, например, задать некоторое количество шин определенной ширины. Тогда Chisel сам подстроится и сгенерирует нужный RTL. Такие нагромождения кода, как в Verilog, здесь просто не нужны. Блоки, разработанные на Chisel, легче переиспользовать и изменять — это для нас важно. Аналоги, которые это поддерживают — например, SpinalHDL и Migen — еще пока сыроваты.

Продемонстрирую преимущества Chisel с помощью примера параметризации:

import chisel3._
import chisel3.util._

// Параметры блока задаются обычным Scala-классом.
case class StreamConfig(
  dataWidth: Int = 32,
  depth: Int = 8
)

class StreamProcessor(cfg: StreamConfig) extends Module {
  val io = IO(new Bundle {
    val in  = Flipped(Decoupled(UInt(cfg.dataWidth.W)))
    val out = Decoupled(UInt(cfg.dataWidth.W))
  })

  // При изменении depth Chisel сгенерирует FIFO другой глубины. Условно меняется структура.
  val fifo = Module(
    new Queue(UInt(cfg.dataWidth.W), cfg.depth)
  )

  fifo.io.enq <> io.in

  // Пример «полезного» преобразования. Здесь изменение неструктурное, такое поддерживается в Verilog макросами.
  io.out.bits  := fifo.io.deq.bits + 1.U
  io.out.valid := fifo.io.deq.valid
  fifo.io.deq.ready := io.out.ready
}

У Chisel есть фреймворк для тестирования, Chiseltest. Но высокоуровневость, обеспечивающая преимущества Chisel в принципе, в тестах имеет ограничения. Мы не способны опуститься на уровень Verilog, то есть понаблюдать сигналы на проводах и повзаимодействовать с ними. Локальные тесты Chiseltest могут вернуться с зелеными галочками. Но когда мы пытаемся использовать сгенерированный Verilog код — например, прицепляем тестовые блоки к FPGA и запускаемся на битстриме, — по какой-нибудь причине вполне может ничего и не заработать. В таком случае приходится часами читать сгенерированный код от Chisel, чтобы понять, как и в ходе какой оптимизации всплывает баг.

Вторая причина: время на каждый сбор прошивки для FPGA-тестов. Для нашего большого блока это примерно восемь часов. Прибавьте к этому время на ручную вычитку кода Chisel — и это выливается в месяцы задержек. Иногда Chiseltest и вправду удобен: проверить функционал верхних уровней, посмотреть регистры. Но мы работаем ниже и дергаем проводки.

Третья причина: Chisel работает на sbt, компиляторе Scala, и поэтому плохо дружит с другими программами. Код на C++ можно встроить куда угодно, а со Scala все сложнее: если нет подходящей библиотеки, то извините. Наша команда пишет в основном на C++, и бо́льшая часть компонентов, с которыми мы работаем, не имеет нативной интеграции с Chisel.

Методы аппаратного тестирования

C недостатками Chisel разобрались. Для поиска нового решения нужно оценить фронт работ. В разработке аппаратуры мы предусматриваем много тестов на разных уровнях:

  • Проверка Scala-логики и параметров: допустимые комбинации настроек, вычисление ширин, построение структуры и обработка ошибочных конфигураций.

  • Проверка elaboration: генерируется ли модуль, нет ли конфликтов подключений, корректно ли формируется RTL для разных наборов параметров.

  • Функциональная проверка RTL: совпадает ли последовательность состояний и выходов с ожидаемым поведением при тактировании, reset, задержках и backpressure.

  • Протокольная проверка: соблюдаются ли handshake, порядок транзакций, ограничения интерфейса и поведение при неполной готовности сторон.

  • Интеграционная проверка: работает ли блок с другими RTL-модулями, программными моделями, драйверами и системными сценариями.

  • Проверка на прототипе или FPGA: подтверждает поведение в более реалистичной среде, но обычно дороже по времени диагностики и автоматизации.

С помощью Chisel можно реализовать только часть этих тестов: на уровне логики, elaboration и функционала (через специальный фреймворк). В остальном на помощь Chisel приходят RTL-симуляторы, формальные способы верификации через математические модели и FPGA-прототипирование.

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

Подход

Для чего лучше всего подходит

Главное преимущество

Главное ограничение

Scala / ChiselTest

Быстрые модульные проверки и TDD

Минимальный порог входа

Связь с моделью и API Chisel

RTL-симулятор

Точная проверка HDL-семантики

Богатая событийная модель

Стоимость лицензий и инфраструктуры

Verilator и C++

Быстрые регрессии и интеграция

Скорость и нативный C++

Не заменяет все виды событийной симуляции

Formal

Свойства и крайние состояния

Доказательство в заданных пределах

Требует формализации свойств

FPGA / прототип

Системные сценарии

Реалистичное окружение

Дорогая диагностика и подготовка

Подробно проанализировав то, что я вкратце свел в этом разделе, мы пришли к следующей схеме. Chiseltest используем для быстрых локальных и elaboration-проверок, Verilator-фреймворк — для RTL-регрессии, протоколов, интеграции и при множестве параметров, а для formal и sign-off задач — специализированные инструменты.

Почему выбрали Verilator

Опыт работы с Verilator у нас уже был. На хакатоне SoC Design Challenge 2026 мы с его помощью проверяли решения участников, и это нам очень понравилось.

Verilator принимает Verilog/SystemVerilog и генерирует модель на C++, которую можно собрать как библиотеку или часть тестового исполняемого файла. Это дает сразу три больших преимущества.

  • Тестовый код становится обычным C++ со всеми вытекающими преимуществами. Доступны GoogleTest, GDB, санитайзеры, профилировщики, стандартные библиотеки, генераторы данных и другие инструменты. И поставить Verilator на CI совсем несложно.

  • Не требуется строить отдельный тестбенч SystemVerilog только ради связи с C++ или вручную проектировать DPI-слой для каждого сценария.

  • Очень высокая скорость по сравнению с Chisel. Для сравнения я поставил на CI функциональные тесты на Verilator и Chisel. 1500 тестов Verilator отработали за три минуты, 15 тестов Chisel — за семь.

Цикловая модель хорошо подходит для больших регрессий синхронных цифровых блоков и позволяет амортизировать компиляцию на множестве тестов. В один исполняемый файл можно включить DUT, дополнительные RTL-модули, эталонную модель, систему проверки результатов, BFM-агенты. Последнее для нас особенно важно, поскольку мы работаем с разными протоколами, а BFM-агенты предоставляют тестам удобный API. Можно обойтись без тысяч строк кода для ручного переключения проводов.

При всех этих преимуществах важно обозначить границы: Verilator не является универсальной заменой всем событийным симуляторам (особенно для тестов), специфическим задержкам или сложным временным конструкциям. Но нам было достаточно и того, что он умеет.

Недостатки Verilator тоже заслуживают упоминания. До версии 5.0 он не поддерживал управление временем из C++. А пятая версия довольное свежая, ей где-то год, и ее до сих пор нет в пакетных менеджерах. Для управления временем приходилось писать отдельный модуль управления на Verilog.

Также у Verilator слабая поддержка довольно специфических, «сложных» фичей SystemVerilog — assertions, generation, property и подобных. Да и в принципе количество открытых issue в официальном репозитории Verilator намекает, что при разработке сколь-нибудь сложного проекта рано или поздно наткнешься здесь на баг или недоработанную фичу.

Наш тестовый фреймворк на Verilator

Для нас преимущества Verilator в сравнении с Chiseltest перевесили недостатки. Так что на основе Verilator мы решили сделать тестовый фреймворк, включающий также Python-скрипты и make-файлы. Согласовали список требований к фреймворку:

  • Единый транзакционный API для отправки и приема данных независимо от конкретного набора сигналов DUT.

  • Поддержка разных входных и выходных интерфейсов — например, AXI-Stream, AXI-Lite, AXI — и внутренних корпоративных протоколов.

  • Управление несколькими clock/reset, детерминированное планирование тактов и возможность воспроизводить редкие временные комбинации.

  • Повторное использование BFM-агентов между блоками и сценариями без копирования pin-level логики.

  • Автоматическая генерация обвязки и тестовых матриц из параметров Chisel и метаданных интерфейсов.

  • Подключение математических эталонных моделей, существующих приложений и стороннего RTL.

  • Диагностика, пригодная для CI: seed, журнал транзакций, дамп волн по ошибке, понятное сообщение о первом расхождении.

  • Разделение времени компиляции и выполнения, кеширование сборки и быстрый повторный запуск выбранного теста.

Реализацию я начинал с мини-тестов, где нужно было пропускать маленькие кусочки данных через модуль. Сейчас в автоматически сгенерированный код на C++ в Verilog у нас встраиваются сгенерированные тесты на Chisel и C++. Затем все это собирается и запускается в модели на C++, которая воспроизводит наш основной блок.

Архитектура фреймворка

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

  1. Этап генерации. Chisel-конфигурация и параметры преобразуются в RTL и машиночитаемое описание интерфейсов.

  2. Этап Verilator. RTL DUT и при необходимости дополнительные RTL-блоки превращаются в C++-модель или библиотеку.

  3. Слой обвязки DUT. Унифицирует доступ к портам, clock/reset и служебным сигналам, скрывает имена полей, что странно генерирует Chisel.

  4. Планировщик симуляции. Определяет порядок изменения входов, вызов eval, фронтов такта, обработки агентов и сбора наблюдений.

  5. BFM-слой. Драйверы, мониторы и респондеры переводят транзакции в сигналы и обратно.

  6. Слой сценария. Задает намерение теста — набор транзакций, ограничения, ошибки, reset, проверка перформанса блока и последовательность фаз.

  7. Scoreboard и референсная модель. Формируют ожидаемый результат и сопоставляют его с наблюдаемыми транзакциями.

  8. GoogleTest. Организует наборы тестов, фикстуры, параметризованные запуски, отчетность и интеграцию с CI.

Вот класс Chisel, который генерирует RTL:

import chisel3.stage.ChiselStage

object GenerateStreamProcessor extends App {
  val cfg = StreamConfig(
    dataWidth = 32,
    depth = 16
  )

  // На этом этапе Chisel обрабатывает схему
  // и генерирует SystemVerilog, который дальше
  // станет входом для Verilator.
  ChiselStage.emitSystemVerilog(
    new StreamProcessor(cfg),
    Array("--target-dir", "generated/rtl")
  )
}

А это пример Hello world в Verilator тестбенча на C++:

#include "VStreamProcessor.h"

int main() {
    // VStreamProcessor — C++-класс,
    // автоматически созданный Verilator из нашего RTL.
    VStreamProcessor dut;

    // Устанавливаем начальные значения входов.
    dut.clock = 0;
    dut.reset = 1;

    // eval() пересчитывает модель после изменения сигналов.
    dut.eval();

    return 0;
}

Формально работать с Verilator можно непосредственно через поля сгенерированного класса. Но если это делать во всех тестах, очень быстро появятся десятки немного различных реализаций clock, reset и порядка вызова eval(). Поэтому первым уровнем нашей обвязки становится единый объект для описания тестбенча:

#include "VStreamProcessor.h"
class Simulator {
public:
    explicit Simulator(VStreamProcessor& dut)
        : dut_(dut) {}
    void tick() {
        // Срез сигнала
        dut_.clock = 0;
        dut_.eval();
        // Фронт сигнала.
        dut_.clock = 1;
        dut_.eval();

        ++cycle_;
    }

    void reset(unsigned cycles = 2) {
        dut_.reset = 1;
        for (unsigned i = 0; i < cycles; ++i)
            tick();
        dut_.reset = 0;
        tick();
    }

    uint64_t cycle() const {
        return cycle_;
    }

private:
    VStreamProcessor& dut_;
    uint64_t cycle_ = 0;
};

BFM-агенты: единый язык транзакций для разных интерфейсов

О том, как BFM-агенты экономят время, я упоминал выше. Система агентов стала важной частью фреймворка. На их уровне мы разделили роли: driver формирует входные транзакции, monitor пассивно наблюдает выход, responder моделирует поведение ведомой стороны и управляет backpressure. Для блока с несовпадающими входным и выходным интерфейсами входной и выходной BFM могут быть разными, а проверка выполняется на общем уровне данных. Одинаковый сценарий можно запускать с разными реализациями интерфейса, если они предоставляют совместимый транзакционный контракт.

Каждый агент содержит протокольный автомат, очередь транзакций, настройки задержек и механизм регистрации событий. Тест работает с объектами транзакций — пакетами, словами, адресными операциями, — а не напрямую с каждым сигналом. Поэтому логи BFM легко становятся основой диагностики: вместо сотен переключений сигналов инженер видит последовательность осмысленных транзакций.

BFM-агент должен уметь намеренно создавать тяжелые условия: случайные паузы, длительный backpressure, burst-граничные случаи, reset в середине обмена и ограничение пропускной способности.

Вот пример реализации. Мы объявляем общую структуру транзакции:

struct StreamTransaction {
    uint32_t data;
};

Определяем непосредственно BFM-агент:

#include <queue>

class StreamDriver {
public:
    explicit StreamDriver(VStreamProcessor& dut)
        : dut_(dut) {}

    // Тест работает с транзакцией, а не с отдельными сигналами
    void push(StreamTransaction transaction) {
        queue_.push(transaction);
    }

    // Метод вызывается планировщиком каждый такт.
    void tick() {
        if (!active_ && !queue_.empty()) {
            current_ = queue_.front();
            queue_.pop();
            active_ = true;
        }

        if (!active_) {
            dut_.io_in_valid = 0;
            return;
        }

        // BFM самостоятельно преобразует высокоуровневую транзакцию в сигналы интерфейса.
        dut_.io_in_valid = 1;
        dut_.io_in_bits  = current_.data;

        if (dut_.io_in_valid && dut_.io_in_ready) {
            active_ = false;
        }
    }

    bool empty() const {
        return queue_.empty() && !active_;
    }

private:
    VStreamProcessor& dut_;

    std::queue<StreamTransaction> queue_;
    StreamTransaction current_{};

    bool active_ = false;
};

Определяем BFM-монитор, который будет получать выходные транзакции с блока:

#include <functional>

class StreamMonitor {
public:
    using Callback =
        std::function<void(const StreamTransaction&)>;

    StreamMonitor(
        VStreamProcessor& dut,
        Callback callback
    )
        : dut_(dut),
          callback_(std::move(callback)) {}

    void tick() {
        // Monitor наблюдает за интерфейсом.
        if (dut_.io_out_valid && dut_.io_out_ready) {
            callback_({
                .data = static_cast<uint32_t>(
                    dut_.io_out_bits
                )
            });
        }
    }

private:
    VStreamProcessor& dut_;
    Callback callback_;
};

А вот пример упрощения, которое обеспечивает BFM. Так выглядит вызов транзакции без BFM:

dut.io_in_bits  = 42;
dut.io_in_valid = 1;

while (!dut.io_in_ready) {
    tick();
}

tick();

dut.io_in_valid = 0;

Та же самая операция, но с BFM:

input.push({42});

Может показаться, что мы специально спустились на уровень ниже, к Verilog, а теперь зачем-то решаем обратную задачу — поднимаемся обратно до функционального уровня. На самом деле, когда мы пишем BFM, то не просто создаем высокоуровневый интерфейс, а сами описываем каждый сигнал и алгоритм работы с ним. Более того, использование BFM не запрещает нам обращаться к сигналам напрямую из теста и проверять особые случаи.

Автогенерация обвязки и тестов

С генерацией обвязки проблема вот в чем. У нас есть BFM-агенты, преимущества которых я только что описал. Но создавать их на Chisel неудобно, поскольку так или иначе нам все равно нужно аппаратно описывать протоколы. Если мы потом захотим протестировать на уровне Chiseltest, снова нужно делать описание, но уже в обратную сторону. Если, например, нужно проверить slave-часть AXI, то придется написать еще одну master-часть AXI, которая будет ходить в этот блок. Дополнительная обвязка понадобится и когда нам нужно сделать код на C++ для Verilator. 

Эти вопросы мы пока решаем, а автогенерация тестов уже работает. Из параметров Chisel и описания портов генерируются C++-константы, типы данных, binding к сигналам и конфигурация BFM. По шаблонам создаются wrapper DUT и каркас тестовой фикстуры. Разработчику остается только указать сценарий и проверяемые свойства.

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

Мы строго придерживаемся принципа: автогенерация не должна скрывать логику полностью. Сгенерированный код остается читаемым, а граница между шаблоном и ручной частью фиксируется явно. Для CI разделяем тесты по их требовательности: дешевый smoke-набор, конфигурационную регрессию и длительные стрессовые тесты. 

Для генерации обвязки используем условно стандартизированные классы, примерно такие:

template <typename T>
struct InputStreamBinding {
    T& dut;

    auto& valid() { return dut.io_in_valid; }
    auto& ready() { return dut.io_in_ready; }
    auto& data()  { return dut.io_in_bits; }
};

Интеграция с математическими моделями, приложениями и другим RTL

Как для команды косимуляции, для нас эта часть фреймворка наиболее важна. FPGA — это лишь один из этапов тестирования. Помимо него, блок должен пройти математическую модель, программный, RTL- и UVM-тест. Поэтому мы стараемся сделать максимально гибкий со стороны тестов инструмент: если в каждом случае писать тестовые блоки заново, уйдет очень много времени.

Существующие библиотеки и приложения можно подключать через тонкий адаптер, не переписывая алгоритм на язык тестбенча. Если часть системы уже существует в RTL, ее можно включить в ту же Verilator-сборку и тестировать взаимодействие блоков без программной имитации. Внешнее приложение может выступать источником данных, потребителем результатов или моделью среды — например, генерировать пакеты, обрабатывать поток или воспроизводить записанный трафик.

В рамках фреймворка эталонная математическая модель может вычислять ожидаемый результат параллельно с DUT. Scoreboard сравнивает эти потоки на уровне транзакций. Вообще, в плане режимов работы наши возможности очень широки. Можно подтянуть какой-нибудь RTL или снапшот, запуститься локально. Или подключить сетевую обвязку и запускаться совместно с FPGA. Мы пишем умные тесты, которые могут и работать изолированно, и к RTL обращаться, и к битстриму.

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

Вместе с дружелюбностью Verilator, которую мы активно эксплуатируем, это дает возможность быстро искать ошибки, в том числе интеграционные. Не нужно тратить много времени, чтобы понять, где ошибка — в тесте, в сети, в приложении-приемнике, в драйвере или в FPGA. Если мы поменяем FPGA-часть на симулятор и ошибка пропадет — значит, проблема в битстриме. Если не пропадет — то не в бистриме. Для аналогичной проверки вместо сетевой части мы можем напрямую прикрутить Verilator. Достижение такой гибкости — очень важная задача косимуляции.

Как в итоге работает фреймворк 

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

После генерации RTL Verilator превращает DUT в C++-модель, а фреймворк добавляет к ней необходимую обвязку. Входной BFM-driver получает от теста обычные транзакции и самостоятельно управляет сигналами интерфейса. На выходе monitor собирает принятые данные обратно в транзакции. Поэтому самому тесту не нужно вручную переключать сигналы, он работает с данными более высокого уровня.

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

Если тест обнаруживает расхождение, фреймворк сохраняет информацию, необходимую для повторного запуска: конфигурацию DUT, seed случайного сценария и контекст первой ошибочной транзакции. При необходимости дополнительно сохраняется вейв-форма. Тот же сценарий затем можно прогнать для другой ширины данных, глубины FIFO или набора интерфейсов, не переписывая саму логику теста.

В итоге бо́льшая часть различий между конфигурациями остается внутри автоматически сгенерированной обвязки и BFM, а сценарий описывает в основном то, что именно мы хотим проверить, а не то, как для этого нужно переключать отдельные сигналы. С другой стороны, благодаря особенностям описания Verilator легко можно воспроизводить более сложные сценарии — с переключениями по срезу, с отслеживанием перформанса блока и работой с внутренними сигналами.

Преимущества, недостатки и границы применимости

Наш фреймворк на Verilator имеет много плюсов. Высокая скорость длительных прогонов, нативная отладка C++, гибкое управление тактами и сигналами, интеграция с моделями и другим RTL, расширяемая диагностика, повторное использование BFM. С Verilator удалось найти баги, которые не были видны ни на FPGA, ни с Chisel — к этим багам меня вывели просадки по шинам. В дальнейшем я также смогу смотреть на вейв-формы и выявить те баги, которые могли проскочить на предыдущих этапах.

Но есть и недостатки. Теряется часть «сахара» Chiseltest. В «плюсах» более жесткие требования к коду и работе с памятью. Часть функций, что есть в Scala под капотом, в C++ приходится писать вручную — например, управление временем и сбросом. Так возникает дублирование знаний о протоколе: интерфейс описан в RTL, а поведение driver/monitor — в C++. Поэтому важно наличие единого источника метаданных и тестов самих BFM.

Для небольшого комбинационного блока использование фреймворка может быть неоправданно дольше, чем запуск отдельного теста. К тому же Verilator подходит не для всех классов ошибок: формальные свойства, специфические X/Z-сценарии и временные эффекты могут требовать других средств.

Отдельно отмечу высокую цену входа: изначально нужно настроить сборку, wrapper, агенты, CI и кеширование артефактов. Но потом вам откроются все плюсы.

Планы развития фреймворка

В будущем мы планируем генерировать BFM по шаблонам: это уменьшит объем pin-level кода от руки и поможет стандартизировать поведение агентов. Будем укреплять дружбу Chisel и C++: транслировать параметры одного в другой, чтобы исключить расхождения констант, ширин и режимов между DUT и тестовой частью.

Планируем дорабатывать системные сценарии: добавим композицию нескольких агентов, управление фазами теста и запуск сложных последовательностей. Для распространенных интерфейсов оформим библиотеку готовых агентов и набор протокольных assertion/checker. Организуем сбор функционального покрытия на уровне транзакций и параметров, чтобы понимать не только количество тестов, но и реально пройденные режимы. Наконец, введем унифицированный формат описания DUT и тестовой конфигурации, который станет единым источником данных для генерации wrapper, BFM и документации.

Если вы занимаетесь аппаратной разработкой и тестированием, обратите внимание на наши вакансии:

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


  1. vaxxabait
    26.08.2026 19:59

    1. Преимущества перед Verilator+CocoTB+cocotbext-* ? PyUVM, cocotb-coverage добавить по желанию, облегчённые варианты вроде Apheleia Verification Library.... (почти) Де-факто стандарт, если нет Xcelium/VCS/Questa.

    2. Преимушества перед Verilator --sc + SystemC reference + UVM-SystemC + CRAVE + FC4SC ? Так сказать, альтернатива из двухтысячных. Системщики ещё прикручивают QEMU.

    3. Стоимость поддержки своего велосипеда вместо вышеозначенных ?


    1. Mankeyy Автор
      26.08.2026 19:59

      Здесь, наверное, в статье стоило четче провести границу - мы не считаем наше решение универсальной или более практичной заменой существующим стекам.
      Попробую на все 3 вопроса ответить одновременно)
      Для нас C++ это не просто язык описания тестбенча, а скорее общая среда для выполнения всей ко-симуляции. У нас существуют и UVM-решения, и DPI-альтернативы, но задача была не в написании своего фреймворка для верификации, а в создании моста между существующей инфраструктурой и модульными тестами под некоторые блоки. Именно поэтому мы стараемся держать эту обвязку из Verilator максимально тонкой, а все сложные сценарии утаскиваем в другие среды выполнения. Сложность разработки такого окружения окупается из-за того, что она может переварить сотни различных конфигураций наших блоков, при этом не требует особой адаптации от других компонентов.


  1. zloy_igrok
    26.08.2026 19:59

    Вы не пробовали писать тестбенч на SystemVerilog и уже в нём связывать DUT с тестами UVM?


    1. Mankeyy Автор
      26.08.2026 19:59

      Мы рассматривали такой вариант, но пока что отказались от него по нескольким причинам:
      1) Мы по-большей части системные программисты, поэтому нам бы понадобилась помощь соседнего отдела в создании такого решения. Хотелось иметь собственную небольшую среду, в которой можно проводить не полную формальную верификацию блоков (потому что блоки инструментальные, а не продуктовые), а скорее просто модульное тестирование и отладку изолированного функционала.
      2) Разработка универсального тестбенча в нашем случае не была бы бесплатной, т.к. конфигурации блоков очень разняться по набору интерфейсов, названиям и т.д. Со стороны UVM также понадобилось бы делать специальные секвенсеры и драйверы.
      Поэтому в конечном итоге, по трудозатратам, то на то и вышло бы примерно. Зато мы теперь имеем кучу описанных тестов, которые очень легко разрабатываются любым членом команды и которым не нужна DPI обвязка для использования программных или системных функций.