Доброго времени суток, дорогие читатели! Данная статья является второй в целом цикле, который называется Demystifying UVM. Цикл будет посвящен глубокому погружению в основные концепции и механизмы работы библиотеки универсальной методологии верификации (Universal Verification Methodology, UVM).
Как часто начинающий верификатор начинает использовать UVM, совершенно не понимая, что вообще происходит. Что такое uvm_component_utils и type_id::create()? Почему у конструктора объекта один аргумент, а у конструктора компонента два? Откуда вдруг “взялась” функция get_full_name()? Как создаются иерархические имена по типу uvm_test_top.env.ag.mon? И что это вообще за uvm_test_top?! Очень много вопросов и очень мало общедоступных ответов.
Автор поставил перед собой задачу рассеять туман над исходным кодом UVM и основными концепциями, используемыми в данной библиотеке.
Стоит заметить, что цикл Demystifying UVM не рассчитан на инженера с околонулевыми знаниями.
Для освоения материала читателю необходимо знать:
queue, [$]);local, protected, virtual;Для получения перечисленных выше знаний рекомендуется ознакомиться с лекцией автора в Школе синтеза цифровых схем: ООП в функциональной верификации. Транзакционная модель тестирования.
Автор убежден, что прежде чем приступить к изучению нового материала, необходимо повторить ранее изученный, который связан с новым. Автор называет этот подход подсвечиванием. По образу и подобию того, как при грозе в темном небе вспыхивают молнии, в разуме человека при повторении уже изученного материала возникают “вспышки”, подсвечивающие ключевые аспекты. Озаренная вспышками часть материала далее успешно служит фундаментом для освоения нового.
Автор настоятельно рекомендует ознакомиться с первой частью статьи: Demystifying UVM: Фабрика, часть 1. В ней была рассмотрена теория о параметризации классов типами в SystemVerilog. Был проанализирован макрос uvm_component_utils и освоены важнейшие концепции singleton-класса и proxy-класса регистрации типа. Также была проанализирована минимальная авторская реализация класса сервисов UVM.
Вспомним, что в первой части статьи был частично определен маршрут создания компонента UVM (то есть любого класса, который наследуется от uvm_component или наследника uvm_component любого уровня) при помощи type_id::create().
Визуализация вызова type_id::create() для некоторого класса драйвера интерфейса AMBA APB1 apb_driver представлена на изображении ниже.
Создание компонента начинается с вызова type_id::create(), где type_id — алиас для класса регистрации uvm_component_registry, параметризованного типом класса T.
Еще раз рассмотрим реализацию метода type_id::create(). Для этого откроем файл src/uvm/uvm_registry.svh:
class uvm_component_registry #(
type T = uvm_component,
string Tname = "<unknown>"
) extends uvm_object_wrapper;
...
static function T create(
string name,
uvm_component parent
);
uvm_component obj;
uvm_coreservice_t cs = uvm_coreservice_t::get();
uvm_factory factory = cs.get_factory();
obj = factory.create_component_by_type(get(), name, parent);
$cast(create, obj);
endfunction
...
endclass
Вызов type_id::create() инициирует получение указателя на объект класса сервисов UVM, который, в свою очередь, содержит указатель на объект фабрики UVM. Через указатель вызывается метод фабрики UVM create_component_by_type(), который возвращает указатель на объект запрашиваемого типа. Запрашиваемый тип — это тип, которым параметризован proxy-класс type_id для которого вызывается метод create(). Но только ли указатель на объект запрашиваемого типа может быть возвращен фабрикой? Узнаем уже совсем скоро!
Рассмотрим исходный код файла, в котором определен тип uvm_factory. Для этого откроем файл src/uvm/uvm_factory.svh:
class uvm_factory;
...
virtual function uvm_component create_component_by_type(
uvm_object_wrapper requested_type,
string name,
uvm_component parent
);
requested_type = find_override_by_type(requested_type);
return requested_type.create_component(name, parent);
endfunction
...
endclass
Рассмотрим реализацию метода create_component_by_type(). При вызове type_id::create() вызывается именно этот метод, а в него в качестве одного из аргументов передается указатель на proxy-класс запрашиваемого типа. Заметим, что create_component_by_type() возвращает указатель на объект через handle типа uvm_component. Это значит, что данный метод фабрики UVM может вернуть указатель на любого наследника uvm_component.
Реализация метода create_component_by_type() представляет собой:
find_override_by_type();create_component() и возвращение указателя на этот объект.Дополненная визуализация вызова type_id::create() для класса apb_driver представлена на изображении ниже.
Важно более подробно остановиться на термине “результирующий тип”. Дело в том, что в ходе создания компонентов верификационного окружения часто используется переопределение типов. Метод find_override_by_type() используется для поиска возможных переопределений. Результатом вызова (то есть результирующим типом) этого метода может быть как указатель на proxy-класс запрашиваемого типа, так и proxy-класс типа, на который запрашиваемый тип был переопределен. Звучит достаточно расплывчато, не так ли? Прежде чем разбирать реализацию find_override_by_type(), следует более подробно ознакомиться с механизмами переопределения типов в UVM. Сделаем это!
Типовое переопределение компонента в UVM выглядит следующим образом:
apb_driver::type_id::set_type_override(extended_apb_driver::get_type());
Строка кода выше при помощи метода set_type_override() переопределяет тип apb_driver на тип extended_apb_driver. Под “переопредением” в данном контексте подразумеваются действия, которые приводят к тому, что любой запрос на создание компонента типа apb_driver через type_id::create() приведет к созданию компонента типа extended_apb_driver. Это значит, что все компоненты, созданные при помощи apb_driver::type_id::create(), будут иметь тип extended_apb_driver.
Рассмотрим простейший пример. Разберем верификационное UVM-окружение, структурная схема которого представлена на изображении ниже.
Окружение состоит из класса теста типа apb_test, в котором содержится два агента типа apb_agent, в каждом из которых содержится драйвер типа apb_driver и монитор типа apb_monitor.
Возможный вариант объявления класса apb_test представлен ниже:
class apb_test extends uvm_test;
`uvm_component_utils(apb_test)
apb_agent ag_master;
apb_agent ag_slave;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
ag_master = apb_agent::type_id::create("ag_master", this);
ag_slave = apb_agent::type_id::create("ag_slave", this);
endfunction
endclass
А теперь предположим, что уже в процессе верификации спецификация на дизайн изменилась, и был добавлен дополнительный функционал. Для взаимодействия с новой версией был разработан новый класс драйвера extended_apb_driver. И теперь необходимо создать такой тест, где все компоненты типа apb_driver должны быть заменены на компоненты типа extended_apb_driver. Код для создания такого тестового сценария представлен ниже.
class extended_apb_apb_test extends apb_test;
`uvm_component_utils(extended_apb_apb_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
apb_driver::type_id::set_type_override(extended_apb_driver::get_type());
super.build_phase(phase);
endfunction
endclass
Буквально при помощи одной строки кода драйверы были “заменены” во всем верификационном окружении. Просто прекрасно! Визуализация влияния вызова apb_driver::type_id::set_type_override(extended_apb_driver::get_type()) на базовую структуру верификационного окружения представлена на изображении ниже.
Чтобы понять, почему пример выше функционирует именно таким образом, погрузимся в код UVM. Начнем разбор механизмов переопределения с метода set_type_override(). Для этого откроем файл src/uvm/uvm_registry.svh:
class uvm_component_registry #(
type T = uvm_component,
string Tname = "<unknown>"
) extends uvm_object_wrapper;
...
static function void set_type_override(
uvm_object_wrapper override_type
);
uvm_coreservice_t cs = uvm_coreservice_t::get();
uvm_factory factory = cs.get_factory();
factory.set_type_override_by_type(get(), override_type);
endfunction
endclass
Прежде всего заметим, что в качестве аргумента в метод передается указатель на proxy-класс типа, который для любого типа можно получить при помощи статического метода get_type(), который определяется при вызове макроса uvm_component_utils и вызывает метод get() proxy-класса для получения указателя на него (пример вызова get_type() представлен в начале раздела).
По аналогии с разобранным ранее методом create() в методе set_type_override() реализуется доступ к классу сервисов UVM (uvm_coreservice_t::get()). Далее при помощи указателя на класс сервисов производится получение доступа к указателю на класс фабрики UVM (uvm_factory) при помощи cs.get_factory(). И, наконец, в 3 строке тела метода производится переопределение типа при помощи set_type_override_by_type().
В метод set_type_override_by_type() передается два указателя на proxy-классы: переопределяемого типа (вызов get() возвращает указатель на proxy-класс типа, для которого был вызван set_type_override()) и типа, на который будет происходить переопределение (передается через аргумент override_type).
Можно сделать вывод, что вызов type_id::set_type_override() приводит к вызову метода set_type_override_by_type() класса фабрики UVM (uvm_factory). Доступ же к классу фабрики производится через класс сервисов UVM (uvm_coreservice_t).
Визуализация вызова type_id::set_type_override() для класса apb_driver представлена на изображении ниже.
Рассмотрим подробно реализацию метода set_type_override_by_type() фабрики UVM. Для этого вновь откроем файл src/uvm/uvm_factory.svh:
class uvm_factory;
...
protected uvm_factory_override m_type_overrides[$];
...
virtual function void set_type_override_by_type(
uvm_object_wrapper original_type,
uvm_object_wrapper override_type
);
bit replaced;
foreach (m_type_overrides[index]) begin
if (m_type_overrides[index].orig_type == original_type) begin
replaced = 1;
m_type_overrides[index].orig_type = original_type;
m_type_overrides[index].ovrd_type = override_type;
end
end
if (!replaced) begin
uvm_factory_override override;
override = new(.orig_type(original_type),
.ovrd_type(override_type));
m_type_overrides.push_back(override);
end
endfunction
...
endclass
У метода set_type_override_by_type() есть два аргумента: original_type и
override_type, которые являются указателями на proxy-класс переопределяемого типа и на proxy-класс типа, на который производится переопределение.
Обратим внимание на объявленное над методом защищенное поле m_type_overrides, которое является очередью, в которой хранится вся информация о переопределении типов. Далее в тексте эта очередь может упоминаться как “очередь переопределений”. Она содержит в себе указатели на объекты типа uvm_factory_override. Рассмотрим этот тип более подробно (описан в этом же файле):
class uvm_factory_override;
uvm_object_wrapper orig_type;
uvm_object_wrapper ovrd_type;
function new (
uvm_object_wrapper orig_type,
uvm_object_wrapper ovrd_type
);
this.orig_type = orig_type;
this.ovrd_type = ovrd_type;
endfunction
endclass
Все, что содержит данный тип в своем определении, — это поля-указатели на proxy-классы переопределяемого типа и типа, на который производится переопределение: orig_type и ovrd_type. Эти указатели должны быть переданы в конструктор при создании объекта данного типа. Делаем вывод, что тип uvm_factory_override является своего рода записью, хранящей информацию о переопределении типа.
Продолжим рассматривать реализацию метода set_type_override_by_type(), в котором производится взаимодействие с очередью переопределений и добавление в нее записей. Обратим внимание на цикл foreach. Он реализует итерирование по всей очереди переопределений. Если в очереди переопределений содержится запись, где поле orig_type указывает на proxy-класс запрашиваемого типа (original_type), то делается вывод о том, что данный тип уже был переопределен ранее, и полю ovrd_type присваивается указатель на proxy-класс override_type. Локальная переменная replaced служит информационным флагом, который принимает значение 1, если в очереди переопределений была найдена запись для типа original_type.
После цикла foreach производится анализ флага replaced. Если он не равен 1, то создается запись о переопределении типа и отправляется в очередь при помощи m_type_overrides.push_back(override).
Дополненная визуализация вызова type_id::set_type_override() для класса apb_driver представлена на изображении ниже.
Закрепим анализ кода примером:
class extended_apb_apb_test extends apb_test;
`uvm_component_utils(extended_apb_apb_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
apb_driver::type_id::set_type_override(extended_1_apb_driver::get_type());
apb_driver::type_id::set_type_override(extended_2_apb_driver::get_type());
super.build_phase(phase);
endfunction
endclass
Как думаете, сколько записей будет содержаться в очереди переопределений m_type_overrides после выполнения кода в функции build_phase(), если изначально очередь была пуста?
Вернемся к методу создания компонентов в классе фабрики UVM create_component_by_type(). Для этого откроем файл src/uvm/uvm_factory.svh:
class uvm_factory;
...
virtual function uvm_component create_component_by_type(
uvm_object_wrapper requested_type,
string name,
uvm_component parent
);
requested_type = find_override_by_type(requested_type);
return requested_type.create_component(name, parent);
endfunction
...
endclass
Еще раз заметим, что реализация метода create_component_by_type() представляет собой:
find_override_by_type();create_component() и возвращение указателя на этот объект.Вооружившись знаниями о механизмах переопределения типов в UVM, разберем реализацию метода find_override_by_type(), который определен также в классе фабрики UVM:
class uvm_factory;
...
virtual function uvm_object_wrapper find_override_by_type(
uvm_object_wrapper requested_type
);
foreach (m_type_overrides[index]) begin
if (m_type_overrides[index].orig_type == requested_type) begin
return find_override_by_type(m_type_overrides[index].ovrd_type);
end
end
return requested_type;
endfunction
...
endclass
Аргумент метода requested_type является указателем на proxy-класс запрашиваемого типа. В методе производится итерирование через очередь переопределений m_type_overrides (в эту очередь могут добавляться записи о переопределениях при вызовах set_type_override_by_type()).
Если в ходе итерирования происходит совпадение запрашиваемого для создания типа и типа, который переопределяется в записи (m_type_overrides[index].orig_type == requested_type), то происходит рекурсивный вызов find_override_by_type(), но уже для типа, на который был переопределен целевой. Если же в ходе итерирования не было найдено переопределений, то возвращается указатель на запрашиваемый тип (return requested_type). Этот возврат является конечной точкой, обрывающей рекурсию.
Итак, возвращаемся к реализации метода create_component_by_type():
class uvm_factory;
...
virtual function uvm_component create_component_by_type(
uvm_object_wrapper requested_type,
string name,
uvm_component parent
);
requested_type = find_override_by_type(requested_type);
return requested_type.create_component(name, parent);
endfunction
...
endclass
После вызова find_override_by_type() возвращается либо указатель на proxy-класс запрашиваемого типа, либо указатель на proxy-класс типа, на который был переопределен запрашиваемый (через set_type_override_by_type()). Далее через полученный указатель на proxy-класс выполняется создание компонента результирующего типа при помощи вызова метода proxy-класса create_component().
Заметим, что указатель на компонент результирующего типа возвращается методом create_component_by_type() в метод create(), а из метода create() возвращается в контекст из которого был инициирован вызов создания через type_id::create().
Дополненная визуализация вызова type_id::set_type_override() для класса apb_driver представлена на изображении ниже.
Закрепим анализ кода примером:
class apb_test extends uvm_test;
`uvm_component_utils(apb_test)
apb_agent ag_master;
apb_agent ag_slave;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
ag_master = apb_agent::type_id::create("ag_master", this);
ag_slave = apb_agent::type_id::create("ag_slave", this);
endfunction
endclass
class extended_apb_apb_test extends apb_test;
`uvm_component_utils(extended_apb_apb_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
apb_driver::type_id::set_type_override(
extended_1_apb_driver::get_type());
extended_1_apb_driver::type_id::set_type_override(
extended_2_apb_driver::get_type());
super.build_phase(phase);
endfunction
endclass
Как думаете, на объект какого типа будет указывать поле drv, а также сколько записей будет содержаться в очереди переопределений m_type_overrides после выполнения кода в функции build_phase() в классе extended_apb_apb_test, если изначально очередь была пуста?
Разобранный в статье исходный код авторской версии библиотеки UVM доступен в директории src/uvm. Каждый исходный файл подробно задокументирован.
Читатель может самостоятельно попрактиковаться в создании компонентов и переопределении их типов. Минимальный задокументированный пример заботливо создан автором и располагается в файле src/test/tb_simple.sv.
Для запуска примера при помощи QuestaSim или Verilator в директории src необходимо выполнить скрипты run_questa.sh и run_verilator.sh соответственно (с аргументом tb_simple):
run_questa.sh tb_simple
run_verilator.sh tb_simple
Лог запуска будет выведен в консоль, а артефакты симуляции сохранены в директорию src/out.
Для добавления новых классов и работы с ними их необходимо объявить в пакете (package test_pkg) в файле src/test/test_pkg.sv. Объекты объявленных классов можно использовать в файле минимального примера src/test/tb_simple.sv.
Вот и подошла к своему логическому завершению вторая статья цикла Demystifying UVM. В ней мы подробно разобрали механизмы создания компонентов UVM и переопределения их типов, развеяли наконец-то туман над исходным кодом фабрики UVM.
О чем же будет следующая статья? Иерархия, фазы, может быть, база ресурсов? Следите за обновлениями в Telegram-канале автора Verification For All (VFA)!
Всего вам наилучшего, дорогие читатели! Мира и процветания!☀️