Привет, Хабр! На связи Михаил Степанов, инженер в группе функциональной верификации 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. Главное — верить в свои силы и не пасовать перед вызовами. Желаем удачи!