тяжелая наркомания: Сборка VPI расширения и запуск Verilog тестов при помощи cmake/ctest

Недавно потребовалось для нужд кровавой отладки сделать свое VPI расширение для verilog HDL и встал вопрос как интегрировать это добро с какой-нибудь нормальной системой сборки. К сожалению, в плане инструментов для сборки, отладки и тестирование люди проектирующие СБИС живут если не в каменном веке, то в своем особом мирке, со своей особой атмосферой. Опенсурсный iverilog в этом плане ничуть не лучше коммерческих тулов, которые кладут огромный болт на многие общепринятые в мире linux практики.
Ну да хрен с ними. Надо решать как с этим жить. В этой заметке я постараюсь пройтись по основным нюансам написания CMakeLists.txt для сборки vpi расширения и его unit-тестирования.

Итак, что такое VPI? Если опустить маркетинговый bullshit, это такой интерфейс, который позволяет добавить к симулятору свои таски/функции, которые реализованы на С. Для этого есть API, используя который надо скомпилировать разделяемую библиотеку. Только в виде расширения у нее вместо .so/.dll будет .vpi. В случае с iverilog для этого существует костыль под названием iverilog-vpi. То есть компилировать наш код предлагается так:

iverilog-vpi vpilib.c src2.c ...

На выходе будет vpilib.vpi, название берется из имени первого файла с исходниками (sic!). С этим безобразием надо было что-то делать, потому я начал с того, что разтрепанировал iverlog-vpi, который оказался просто баш скриптом. И в самом верху скрипта мы обнаруживаем столь нужное

# These are the variables used for compiling files
CC="gcc -std=gnu99"
CXX=g++
CFLAGS="-fPIC -Wall -Wshadow -g -O2 -fstack-protector --param=ssp-buffer-size=4 -Wformat -Werror=format-security -I/usr/include/iverilog"
 
SUFFIX=
 
# These are used for linking...
LD=$CC
LDFLAGS="-shared -L/usr/lib/x86_64-linux-gnu"
LDLIBS="-lveriuser$SUFFIX -lvpi$SUFFIX"

Окей, ребята ничегошеньки не слышали про pkg-config, как теперь будем с этим жить? Ведь хочется нормальных юнит-тестов и прочих плюшек. Выход был один — научить cmake собирать для нас VPI. Для этого открываем CMakeLists.txt и начинаем готовить очень сильное колдунство:

Начал я с того, что в коде разделил собственно часть, которая объявляет VPI таски и привязывает API моей библиотеки к verilog’у и все остальное.

SET(CORE_SOURCES core.c panic.c)
SET(VPI_SOURCES vpi_wrapper.c)

Следующим шагом, переосмыслив все то, что мы увидели в iverilog-vpi стали следующие строчки, куда я тупо запихнул все те флаги, которые использует iverilog-vpi, а так же установил на выходное имя файла библиотеки суффикс «.vpi» и убрал префикс.

SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99 -fPIC -Wall -Wshadow -g -O2 -fstack-protector --param=ssp-buffer-size=4 -Wformat -Werror=format-security")
SET(CMAKE_EXE_LINKER_FLAGS "-L/usr/lib/x86_64-linux-gnu -lrt -lpthread -ldl -lm")
SET(CMAKE_SHARED_LIBRARY_SUFFIX ".vpi")
SET(CMAKE_SHARED_LIBRARY_PREFIX "")
 
INCLUDE_DIRECTORIES(
  /usr/include/iverilog
  ${CMAKE_SOURCE_DIR}/include
)

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

Начнем с простого и понятного — с тестах на родном для нас языке С. Тесты на языке С я привык делать по такому шаблону:

  • инициализация
  • собственно тестовый сценарий
  • полная деинициализация и освобждение всех ресурсов

Такой подход позволяет прогнать тестовый сценарий через valgrind и сразу увидеть есть на каких тестах что-то потечет. Как правило я запускаю каждый тест дважды. Один раз без vagrind’а, а один раз с ним. Иногда некоторые тупые на вид косяки могут вызывать ситуацию когда наличие отладчика сказывается на результатах.  Ниже я привожу пример из другого проекта как можно пачкой добавлять unit-test’ы к проекту. Эта функция подсасывает из указанной директории все .c файлы, каждый компилирует как бинарник и добавляет в ctest в двух экземплярах: обычный запуск, запуск из под valgrind’а. В самом примитивном виде это выглядит вот так:

file(GLOB TESTS_C ${CMAKE_SOURCE_DIR}/tests/c/*.c)
foreach(file ${TESTS_C})
    GET_FILENAME_COMPONENT(f ${file} NAME_WE)
    add_executable(${f} ${file})
    target_link_libraries(${f} lprobestatic)
    add_test(${f} ./${f})
    add_test(memcheck-${f} valgrind
        --error-exitcode=1 --read-var-info=yes
        --leak-check=full --show-leak-kinds=all
        --suppressions=${CMAKE_SOURCE_DIR}/valgrind.suppress
        --undef-value-errors=no --xml=yes --xml-file=${f}.xml
      ./${f})
endforeach()

И тут нас ждут очень неприятные грабли. Если мы просто слинкуем тест с vpi расширением, то получим ошибки в виде undefined reference, так как ожидается что это дело будет запускать симулятор. Решением было выкинуть обертку vpi и тестировать только нашу бибилиотеку на этом этапе. Это можно сделать например так:

add_library(lprobestatic STATIC ${CORE_SOURCES})
add_library(lprobe SHARED ${VPI_SOURCES})
target_link_libraries(lprobe lprobestatic pthread rt)

И уже линковать c этой библиотекой наше vpi расширение:

add_library(lprobe SHARED ${SOURCES})
target_link_libraries(lprobe pthread rt lprobestatic

Ну ладно, теперь двигаемся дальше, а как нам быть с тестбенчами на verilog, которые подключают это vpi расширение? Ведь нам их надо собрать и запустить?
В случае с iverilog каждый тестбенч собирается в скрипт для инструмента vvp, который и запускает симуляцию. Из командной строки сборка и запуск тестбенча выглядят примерно так:

iverilog -m myvpilib 1.v 2.v -o testname
vvp -M/path/to/dir/with/vpi ./test

Немного подумав мы получаем вот такой вот костыль, который добавляет таргеты после сборки, собственно, vpi расширения. lprobe в моем случае это имя vpi расширения, которое мы собираем.

function(verilog_test_case name)
    foreach(f ${ARGN})
        SET(files "${files} ${CMAKE_SOURCE_DIR}/${f}")
    endforeach()
 
    add_custom_command(TARGET lprobe POST_BUILD
      COMMAND iverilog -m lprobe ${files} -o ${name}
      COMMENT "Compiling Verilog Testbench: ${name}"
    )
    add_test(${name}-run vvp -M ${CMAKE_BINARY_DIR} ./${name})
endfunction()
 
file(GLOB TESTS_V ${CMAKE_SOURCE_DIR}/tests/verilog/*.v)
foreach(file ${TESTS_V})
  GET_FILENAME_COMPONENT(f ${file} NAME_WE)
  verilog_test_case(${f} ${file})
endforeach()

Вот, собственно и все. Такой подход позволяет нам использовать все плюшки cmake и без проблем использовать такие радости жизни, как coveralls, coverity и прочие, которые только можно прикрутить к cmake.

Так как iverilog не единственный в мире симулятор, то правильным подходом будет вынести большую часть iverilog-специфичных штук в отдельный cmake скрипт и подключать в зависимости от того, к какому симулятору verilog мы заходим это добро прикрутить

Добавить комментарий