Demystifying UVM: Фабрика, часть 2
Вступление
Цикл статей Demystifying UVM
Доброго времени суток, дорогие читатели! Данная статья является второй в целом цикле, который называется Demystifying UVM. Цикл будет посвящен глубокому погружению в основные концепции и механизмы работы библиотеки универсальной методологии верификации (Universal Verification Methodology, UVM).
Motivation или зачем это всё
Как часто начинающий верификатор начинает использовать UVM, совершенно не понимая, что вообще происходит. Что такое uvm_component_utils
и type_id::create()
? Почему у конструктора объекта один аргумент, а у конструктора компонента два? Откуда вдруг "взялась" функция get_full_name()
? Как создаются иерерхические имена по типу uvm_test_top.env.ag.mon
? И что это вообще за uvm_test_top
?! Очень много вопросов и очень мало общедоступных ответов.
Автор поставил перед собой задачу рассеять туман над исходным кодом UVM и основными концепциями, используемыми в данной библиотеке.
Необходимые знания
Стоит заметить, что цикл Demystifying UVM не рассчитан на инженера с околонулевыми знаниями.
Для освоения материала читателю необходимо знать:
- принципы ООП в SystemVerilog;
- очереди (
queue
,[$]
); - ключевые слова
local
,protected
,virtual
; - upcasting классов в SystemVerilog;
- downcasting классов в SystemVerilog;
- статические методы классов в SystemVerilog;
- параметризацию классов в SystemVerilog.
Для получения перечисленных выше знаний рекомендуется ознакомиться с лекцией автора в Школе синтеза цифровых схем: ООП в функциональной верификации. Транзакционная модель тестирования.
Подсвечивание
Автор убежден, что прежде чем приступить к изучению нового материала, необходимо повторить ранее изученный, который связан с новым. Автор называет этот подход подсвечиванием. По образу и подобию того, как при грозе в темном небе вспыхивают молнии, в разуме человека при повторении уже изученного материала возникают "вспышки", подсвечивающие ключевые аспекты. Озаренная вспышками часть материала далее успешно служит фундаментом для освоения нового.
Первая часть статьи
Автор настоятельно рекомендует ознакомиться с первой частью статьи: Demystifying UVM: Фабрика, часть 1. В ней была рассмотрена теория о параметризации классов типами в SystemVerilog. Был проанализирован макрос uvm_component_utils
и освоены важнейшие концепции singleton-класса и proxy-класса регистрации типа. Также была проанализирована минимальная авторская реализация класса сервисов UVM.
Маршрут создания компонента 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
Первое касание
Рассмотрим исходный код файла, в котором определен тип 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 в настоящий момент нет поддержки фаз UVM (UVM phases), которые используются в примере. Автор решил не изобретать временных "костылей" в виде пользовательского метода build()
и не делать запускаемого примера.
Spoiler: Разбор фаз UVM будет в следующих статьях цикла Demystifying UVM.
Чтобы понять, почему пример выше функционирует именно таким образом, погрузимся в код 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()
, если изначально очередь была пуста?
Раскройте, чтобы узнать ответ
В очереди будет содержаться одна запись. Первый вызов set_type_override()
создаст и отправит в m_type_overrides
запись о переопределении типа apb_driver
на extended_1_apb_driver
. Второй вызов set_type_override()
поменяет указатель ovrd_type
в уже созданной записи с proxy-класса для типа extended_1_apb_driver
на proxy-класс типа extended_2_apb_driver
.
Визуализация представлена на изображении ниже.
Красным отмечена запись переопределения на тип extended_1_apb_driver
, которая была перезаписана переопределением на тип extended_2_apb_driver
.
Создание компонентов
Вернемся к методу создания компонентов в классе фабрики 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
, если изначально очередь была пуста?
Раскройте, чтобы узнать ответ
Поле drv
будет указывать на объект типа extended_2_apb_driver
.
Первый вызов set_type_override()
создаст и отправит в m_type_overrides
запись о переопределении типа apb_driver
на extended_1_apb_driver
. Второй вызов set_type_override()
создаст и отправит в m_type_overrides
запись о переопределении типа extended_1_apb_driver
на extended_2_apb_driver
. Таким образом, в очереди переопределений m_type_overrides
будет содержаться две записи.
Вызов type_id::create()
приведет к вызову метода create_component_by_type()
, который в свою очередь приведет к вызову find_override_by_type()
, результатом которого будет указатель на proxy-класс для типа extended_2_apb_driver
. Почему это так? Потому что сначала в m_types_override
будет найдено переопределение типа apb_driver
на тип extended_1_apb_driver
, далее будет рекурсивно выполнен поиск для типа extended_1_apb_driver
и будет найдено переопределение этого типа на extended_2_apb_driver
.
В завершение будет вызыван метод create_component()
у proxy-класса для типа extended_2_apb_driver
, который вернет указатель на объект этого типа.
Визуализация примера представлена на изображении ниже.
Playground
Разобранный в статье исходный код авторской версии библиотеки 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
.
To be continued...
Вот и подошла к своему логическому завершению вторая статья цикла Demystifying UVM. В ней мы подробно разобрали механизмы создания компонентов UVM и переопределения их типов, развеяли наконец-то туман над исходным кодом фабрики UVM.
О чем же будет следующая статья? Иерархия, фазы, может быть, база ресурсов? Следите за обновлениями в Telegram-канале автора Verification For All (VFA)!
Всего вам наилучшего, дорогие читатели! Мира и процветания!☀️
Upcasting и downcasting в SystemVerilog был подробно разобран автором в лекции в Школе синтеза цифровых схем: ООП в функциональной верификации. Транзакционная модель тестирования (ссылка содержит таймкод на начало теории об upcasting).