Привет, Хабр! На связи Михаил Степанов, инженер в группе функциональной верификации YADRO. Еще в прошлом году мы с моим коллегой Романом Казаченко участвовали в хакатоне по разработке микропроцессоров как студенты, а сейчас — помогаем с задачами для SoC Design Challenge как сотрудники компании-организатора. В статье расскажем, что ждет участников трека «Системная верификация СнК» в этом году и как подготовиться к этому испытанию.
Если вы не планируете участвовать в хакатоне, но вам интересно, как инженеры тестируют системы на кристалле перед запуском в производство, эта статья тоже будет вам полезна. На примере заданий хакатона я кратко объясню, что такое системная верификация, из каких блоков состоят СнК и какие инструменты используются для их тестирования.

Инженерный хакатон SoC Design Challenge от НИУ МИЭТ и YADRO пройдет 18–20 апреля. Если вы студент очной формы обучения, то проверить свои знания и получить новые навыки можно в треках «Топологическое проектирование», «UVM-верификация», «Системная верификация СнК» и «RTL-проектирование». Хакатон — это отличная возможность пообщаться со специалистами «железной» индустрии, продемонстрировать им свои знания и получить классные подарки. Присоединяйтесь, заявки принимаются до 18 марта.
Как попасть на хакатон
В прошлом году мы с Ромой были студентами выпускного курса «Информатика и вычислительная техника» в ИТМО и решили испытать себя на хакатоне SoC Design Challenge. Тогда у нас было немного опыта в разработке системного ПО, поэтому мы выбрали трек «Системное программирование». В этом году его переименовали в «Системную верификацию СнК», что точнее отражает суть задания.
Путь истового инженера начинается с несложного отборочного теста. Обычно, чтобы его успешно пройти, достаточно знаний на уровне второго и третьего курсов. Самое сложное — успеть ответить на все вопросы за отведенные четверть часа. В интернете искать ответы можно, но мы не рекомендуем — можете впустую потратить драгоценные минуты.
Рекомендуем сразу оценить, сколько времени вы сможете потратить на каждый вопрос. Если над каким-то из них нужно поломать голову, лучше его пропустить и попробовать сделать следующее задание. Так вы успеете больше за отведенное время и не потеряете потенциальные баллы.

Если вы пройдете отбор, то вас пригласят на основной этап в Зеленоград и поселят в классном отеле с трехразовым питанием. На решение кейса у вашей команды будет три дня. Первые два дня работать придется с утра до вечера, можете ориентироваться на восемь часов в день. В последний день времени будет меньше из-за подведения итогов. Рекомендуем быть готовыми трудиться над заданием по вечерам, но не в ущерб сну. Отдых — залог эффективной работы и вашей победы.
На SoC Design Challenge 2025 несколько треков с соответствующими заданиями: «Топологическое проектирование», «UVM-верификация», «Системная верификация СнК» и «RTL-проектирование». Описания заданий с прошлогоднего хакатона можно посмотреть в анонсе хакатона. В этой статье я делюсь опытом нашей команды в треке по системной верификации и даю подробные советы по работе с инструментами, которые пригодятся вам в этом году. Также, как участник процесса подготовки заданий, рассказываю об изменениях по сравнению с прошлым годом.
Про задание трека «Системная верификация СнК»
Нам нужно было провести верификацию аппаратного блока в косимуляционном окружении. Давайте разберемся, что это значит.
Системы на кристалле состоят из логических и функциональных блоков. Такое деление позволяет независимо разрабатывать разные части системы, а затем модульно отлаживать их в изолированном окружении. Функциональная верификация — это процесс тестирования таких блоков по спецификации. Она проводится перед отправкой чипа на завод и называется pre-silicon стадией. Без этой процедуры велик шанс получить нерабочий чип, причем в объемах всей произведенной партии.
Проводить тесты можно при помощи таких инструментов, как:
Функциональные симуляторы (например, QEMU) — работают очень быстро, но в отличие от других инструментов не предоставляют достоверной информации о работе устройства. Зато QEMU может почти молниеносно осуществить подачу требуемых данных и выдать результат в виде «прошел» или «не прошел», а также некоторые логи.
Потактовые симуляторы (например, VCS) — у них есть внутренняя система планирования времени, но скорость работы низкая. Они нужны для сбора трассировочных файлов, по которым можно восстановить состояние любого регистра и провода в любой момент.
ПЛИС — специальные вычислительные платформы, на которые можно загрузить и запустить «образ» своей микросхемы, будто это уже готовое физическое устройство. ПЛИС значительно быстрее потактовых симуляторов, но при этом дает представление о реальном поведении системы на кристалле.
У каждого решения есть плюсы и минусы, и не всегда очевидно, какое лучше подходит для решения конкретной задачи. Именно поэтому придумали концепцию косимуляции, при которой микросхема делится на небольшие логические части, каждая из которых запускается в отдельном окружении. Наша задача как инженеров верификации — настроить бесшовное соединение этих частей. Так мы заметно ускоряем работу потактового симулятора, потому что распределяем его нагрузку на другие элементы системы.
На хакатоне участникам предложили поработать с небольшим DUT-устройством (Device Under Test): UART 16650. Нам нужно было найти специ��икацию блока, внимательно ее изучить и составить тестовый план, который покрывает все основные главы спецификации. После составления тестового плана и написания тестов нужно прогнать их в двух разных конфигурациях интерфейса:
c обратной петлей — UART соединен сам с собой,
с подключением к термодатчику — интерфейс подключен к устройству, взаимодействие с которым также можно протестировать.
Успеть за три дня
Задание сначала может показаться сложным и не очень понятным. Но если грамотно разбить его на подзадачи, а затем постепенно двигаться к решению, то вам вполне хватит трех дней на успешную реализацию.
В первый день мы решили засесть за изучение материалов и спецификаций, а не бросаться судорожно писать тесты. Самой большой проблемой для нас тогда был страх перед непонятным и незнакомым заданием. Но он быстро прошел — заботливые инженеры YADRO снабдили нас исчерпывающей документацией. В ней описано, что и как устроено, а также в каком порядке выполнять работу.
В общем, не старайтесь в первый день стать самыми быстрыми системными программистами на хакатоне. Как показала практика — медленный, но осознанный подход приводит к лучшим результатам в ограниченное время. Так проще выходить из тупиков, в которых вы наверняка окажитесь, а их может быть много.

Если на какую-то подзадачу вы безрезультатно потратили много времени, то попросите помощи у менторов. Штрафные баллы за это не начислят, а вы быстрее выйдите из сложной ситуации.
Отметим, что задание и исходный код могут быть неидеальны. Мы, например, нашли несоответствие спецификации RTL-коду. Но не стоит сразу же писать об этом во всех чатах хакатона — лучше несколько раз проверить найденную ошибку и записать ее в отчет по верификации.
Поспешайте медленно
Правильно презентовать результаты перед жюри так же важно, как и решить саму задачу. В некоторых заданиях нужно предоставить результат в конкретном формате, например:
корректно сформировать план тестирования,
прогнать все тесты через CI,
нарисовать все нужные схемы и таблицы,
добавить текстовое пояснение к каждому тесту.
Не рекомендуем гнаться за объемом написанного кода и количеством тестов — можете не успеть привести код в порядок и потерять заветные баллы.
Распределите роли в команде, а работу разбейте на микроспринты. Задач и аспектов много, их надо учитывать на каждом этапе выполнения: поработали, обменялись кодом, провели кросс-ревью, позапускали на регрессе, записали проблемы и баги, задали вопросы ментору, повторили.

Залог вашего успеха — баланс скорости и осознанности. Если наброситься на выполнение заданий, то можно упустить важные моменты и прийти к финишу в аутсайдерах после спринтерского старта. Однако излишняя медлительность может заставить вас потратить время на задачи, которые для победы попросту не нужны.
На этом мы заканчиваем с вьетнамскими флешбэками нашим опытом на SoC Design Challenge 2024 и переходим к хакатону этого года, но уже с другой стороны — специалистов, которые помогают организаторам. Спойлеров не будет, а вот о критериях оценки ваших работ и инструментах с конкретными примерами кода с удовольствием расскажем.
Что будет на SoC Design Challenge 2025
Расскажем, что ждет участников трека «Системная верификация СнК» в апреле. Вы снова будете работать с DUT, для которого нужно провести верификацию. Но на этот раз не UART. А что именно — узнаете на самом хакатоне, всех секретов раскрывать не будем. Для DUT нужно будет разработать тестовый план и сами тесты.
В чем отличия от задания прошлого года:
Участникам больше не придется копаться в самом RTL-коде. Мы решили, что концепция «черного ящика» лучше подходит под формат проведения. К тому же не все участники в прошлом году были хорошо знакомы с SystemVerilog, что стало для них дополнительной сложностью.
Само задание не будет запускаться с целью проверки, что все работает. Мы планируем намеренно добавить некоторое количество неисправностей, чтобы участники выявили ситуации, в которых эта неисправность проявляется, и описали ее.
Зафиксированных тестовых данных не будет. Участники должны сами разобраться в том, как лучше представить входные данные и подготовить несколько шаблонов.
Надеемся, что изменения сделают хакатон еще более привлекательным для молодых специалистов.
Как подготовиться к хакатону
Далее поделимся критериями оценки работ участников, а также подробно разберем инструменты QEMU и Verilator, без которых на хакатоне не обойтись. Устраивайтесь поудобнее, будет много кода.
На что обращают внимание судьи
Как судьи выбирают победителей — наверное, самая важная информация. Поделимся основными критериями оценки:
Качество кода. Здесь будут приветствоваться проверка с помощью статических анализаторов, логичные и понятные названия переменных и функций, лаконичность самого кода и интересные входные данные.
Тестовое покрытие. Судьи соберут процент покрытых строчек кода с DUT и узнают, насколько хорошо вы справились с написанием тестов.
Количество найденных ошибок. Как уже говорилось, мы намеренно добавим несколько ошибок. Чем больше ошибок вы найдете, тем больше баллов получите.
Тестовый план. Он должен быть подробным и понятным.
Обзор инструментов с примерами кода: QEMU и Verilator
Мы также решили немного изменить нашу систему косимуляции. В распоряжении участников будет два инструмента: QEMU и Verilator. Займемся разработкой небольшой библиотеки rtl-bridge для Verilator — она будет отвечать за коммуникацию двух ч��стей системы. Давайте разберемся, что это за инструменты.
QEMU
В косимуляции QEMU в основном нужен для управления тестом: передавать и считывать данные, а также проверять результаты на корректность.
Компания Xilinx уже задумывалась о создании подобных систем косимуляции, поэтому они разработали Remote Port — протокол передачи данных через сокеты. Чтобы этот протокол работал в реальных условиях, Xilinx создала свою версию QEMU и устройство с поддержкой взаимодействия по Remote-Port. Именно ее мы возьмем для небольшого примера.
Для начала надо скачать и собрать QEMU из официального репозитория. Процесс описан в файле README, поэтому подробно разбирать его не будем. Отметим, что не обязательно собирать все таргеты QEMU. Для это во время конфигурирования замените команду на такую:
configure --target-list=riscv32-softmmu,riscv64-softmmu
Рекомендуем использовать версию g++ 11.4.0, так как на более старых версиях могут возникнуть проблемы со сборкой.
Выбираем машину для запуска будущего теста с помощью команды:
qemu-system-riscv32 --machine ?
Получаем такой список:

Поскольку мы проводим косимуляцию, выбираем virt-cosim.
Напишем простой тест, чтобы отправить строчку «Hello world!» на сторону RTL-кода. Мы планируем писать bare metal-тест, который запускается на «голом железе», то есть без операционной системы. Поэтому нужно узнать регистры и области памяти, в которые будет записываться информация.
Сначала нам нужны регистры UART, чтобы тест мог вывести информацию в консоль QEMU. Открываем файл /hw/riscv/virt.c и находим таблицу virt_memmap. В ней указаны адреса, по которым находятся виртуальные устройства:

UART находится по адресу 0x10000000. Затем ищем вхождение строки «UART» в файле и находим следующий блок кода. Он показывает, что это устройство соответствует спецификации ns16650a:

Документацию к устройству можно прочитать самостоятельно, поэтому приводим уже готовый код:
#define UART0_BASE 0x10000000 #define REG(base, offset) ((*((volatile unsigned char *)(base + offset)))) #define UART0_DR REG(UART0_BASE, 0x00) #define UART0_FCR REG(UART0_BASE, 0x02) #define UART0_LSR REG(UART0_BASE, 0x05) #define UARTFCR_FFENA 0x01 // UART FIFO Control Register enable bit #define UARTLSR_THRE 0x20 // UART Line Status Register Transmit Hold Register Empty bit #define UART0_FF_THR_EMPTY (UART0_LSR & UARTLSR_THRE) void uart_putc(char c) { while (!UART0_FF_THR_EMPTY); // Ждем, пока UART не освободится UART0_DR = c; // Пишем символ в регистр UART } void uart_puts(const char *str) { while (*str) { // Цикл для всех символов строки uart_putc(*str++); // Пишем очередной символ } }
Функция uart_putc нужна для вывода в консоль одного символа, а uart_puts — для вывода строки.
Теперь пишем тело теста. Для этого выясним, по каким адресам расположено косимуляционное устройство. В таблице virt_memmap указано, что устройство находится по адресу 0x28000000. Тело нашего теста выглядит так:
void write_mem_32(int addr, int value) { volatile int *const __ptr = (volatile int *)(addr); *__ptr = value; }; int read_mem_32(const int addr) { const volatile int *const __ptr = (const volatile int *)(addr); const int __value = *__ptr; return __value; }; void main(){ UART0_FCR = UARTFCR_FFENA; uart_puts("main start\n"); const char *str = "Hello World!"; while(*str) { uart_puts("Send char: "); uart_putc(*str); uart_putc('\n'); write_mem_32(0x28000000, *str++); } write_mem_32(0x28000004, 1); while (1); }
Отметим три важных момента:
Мы добавили шаблонные функции для записи в память. Формально это не обязательно, но позволяет отделить использование обычной памяти от обращения к устройствам.
В конце присутствует бесконечный цикл, чтобы машина QEMU не завершала свое выполнение на некотором адресе — это может привести в непредвиденной ошибке. При желании вы можете организовать штатное завершение выполнения.
Перед бесконечным циклом мы пишем в адрес, отличный от адреса для записи строки. Эта условность нужна, чтобы мы могли завершить симуляцию.
Для сборки нам нужные специальные компиляторы, которые умеют собирать код под платформу RISC-V. Рекомендую использовать инструменты Syntacore.
Перед сборкой лучше написать код на ассемблере, чтобы установить корректный указатель на стек и линковочный файл для правильного расположения секций:
.global _start .section .text._start _start: la sp, __stack_top add s0, sp, zero jal zero, main loop: j loop .section .data .space 1024*8 .align 16 __stack_top:
DRAM_BASE = 0x80000000; DRAM_SIZE = 0x20000000; MEMORY { DRAM (rwx): ORIGIN = DRAM_BASE, LENGTH = DRAM_SIZE } SECTIONS { . = DRAM_BASE; .text : { KEEP(*(.text._start)); *(.text*); } . = ALIGN (CONSTANT (COMMONPAGESIZE)); .data : { *(.data*) } }
Пришло время заняться сборкой с помощью инструментов от Syntacore. Makefile будет состоять из четырех этапов:
Сборка кода на C.
Сборка ассемблера.
Компоновка всего этого добра в elf-файл.
Запуск самого QEMU.
.PHONY: all QEMU dir all: dir QEMU dir: mkdir build/ build/test.o: src/test.c riscv64-unknown-elf-gcc -c -g -O0 -Iinclude/ -ffreestanding -march=rv32i -mabi=ilp32 -o build/test.o src/test.c build/start.o: asm/start.s riscv64-unknown-elf-as -g -march=rv32i -mabi=ilp32 -o build/start.o asm/start.s build/hello.elf: build/test.o build/start.o riscv64-unknown-elf-ld -T ld/baremetal.ld -m elf32lriscv -o build/hello.elf build/test.o build/start.o QEMU: build/hello.elf ${XILINX_QEMU}/QEMU-system-riscv32 -M virt-cosim -chardev socket,id=cosim,path=/tmp/cosim.sock,server=on -nographic -machine-path /tmp/machine-riscv32 -bios build/hello.elf
Разберемся, как запустить QEMU:
Укажем cosim-машину, чтобы создалось устройство cosim.
В качестве используемого сокета укажем UNIX-сокет по пути /tmp/cosim.sock.
Укажем директорию для создания записей о работе машины.
Выключим графику.
Передадим elf-файл под видом BIOS.
Поясним последний пункт: в линковочном файле указан 0x80000000 — это стандартный адрес расположения BIOS. Поэтому передаем elf-файл под видом BIOS. Если мы захотим запустить программу на другом адресе, сначала запустится OpenSBI, который проверит подключенные устройства и передаст управление прикладной функции. Тогда нужно указать другой адрес и запускать QEMU с параметром loader.
Verilator
Приступаем к созданию второй части системы. Для этого устанавливаем Verilator версии 5 и выше. Это потактовый симулятор для языка Verilog, которым можно управлять при помощи программы на C++.
Чтобы разобраться, как работает система Verilator, напишем такую программу:
#include "Vtestbench.h" #include "verilated.h" int main(int argc, char** argv) { VerilatedContext* contextp = new VerilatedContext; contextp->commandArgs(argc, argv); Vtestbench* top = new Vtestbench{contextp}; while (!contextp->gotFinish()) { top->eval(); contextp->time(top->nextTimeSlot()); } delete top; delete contextp; return 0; }
В программе создаем экземпляр класса, который отвечает за логику нашего Verilog-кода. Пока симуляция не закончится, в цикле производим подсчет новых значений регистров и проводов, а также двигаем время симуляции. Можно считать, что top->eval() передает управление коду на verilog, а contextp->time(top->nextTimeSlot()) изменяет значение clk.
Для написания testbench нужно понять концепцию DPI-функций. Фактически это функции, которые позволяют передавать данные и поток управления между кодом на C++ и кодом на Verilog. Такой подход позволит выполнять логику RTL-кода и при этом предоставлять интерфейс для Remote-Port.
Для косимуляции нам понадобится несколько функций, которые передают управление из C++ кода и обратно:
sv_write — функция из C++ сообщает Verilog, что пришла транзакция на запись,
c_write_resp — Verilog сообщает C++, что транзакцию обработали,
accept_transaction — Verilog опрашивает код на C++ на предмет пришедших транзакций,
sv_finish — функция из C++ завершает симуляцию.
Схема вызовов Verilog из кода C++ и обратно:

Теперь напишем Testbench (пока только базовую часть) и объявим все необходимые функции. Про особенности объявления DPI-функций можете почитать на официальном сайте Verilator.
`timescale 1ns / 1ps import "DPI-C" context task c_write_resp; import "DPI-C" context task accept_transaction; module testbench; export "DPI-C" task sv_write; export "DPI-C" task sv_finish; reg rst_n; reg clk; always begin #5 clk= ~clk; end
Допишем часть опроса на наличие транзакций и сами DPI-функции:
initial begin rst_n = 0; clk = 0; #10; rst_n = 1; end task sv_write; input int addr; input int data; $display("Get on addr %d char %c", addr, data); // Получаем адрес и данные и выводим их в консоль c_write_resp(); endtask always @(posedge clk) begin if (rst_n) begin accept_transaction(); end end task sv_finish; $finish(); endtask
Теперь нужно обеспечить работу всех вызовов на стороне C++. Для этого создадим структуру, которая будет хранить данные о пришедшей транзакции, и напишем функции подключения и отключения сокета:
static struct { int value; // Значение bool enable; // Транзакция действительно пришла long addr; // Адрес volatile bool ping; // Для опроса, действительно ли транзакция была обработана } transaction; void sk_close() { shutdown(fd, SHUT_RDWR); close(fd); } void sk_connect() { int fd_ = socket(AF_UNIX, SOCK_STREAM, 0); struct sockaddr_un addr_; addr_.sun_family = AF_UNIX; strcpy(addr_.sun_path, "/tmp/cosim.sock"); connect(fd_, (sockaddr*)&addr_, sizeof(addr_)); fd = fd_; std::thread(&readSerializer).detach(); }
В нашем случае readSerializer — это поток, который все время читает сокет и сохраняет полученные пакеты Remote-Port. Код этой функции выглядит так:
static void readSerializer() { while (1) { std::vector<uint32_t> packet; packet.resize(30, 0); if (readPacket((uint8_t*)packet.data())) { handleRequest(packet.data()); } } } static int readSocket(uint8_t* data, size_t size) { int ret = read(fd, data, size); if (ret <= 0) exit(1); return be32toh(((uint32_t*)data)[1]); } static int readPacket(uint8_t* packet) { int ret = readSocket(packet, 20); readSocket(packet + 20, ret); uint32_t* p = (uint32_t*)packet; return ret; }
Для обработки транзакций напишем функцию handleRequest. Рекомендуем почитать документацию к протоколу Remote-Port, чтобы разобраться с кодом.
Обратите внимание, что все данные приходят в обратной последовательности байт, поэтому необходимо использовать функцию be32toh. Она возвращает байтам правильную последовательность. Также научимся обрабатывать sync-пакеты для корректной работы с QEMU.
Пример кода, который обрабатывает sync-, write- и read-пакеты:
void handleRequest(uint32_t* data) { switch (be32toh(data[0])) { // Проверяем поле command в пакете case 4: // Если команда write onWriteOperation((be32toh(data[9]) << 32) + be32toh(data[10]), ((uint32_t*)(((uint8_t*)data) + 2))[14]); // Сохраняем адрес и данные из пакета, ждем ответ от verilog data[1] = be32toh(be32toh(data[1]) - 4); // уменьшаем поле длины data[3] = be32toh(2); // Выставляем бит response writePacket(data); // Отправляем ответ break; case 6: // Если команда sync data[3] = be32toh(2); // Выставляем бит response writePacket(data); // Отправляем ответ break; default: return; } } void onWriteOperation(long addr, int value) { transaction.value = value; transaction.addr = addr; transaction.ping = false; transaction.enable = true; while (!transaction.ping) {} // Ждем, пока не придет ответ от Verilog } void writeSocket(uint8_t* data, size_t size) { write(fd, data, size); } void writePacket(uint32_t* data) { writeSocket((uint8_t*)data, 20 + be32toh(data[1]));
Осталось написать функцию опроса из Verilog, она совсем простая:
int accept_transaction() { if (transaction.enable) { transaction.enable = false; sv_write(transaction.addr, transaction.value); } return 0; }
Доработаем main, чтобы он создавал сокет:
#include "Vtestbench.h" #include "svdpi.h" #include "verilated.h" void sk_connect(); void sk_сlose(); int main(int argc, char** argv) { VerilatedContext* contextp = new VerilatedContext; contextp->commandArgs(argc, argv); Vtestbench* top = new Vtestbench{contextp}; sk_connect(); while (!contextp->gotFinish()) { top->eval(); contextp->time(top->nextTimeSlot()); } void sk_close();и delete top; delete contextp; return 0; }
Теперь можно запускать тест в косимуляции. Приведу пример своего Makefile:
.PHONY: all clean sim all: sim TARGET = main TOP_MODULE = testbench BUILD_DIR = build SIM_DIR = $(BUILD_DIR)/simulation SRC_DIR = rtl-bridge SRCS := $(shell find ./$(SRC_DIR) -name '*.cpp') VERILATOR_FLAGS = --trace --exe --cc --build --timing -Wno-PINMISSING -Wno-IMPLICIT -Wno-WIDTHEXPAND -Wno-INITIALDLY -Wno-CASEINCOMPLETE -Wno-WIDTHTRUNC --Mdir $(SIM_DIR) TARGET = main $(TARGET): $(SRCS) $(SRC_DIR)/$(TOP_MODULE).sv mkdir -p $(SIM_DIR) verilator $(VERILATOR_FLAGS) -o V$(TOP_MODULE) $(SRC_DIR)/$(TOP_MODULE).sv $(SRCS) make -C $(SIM_DIR) -f V$(TOP_MODULE).mk ln -sf $(SIM_DIR)/V$(TOP_MODULE) $(TARGET) sim: $(TARGET) Makefile ./$(TARGET) clean: rm -rf $(BUILD_DIR) $(TARGET)
В окне с QEMU:

В окне с Verilator:

Общая схема полученной системы:

Ждем на хакатоне
Еще есть время разобраться с инструментами и поработать с исходниками, которые мы упомянули в статье. Всех студентов, которым интересно проверить свои силы и пообщаться с профессионалами рынка системного программирования, ждем на инженерном хакатоне SoC Design Challenge 2025. Главное — верить в свои силы и не пасовать перед вызовами. Желаем удачи!